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

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