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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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. from Lodel.utils.mlstring import MlString
  6. import logging
  7. import sqlalchemy as sql
  8. from Database import sqlutils
  9. import EditorialModel.fieldtypes as ftypes
  10. from collections import OrderedDict
  11. logger = logging.getLogger('Lodel2.EditorialModel')
  12. ## This class is the mother class of all editorial model objects
  13. #
  14. # It gather all the properties and mechanism that are common to every editorial model objects
  15. # @see EditorialModel::classes::EmClass, EditorialModel::types::EmType, EditorialModel::fieldgroups::EmFieldGroup, EditorialModel::fields::EmField
  16. # @pure
  17. class EmComponent(object):
  18. ## The name of the engine configuration
  19. # @todo Not a good idea to store it here
  20. dbconf = 'default'
  21. ## The table in wich we store data for this object
  22. # None for EmComponent
  23. table = None
  24. ## Used by EmComponent::modify_rank
  25. ranked_in = None
  26. ##Read only properties
  27. _ro_properties = [ 'date_update', 'date_create', 'uid', 'rank']
  28. ## Instaciate an EmComponent
  29. # @param id_or_name int|str: name or id of the object
  30. # @throw TypeError if id_or_name is not an integer nor a string
  31. # @throw NotImplementedError if called with EmComponent
  32. def __init__(self, id_or_name):
  33. if type(self) is EmComponent:
  34. raise NotImplementedError('Abstract class')
  35. # data fields of the object
  36. self._fields = OrderedDict([('uid', ftypes.EmField_integer()), ('name', ftypes.EmField_char()), ('rank', ftypes.EmField_integer()), ('date_update', ftypes.EmField_date()), ('date_create', ftypes.EmField_date()), ('string', ftypes.EmField_mlstring()), ('help', ftypes.EmField_mlstring())] + self._fields)
  37. # populate
  38. if isinstance(id_or_name, int):
  39. self._fields['uid'].value = id_or_name #read only propertie set
  40. elif isinstance(id_or_name, str):
  41. self.name = id_or_name
  42. else:
  43. raise TypeError('Bad argument: expecting <int> or <str> but got : '+str(type(id_or_name)))
  44. self.populate()
  45. ## Access values of data fields from the object properties
  46. # @param name str: The propertie name
  47. # @throw AttributeError if attribute don't exists
  48. def __getattr__(self, name):
  49. if name != '_fields' and name in self._fields:
  50. return self._fields[name].value
  51. raise AttributeError('Error unknown attribute : '+name)
  52. ## Set values of data fields from the object properties
  53. # @param name str: The propertie name
  54. # @param value *: The value
  55. def __setattr__(self, name, value):
  56. if name in self.__class__._ro_properties:
  57. raise TypeError("Propertie '"+name+"' is readonly")
  58. if name != '_fields' and hasattr(self, '_fields') and name in object.__getattribute__(self, '_fields'):
  59. self._fields[name].from_python(value)
  60. else:
  61. object.__setattr__(self, name, value)
  62. ## Lookup in the database properties of the object to populate the properties
  63. # @throw EmComponentNotExistError if the instance is not anymore stored in database
  64. def populate(self):
  65. records = self._populateDb() #Db query
  66. for record in records:
  67. for keys in self._fields.keys():
  68. if keys in record:
  69. self._fields[keys].from_string(record[keys])
  70. @classmethod
  71. ## Shortcut that return the sqlAlchemy engine
  72. def getDbE(c):
  73. return sqlutils.getEngine(c.dbconf)
  74. ## Do the query on the database for EmComponent::populate()
  75. # @throw EmComponentNotExistError if the instance is not anymore stored in database
  76. def _populateDb(self):
  77. dbe = self.__class__.getDbE()
  78. component = sql.Table(self.table, sqlutils.meta(dbe))
  79. req = sql.sql.select([component])
  80. if self.uid == None:
  81. req = req.where(component.c.name == self.name)
  82. else:
  83. req = req.where(component.c.uid == self.uid)
  84. c = dbe.connect()
  85. res = c.execute(req)
  86. res = res.fetchall()
  87. c.close()
  88. if not res or len(res) == 0:
  89. raise EmComponentNotExistError("No component found with "+('name ' + self.name if self.uid == None else 'uid ' + str(self.uid) ))
  90. return res
  91. ## Insert a new component in the database
  92. #
  93. # This function create and assign a new UID and handle the date_create and date_update values
  94. #
  95. # @param **kwargs : Names arguments representing object properties
  96. # @return An instance of the created component
  97. # @throw TypeError if an element of kwargs isn't a valid object propertie or if a mandatory argument is missing
  98. #
  99. # @todo Check that the query didn't failed
  100. # @todo Check that every mandatory _fields are given in args
  101. # @todo Put a real rank at creation
  102. # @todo Stop using datetime.datetime.utcnow() for date_update and date_create init
  103. @classmethod
  104. def create(cl, **kwargs):
  105. for argname in kwargs:
  106. if argname in ['date_update', 'date_create', 'rank', 'uid']: #Automatic properties
  107. raise TypeError("Invalid argument : "+argname)
  108. #TODO check that every mandatory _fields are here like below for example
  109. #for name in cl._fields:
  110. # if cl._fields[name].notNull and cl._fields[name].default == None:
  111. # raise TypeError("Missing argument : "+name)
  112. kwargs['uid'] = cl.newUid()
  113. kwargs['date_update'] = kwargs['date_create'] = datetime.datetime.utcnow()
  114. dbe = cl.getDbE()
  115. conn = dbe.connect()
  116. kwargs['rank'] = -1 #Warning !!!
  117. table = sql.Table(cl.table, sqlutils.meta(dbe))
  118. req = table.insert(kwargs)
  119. res = conn.execute(req) #Check res?
  120. conn.close()
  121. return cl(kwargs['name']) #Maybe no need to check res because this would fail if the query failed
  122. ## Write the representation of the component in the database
  123. # @return bool
  124. # @todo stop using datetime.datetime.utcnow() for date_update update
  125. def save(self):
  126. values = {}
  127. for name, field in self._fields.items():
  128. values[name] = field.to_sql()
  129. #Don't allow creation date overwritting
  130. """
  131. if 'date_create' in values:
  132. del values['date_create']
  133. logger.warning("date_create supplied for save, but overwritting of date_create not allowed, the date will not be changed")
  134. """
  135. values['date_update'] = datetime.datetime.utcnow()
  136. self._saveDb(values)
  137. ## Do the query in the datbase for EmComponent::save()
  138. # @param values dict: A dictionnary of the values to insert
  139. # @throw RunTimeError if it was unable to do the Db update
  140. def _saveDb(self, values):
  141. """ Do the query on the db """
  142. dbe = self.__class__.getDbE()
  143. component = sql.Table(self.table, sqlutils.meta(dbe))
  144. req = sql.update(component, values = values).where(component.c.uid == self.uid)
  145. c = dbe.connect()
  146. res = c.execute(req)
  147. c.close()
  148. if not res:
  149. raise RuntimeError("Unable to save the component in the database")
  150. ## Delete this component data in the database
  151. # @return bool
  152. # @todo Use something like __del__ instead (or call it at the end)
  153. # @throw RunTimeError if it was unable to do the deletion
  154. def delete(self):
  155. #<SQL>
  156. dbe = self.__class__.getDbE()
  157. component = sql.Table(self.table, sqlutils.meta(dbe))
  158. req= component.delete().where(component.c.uid == self.uid)
  159. c = dbe.connect()
  160. res = c.execute(req)
  161. c.close
  162. if not res:
  163. raise RuntimeError("Unable to delete the component in the database")
  164. #</SQL>
  165. pass
  166. ## modify_rank
  167. #
  168. # 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é.
  169. #
  170. # @param new_rank int: le rank ou modificateur de rank
  171. # @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.
  172. #
  173. # @return bool: True en cas de réussite False en cas d'echec.
  174. # @throw TypeError if an argument isn't from the expected type
  175. # @thrown ValueError if an argument as a wrong value but is of the good type
  176. def modify_rank(self, new_rank, sign = '='):
  177. if(type(new_rank) is int):
  178. if(new_rank >= 0):
  179. dbe = self.__class__.getDbE()
  180. component = sql.Table(self.table, sqlutils.meta(dbe))
  181. req = sql.sql.select([component.c.uid, component.c.rank])
  182. if(type(sign) is not str):
  183. logger.error("Bad argument")
  184. raise TypeError('Excepted a string (\'=\' or \'+\' or \'-\') not a '+str(type(sign)))
  185. if(sign == '='):
  186. req = sql.sql.select([component.c.uid, component.c.rank])
  187. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank == new_rank))
  188. c = dbe.connect()
  189. res = c.execute(req)
  190. res = res.fetchone()
  191. c.close()
  192. if(res != None):
  193. if(new_rank < self.rank):
  194. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & ( component.c.rank >= new_rank) & (component.c.rank < self.rank))
  195. else:
  196. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank <= new_rank) & (component.c.rank > self.rank))
  197. c = dbe.connect()
  198. res = c.execute(req)
  199. res = res.fetchall()
  200. vals = list()
  201. vals.append({'id' : self.uid, 'rank' : new_rank})
  202. for row in res:
  203. if(new_rank < self.rank):
  204. vals.append({'id' : row.uid, 'rank' : row.rank+1})
  205. else:
  206. vals.append({'id' : row.uid, 'rank' : row.rank-1})
  207. req = component.update().where(component.c.uid == sql.bindparam('id')).values(rank = sql.bindparam('rank'))
  208. c.execute(req, vals)
  209. c.close()
  210. self._fields['rank'].value = new_rank
  211. else:
  212. logger.error("Bad argument")
  213. raise ValueError('new_rank to big, new_rank - 1 doesn\'t exist. new_rank = '+str((new_rank)))
  214. elif(sign == '+'):
  215. req = sql.sql.select([component.c.uid, component.c.rank])
  216. req = req.where((getattr(component.c, self.ranked_in) == getattr(self, self.ranked_in)) & (component.c.rank == self.rank + new_rank))
  217. c = dbe.connect()
  218. res = c.execute(req)
  219. res = res.fetchone()
  220. c.close()
  221. if(res != None):
  222. if(new_rank != 0):
  223. 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))
  224. c = dbe.connect()
  225. res = c.execute(req)
  226. res = res.fetchall()
  227. vals = list()
  228. vals.append({'id' : self.uid, 'rank' : self.rank + new_rank})
  229. for row in res:
  230. vals.append({'id' : row.uid, 'rank' : row.rank - 1})
  231. req = component.update().where(component.c.uid == sql.bindparam('id')).values(rank = sql.bindparam('rank'))
  232. c.execute(req, vals)
  233. c.close()
  234. self._fields['rank'] += new_rank
  235. else:
  236. logger.error("Bad argument")
  237. raise ValueError('Excepted a positive int not a null. new_rank = '+str((new_rank)))
  238. else:
  239. logger.error("Bad argument")
  240. raise ValueError('new_rank to big, rank + new rank doesn\'t exist. new_rank = '+str((new_rank)))
  241. elif(sign == '-'):
  242. if((self.rank + new_rank) > 0):
  243. if(new_rank != 0):
  244. 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))
  245. c = dbe.connect()
  246. res = c.execute(req)
  247. res = res.fetchall()
  248. vals = list()
  249. vals.append({'id' : self.uid, 'rank' : self.rank - new_rank})
  250. for row in res:
  251. vals.append({'id' : row.uid, 'rank' : row.rank + 1})
  252. req = component.update().where(component.c.uid == sql.bindparam('id')).values(rank = sql.bindparam('rank'))
  253. c.execute(req, vals)
  254. c.close()
  255. self._fields['rank'] -= new_rank
  256. else:
  257. logger.error("Bad argument")
  258. raise ValueError('Excepted a positive int not a null. new_rank = '+str((new_rank)))
  259. else:
  260. logger.error("Bad argument")
  261. raise ValueError('new_rank to big, rank - new rank is negative. new_rank = '+str((new_rank)))
  262. else:
  263. logger.error("Bad argument")
  264. raise ValueError('Excepted a string (\'=\' or \'+\' or \'-\') not a '+str((sign)))
  265. else:
  266. logger.error("Bad argument")
  267. raise ValueError('Excepted a positive int not a negative. new_rank = '+str((new_rank)))
  268. else:
  269. logger.error("Bad argument")
  270. raise TypeError('Excepted a int not a '+str(type(new_rank)))
  271. def __repr__(self):
  272. if self.name is None:
  273. return "<%s #%s, 'non populated'>" % (type(self).__name__, self.uid)
  274. else:
  275. return "<%s #%s, '%s'>" % (type(self).__name__, self.uid, self.name)
  276. @classmethod
  277. ## Register a new component in UID table
  278. #
  279. # Use the class property table
  280. # @return A new uid (an integer)
  281. def newUid(cl):
  282. if cl.table == None:
  283. raise NotImplementedError("Abstract method")
  284. dbe = cl.getDbE()
  285. uidtable = sql.Table('uids', sqlutils.meta(dbe))
  286. conn = dbe.connect()
  287. req = uidtable.insert(values={'table':cl.table})
  288. res = conn.execute(req)
  289. uid = res.inserted_primary_key[0]
  290. logger.debug("Registering a new UID '"+str(uid)+"' for '"+cl.table+"' component")
  291. conn.close()
  292. return uid
  293. ## An exception class to tell that a component don't exist
  294. class EmComponentNotExistError(Exception):
  295. pass