Açıklama Yok
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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. # -*- coding: utf-8 -*-
  2. ## @file components.py
  3. # Defines the EditorialModel::components::EmComponent class and the EditorialModel::components::ComponentNotExistError exception class
  4. import datetime
  5. import logging
  6. import EditorialModel.fieldtypes as ftypes
  7. from collections import OrderedDict
  8. logger = logging.getLogger('Lodel2.EditorialModel')
  9. ## This class is the mother class of all editorial model objects
  10. #
  11. # It gather all the properties and mechanism that are common to every editorial model objects
  12. # @see EditorialModel::classes::EmClass, EditorialModel::types::EmType, EditorialModel::fieldgroups::EmFieldGroup, EditorialModel::fields::EmField
  13. # @pure
  14. class EmComponent(object):
  15. ## Used by EmComponent::modify_rank
  16. ranked_in = None
  17. ## Read only properties
  18. _ro_properties = ['date_update', 'date_create', 'uid', 'rank', 'deleted']
  19. ## @brief List fields name and fieldtype
  20. #
  21. # This is a list that describe database fields common for each EmComponent child classes.
  22. # A database field is defined here by a tuple(name, type) with name a string and type an EditorialModel.fieldtypes.EmFieldType
  23. # @warning The EmFieldType in second position in the tuples must be a class type and not a class instance !!!
  24. # @see EditorialModel::classes::EmClass::_fields EditorialModel::fieldgroups::EmFieldGroup::_fields EditorialModel::types::EmType::_fields EditorialModel::fields::EmField::_fields
  25. _fields = [
  26. ('uid', ftypes.EmField_integer),
  27. ('name', ftypes.EmField_char),
  28. ('rank', ftypes.EmField_integer),
  29. ('date_update', ftypes.EmField_date),
  30. ('date_create', ftypes.EmField_date),
  31. ('string', ftypes.EmField_mlstring),
  32. ('help', ftypes.EmField_mlstring)
  33. ]
  34. ## Instaciate an EmComponent
  35. # @param id_or_name int|str: name or id of the object
  36. # @throw TypeError if id_or_name is not an integer nor a string
  37. # @throw NotImplementedError if called with EmComponent
  38. def __init__(self, data, components):
  39. if type(self) == EmComponent:
  40. raise NotImplementedError('Abstract class')
  41. ## @brief An OrderedDict storing fields name and values
  42. # Values are handled by EditorialModel::fieldtypes::EmFieldType
  43. # @warning \ref _fields instance property is not the same than EmComponent::_fields class property. In the instance property the EditorialModel::fieldtypes::EmFieldType are instanciated to be able to handle datas
  44. # @see EmComponent::_fields EditorialModel::fieldtypes::EmFieldType
  45. self._fields = OrderedDict([(name, ftype()) for (name, ftype) in (EmComponent._fields + self.__class__._fields)])
  46. self.components = components
  47. for name, value in data.items():
  48. if name in self._fields:
  49. self._fields[name].from_string(value)
  50. ## @brief Access an attribute of an EmComponent
  51. # This method is overloads the default __getattr__ to search in EmComponents::_fields . If there is an EditorialModel::EmField with a corresponding name in the component
  52. # it returns its value.
  53. # @param name str: The attribute name
  54. # @throw AttributeError if attribute don't exists
  55. # @see EditorialModel::EmField::value
  56. def __getattr__(self, name):
  57. if name != '_fields' and name in self._fields:
  58. return self._fields[name].value
  59. else:
  60. return super(EmComponent, self).__getattribute__(name)
  61. ## @brief Access an EmComponent attribute
  62. # This function overload the default __getattribute__ in order to check if the EmComponent was deleted.
  63. # @param name str: The attribute name
  64. # @throw EmComponentNotExistError if the component was deleted
  65. def __getattribute__(self, name):
  66. if super(EmComponent, self).__getattribute__('deleted'):
  67. raise EmComponentNotExistError("This " + super(EmComponent, self).__getattribute__('__class__').__name__ + " has been deleted")
  68. res = super(EmComponent, self).__getattribute(name)
  69. return res
  70. ## Set the value of an EmComponent attribute
  71. # @param name str: The propertie name
  72. # @param value *: The value
  73. def __setattr__(self, name, value):
  74. if name in self.__class__._ro_properties:
  75. raise TypeError("Propertie '" + name + "' is readonly")
  76. if name != '_fields' and hasattr(self, '_fields') and name in object.__getattribute__(self, '_fields'):
  77. self._fields[name].from_python(value)
  78. else:
  79. object.__setattr__(self, name, value)
  80. ## @brief Hash function that allows to compare two EmComponent
  81. # @return EmComponent+ClassName+uid
  82. def __hash__(self):
  83. return "EmComponent"+self.__class__.__name__+str(self.uid)
  84. ## @brief Test if two EmComponent are "equals"
  85. # @return True or False
  86. def __eq__(self, other):
  87. return self.__class__ == other.__class__ and self.uid == other.uid
  88. ## Set all fields
  89. def populate(self):
  90. records = [] # TODO
  91. for record in records:
  92. for keys in self._fields.keys():
  93. if keys in record:
  94. self._fields[keys].from_string(record[keys])
  95. super(EmComponent, self).__setattr__('deleted', False)
  96. ## Insert a new component
  97. #
  98. # This function create and assign a new UID and handle the date_create and date_update values
  99. # @warning There is a mandatory argument dbconf that indicate wich database configuration to use
  100. # @param **kwargs : Names arguments representing object properties
  101. # @return An instance of the created component
  102. # @throw TypeError if an element of kwargs isn't a valid object propertie or if a mandatory argument is missing
  103. # @throw RuntimeError if the creation fails at database level
  104. # @todo Check that every mandatory _fields are given in args
  105. # @todo Stop using datetime.datetime.utcnow() for date_update and date_create init
  106. @classmethod
  107. def create(cls, **kwargs):
  108. #Checking for invalid arguments
  109. valid_args = [ name for name,_ in (cls._fields + EmComponent._fields)]
  110. for argname in kwargs:
  111. if argname in ['date_update', 'date_create', 'rank', 'uid']: # Automatic properties
  112. raise TypeError("Invalid argument : " + argname)
  113. elif argname not in valid_args:
  114. raise TypeError("Unexcepted keyword argument '" + argname + "' for " + cls.__name__ + " creation")
  115. #Check uniq names constraint
  116. try:
  117. name = kwargs['name']
  118. exist = cls(name)
  119. for kname in kwargs:
  120. if not (getattr(exist, kname) == kwargs[kname]):
  121. raise EmComponentExistError("An " + cls.__name__ + " named " + name + " allready exists with a different " + kname)
  122. logger.info("Trying to create an " + cls.__name__ + " that allready exist with same attribute. Returning the existing one")
  123. return exist
  124. except EmComponentNotExistError:
  125. pass
  126. kwargs['uid'] = cls.new_uid()
  127. kwargs['date_update'] = kwargs['date_create'] = datetime.datetime.utcnow()
  128. kwargs['rank'] = cls._get_max_rank( kwargs[cls.ranked_in], dbe )+1
  129. return cls(kwargs['name'], dbconf)
  130. ## Delete this component
  131. # @return bool : True if deleted False if deletion aborded
  132. # @throw RunTimeError if it was unable to do the deletion
  133. def delete(self):
  134. pass
  135. ## @brief Get the maximum rank given an EmComponent child class and a ranked_in filter
  136. # @param ranked_in_value mixed: The rank "family"
  137. # @return -1 if no EmComponent found else return an integer >= 0
  138. @classmethod
  139. def _get_max_rank(cls, ranked_in_value):
  140. pass
  141. ## Only make a call to the class method
  142. # @return A positive integer or -1 if no components
  143. # @see EmComponent::_get_max_rank()
  144. def get_max_rank(self, ranked_in_value):
  145. return self.__class__._get_max_rank(ranked_in_value)
  146. ## Set a new rank for this component
  147. # @note This function assume that ranks are properly set from 1 to x with no gap
  148. # @param new_rank int: The new rank
  149. # @return True if success False if not
  150. # @throw TypeError If bad argument type
  151. # @throw ValueError if out of bound value
  152. def set_rank(self, new_rank):
  153. if not isinstance(new_rank, int):
  154. raise TypeError("Excepted <class int> but got "+str(type(new_rank)))
  155. if new_rank < 0 or new_rank > self.get_max_rank(getattr(self, self.ranked_in)):
  156. raise ValueError("Invalid new rank : "+str(new_rank))
  157. mod = new_rank - self.rank #Allow to know the "direction" of the "move"
  158. if mod == 0: #No modifications
  159. return True
  160. limits = [ self.rank + ( 1 if mod > 0 else -1), new_rank ] #The range of modified ranks
  161. limits.sort()
  162. dbe = self.db_engine
  163. conn = dbe.connect()
  164. table = sqlutils.get_table(self)
  165. #Selecting the components that will be modified
  166. req = table.select().where( getattr(table.c, self.ranked_in) == getattr(self, self.ranked_in)).where(table.c.rank >= limits[0]).where(table.c.rank <= limits[1])
  167. res = conn.execute(req)
  168. if not res: #Db error... Maybe false is a bit silent for a failuer
  169. return False
  170. rows = res.fetchall()
  171. updated_ranks = [{'b_uid': self.uid, 'b_rank': new_rank}]
  172. for row in rows:
  173. updated_ranks.append({'b_uid': row['uid'], 'b_rank': row['rank'] + (-1 if mod > 0 else 1)})
  174. req = table.update().where(table.c.uid == sql.bindparam('b_uid')).values(rank=sql.bindparam('b_rank'))
  175. res = conn.execute(req, updated_ranks)
  176. conn.close()
  177. if res:
  178. #Instance rank update
  179. self._fields['rank'].value = new_rank
  180. return bool(res)
  181. ## @brief Modify a rank given a sign and a new_rank
  182. # - If sign is '=' set the rank to new_rank
  183. # - If sign is '-' set the rank to cur_rank - new_rank
  184. # - If sign is '+' set the rank to cur_rank + new_rank
  185. # @param new_rank int: The new_rank or rank modifier
  186. # @param sign str: Can be one of '=', '+', '-'
  187. # @return True if success False if fails
  188. # @throw TypeError If bad argument type
  189. # @throw ValueError if out of bound value
  190. def modify_rank(self,new_rank, sign='='):
  191. if not isinstance(new_rank, int) or not isinstance(sign, str):
  192. raise TypeError("Excepted <class int>, <class str>. But got "+str(type(new_rank))+", "+str(type(sign)))
  193. if sign == '+':
  194. return self.set_rank(self.rank + new_rank)
  195. elif sign == '-':
  196. return self.set_rank(self.rank - new_rank)
  197. elif sign == '=':
  198. return self.set_rank(new_rank)
  199. else:
  200. raise ValueError("Excepted one of '=', '+', '-' for sign argument, but got "+sign)
  201. ## @brief Return a string representation of the component
  202. # @return A string representation of the component
  203. def __repr__(self):
  204. if self.name is None:
  205. return "<%s #%s, 'non populated'>" % (type(self).__name__, self.uid)
  206. else:
  207. return "<%s #%s, '%s'>" % (type(self).__name__, self.uid, self.name)
  208. @classmethod
  209. ## Register a new component in UID table
  210. #
  211. # Use the class property table
  212. # @return A new uid (an integer)
  213. def new_uid(cls, db_engine):
  214. if cls.table is None:
  215. raise NotImplementedError("Abstract method")
  216. dbe = db_engine
  217. uidtable = sql.Table('uids', sqlutils.meta(dbe))
  218. conn = dbe.connect()
  219. req = uidtable.insert(values={'table': cls.table})
  220. res = conn.execute(req)
  221. uid = res.inserted_primary_key[0]
  222. logger.debug("Registering a new UID '" + str(uid) + "' for '" + cls.table + "' component")
  223. conn.close()
  224. return uid
  225. ## @brief An exception class to tell that a component don't exist
  226. class EmComponentNotExistError(Exception):
  227. pass
  228. ## @brief Raised on uniq constraint error at creation
  229. # This exception class is dedicated to be raised when create() method is called
  230. # if an EmComponent with this name but different parameters allready exist
  231. class EmComponentExistError(Exception):
  232. pass
  233. ## @brief An exception class to tell that no ranking exist yet for the group of the object
  234. class EmComponentRankingNotExistError(Exception):
  235. pass