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 17KB

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