暫無描述
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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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 Property that takes value of a copy of the back_reference tuple
  234. @property
  235. def back_reference(self):
  236. return copy.copy(self.__back_reference)
  237. ##@brief Property that takes value of datahandler of the backreference or
  238. #None
  239. @property
  240. def back_ref_datahandler(self):
  241. if self.__back_reference is None:
  242. return None
  243. return self.__back_reference[0].data_handler(self.__back_reference[1])
  244. @property
  245. def linked_classes(self):
  246. return copy.copy(self.__allowed_classes)
  247. ##@brief Set the back reference for this field.
  248. def _set_back_reference(self, back_reference):
  249. self.__back_reference = back_reference
  250. ##@brief Check and cast value in appropriate type
  251. #@param value *
  252. #@throw FieldValidationError if value is an appropriate type
  253. #@return value
  254. #@todo implement the check when we have LeObject uid check value
  255. def _check_data_value(self, value):
  256. from lodel.leapi.leobject import LeObject
  257. value = super()._check_data_value(value)
  258. if not (hasattr(value, '__class__') and
  259. issubclass(value.__class__, LeObject)):
  260. if self.__allowed_classes:
  261. rcls = list(self.__allowed_classes)[0]
  262. uidname = rcls.uid_fieldname()[0]# TODO multiple uid is broken
  263. uiddh = rcls.data_handler(uidname)
  264. value = uiddh._check_data_value(value)
  265. else:
  266. raise FieldValidationError("Reference datahandler can not check this value %s if any allowed_class is allowed." % value)
  267. return value
  268. ##@brief Check datas consistency
  269. #@param emcomponent EmComponent : An EmComponent child class instance
  270. #@param fname : the field name
  271. #@param datas dict : dict storing fields values
  272. #@return an Exception instance if fails else True
  273. #@todo check for performance issue and check logics
  274. #@todo Implements consistency checking on value : Check that the given value
  275. #points onto an allowed class
  276. #@warning composed uid capabilities broken here
  277. def check_data_consistency(self, emcomponent, fname, datas):
  278. rep = super().check_data_consistency(emcomponent, fname, datas)
  279. if isinstance(rep, Exception):
  280. return rep
  281. if self.back_reference is None:
  282. return True
  283. # !! Reimplement instance fetching in construct data !!
  284. dh = emcomponent.field(fname)
  285. uid = datas[emcomponent.uid_fieldname()[0]] #multi uid broken here
  286. target_class = self.back_reference[0]
  287. target_field = self.back_reference[1]
  288. target_uidfield = target_class.uid_fieldname()[0] #multi uid broken here
  289. value = datas[fname]
  290. obj = target_class.get([(target_uidfield , '=', value)])
  291. if len(obj) == 0:
  292. logger.warning('Object referenced does not exist')
  293. return False
  294. return True
  295. ##@brief This class represent a data_handler for single reference to another object
  296. #
  297. # The fields using this data handlers are like "foreign key" on another object
  298. class SingleRef(Reference):
  299. def __init__(self, allowed_classes = None, **kwargs):
  300. super().__init__(allowed_classes = allowed_classes)
  301. ##@brief Check and cast value in appropriate type
  302. #@param value: *
  303. #@throw FieldValidationError if value is unappropriate or can not be cast
  304. #@return value
  305. def _check_data_value(self, value):
  306. value = super()._check_data_value(value)
  307. logger.warning("A vérifier..provisoire pour les tests")
  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 Method designed to return an empty value for this kind of
  323. #multipleref
  324. @classmethod
  325. def empty(cls):
  326. return None
  327. ##@brief Check and cast value in appropriate type
  328. #@param value *
  329. #@throw FieldValidationError if value is unappropriate or can not be cast
  330. #@return value
  331. #@TODO Writing test error for errors when stored multiple references in one field
  332. def _check_data_value(self, value):
  333. value = DataHandler._check_data_value(self,value)
  334. if not hasattr(value, '__iter__'):
  335. raise FieldValidationError("MultipleRef has to be an iterable or a string, '%s' found" % value)
  336. if self.max_item is not None:
  337. if self.max_item < len(value):
  338. raise FieldValidationError("Too many items")
  339. new_val = list()
  340. error_list = list()
  341. for i,v in enumerate(value):
  342. try:
  343. v = super()._check_data_value(v)
  344. new_val.append(v)
  345. except (FieldValidationError) as f:
  346. error_list.append(repr(v))
  347. if len(error_list) > 0:
  348. raise FieldValidationError("MultipleRef have for invalid values [%s] :" % (",".join(error_list)))
  349. return new_val
  350. ##@brief Construct a multiple ref data
  351. def construct_data(self, emcomponent, fname, datas, cur_value):
  352. cur_value = super().construct_data(emcomponent, fname, datas, cur_value)
  353. if cur_value is not None:
  354. if self.back_reference is not None:
  355. br_class = self.back_reference[0]
  356. for br_id in cur_value:
  357. query_filters = list()
  358. query_filters.append((br_class.uid_fieldname()[0], '=', br_id))
  359. br_obj = br_class.get(query_filters)
  360. if len(br_obj) != 0:
  361. br_list = br_obj[0].data(self.back_reference[1])
  362. if br_list is None:
  363. br_list = list()
  364. if br_id not in br_list:
  365. br_list.append(br_id)
  366. return cur_value
  367. ## @brief Checks the backreference, updates it if it is not complete
  368. # @param emcomponent EmComponent : An EmComponent child class instance
  369. # @param fname : the field name
  370. # @param datas dict : dict storing fields values
  371. # @note Not done in case of delete
  372. def make_consistency(self, emcomponent, fname, datas, type_query):
  373. dh = emcomponent.field(fname)
  374. logger.info('Warning : multiple uid capabilities are broken here')
  375. uid = datas[emcomponent.uid_fieldname()[0]]
  376. if self.back_reference is not None:
  377. target_class = self.back_reference[0]
  378. target_field = self.back_reference[1]
  379. target_uidfield = target_class.uid_fieldname()[0]
  380. l_value = datas[fname]
  381. if l_value is not None:
  382. for value in l_value:
  383. query_filters = list()
  384. query_filters.append((target_uidfield , '=', value))
  385. obj = target_class.get(query_filters)
  386. if len(obj) == 0:
  387. logger.warning('Object referenced does not exist')
  388. return False
  389. l_uids_ref = obj[0].data(target_field)
  390. if l_uids_ref is None:
  391. l_uids_ref = list()
  392. if uid not in l_uids_ref:
  393. l_uids_ref.append(uid)
  394. obj[0].set_data(target_field, l_uids_ref)
  395. obj[0].update()
  396. if type_query == 'update':
  397. query_filters = list()
  398. query_filters.append((uid, ' in ', target_field))
  399. objects = target_class.get(query_filters)
  400. if l_value is None:
  401. l_value = list()
  402. if len(objects) != len(l_value):
  403. for obj in objects:
  404. l_uids_ref = obj.data(target_field)
  405. if obj.data(target_uidfield) not in l_value:
  406. l_uids_ref.remove(uid)
  407. obj.set_data(target_field, l_uids_ref)
  408. obj.update()
  409. ## @brief Class designed to handle datas access will fieldtypes are constructing datas
  410. #@ingroup lodel2_datahandlers
  411. #
  412. # This class is designed to allow automatic scheduling of construct_data calls.
  413. #
  414. # In theory it's able to detect circular dependencies
  415. # @todo test circular deps detection
  416. # @todo test circulat deps false positiv
  417. class DatasConstructor(object):
  418. ## @brief Init a DatasConstructor
  419. # @param lec LeCrud : @ref LeObject child class
  420. # @param datas dict : dict with field name as key and field values as value
  421. # @param fields_handler dict : dict with field name as key and data handler instance as value
  422. def __init__(self, leobject, datas, fields_handler):
  423. ## Stores concerned class
  424. self._leobject = leobject
  425. ## Stores datas and constructed datas
  426. self._datas = copy.copy(datas)
  427. ## Stores fieldtypes
  428. self._fields_handler = fields_handler
  429. ## Stores list of fieldname for constructed datas
  430. self._constructed = []
  431. ## Stores construct calls list
  432. self._construct_calls = []
  433. ## @brief Implements the dict.keys() method on instance
  434. def keys(self):
  435. return self._datas.keys()
  436. ## @brief Allows to access the instance like a dict
  437. def __getitem__(self, fname):
  438. if fname not in self._constructed:
  439. if fname in self._construct_calls:
  440. raise RuntimeError('Probably circular dependencies in fieldtypes')
  441. cur_value = self._datas[fname] if fname in self._datas else None
  442. self._datas[fname] = self._fields_handler[fname].construct_data(self._leobject, fname, self, cur_value)
  443. self._constructed.append(fname)
  444. return self._datas[fname]
  445. ## @brief Allows to set instance values like a dict
  446. # @warning Should not append in theory
  447. def __setitem__(self, fname, value):
  448. self._datas[fname] = value
  449. warnings.warn("Setting value of an DatasConstructor instance")