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.

lerelation.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. #-*- coding: utf-8 -*-
  2. import copy
  3. import re
  4. import warnings
  5. import EditorialModel.classtypes
  6. import EditorialModel.fieldtypes.leo as ft_leo
  7. from . import lecrud
  8. from . import leobject
  9. from . import lefactory
  10. ## @brief Main class for relations
  11. class _LeRelation(lecrud._LeCrud):
  12. _superior_field_name = None
  13. _subordinate_field_name = None
  14. ## @brief Stores the list of fieldtypes that are common to all relations
  15. _rel_fieldtypes = dict()
  16. def __init__(self, id_relation, **kwargs):
  17. super().__init__(id_relation, **kwargs)
  18. ## @brief Forge a filter to match the superior
  19. @classmethod
  20. def sup_filter(self, leo):
  21. if isinstance(leo, leobject._LeObject):
  22. return (self._superior_field_name, '=', leo)
  23. ## @brief Forge a filter to match the superior
  24. @classmethod
  25. def sub_filter(self, leo):
  26. if isinstance(leo, leobject._LeObject):
  27. return (self._subordinate_field_name, '=', leo)
  28. ## @return The name of the uniq id field
  29. @classmethod
  30. def uidname(cls):
  31. return EditorialModel.classtypes.relation_uid
  32. ## @return a dict with field name as key and fieldtype instance as value
  33. @classmethod
  34. def fieldtypes(cls):
  35. rel_ft = dict()
  36. rel_ft.update(cls._uid_fieldtype)
  37. rel_ft.update(cls._rel_fieldtypes)
  38. if cls.implements_lerel2type():
  39. rel_ft.update(cls._rel_attr_fieldtypes)
  40. return rel_ft
  41. @classmethod
  42. def _prepare_relational_fields(cls, field):
  43. return lecrud.LeApiQueryError("Relational field '%s' given but %s doesn't is not a LeObject" % (field,
  44. cls.__name__))
  45. ## @brief Prepare filters before sending them to the datasource
  46. # @param cls : Concerned class
  47. # @param filters_l list : List of filters
  48. # @return prepared and checked filters
  49. @classmethod
  50. def _prepare_filters(cls, filters_l):
  51. filters, rel_filters = super()._prepare_filters(filters_l)
  52. res_filters = list()
  53. for field, op, value in filters:
  54. if field in [cls._superior_field_name, cls._subordinate_field_name]:
  55. if isinstance(value, str):
  56. try:
  57. value = int(value)
  58. except ValueError as e:
  59. raise LeApiDataCheckError("Wrong value given for '%s'"%field)
  60. if isinstance(value, int):
  61. value = cls.name2class('LeObject')(value)
  62. res_filters.append( (field, op, value) )
  63. return res_filters, rel_filters
  64. @classmethod
  65. ## @brief deletes a relation between two objects
  66. # @param filters_list list
  67. # @param target_class str
  68. def delete(cls, filters_list, target_class):
  69. filters, rel_filters = cls._prepare_filters(filters_list)
  70. if isinstance(target_class, str):
  71. target_class = cls.name2class(target_class)
  72. ret = cls._datasource.delete(target_class, filters)
  73. return True if ret == 1 else False
  74. ## @brief move to the first rank
  75. # @return True in case of success, False in case of failure
  76. def move_first(self):
  77. return self.set_rank('first')
  78. ## @brief move to the last rank
  79. # @return True in case of success, False in case of failure
  80. def move_last(self):
  81. return self.set_rank('last')
  82. ## @brief move to the given rank defined by a shift step
  83. # @param step int : The step
  84. # @return True in case of success, False in case of failure
  85. # @throw ValueError if step is not castable into an integer
  86. def shift_rank(self, step):
  87. step = int(step)
  88. return self.set_rank(self.rank + step)
  89. ## @brief modify a relation rank
  90. # @param new_rank int|str : The new rank can be an integer > 1 or strings 'first' or 'last'
  91. # @return True in case of success, False in case of failure
  92. # @throw ValueError if step is not castable into an integer
  93. def set_rank(self, new_rank):
  94. raise NotImplemented("Abtract method")
  95. ## @brief Implements set_rank
  96. def _set_rank(self, new_rank, **get_max_rank_args):
  97. max_rank = self.get_max_rank(**get_max_rank_args)
  98. try:
  99. new_rank = int(new_rank)
  100. except ValueError:
  101. if new_rank == 'first':
  102. new_rank = 1
  103. elif new_rank == 'last':
  104. new_rank = max_rank
  105. else:
  106. raise ValueError("The new rank can be an integer > 1 or strings 'first' or 'last', but %s given"%new_rank)
  107. if self.rank == new_rank:
  108. return True
  109. if new_rank < 1:
  110. if strict:
  111. raise ValueError("Rank must be >= 1, but %d given"%rank)
  112. new_rank = 1
  113. elif new_rank > max_rank:
  114. if strict:
  115. raise ValueError("Rank is too big (max_rank = %d), but %d given"%(max_rank,rank))
  116. new_rank = max_rank
  117. self._datasource.update_rank(self, new_rank)
  118. ## @returns The maximum assignable rank for this relation
  119. # @todo implementation
  120. def get_max_rank(self):
  121. raise NotImplemented("Abstract method")
  122. ## @brief Abstract class to handle hierarchy relations
  123. class _LeHierarch(_LeRelation):
  124. ## @brief Delete current instance from DB
  125. def delete(self):
  126. lecrud._LeCrud._delete(self)
  127. ## @brief modify a LeHierarch rank
  128. # @param new_rank int|str : The new rank can be an integer > 1 or strings 'first' or 'last'
  129. # @return True in case of success, False in case of failure
  130. # @throw ValueError if step is not castable into an integer
  131. def set_rank(self, new_rank):
  132. return self._set_rank(
  133. new_rank,
  134. id_superior=getattr(self, self.uidname()),
  135. nature=self.nature
  136. )
  137. @classmethod
  138. def insert(cls, datas):
  139. # Checks if the relation exists
  140. datas[EditorialModel.classtypes.relation_name] = None
  141. res = cls.get(
  142. [(cls._subordinate_field_name, '=', datas['subordinate']), ('nature', '=', datas['nature'])],
  143. [ cls.uidname() ]
  144. )
  145. if not(res is None) and len(res) > 0:
  146. return False
  147. return super().insert(datas, 'LeHierarch')
  148. ## @brief Get maximum assignable rank given a superior id and a nature
  149. # @return an integer > 1
  150. @classmethod
  151. def get_max_rank(cls, id_superior, nature):
  152. if nature not in EditorialModel.classtypes.EmNature.getall():
  153. raise ValueError("Unknow relation nature '%s'" % nature)
  154. sql_res = cls.get(
  155. query_filters=[
  156. ('nature','=', nature),
  157. (cls._superior_field_name, '=', id_superior),
  158. ],
  159. field_list=['rank'],
  160. order=[('rank', 'DESC')],
  161. limit=1,
  162. instanciate=False
  163. )
  164. return sql_res[0]['rank']+1 if not(sql_res is None) and len(sql_res) > 0 else 1
  165. ## @brief instanciate the relevant lodel object using a dict of datas
  166. @classmethod
  167. def object_from_data(cls, datas):
  168. return cls.name2class('LeHierarch')(**datas)
  169. ## @brief Abstract class to handle rel2type relations
  170. class _LeRel2Type(_LeRelation):
  171. ## @brief Stores the list of fieldtypes handling relations attributes
  172. _rel_attr_fieldtypes = dict()
  173. ## @brief Stores the LeClass child class used as superior
  174. _superior_cls = None
  175. ## @brief Stores the LeType child class used as subordinate
  176. _subordinate_cls = None
  177. ## @brief Stores the relation name for a rel2type
  178. _relation_name = None
  179. ## @brief Delete current instance from DB
  180. def delete(self):
  181. lecrud._LeCrud._delete(self)
  182. ## @brief modify a LeRel2Type rank
  183. # @param new_rank int|str : The new rank can be an integer > 1 or strings 'first' or 'last'
  184. # @return True in case of success, False in case of failure
  185. # @throw ValueError if step is not castable into an integer
  186. def set_rank(self, new_rank):
  187. if self._relation_name is None:
  188. raise NotImplementedError("Abstract method")
  189. return self._set_rank(new_rank, superior = self.superior, relation_name = self._relation_name)
  190. @classmethod
  191. def get_max_rank(cls, superior, relation_name):
  192. # SELECT rank FROM relation JOIN object ON object.lodel_id = id_subordinate WHERE object.type_id = <type_em_id>
  193. ret = cls.get(
  194. query_filters = [
  195. (EditorialModel.classtypes.relation_name, '=', relation_name),
  196. (EditorialModel.classtypes.relation_superior, '=', superior),
  197. ],
  198. field_list = ['rank'],
  199. order = [('rank', 'DESC')],
  200. limit = 1,
  201. instanciate = False
  202. )
  203. return 1 if ret is None else ret[0]['rank']
  204. ## @brief Implements insert for rel2type
  205. # @todo checks when autodetecing the rel2type class
  206. @classmethod
  207. def insert(cls, datas, classname = None):
  208. #Set the nature
  209. if 'nature' not in datas:
  210. datas['nature'] = None
  211. if cls.__name__ == 'LeRel2Type' and classname is None:
  212. if EditorialModel.classtypes.relation_name not in datas:
  213. raise RuntimeError("Unable to autodetect rel2type. No relation_name given")
  214. # autodetect the rel2type child class (BROKEN)
  215. classname = relname(datas[self._superior_field_name], datas[self._subordinate_field_name], datas[EditorialModel.classtypes.relation_name])
  216. else:
  217. if classname != None:
  218. ccls = cls.name2class(classname)
  219. if ccls == False:
  220. raise lecrud.LeApiErrors("Bad classname given")
  221. relation_name = ccls._relation_name
  222. else:
  223. relation_name = cls._relation_name
  224. datas[EditorialModel.classtypes.relation_name] = relation_name
  225. return super().insert(datas, classname)
  226. ## @brief Given a superior and a subordinate, returns the classname of the give rel2type
  227. # @param lesupclass LeClass : LeClass child class (not an instance) (can be a LeType or a LeClass child)
  228. # @param lesubclass LeType : A LeType child class (not an instance)
  229. # @return a name as string
  230. @classmethod
  231. def relname(cls, lesupclass, lesubclass, relation_name):
  232. supname = lesupclass._leclass.__name__ if lesupclass.implements_letype() else lesupclass.__name__
  233. subname = lesubclass.__name__
  234. return cls.name2rel2type(supname, subname, relation_name)
  235. ## @brief instanciate the relevant lodel object using a dict of datas
  236. @classmethod
  237. def object_from_data(cls, datas):
  238. le_object = cls.name2class('LeObject')
  239. class_name = le_object._me_uid[datas['class_id']].__name__
  240. type_name = le_object._me_uid[datas['type_id']].__name__
  241. relation_classname = lecrud._LeCrud.name2rel2type(class_name, type_name, EditorialModel.classtypes.relation_name)
  242. del(datas['class_id'], datas['type_id'])
  243. return cls.name2class(relation_classname)(**datas)