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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. ## Lookup in the database properties of the object to populate the properties
  94. # @throw EmComponentNotExistError if the instance is not anymore stored in database
  95. def populate(self):
  96. records = self._populate_db() # Db query
  97. for record in records:
  98. for keys in self._fields.keys():
  99. if keys in record:
  100. self._fields[keys].from_string(record[keys])
  101. super(EmComponent, self).__setattr__('deleted', False)
  102. @classmethod
  103. ## Shortcut that return the sqlAlchemy engine
  104. def db_engine(cls):
  105. return sqlutils.getEngine(cls.dbconf)
  106. ## Do the query on the database for EmComponent::populate()
  107. # @throw EmComponentNotExistError if the instance is not anymore stored in database
  108. def _populate_db(self):
  109. dbe = self.__class__.db_engine()
  110. component = sql.Table(self.table, sqlutils.meta(dbe))
  111. req = sql.sql.select([component])
  112. if self.uid is None:
  113. req = req.where(component.c.name == self.name)
  114. else:
  115. req = req.where(component.c.uid == self.uid)
  116. conn = dbe.connect()
  117. res = conn.execute(req)
  118. res = res.fetchall()
  119. conn.close()
  120. if not res or len(res) == 0:
  121. raise EmComponentNotExistError("No " + self.__class__.__name__ + " found with " + ('name ' + self.name if self.uid is None else 'uid ' + str(self.uid)))
  122. return res
  123. ## Insert a new component in the database
  124. #
  125. # This function create and assign a new UID and handle the date_create and date_update values
  126. #
  127. # @param **kwargs : Names arguments representing object properties
  128. # @return An instance of the created component
  129. # @throw TypeError if an element of kwargs isn't a valid object propertie or if a mandatory argument is missing
  130. #
  131. # @todo Check that the query didn't failed
  132. # @todo Check that every mandatory _fields are given in args
  133. # @todo Put a real rank at creation
  134. # @todo Stop using datetime.datetime.utcnow() for date_update and date_create init
  135. @classmethod
  136. def create(cls, **kwargs):
  137. for argname in kwargs:
  138. if argname in ['date_update', 'date_create', 'rank', 'uid']: # Automatic properties
  139. raise TypeError("Invalid argument : " + argname)
  140. #TODO check that every mandatory _fields are here like below for example
  141. #for name in cls._fields:
  142. # if cls._fields[name].notNull and cls._fields[name].default == None:
  143. # raise TypeError("Missing argument : "+name)
  144. kwargs['uid'] = cls.new_uid()
  145. kwargs['date_update'] = kwargs['date_create'] = datetime.datetime.utcnow()
  146. dbe = cls.db_engine()
  147. conn = dbe.connect()
  148. #kwargs['rank'] = cls.get_max_rank(kwargs[cls.ranked_in]) + 1 #Warning !!!
  149. kwargs['rank'] = -1
  150. table = sql.Table(cls.table, sqlutils.meta(dbe))
  151. req = table.insert(kwargs)
  152. conn.execute(req) # Check ?
  153. conn.close()
  154. return cls(kwargs['name']) # Maybe no need to check res because this would fail if the query failed
  155. ## Write the representation of the component in the database
  156. # @return bool
  157. # @todo stop using datetime.datetime.utcnow() for date_update update
  158. def save(self):
  159. values = {}
  160. for name, field in self._fields.items():
  161. values[name] = field.to_sql()
  162. # Don't allow creation date overwritting
  163. #if 'date_create' in values:
  164. #del values['date_create']
  165. #logger.warning("date_create supplied for save, but overwritting of date_create not allowed, the date will not be changed")
  166. values['date_update'] = datetime.datetime.utcnow()
  167. self._save_db(values)
  168. ## Do the query in the datbase for EmComponent::save()
  169. # @param values dict: A dictionnary of the values to insert
  170. # @throw RunTimeError if it was unable to do the Db update
  171. def _save_db(self, values):
  172. """ Do the query on the db """
  173. dbe = self.__class__.db_engine()
  174. component = sql.Table(self.table, sqlutils.meta(dbe))
  175. req = sql.update(component, values=values).where(component.c.uid == self.uid)
  176. conn = dbe.connect()
  177. res = conn.execute(req)
  178. conn.close()
  179. if not res:
  180. raise RuntimeError("Unable to save the component in the database")
  181. ## Delete this component data in the database
  182. # @return bool : True if deleted False if deletion aborded
  183. # @todo Use something like __del__ instead (or call it at the end)
  184. # @throw RunTimeError if it was unable to do the deletion
  185. def delete(self):
  186. #<SQL>
  187. dbe = self.__class__.db_engine()
  188. component = sql.Table(self.table, sqlutils.meta(dbe))
  189. req = component.delete().where(component.c.uid == self.uid)
  190. conn = dbe.connect()
  191. res = conn.execute(req)
  192. conn.close()
  193. if not res:
  194. raise RuntimeError("Unable to delete the component in the database")
  195. super(EmComponent, self).__setattr__('deleted', True)
  196. #</SQL>
  197. return True
  198. ## get_max_rank
  199. # Retourne le rank le plus élevé pour le groupe de component au quel apartient l'objet actuelle
  200. #return int
  201. @classmethod
  202. def get_max_rank(cls, ranked_in_value):
  203. dbe = cls.getDbE()
  204. component = sql.Table(cls.table, sqlutils.meta(dbe))
  205. req = sql.sql.select([component.c.rank]).where(getattr(component.c, cls.ranked_in) == ranked_in_value).order_by(component.c.rank.desc())
  206. c = dbe.connect()
  207. res = c.execute(req)
  208. res = res.fetchone()
  209. c.close()
  210. if(res != None):
  211. return res['rank']
  212. else:
  213. return -1
  214. #logger.error("Bad argument")
  215. #raise EmComponentRankingNotExistError('The ranking of the component named : ' + self.name + 'is empty')
  216. ## modify_rank
  217. #
  218. # Permet de changer le rank d'un component, soit en lui donnant un rank précis, soit en augmentant ou reduisant sont rank actuelle d'une valleur donné.
  219. #
  220. # @param new_rank int: le rank ou modificateur de rank
  221. # @param sign str: Un charactère qui peut être : '=' pour afecter un rank, '+' pour ajouter le modificateur de rank ou '-' pour soustraire le modificateur de rank.
  222. #
  223. # @return bool: True en cas de réussite False en cas d'echec.
  224. # @throw TypeError if an argument isn't from the expected type
  225. # @thrown ValueError if an argument as a wrong value but is of the good type
  226. def modify_rank(self, new_rank, sign='='):
  227. if isinstance(new_rank, int):
  228. if (new_rank >= 0):
  229. dbe = self.__class__.db_engine()
  230. component = sql.Table(self.table, sqlutils.meta(dbe))
  231. req = sql.sql.select([component.c.uid, component.c.rank])
  232. if (type(sign) is not str):
  233. logger.error("Bad argument")
  234. raise TypeError('Excepted a string (\'=\' or \'+\' or \'-\') not a ' + str(type(sign)))
  235. if (sign == '='):
  236. req = sql.sql.select([component.c.uid, component.c.rank])
  237. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank == new_rank))
  238. conn = dbe.connect()
  239. res = conn.execute(req)
  240. res = res.fetchone()
  241. conn.close()
  242. if (res is not None):
  243. req = sql.sql.select([component.c.uid, component.c.rank])
  244. if(new_rank < self.rank):
  245. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank >= new_rank) & (component.c.rank < self.rank))
  246. else:
  247. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank <= new_rank) & (component.c.rank > self.rank))
  248. conn = dbe.connect()
  249. res = conn.execute(req)
  250. res = res.fetchall()
  251. vals = list()
  252. vals.append({'id': self.uid, 'rank': new_rank})
  253. for row in res:
  254. if(new_rank < self.rank):
  255. vals.append({'id': row.uid, 'rank': row.rank + 1})
  256. else:
  257. vals.append({'id': row.uid, 'rank': row.rank - 1})
  258. req = component.update().where(component.c.uid == sql.bindparam('id')).values(rank=sql.bindparam('rank'))
  259. conn.execute(req, vals)
  260. conn.close()
  261. self._fields['rank'].value = new_rank
  262. else:
  263. logger.error("Bad argument")
  264. raise ValueError('new_rank to big, new_rank - 1 doesn\'t exist. new_rank = '+str((new_rank)))
  265. elif(sign == '+' and self.rank + new_rank <= self.__class__.get_max_rank(getattr(self, self.__class__.ranked_in))):
  266. req = sql.sql.select([component.c.uid, component.c.rank])
  267. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank == self.rank + new_rank))
  268. conn = dbe.connect()
  269. res = conn.execute(req)
  270. res = res.fetchone()
  271. conn.close()
  272. if (res is not None):
  273. if (new_rank != 0):
  274. req = sql.sql.select([component.c.uid, component.c.rank])
  275. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank <= self.rank + new_rank) & (component.c.rank > self.rank))
  276. conn = dbe.connect()
  277. res = conn.execute(req)
  278. res = res.fetchall()
  279. vals = list()
  280. vals.append({'id': self.uid, 'rank': self.rank + new_rank})
  281. for row in res:
  282. vals.append({'id': row.uid, 'rank': row.rank - 1})
  283. req = component.update().where(component.c.uid == sql.bindparam('id')).values(rank=sql.bindparam('rank'))
  284. conn.execute(req, vals)
  285. conn.close()
  286. self._fields['rank'] += new_rank
  287. else:
  288. logger.error("Bad argument")
  289. raise ValueError('Excepted a positive int not a null. new_rank = ' + str((new_rank)))
  290. else:
  291. logger.error("Bad argument")
  292. raise ValueError('new_rank to big, rank + new rank doesn\'t exist. new_rank = ' + str((new_rank)))
  293. elif (sign == '-' and self.rank - new_rank >= 0):
  294. if ((self.rank + new_rank) > 0):
  295. if (new_rank != 0):
  296. req = sql.sql.select([component.c.uid, component.c.rank])
  297. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank >= self.rank - new_rank) & (component.c.rank < self.rank))
  298. conn = dbe.connect()
  299. res = conn.execute(req)
  300. res = res.fetchall()
  301. vals = list()
  302. vals.append({'id': self.uid, 'rank': self.rank - new_rank})
  303. for row in res:
  304. vals.append({'id': row.uid, 'rank': row.rank + 1})
  305. req = component.update().where(component.c.uid == sql.bindparam('id')).values(rank=sql.bindparam('rank'))
  306. conn.execute(req, vals)
  307. conn.close()
  308. self._fields['rank'] -= new_rank
  309. else:
  310. logger.error("Bad argument")
  311. raise ValueError('Excepted a positive int not a null. new_rank = ' + str((new_rank)))
  312. else:
  313. logger.error("Bad argument")
  314. raise ValueError('new_rank to big, rank - new rank is negative. new_rank = ' + str((new_rank)))
  315. else:
  316. logger.error("Bad argument")
  317. raise ValueError('Excepted a string (\'=\' or \'+\' or \'-\') not a ' + str((sign)))
  318. else:
  319. logger.error("Bad argument")
  320. raise ValueError('Excepted a positive int not a negative. new_rank = ' + str((new_rank)))
  321. else:
  322. logger.error("Bad argument")
  323. raise TypeError('Excepted a int not a ' + str(type(new_rank)))
  324. def __repr__(self):
  325. if self.name is None:
  326. return "<%s #%s, 'non populated'>" % (type(self).__name__, self.uid)
  327. else:
  328. return "<%s #%s, '%s'>" % (type(self).__name__, self.uid, self.name)
  329. @classmethod
  330. ## Register a new component in UID table
  331. #
  332. # Use the class property table
  333. # @return A new uid (an integer)
  334. def new_uid(cls):
  335. if cls.table is None:
  336. raise NotImplementedError("Abstract method")
  337. dbe = cls.db_engine()
  338. uidtable = sql.Table('uids', sqlutils.meta(dbe))
  339. conn = dbe.connect()
  340. req = uidtable.insert(values={'table': cls.table})
  341. res = conn.execute(req)
  342. uid = res.inserted_primary_key[0]
  343. logger.debug("Registering a new UID '" + str(uid) + "' for '" + cls.table + "' component")
  344. conn.close()
  345. return uid
  346. ## @brief An exception class to tell that a component don't exist
  347. class EmComponentNotExistError(Exception):
  348. pass
  349. ## @brief An exception class to tell that no ranking exist yet for the group of the object
  350. class EmComponentRankingNotExistError(Exception):
  351. pass