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.

lecrud.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. #-*- coding: utf-8 -*-
  2. ## @package leapi.lecrud
  3. # @brief This package contains the abstract class representing Lodel Editorial components
  4. #
  5. import warnings
  6. import importlib
  7. import re
  8. import leapi.leobject
  9. import EditorialModel.fieldtypes.generic
  10. ## @brief When an error concern a query
  11. class LeApiQueryError(EditorialModel.fieldtypes.generic.FieldTypeDataCheckError): pass
  12. ## @brief When an error concerns a datas
  13. class LeApiDataCheckError(EditorialModel.fieldtypes.generic.FieldTypeDataCheckError): pass
  14. ## @brief Main class to handler lodel editorial components (relations and objects)
  15. class _LeCrud(object):
  16. ## @brief The datasource
  17. _datasource = None
  18. ## @brief abstract property to store the fieldtype representing the component identifier
  19. _uid_fieldtype = None #Will be a dict fieldname => fieldtype
  20. ## @brief will store all the fieldtypes (child classes handle it)
  21. _fieldtypes_all = None
  22. ## @brief Stores a regular expression to parse query filters strings
  23. _query_re = None
  24. ## @brief Stores Query filters operators
  25. _query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ']
  26. def __init__(self):
  27. raise NotImplementedError("Abstract class")
  28. ## @brief Given a dynamically generated class name return the corresponding python Class
  29. # @param name str : a concrete class name
  30. # @return False if no such component
  31. @classmethod
  32. def name2class(cls, name):
  33. mod = importlib.import_module(cls.__module__)
  34. try:
  35. return getattr(mod, name)
  36. except AttributeError:
  37. return False
  38. ## @return LeObject class
  39. @classmethod
  40. def leobject(cls):
  41. return cls.name2class('LeObject')
  42. ## @return A dict with key field name and value a fieldtype instance
  43. @classmethod
  44. def fieldtypes(cls):
  45. raise NotImplementedError("Abstract method") #child classes should return their uid fieldtype
  46. ## @return A dict with fieldtypes marked as internal
  47. @classmethod
  48. def fieldtypes_internal(self):
  49. return { fname: ft for fname, ft in cls.fieldtypes().items() if hasattr(ft, 'internal') and ft.internal }
  50. ## @return A list of field name
  51. @classmethod
  52. def fieldlist(cls):
  53. return cls.fieldtypes().keys()
  54. ## @brief Update a component in DB
  55. # @param datas dict : If None use instance attributes to update de DB
  56. # @return True if success
  57. # @todo better error handling
  58. def update(self, datas = None):
  59. datas = self.datas(internal=False) if datas is None else datas
  60. upd_datas = self.prepare_datas(datas, complete = False, allow_internal = False)
  61. filters = [self._id_filter()]
  62. rel_filters = []
  63. ret = self._datasource.update(self.__class__, filters, rel_filters, upd_datas)
  64. if ret == 1:
  65. return True
  66. else:
  67. #ERROR HANDLING
  68. return False
  69. ## @brief Delete a component
  70. # @return True if success
  71. # @todo better error handling
  72. def delete(self):
  73. filters = [self._id_filter()]
  74. rel_filters = []
  75. ret = self._datasource.delete(self.__class__, filters, rel_filters)
  76. if ret == 1:
  77. return True
  78. else:
  79. #ERROR HANDLING
  80. return False
  81. ## @brief Check that datas are valid for this type
  82. # @param datas dict : key == field name value are field values
  83. # @param complete bool : if True expect that datas provide values for all non internal fields
  84. # @param allow_internal bool : if True don't raise an error if a field is internal
  85. # @return Checked datas
  86. # @throw LeApiDataCheckError if errors reported during check
  87. @classmethod
  88. def check_datas_value(cls, datas, complete = False, allow_internal = True):
  89. err_l = [] #Stores errors
  90. correct = [] #Valid fields name
  91. mandatory = [] #mandatory fields name
  92. for fname, ftt in cls.fieldtypes().items():
  93. if allow_internal and not ftt.is_internal():
  94. correct.append(fname)
  95. if complete and not hasattr(ftt, 'default'):
  96. mandatory.append(fname)
  97. mandatory = set(mandatory)
  98. correct = set(correct)
  99. provided = set(datas.keys())
  100. #searching unknow fields
  101. unknown = provided - correct
  102. for u_f in unknown:
  103. #here we can check if the field is unknown or rejected because it is internal
  104. err_l.append(AttributeError("Unknown or unauthorized field '%s'"%u_f))
  105. #searching missings fields
  106. missings = mandatory - provided
  107. for miss_field in missings:
  108. err_l.append(AttributeError("The data for field '%s' is missing"%miss_field))
  109. #Checks datas
  110. checked_datas = dict()
  111. for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
  112. checked_datas[name], err = cls.fieldtypes()[name].check_data_value(value)
  113. if err:
  114. err_l.append(err)
  115. if len(err_l) > 0:
  116. raise LeApiDataCheckError("The argument complete was %s but some fields are missing : %s" % (complete, err_l))
  117. return checked_datas
  118. ## @brief Retrieve a collection of lodel editorial components
  119. #
  120. # @param query_filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
  121. # @param field_list list|None : list of string representing fields see @ref leobject_filters
  122. # @return A list of lodel editorial components instance
  123. # @todo think about LeObject and LeClass instanciation (partial instanciation, etc)
  124. @classmethod
  125. def get(cls, query_filters, field_list = None):
  126. if not(isinstance(cls, cls.name2class('LeObject'))) and not(isinstance(cls, cls.name2class('LeRelation'))):
  127. raise NotImplementedError("Cannot call get with LeCrud")
  128. if field_list is None or len(field_list) == 0:
  129. #default field_list
  130. field_list = cls.default_fieldlist()
  131. field_list = cls.prepare_field_list(field_list) #Can raise LeApiDataCheckError
  132. #preparing filters
  133. filters, relational_filters = cls._prepare_filters(field_list)
  134. #Fetching datas from datasource
  135. db_datas = cls._datasource.get(cls, filters, relational_filters)
  136. return [ cls(**datas) for datas in db_datas]
  137. ## @brief Given filters delete components
  138. # @param filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
  139. # @return the number of deleted components
  140. # @todo Check for Abstract calls (with cls == LeCrud)
  141. @classmethod
  142. def delete_multi(cls, filters):
  143. filters, rel_filters = cls._prepare_filters(filters)
  144. return cls._datasource.delete(cls, filters, rel_filters)
  145. ## @brief Insert a new component
  146. # @param datas dict : The value of object we want to insert
  147. # @return A new id if success else False
  148. @classmethod
  149. def insert(cls, datas):
  150. insert_datas = self.prepare_datas(datas, complete = True, allow_internal = False)
  151. return cls._datasource.insert(cls, insert_datas)
  152. ## @brief Check and prepare datas
  153. #
  154. # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
  155. #
  156. # @param datas dict : {fieldname : fieldvalue, ...}
  157. # @param complete bool : If True you MUST give all the datas
  158. # @param allow_internal : Wether or not interal fields are expected in datas
  159. # @return Datas ready for use
  160. @classmethod
  161. def prepare_datas(cls, datas, complete = False, allow_internal = True):
  162. if not complete:
  163. warnings.warn("Actual implementation can make datas construction and consitency checks fails when datas are not complete")
  164. ret_dats = self.check_datas_value(cls, datas, complete, allow_internal)
  165. ret_datas = self._construct_datas(cls, ret_datas)
  166. ret_datas = self._check_data_consistency(cls, ret_datas)
  167. return ret_datas
  168. #-###################-#
  169. # Private methods #
  170. #-###################-#
  171. ## @brief Build a filter to select an object with a specific ID
  172. # @warning assert that the uid is not composed with multiple fieldtypes
  173. # @return A filter of the form tuple(UID, '=', self.UID)
  174. def _id_filter(self):
  175. id_name = self._uid_fieldtype.keys()[0]
  176. return ( id_name, '=', getattr(self, id_name) )
  177. ## @brief Construct datas values
  178. #
  179. # @warning assert that datas is complete
  180. #
  181. # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
  182. # @return A new dict of datas
  183. # @todo Decide wether or not the datas are modifed inplace or returned in a new dict (second solution for the moment)
  184. @classmethod
  185. def _construct_datas(cls, datas):
  186. res_datas = dict()
  187. for fname, ftype in cls.fieldtypes().items():
  188. if fname in datas:
  189. res_datas[fname] = ftype.construct_data(datas)
  190. return res_datas
  191. ## @brief Check datas consistency
  192. # @warning assert that datas is complete
  193. #
  194. # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
  195. # @throw LeApiDataCheckError if fails
  196. @classmethod
  197. def _check_datas_consistency(cls, datas):
  198. err_l = []
  199. for fname, ftype in cls.fieldtypes().items():
  200. ret = ftype.check_data_consistency(datas)
  201. if isinstance(ret, Exception):
  202. err_l.append(ret)
  203. if len(err_l) > 0:
  204. raise LeApiDataCheckError("Datas consistency checks fails", err_l)
  205. ## @brief Prepare a field_list
  206. # @param field_list list : List of string representing fields
  207. # @return A well formated field list
  208. # @throw LeApiDataCheckError if invalid field given
  209. @classmethod
  210. def _prepare_field_list(cls, field_list):
  211. ret_field_list = list()
  212. for field in field_list:
  213. if cls._field_is_relational(field):
  214. ret = cls._prepare_relational_field(field)
  215. else:
  216. ret = cls._check_field(field)
  217. if isinstance(ret, Exception):
  218. err_l.append(ret)
  219. else:
  220. ret_field_list.append(ret)
  221. if len(err_l) > 0:
  222. raise LeApiDataCheckError(err_l)
  223. return ret_field_list
  224. ## @brief Check that a relational field is valid
  225. # @param field str : a relational field
  226. # @return a nature
  227. @classmethod
  228. def _prepare_relational_fields(cls, field):
  229. raise NotImplementedError("Abstract method")
  230. ## @brief Check that the field list only contains fields that are in the current class
  231. # @return None if no problem, else returns a list of exceptions that occurs during the check
  232. @classmethod
  233. def _check_field(cls, field):
  234. err_l = list()
  235. if field not in cls.fieldlist():
  236. return ValueError("No such field '%s' in %s"%(field, cls.__name__))
  237. return field
  238. ## @brief Prepare filters for datasource
  239. #
  240. # This method divide filters in two categories :
  241. # - filters : standart FIELDNAME OP VALUE filter
  242. # - relationnal_filters : filter on object relation RELATION_NATURE OP VALUE
  243. #
  244. # Both categories of filters are represented in the same way, a tuple with 3 elements (NAME|NAT , OP, VALUE )
  245. #
  246. # @param filters_l list : This list can contain str "FIELDNAME OP VALUE" and tuples (FIELDNAME, OP, VALUE)
  247. # @return a tuple(FILTERS, RELATIONNAL_FILTERS
  248. #
  249. # @see @ref datasource_side
  250. @classmethod
  251. def _prepare_filters(cls, filters_l):
  252. filters = list()
  253. res_filters = list()
  254. rel_filters = list()
  255. err_l = list()
  256. for fil in filters_l:
  257. if len(fil) == 3 and not isinstance(fil, str):
  258. filters.append(tuple(fil))
  259. else:
  260. filters.append(cls._split_filter(fil))
  261. for field, operator, value in filters:
  262. if cls._field_is_relational(field):
  263. #Checks relational fields
  264. ret = cls._prepare_relational_field(field)
  265. if isinstance(ret, Exception):
  266. err_l.append(ret)
  267. else:
  268. rel_filters.append((ret, operator, value))
  269. else:
  270. #Checks other fields
  271. ret = cls._check_field(field)
  272. if isinstance(ret, Exception):
  273. err_l.append(ret)
  274. else:
  275. res_filters.append((field,operator, value))
  276. if len(err_l) > 0:
  277. raise LeApiDataCheckError(err_l)
  278. return (res_filters, rel_filters)
  279. ## @brief Check and split a query filter
  280. # @note The query_filter format is "FIELD OPERATOR VALUE"
  281. # @param query_filter str : A query_filter string
  282. # @param cls
  283. # @return a tuple (FIELD, OPERATOR, VALUE)
  284. @classmethod
  285. def _split_filter(cls, query_filter):
  286. if cls._query_re is None:
  287. cls._compile_query_re()
  288. matches = cls._query_re.match(query_filter)
  289. if not matches:
  290. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  291. result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
  292. for r in result:
  293. if len(r) == 0:
  294. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  295. return result
  296. ## @brief Compile the regex for query_filter processing
  297. # @note Set _LeObject._query_re
  298. @classmethod
  299. def _compile_query_re(cls):
  300. op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
  301. for operator in cls._query_operators[1:]:
  302. op_re_piece += '|(%s)'%operator.replace(' ', '\s')
  303. op_re_piece += ')'
  304. cls._query_re = re.compile('^\s*(?P<field>(((superior)|(subordinate))\.)?[a-z_][a-z0-9\-_]*)\s*'+op_re_piece+'\s*(?P<value>[^<>=!].*)\s*$', flags=re.IGNORECASE)
  305. pass
  306. ## @brief Check if a field is relational or not
  307. # @param field str : the field to test
  308. # @return True if the field is relational else False
  309. @staticmethod
  310. def _field_is_relational(field):
  311. return field.startswith('superior.') or field.startswith('subordinate')