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

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