説明なし
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

lecrud.py 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. #-*- coding: utf-8 -*-
  2. ## @package leapi.lecrud
  3. # @brief This package contains the abstract class representing Lodel Editorial components
  4. #
  5. import copy
  6. import warnings
  7. import importlib
  8. import re
  9. from Lodel import logger
  10. from EditorialModel.fieldtypes.generic import DatasConstructor
  11. from Lodel.hooks import LodelHook
  12. REL_SUP = 0
  13. REL_SUB = 1
  14. class LeApiErrors(Exception):
  15. ## @brief Instanciate a new exceptions handling multiple exceptions
  16. # @param msg str : Exception message
  17. # @param exceptions dict : A list of data check Exception with concerned field (or stuff) as key
  18. def __init__(self, msg = "Unknow error", exceptions = None):
  19. self._msg = msg
  20. self._exceptions = dict() if exceptions is None else exceptions
  21. def __repr__(self):
  22. return self.__str__()
  23. def __str__(self):
  24. msg = self._msg
  25. for obj, expt in self._exceptions.items():
  26. msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
  27. expt_obj = obj,
  28. expt_name=expt.__class__.__name__,
  29. expt_msg=str(expt)
  30. )
  31. return msg
  32. ## @brief When an error concern a query
  33. class LeApiQueryError(LeApiErrors): pass
  34. ## @brief When an error concerns a datas
  35. class LeApiDataCheckError(LeApiErrors): pass
  36. ## @brief Main class to handler lodel editorial components (relations and objects)
  37. class _LeCrud(object):
  38. ## @brief The datasource
  39. _datasource = None
  40. ## @brief abstract property to store the fieldtype representing the component identifier
  41. _uid_fieldtype = None #Will be a dict fieldname => fieldtype
  42. ## @brief will store all the fieldtypes (child classes handle it)
  43. _fieldtypes_all = None
  44. ## @brief Stores a regular expression to parse query filters strings
  45. _query_re = None
  46. ## @brief Stores Query filters operators
  47. _query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ', ' like ', ' not like ']
  48. ## @brief Asbtract constructor for every child classes
  49. # @param uid int : lodel_id if LeObject, id_relation if its a LeRelation
  50. # @param **kwargs : datas !
  51. # @throw NotImplementedError if trying to instanciate a class that cannot be instanciated
  52. def __init__(self, uid, **kwargs):
  53. if len(kwargs) > 0:
  54. if not self.implements_leobject() and not self.implements_lerelation():
  55. raise NotImplementedError("Abstract class !")
  56. # Try to get the name of the uid field (lodel_id for objects, id_relation for relations)
  57. try:
  58. uid_name = self.uidname()
  59. except NotImplementedError: #Should never append
  60. raise NotImplementedError("Abstract class ! You can only do partial instanciation on classes that have an uid name ! (LeObject & childs + LeRelation & childs)")
  61. # Checking uid value
  62. uid, err = self._uid_fieldtype[uid_name].check_data_value(uid)
  63. if isinstance(err, Exception):
  64. raise err
  65. setattr(self, uid_name, uid)
  66. if uid_name in kwargs:
  67. warnings.warn("When instanciating the uid was given in the uid argument but was also provided in kwargs. Droping the kwargs uid")
  68. del(kwargs[uid_name])
  69. # Populating the object with given datas
  70. errors = dict()
  71. for name, value in kwargs.items():
  72. if name not in self.fieldlist():
  73. errors[name] = AttributeError("No such field '%s' for %s"%(name, self.__class__.__name__))
  74. else:
  75. cvalue, err = self.fieldtypes()[name].check_data_value(value)
  76. if isinstance(err, Exception):
  77. errors[name] = err
  78. else:
  79. setattr(self, name, cvalue)
  80. if len(errors) > 0:
  81. raise LeApiDataCheckError("Invalid arguments given to constructor", errors)
  82. ## @brief A flag to indicate if the object was fully intanciated or not
  83. self._instanciation_complete = len(kwargs) + 1 == len(self.fieldlist())
  84. ## @brief Convert an EmType or EmClass name in a python class name
  85. # @param name str : The name
  86. # @return name.title()
  87. @staticmethod
  88. def name2classname(name):
  89. if not isinstance(name, str):
  90. raise AttributeError("Argument name should be a str and not a %s" % type(name))
  91. return name.title()
  92. ## @brief Convert an EmCalss and EmType name in a rel2type class name
  93. # @param class_name str : The name of concerned class
  94. # @param type_name str : The name of the concerned type
  95. # @param relation_name str : The name of the relation (the name of the rel2type field in the LeClass)
  96. # @return name.title()
  97. @staticmethod
  98. def name2rel2type(class_name, type_name, relation_name):
  99. cls_name = "Rel%s%s%s"%(_LeCrud.name2classname(class_name), _LeCrud.name2classname(type_name), relation_name.title())
  100. return cls_name
  101. ## @brief Given a dynamically generated class name return the corresponding python Class
  102. # @param name str : a concrete class name
  103. # @param cls
  104. # @return False if no such component
  105. @classmethod
  106. def name2class(cls, name):
  107. if not isinstance(name, str):
  108. raise ValueError("Expected name argument as a string but got %s instead"%(type(name)))
  109. mod = importlib.import_module(cls.__module__)
  110. try:
  111. return getattr(mod, name)
  112. except AttributeError:
  113. return False
  114. ## @return LeObject class
  115. @classmethod
  116. def leobject(cls):
  117. return cls.name2class('LeObject')
  118. ## @return A dict with key field name and value a fieldtype instance
  119. @classmethod
  120. def fieldtypes(cls):
  121. raise NotImplementedError("Abstract method") #child classes should return their uid fieldtype
  122. ## @return A dict with fieldtypes marked as internal
  123. # @todo check if this method is in use, else delete it
  124. @classmethod
  125. def fieldtypes_internal(self):
  126. return { fname: ft for fname, ft in cls.fieldtypes().items() if hasattr(ft, 'internal') and ft.internal }
  127. ## @return A list of field name
  128. @classmethod
  129. def fieldlist(cls):
  130. return list(cls.fieldtypes().keys())
  131. ## @return The name of the uniq id field
  132. # @todo test for abstract method !!!
  133. @classmethod
  134. def uidname(cls):
  135. raise NotImplementedError("Abstract method uid_name for %s!"%cls.__name__)
  136. ## @return maybe Bool: True if cls implements LeType
  137. # @param cls Class: a Class or instanciated object
  138. @classmethod
  139. def implements_letype(cls):
  140. return hasattr(cls, '_leclass')
  141. ## @return maybe Bool: True if cls implements LeClass
  142. # @param cls Class: a Class or instanciated object
  143. @classmethod
  144. def implements_leclass(cls):
  145. return hasattr(cls, '_class_id')
  146. ## @return maybe Bool: True if cls implements LeObject
  147. # @param cls Class: a Class or instanciated object
  148. @classmethod
  149. def implements_leobject(cls):
  150. return hasattr(cls, '_me_uid')
  151. ## @return maybe Bool: True if cls is a LeType or an instance of LeType
  152. # @param cls Class: a Class or instanciated object
  153. @classmethod
  154. def is_letype(cls):
  155. return cls.implements_letype()
  156. ## @return maybe Bool: True if cls is a LeClass or an instance of LeClass
  157. # @param cls Class: a Class or instanciated object
  158. @classmethod
  159. def is_leclass(cls):
  160. return cls.implements_leclass() and not cls.implements_letype()
  161. ## @return maybe Bool: True if cls is a LeClass or an instance of LeClass
  162. # @param cls Class: a Class or instanciated object
  163. @classmethod
  164. def is_leobject(cls):
  165. return cls.implements_leobject() and not cls.implements_leclass()
  166. ## @return maybe Bool: True if cls implements LeRelation
  167. # @param cls Class: a Class or instanciated object
  168. @classmethod
  169. def implements_lerelation(cls):
  170. return hasattr(cls, '_superior_field_name')
  171. ## @return maybe Bool: True if cls implements LeRel2Type
  172. # @param cls Class: a Class or instanciated object
  173. @classmethod
  174. def implements_lerel2type(cls):
  175. return hasattr(cls, '_rel_attr_fieldtypes')
  176. ## @return maybe Bool: True if cls is a LeHierarch or an instance of LeHierarch
  177. # @param cls Class: a Class or instanciated object
  178. @classmethod
  179. def is_lehierarch(cls):
  180. return cls.implements_lerelation() and not cls.implements_lerel2type()
  181. ## @return maybe Bool: True if cls is a LeRel2Type or an instance of LeRel2Type
  182. # @param cls Class: a Class or instanciated object
  183. @classmethod
  184. def is_lerel2type(cls):
  185. return cls.implements_lerel2type()
  186. def uidget(self):
  187. return getattr(self, self.uidname())
  188. ## @brief Returns object datas
  189. # @param internal bool : If True return all datas including internal fields
  190. # @param lang str | None : if None return datas indexed with field name, else datas are indexed with field name translation
  191. # @return a dict of fieldname : value
  192. def datas(self, internal = True, lang = None):
  193. res = dict()
  194. for fname, ftt in self.fieldtypes().items():
  195. if (internal or (not internal and not ftt.is_internal)) and hasattr(self, fname):
  196. if lang is None:
  197. res[fname] = getattr(self, fname)
  198. else:
  199. res[self.ml_fields_strings[fname][lang]] = getattr(self, fname)
  200. return res
  201. ## @brief Indicates if an instance is complete
  202. # @return a bool
  203. def is_complete(self):
  204. return self._instanciation_complete
  205. ## @brief Populate the LeType wih datas from DB
  206. # @param field_list None|list : List of fieldname to fetch. If None fetch all the missing datas
  207. # @todo Add checks to forbid the use of this method on abtract classes (LeObject, LeClass, LeType, LeRel2Type, LeRelation etc...)
  208. def populate(self, field_list=None):
  209. if not self.is_complete():
  210. if field_list == None:
  211. field_list = [ fname for fname in self.fieldlist() if not hasattr(self, fname) ]
  212. filters = [self._id_filter()]
  213. rel_filters = []
  214. # Getting datas from db
  215. fdatas = self._datasource.select(self.__class__, field_list, filters, rel_filters)
  216. if fdatas is None or len(fdatas) == 0:
  217. raise LeApiQueryError("Error when trying to populate an object. For type %s id : %d"% (self.__class__.__name__, self.lodel_id))
  218. # Setting datas
  219. for fname, fval in fdatas[0].items():
  220. setattr(self, fname, fval)
  221. self._instanciation_complete = True
  222. ## @brief Return the corresponding instance
  223. #
  224. # @note this method is a kind of factory. Allowing to make a partial instance
  225. # of abstract types using only an uid and then fetching an complete instance of
  226. # the correct class
  227. # @return Corresponding populated LeObject
  228. def get_instance(self):
  229. if self.is_complete():
  230. return self
  231. uid_fname = self.uidname()
  232. qfilter = '{uid_fname} = {uid}'.format(uid_fname = uid_fname, uid = getattr(self, uid_fname))
  233. return leobject.get([qfilter])[0]
  234. ## @brief Update a component in DB
  235. # @param datas dict : If None use instance attributes to update de DB
  236. # @return True if success
  237. # @todo better error handling
  238. # @todo for check_data_consistency, datas must be populated to make update safe !
  239. def update(self, datas=None):
  240. kwargs = locals()
  241. del(kwargs['self'])
  242. kwargs = LodelHook.call_hook('leapi_update_pre', self, kwargs)
  243. ret = self.__update_unsafe(**kwargs)
  244. return LodelHook.call_hook('leapi_update_post', self, ret)
  245. ## @brief Unsafe, without hooks version of insert method
  246. # @see _LeCrud.update()
  247. def __update_unsafe(self, datas=None):
  248. if not self.is_complete():
  249. self.populate()
  250. warnings.warn("\nThis object %s is not complete and has been populated when update was called. This is very unsafe\n" % self)
  251. datas = self.datas(internal=False) if datas is None else datas
  252. upd_datas = self.prepare_datas(datas, complete = False, allow_internal = False)
  253. filters = [self._id_filter()]
  254. rel_filters = []
  255. ret = self._datasource.update(self.__class__, self.uidget(), **upd_datas)
  256. if ret == 1:
  257. return True
  258. else:
  259. #ERROR HANDLING
  260. return False
  261. ## @brief Delete a component
  262. # @return True if success
  263. # @todo better error handling
  264. def delete(self):
  265. LodelHook.call_hook('leapi_delete_pre', self, None)
  266. ret = self._datasource.delete(self.__class__, self.uidget())
  267. return LodelHook.call_hook('leapi_delete_post', self, ret)
  268. ## @brief Check that datas are valid for this type
  269. # @param datas dict : key == field name value are field values
  270. # @param complete bool : if True expect that datas provide values for all non internal fields
  271. # @param allow_internal bool : if True don't raise an error if a field is internal
  272. # @param cls
  273. # @return Checked datas
  274. # @throw LeApiDataCheckError if errors reported during check
  275. @classmethod
  276. def check_datas_value(cls, datas, complete = False, allow_internal = True):
  277. err_l = dict() #Stores errors
  278. correct = [] #Valid fields name
  279. mandatory = [] #mandatory fields name
  280. for fname, ftt in cls.fieldtypes().items():
  281. if allow_internal or not ftt.is_internal():
  282. correct.append(fname)
  283. if complete and not hasattr(ftt, 'default'):
  284. mandatory.append(fname)
  285. mandatory = set(mandatory)
  286. correct = set(correct)
  287. provided = set(datas.keys())
  288. #searching unknow fields
  289. unknown = provided - correct
  290. for u_f in unknown:
  291. #here we can check if the field is unknown or rejected because it is internal
  292. err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'"%u_f)
  293. #searching missings fields
  294. missings = mandatory - provided
  295. for miss_field in missings:
  296. err_l[miss_field] = AttributeError("The data for field '%s' is missing"%miss_field)
  297. #Checks datas
  298. checked_datas = dict()
  299. for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
  300. ft = cls.fieldtypes()
  301. ft = ft[name]
  302. r = ft.check_data_value(value)
  303. checked_datas[name], err = r
  304. #checked_datas[name], err = cls.fieldtypes()[name].check_data_value(value)
  305. if err:
  306. err_l[name] = err
  307. if len(err_l) > 0:
  308. raise LeApiDataCheckError("Error while checking datas", err_l)
  309. return checked_datas
  310. ## @brief Retrieve a collection of lodel editorial components
  311. #
  312. # @param query_filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
  313. # @param field_list list|None : list of string representing fields see @ref leobject_filters
  314. # @param order list : A list of field names or tuple (FIELDNAME, [ASC | DESC])
  315. # @param group list : A list of field names or tuple (FIELDNAME, [ASC | DESC])
  316. # @param limit int : The maximum number of returned results
  317. # @param offset int : offset
  318. # @param instanciate bool : If True return an instance, else return a dict
  319. # @param cls
  320. # @return A list of lodel editorial components instance
  321. # @todo think about LeObject and LeClass instanciation (partial instanciation, etc)
  322. @classmethod
  323. def get(cls, query_filters, field_list=None, order=None, group=None, limit=None, offset=0, instanciate=True):
  324. kwargs = locals()
  325. del(kwargs['cls'])
  326. kwargs = LodelHook.call_hook('leapi_get_pre', cls, kwargs)
  327. ret = cls.__get_unsafe(**kwargs)
  328. return LodelHook.call_hook('leapi_get_post', cls, ret)
  329. ## @brief Unsafe, without hooks version of get() method
  330. # @see _LeCrud.get()
  331. @classmethod
  332. def __get_unsafe(cls, query_filters, field_list=None, order=None, group=None, limit=None, offset=0, instanciate=True):
  333. if field_list is None or len(field_list) == 0:
  334. #default field_list
  335. field_list = cls.fieldlist()
  336. field_list = cls._prepare_field_list(field_list) #Can raise LeApiDataCheckError
  337. #preparing filters
  338. filters, relational_filters = cls._prepare_filters(query_filters)
  339. #preparing order
  340. if order:
  341. order = cls._prepare_order_fields(order)
  342. if isinstance(order, Exception):
  343. raise order #can be buffered and raised later, but _prepare_filters raise when fails
  344. #preparing groups
  345. if group:
  346. group = cls._prepare_order_fields(group)
  347. if isinstance(group, Exception):
  348. raise group # can also be buffered and raised later
  349. #checking limit and offset values
  350. if not (limit is None):
  351. if limit <= 0:
  352. raise ValueError("Invalid limit given : %d"%limit)
  353. if not (offset is None):
  354. if offset < 0:
  355. raise ValueError("Invalid offset given : %d"%offset)
  356. #Fetching editorial components from datasource
  357. results = cls._datasource.select(
  358. target_cls = cls,
  359. field_list = field_list,
  360. filters = filters,
  361. rel_filters = relational_filters,
  362. order=order,
  363. group=group,
  364. limit=limit,
  365. offset=offset,
  366. instanciate=instanciate
  367. )
  368. return results
  369. ## @brief Insert a new component
  370. # @param datas dict : The value of object we want to insert
  371. # @param classname str : The class name
  372. # @param cls
  373. # @return A new id if success else False
  374. @classmethod
  375. def insert(cls, datas, classname=None):
  376. kwargs = locals()
  377. del(kwargs['cls'])
  378. kwargs = LodelHook.call_hook('leapi_insert_pre', cls, kwargs)
  379. ret = cls.__insert_unsafe(**kwargs)
  380. return LodelHook.call_hook('leapi_insert_post', cls, ret)
  381. ## @brief Unsafe, without hooks version of insert() method
  382. # @see _LeCrud.insert()
  383. @classmethod
  384. def __insert_unsafe(cls, datas, classname=None):
  385. callcls = cls if classname is None else cls.name2class(classname)
  386. if not callcls:
  387. raise LeApiErrors("Error when inserting",{'error':ValueError("The class '%s' was not found"%classname)})
  388. if not callcls.implements_letype() and not callcls.implements_lerelation():
  389. raise ValueError("You can only insert relations and LeTypes objects but tying to insert a '%s'"%callcls.__name__)
  390. insert_datas = callcls.prepare_datas(datas, complete = True, allow_internal = False)
  391. return callcls._datasource.insert(callcls, **insert_datas)
  392. ## @brief Check and prepare datas
  393. #
  394. # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
  395. #
  396. # @param datas dict : {fieldname : fieldvalue, ...}
  397. # @param complete bool : If True you MUST give all the datas
  398. # @param allow_internal : Wether or not interal fields are expected in datas
  399. # @param cls
  400. # @return Datas ready for use
  401. # @todo: complete is very unsafe, find a way to get rid of it
  402. @classmethod
  403. def prepare_datas(cls, datas, complete=False, allow_internal=True):
  404. if not complete:
  405. warnings.warn("\nActual implementation can make datas construction and consitency unsafe when datas are not complete\n")
  406. ret_datas = cls.check_datas_value(datas, complete, allow_internal)
  407. if isinstance(ret_datas, Exception):
  408. raise ret_datas
  409. if complete:
  410. ret_datas = cls._construct_datas(ret_datas)
  411. cls._check_datas_consistency(ret_datas)
  412. return ret_datas
  413. #-###################-#
  414. # Private methods #
  415. #-###################-#
  416. ## @brief Build a filter to select an object with a specific ID
  417. # @warning assert that the uid is not composed with multiple fieldtypes
  418. # @return A filter of the form tuple(UID, '=', self.UID)
  419. # @todo This method should not be private
  420. def _id_filter(self):
  421. id_name = self.uidname()
  422. return ( id_name, '=', getattr(self, id_name) )
  423. ## @brief Construct datas values
  424. #
  425. # @param cls
  426. # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
  427. # @return A new dict of datas
  428. @classmethod
  429. def _construct_datas(cls, datas):
  430. constructor = DatasConstructor(cls, datas, cls.fieldtypes())
  431. ret = {
  432. fname:constructor[fname]
  433. for fname, ftype in cls.fieldtypes().items()
  434. if not ftype.is_internal() or ftype.internal != 'autosql'
  435. }
  436. return ret
  437. ## @brief Check datas consistency
  438. # @warning assert that datas is complete
  439. # @param cls
  440. # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
  441. # @throw LeApiDataCheckError if fails
  442. @classmethod
  443. def _check_datas_consistency(cls, datas):
  444. err_l = []
  445. err_l = dict()
  446. for fname, ftype in cls.fieldtypes().items():
  447. ret = ftype.check_data_consistency(cls, fname, datas)
  448. if isinstance(ret, Exception):
  449. err_l[fname] = ret
  450. if len(err_l) > 0:
  451. raise LeApiDataCheckError("Datas consistency checks fails", err_l)
  452. ## @brief Prepare a field_list
  453. # @param cls
  454. # @param field_list list : List of string representing fields
  455. # @return A well formated field list
  456. # @throw LeApiDataCheckError if invalid field given
  457. @classmethod
  458. def _prepare_field_list(cls, field_list):
  459. err_l = dict()
  460. ret_field_list = list()
  461. for field in field_list:
  462. if cls._field_is_relational(field):
  463. ret = cls._prepare_relational_fields(field)
  464. else:
  465. ret = cls._check_field(field)
  466. if isinstance(ret, Exception):
  467. err_l[field] = ret
  468. else:
  469. ret_field_list.append(ret)
  470. if len(err_l) > 0:
  471. raise LeApiDataCheckError(err_l)
  472. return ret_field_list
  473. ## @brief Check that a relational field is valid
  474. # @param cls
  475. # @param field str : a relational field
  476. # @return a nature
  477. @classmethod
  478. def _prepare_relational_fields(cls, field):
  479. raise NotImplementedError("Abstract method")
  480. ## @brief Check that the field list only contains fields that are in the current class
  481. # @param cls
  482. # @param field : a field
  483. # @return None if no problem, else returns a list of exceptions that occurs during the check
  484. @classmethod
  485. def _check_field(cls, field):
  486. if field not in cls.fieldlist():
  487. return ValueError("No such field '%s' in %s"%(field, cls.__name__))
  488. return field
  489. ## @brief Prepare the order parameter for the get method
  490. # @note if an item in order_list is just a str it is considered as ASC by default
  491. # @param cls
  492. # @param order_field_list list : A list of field name or tuple (FIELDNAME, [ASC|DESC])
  493. # @return a list of tuple (FIELDNAME, [ASC|DESC] )
  494. @classmethod
  495. def _prepare_order_fields(cls, order_field_list):
  496. errors = dict()
  497. result = []
  498. for order_field in order_field_list:
  499. if not isinstance(order_field, tuple):
  500. order_field = (order_field, 'ASC')
  501. if len(order_field) != 2 or order_field[1].upper() not in ['ASC', 'DESC']:
  502. errors[order_field] = ValueError("Expected a string or a tuple with (FIELDNAME, ['ASC'|'DESC']) but got : %s"%order_field)
  503. else:
  504. ret = cls._check_field(order_field[0])
  505. if isinstance(ret, Exception):
  506. errors[order_field] = ret
  507. order_field = (order_field[0], order_field[1].upper())
  508. result.append(order_field)
  509. if len(errors) > 0:
  510. return LeApiErrors("Errors when preparing ordering fields", errors)
  511. return result
  512. ## @brief Prepare filters for datasource
  513. #
  514. # This method divide filters in two categories :
  515. # - filters : standart FIELDNAME OP VALUE filter
  516. # - relationnal_filters : filter on object relation RELATION_NATURE OP VALUE
  517. #
  518. # Both categories of filters are represented in the same way, a tuple with 3 elements (NAME|NAT , OP, VALUE )
  519. #
  520. # @param cls
  521. # @param filters_l list : This list can contain str "FIELDNAME OP VALUE" and tuples (FIELDNAME, OP, VALUE)
  522. # @return a tuple(FILTERS, RELATIONNAL_FILTERS
  523. #
  524. # @see @ref datasource_side
  525. @classmethod
  526. def _prepare_filters(cls, filters_l):
  527. filters = list()
  528. res_filters = list()
  529. rel_filters = list()
  530. err_l = dict()
  531. #Splitting in tuple if necessary
  532. for fil in filters_l:
  533. if len(fil) == 3 and not isinstance(fil, str):
  534. filters.append(tuple(fil))
  535. else:
  536. filters.append(cls._split_filter(fil))
  537. for field, operator, value in filters:
  538. if cls._field_is_relational(field):
  539. #Checks relational fields
  540. ret = cls._prepare_relational_fields(field)
  541. if isinstance(ret, Exception):
  542. err_l[field] = ret
  543. else:
  544. rel_filters.append((ret, operator, value))
  545. else:
  546. #Checks other fields
  547. ret = cls._check_field(field)
  548. if isinstance(ret, Exception):
  549. err_l[field] = ret
  550. else:
  551. res_filters.append((field,operator, value))
  552. if len(err_l) > 0:
  553. raise LeApiDataCheckError("Error while preparing filters : ", err_l)
  554. return (res_filters, rel_filters)
  555. ## @brief Check and split a query filter
  556. # @note The query_filter format is "FIELD OPERATOR VALUE"
  557. # @param query_filter str : A query_filter string
  558. # @param cls
  559. # @return a tuple (FIELD, OPERATOR, VALUE)
  560. @classmethod
  561. def _split_filter(cls, query_filter):
  562. if cls._query_re is None:
  563. cls._compile_query_re()
  564. matches = cls._query_re.match(query_filter)
  565. if not matches:
  566. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  567. result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
  568. for r in result:
  569. if len(r) == 0:
  570. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  571. return result
  572. ## @brief Compile the regex for query_filter processing
  573. # @note Set _LeObject._query_re
  574. @classmethod
  575. def _compile_query_re(cls):
  576. op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
  577. for operator in cls._query_operators[1:]:
  578. op_re_piece += '|(%s)'%operator.replace(' ', '\s')
  579. op_re_piece += ')'
  580. 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)
  581. pass
  582. ## @brief Check if a field is relational or not
  583. # @param field str : the field to test
  584. # @return True if the field is relational else False
  585. @staticmethod
  586. def _field_is_relational(field):
  587. return field.startswith('superior.') or field.startswith('subordinate.')
  588. ## @page leobject_filters LeObject query filters
  589. # The LeObject API provide methods that accept filters allowing the user
  590. # to query the database and fetch LodelEditorialObjects.
  591. #
  592. # The LeObject API translate those filters for the datasource.
  593. #
  594. # @section api_user_side API user side filters
  595. # Filters are string expressing a condition. The string composition
  596. # is as follow : "<FIELD> <OPERATOR> <VALUE>"
  597. # @subsection fpart FIELD
  598. # @subsubsection standart fields
  599. # Standart fields, represents a value of the LeObject for example "title", "lodel_id" etc.
  600. # @subsubsection rfields relationnal fields
  601. # relationnal fields, represents a relation with the object hierarchy. Those fields are composed as follow :
  602. # "<RELATION>.<NATURE>".
  603. #
  604. # - Relation can takes two values : superiors or subordinates
  605. # - Nature is a relation nature ( see EditorialModel.classtypes )
  606. # Examples : "superiors.parent", "subordinates.translation" etc.
  607. # @note The field_list arguement of leapi.leapi._LeObject.get() use the same syntax than the FIELD filter part
  608. # @subsection oppart OPERATOR
  609. # The OPERATOR part of a filter is a comparison operator. There is
  610. # - standart comparison operators : = , <, > , <=, >=, !=
  611. # - vagueness string comparison 'like' and 'not like'
  612. # - list operators : 'in' and 'not in'
  613. # The list of allowed operators is sotred at leapi.leapi._LeObject._query_operators .
  614. # @subsection valpart VALUE
  615. # The VALUE part of a filter is... just a value...
  616. #
  617. # @section datasource_side Datasource side filters
  618. # As said above the API "translate" filters before forwarding them to the datasource.
  619. #
  620. # The translation process transform filters in tuple composed of 3 elements
  621. # ( @ref fpart , @ref oppart , @ref valpart ). Each element is a string.
  622. #
  623. # There is a special case for @ref rfields : the field element is a tuple composed with two elements
  624. # ( RELATION, NATURE ) where NATURE is a string ( see EditorialModel.classtypes ) and RELATION is one of
  625. # the defined constant :
  626. #
  627. # - leapi.lecrud.REL_SUB for "subordinates"
  628. # - leapi.lecrud.REL_SUP for "superiors"
  629. #
  630. # @note The filters translation process also check if given field are valids compared to the concerned letype and/or the leclass
  631. ## @page lecrud_instanciation LeCrud child classes instanciations
  632. #
  633. # _LeCrud provide a generic __init__ method for all its child classes. The following notes are
  634. # important parts of the instanciation mechanism.
  635. #
  636. # The constructor takes 2 parameters :
  637. # - a uniq identifier (uid)
  638. # - **kwargs for object datas
  639. #
  640. # @section lecrud_pi Partial instancation
  641. #
  642. # You can make partial instanciations by giving only parts of datas and even by giving only a uid
  643. #
  644. # @warning Partial instanciation needs an uid field name (lodel_id for LeObject and id_relation for LeRelation). This implies that you cannot make partial instance of a LeCrud.
  645. #
  646. # @subsection lecrud_pitools Partial instances tools
  647. #
  648. # The _LeCrud.is_complete() method indicates whether or not an instance is partial.
  649. #
  650. # The _LeCrud.populate() method fetch missing datas
  651. #
  652. # You partially instanciate an abtract class (like LeClass or LeRelation) using only a uid. Then you cannot populate this kind of instance (you cannot dinamically change the type of an instance). The _LeCrud.get_instance() method returns a populated instance with the good type.
  653. #