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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. ## @package EditorialModel.components
  3. # @brief Base objects for all EditorialModel components
  4. #
  5. # Defines the EditorialModel::components::EmComponent class
  6. import datetime
  7. import hashlib
  8. import EditorialModel
  9. from Lodel.utils.mlstring import MlString
  10. ## @brief This class is the mother class of all editorial model objects
  11. #
  12. # It gather all the properties and mechanism that are common to every editorial model objects
  13. # @see EditorialModel::classes::EmClass, EditorialModel::types::EmType, EditorialModel::fieldgroups::EmFieldGroup, EditorialModel::fields::EmField
  14. class EmComponent(object):
  15. ## Used by EmComponent::modify_rank
  16. ranked_in = None
  17. def __init__(self, model, uid, name, string=None, help_text=None, date_update=None, date_create=None, rank=None):
  18. if type(self) == EmComponent:
  19. raise NotImplementedError('Abstract class')
  20. if model.__class__.__name__ != 'Model':
  21. raise TypeError("Excepted type for 'model' arg is <class 'Model'> but got {} instead".format(type(model)))
  22. self.model = model
  23. self.uid = uid
  24. self.check_type('uid', int)
  25. self.name = name
  26. self.check_type('name', str)
  27. self.string = MlString() if string is None else string
  28. self.check_type('string', MlString)
  29. self.help_text = MlString() if help_text is None else help_text
  30. self.check_type('help_text', MlString)
  31. self.date_update = datetime.datetime.now() if date_update is None else date_update # WARNING timezone !
  32. self.check_type('date_update', datetime.datetime)
  33. self.date_create = datetime.datetime.now() if date_create is None else date_create # WARNING timezone !
  34. self.check_type('date_create', datetime.datetime)
  35. self._inited = False
  36. #Handling specials ranks for component creation
  37. self.rank = rank
  38. ## @brief Return a dict with attributes name as key and attributes value as value
  39. # @note Used at creation and deletion to call the migration handler
  40. def attr_dump(self):
  41. return {fname: fval for fname, fval in self.__dict__.items() if not (fname.startswith('_') or (fname == 'uid') or (fname == 'model'))}
  42. ## @brief Return a dict with attributes name as key and attributes value flattened
  43. def attr_flat(self):
  44. attributes_dump = self.attr_dump()
  45. for attr_name in list(attributes_dump.keys()):
  46. if isinstance(attributes_dump[attr_name], EmComponent):
  47. attributes_dump[attr_name] = attributes_dump[attr_name].uid
  48. elif isinstance(attributes_dump[attr_name], MlString):
  49. attributes_dump[attr_name] = attributes_dump[attr_name].__str__()
  50. attributes_dump['component'] = self.__class__.__name__
  51. return attributes_dump
  52. @property
  53. ## @brief Provide a uniq name
  54. #
  55. # Identify a component with his type and name
  56. def uniq_name(self):
  57. uname = self.__class__.__name__
  58. if not isinstance(self, EditorialModel.fields.EmField): # WARNING this could crash with fieldtypes
  59. try:
  60. uname += '_' + self.em_class.name # TODO Ajouter la propriété
  61. except AttributeError:
  62. pass
  63. uname += '_' + self.name
  64. return uname
  65. ## @brief This function has to be called after the instanciation, checks, and init manipulations are done
  66. # @note Create a new attribute _inited that allow __setattr__ to know if it has or not to call the migration handler
  67. def init_ended(self):
  68. self._inited = True
  69. ## @brief Reimplementation for calling the migration handler to register the change
  70. def __setattr__(self, attr_name, value):
  71. inited = '_inited' in self.__dict__ and self.__dict__['_inited']
  72. if inited:
  73. # if fails raise MigrationHandlerChangeError
  74. self.model.migration_handler.register_change(self.model, self.uid, {attr_name: getattr(self, attr_name)}, {attr_name: value})
  75. super(EmComponent, self).__setattr__(attr_name, value)
  76. if inited:
  77. self.model.migration_handler.register_model_state(self.model, hash(self.model))
  78. ## Check the type of attribute named var_name
  79. # @param var_name str : the attribute name
  80. # @param excepted_type tuple|type : Tuple of type or a type
  81. # @throw AttributeError if wrong type detected
  82. def check_type(self, var_name, excepted_type):
  83. var = getattr(self, var_name)
  84. if not isinstance(var, excepted_type):
  85. raise AttributeError("Excepted %s to be an %s but got %s instead" % (var_name, str(excepted_type), str(type(var))))
  86. ## @brief Hash function that allows to compare two EmComponent
  87. # @return EmComponent+ClassName+uid
  88. def __hash__(self):
  89. # flatten list of attributes of the component to an ordered list
  90. # so every time we have the same string representation
  91. attributes_flat = self.attr_flat()
  92. ordered_attributes = sorted(list(attributes_flat.keys()))
  93. component_dump = []
  94. for attr_name in ordered_attributes:
  95. if isinstance(attributes_flat[attr_name], datetime.datetime): # drop date values
  96. continue
  97. component_dump.append((attr_name, attributes_flat[attr_name]))
  98. return int(hashlib.md5(str(component_dump).encode('utf-8')).hexdigest(), 16)
  99. ## @brief Test if two EmComponent are "equals"
  100. # @return True or False
  101. def __eq__(self, other):
  102. return hash(self) == hash(other)
  103. ## Check if the EmComponent is valid
  104. # This function has to check that rank are correct and continuous other checks are made in childs classes
  105. # @warning Hardcoded minimum rank
  106. # @warning Rank modified by _fields['rank'].value
  107. # @throw EmComponentCheckError if fails
  108. def check(self):
  109. self.model.sort_components(self.__class__)
  110. if self.get_max_rank() != len(self.same_rank_group()) or self.rank <= 0:
  111. #Non continuous ranks
  112. for i, component in enumerate(self.same_rank_group()):
  113. component.rank = i + 1
  114. # No need to sort again here
  115. ## @brief Delete predicate. Indicates if a component can be deleted
  116. # @return True if deletion OK else return False
  117. def delete_check(self):
  118. raise NotImplementedError("Virtual method")
  119. ## @brief Get the maximum rank given an EmComponent child class and a ranked_in filter
  120. # @return The max rank is the rank group or 0 if no components in that group
  121. def get_max_rank(self):
  122. same_rgroup = self.same_rank_group()
  123. return max([comp.rank for comp in same_rgroup]) if len(same_rgroup) > 0 else 0
  124. ## Return an array of instances that are concerned by the same rank
  125. # @return An array of instances that are concerned by the same rank
  126. def same_rank_group(self):
  127. components = self.model.components(self.__class__)
  128. ranked_in = self.__class__.ranked_in
  129. return [c for c in components if getattr(c, ranked_in) == getattr(self, ranked_in)]
  130. ## Set a new rank for this component
  131. # @note This function assume that ranks are properly set from 1 to x with no gap
  132. #
  133. # @warning Hardcoded minimum rank
  134. # @warning Rank modified by _fields['rank'].value
  135. #
  136. # @param new_rank int: The new rank
  137. #
  138. # @throw TypeError If bad argument type
  139. # @throw ValueError if out of bound value
  140. def set_rank(self, new_rank):
  141. if not isinstance(new_rank, int):
  142. raise TypeError("Excepted <class int> but got " + str(type(new_rank)))
  143. if new_rank <= 0 or (new_rank > 1 and new_rank > self.get_max_rank()):
  144. raise ValueError("Invalid new rank : " + str(new_rank))
  145. mod = new_rank - self.rank # Indicates the "direction" of the "move"
  146. if mod == 0:
  147. return True
  148. limits = [self.rank + (1 if mod > 0 else -1), new_rank] # The range of modified ranks
  149. limits.sort()
  150. for component in [c for c in self.same_rank_group() if c.rank >= limits[0] and c.rank <= limits[1]]:
  151. component.rank = component.rank + (-1 if mod > 0 else 1)
  152. self.rank = new_rank
  153. self.model.sort_components(self.__class__)
  154. ## Modify a rank given an integer modifier
  155. # @note this method always tries to make the modification : if modifier is too big put
  156. # the component in last position, if modifier is to small put the component
  157. # in first position
  158. # @param rank_mod int : can be a negative positive or zero integer
  159. # @return True if the modification was made as wanted else return false
  160. # @throw TypeError if rank_mod is not an integer
  161. def modify_rank(self, rank_mod):
  162. if not isinstance(rank_mod, int):
  163. raise TypeError("Excepted <class int>. But got %s" % str(type(rank_mod)))
  164. ret = True
  165. new_rank = self.rank + rank_mod
  166. if new_rank < 1:
  167. ret = False
  168. new_rank = 1
  169. elif new_rank > self.get_max_rank():
  170. ret = False
  171. new_rank = self.get_max_rank()
  172. self.set_rank(new_rank)
  173. return ret
  174. ## @brief Return a string representation of the component
  175. # @return A string representation of the component
  176. def __repr__(self):
  177. if self.name is None:
  178. return "<%s #%s, 'non populated'>" % (type(self).__name__, self.uid)
  179. else:
  180. return "<%s #%s, '%s'>" % (type(self).__name__, self.uid, self.name)