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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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 sqlalchemy as sql
  7. from Database import sqlutils
  8. import EditorialModel.fieldtypes as ftypes
  9. from collections import OrderedDict
  10. logger = logging.getLogger('Lodel2.EditorialModel')
  11. ## This class is the mother class of all editorial model objects
  12. #
  13. # It gather all the properties and mechanism that are common to every editorial model objects
  14. # @see EditorialModel::classes::EmClass, EditorialModel::types::EmType, EditorialModel::fieldgroups::EmFieldGroup, EditorialModel::fields::EmField
  15. # @pure
  16. class EmComponent(object):
  17. ## The name of the engine configuration
  18. # @todo Not a good idea to store it here
  19. dbconf = 'default'
  20. ## The table in wich we store data for this object
  21. # None for EmComponent
  22. table = None
  23. ## Used by EmComponent::modify_rank
  24. ranked_in = None
  25. ## Read only properties
  26. _ro_properties = ['date_update', 'date_create', 'uid', 'rank', 'deleted']
  27. ## @brief List fields name and fieldtype
  28. #
  29. # This is a list that describe database fields common for each EmComponent child classes.
  30. # A database field is defined here by a tuple(name, type) with name a string and type an EditorialModel.fieldtypes.EmFieldType
  31. # @warning The EmFieldType in second position in the tuples must be a class type and not a class instance !!!
  32. # @see EditorialModel::classes::EmClass::_fields EditorialModel::fieldgroups::EmFieldGroup::_fields EditorialModel::types::EmType::_fields EditorialModel::fields::EmField::_fields
  33. _fields = [
  34. ('uid', ftypes.EmField_integer),
  35. ('name', ftypes.EmField_char),
  36. ('rank', ftypes.EmField_integer),
  37. ('date_update', ftypes.EmField_date),
  38. ('date_create', ftypes.EmField_date),
  39. ('string', ftypes.EmField_mlstring),
  40. ('help', ftypes.EmField_mlstring)
  41. ]
  42. ## Instaciate an EmComponent
  43. # @param id_or_name int|str: name or id of the object
  44. # @throw TypeError if id_or_name is not an integer nor a string
  45. # @throw NotImplementedError if called with EmComponent
  46. def __init__(self, id_or_name):
  47. if type(self) == EmComponent:
  48. raise NotImplementedError('Abstract class')
  49. ## @brief An OrderedDict storing fields name and values
  50. # Values are handled by EditorialModel::fieldtypes::EmFieldType
  51. # @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
  52. # @see EmComponent::_fields EditorialModel::fieldtypes::EmFieldType
  53. self._fields = OrderedDict([(name, ftype()) for (name, ftype) in (EmComponent._fields + self.__class__._fields)])
  54. # populate
  55. if isinstance(id_or_name, int):
  56. self._fields['uid'].value = id_or_name # read only propertie set
  57. elif isinstance(id_or_name, str):
  58. self.name = id_or_name
  59. else:
  60. raise TypeError('Bad argument: expecting <int> or <str> but got : ' + str(type(id_or_name)))
  61. self.table = self.__class__.table
  62. self.populate()
  63. ## @brief Access an attribute of an EmComponent
  64. # 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
  65. # it returns its value.
  66. # @param name str: The attribute name
  67. # @throw AttributeError if attribute don't exists
  68. # @see EditorialModel::EmField::value
  69. def __getattr__(self, name):
  70. if name != '_fields' and name in self._fields:
  71. return self._fields[name].value
  72. else:
  73. return super(EmComponent, self).__getattribute__(name)
  74. ## @brief Access an EmComponent attribute
  75. # This function overload the default __getattribute__ in order to check if the EmComponent was deleted.
  76. # @param name str: The attribute name
  77. # @throw EmComponentNotExistError if the component was deleted
  78. def __getattribute__(self, name):
  79. if super(EmComponent, self).__getattribute__('deleted'):
  80. raise EmComponentNotExistError("This " + super(EmComponent, self).__getattribute__('__class__').__name__ + " has been deleted")
  81. res = super(EmComponent, self).__getattribute(name)
  82. return res
  83. ## Set the value of an EmComponent attribute
  84. # @param name str: The propertie name
  85. # @param value *: The value
  86. def __setattr__(self, name, value):
  87. if name in self.__class__._ro_properties:
  88. raise TypeError("Propertie '" + name + "' is readonly")
  89. if name != '_fields' and hasattr(self, '_fields') and name in object.__getattribute__(self, '_fields'):
  90. self._fields[name].from_python(value)
  91. else:
  92. object.__setattr__(self, name, value)
  93. ## @brief Hash function that allows to compare two EmComponent
  94. # @return EmComponent+ClassName+uid
  95. def __hash__(self):
  96. return "EmComponent"+self.__class__.__name__+str(self.uid)
  97. ## @brief Test if two EmComponent are "equals"
  98. # @return True or False
  99. def __eq__(self, other):
  100. return self.__class__ == other.__class__ and self.uid == other.uid
  101. ## Lookup in the database properties of the object to populate the properties
  102. # @throw EmComponentNotExistError if the instance is not anymore stored in database
  103. def populate(self):
  104. records = self._populate_db() # Db query
  105. for record in records:
  106. for keys in self._fields.keys():
  107. if keys in record:
  108. self._fields[keys].from_string(record[keys])
  109. super(EmComponent, self).__setattr__('deleted', False)
  110. @classmethod
  111. ## Shortcut that return the sqlAlchemy engine
  112. def db_engine(cls):
  113. return sqlutils.get_engine(cls.dbconf)
  114. ## Do the query on the database for EmComponent::populate()
  115. # @throw EmComponentNotExistError if the instance is not anymore stored in database
  116. def _populate_db(self):
  117. dbe = self.__class__.db_engine()
  118. component = sql.Table(self.table, sqlutils.meta(dbe))
  119. req = sql.sql.select([component])
  120. if self.uid is None:
  121. req = req.where(component.c.name == self.name)
  122. else:
  123. req = req.where(component.c.uid == self.uid)
  124. conn = dbe.connect()
  125. res = conn.execute(req)
  126. res = res.fetchall()
  127. conn.close()
  128. if not res or len(res) == 0:
  129. raise EmComponentNotExistError("No " + self.__class__.__name__ + " found with " + ('name ' + self.name if self.uid is None else 'uid ' + str(self.uid)))
  130. return res
  131. ## Insert a new component in the database
  132. #
  133. # This function create and assign a new UID and handle the date_create and date_update values
  134. #
  135. # @param **kwargs : Names arguments representing object properties
  136. # @return An instance of the created component
  137. # @throw TypeError if an element of kwargs isn't a valid object propertie or if a mandatory argument is missing
  138. # @throw RuntimeError if the creation fails at database level
  139. # @todo Check that every mandatory _fields are given in args
  140. # @todo Stop using datetime.datetime.utcnow() for date_update and date_create init
  141. @classmethod
  142. def create(cls, **kwargs):
  143. #Checking for invalid arguments
  144. valid_args = [ name for name,_ in (cls._fields + EmComponent._fields)]
  145. for argname in kwargs:
  146. if argname in ['date_update', 'date_create', 'rank', 'uid']: # Automatic properties
  147. raise TypeError("Invalid argument : " + argname)
  148. elif argname not in valid_args:
  149. raise TypeError("Unexcepted keyword argument '"+argname+"' for "+cls.__name__+" creation")
  150. #Check uniq names constraint
  151. try:
  152. name = kwargs['name']
  153. exist = cls(name)
  154. for kname in kwargs:
  155. if not (getattr(exist, kname) == kwargs[kname]):
  156. raise EmComponentExistError("An "+cls.__name__+" named "+name+" allready exists with a different "+kname)
  157. logger.info("Trying to create an "+cls.__name__+" that allready exist with same attribute. Returning the existing one")
  158. return exist
  159. except EmComponentNotExistError:
  160. pass
  161. # Mandatory fields check (actual fieldtypes don't allow this check
  162. #for name in cls._fields:
  163. # if cls._fields[name].notNull and cls._fields[name].default == None:
  164. # raise TypeError("Missing argument : "+name)
  165. kwargs['uid'] = cls.new_uid()
  166. kwargs['date_update'] = kwargs['date_create'] = datetime.datetime.utcnow()
  167. dbe = cls.db_engine()
  168. conn = dbe.connect()
  169. kwargs['rank'] = cls.get_max_rank( kwargs[cls.ranked_in] )+1
  170. table = sql.Table(cls.table, sqlutils.meta(dbe))
  171. req = table.insert(kwargs)
  172. if not conn.execute(req):
  173. raise RuntimeError("Unable to create the "+cls.__class__.__name__+" EmComponent ")
  174. conn.close()
  175. return cls(kwargs['name'])
  176. ## Write the representation of the component in the database
  177. # @return bool
  178. # @todo stop using datetime.datetime.utcnow() for date_update update
  179. def save(self):
  180. values = {}
  181. for name, field in self._fields.items():
  182. values[name] = field.to_sql()
  183. # Don't allow creation date overwritting
  184. #if 'date_create' in values:
  185. #del values['date_create']
  186. #logger.warning("date_create supplied for save, but overwritting of date_create not allowed, the date will not be changed")
  187. values['date_update'] = datetime.datetime.utcnow()
  188. self._save_db(values)
  189. ## Do the query in the datbase for EmComponent::save()
  190. # @param values dict: A dictionnary of the values to insert
  191. # @throw RunTimeError if it was unable to do the Db update
  192. def _save_db(self, values):
  193. """ Do the query on the db """
  194. dbe = self.__class__.db_engine()
  195. component = sql.Table(self.table, sqlutils.meta(dbe))
  196. req = sql.update(component, values=values).where(component.c.uid == self.uid)
  197. conn = dbe.connect()
  198. res = conn.execute(req)
  199. conn.close()
  200. if not res:
  201. raise RuntimeError("Unable to save the component in the database")
  202. ## Delete this component data in the database
  203. # @return bool : True if deleted False if deletion aborded
  204. # @throw RunTimeError if it was unable to do the deletion
  205. def delete(self):
  206. #<SQL>
  207. dbe = self.__class__.db_engine()
  208. component = sql.Table(self.table, sqlutils.meta(dbe))
  209. req = component.delete().where(component.c.uid == self.uid)
  210. conn = dbe.connect()
  211. res = conn.execute(req)
  212. conn.close()
  213. if not res:
  214. raise RuntimeError("Unable to delete the component in the database")
  215. #</SQL>
  216. super(EmComponent, self).__setattr__('deleted', True)
  217. return True
  218. ## get_max_rank
  219. # Retourne le rank le plus élevé pour le groupe de component au quel apartient l'objet actuelle
  220. # @param ranked_in_value mixed: The rank "family"
  221. # @param return -1 if no EmComponent found else return an integer >= 0
  222. @classmethod
  223. def get_max_rank(cls, ranked_in_value):
  224. dbe = cls.db_engine()
  225. component = sql.Table(cls.table, sqlutils.meta(dbe))
  226. req = sql.sql.select([component.c.rank]).where(getattr(component.c, cls.ranked_in) == ranked_in_value).order_by(component.c.rank.desc())
  227. c = dbe.connect()
  228. res = c.execute(req)
  229. res = res.fetchone()
  230. c.close()
  231. if res != None:
  232. return res['rank']
  233. else:
  234. return -1
  235. ## Set a new rank for this component
  236. # @note This function assume that ranks are properly set from 1 to x with no gap
  237. # @param new_rank int: The new rank
  238. # @return True if success False if not
  239. # @throw TypeError If bad argument type
  240. # @throw ValueError if out of bound value
  241. def set_rank(self, new_rank):
  242. if not isinstance(new_rank, int):
  243. raise TypeError("Excepted <class int> but got "+str(type(new_rank)))
  244. if new_rank < 0 or new_rank > self.get_max_rank(getattr(self, self.ranked_in)):
  245. raise ValueError("Invalid new rank : "+str(new_rank))
  246. mod = new_rank - self.rank #Allow to know the "direction" of the "move"
  247. if mod == 0: #No modifications
  248. return True
  249. limits = [ self.rank + ( 1 if mod > 0 else -1), new_rank ] #The range of modified ranks
  250. limits.sort()
  251. dbe = self.db_engine()
  252. conn = dbe.connect()
  253. table = sqlutils.get_table(self.__class__)
  254. #Selecting the components that will be modified
  255. 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])
  256. res = conn.execute(req)
  257. if not res: #Db error... Maybe false is a bit silent for a failuer
  258. return False
  259. rows = res.fetchall()
  260. updated_ranks = [{'b_uid': self.uid, 'b_rank': new_rank}]
  261. for row in rows:
  262. updated_ranks.append({'b_uid': row['uid'], 'b_rank': row['rank'] + (-1 if mod > 0 else 1)})
  263. req = table.update().where(table.c.uid == sql.bindparam('b_uid')).values(rank=sql.bindparam('b_rank'))
  264. res = conn.execute(req, updated_ranks)
  265. conn.close()
  266. if res:
  267. #Instance rank update
  268. self._fields['rank'].value = new_rank
  269. return bool(res)
  270. ## @brief Modify a rank given a sign and a new_rank
  271. # - If sign is '=' set the rank to new_rank
  272. # - If sign is '-' set the rank to cur_rank - new_rank
  273. # - If sign is '+' set the rank to cur_rank + new_rank
  274. # @param new_rank int: The new_rank or rank modifier
  275. # @param sign str: Can be one of '=', '+', '-'
  276. # @return True if success False if fails
  277. # @throw TypeError If bad argument type
  278. # @throw ValueError if out of bound value
  279. def modify_rank(self,new_rank, sign='='):
  280. if not isinstance(new_rank, int) or not isinstance(sign, str):
  281. raise TypeError("Excepted <class int>, <class str>. But got "+str(type(new_rank))+", "+str(type(sign)))
  282. if sign == '+':
  283. return self.set_rank(self.rank + new_rank)
  284. elif sign == '-':
  285. return self.set_rank(self.rank - new_rank)
  286. elif sign == '=':
  287. return self.set_rank(new_rank)
  288. else:
  289. raise ValueError("Excepted one of '=', '+', '-' for sign argument, but got "+sign)
  290. ## @brief Return a string representation of the component
  291. # @return A string representation of the component
  292. def __repr__(self):
  293. if self.name is None:
  294. return "<%s #%s, 'non populated'>" % (type(self).__name__, self.uid)
  295. else:
  296. return "<%s #%s, '%s'>" % (type(self).__name__, self.uid, self.name)
  297. @classmethod
  298. ## Register a new component in UID table
  299. #
  300. # Use the class property table
  301. # @return A new uid (an integer)
  302. def new_uid(cls):
  303. if cls.table is None:
  304. raise NotImplementedError("Abstract method")
  305. dbe = cls.db_engine()
  306. uidtable = sql.Table('uids', sqlutils.meta(dbe))
  307. conn = dbe.connect()
  308. req = uidtable.insert(values={'table': cls.table})
  309. res = conn.execute(req)
  310. uid = res.inserted_primary_key[0]
  311. logger.debug("Registering a new UID '" + str(uid) + "' for '" + cls.table + "' component")
  312. conn.close()
  313. return uid
  314. ## @brief An exception class to tell that a component don't exist
  315. class EmComponentNotExistError(Exception):
  316. pass
  317. ## @brief Raised on uniq constraint error at creation
  318. # This exception class is dedicated to be raised when create() method is called
  319. # if an EmComponent with this name but different parameters allready exist
  320. class EmComponentExistError(Exception):
  321. pass
  322. ## @brief An exception class to tell that no ranking exist yet for the group of the object
  323. class EmComponentRankingNotExistError(Exception):
  324. pass