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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. # @package lodel.editorial_model.components
  2. #@brief Defines all @ref lodel2_em "EM" components
  3. #@ingroup lodel2_em
  4. import itertools
  5. import warnings
  6. import copy
  7. import hashlib
  8. from lodel.utils.mlstring import MlString
  9. from lodel.mlnamedobject.mlnamedobject import MlNamedObject
  10. from lodel.settings import import Settings
  11. from lodel.editorial_model.exceptions import EditorialModelError, assert_edit
  12. from lodel.leapi.leobject import CLASS_ID_FIELDNAME
  13. ## @brief Abstract class to represent editorial model components
  14. # @see EmClass EmField
  15. # @todo forbid '.' in uid
  16. #@ingroup lodel2_em
  17. class EmComponent(MlNamedObject):
  18. ## @brief Instanciate an EmComponent
  19. # @param uid str : uniq identifier
  20. # @param display_name MlString|str|dict : component display_name
  21. # @param help_text MlString|str|dict : help_text
  22. def __init__(self, uid, display_name=None, help_text=None, group=None):
  23. if self.__class__ == EmComponent:
  24. raise NotImplementedError('EmComponent is an abstract class')
  25. self.uid = uid
  26. self.group = group
  27. super().__init__(display_name, help_text)
  28. ## @brief Returns the display_name of the component if it is not None, its uid else
  29. def __str__(self):
  30. if self.display_name is None:
  31. return str(self.uid)
  32. return str(self.display_name)
  33. ## @brief Returns a hash code for the component
  34. def d_hash(self):
  35. m = hashlib.md5()
  36. for data in (
  37. self.uid,
  38. 'NODISPNAME' if self.display_name is None else str(self.display_name.d_hash()),
  39. 'NOHELP' if self.help_text is None else str(self.help_text.d_hash()),
  40. 'NOGROUP' if self.group is None else str(self.group.d_hash()),
  41. ):
  42. m.update(bytes(data, 'utf-8'))
  43. return int.from_bytes(m.digest(), byteorder='big')
  44. ## @brief Handles editorial model objects classes
  45. #@ingroup lodel2_em
  46. class EmClass(EmComponent):
  47. ## @brief Instanciates a new EmClass
  48. #@param uid str : uniq identifier
  49. #@param display_name MlString|str|dict : component display_name
  50. #@param abstract bool : set the class as asbtract if True
  51. #@param pure_abstract bool : if True the EmClass will not be represented in
  52. # leapi dyncode
  53. #@param parents list: parent EmClass list or uid list
  54. #@param help_text MlString|str|dict : help_text
  55. #@param datasources str|tuple|list : The datasource name ( see
  56. #@ref lodel2_datasources ) or two names (first is read_only datasource the
  57. # second is read write)
  58. def __init__(
  59. self, uid, display_name=None, help_text=None, abstract=False,
  60. parents=None, group=None, pure_abstract=False,
  61. datasources='default'):
  62. super().__init__(uid, display_name, help_text, group)
  63. self.abstract = bool(abstract)
  64. self.pure_abstract = bool(pure_abstract)
  65. self.__datasource = datasources
  66. if not isinstance(datasources, str) and len(datasources) != 2:
  67. raise ValueError("datasources argument can be a single datasource\
  68. name or two names in a tuple or a list")
  69. if self.pure_abstract:
  70. self.abtract = True
  71. if parents is not None:
  72. if not isinstance(parents, list):
  73. parents = [parents]
  74. for parent in parents:
  75. if not isinstance(parent, EmClass):
  76. raise ValueError(
  77. "<class EmClass> expected in parents list, but %s found" % type(parent))
  78. else:
  79. parents = list()
  80. self.parents = parents
  81. ## @brief Stores EmFields instances indexed by field uid
  82. self.__fields = dict()
  83. self.group = group
  84. if group is None:
  85. warnings.warn("NO GROUP FOR EMCLASS %s" % uid)
  86. else:
  87. group.add_components([self])
  88. # Adding common field
  89. if not self.abstract:
  90. self.new_field(
  91. CLASS_ID_FIELDNAME,
  92. display_name={
  93. 'eng': "LeObject subclass identifier",
  94. 'fre': "Identifiant de la class fille de LeObject"},
  95. help_text={
  96. 'eng': "Allow to create instance of the good class when\
  97. fetching arbitrary datas from DB"},
  98. data_handler='LeobjectSubclassIdentifier',
  99. internal=True,
  100. group=group)
  101. ## @brief Property that represents a dict of all fields
  102. # (the EmField objects defined in this class and all their parents)
  103. # @todo use Settings.editorialmodel.groups to determine which 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 Returns the list of all dependencies
  116. #
  117. # Recursive 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 Keeps 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 Adds a field to the EmClass
  147. # @param emfield EmField : an EmField instance
  148. # @warning do not add an EmField already in another class !
  149. # @throw EditorialModelException if an EmField with same uid already in this EmClass (overwriting 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 overrides a parent field, but data_handlers are not compatible" % emfield.uid)
  162. self.__fields[emfield.uid] = emfield
  163. return emfield
  164. ## @brief Creates a new EmField and adds 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 Instanciates 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 Returns 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 Creates 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_dependency(grp)
  261. ## @brief Returns EmGroup dependencies
  262. # @param recursive bool : if True returns all dependencies and their own 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 returns 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 returns 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 returns 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 Adds 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 dependency
  326. # @param em_group EmGroup|iterable : an EmGroup instance or list of instances
  327. def add_dependency(self, grp):
  328. assert_edit()
  329. try:
  330. for group in grp:
  331. self.add_dependency(group)
  332. return
  333. except TypeError:
  334. pass
  335. if grp.uid in self.require:
  336. return
  337. if self.__circular_dependency(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 dependency
  359. # @return True if circular dep found else False
  360. def __circular_dependency(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. ## @brief Computes a d-hash code for the EmGroup
  374. # @return a string
  375. def d_hash(self):
  376. payload = "%s%s%s" % (
  377. self.uid,
  378. 'NODNAME' if self.display_name is None else self.display_name.d_hash(),
  379. 'NOHELP' if self.help_text is None else self.help_text.d_hash()
  380. )
  381. for recurs in (False, True):
  382. deps = self.dependencies(recurs)
  383. for dep_uid in sorted(deps.keys()):
  384. payload += str(deps[dep_uid].d_hash())
  385. for req_by_uid in self.required_by:
  386. payload += req_by_uid
  387. return int.from_bytes(
  388. bytes(payload, 'utf-8'),
  389. byteorder='big'
  390. )
  391. ## @brief Complete string representation of an EmGroup
  392. # @return a string
  393. def __repr__(self):
  394. return "<class EmGroup '%s' depends : [%s]>" % (self.uid, ', '.join([duid for duid in self.dependencies(False)]))