#
# This file is part of Lodel 2 (https://github.com/OpenEdition)
#
# Copyright (C) 2015-2017 Cléo UMS-3287
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
# @package lodel.editorial_model.components
#@brief Defines all @ref lodel2_em "EM" components
#@ingroup lodel2_em
import itertools
import warnings
import copy
import hashlib
from lodel.context import LodelContext
LodelContext.expose_modules(globals(), {
'lodel.utils.mlstring': ['MlString'],
'lodel.mlnamedobject.mlnamedobject': ['MlNamedObject'],
'lodel.settings': ['Settings'],
'lodel.editorial_model.exceptions': ['EditorialModelError', 'assert_edit'],
'lodel.leapi.leobject': ['CLASS_ID_FIELDNAME']})
## @brief Abstract class to represent editorial model components
# @see EmClass EmField
# @todo forbid '.' in uid
#@ingroup lodel2_em
class EmComponent(MlNamedObject):
## @brief Instanciate an EmComponent
# @param uid str : uniq identifier
# @param display_name MlString|str|dict : component display_name
# @param help_text MlString|str|dict : help_text
def __init__(self, uid, display_name=None, help_text=None, group=None):
if self.__class__ == EmComponent:
raise NotImplementedError('EmComponent is an abstract class')
self.uid = uid
self.group = group
super().__init__(display_name, help_text)
## @brief Returns the display_name of the component if it is not None, its uid else
def __str__(self):
if self.display_name is None:
return str(self.uid)
return str(self.display_name)
## @brief Returns a hash code for the component
def d_hash(self):
m = hashlib.md5()
for data in (
self.uid,
'NODISPNAME' if self.display_name is None else str(self.display_name.d_hash()),
'NOHELP' if self.help_text is None else str(self.help_text.d_hash()),
'NOGROUP' if self.group is None else str(self.group.d_hash()),
):
m.update(bytes(data, 'utf-8'))
return int.from_bytes(m.digest(), byteorder='big')
## @brief Handles editorial model objects classes
#@ingroup lodel2_em
class EmClass(EmComponent):
## @brief Instanciates a new EmClass
#@param uid str : uniq identifier
#@param display_name MlString|str|dict : component display_name
#@param abstract bool : set the class as asbtract if True
#@param pure_abstract bool : if True the EmClass will not be represented in
# leapi dyncode
#@param parents list: parent EmClass list or uid list
#@param help_text MlString|str|dict : help_text
#@param datasources str|tuple|list : The datasource name ( see
#@ref lodel2_datasources ) or two names (first is read_only datasource the
# second is read write)
def __init__(
self, uid, display_name=None, help_text=None, abstract=False,
parents=None, group=None, pure_abstract=False,
datasources='default'):
super().__init__(uid, display_name, help_text, group)
self.abstract = bool(abstract)
self.pure_abstract = bool(pure_abstract)
self.__datasource = datasources
if not isinstance(datasources, str) and len(datasources) != 2:
raise ValueError("datasources argument can be a single datasource\
name or two names in a tuple or a list")
if self.pure_abstract:
self.abtract = True
if parents is not None:
if not isinstance(parents, list):
parents = [parents]
for parent in parents:
if not isinstance(parent, EmClass):
raise ValueError(
" expected in parents list, but %s found" % type(parent))
else:
parents = list()
self.parents = parents
## @brief Stores EmFields instances indexed by field uid
self.__fields = dict()
self.group = group
if group is None:
warnings.warn("NO GROUP FOR EMCLASS %s" % uid)
else:
group.add_components([self])
# Adding common field
if not self.abstract:
self.new_field(
CLASS_ID_FIELDNAME,
display_name={
'eng': "LeObject subclass identifier",
'fre': "Identifiant de la class fille de LeObject"},
help_text={
'eng': "Allow to create instance of the good class when\
fetching arbitrary datas from DB"},
data_handler='LeobjectSubclassIdentifier',
internal=True,
group=group)
## @brief Property that represents a dict of all fields
# (the EmField objects defined in this class and all their parents)
# @todo use Settings.editorialmodel.groups to determine which fields should be returned
@property
def __all_fields(self):
res = dict()
for pfields in [p.__all_fields for p in self.parents]:
res.update(pfields)
res.update(self.__fields)
return res
## @brief RO access to datasource attribute
@property
def datasource(self):
return self.__datasource
## @brief Returns the list of all dependencies
#
# Recursive parents listing
@property
def parents_recc(self):
if len(self.parents) == 0:
return set()
res = set(self.parents)
for parent in self.parents:
res |= parent.parents_recc
return res
## @brief EmField getter
# @param uid None | str : If None returns an iterator on EmField instances else return an EmField instance
# @param no_parents bool : If True returns only fields defined is this class and not the one defined in parents classes
# @return A list on EmFields instances (if uid is None) else return an EmField instance
# @todo use Settings.editorialmodel.groups to determine wich fields should be returned
def fields(self, uid=None, no_parents=False):
fields = self.__fields if no_parents else self.__all_fields
try:
return list(fields.values()) if uid is None else fields[uid]
except KeyError:
raise EditorialModelError("No such EmField '%s'" % uid)
## @brief Keeps in __fields only fields contained in active groups
def _set_active_fields(self, active_groups):
if not Settings.editorialmodel.editormode:
active_fields = []
for grp_name, agrp in active_groups.items():
active_fields += [emc for emc in agrp.components()
if isinstance(emc, EmField)]
self.__fields = {fname: fdh for fname, fdh in self.__fields.items()
if fdh in active_fields}
## @brief Adds a field to the EmClass
# @param emfield EmField : an EmField instance
# @warning do not add an EmField already in another class !
# @throw EditorialModelException if an EmField with same uid already in this EmClass (overwriting allowed from parents)
# @todo End the override checks (needs methods in data_handlers)
def add_field(self, emfield):
assert_edit()
if emfield.uid in self.__fields:
raise EditorialModelError(
"Duplicated uid '%s' for EmField in this class ( %s )" % (emfield.uid, self))
# Incomplete field override check
if emfield.uid in self.__all_fields:
parent_field = self.__all_fields[emfield.uid]
if not emfield.data_handler_instance.can_override(parent_field.data_handler_instance):
raise AttributeError(
"'%s' field overrides a parent field, but data_handlers are not compatible" % emfield.uid)
self.__fields[emfield.uid] = emfield
return emfield
## @brief Creates a new EmField and adds it to the EmClass
# @param data_handler str : A DataHandler name
# @param uid str : the EmField uniq id
# @param **field_kwargs : EmField constructor parameters ( see @ref EmField.__init__() )
def new_field(self, uid, data_handler, **field_kwargs):
assert_edit()
return self.add_field(EmField(uid, data_handler, self, **field_kwargs))
def d_hash(self):
m = hashlib.md5()
payload = str(super().d_hash()) + ("1" if self.abstract else "0")
for p in sorted(self.parents):
payload += str(p.d_hash())
for fuid in sorted(self.__fields.keys()):
payload += str(self.__fields[fuid].d_hash())
m.update(bytes(payload, 'utf-8'))
return int.from_bytes(m.digest(), byteorder='big')
def __str__(self):
return "" % self.uid
def __repr__(self):
if not self.abstract:
abstract = ''
elif self.pure_abstract:
abstract = 'PureAbstract'
else:
abstract = 'Abstract'
return "" % (abstract, repr(self.uid))
## @brief Handles editorial model classes fields
#@ingroup lodel2_em
class EmField(EmComponent):
## @brief Instanciates a new EmField
# @param uid str : uniq identifier
# @param display_name MlString|str|dict : field display_name
# @param data_handler str : A DataHandler name
# @param help_text MlString|str|dict : help text
# @param group EmGroup :
# @param **handler_kwargs : data handler arguments
def __init__(self, uid, data_handler, em_class=None, display_name=None, help_text=None, group=None, **handler_kwargs):
from lodel.leapi.datahandlers.base_classes import DataHandler
super().__init__(uid, display_name, help_text, group)
## @brief The data handler name
self.data_handler_name = data_handler
## @brief The data handler class
self.data_handler_cls = DataHandler.from_name(data_handler)
## @brief The data handler instance associated with this EmField
self.data_handler_instance = self.data_handler_cls(**handler_kwargs)
## @brief Stores data handler instanciation options
self.data_handler_options = handler_kwargs
## @brief Stores the emclass that contains this field (set by EmClass.add_field() method)
self._emclass = em_class
if self._emclass is None:
warnings.warn("No EmClass for field %s" % uid)
if group is None:
warnings.warn("No EmGroup for field %s" % uid)
else:
group.add_components([self])
## @brief Returns data_handler_name attribute
def get_data_handler_name(self):
return copy.copy(self.data_handler_name)
## @brief Returns data_handler_cls attribute
def get_data_handler_cls(self):
return copy.copy(self.data_handler_cls)
##@brief Returns the uid of the emclass which contains this field
def get_emclass_uid(self):
return self._emclass.uid
# @warning Not complete !
# @todo Complete the hash when data handlers becomes available
def d_hash(self):
return int.from_bytes(hashlib.md5(
bytes(
"%s%s%s" % (super().d_hash(),
self.data_handler_name,
self.data_handler_options),
'utf-8')
).digest(), byteorder='big')
## @brief Handles functionnal group of EmComponents
#@ingroup lodel2_em
class EmGroup(MlNamedObject):
## @brief Creates a new EmGroup
# @note you should NEVER call the constructor yourself. Use Model.add_group instead
# @param uid str : Uniq identifier
# @param depends list : A list of EmGroup dependencies
# @param display_name MlString|str :
# @param help_text MlString|str :
def __init__(self, uid, depends=None, display_name=None, help_text=None):
self.uid = uid
## @brief Stores the list of groups that depends on this EmGroup indexed by uid
self.required_by = dict()
## @brief Stores the list of dependencies (EmGroup) indexed by uid
self.require = dict()
## @brief Stores the list of EmComponent instances contained in this group
self.__components = set()
super().__init__(display_name, help_text)
if depends is not None:
for grp in depends:
if not isinstance(grp, EmGroup):
raise ValueError("EmGroup expected in depends argument but %s found" % grp)
self.add_dependency(grp)
## @brief Returns EmGroup dependencies
# @param recursive bool : if True returns all dependencies and their own dependencies
# @return a dict of EmGroup identified by uid
def dependencies(self, recursive=False):
res = copy.copy(self.require)
if not recursive:
return res
to_scan = list(res.values())
while len(to_scan) > 0:
cur_dep = to_scan.pop()
for new_dep in cur_dep.require.values():
if new_dep not in res:
to_scan.append(new_dep)
res[new_dep.uid] = new_dep
return res
## @brief Returns EmGroup applicants
# @param recursive bool : if True returns all dependencies and their dependencies
# @returns a dict of EmGroup identified by uid
def applicants(self, recursive=False):
res = copy.copy(self.required_by)
if not recursive:
return res
to_scan = list(res.values())
while len(to_scan) > 0:
cur_app = to_scan.pop()
for new_app in cur_app.required_by.values():
if new_app not in res:
to_scan.append(new_app)
res[new_app.uid] = new_app
return res
## @brief Returns EmGroup components
# @returns a copy of the set of components
def components(self):
return (self.__components).copy()
## @brief Returns EmGroup display_name
# @param lang str | None : If None returns default lang translation
# @returns None if display_name is None, a str for display_name else
def get_display_name(self, lang=None):
name = self.display_name
if name is None:
return None
return name.get(lang)
## @brief Returns EmGroup help_text
# @param lang str | None : If None returns default lang translation
# @returns None if display_name is None, a str for display_name else
def get_help_text(self, lang=None):
help = self.help_text
if help is None:
return None
return help.get(lang)
## @brief Adds components in a group
# @param components list : EmComponent instances list
def add_components(self, components):
assert_edit()
for component in components:
if isinstance(component, EmField):
if component._emclass is None:
msg = "Adding an orphan EmField '%s' to EmGroup '%s'"
msg %= (component, self)
warnings.warn(msg)
elif not isinstance(component, EmClass):
raise EditorialModelError(
"Expecting components to be a list of EmComponent, but %s found in the list" % type(component))
self.__components |= set(components)
## @brief Add a dependency
# @param em_group EmGroup|iterable : an EmGroup instance or list of instances
def add_dependency(self, grp):
assert_edit()
try:
for group in grp:
self.add_dependency(group)
return
except TypeError:
pass
if grp.uid in self.require:
return
if self.__circular_dependency(grp):
raise EditorialModelError("Circular dependencie detected, cannot add dependencie")
self.require[grp.uid] = grp
grp.required_by[self.uid] = self
## @brief Add a applicant
# @param em_group EmGroup|iterable : an EmGroup instance or list of instance
# Useless ???
def add_applicant(self, grp):
assert_edit()
try:
for group in grp:
self.add_applicant(group)
return
except TypeError:
pass
if grp.uid in self.required_by:
return
if self.__circular_applicant(grp):
raise EditorialModelError("Circular applicant detected, cannot add applicant")
self.required_by[grp.uid] = grp
grp.require[self.uid] = self
## @brief Search for circular dependency
# @return True if circular dep found else False
def __circular_dependency(self, new_dep):
return self.uid in new_dep.dependencies(True)
## @brief Search for circular applicant
# @return True if circular app found else False
def __circular_applicant(self, new_app):
return self.uid in new_app.applicants(True)
## @brief Fancy string representation of an EmGroup
# @return a string
def __str__(self):
if self.display_name is None:
return self.uid
else:
return self.display_name.get()
## @brief Computes a d-hash code for the EmGroup
# @return a string
def d_hash(self):
payload = "%s%s%s" % (
self.uid,
'NODNAME' if self.display_name is None else self.display_name.d_hash(),
'NOHELP' if self.help_text is None else self.help_text.d_hash()
)
for recurs in (False, True):
deps = self.dependencies(recurs)
for dep_uid in sorted(deps.keys()):
payload += str(deps[dep_uid].d_hash())
for req_by_uid in self.required_by:
payload += req_by_uid
return int.from_bytes(
bytes(payload, 'utf-8'),
byteorder='big'
)
## @brief Complete string representation of an EmGroup
# @return a string
def __repr__(self):
return "" % (self.uid, ', '.join([duid for duid in self.dependencies(False)]))