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.

base_classes.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. #-*- co:ding: utf-8 -*-
  2. ## @package lodel.leapi.datahandlers.base_classes Define all base/abstract class for data handlers
  3. #
  4. # Contains custom exceptions too
  5. import copy
  6. import importlib
  7. import inspect
  8. import warnings
  9. from lodel.exceptions import *
  10. from lodel import logger
  11. ##@brief Base class for all data handlers
  12. #@ingroup lodel2_datahandlers
  13. class DataHandler(object):
  14. _HANDLERS_MODULES = ('datas_base', 'datas', 'references')
  15. ##@brief Stores the DataHandler childs classes indexed by name
  16. _base_handlers = None
  17. ##@brief Stores custom datahandlers classes indexed by name
  18. # @todo do it ! (like plugins, register handlers... blablabla)
  19. __custom_handlers = dict()
  20. help_text = 'Generic Field Data Handler'
  21. ##@brief List fields that will be exposed to the construct_data_method
  22. _construct_datas_deps = []
  23. directly_editable = True
  24. ##@brief constructor
  25. # @param internal False | str : define whether or not a field is internal
  26. # @param immutable bool : indicates if the fieldtype has to be defined in child classes of LeObject or if it is
  27. # designed globally and immutable
  28. # @param **args
  29. # @throw NotImplementedError if it is instanciated directly
  30. def __init__(self, **kwargs):
  31. if self.__class__ == DataHandler:
  32. raise NotImplementedError("Abstract class")
  33. self.__arguments = kwargs
  34. self.nullable = True
  35. self.uniq = False
  36. self.immutable = False
  37. self.primary_key = False
  38. self.internal = False
  39. if 'default' in kwargs:
  40. self.default, error = self.check_data_value(kwargs['default'])
  41. if error:
  42. raise error
  43. del(kwargs['default'])
  44. for argname, argval in kwargs.items():
  45. setattr(self, argname, argval)
  46. ## Fieldtype name
  47. @classmethod
  48. def name(cls):
  49. return cls.__module__.split('.')[-1]
  50. @classmethod
  51. def is_reference(cls):
  52. return issubclass(cls, Reference)
  53. @classmethod
  54. def is_singlereference(cls):
  55. return issubclass(cls, SingleRef)
  56. def is_primary_key(self):
  57. return self.primary_key
  58. ##@brief checks if a fieldtype is internal
  59. # @return bool
  60. def is_internal(self):
  61. return self.internal is not False
  62. ##brief check if a value can be nullable
  63. #@param value *
  64. #@throw DataNoneValid if value is None and nullable. LodelExceptions if not nullable
  65. #@return value (if not None)
  66. # @return value
  67. def _check_data_value(self, value):
  68. if value is None:
  69. if not self.nullable:
  70. raise LodelExceptions("None value is forbidden for this data field")
  71. raise DataNoneValid("None with a nullable. This exeption is allowed")
  72. return value
  73. ##@brief calls the data_field (defined in derived class) _check_data_value() method
  74. #@param value *
  75. #@return tuple (value|None, None|error) value can be cast if NoneError
  76. def check_data_value(self, value):
  77. try:
  78. value = self._check_data_value(value)
  79. except DataNoneValid as expt:
  80. return value, None
  81. except (LodelExceptions, FieldValidationError) as expt:
  82. return None, expt
  83. return value, None
  84. ##@brief checks if this class can override the given data handler
  85. # @param data_handler DataHandler
  86. # @return bool
  87. def can_override(self, data_handler):
  88. if data_handler.__class__.base_type != self.__class__.base_type:
  89. return False
  90. return True
  91. ##@brief Build field value
  92. #@ingroup lodel2_dh_checks
  93. #@warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  94. #@ref _construct_data() and @ref lodel2_dh_check_impl )
  95. #@param emcomponent EmComponent : An EmComponent child class instance
  96. #@param fname str : The field name
  97. #@param datas dict : dict storing fields values (from the component)
  98. #@param cur_value : the value from the current field (identified by fieldname)
  99. #@return the value
  100. #@throw RunTimeError if data construction fails
  101. #@todo raise something else
  102. def construct_data(self, emcomponent, fname, datas, cur_value):
  103. emcomponent_fields = emcomponent.fields()
  104. data_handler = None
  105. if fname in emcomponent_fields:
  106. data_handler = emcomponent_fields[fname]
  107. new_val = cur_value
  108. if fname in datas.keys():
  109. pass
  110. elif data_handler is not None and hasattr(data_handler, 'default'):
  111. new_val = data_handler.default
  112. elif data_handler is not None and data_handler.nullable:
  113. new_val = None
  114. return self._construct_data(emcomponent, fname, datas, new_val)
  115. ##@brief Designed to be reimplemented by child classes
  116. #@param emcomponent EmComponent : An EmComponent child class instance
  117. #@param fname str : The field name
  118. #@param datas dict : dict storing fields values (from the component)
  119. #@param cur_value : the value from the current field (identified by fieldname)
  120. #@return the value
  121. #@see construct_data() lodel2_dh_check_impl
  122. def _construct_data(self, empcomponent, fname, datas, cur_value):
  123. return cur_value
  124. ##@brief Check datas consistency
  125. #@ingroup lodel2_dh_checks
  126. #@warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  127. #@ref _construct_data() and @ref lodel2_dh_check_impl )
  128. #@warning the datas argument looks like a dict but is not a dict
  129. #see @ref base_classes.DatasConstructor "DatasConstructor" and
  130. #@ref lodel2_dh_datas_construction "Datas construction section"
  131. #@param emcomponent EmComponent : An EmComponent child class instance
  132. #@param fname : the field name
  133. #@param datas dict : dict storing fields values
  134. #@return an Exception instance if fails else True
  135. #@todo A implémenter
  136. def check_data_consistency(self, emcomponent, fname, datas):
  137. return self._check_data_consistency(emcomponent, fname, datas)
  138. ##@brief Designed to be reimplemented by child classes
  139. #@param emcomponent EmComponent : An EmComponent child class instance
  140. #@param fname : the field name
  141. #@param datas dict : dict storing fields values
  142. #@return an Exception instance if fails else True
  143. #@see check_data_consistency() lodel2_dh_check_impl
  144. def _check_data_consistency(self, emcomponent, fname, datas):
  145. return True
  146. ##@brief make consistency after a query
  147. # @param emcomponent EmComponent : An EmComponent child class instance
  148. # @param fname : the field name
  149. # @param datas dict : dict storing fields values
  150. # @return an Exception instance if fails else True
  151. # @todo A implémenter
  152. def make_consistency(self, emcomponent, fname, datas):
  153. pass
  154. ##@brief This method is use by plugins to register new data handlers
  155. @classmethod
  156. def register_new_handler(cls, name, data_handler):
  157. if not inspect.isclass(data_handler):
  158. raise ValueError("A class was expected but %s given" % type(data_handler))
  159. if not issubclass(data_handler, DataHandler):
  160. raise ValueError("A data handler HAS TO be a child class of DataHandler")
  161. cls.__custom_handlers[name] = data_handler
  162. ##@brief Load all datahandlers
  163. @classmethod
  164. def load_base_handlers(cls):
  165. if cls._base_handlers is None:
  166. cls._base_handlers = dict()
  167. for module_name in cls._HANDLERS_MODULES:
  168. module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name)
  169. for name, obj in inspect.getmembers(module):
  170. if inspect.isclass(obj):
  171. logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__))
  172. cls._base_handlers[name.lower()] = obj
  173. return copy.copy(cls._base_handlers)
  174. ##@brief given a field type name, returns the associated python class
  175. # @param fieldtype_name str : A field type name (not case sensitive)
  176. # @return DataField child class
  177. # @note To access custom data handlers it can be cool to prefix the handler name by plugin name for example ? (to ensure name unicity)
  178. @classmethod
  179. def from_name(cls, name):
  180. cls.load_base_handlers()
  181. all_handlers = dict(cls._base_handlers, **cls.__custom_handlers)
  182. name = name.lower()
  183. if name not in all_handlers:
  184. raise NameError("No data handlers named '%s'" % (name,))
  185. return all_handlers[name]
  186. ##@brief Return the module name to import in order to use the datahandler
  187. # @param data_handler_name str : Data handler name
  188. # @return a str
  189. @classmethod
  190. def module_name(cls, name):
  191. name = name.lower()
  192. handler_class = cls.from_name(name)
  193. return '{module_name}.{class_name}'.format(
  194. module_name = handler_class.__module__,
  195. class_name = handler_class.__name__
  196. )
  197. ##@brief __hash__ implementation for fieldtypes
  198. def __hash__(self):
  199. hash_dats = [self.__class__.__module__]
  200. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  201. hash_dats.append((kdic, getattr(self, kdic)))
  202. return hash(tuple(hash_dats))
  203. ##@brief Base class for datas data handler (by opposition with references)
  204. #@ingroup lodel2_datahandlers
  205. class DataField(DataHandler):
  206. pass
  207. ##@brief Abstract class for all references
  208. #@ingroup lodel2_datahandlers
  209. #
  210. # References are fields that stores a reference to another
  211. # editorial object
  212. #@todo Construct data implementation : transform the data into a LeObject
  213. #instance
  214. #@todo Check data consistency implementation : check that LeObject instance
  215. #is from an allowed class
  216. class Reference(DataHandler):
  217. base_type="ref"
  218. ##@brief Instanciation
  219. # @param allowed_classes list | None : list of allowed em classes if None no restriction
  220. # @param back_reference tuple | None : tuple containing (LeObject child class, fieldname)
  221. # @param internal bool : if False, the field is not internal
  222. # @param **kwargs : other arguments
  223. def __init__(self, allowed_classes = None, back_reference = None, internal=False, **kwargs):
  224. self.__allowed_classes = set() if allowed_classes is None else set(allowed_classes)
  225. self.allowed_classes = list() if allowed_classes is None else allowed_classes
  226. if back_reference is not None:
  227. if len(back_reference) != 2:
  228. raise ValueError("A tuple (classname, fieldname) expected but got '%s'" % back_reference)
  229. #if not issubclass(lodel.leapi.leobject.LeObject, back_reference[0]) or not isinstance(back_reference[1], str):
  230. # raise TypeError("Back reference was expected to be a tuple(<class LeObject>, str) but got : (%s, %s)" % (back_reference[0], back_reference[1]))
  231. self.__back_reference = back_reference
  232. super().__init__(internal=internal, **kwargs)
  233. ##@brief Method designed to return an empty value for this kind of
  234. #multipleref
  235. @classmethod
  236. def empty(cls):
  237. return None
  238. ##@brief Property that takes value of a copy of the back_reference tuple
  239. @property
  240. def back_reference(self):
  241. return copy.copy(self.__back_reference)
  242. ##@brief Property that takes value of datahandler of the backreference or
  243. #None
  244. @property
  245. def back_ref_datahandler(self):
  246. if self.__back_reference is None:
  247. return None
  248. return self.__back_reference[0].data_handler(self.__back_reference[1])
  249. @property
  250. def linked_classes(self):
  251. return copy.copy(self.__allowed_classes)
  252. ##@brief Set the back reference for this field.
  253. def _set_back_reference(self, back_reference):
  254. self.__back_reference = back_reference
  255. ##@brief Check and cast value in appropriate type
  256. #@param value *
  257. #@throw FieldValidationError if value is an appropriate type
  258. #@return value
  259. #@todo implement the check when we have LeObject uid check value
  260. def _check_data_value(self, value):
  261. from lodel.leapi.leobject import LeObject
  262. value = super()._check_data_value(value)
  263. if not (hasattr(value, '__class__') and
  264. issubclass(value.__class__, LeObject)):
  265. if self.__allowed_classes:
  266. rcls = list(self.__allowed_classes)[0]
  267. uidname = rcls.uid_fieldname()[0]# TODO multiple uid is broken
  268. uiddh = rcls.data_handler(uidname)
  269. value = uiddh._check_data_value(value)
  270. else:
  271. raise FieldValidationError("Reference datahandler can not check this value %s if any allowed_class is allowed." % value)
  272. return value
  273. ##@brief Check datas consistency
  274. #@param emcomponent EmComponent : An EmComponent child class instance
  275. #@param fname : the field name
  276. #@param datas dict : dict storing fields values
  277. #@return an Exception instance if fails else True
  278. #@todo check for performance issue and check logics
  279. #@todo Implements consistency checking on value : Check that the given value
  280. #points onto an allowed class
  281. #@warning composed uid capabilities broken here
  282. def check_data_consistency(self, emcomponent, fname, datas):
  283. rep = super().check_data_consistency(emcomponent, fname, datas)
  284. if isinstance(rep, Exception):
  285. return rep
  286. if self.back_reference is None:
  287. return True
  288. # !! Reimplement instance fetching in construct data !!
  289. dh = emcomponent.field(fname)
  290. uid = datas[emcomponent.uid_fieldname()[0]] #multi uid broken here
  291. target_class = self.back_reference[0]
  292. target_field = self.back_reference[1]
  293. target_uidfield = target_class.uid_fieldname()[0] #multi uid broken here
  294. value = datas[fname]
  295. obj = target_class.get([(target_uidfield , '=', value)])
  296. if len(obj) == 0:
  297. logger.warning('Object referenced does not exist')
  298. return False
  299. return True
  300. ##@brief This class represent a data_handler for single reference to another object
  301. #
  302. # The fields using this data handlers are like "foreign key" on another object
  303. class SingleRef(Reference):
  304. def __init__(self, allowed_classes = None, **kwargs):
  305. super().__init__(allowed_classes = allowed_classes)
  306. ##@brief Check and cast value in appropriate type
  307. #@param value: *
  308. #@throw FieldValidationError if value is unappropriate or can not be cast
  309. #@return value
  310. def _check_data_value(self, value):
  311. value = super()._check_data_value(value)
  312. logger.warning("A vérifier..provisoire pour les tests")
  313. #if (expt is None and (len(val)>1)):
  314. # raise FieldValidationError("List or string expected for a set field")
  315. return value
  316. ##@brief This class represent a data_handler for multiple references to another object
  317. #@ingroup lodel2_datahandlers
  318. #
  319. # The fields using this data handlers are like SingleRef but can store multiple references in one field
  320. # @note for the moment split on ',' chars
  321. class MultipleRef(Reference):
  322. ##
  323. # @param max_item int | None : indicate the maximum number of item referenced by this field, None mean no limit
  324. def __init__(self, max_item = None, **kwargs):
  325. self.max_item = max_item
  326. super().__init__(**kwargs)
  327. ##@brief Method designed to return an empty value for this kind of
  328. #multipleref
  329. @classmethod
  330. def empty(cls):
  331. return []
  332. ##@brief Check and cast value in appropriate type
  333. #@param value *
  334. #@throw FieldValidationError if value is unappropriate or can not be cast
  335. #@return value
  336. #@TODO Writing test error for errors when stored multiple references in one field
  337. def _check_data_value(self, value):
  338. value = DataHandler._check_data_value(self,value)
  339. if not hasattr(value, '__iter__'):
  340. raise FieldValidationError("MultipleRef has to be an iterable or a string, '%s' found" % value)
  341. if self.max_item is not None:
  342. if self.max_item < len(value):
  343. raise FieldValidationError("Too many items")
  344. new_val = list()
  345. error_list = list()
  346. for i,v in enumerate(value):
  347. try:
  348. v = super()._check_data_value(v)
  349. new_val.append(v)
  350. except (FieldValidationError) as f:
  351. error_list.append(repr(v))
  352. if len(error_list) > 0:
  353. raise FieldValidationError("MultipleRef have for invalid values [%s] :" % (",".join(error_list)))
  354. return new_val
  355. ##@brief Construct a multiple ref data
  356. def construct_data(self, emcomponent, fname, datas, cur_value):
  357. cur_value = super().construct_data(emcomponent, fname, datas, cur_value)
  358. if cur_value is not None:
  359. if self.back_reference is not None:
  360. br_class = self.back_reference[0]
  361. for br_id in cur_value:
  362. query_filters = list()
  363. query_filters.append((br_class.uid_fieldname()[0], '=', br_id))
  364. br_obj = br_class.get(query_filters)
  365. if len(br_obj) != 0:
  366. br_list = br_obj[0].data(self.back_reference[1])
  367. if br_list is None:
  368. br_list = list()
  369. if br_id not in br_list:
  370. br_list.append(br_id)
  371. return cur_value
  372. ## @brief Checks the backreference, updates it if it is not complete
  373. # @param emcomponent EmComponent : An EmComponent child class instance
  374. # @param fname : the field name
  375. # @param datas dict : dict storing fields values
  376. # @note Not done in case of delete
  377. def make_consistency(self, emcomponent, fname, datas, type_query):
  378. dh = emcomponent.field(fname)
  379. logger.info('Warning : multiple uid capabilities are broken here')
  380. uid = datas[emcomponent.uid_fieldname()[0]]
  381. if self.back_reference is not None:
  382. target_class = self.back_reference[0]
  383. target_field = self.back_reference[1]
  384. target_uidfield = target_class.uid_fieldname()[0]
  385. l_value = datas[fname]
  386. if l_value is not None:
  387. for value in l_value:
  388. query_filters = list()
  389. query_filters.append((target_uidfield , '=', value))
  390. obj = target_class.get(query_filters)
  391. if len(obj) == 0:
  392. logger.warning('Object referenced does not exist')
  393. return False
  394. l_uids_ref = obj[0].data(target_field)
  395. if l_uids_ref is None:
  396. l_uids_ref = list()
  397. if uid not in l_uids_ref:
  398. l_uids_ref.append(uid)
  399. obj[0].set_data(target_field, l_uids_ref)
  400. obj[0].update()
  401. if type_query == 'update':
  402. query_filters = list()
  403. query_filters.append((uid, ' in ', target_field))
  404. objects = target_class.get(query_filters)
  405. if l_value is None:
  406. l_value = list()
  407. if len(objects) != len(l_value):
  408. for obj in objects:
  409. l_uids_ref = obj.data(target_field)
  410. if obj.data(target_uidfield) not in l_value:
  411. l_uids_ref.remove(uid)
  412. obj.set_data(target_field, l_uids_ref)
  413. obj.update()
  414. ## @brief Class designed to handle datas access will fieldtypes are constructing datas
  415. #@ingroup lodel2_datahandlers
  416. #
  417. # This class is designed to allow automatic scheduling of construct_data calls.
  418. #
  419. # In theory it's able to detect circular dependencies
  420. # @todo test circular deps detection
  421. # @todo test circulat deps false positiv
  422. class DatasConstructor(object):
  423. ## @brief Init a DatasConstructor
  424. # @param lec LeCrud : @ref LeObject child class
  425. # @param datas dict : dict with field name as key and field values as value
  426. # @param fields_handler dict : dict with field name as key and data handler instance as value
  427. def __init__(self, leobject, datas, fields_handler):
  428. ## Stores concerned class
  429. self._leobject = leobject
  430. ## Stores datas and constructed datas
  431. self._datas = copy.copy(datas)
  432. ## Stores fieldtypes
  433. self._fields_handler = fields_handler
  434. ## Stores list of fieldname for constructed datas
  435. self._constructed = []
  436. ## Stores construct calls list
  437. self._construct_calls = []
  438. ## @brief Implements the dict.keys() method on instance
  439. def keys(self):
  440. return self._datas.keys()
  441. ## @brief Allows to access the instance like a dict
  442. def __getitem__(self, fname):
  443. if fname not in self._constructed:
  444. if fname in self._construct_calls:
  445. raise RuntimeError('Probably circular dependencies in fieldtypes')
  446. cur_value = self._datas[fname] if fname in self._datas else None
  447. self._datas[fname] = self._fields_handler[fname].construct_data(self._leobject, fname, self, cur_value)
  448. self._constructed.append(fname)
  449. return self._datas[fname]
  450. ## @brief Allows to set instance values like a dict
  451. # @warning Should not append in theory
  452. def __setitem__(self, fname, value):
  453. self._datas[fname] = value
  454. warnings.warn("Setting value of an DatasConstructor instance")