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

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