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

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