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.

model.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #-*- coding: utf-8 -*-
  2. ## @file editorialmodel.py
  3. # Manage instance of an editorial model
  4. import random
  5. import time
  6. import EditorialModel
  7. from EditorialModel.migrationhandler.dummy import DummyMigrationHandler
  8. from EditorialModel.backend.dummy_backend import EmBackendDummy
  9. from EditorialModel.classes import EmClass
  10. from EditorialModel.fieldgroups import EmFieldGroup
  11. from EditorialModel.fields import EmField
  12. from EditorialModel.types import EmType
  13. from EditorialModel.classtypes import EmClassType
  14. from Lodel.utils.mlstring import MlString
  15. from EditorialModel.exceptions import EmComponentCheckError, EmComponentNotExistError, MigrationHandlerChangeError
  16. import hashlib
  17. ## Manages the Editorial Model
  18. class Model(object):
  19. components_class = [EmClass, EmType, EmFieldGroup, EmField]
  20. ## Constructor
  21. #
  22. # @param backend unknown: A backend object instanciated from one of the classes in the backend module
  23. def __init__(self, backend, migration_handler=None):
  24. if migration_handler is None:
  25. self.migration_handler = DummyMigrationHandler()
  26. elif issubclass(migration_handler.__class__, DummyMigrationHandler):
  27. self.migration_handler = migration_handler
  28. else:
  29. raise TypeError("migration_handler should be an instance from a subclass of DummyMigrationhandler")
  30. self.backend = None
  31. self.set_backend(backend)
  32. self._components = {'uids': {}, 'EmClass': [], 'EmType': [], 'EmField': [], 'EmFieldGroup': []}
  33. self.load()
  34. def __hash__(self):
  35. components_dump = ""
  36. for _, comp in self._components['uids'].items():
  37. components_dump += str(hash(comp))
  38. hashstring = hashlib.new('sha512')
  39. hashstring.update(components_dump.encode('utf-8'))
  40. return int(hashstring.hexdigest(), 16)
  41. def __eq__(self, other):
  42. return self.__hash__() == other.__hash__()
  43. @staticmethod
  44. ## Given a name return an EmComponent child class
  45. # @param class_name str : The name to identify an EmComponent class
  46. # @return A python class or False if the class_name is not a name of an EmComponent child class
  47. def emclass_from_name(class_name):
  48. for cls in Model.components_class:
  49. if cls.__name__ == class_name:
  50. return cls
  51. return False
  52. @staticmethod
  53. ## Given a python class return a name
  54. # @param cls : The python class we want the name
  55. # @return A class name as string or False if cls is not an EmComponent child class
  56. # @todo réécrire le split, c'est pas bô
  57. def name_from_emclass(em_class):
  58. if em_class not in Model.components_class:
  59. if issubclass(em_class, EmField):
  60. return 'EmField'
  61. return False
  62. return em_class.__name__
  63. ## Loads the structure of the Editorial Model
  64. #
  65. # Gets all the objects contained in that structure and creates a dict indexed by their uids
  66. # @todo Change the thrown exception when a components check fails
  67. # @throw ValueError When a component class don't exists
  68. def load(self):
  69. datas = self.backend.load()
  70. for uid, kwargs in datas.items():
  71. #Store and delete the EmComponent class name from datas
  72. cls_name = kwargs['component']
  73. del kwargs['component']
  74. if cls_name == 'EmField':
  75. #Special EmField process because of fieldtypes
  76. if not 'fieldtype' in kwargs:
  77. raise AttributeError("Missing 'fieldtype' from EmField instanciation")
  78. cls = EditorialModel.fields.EmField.get_field_class(kwargs['fieldtype'])
  79. else:
  80. cls = self.emclass_from_name(cls_name)
  81. if cls:
  82. kwargs['uid'] = uid
  83. # create a dict for the component and one indexed by uids, store instanciated component in it
  84. self._components['uids'][uid] = cls(model=self, **kwargs)
  85. self._components[cls_name].append(self._components['uids'][uid])
  86. else:
  87. raise ValueError("Unknow EmComponent class : '" + cls_name + "'")
  88. #Sorting by rank
  89. for component_class in Model.components_class:
  90. self.sort_components(component_class)
  91. #Check integrity
  92. for uid, component in self._components['uids'].items():
  93. try:
  94. component.check()
  95. except EmComponentCheckError as exception_object:
  96. raise EmComponentCheckError("The component with uid %d is not valid. Check returns the following error : \"%s\"" % (uid, str(exception_object)))
  97. #Everything is done. Indicating that the component initialisation is over
  98. component.init_ended()
  99. ## Saves data using the current backend
  100. # @param filename str | None : if None use the current backend file (provided at backend instanciation)
  101. def save(self, filename = None):
  102. return self.backend.save(self, filename)
  103. ## Given a EmComponent child class return a list of instances
  104. # @param cls EmComponent : A python class
  105. # @return a list of instances or False if the class is not an EmComponent child
  106. def components(self, cls=None):
  107. if cls is None:
  108. return [ self.component(uid) for uid in self._components['uids'] ]
  109. key_name = self.name_from_emclass(cls)
  110. return False if key_name is False else self._components[key_name]
  111. ## Return an EmComponent given an uid
  112. # @param uid int : An EmComponent uid
  113. # @return The corresponding instance or False if uid don't exists
  114. def component(self, uid):
  115. return False if uid not in self._components['uids'] else self._components['uids'][uid]
  116. ## Sort components by rank in Model::_components
  117. # @param emclass pythonClass : The type of components to sort
  118. # @throw AttributeError if emclass is not valid
  119. # @warning disabled the test on component_class because of EmField new way of working
  120. def sort_components(self, component_class):
  121. #if component_class not in self.components_class:
  122. # raise AttributeError("Bad argument emclass : '" + str(component_class) + "', excpeting one of " + str(self.components_class))
  123. self._components[self.name_from_emclass(component_class)] = sorted(self.components(component_class), key=lambda comp: comp.rank)
  124. ## Return a new uid
  125. # @return a new uid
  126. def new_uid(self):
  127. used_uid = [int(uid) for uid in self._components['uids'].keys()]
  128. return sorted(used_uid)[-1] + 1 if len(used_uid) > 0 else 1
  129. ## Create a component from a component type and datas
  130. #
  131. # @note if datas does not contains a rank the new component will be added last
  132. # @note datas['rank'] can be an integer or two specials strings 'last' or 'first'
  133. # @param component_type str : a component type ( component_class, component_fieldgroup, component_field or component_type )
  134. # @param datas dict : the options needed by the component creation
  135. # @throw ValueError if datas['rank'] is not valid (too big or too small, not an integer nor 'last' or 'first' )
  136. # @todo Handle a raise from the migration handler
  137. # @todo Transform the datas arg in **datas ?
  138. def create_component(self, component_type, datas, uid=None):
  139. if not (uid is None) and (not isinstance(uid, int) or uid <= 0 or uid in self._components['uids']):
  140. raise ValueError("Invalid uid provided")
  141. if component_type not in [ n for n in self._components.keys() if n != 'uids' ]:
  142. raise ValueError("Invalid component_type rpovided")
  143. elif component_type == 'EmField':
  144. #special process for EmField
  145. if not 'fieldtype' in datas:
  146. raise AttributeError("Missing 'fieldtype' from EmField instanciation")
  147. em_obj = EditorialModel.fields.EmField.get_field_class(datas['fieldtype'])
  148. else:
  149. em_obj = self.emclass_from_name(component_type)
  150. rank = 'last'
  151. if 'rank' in datas:
  152. rank = datas['rank']
  153. del datas['rank']
  154. datas['uid'] = uid if uid else self.new_uid()
  155. em_component = em_obj(model=self, **datas)
  156. em_component.rank = em_component.get_max_rank() + 1 # Inserting last by default
  157. self._components['uids'][em_component.uid] = em_component
  158. self._components[component_type].append(em_component)
  159. if rank != 'last':
  160. em_component.set_rank(1 if rank == 'first' else rank)
  161. #everything done, indicating that initialisation is over
  162. em_component.init_ended()
  163. #register the creation in migration handler
  164. try:
  165. self.migration_handler.register_change(self, em_component.uid, None, em_component.attr_dump())
  166. except MigrationHandlerChangeError as exception_object:
  167. #Revert the creation
  168. self.components(em_component.__class__).remove(em_component)
  169. del self._components['uids'][em_component.uid]
  170. raise exception_object
  171. self.migration_handler.register_model_state(self, hash(self))
  172. return em_component
  173. ## Delete a component
  174. # @param uid int : Component identifier
  175. # @throw EmComponentNotExistError
  176. # @todo unable uid check
  177. # @todo Handle a raise from the migration handler
  178. def delete_component(self, uid):
  179. em_component = self.component(uid)
  180. if not em_component:
  181. raise EmComponentNotExistError()
  182. if em_component.delete_check():
  183. #register the deletion in migration handler
  184. self.migration_handler.register_change(self, uid, self.component(uid).attr_dump(), None)
  185. # delete internal lists
  186. self._components[self.name_from_emclass(em_component.__class__)].remove(em_component)
  187. del self._components['uids'][uid]
  188. #Register the new EM state
  189. self.migration_handler.register_model_state(self, hash(self))
  190. return True
  191. return False
  192. ## Changes the current backend
  193. #
  194. # @param backend unknown: A backend object
  195. def set_backend(self, backend):
  196. if issubclass(backend.__class__, EmBackendDummy):
  197. self.backend = backend
  198. else:
  199. raise TypeError('Backend should be an instance of a EmBackednDummy subclass')
  200. ## Returns a list of all the EmClass objects of the model
  201. def classes(self):
  202. return list(self._components[self.name_from_emclass(EmClass)])
  203. ## Use a new migration handler, re-apply all the ME to this handler
  204. #
  205. # @param new_mh MigrationHandler: A migration_handler object
  206. # @warning : if a relational-attribute field (with 'rel_field_id') comes before it's relational field (with 'rel_to_type_id'), this will blow up
  207. def migrate_handler(self, new_mh):
  208. new_me = Model(EmBackendDummy(), new_mh)
  209. relations = {'fields_list': [], 'superiors_list': []}
  210. # re-create component one by one, in components_class[] order
  211. for cls in self.components_class:
  212. for component in self.components(cls):
  213. component_type = self.name_from_emclass(cls)
  214. component_dump = component.attr_dump()
  215. # Save relations between component to apply them later
  216. for relation in relations.keys():
  217. if relation in component_dump and component_dump[relation]:
  218. relations[relation].append((component.uid, component_dump[relation]))
  219. del component_dump[relation]
  220. new_me.create_component(component_type, component_dump, component.uid)
  221. # apply selected field to types
  222. for fields_list in relations['fields_list']:
  223. uid, fields = fields_list
  224. for field_id in fields:
  225. new_me.component(uid).select_field(new_me.component(field_id))
  226. # add superiors to types
  227. for superiors_list in relations['superiors_list']:
  228. uid, sup_list = superiors_list
  229. for nature, superiors_uid in sup_list.items():
  230. for superior_uid in superiors_uid:
  231. new_me.component(uid).add_superior(new_me.component(superior_uid), nature)
  232. del new_me
  233. self.migration_handler = new_mh
  234. @classmethod
  235. ## @brief Generate a random editorial model
  236. #
  237. # The random generator can be tuned with integer parameters
  238. # that represent probability or maximum numbers of items.
  239. # The probability (chances) works like 1/x chances to append
  240. # with x the tunable parameter
  241. # Tunable generator parameters :
  242. # - classtype : Chances for a classtype to be empty (default 0)
  243. # - nclass : Maximum number of classes per classtypes (default 5)
  244. # - nofg : Chances for a classe to have no fieldgroup associated to it (default 10)
  245. # - notype : Chances for a classe to have no type associated to it (default 5)
  246. # - seltype : Chances for a type to select an optionnal field (default 2)
  247. # - ntypesuperiors : Chances for a type to link with a superiors (default 3)
  248. # - nofields : Chances for a fieldgroup to be empty (default 10)
  249. # - nfields : Maximum number of field per fieldgroups (default 8)
  250. # - rfields : Maximum number of relation_to_type attributes fields (default 5)
  251. # - optfield : Chances for a field to be optionnal (default 2)
  252. # @param backend : A backend to use with the new EM
  253. # @param **kwargs dict : Provide tunable generation parameter
  254. # @return A randomly generate EM
  255. def random(cls, backend, **kwargs):
  256. em = Model(backend)
  257. chances = {
  258. 'classtype' : 0, # a class in classtype
  259. 'nclass': 5, #max number of classes per classtype
  260. 'nofg': 10, #no fieldgroup in a class
  261. 'nfg': 5, #max number of fieldgroups per classes
  262. 'notype': 10, # no types in a class
  263. 'ntype': 8, # max number of types in a class
  264. 'seltype': 2, #chances to select an optional field
  265. 'ntypesuperiors': 2, #chances to link with a superior
  266. 'nofields': 10, # no fields in a fieldgroup
  267. 'nfields' : 8, #max number of fields per fieldgroups
  268. 'rfields': 5,#max number of attributes relation fields
  269. 'optfield': 2, #chances to be optionnal
  270. }
  271. for name,value in kwargs.items():
  272. if name not in chances:
  273. #warning
  274. pass
  275. else:
  276. chances[name] = value
  277. #classes creation
  278. for classtype in EmClassType.getall():
  279. if random.randint(0,chances['classtype']) == 0:
  280. for _ in range(random.randint(1,chances['nclass'])):
  281. cdats = cls._rnd_component_datas()
  282. cdats['classtype'] = classtype['name']
  283. em.create_component('EmClass', cdats)
  284. for emclass in em.classes():
  285. #fieldgroups creation
  286. if random.randint(0, chances['nofg']) != 0:
  287. for _ in range(random.randint(1, chances['nfg'])):
  288. fgdats = cls._rnd_component_datas()
  289. fgdats['class_id'] = emclass.uid
  290. em.create_component('EmFieldGroup', fgdats)
  291. #types creation
  292. if random.randint(0, chances['notype']) != 0:
  293. for _ in range(random.randint(1, chances['ntype'])):
  294. tdats = cls._rnd_component_datas()
  295. tdats['class_id'] = emclass.uid
  296. em.create_component('EmType', tdats)
  297. #random type hierarchy
  298. for emtype in em.components(EmType):
  299. possible = emtype.possible_superiors()
  300. for nat in possible:
  301. if len(possible[nat]) > 0 and random.randint(0, chances['ntypesuperiors']) == 0:
  302. random.shuffle(possible[nat])
  303. for i in range(random.randint(1, len(possible[nat]))):
  304. emtype.add_superior(possible[nat][i], nat)
  305. #fields creation
  306. ft_l = EmField.fieldtypes_list()
  307. for emfg in em.components(EmFieldGroup):
  308. if random.randint(0, chances['nofields']) != 0:
  309. for _ in range(random.randint(1, chances['nfields'])):
  310. ft = ft_l[random.randint(0,len(ft_l)-1)]
  311. fdats = cls._rnd_component_datas()
  312. fdats['fieldtype']=ft
  313. fdats['fieldgroup_id'] = emfg.uid
  314. if ft == 'rel2type':
  315. emtypes = em.components(EmType)
  316. fdats['rel_to_type_id'] = emtypes[random.randint(0,len(emtypes)-1)].uid
  317. if random.randint(0,chances['optfield']) == 0:
  318. fdats['optional'] = True
  319. em.create_component('EmField', fdats)
  320. #relationnal fiels creation
  321. ft_l = [ ft for ft in EmField.fieldtypes_list() if ft != 'rel2type' ]
  322. for emrelf in [ f for f in em.components(EmField) if f.ftype == 'rel2type' ]:
  323. for _ in range(0,chances['rfields']):
  324. ft = ft_l[random.randint(0, len(ft_l)-1)]
  325. fdats = cls._rnd_component_datas()
  326. fdats['fieldtype'] = ft
  327. fdats['fieldgroup_id'] = emrelf.fieldgroup_id
  328. if random.randint(0, chances['optfield']) == 0:
  329. fdats['optional'] = True
  330. em.create_component('EmField', fdats)
  331. #selection optionnal fields
  332. for emtype in em.components(EmType):
  333. selectable = [field for fieldgroup in emtype.fieldgroups() for field in fieldgroup.fields() if field.optional ]
  334. for field in selectable:
  335. if random.randint(0,chances['seltype']) == 0:
  336. emtype.select_field(field)
  337. return em
  338. @staticmethod
  339. ## @brief Generate a random string
  340. # @warning dirty cache trick with globals()
  341. # @return a randomly selected string
  342. def _rnd_str(words_src='/usr/share/dict/words'):
  343. if '_words' not in globals() or globals()['_words_fname'] != words_src:
  344. globals()['_words_fname'] = words_src
  345. with open(words_src, 'r') as fpw:
  346. globals()['_words'] = [ l.strip() for l in fpw ]
  347. words = globals()['_words']
  348. return words[random.randint(0,len(words)-1)]
  349. @classmethod
  350. ## @brief Generate a random MlString
  351. # @param nlng : Number of langs in the MlString
  352. # @return a random MlString with nlng translations
  353. # @todo use a dict to generated langages
  354. def _rnd_mlstr(cls, nlng):
  355. ret = MlString()
  356. for _ in range(nlng):
  357. ret.set(cls._rnd_str(), cls._rnd_str())
  358. return ret
  359. @classmethod
  360. ## @brief returns randomly generated datas for an EmComponent
  361. # @return a dict with name, string and help_text
  362. def _rnd_component_datas(cls):
  363. mlstr_nlang = 2;
  364. ret = dict()
  365. ret['name'] = cls._rnd_str()
  366. ret['string'] = cls._rnd_mlstr(mlstr_nlang)
  367. ret['help_text'] = cls._rnd_mlstr(mlstr_nlang)
  368. return ret