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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. #-*- coding: utf-8 -*-
  2. ## @package leapi.lecrud
  3. # @brief This package contains the abstract class representing Lodel Editorial components
  4. #
  5. import importlib
  6. import re
  7. import EditorialModel.fieldtypes.generic
  8. ## @brief Main class to handler lodel editorial components (relations and objects)
  9. class _LeCrud(object):
  10. ## @brief The datasource
  11. _datasource = None
  12. ## @brief abstract property to store the fieldtype representing the component identifier
  13. _uid_fieldtype = None #Will be a dict fieldname => fieldtype
  14. ## @brief will store all the fieldtypes (child classes handle it)
  15. _fieldtypes_all = None
  16. ## @brief Stores a regular expression to parse query filters strings
  17. _query_re = None
  18. ## @brief Stores Query filters operators
  19. _query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ']
  20. def __init__(self):
  21. raise NotImplementedError("Abstract class")
  22. ## @brief Given a dynamically generated class name return the corresponding python Class
  23. # @param name str : a concrete class name
  24. # @return False if no such component
  25. @classmethod
  26. def name2class(cls, name):
  27. mod = importlib.import_module(cls.__module__)
  28. try:
  29. return getattr(mod, name)
  30. except AttributeError:
  31. return False
  32. @classmethod
  33. def leobject(cls):
  34. return cls.name2class('LeObject')
  35. ## @return A dict with key field name and value a fieldtype instance
  36. @classmethod
  37. def fieldtypes(cls):
  38. raise NotImplementedError("Abstract method") #child classes should return their uid fieldtype
  39. ## @return A dict with fieldtypes marked as internal
  40. @classmethod
  41. def fieldtypes_internal(self):
  42. return { fname: ft for fname, ft in cls.fieldtypes().items() if hasattr(ft, 'internal') and ft.internal }
  43. ## @brief Check fields values
  44. # @param complete bool : If True expect that datas contains all fieldtypes with no default values
  45. # @param allow_internal bool : If False consider datas as invalid if a value is given for an internal fieldtype
  46. # @return None if no errors, else returns a list of Exception instances that occurs during the check
  47. @classmethod
  48. def check_datas_errors(cls, complete = False, allow_internal = False, **datas):
  49. intern_fields_name = cls.fieldtypes_internal().keys()
  50. fieldtypes = cls.fieldtypes()
  51. err_l = list()
  52. for dname, dval in datas.items():
  53. if not allow_internal and dname in intern_fields_name:
  54. err_l.append(AttributeError("The field '%s' is internal"%dname))
  55. if dname not in fieldtypes.keys():
  56. err_l.append(AttributeError("No field named '%s' in %s"%(dname, cls.__name__)))
  57. check_ret = cls.fieldtypes[dname].check_error(dval)
  58. if not(ret is None):
  59. err_l += check_ret
  60. if complete:
  61. #mandatory are fields with no default values
  62. mandatory_fields = set([ ft for fname, ft in fieldtypes.items() if not hasattr(ft, 'default')])
  63. #internal fields are considered as having default values (or calculated values)
  64. mandatory_fields -= intern_fields_name
  65. missing = mandatory_fields - set(datas.keys())
  66. if len(missing) > 0:
  67. for missing_field_name in missing:
  68. err_l.append(AttributeError("Value for field '%s' is missing"%missing_field_name))
  69. return None if len(err_l) == 0 else err_l
  70. ## @brief Check fields values
  71. # @param complete bool : If True expect that datas contains all fieldtypes with no default values
  72. # @param allow_internal bool : If False consider datas as invalid if a value is given for an internal fieldtype
  73. # @throw LeApiDataCheckError if checks fails
  74. # @return None
  75. @classmethod
  76. def check_datas_or_raises(cls, complete = False, allow_internal = False, **datas):
  77. ret = cls.check_datas_errors(complete, allow_internal, **datas)
  78. if not(ret is None):
  79. raise LeApiDataCheckError(ret)
  80. @classmethod
  81. def fieldlist(cls):
  82. return cls.fieldtypes().keys()
  83. ## @brief Retrieve a collection of lodel editorial components
  84. #
  85. # @param query_filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
  86. # @param field_list list|None : list of string representing fields see @ref leobject_filters
  87. # @return A list of lodel editorial components instance
  88. # @todo think about LeObject and LeClass instanciation (partial instanciation, etc)
  89. @classmethod
  90. def get(cls, query_filters, field_list = None):
  91. if not(isinstance(cls, cls.name2class('LeObject'))) and not(isinstance(cls, cls.name2class('LeRelation'))):
  92. raise NotImplementedError("Cannot call get with LeCrud")
  93. if field_list is None or len(field_list) == 0:
  94. #default field_list
  95. field_list = cls.default_fieldlist()
  96. field_list = cls.prepare_field_list(field_list) #Can raise LeApiDataCheckError
  97. #preparing filters
  98. filters, relational_filters = cls._prepare_filters(field_list)
  99. #Fetching datas from datasource
  100. db_datas = cls._datasource.get(cls, filters, relational_filters)
  101. return [ cls(**datas) for datas in db_datas]
  102. ## @brief Prepare a field_list
  103. # @param field_list list : List of string representing fields
  104. # @return A well formated field list
  105. # @throw LeApiDataCheckError if invalid field given
  106. @classmethod
  107. def _prepare_field_list(cls, field_list):
  108. ret_field_list = list()
  109. for field in field_list:
  110. if cls._field_is_relational(field):
  111. ret = cls._prepare_relational_field(field)
  112. else:
  113. ret = cls._check_field(field)
  114. if isinstance(ret, Exception):
  115. err_l.append(ret)
  116. else:
  117. ret_field_list.append(ret)
  118. if len(err_l) > 0:
  119. raise LeApiDataCheckError(err_l)
  120. return ret_field_list
  121. ## @brief Check that a relational field is valid
  122. # @param field str : a relational field
  123. # @return a nature
  124. @classmethod
  125. def _prepare_relational_fields(cls, field):
  126. raise NotImplementedError("Abstract method")
  127. ## @brief Check that the field list only contains fields that are in the current class
  128. # @return None if no problem, else returns a list of exceptions that occurs during the check
  129. @classmethod
  130. def _check_field(cls, field):
  131. err_l = list()
  132. if field not in cls.fieldlist():
  133. return ValueError("No such field '%s' in %s"%(field, cls.__name__))
  134. return field
  135. ## @brief Check if a field is relational or not
  136. # @param field str : the field to test
  137. # @return True if the field is relational else False
  138. @staticmethod
  139. def _field_is_relational(field):
  140. return field.startswith('superior.') or field.startswith('subordinate')
  141. ## @brief Prepare filters for datasource
  142. #
  143. # This method divide filters in two categories :
  144. # - filters : standart FIELDNAME OP VALUE filter
  145. # - relationnal_filters : filter on object relation RELATION_NATURE OP VALUE
  146. #
  147. # Both categories of filters are represented in the same way, a tuple with 3 elements (NAME|NAT , OP, VALUE )
  148. #
  149. # @param filters_l list : This list can contain str "FIELDNAME OP VALUE" and tuples (FIELDNAME, OP, VALUE)
  150. # @return a tuple(FILTERS, RELATIONNAL_FILTERS
  151. #
  152. # @see @ref datasource_side
  153. @classmethod
  154. def _prepare_filters(cls, filters_l):
  155. filters = list()
  156. res_filters = list()
  157. rel_filters = list()
  158. err_l = list()
  159. for fil in filters_l:
  160. if len(fil) == 3 and not isinstance(fil, str):
  161. filters.append(tuple(fil))
  162. else:
  163. filters.append(cls._split_filter(fil))
  164. for field, operator, value in filters:
  165. if cls._field_is_relational(field):
  166. #Checks relational fields
  167. ret = cls._prepare_relational_field(field)
  168. if isinstance(ret, Exception):
  169. err_l.append(ret)
  170. else:
  171. rel_filters.append((ret, operator, value))
  172. else:
  173. #Checks other fields
  174. ret = cls._check_field(field)
  175. if isinstance(ret, Exception):
  176. err_l.append(ret)
  177. else:
  178. res_filters.append((field,operator, value))
  179. if len(err_l) > 0:
  180. raise LeApiDataCheckError(err_l)
  181. return (res_filters, rel_filters)
  182. ## @brief Check and split a query filter
  183. # @note The query_filter format is "FIELD OPERATOR VALUE"
  184. # @param query_filter str : A query_filter string
  185. # @param cls
  186. # @return a tuple (FIELD, OPERATOR, VALUE)
  187. @classmethod
  188. def _split_filter(cls, query_filter):
  189. if cls._query_re is None:
  190. cls._compile_query_re()
  191. matches = cls._query_re.match(query_filter)
  192. if not matches:
  193. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  194. result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
  195. for r in result:
  196. if len(r) == 0:
  197. raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
  198. return result
  199. ## @brief Compile the regex for query_filter processing
  200. # @note Set _LeObject._query_re
  201. @classmethod
  202. def _compile_query_re(cls):
  203. op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
  204. for operator in cls._query_operators[1:]:
  205. op_re_piece += '|(%s)'%operator.replace(' ', '\s')
  206. op_re_piece += ')'
  207. 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)
  208. pass
  209. class LeApiQueryError(EditorialModel.fieldtypes.generic.FieldTypeDataCheckError):
  210. pass
  211. class LeApiDataCheckError(EditorialModel.fieldtypes.generic.FieldTypeDataCheckError):
  212. pass