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.

query.py 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. #
  2. # This file is part of Lodel 2 (https://github.com/OpenEdition)
  3. #
  4. # Copyright (C) 2015-2017 Cléo UMS-3287
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as published
  8. # by the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. ## @package lodel.leapi.lequery
  20. import re
  21. import copy
  22. import inspect
  23. import warnings
  24. from lodel.context import LodelContext
  25. LodelContext.expose_modules(globals(), {
  26. 'lodel.leapi.exceptions': ['LeApiError', 'LeApiErrors',
  27. 'LeApiDataCheckError', 'LeApiDataCheckErrors', 'LeApiQueryError',
  28. 'LeApiQueryErrors'],
  29. 'lodel.plugin.hooks': ['LodelHook'],
  30. 'lodel.logger': ['logger']})
  31. # @todo check data when running query
  32. class LeQuery(object):
  33. ## @brief Hookname prefix
  34. _hook_prefix = None
  35. ## @brief arguments for the LeObject.check_data_value()
  36. _data_check_args = {'complete': False, 'allow_internal': False}
  37. ## @brief Abstract constructor
  38. # @param target_class LeObject : class of object the query is about
  39. def __init__(self, target_class):
  40. from .leobject import LeObject
  41. if self._hook_prefix is None:
  42. raise NotImplementedError("Abstract class")
  43. if not inspect.isclass(target_class) or \
  44. not issubclass(target_class, LeObject):
  45. raise TypeError(
  46. "target class has to be a child class of LeObject but %s given" % target_class)
  47. self._target_class = target_class
  48. self._ro_datasource = target_class._ro_datasource
  49. self._rw_datasource = target_class._rw_datasource
  50. ## @brief Executes a query and returns the result
  51. #@param **data
  52. #@return the query result
  53. #@see LeQuery._query()
  54. #@todo check that the check_datas_value is not duplicated/useless
  55. def execute(self, data):
  56. if data is not None:
  57. self._target_class.check_datas_value(
  58. data,
  59. **self._data_check_args)
  60. self._target_class.prepare_datas(data) # not yet implemented
  61. if self._hook_prefix is None:
  62. raise NotImplementedError("Abstract method")
  63. LodelHook.call_hook(self._hook_prefix + 'pre',
  64. self._target_class,
  65. data)
  66. ret = self._query(data=data)
  67. ret = LodelHook.call_hook(self._hook_prefix + 'post',
  68. self._target_class,
  69. ret)
  70. return ret
  71. ## @brief Child classes implement this method to execute the query
  72. #@param **data
  73. #@return query result
  74. def _query(self, **data):
  75. raise NotImplementedError("Asbtract method")
  76. # @return a dict with query infos
  77. def dump_infos(self):
  78. return {'target_class': self._target_class}
  79. def __repr__(self):
  80. ret = "<{classname} target={target_class}>"
  81. return ret.format(
  82. classname=self.__class__.__name__,
  83. target_class=self._target_class)
  84. ## @brief Abstract class handling query with filters
  85. class LeFilteredQuery(LeQuery):
  86. ## @brief The available operators used in query definitions
  87. _query_operators = [
  88. ' = ',
  89. ' <= ',
  90. ' >= ',
  91. ' != ',
  92. ' < ',
  93. ' > ',
  94. ' in ',
  95. ' not in ',
  96. ' like ',
  97. ' not like ']
  98. ## @brief Regular expression to process filters
  99. _query_re = None
  100. ## @brief Abtract constructor for queries with filter
  101. #@param target_class LeObject : class of object the query is about
  102. #@param query_filters list : with a tuple (only one filter) or a list of
  103. # tuple or a dict: {OP,list(filters)} with OP = 'OR' or 'AND for tuple
  104. # (FIELD,OPERATOR,VALUE)
  105. def __init__(self, target_class, query_filters=None):
  106. super().__init__(target_class)
  107. ## @brief The query filter tuple(std_filter, relational_filters)
  108. self._query_filter = None
  109. ## @brief Stores potential subqueries (used when a query implies
  110. # more than one datasource.
  111. #
  112. # Subqueries are tuple(target_class_ref_field, LeGetQuery)
  113. self.subqueries = None
  114. query_filters = [] if query_filters is None else query_filters
  115. self.set_query_filter(query_filters)
  116. ## @brief Abstract FilteredQuery execution method
  117. #
  118. # This method takes care to execute subqueries before calling super execute
  119. def execute(self, data=None):
  120. # copy originals filters
  121. orig_filters = copy.copy(self._query_filter)
  122. std_filters, rel_filters = self._query_filter
  123. for rfield, subq in self.subqueries:
  124. subq_res = subq.execute()
  125. std_filters.append(
  126. (rfield, ' in ', subq_res))
  127. self._query_filter = (std_filters, rel_filters)
  128. try:
  129. filters, rel_filters = self._query_filter
  130. res = super().execute(data)
  131. except Exception as e:
  132. # restoring filters even if an exception is raised
  133. self.__query_filter = orig_filters
  134. raise e # reraise
  135. # restoring filters
  136. self._query_filter = orig_filters
  137. return res
  138. ## @brief Add filter(s) to the query
  139. #
  140. # This method is also able to slice query if different datasources are
  141. # implied in the request
  142. #
  143. #@param query_filter list|tuple|str : A single filter or a list of filters
  144. #@see LeFilteredQuery._prepare_filters()
  145. #@warning Does not support multiple UID
  146. def set_query_filter(self, query_filter):
  147. if isinstance(query_filter, str):
  148. query_filter = [query_filter]
  149. # Query filter preparation
  150. filters_orig, rel_filters = self._prepare_filters(query_filter)
  151. # Here we know that each relational filter concerns only one datasource
  152. # thank's to _prepare_relational_fields
  153. # Multiple datasources detection
  154. self_ds_name = self._target_class._datasource_name
  155. result_rel_filters = list() # The filters that will remain in the query
  156. other_ds_filters = dict()
  157. for rfilter in rel_filters:
  158. (rfield, ref_dict), op, value = rfilter
  159. # rfield : the field in self._target_class
  160. tmp_rel_filter = dict() # designed to store rel_field of same DS
  161. # First step : simplification
  162. # Trying to delete relational filters done on referenced class uid
  163. for tclass, tfield in copy.copy(ref_dict).items():
  164. # tclass : referenced target class
  165. # tfield : referenced field from target class
  166. #
  167. # !!!WARNING!!!
  168. # The line below breaks multi UID support
  169. #
  170. if tfield == tclass.uid_fieldname()[0]:
  171. # This relational filter can be simplified as
  172. # ref_field, op, value
  173. # Note : we will have to dedup filters_orig
  174. filters_orig.append((rfield, op, value))
  175. del(ref_dict[tclass])
  176. if len(ref_dict) == 0:
  177. continue
  178. # Determines what to do with the other relational filters according
  179. # to the referenced class datasource
  180. # Remember : all classes in a relational filter have the same
  181. # datasource
  182. tclass = list(ref_dict.keys())[0]
  183. cur_ds = tclass._datasource_name
  184. if cur_ds == self_ds_name:
  185. # Same datasource, the filter stays in this query
  186. result_rel_filters.append(((rfield, ref_dict), op, value))
  187. else:
  188. # Different datasources, we will have to create a subquery
  189. if cur_ds not in other_ds_filters:
  190. other_ds_filters[cur_ds] = list()
  191. other_ds_filters[cur_ds].append(
  192. ((rfield, ref_dict), op, value))
  193. # deduplication of std filters
  194. filters_cp = set()
  195. if not isinstance(filters_orig, set):
  196. for i, cfilt in enumerate(filters_orig):
  197. a, b, c = cfilt
  198. if isinstance(c, list): # list are not hashable
  199. newc = tuple(c)
  200. else:
  201. newc = c
  202. old_len = len(filters_cp)
  203. filters_cp |= set((a, b, newc))
  204. if len(filters_cp) == old_len:
  205. del(filters_orig[i])
  206. # Sets _query_filter attribute of self query
  207. self._query_filter = (filters_orig, result_rel_filters)
  208. # Sub queries creation
  209. subq = list()
  210. for ds, rfilters in other_ds_filters.items():
  211. for rfilter in rfilters:
  212. (rfield, ref_dict), op, value = rfilter
  213. for tclass, tfield in ref_dict.items():
  214. query = LeGetQuery(
  215. target_class=tclass,
  216. query_filters=[(tfield, op, value)],
  217. field_list=[tfield])
  218. subq.append((rfield, query))
  219. self.subqueries = subq
  220. # @return informations
  221. def dump_infos(self):
  222. ret = super().dump_infos()
  223. ret['query_filter'] = self._query_filter
  224. ret['subqueries'] = self.subqueries
  225. return ret
  226. def __repr__(self):
  227. res = "<{classname} target={target_class} query_filter={query_filter}"
  228. res = res.format(
  229. classname=self.__class__.__name__,
  230. query_filter=self._query_filter,
  231. target_class=self._target_class)
  232. if len(self.subqueries) > 0:
  233. for n, subq in enumerate(self.subqueries):
  234. res += "\n\tSubquerie %d : %s"
  235. res %= (n, subq)
  236. res += '>'
  237. return res
  238. ## @brief Prepares filters for datasource
  239. #
  240. # A filter can be a string or a tuple with len = 3.
  241. #
  242. # This method divide filters in two categories :
  243. #
  244. #@par Simple filters
  245. #
  246. # Those filters concern fields that represent object values (a title,
  247. # the content, etc.) They are composed of three elements : FIELDNAME OP
  248. # VALUE . Where :
  249. #- FIELDNAME is the name of the field
  250. #- OP is one of the authorized comparison operands (see
  251. #@ref LeFilteredQuery.query_operators )
  252. #- VALUE is... a value
  253. #
  254. #@par Relational filters
  255. #
  256. # Those filters concern on reference fields (see the corresponding
  257. # abstract datahandler @ref lodel.leapi.datahandlers.base_classes.Reference)
  258. # The filter as quite the same composition than simple filters :
  259. # FIELDNAME[.REF_FIELD] OP VALUE . Where :
  260. #- FIELDNAME is the name of the reference field
  261. #- REF_FIELD is an optionnal addon to the base field. It indicates on which
  262. # field of the referenced object the comparison as to be done. If no
  263. # REF_FIELD is indicated the comparison will be done on identifier.
  264. #
  265. #@param cls
  266. #@param filters_l list : This list of str or tuple (or both)
  267. #@return a tuple(FILTERS, RELATIONNAL_FILTERS
  268. #@todo move this doc in another place (a dedicated page ?)
  269. #@warning Does not support multiple UID for an EmClass
  270. def _prepare_filters(self, filters_l):
  271. filters = list()
  272. res_filters = list()
  273. rel_filters = list()
  274. err_l = dict()
  275. # Splitting in tuple if necessary
  276. for i, fil in enumerate(filters_l):
  277. if len(fil) == 3 and not isinstance(fil, str):
  278. filters.append(tuple(fil))
  279. else:
  280. try:
  281. filters.append(self.split_filter(fil))
  282. except ValueError as e:
  283. err_l["filter %d" % i] = e
  284. for field, operator, value in filters:
  285. err_key = "%s %s %s" % (field, operator, value) # to push in err_l
  286. # Splitting field name to be able to detect a relational field
  287. field_spl = field.split('.')
  288. if len(field_spl) == 2:
  289. field, ref_field = field_spl
  290. elif len(field_spl) == 1:
  291. ref_field = None
  292. else:
  293. err_l[field] = NameError("'%s' is not a valid relational \
  294. field name" % field)
  295. continue
  296. # Checking field against target_class
  297. ret = self._check_field(self._target_class, field)
  298. if isinstance(ret, Exception):
  299. err_l[field] = ret
  300. continue
  301. field_datahandler = self._target_class.field(field)
  302. if isinstance(field_datahandler, Exception):
  303. err_l[field] = field_datahandler
  304. continue
  305. if ref_field is not None and not field_datahandler.is_reference():
  306. # inconsistency
  307. err_l[field] = NameError("The field '%s' in %s is not \
  308. a relational field, but %s.%s was present in the filter"
  309. % (field,
  310. self._target_class.__name__,
  311. field,
  312. ref_field))
  313. if field_datahandler.is_reference():
  314. # Relationnal field
  315. if ref_field is None:
  316. # ref_field default value
  317. #
  318. # !!! WARNING !!!
  319. # This piece of code does not supports multiple UID for an
  320. # emclass
  321. #
  322. ref_uid = [
  323. lc._uid[0] for lc in field_datahandler.linked_classes]
  324. if len(set(ref_uid)) == 1:
  325. ref_field = ref_uid[0]
  326. else:
  327. if len(ref_uid) > 1:
  328. msg = "The referenced classes are identified by \
  329. fields with different names. Unable to determine which field to use for the \
  330. reference"
  331. else:
  332. msg = "Unknow error when trying to determine which \
  333. field to use for the relational filter"
  334. err_l[err_key] = RuntimeError(msg)
  335. continue
  336. # Prepares relational field
  337. ret = self._prepare_relational_fields(field, ref_field)
  338. if isinstance(ret, Exception):
  339. err_l[err_key] = ret
  340. continue
  341. else:
  342. rel_filters.append((ret, operator, value))
  343. else:
  344. value_orig = value
  345. value, error = field_datahandler.check_data_value(value)
  346. if isinstance(error, Exception):
  347. value = value_orig
  348. res_filters.append((field, operator, value))
  349. if len(err_l) > 0:
  350. raise LeApiDataCheckErrors(
  351. "Error while preparing filters : ",
  352. err_l)
  353. return (res_filters, rel_filters)
  354. ## @brief Checks and splits a query filter
  355. # @note The query_filter format is "FIELD OPERATOR VALUE"
  356. # @param query_filter str : A query_filter string
  357. # @param cls
  358. # @return a tuple (FIELD, OPERATOR, VALUE)
  359. @classmethod
  360. def split_filter(cls, query_filter):
  361. if cls._query_re is None:
  362. cls.__compile_query_re()
  363. matches = cls._query_re.match(query_filter)
  364. if not matches:
  365. msg = "The query_filter '%s' seems to be invalid"
  366. raise ValueError(msg % query_filter)
  367. result = (
  368. matches.group('field'),
  369. re.sub(r'\s', ' ', matches.group('operator'), count=0),
  370. matches.group('value').strip())
  371. result = [r.strip() for r in result]
  372. for r in result:
  373. if len(r) == 0:
  374. msg = "The query_filter '%s' seems to be invalid"
  375. raise ValueError(msg % query_filter)
  376. return result
  377. ## @brief Compiles the regex for query_filter processing
  378. # @note Sets _LeObject._query_re
  379. @classmethod
  380. def __compile_query_re(cls):
  381. op_re_piece = '(?P<operator>(%s)'
  382. op_re_piece %= cls._query_operators[0].replace(' ', '\s')
  383. for operator in cls._query_operators[1:]:
  384. op_re_piece += '|(%s)' % operator.replace(' ', '\s')
  385. op_re_piece += ')'
  386. re_full = '^\s*(?P<field>([a-z_][a-z0-9\-_]*\.)?[a-z_][a-z0-9\-_]*)\s*'
  387. re_full += op_re_piece + '\s*(?P<value>.*)\s*$'
  388. cls._query_re = re.compile(re_full, flags=re.IGNORECASE)
  389. pass
  390. @classmethod
  391. def _check_field(cls, target_class, fieldname):
  392. try:
  393. target_class.field(fieldname)
  394. except NameError as e:
  395. msg = "No field named '%s' in %s'"
  396. msg %= (fieldname, target_class.__name__)
  397. return NameError(msg)
  398. ## @brief Prepares a relational filter
  399. #
  400. # Relational filters are composed of a tuple like the simple filters
  401. # but the first element of this tuple is also a tuple :
  402. #
  403. #<code>((FIELDNAME, {REF_CLASS: REF_FIELD}), OP, VALUE)</code>
  404. # Where :
  405. #- FIELDNAME is the field name is the target class
  406. #- the second element is a dict with :
  407. # - REF_CLASS as key. It's a LeObject child class
  408. # - REF_FIELD as value. The name of the referenced field in the REF_CLASS
  409. #
  410. # Visibly the REF_FIELD value of the dict will vary only when
  411. # no REF_FIELD is explicitly given in the filter string notation
  412. # and REF_CLASS classes have different uids
  413. #
  414. #@par String notation examples
  415. #<pre>contributeur IN (1,2,3,5)</pre> will be transformed into :
  416. #<pre>(
  417. # (
  418. # contributeur,
  419. # {
  420. # auteur: 'lodel_id',
  421. # traducteur: 'lodel_id'
  422. # }
  423. # ),
  424. # ' IN ',
  425. # [ 1,2,3,5 ])</pre>
  426. #@todo move the documentation to another place
  427. #
  428. #@param fieldname str : The relational field name
  429. #@param ref_field str|None : The referenced field name (if None uses
  430. # uniq identifiers as referenced field
  431. #@return a well formed relational filter tuple or an Exception instance
  432. def _prepare_relational_fields(self, fieldname, ref_field=None):
  433. datahandler = self._target_class.field(fieldname)
  434. # now we are going to fetch the referenced class to see if the
  435. # reference field is valid
  436. ref_classes = datahandler.linked_classes
  437. ref_dict = dict()
  438. if ref_field is None:
  439. for ref_class in ref_classes:
  440. ref_dict[ref_class] = ref_class.uid_fieldname
  441. else:
  442. r_ds = None
  443. for ref_class in ref_classes:
  444. if r_ds is None:
  445. r_ds = ref_class._datasource_name
  446. elif ref_class._datasource_name != r_ds:
  447. return RuntimeError("All referenced classes don't have the\
  448. same datasource. Query not possible")
  449. if ref_field in ref_class.fieldnames(True):
  450. ref_dict[ref_class] = ref_field
  451. else:
  452. msg = "Warning the class %s is not considered in \
  453. the relational filter %s"
  454. msg %= (ref_class.__name__, ref_field)
  455. logger.debug(msg)
  456. if len(ref_dict) == 0:
  457. return NameError("No field named '%s' in referenced objects [%s]"
  458. % (ref_field,
  459. ','.join([rc.__name__ for rc in ref_classes])))
  460. return (fieldname, ref_dict)
  461. ## @brief A query to insert a new object
  462. class LeInsertQuery(LeQuery):
  463. _hook_prefix = 'leapi_insert_'
  464. _data_check_args = {'complete': True, 'allow_internal': False}
  465. def __init__(self, target_class):
  466. if target_class.is_abstract():
  467. raise LeApiQueryError("Trying to create an insert query on an \
  468. abstract LeObject : %s" % target_class)
  469. super().__init__(target_class)
  470. #  @brief Implements an insert query operation, with only one insertion
  471. # @param data : data to be inserted
  472. def _query(self, data):
  473. data = self._target_class.prepare_datas(data, True, False)
  474. id_inserted = self._rw_datasource.insert(self._target_class, data)
  475. return id_inserted
  476. """
  477. ## @brief Implements an insert query operation, with multiple insertions
  478. # @param data : list of **data to be inserted
  479. def _query(self, data):
  480. nb_inserted = self._datasource.insert_multi(
  481. self._target_class,data_list)
  482. if nb_inserted < 0:
  483. raise LeApiQueryError("Multiple insertions error")
  484. return nb_inserted
  485. """
  486. #  @brief Executes the insert query
  487. def execute(self, data):
  488. return super().execute(data=data)
  489. ## @brief A query to update data for a given object
  490. #
  491. #@todo Change behavior, Huge optimization problem when updating using filters
  492. # and not instance. We have to run a GET and then one update by fetched object...
  493. class LeUpdateQuery(LeFilteredQuery):
  494. _hook_prefix = 'leapi_update_'
  495. _data_check_args = {'complete': False, 'allow_internal': False}
  496. ## @brief Instanciates an update query
  497. #
  498. # If a class and not an instance is given, no query_filters are expected
  499. # and the update will be fast and simple. Else we have to run a get query
  500. # before updating (to fetch data, update them and then, construct them
  501. # and check their consistency)
  502. #@param target LeObject clas or instance
  503. #@param query_filters list|None
  504. #@todo change strategy with instance update. We have to accept data for
  505. # the execute method
  506. def __init__(self, target, query_filters=None):
  507. ## @brief This attr is set only if the target argument is an
  508. # instance of a LeObject subclass
  509. self.__leobject_instance_datas = None
  510. target_class = target
  511. if not inspect.isclass(target):
  512. if query_filters is not None:
  513. msg = "No query_filters accepted when an instance is given as \
  514. target to LeUpdateQuery constructor"
  515. raise AttributeError(msg)
  516. target_class = target.__class__
  517. if target_class.initialized:
  518. self.__leobject_instance_datas = target.datas(True)
  519. else:
  520. query_filters = [(target._uid[0], '=', target.uid())]
  521. super().__init__(target_class, query_filters)
  522. ## @brief Implements an update query
  523. #@param data dict : data to be updated
  524. #@return the number of updated items
  525. #@todo change stategy for instance update. Data should be allowed
  526. # for execute method (and query)
  527. def _query(self, data):
  528. uid_name = self._target_class._uid[0]
  529. if self.__leobject_instance_datas is not None:
  530. # Instance update
  531. # Building query_filter
  532. filters = [(
  533. uid_name,
  534. '=',
  535. str(self.__leobject_instance_datas[uid_name]))]
  536. res = self._rw_datasource.update(
  537. self._target_class, filters, [],
  538. self.__leobject_instance_datas)
  539. else:
  540. # Update by filters, we have to fetch data before updating
  541. res = self._ro_datasource.select(
  542. self._target_class, self._target_class.fieldnames(True),
  543. self._query_filter[0],
  544. self._query_filter[1])
  545. # Checking and constructing data
  546. upd_data = dict()
  547. for res_data in res:
  548. res_data.update(data)
  549. res_data = self._target_class.prepare_datas(
  550. res_data, True, True)
  551. filters = [(uid_name, '=', res_data[uid_name])]
  552. res = self._rw_datasource.update(
  553. self._target_class, filters, [],
  554. res_data)
  555. return res
  556. #  @brief Execute the update query
  557. def execute(self, data=None):
  558. if self.__leobject_instance_datas is not None and data is not None:
  559. raise LeApiQueryError("No data expected when running an update \
  560. query on an instance")
  561. if self.__leobject_instance_datas is None and data is None:
  562. raise LeApiQueryError("Data are mandatory when running an update \
  563. query on a class with filters")
  564. return super().execute(data=data)
  565. ## @brief A query to delete an object
  566. class LeDeleteQuery(LeFilteredQuery):
  567. _hook_prefix = 'leapi_delete_'
  568. def __init__(self, target_class, query_filter):
  569. super().__init__(target_class, query_filter)
  570. #  @brief Executes the delete query
  571. # @param data
  572. def execute(self, data=None):
  573. return super().execute()
  574. ## @brief Implements delete query operations
  575. # @param data
  576. #@return the number of deleted items
  577. def _query(self, data=None):
  578. filters, rel_filters = self._query_filter
  579. nb_deleted = self._rw_datasource.delete(
  580. self._target_class, filters, rel_filters)
  581. return nb_deleted
  582. class LeGetQuery(LeFilteredQuery):
  583. _hook_prefix = 'leapi_get_'
  584. ## @brief Instanciates a new get query
  585. #@param target_class LeObject : class of object the query is about
  586. #@param query_filters dict : {OP, list of query filters}
  587. # or tuple (FIELD, OPERATOR, VALUE) )
  588. #@param kwargs dict : other query-related arguments and options
  589. # - field_list list|None : list of string representing fields see @ref leobject_filters
  590. # - order list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
  591. # - group list : A list of field names or tuple (FIELDNAME,[ASC | DESC])
  592. # - limit int : The maximum number of returned results
  593. # - offset int : offset
  594. def __init__(self, target_class, query_filters, **kwargs):
  595. super().__init__(target_class, query_filters)
  596. ## @brief The fields to get
  597. self._field_list = None
  598. ## @brief An equivalent to the SQL ORDER BY
  599. self._order = None
  600. ## @brief An equivalent to the SQL GROUP BY
  601. self._group = None
  602. ## @brief An equivalent to the SQL LIMIT x
  603. self._limit = None
  604. ## @brief An equivalent to the SQL LIMIT x, OFFSET
  605. self._offset = 0
  606. # Checking kwargs and assigning default values if there is some
  607. for argname in kwargs:
  608. if argname not in (
  609. 'field_list', 'order', 'group', 'limit', 'offset'):
  610. raise TypeError("Unexpected argument '%s'" % argname)
  611. if 'field_list' not in kwargs:
  612. self.set_field_list(target_class.fieldnames(include_ro=True))
  613. else:
  614. self.set_field_list(kwargs['field_list'])
  615. if 'order' in kwargs:
  616. # check kwargs['order']
  617. self._order = kwargs['order']
  618. if 'group' in kwargs:
  619. # check kwargs['group']
  620. self._group = kwargs['group']
  621. if 'limit' in kwargs and kwargs['limit'] is not None:
  622. try:
  623. self._limit = int(kwargs['limit'])
  624. if self._limit <= 0:
  625. raise ValueError()
  626. except ValueError:
  627. msg = "limit argument expected to be an interger > 0"
  628. raise ValueError(msg)
  629. if 'offset' in kwargs:
  630. try:
  631. self._offset = int(kwargs['offset'])
  632. if self._offset < 0:
  633. raise ValueError()
  634. except ValueError:
  635. msg = "offset argument expected to be an integer >= 0"
  636. raise ValueError(msg)
  637. ## @brief Set the field list
  638. # @param field_list list | None : If None use all fields
  639. # @return None
  640. # @throw LeApiQueryError if unknown field given
  641. def set_field_list(self, field_list):
  642. err_l = dict()
  643. if field_list is not None:
  644. for fieldname in field_list:
  645. ret = self._check_field(self._target_class, fieldname)
  646. if isinstance(ret, Exception):
  647. msg = "No field named '%s' in %s"
  648. msg %= (fieldname, self._target_class.__name__)
  649. expt = NameError(msg)
  650. err_l[fieldname] = expt
  651. if len(err_l) > 0:
  652. msg = "Error while setting field_list in a get query"
  653. raise LeApiQueryErrors(msg=msg, exceptions=err_l)
  654. self._field_list = list(set(field_list))
  655. ## @brief Executes the get query
  656. def execute(self, data=None):
  657. return super().execute()
  658. ## @brief Implements select query operations
  659. # @return a list containing the item(s)
  660. def _query(self, data=None):
  661. # select data corresponding to query_filter
  662. fl = list(self._field_list) if self._field_list is not None else None
  663. l_data = self._ro_datasource.select(
  664. target=self._target_class,
  665. field_list=fl,
  666. filters=self._query_filter[0],
  667. relational_filters=self._query_filter[1],
  668. order=self._order,
  669. group=self._group,
  670. limit=self._limit,
  671. offset=self._offset)
  672. return l_data
  673. ## @brief Returns a dict with query infos
  674. # @return a dict
  675. def dump_infos(self):
  676. ret = super().dump_infos()
  677. ret.update({'field_list': self._field_list,
  678. 'order': self._order,
  679. 'group': self._group,
  680. 'limit': self._limit,
  681. 'offset': self._offset,
  682. })
  683. return ret
  684. ## @brief Returns a string representation of the query
  685. # @return a string
  686. def __repr__(self):
  687. res = "<LeGetQuery target={target_class} filter={query_filter} \
  688. field_list={field_list} order={order} group={group} limit={limit} \
  689. offset={offset}"
  690. res = res.format(**self.dump_infos())
  691. if len(self.subqueries) > 0:
  692. for n, subq in enumerate(self.subqueries):
  693. res += "\n\tSubquerie %d : %s"
  694. res %= (n, subq)
  695. res += ">"
  696. return res