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.

leobject.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. #-*- coding: utf-8 -*-
  2. ## @package leobject API to access lodel datas
  3. #
  4. # This package contains abstract classes leobject.leclass.LeClass , leobject.letype.LeType, leobject.leobject._LeObject.
  5. # Those abstract classes are designed to be mother classes of dynamically generated classes ( see leobject.lefactory.LeFactory )
  6. ## @package leobject.leobject
  7. # @brief Abstract class designed to be implemented by LeObject
  8. #
  9. # @note LeObject will be generated by leobject.lefactory.LeFactory
  10. import re
  11. import leobject
  12. import EditorialModel
  13. from EditorialModel.types import EmType
  14. REL_SUP = 0
  15. REL_SUB = 1
  16. ## @brief Main class to handle objects defined by the types of an Editorial Model
  17. class _LeObject(object):
  18. ## @brief The editorial model
  19. _model = None
  20. ## @brief The datasource
  21. _datasource = None
  22. ## @brief maps em uid with LeType or LeClass keys are uid values are LeObject childs classes
  23. _me_uid = dict()
  24. _query_re = None
  25. _query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ']
  26. ## @brief Instantiate with a Model and a DataSource
  27. # @param **kwargs dict : datas usefull to instanciate a _LeObject
  28. def __init__(self, **kwargs):
  29. raise NotImplementedError("Abstract constructor")
  30. ## @brief Given a ME uid return the corresponding LeClass or LeType class
  31. # @return a LeType or LeClass child class
  32. # @throw KeyError if no corresponding child classes
  33. @classmethod
  34. def uid2leobj(cls, uid):
  35. uid = int(uid)
  36. if uid not in cls._me_uid:
  37. raise KeyError("No LeType or LeClass child classes with uid '%d'"%uid)
  38. return cls._me_uid[uid]
  39. ## @brief Creates new entries in the datasource
  40. # @param datas list : A list a dict with fieldname as key
  41. # @param cls
  42. # @return a list of inserted lodel_id
  43. # @see leobject.datasources.dummy.DummyDatasource.insert(), leobject.letype.LeType.insert()
  44. @classmethod
  45. def insert(cls, letype, datas):
  46. if cls == _LeObject:
  47. raise NotImplementedError("Abstract method")
  48. letype,leclass = cls._prepare_targets(letype)
  49. if letype is None:
  50. raise ValueError("letype argument cannot be None")
  51. for data in datas:
  52. letype.check_datas_or_raise(data, complete = True)
  53. return cls._datasource.insert(letype, leclass, datas)
  54. ## @brief Delete LeObjects given filters
  55. # @param cls
  56. # @param letype LeType|str : LeType child class or name
  57. # @param leclass LeClass|str : LeClass child class or name
  58. # @param filters list : list of filters (see @ref leobject_filters)
  59. # @return bool
  60. @classmethod
  61. def delete(cls, letype, leclass, filters):
  62. filters,relationnal_filters = leobject.leobject._LeObject._prepare_filters(filters, cls, cls._leclass)
  63. letype, leclass = cls._prepare_targets(letype, leclass)
  64. return cls._datasource(letype, leclass, filters, relationnal_filters)
  65. ## @brief Update LeObjects given filters and datas
  66. # @param cls
  67. # @param letype LeType|str : LeType child class or name
  68. # @param leclass LeClass|str : LeClass child class or name
  69. # @param filters list : list of filters (see @ref leobject_filters)
  70. @classmethod
  71. def update(cls, letype, leclass, filters, datas):
  72. filters,relationnal_filters = leobject.leobject._LeObject._prepare_filters(filters, cls, cls._leclass)
  73. letype, leclass = cls._prepare_targets(letype, leclass)
  74. if letype is None:
  75. raise ValueError("Argument letype cannot be None")
  76. letype.check_datas_or_raise(datas, False)
  77. return cls._datasource(letype, leclass, filters, relationnal_filters, datas)
  78. ## @brief make a search to retrieve a collection of LeObject
  79. # @param query_filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
  80. # @param field_list list|None : list of string representing fields see @ref leobject_filters
  81. # @param typename str : The name of the LeType we want
  82. # @param classname str : The name of the LeClass we want
  83. # @return responses ({string:*}): a list of dict with field:value
  84. def get(self, query_filters, field_list = None, typename = None, classname = None):
  85. letype,leclass = self._prepare_targets(typename, classname)
  86. #Fetching LeType
  87. if typename is None:
  88. if 'type_id' not in field_list:
  89. field_list.append('type_id')
  90. #Checking field_list
  91. if field_list is None:
  92. if not (letype is None):
  93. flist = letype._fields
  94. elif not (leclass is None):
  95. flist = leclass._fieldtypes.keys()
  96. else:
  97. flist = EditorialModel.classtype.common_fields.keys()
  98. else:
  99. LeFactory._check_fields(letype, leclass, field_list)
  100. #preparing filters
  101. filters, relationnal_filters = self._prepare_filters(query_filters, letype, leclass)
  102. #Fetching datas from datasource
  103. datas = self._datasource.get(emclass, emtype, field_list, filters, relational_filters)
  104. #Instanciating corresponding LeType child classes with datas
  105. result = list()
  106. for leobj_datas in datas:
  107. letype = self.uid2leobj(datas['type_id']) if letype is None else letype
  108. result.append(letype(datas))
  109. return result
  110. ## @brief Preparing letype and leclass arguments
  111. #
  112. # This function will do multiple things :
  113. # - Convert string to LeType or LeClass child instances
  114. # - If both letype and leclass given, check that letype inherit from leclass
  115. #  - If only a letype is given, fetch the parent leclass
  116. # @note If we give only a leclass as argument returned letype will be None
  117. # @note Its possible to give letype=None and leclass=None. In this case the method will return tuple(None,None)
  118. # @param letype LeType|str|None : LeType child instant or its name
  119. # @param leclass LeClass|str|None : LeClass child instant or its name
  120. # @return a tuple with 2 python classes (LeTypeChild, LeClassChild)
  121. @staticmethod
  122. def _prepare_targets(letype = None , leclass = None):
  123. if not(leclass is None):
  124. if isinstance(leclass, str):
  125. leclass = leobject.lefactory.LeFactory.leobj_from_name(leclass)
  126. if not isinstance(leclass, type) or not (leobject.leclass.LeClass in leclass.__bases__) or leclass.__class__ == leobject.leclass.LeClass:
  127. raise ValueError("None | str | LeType child class excpected, but got : '%s' %s"%(leclass,type(leclass)))
  128. if not(letype is None):
  129. if isinstance(letype, str):
  130. letype = leobject.lefactory.LeFactory.leobj_from_name(letype)
  131. if not isinstance(letype, type) or not leobject.letype.LeType in letype.__bases__ or letype.__class__ == leobject.letype.LeType:
  132. raise ValueError("None | str | LeType child class excpected, but got : %s"%type(letype))
  133. if leclass is None:
  134. leclass = letype._leclass
  135. elif leclass != letype._leclass:
  136. raise ValueError("LeType child class %s does'nt inherite from LeClass %s"%(letype.__name__, leclass.__name__))
  137. return (letype, leclass)
  138. ## @brief Check if a fieldname is valid
  139. # @param letype LeType|None : The concerned type (or None)
  140. # @param leclass LeClass|None : The concerned class (or None)
  141. # @param fields list : List of string representing fields
  142. # @throw LeObjectQueryError if their is some problems
  143. # @throw AttributeError if letype is not from the leclass class
  144. # @todo Delete the checks of letype and leclass and ensure that this method is called with letype and leclass arguments from _prepare_targets()
  145. #
  146. # @see @ref leobject_filters
  147. @staticmethod
  148. def _check_fields(letype, leclass, fields):
  149. #Checking that fields in the query_filters are correct
  150. if letype is None and leclass is None:
  151. #Only fields from the object table are allowed
  152. for field in fields:
  153. if field not in EditorialModel.classtypes.common_fields.keys():
  154. raise LeObjectQueryError("Not typename and no classname given, but the field %s is not in the common_fields list"%field)
  155. else:
  156. if letype is None:
  157. field_l = leclass._fieldtypes.keys()
  158. else:
  159. if not (leclass is None):
  160. if letype._leclass != leclass:
  161. raise AttributeError("The EmType %s is not a specialisation of the EmClass %s"%(typename, classname))
  162. field_l = letype._fields
  163. #Checks that fields are in this type
  164. for field in fields:
  165. if field not in field_l:
  166. raise LeObjectQueryError("No field named '%s' in '%s'"%(field, typename))
  167. pass
  168. ## @brief Prepare filters for datasource
  169. #
  170. # This method divide filters in two categories :
  171. # - filters : standart FIELDNAME OP VALUE filter
  172. # - relationnal_filters : filter on object relation RELATION_NATURE OP VALUE
  173. #
  174. # Both categories of filters are represented in the same way, a tuple with 3 elements (NAME|NAT , OP, VALUE )
  175. #
  176. # @warning This method assume that letype and leclass are returned from _LeObject._prepare_targets() method
  177. # @param filters_l list : This list can contain str "FIELDNAME OP VALUE" and tuples (FIELDNAME, OP, VALUE)
  178. # @param letype LeType|None : needed to check filters
  179. # @param leclass LeClass|None : needed to check filters
  180. # @return a tuple(FILTERS, RELATIONNAL_FILTERS
  181. #
  182. # @see @ref datasource_side
  183. @staticmethod
  184. def _prepare_filters(filters_l, letype = None, leclass = None):
  185. filters = list()
  186. for fil in filters_l:
  187. if len(fil) == 3 and not isinstance(fil, str):
  188. filters.append(tuple(fil))
  189. else:
  190. filters.append(_LeObject._split_filter(fil))
  191. #Checking relational filters (for the moment fields like superior.NATURE)
  192. relational_filters = [ (_LeObject._prepare_relational_field(field), operator, value) for field, operator, value in filters if _LeObject._field_is_relational(field)]
  193. filters = [f for f in filters if not _LeObject._field_is_relational(f[0])]
  194. #Checking the rest of the fields
  195. _LeObject._check_fields(letype, leclass, [ f[0] for f in filters ])
  196. return (filters, relational_filters)
  197. ## @brief Check if a field is relational or not
  198. # @param field str : the field to test
  199. # @return True if the field is relational else False
  200. @staticmethod
  201. def _field_is_relational(field):
  202. return field.startswith('superior.') or field.startswith('subordinate')
  203. ## @brief Check that a relational field is valid
  204. # @param field str : a relational field
  205. # @return a nature
  206. @staticmethod
  207. def _prepare_relational_field(field):
  208. spl = field.split('.')
  209. if len(spl) != 2:
  210. raise LeObjectQueryError("The relationalfield '%s' is not valid"%field)
  211. nature = spl[-1]
  212. if nature not in EditorialModel.classtypes.EmNature.getall():
  213. raise LeObjectQueryError("'%s' is not a valid nature in the field %s"%(nature, field))
  214. if spl[0] == 'superior':
  215. return (REL_SUP, nature)
  216. elif spl[0] == 'subordinate':
  217. return (REL_SUB, nature)
  218. else:
  219. raise LeObjectQueryError("Invalid preffix for relationnal field : '%s'"%spl[0])
  220. ## @brief Check and split a query filter
  221. # @note The query_filter format is "FIELD OPERATOR VALUE"
  222. # @param query_filter str : A query_filter string
  223. # @param cls
  224. # @return a tuple (FIELD, OPERATOR, VALUE)
  225. @classmethod
  226. def _split_filter(cls, query_filter):
  227. if cls._query_re is None:
  228. cls._compile_query_re()
  229. matches = cls._query_re.match(query_filter)
  230. if not matches:
  231. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  232. result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
  233. for r in result:
  234. if len(r) == 0:
  235. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  236. return result
  237. ## @brief Compile the regex for query_filter processing
  238. # @note Set _LeObject._query_re
  239. @classmethod
  240. def _compile_query_re(cls):
  241. op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
  242. for operator in cls._query_operators[1:]:
  243. op_re_piece += '|(%s)'%operator.replace(' ', '\s')
  244. op_re_piece += ')'
  245. 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)
  246. pass
  247. class LeObjectError(Exception):
  248. pass
  249. class LeObjectQueryError(LeObjectError):
  250. pass
  251. ## @page leobject_filters LeObject query filters
  252. # The LeObject API provide methods that accept filters allowing the user
  253. # to query the database and fetch LodelEditorialObjects.
  254. #
  255. # The LeObject API translate those filters for the datasource.
  256. #
  257. # @section api_user_side API user side filters
  258. # Filters are string expressing a condition. The string composition
  259. # is as follow : "<FIELD> <OPERATOR> <VALUE>"
  260. # @subsection fpart FIELD
  261. # @subsubsection standart fields
  262. # Standart fields, represents a value of the LeObject for example "title", "lodel_id" etc.
  263. # @subsubsection rfields relationnal fields
  264. # relationnal fields, represents a relation with the object hierarchy. Those fields are composed as follow :
  265. # "<RELATION>.<NATURE>".
  266. #
  267. # - Relation can takes two values : superiors or subordinates
  268. # - Nature is a relation nature ( see EditorialModel.classtypes )
  269. # Examples : "superiors.parent", "subordinates.translation" etc.
  270. # @note The field_list arguement of leobject.leobject._LeObject.get() use the same syntax than the FIELD filter part
  271. # @subsection oppart OPERATOR
  272. # The OPERATOR part of a filter is a comparison operator. There is
  273. # - standart comparison operators : = , <, > , <=, >=, !=
  274. # - list operators : 'in' and 'not in'
  275. # The list of allowed operators is sotred at leobject.leobject._LeObject._query_operators .
  276. # @subsection valpart VALUE
  277. # The VALUE part of a filter is... just a value...
  278. #
  279. # @section datasource_side Datasource side filters
  280. # As said above the API "translate" filters before forwarding them to the datasource.
  281. #
  282. # The translation process transform filters in tuple composed of 3 elements
  283. # ( @ref fpart , @ref oppart , @ref valpart ). Each element is a string.
  284. #
  285. # There is a special case for @ref rfields : the field element is a tuple composed with two elements
  286. # ( RELATION, NATURE ) where NATURE is a string ( see EditorialModel.classtypes ) and RELATION is one of
  287. # the defined constant :
  288. #
  289. # - leobject.leobject.REL_SUB for "subordinates"
  290. # - leobject.leobject.REL_SUP for "superiors"
  291. #
  292. # @note The filters translation process also check if given field are valids compared to the concerned letype and/or the leclass