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

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