No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

components.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. #-*- coding: utf-8 -*-
  2. import itertools
  3. import warnings
  4. import copy
  5. import hashlib
  6. from lodel.utils.mlstring import MlString
  7. from lodel.editorial_model.exceptions import *
  8. ##@brief Abstract class to represent editorial model components
  9. # @see EmClass EmField
  10. # @todo forbid '.' in uid
  11. class EmComponent(object):
  12. ##@brief Instanciate an EmComponent
  13. # @param uid str : uniq identifier
  14. # @param display_name MlString|str|dict : component display_name
  15. # @param help_text MlString|str|dict : help_text
  16. def __init__(self, uid, display_name = None, help_text = None, group = None):
  17. if self.__class__ == EmComponent:
  18. raise NotImplementedError('EmComponent is an abstract class')
  19. self.uid = uid
  20. self.display_name = None if display_name is None else MlString(display_name)
  21. self.help_text = None if help_text is None else MlString(help_text)
  22. self.group = group
  23. def __str__(self):
  24. if self.display_name is None:
  25. return str(self.uid)
  26. return str(self.display_name)
  27. def d_hash(self):
  28. m = hashlib.md5()
  29. for data in (
  30. self.uid,
  31. 'NODISPNAME' if self.display_name is None else str(self.display_name.d_hash()),
  32. 'NOHELP' if self.help_text is None else str(self.help_text.d_hash()),
  33. 'NOGROUP' if self.group is None else str(self.group.d_hash()),
  34. ):
  35. m.update(bytes(data, 'utf-8'))
  36. return int.from_bytes(m.digest(), byteorder='big')
  37. ##@brief Handles editorial model objects classes
  38. class EmClass(EmComponent):
  39. ##@brief Instanciate a new EmClass
  40. # @param uid str : uniq identifier
  41. # @param display_name MlString|str|dict : component display_name
  42. # @param abstract bool : set the class as asbtract if True
  43. # @param pure_abstract bool : if True the EmClass will not be represented in leapi dyncode
  44. # @param parents list: parent EmClass list or uid list
  45. # @param help_text MlString|str|dict : help_text
  46. # @param datasource str : The datasource name ( see @ref lodel2_datasources )
  47. def __init__(self, uid, display_name = None, help_text = None, abstract = False, parents = None, group = None, pure_abstract = False, datasource = 'default'):
  48. super().__init__(uid, display_name, help_text, group)
  49. self.abstract = bool(abstract)
  50. self.pure_abstract = bool(pure_abstract)
  51. self.__datasource = datasource
  52. if self.pure_abstract:
  53. self.abtract = True
  54. if parents is not None:
  55. if not isinstance(parents, list):
  56. parents = [parents]
  57. for parent in parents:
  58. if not isinstance(parent, EmClass):
  59. raise ValueError("<class EmClass> expected in parents list, but %s found" % type(parent))
  60. else:
  61. parents = list()
  62. self.parents = parents
  63. ##@brief Stores EmFields instances indexed by field uid
  64. self.__fields = dict()
  65. ##@brief Property that represent a dict of all fields (the EmField defined in this class and all its parents)
  66. # @todo use Settings.editorialmodel.groups to determine wich fields should be returned
  67. @property
  68. def __all_fields(self):
  69. res = dict()
  70. for pfields in [ p.__all_fields for p in self.parents]:
  71. res.update(pfields)
  72. res.update(self.__fields)
  73. return res
  74. ##@brief RO access to datasource attribute
  75. @property
  76. def datasource(self):
  77. return self.__datasource
  78. ##@brief Return the list of all dependencies
  79. #
  80. # Reccursive parents listing
  81. @property
  82. def parents_recc(self):
  83. if len(self.parents) == 0:
  84. return set()
  85. res = set(self.parents)
  86. for parent in self.parents:
  87. res |= parent.parents_recc
  88. return res
  89. ##@brief EmField getter
  90. # @param uid None | str : If None returns an iterator on EmField instances else return an EmField instance
  91. # @param no_parents bool : If True returns only fields defined is this class and not the one defined in parents classes
  92. # @return A list on EmFields instances (if uid is None) else return an EmField instance
  93. # @todo use Settings.editorialmodel.groups to determine wich fields should be returned
  94. def fields(self, uid = None, no_parents = False):
  95. fields = self.__fields if no_parents else self.__all_fields
  96. try:
  97. return list(fields.values()) if uid is None else fields[uid]
  98. except KeyError:
  99. raise EditorialModelError("No such EmField '%s'" % uid)
  100. ##@brief Add a field to the EmClass
  101. # @param emfield EmField : an EmField instance
  102. # @warning do not add an EmField allready in another class !
  103. # @throw EditorialModelException if an EmField with same uid allready in this EmClass (overwritting allowed from parents)
  104. # @todo End the override checks (needs methods in data_handlers)
  105. def add_field(self, emfield):
  106. if emfield.uid in self.__fields:
  107. raise EditorialModelError("Duplicated uid '%s' for EmField in this class ( %s )" % (emfield.uid, self))
  108. # Incomplete field override check
  109. if emfield.uid in self.__all_fields:
  110. parent_field = self.__all_fields[emfield.uid]
  111. if not emfield.data_handler_instance.can_override(parent_field.data_handler_instance):
  112. raise AttributeError("'%s' field override a parent field, but data_handles are not compatible" % emfield.uid)
  113. self.__fields[emfield.uid] = emfield
  114. emfield._emclass = self
  115. return emfield
  116. ##@brief Create a new EmField and add it to the EmClass
  117. # @param data_handler str : A DataHandler name
  118. # @param uid str : the EmField uniq id
  119. # @param **field_kwargs : EmField constructor parameters ( see @ref EmField.__init__() )
  120. def new_field(self, uid, data_handler, **field_kwargs):
  121. return self.add_field(EmField(uid, data_handler, **field_kwargs))
  122. def d_hash(self):
  123. m = hashlib.md5()
  124. payload = str(super().d_hash()) + ("1" if self.abstract else "0")
  125. for p in sorted(self.parents):
  126. payload += str(p.d_hash())
  127. for fuid in sorted(self.__fields.keys()):
  128. payload += str(self.__fields[fuid].d_hash())
  129. m.update(bytes(payload, 'utf-8'))
  130. return int.from_bytes(m.digest(), byteorder='big')
  131. def __str__(self):
  132. return "<class EmClass %s>" % self.uid
  133. def __repr__(self):
  134. if not self.abstract:
  135. abstract = ''
  136. elif self.pure_abstract:
  137. abstract = 'PureAbstract'
  138. else:
  139. abstract = 'Abstract'
  140. return "<class %s EmClass uid=%s>" % (abstract, repr(self.uid) )
  141. ##@brief Handles editorial model classes fields
  142. class EmField(EmComponent):
  143. ##@brief Instanciate a new EmField
  144. # @param uid str : uniq identifier
  145. # @param display_name MlString|str|dict : field display_name
  146. # @param data_handler str : A DataHandler name
  147. # @param help_text MlString|str|dict : help text
  148. # @param group EmGroup :
  149. # @param **handler_kwargs : data handler arguments
  150. def __init__(self, uid, data_handler, display_name = None, help_text = None, group = None, **handler_kwargs):
  151. from lodel.leapi.datahandlers.base_classes import DataHandler
  152. super().__init__(uid, display_name, help_text, group)
  153. ##@brief The data handler name
  154. self.data_handler_name = data_handler
  155. ##@brief The data handler class
  156. self.data_handler_cls = DataHandler.from_name(data_handler)
  157. ##@brief The data handler instance associated with this EmField
  158. self.data_handler_instance = self.data_handler_cls(**handler_kwargs)
  159. ##@brief Stores data handler instanciation options
  160. self.data_handler_options = handler_kwargs
  161. ##@brief Stores the emclass that contains this field (set by EmClass.add_field() method)
  162. self._emclass = None
  163. ##@brief Returns data_handler_name attribute
  164. def get_data_handler_name(self):
  165. return copy.copy(self.data_handler_name)
  166. ##@brief Returns data_handler_cls attribute
  167. def get_data_handler_cls(self):
  168. return copy.copy(selfdata_handler_cls)
  169. ##@brief Returne the uid of the emclass which contains this field
  170. def get_emclass_uid(self):
  171. return self._emclass.uid
  172. # @warning Not complete !
  173. # @todo Complete the hash when data handlers becomes available
  174. def d_hash(self):
  175. return int.from_bytes(hashlib.md5(
  176. bytes(
  177. "%s%s%s" % ( super().d_hash(),
  178. self.data_handler_name,
  179. self.data_handler_options),
  180. 'utf-8')
  181. ).digest(), byteorder='big')
  182. ##@brief Handles functionnal group of EmComponents
  183. class EmGroup(object):
  184. ##@brief Create a new EmGroup
  185. # @note you should NEVER call the constructor yourself. Use Model.add_group instead
  186. # @param uid str : Uniq identifier
  187. # @param depends list : A list of EmGroup dependencies
  188. # @param display_name MlString|str :
  189. # @param help_text MlString|str :
  190. def __init__(self, uid, depends = None, display_name = None, help_text = None):
  191. self.uid = uid
  192. ##@brief Stores the list of groups that depends on this EmGroup indexed by uid
  193. self.required_by = dict()
  194. ##@brief Stores the list of dependencies (EmGroup) indexed by uid
  195. self.require = dict()
  196. ##@brief Stores the list of EmComponent instances contained in this group
  197. self.__components = set()
  198. self.display_name = None if display_name is None else MlString(display_name)
  199. self.help_text = None if help_text is None else MlString(help_text)
  200. if depends is not None:
  201. for grp in depends:
  202. if not isinstance(grp, EmGroup):
  203. raise ValueError("EmGroup expected in depends argument but %s found" % grp)
  204. self.add_dependencie(grp)
  205. ##@brief Returns EmGroup dependencie
  206. # @param recursive bool : if True return all dependencies and their dependencies
  207. # @return a dict of EmGroup identified by uid
  208. def dependencies(self, recursive = False):
  209. res = copy.copy(self.require)
  210. if not recursive:
  211. return res
  212. to_scan = list(res.values())
  213. while len(to_scan) > 0:
  214. cur_dep = to_scan.pop()
  215. for new_dep in cur_dep.require.values():
  216. if new_dep not in res:
  217. to_scan.append(new_dep)
  218. res[new_dep.uid] = new_dep
  219. return res
  220. ##@brief Returns EmGroup applicants
  221. # @param recursive bool : if True return all dependencies and their dependencies
  222. # @returns a dict of EmGroup identified by uid
  223. def applicants(self, recursive = False):
  224. res = copy.copy(self.required_by)
  225. if not recursive:
  226. return res
  227. to_scan = list(res.values())
  228. while len(to_scan) > 0:
  229. cur_app = to_scan.pop()
  230. for new_app in cur_app.required_by.values():
  231. if new_app not in res:
  232. to_scan.append(new_app)
  233. res[new_app.uid] = new_app
  234. return res
  235. ##@brief Returns EmGroup components
  236. # @returns a copy of the set of components
  237. def components(self):
  238. return (self.__components).copy()
  239. ##@brief Returns EmGroup display_name
  240. # @param lang str | None : If None return default lang translation
  241. # @returns None if display_name is None, a str for display_name else
  242. def get_display_name(self, lang=None):
  243. name=self.display_name
  244. if name is None : return None
  245. return name.get(lang);
  246. ##@brief Returns EmGroup help_text
  247. # @param lang str | None : If None return default lang translation
  248. # @returns None if display_name is None, a str for display_name else
  249. def get_help_text(self, lang=None):
  250. help=self.help_text
  251. if help is None : return None
  252. return help.get(lang);
  253. ##@brief Add components in a group
  254. # @param components list : EmComponent instances list
  255. def add_components(self, components):
  256. for component in components:
  257. if isinstance(component, EmField):
  258. if component._emclass is None:
  259. warnings.warn("Adding an orphan EmField to an EmGroup")
  260. elif not isinstance(component, EmClass):
  261. raise EditorialModelError("Expecting components to be a list of EmComponent, but %s found in the list" % type(component))
  262. self.__components |= set(components)
  263. ##@brief Add a dependencie
  264. # @param em_group EmGroup|iterable : an EmGroup instance or list of instance
  265. def add_dependencie(self, grp):
  266. try:
  267. for group in grp:
  268. self.add_dependencie(group)
  269. return
  270. except TypeError: pass
  271. if grp.uid in self.require:
  272. return
  273. if self.__circular_dependencie(grp):
  274. raise EditorialModelError("Circular dependencie detected, cannot add dependencie")
  275. self.require[grp.uid] = grp
  276. grp.required_by[self.uid] = self
  277. ##@brief Add a applicant
  278. # @param em_group EmGroup|iterable : an EmGroup instance or list of instance
  279. # Useless ???
  280. def add_applicant(self, grp):
  281. try:
  282. for group in grp:
  283. self.add_applicant(group)
  284. return
  285. except TypeError: pass
  286. if grp.uid in self.required_by:
  287. return
  288. if self.__circular_applicant(grp):
  289. raise EditorialModelError("Circular applicant detected, cannot add applicant")
  290. self.required_by[grp.uid] = grp
  291. grp.require[self.uid] = self
  292. ##@brief Search for circular dependencie
  293. # @return True if circular dep found else False
  294. def __circular_dependencie(self, new_dep):
  295. return self.uid in new_dep.dependencies(True)
  296. ##@brief Search for circular applicant
  297. # @return True if circular app found else False
  298. def __circular_applicant(self, new_app):
  299. return self.uid in new_app.applicants(True)
  300. ##@brief Fancy string representation of an EmGroup
  301. # @return a string
  302. def __str__(self):
  303. if self.display_name is None:
  304. return self.uid
  305. else:
  306. return self.display_name.get()
  307. def d_hash(self):
  308. payload = "%s%s%s" % (
  309. self.uid,
  310. 'NODNAME' if self.display_name is None else self.display_name.d_hash(),
  311. 'NOHELP' if self.help_text is None else self.help_text.d_hash()
  312. )
  313. for recurs in (False, True):
  314. deps = self.dependencies(recurs)
  315. for dep_uid in sorted(deps.keys()):
  316. payload += str(deps[dep_uid].d_hash())
  317. for req_by_uid in self.required_by:
  318. payload += req_by_uid
  319. return int.from_bytes(
  320. bytes(payload, 'utf-8'),
  321. byteorder = 'big'
  322. )
  323. ##@brief Complete string representation of an EmGroup
  324. # @return a string
  325. def __repr__(self):
  326. return "<class EmGroup '%s' depends : [%s]>" % (self.uid, ', '.join([duid for duid in self.dependencies(False)]) )