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

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