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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. #
  2. # This file is part of Lodel 2 (https://github.com/OpenEdition)
  3. #
  4. # Copyright (C) 2015-2017 Cléo UMS-3287
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as published
  8. # by the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. ##
  20. #  @package lodel.leapi.datahandlers.base_classes Defines all base/abstract
  21. # classes for DataHandlers
  22. #
  23. # Contains custom exceptions too
  24. import copy
  25. import importlib
  26. import inspect
  27. import warnings
  28. from lodel.context import LodelContext
  29. LodelContext.expose_modules(globals(), {
  30. 'lodel.exceptions': [
  31. 'LodelException',
  32. 'LodelExceptions',
  33. 'LodelFatalError',
  34. 'DataNoneValid',
  35. 'FieldValidationError'
  36. ],
  37. 'lodel.mlnamedobject.mlnamedobject': ['MlNamedObject'],
  38. 'lodel.leapi.datahandlers.exceptions': [
  39. 'LodelDataHandlerConsistencyException',
  40. 'LodelDataHandlerException'
  41. ],
  42. 'lodel.validator.validator': [
  43. 'ValidationError'
  44. ],
  45. 'lodel.logger': 'logger',
  46. 'lodel.utils.mlstring': ['MlString']})
  47. ##
  48. # @brief Base class for all DataHandlers
  49. # @ingroup lodel2_datahandlers
  50. #
  51. # @remarks Some of the methods and properties in this "abstract" class are
  52. # bounded to its children. This implies that the parent
  53. # is aware of its children, which is an absolute anti-pattern
  54. # (Liskov / OC violation), a source of confusion and a decrased
  55. # maintainability. Aggregation =/= Inheritance
  56. # Concerned methods are: is_reference; is_singlereference.
  57. # Concerned properties are __custom_datahandlers; base_handlers.
  58. # @remarks What is the purpose of an internal property being set to a
  59. # string (namely 'automatic')
  60. # @remarks Two sets of methods appears a little strange in regards to their
  61. # visibility.
  62. # - @ref _construct_data / @ref construct_data
  63. # - @ref _check_data_consistency / @ref check_data_consistency
  64. class DataHandler(MlNamedObject):
  65. base_type = "type"
  66. _HANDLERS_MODULES = ('datas_base', 'datas', 'references')
  67. ##
  68. # @brief Stores the DataHandler child classes indexed by name
  69. _base_handlers = None
  70. ##
  71. # @brief Stores custom DataHandlers classes indexed by name
  72. # @todo do it ! (like plugins, register handlers... blablabla)
  73. __custom_handlers = dict()
  74. help_text = 'Generic Field Data Handler'
  75. display_name = "Generic Field"
  76. options_spec = dict()
  77. options_values = dict()
  78. ##
  79. # @brief Lists fields that will be exposed to the construct_data method
  80. _construct_datas_deps = []
  81. directly_editable = True
  82. ##
  83. # @brief constructor
  84. #
  85. # @param internal False | str : define whether or not a field is internal
  86. # @param immutable bool : Indicates if the fieldtype has to be defined in child classes of
  87. # LeObject or if it is designed globally and immutable
  88. # @throw NotImplementedError If it is instantiated directly
  89. # @remarks Shouldn't the class be declared abstract? No need to check if it
  90. # is instantiated directly, no exception to throw, cleaner code.
  91. def __init__(self, **kwargs):
  92. if self.__class__ == DataHandler:
  93. raise NotImplementedError("Abstract class")
  94. self.__arguments = kwargs
  95. self.nullable = True
  96. self.uniq = False
  97. self.immutable = False
  98. self.primary_key = False
  99. self.internal = False
  100. if 'default' in kwargs:
  101. self.default, error = self.check_data_value(kwargs['default'])
  102. if error:
  103. raise error
  104. del kwargs['default']
  105. for argname, argval in kwargs.items():
  106. setattr(self, argname, argval)
  107. self.check_options()
  108. display_name = kwargs.get('display_name', MlString(self.display_name))
  109. help_text = kwargs.get('help_text', MlString(self.help_text))
  110. super().__init__(display_name, help_text)
  111. ##
  112. # @brief Sets properly cast and checked options for the DataHandler
  113. #
  114. # @throw LodelDataHandlerNotAllowedOptionException when a passed option
  115. # is not in the option specifications of the DataHandler
  116. def check_options(self):
  117. for option_name, option_datas in self.options_spec.items():
  118. if option_name in self.options_values:
  119. # There is a configured option, we check its value
  120. try:
  121. self.options_values[option_name] = option_datas[1].check_value(
  122. self.options_values[option_name])
  123. except ValueError:
  124. pass # TODO Deal with the case where the value used for an option is invalid
  125. else:
  126. # This option was not configured, we get the default value from the specs
  127. self.options_values[option_name] = option_datas[0]
  128. ##
  129. # @return string: Field type name
  130. @classmethod
  131. def name(cls):
  132. return cls.__module__.split('.')[-1]
  133. ##
  134. # @return bool: True if subclass is of Reference type, False otherwise.
  135. @classmethod
  136. def is_reference(cls):
  137. return issubclass(cls, Reference)
  138. ##
  139. # @return bool: True if subclass is of SingleRef type, False otherwise.
  140. @classmethod
  141. def is_singlereference(cls):
  142. return issubclass(cls, SingleRef)
  143. ##
  144. # @return bool: True if the field is a primary_key, False otherwise.
  145. def is_primary_key(self):
  146. return self.primary_key
  147. ##
  148. # @brief checks if a field type is internal
  149. # @return bool: True if the field is internal, False otherwise.
  150. def is_internal(self):
  151. return self.internal is not False
  152. ##
  153. # @brief check if a value can be nullable
  154. #
  155. # @param value *
  156. # @throw DataNoneValid if value is None and nullable.
  157. # @throw LodelExceptions if not nullable
  158. # @return value (if not None)
  159. # @return value
  160. #
  161. # @remarks why are there an thrown exception if it is allowed?
  162. # Exceptions are no message brokers
  163. def _check_data_value(self, value):
  164. if value is None:
  165. if not self.nullable:
  166. raise LodelExceptions("None value is forbidden for this data field")
  167. raise DataNoneValid("None with a nullable. This exception is allowed")
  168. return value
  169. ##
  170. # @brief calls the data_field (defined in derived class) _check_data_value() method
  171. # @param value *
  172. # @return tuple (value|None, None|error) value can be cast if NoneError
  173. # @remarks Consider renaming this method, such as '_is_data_nullable'.
  174. # @remarks Exceptions ARE NOT message brokers! Moreover, those two methods
  175. # are more complicated than required. In case 'value' is None,
  176. # the returned value is the same as the input value. This is the
  177. # same behavior as when the value is not None!
  178. # @return What's a "NoneError"? Value can be cast to what?
  179. def check_data_value(self, value):
  180. try:
  181. value = self._check_data_value(value)
  182. except DataNoneValid as expt:
  183. return value, None
  184. except (LodelExceptions, FieldValidationError) as expt:
  185. return None, expt
  186. return value, None
  187. ##
  188. # @brief Checks if this class can override the given data handler.
  189. # i.e. both class having the same base_type.
  190. # @param data_handler DataHandler
  191. # @return bool
  192. # @remarks Simplify by "return data_handler.__class__.base_type == self.__class__.base_type"?
  193. def can_override(self, data_handler):
  194. if data_handler.__class__.base_type != self.__class__.base_type:
  195. return False
  196. return True
  197. ##
  198. # @brief Build field value
  199. #
  200. # @ingroup lodel2_dh_checks
  201. # @ref _construct_data() and @ref lodel2_dh_check_impl )
  202. #
  203. # @param emcomponent EmComponent : An EmComponent child class instance
  204. # @param fname str : The field name
  205. # @param datas dict : dict storing fields values (from the component)
  206. # @param cur_value : the value from the current field (identified by fieldname)
  207. # @return the value
  208. # @throw RunTimeError if data construction fails
  209. #
  210. # @warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  211. # @todo raise something else
  212. #
  213. # @remarks What the todo up right here means? Raise what? When?
  214. # @remarks Nothing is being raised in this method, should it?
  215. def construct_data(self, emcomponent, fname, datas, cur_value):
  216. emcomponent_fields = emcomponent.fields()
  217. data_handler = None
  218. if fname in emcomponent_fields:
  219. data_handler = emcomponent_fields[fname]
  220. new_val = cur_value
  221. if fname in datas.keys():
  222. pass
  223. elif data_handler is not None and hasattr(data_handler, 'default'):
  224. new_val = data_handler.default
  225. elif data_handler is not None and data_handler.nullable:
  226. new_val = None
  227. return self._construct_data(emcomponent, fname, datas, new_val)
  228. ##
  229. # @brief Designed to be reimplemented by child classes
  230. #
  231. # @param emcomponent EmComponent : An EmComponent child class instance
  232. # @param fname str : The field name
  233. # @param datas dict : dict storing fields values (from the component)
  234. # @param cur_value : the value from the current field (identified by fieldname)
  235. # @return the value
  236. # @see construct_data() lodel2_dh_check_impl
  237. def _construct_data(self, emcomponent, fname, datas, cur_value):
  238. return cur_value
  239. ##
  240. # @brief Check data consistency
  241. # @ingroup lodel2_dh_checks
  242. #
  243. # @ref lodel2_dh_datas_construction "Data construction section"
  244. # @param emcomponent EmComponent : An EmComponent child class instance
  245. # @param fname : the field name
  246. # @param datas dict : dict storing fields values
  247. # @return an Exception instance if fails else True
  248. #
  249. # @warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  250. # @ref _construct_data() and @ref lodel2_dh_check_impl )
  251. # @warning the data argument looks like a dict but is not a dict
  252. # see @ref base_classes.DatasConstructor "DatasConstructor" and
  253. # @todo A implémenter
  254. def check_data_consistency(self, emcomponent, fname, datas):
  255. return self._check_data_consistency(emcomponent, fname, datas)
  256. ##
  257. # @brief Designed to be reimplemented by child classes
  258. #
  259. # @param emcomponent EmComponent : An EmComponent child class instance
  260. # @param fname : the field name
  261. # @param datas dict : dict storing fields values
  262. # @return an Exception instance if fails else True
  263. #
  264. # @see check_data_consistency() lodel2_dh_check_impl
  265. def _check_data_consistency(self, emcomponent, fname, datas):
  266. return True
  267. ##
  268. # @brief Makes consistency after a query
  269. #
  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. #
  275. # @todo To be implemented
  276. # @remarks It not clear what is the intent of this method...
  277. def make_consistency(self, emcomponent, fname, datas):
  278. pass
  279. ##
  280. # @brief Registers a new data handlers
  281. #
  282. # @note Used by plugins.
  283. # @remarks This method is actually never used anywhere. May consider removing it.
  284. @classmethod
  285. def register_new_handler(cls, name, data_handler):
  286. if not inspect.isclass(data_handler):
  287. raise ValueError("A class was expected but %s given" % type(data_handler))
  288. if not issubclass(data_handler, DataHandler):
  289. raise ValueError("A data handler HAS TO be a child class of DataHandler")
  290. cls.__custom_handlers[name] = data_handler
  291. ##
  292. # @brief Loads all DataHandlers
  293. @classmethod
  294. def load_base_handlers(cls):
  295. if cls._base_handlers is None:
  296. cls._base_handlers = dict()
  297. for module_name in cls._HANDLERS_MODULES:
  298. module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name)
  299. for name, obj in inspect.getmembers(module):
  300. if inspect.isclass(obj):
  301. logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__))
  302. cls._base_handlers[name.lower()] = obj
  303. return copy.copy(cls._base_handlers)
  304. ##
  305. # @brief given a field type name, returns the associated python class
  306. #
  307. # @param name str : A field type name (not case sensitive)
  308. # @return DataField child class
  309. # @throw NameError
  310. #
  311. # @note Would not it be better to prefix the DataHandler name with the
  312. # plugin's one so that it is ensured names are unique?
  313. # @remarks "do/get what from name?" Consider renaming this method (e.g.
  314. # 'get_datafield_from_name')
  315. @classmethod
  316. def from_name(cls, name):
  317. cls.load_base_handlers()
  318. all_handlers = dict(cls._base_handlers, **cls.__custom_handlers)
  319. name = name.lower()
  320. if name not in all_handlers:
  321. raise NameError("No data handlers named '%s'" % (name,))
  322. return all_handlers[name]
  323. ##
  324. # @brief List all DataHandlers
  325. # @return a dict with, display_name for keys, and a dict for value
  326. # @remarks ATM, solely used by the EditorialModel.
  327. # @remarks EditorialModel own class does nothing but calls this class.
  328. # Moreover, nothing calls it anyway.
  329. # @remarks It also seems like it is an EM related concern, and has
  330. # nothing to do with this class. That list appears to be doing
  331. # a purely presentational job. Isn't that a serialization instead?
  332. @classmethod
  333. def list_data_handlers(cls):
  334. cls.load_base_handlers()
  335. all_handlers = dict(cls._base_handlers, **cls.__custom_handlers)
  336. list_dh = dict()
  337. for hdl in all_handlers:
  338. options = dict({'nullable': hdl.nullable,
  339. 'internal': hdl.internal,
  340. 'immutable': hdl.immutable,
  341. 'primary_key': hdl.primary_key}, hdl.options_spec)
  342. list_dh[hdl.display_name] = {'help_text': hdl.help_text, 'options': options}
  343. return list_dh
  344. ##
  345. # @brief Return the module name to import in order to use the DataHandler
  346. # @param datahandler_name str : Data handler name
  347. # @return str
  348. # @remarks consider renaming this (e.g. "datahandler_module_name")
  349. @classmethod
  350. def module_name(cls, datahandler_name):
  351. datahandler_name = datahandler_name.lower()
  352. handler_class = cls.from_name(datahandler_name)
  353. return '{module_name}.{class_name}'.format(
  354. module_name=handler_class.__module__,
  355. class_name=handler_class.__name__
  356. )
  357. ##
  358. # @brief __hash__ implementation for field types
  359. def __hash__(self):
  360. hash_dats = [self.__class__.__module__]
  361. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  362. hash_dats.append((kdic, getattr(self, kdic)))
  363. return hash(tuple(hash_dats))
  364. ##
  365. # @brief Base class for data data handler (by opposition with references)
  366. # @ingroup lodel2_datahandlers
  367. class DataField(DataHandler):
  368. pass
  369. ##
  370. # @brief Abstract class for all references
  371. # @ingroup lodel2_datahandlers
  372. #
  373. # References are fields that stores a reference to another
  374. # editorial object
  375. # @todo Construct data implementation : transform the data into a LeObject instance
  376. class Reference(DataHandler):
  377. base_type = "ref"
  378. ##
  379. # @brief Instantiation
  380. # @param allowed_classes list | None : list of allowed em classes if None no restriction
  381. # @param back_reference tuple | None : tuple containing (LeObject child class, field name)
  382. # @param internal bool | string: if False, the field is not internal
  383. # @param **kwargs : other arguments
  384. # @throw ValueError
  385. # @remarks internal may hold the string value 'automatic'. So far, nothing
  386. # mentions what that means, and nothing seems to be aware
  387. # of an 'automatic' value (at least not in leapi package)
  388. def __init__(self, allowed_classes=None, back_reference=None, internal=False, **kwargs):
  389. self.__allowed_classes = set() if allowed_classes is None else set(allowed_classes)
  390. ##
  391. # @note what is "useful to Jinja 2"?
  392. # For now useful to jinja 2
  393. self.allowed_classes = list() if allowed_classes is None else allowed_classes
  394. if back_reference is not None:
  395. if len(back_reference) != 2:
  396. raise ValueError(
  397. "A tuple (classname, fieldname) expected but got '%s'" % back_reference)
  398. ##
  399. # @note Why is there commented out code? Should it be deleted? Ractivated?
  400. # if not issubclass(lodel.leapi.leobject.LeObject, back_reference[0])
  401. # or not isinstance(back_reference[1], str):
  402. # raise TypeError("Back reference was expected to be a tuple(<class LeObject>, str)
  403. # but got : (%s, %s)" % (back_reference[0], back_reference[1]))
  404. self.__back_reference = back_reference
  405. super().__init__(internal=internal, **kwargs)
  406. ##
  407. # @brief Method designed to return an empty value for this kind of
  408. # multipleref
  409. # @remarks purpose!?
  410. @classmethod
  411. def empty(cls):
  412. return None
  413. ##
  414. # @brief Property that takes value of a copy of the back_reference tuple
  415. @property
  416. def back_reference(self):
  417. return copy.copy(self.__back_reference)
  418. ##
  419. # @brief Property that takes value of datahandler of the backreference or
  420. # None
  421. @property
  422. def back_ref_datahandler(self):
  423. if self.__back_reference is None:
  424. return None
  425. return self.__back_reference[0].data_handler(self.__back_reference[1])
  426. @property
  427. def linked_classes(self):
  428. return copy.copy(self.__allowed_classes)
  429. ##
  430. # @brief Sets a back reference.
  431. def _set_back_reference(self, back_reference):
  432. self.__back_reference = back_reference
  433. ##
  434. # @brief Check and cast value in the appropriate type
  435. #
  436. # @param value
  437. # @throw FieldValidationError if value is an appropriate type
  438. # @return value
  439. # @todo implement the check when we have LeObject uid check value
  440. def _check_data_value(self, value):
  441. from lodel.leapi.leobject import LeObject
  442. value = super()._check_data_value(value)
  443. if not (hasattr(value, '__class__') and
  444. issubclass(value.__class__, LeObject)):
  445. if self.__allowed_classes:
  446. rcls = list(self.__allowed_classes)[0]
  447. uidname = rcls.uid_fieldname()[0] # TODO multiple uid is broken
  448. uiddh = rcls.data_handler(uidname)
  449. value = uiddh._check_data_value(value)
  450. else:
  451. raise FieldValidationError(
  452. "Reference datahandler can not check this value %s if any allowed_class is allowed. " % value)
  453. return value
  454. ##
  455. # @brief Check data consistency
  456. #
  457. # @param emcomponent EmComponent :
  458. # @param fname string : the field name
  459. # @param datas dict : dict storing fields values
  460. # @return bool | Exception :
  461. #
  462. # @todo check for performance issues and checks logic
  463. # @warning composed uid capabilities are broken
  464. # @remarks Is that really a legitimate case of retuning an Exception object?
  465. def check_data_consistency(self, emcomponent, fname, datas):
  466. rep = super().check_data_consistency(emcomponent, fname, datas)
  467. if isinstance(rep, Exception):
  468. return rep
  469. if self.back_reference is None:
  470. return True
  471. ##
  472. # @todo Reimplement instance fetching in construct data
  473. # @remarks Set the previous todo as one, looked like it was intended to be.
  474. target_class = self.back_reference[0]
  475. if target_class not in self.__allowed_classes:
  476. logger.warning('Class of the back_reference given is not an allowed class')
  477. return False
  478. value = datas[fname]
  479. ##
  480. # @warning multi uid broken here
  481. # @remarks Why is that broken? Any clue? Set as a warning.
  482. target_uidfield = target_class.uid_fieldname()[0]
  483. obj = target_class.get([(target_uidfield, '=', value)])
  484. if len(obj) == 0:
  485. logger.warning('Object referenced does not exist')
  486. return False
  487. return True
  488. ##
  489. # @brief Utility method designed to fetch referenced objects
  490. #
  491. # @param value mixed : the field value
  492. # @throw NotImplementedError
  493. # @remarks Not implemented? Consider renaming?
  494. def get_referenced(self, value):
  495. raise NotImplementedError
  496. ##
  497. # @brief DataHandler for single reference to another object
  498. #
  499. # An instance of this class acts like a "foreign key" to another object
  500. class SingleRef(Reference):
  501. def __init__(self, allowed_classes=None, **kwargs):
  502. super().__init__(allowed_classes=allowed_classes, **kwargs)
  503. ##
  504. # @brief Checks and casts value to the appropriate type
  505. #
  506. # @param value: mixed
  507. # @throw FieldValidationError if value is inappropriate or can not be cast
  508. # @return mixed
  509. def _check_data_value(self, value):
  510. value = super()._check_data_value(value)
  511. return value
  512. ##
  513. # @brief Utility method to fetch referenced objects
  514. #
  515. # @param value mixed : the field value
  516. # @return A LeObject child class instance
  517. # @throw LodelDataHandlerConsistencyException if no referenced object found
  518. # @remarks Consider renaming (e.g. get_referenced_object)?
  519. def get_referenced(self, value):
  520. for leo_cls in self.linked_classes:
  521. res = leo_cls.get_from_uid(value)
  522. if res is not None:
  523. return res
  524. raise LodelDataHandlerConsistencyException("Unable to find \
  525. referenced object with uid %s" % value)
  526. ##
  527. # @brief DataHandler for multiple references to another object
  528. # @ingroup lodel2_datahandlers
  529. #
  530. # The fields using this data handlers are like SingleRef but can store multiple
  531. # references in one field.
  532. # @note for the moment split on ',' chars
  533. class MultipleRef(Reference):
  534. ##
  535. # @brief Constructor
  536. #
  537. # @param max_item int | None : indicate the maximum number of item referenced
  538. # by this field, None mean no limit
  539. def __init__(self, max_item=None, **kwargs):
  540. self.max_item = max_item
  541. super().__init__(**kwargs)
  542. ##
  543. # @brief Method designed to return an empty value for this kind of
  544. # multipleref
  545. # @remarks Purpose!?
  546. @classmethod
  547. def empty(cls):
  548. return []
  549. ##
  550. # @brief Check and cast value in appropriate type
  551. # @param value mixed
  552. # @throw FieldValidationError if value is unappropriate or can not be cast
  553. # @return value
  554. # @todo Writing test error for errors when stored multiple references in one field
  555. def _check_data_value(self, value):
  556. value = DataHandler._check_data_value(self, value)
  557. if not hasattr(value, '__iter__'):
  558. raise FieldValidationError(
  559. "MultipleRef has to be an iterable or a string, '%s' found" % value)
  560. if self.max_item is not None:
  561. if self.max_item < len(value):
  562. raise FieldValidationError("Too many items")
  563. new_val = list()
  564. error_list = list()
  565. for i, v in enumerate(value):
  566. try:
  567. v = super()._check_data_value(v)
  568. new_val.append(v)
  569. except (FieldValidationError):
  570. error_list.append(repr(v))
  571. if len(error_list) > 0:
  572. raise FieldValidationError(
  573. "MultipleRef have for invalid values [%s] :" % (",".join(error_list)))
  574. return new_val
  575. ##
  576. # @brief Utility method designed to fetch referenced objects
  577. #
  578. # @param values mixed : the field values
  579. # @return A list of LeObject child class instance
  580. # @throw LodelDataHandlerConsistencyException if some referenced objects
  581. # were not found
  582. def get_referenced(self, values):
  583. if values is None or len(values) == 0:
  584. return list()
  585. left = set(values)
  586. values = set(values)
  587. res = list()
  588. for leo_cls in self.linked_classes:
  589. uidname = leo_cls.uid_fieldname()[0] # MULTIPLE UID BROKEN HERE
  590. tmp_res = leo_cls.get(('%s in (%s)' % (uidname, ','.join(
  591. [str(l) for l in left]))))
  592. left ^= set((leo.uid() for leo in tmp_res))
  593. res += tmp_res
  594. if len(left) == 0:
  595. return res
  596. raise LodelDataHandlerConsistencyException("Unable to find \
  597. some referenced objects. Following uids were not found : %s" % ','.join(left))
  598. ##
  599. # @brief Class designed to handle data access while field types are constructing data
  600. # @ingroup lodel2_datahandlers
  601. #
  602. # This class is designed to allow automatic scheduling of construct_data calls.
  603. #
  604. # In theory it has the ability to detect circular dependencies
  605. # @todo test circular deps detection
  606. # @todo test circular deps false positive
  607. # @remarks Would not it be better to make sure what the code actually is doing?
  608. class DatasConstructor(object):
  609. ##
  610. # @brief Init a DatasConstructor
  611. #
  612. # @param leobject LeObject
  613. # @param datas dict : dict with field name as key and field values as value
  614. # @param fields_handler dict : dict with field name as key and data handler instance as value
  615. def __init__(self, leobject, datas, fields_handler):
  616. self._leobject = leobject
  617. self._datas = copy.copy(datas)
  618. # Stores fieldtypes
  619. self._fields_handler = fields_handler
  620. # Stores list of fieldname for constructed
  621. self._constructed = []
  622. # Stores construct calls list
  623. self._construct_calls = []
  624. ##
  625. # @brief Implements the dict.keys() method on instance
  626. #
  627. # @return list
  628. def keys(self):
  629. return self._datas.keys()
  630. ##
  631. # @brief Allows to access the instance like a dict
  632. #
  633. # @param fname string: The field name
  634. # @return field values
  635. # @throw RuntimeError
  636. #
  637. # @note Determine return type
  638. def __getitem__(self, fname):
  639. if fname not in self._constructed:
  640. if fname in self._construct_calls:
  641. raise RuntimeError('Probably circular dependencies in fieldtypes')
  642. cur_value = self._datas[fname] if fname in self._datas else None
  643. self._datas[fname] = self._fields_handler[fname].construct_data(
  644. self._leobject, fname, self, cur_value)
  645. self._constructed.append(fname)
  646. return self._datas[fname]
  647. ##
  648. # @brief Allows to set instance values like a dict
  649. #
  650. # @warning Should not append in theory
  651. #
  652. # @remarks Why is a warning issued any time we call this method?
  653. def __setitem__(self, fname, value):
  654. self._datas[fname] = value
  655. warnings.warn("Setting value of an DatasConstructor instance")
  656. ##
  657. # @brief Class designed to handle a DataHandler option
  658. class DatahandlerOption(MlNamedObject):
  659. ##
  660. # @brief instantiates a new DataHandlerOption object
  661. #
  662. # @param id str
  663. # @param display_name MlString
  664. # @param help_text MlString
  665. # @param validator function
  666. def __init__(self, id, display_name, help_text, validator):
  667. self.__id = id
  668. self.__validator = validator
  669. super().__init__(display_name, help_text)
  670. ##
  671. # @brief Accessor to the id property.
  672. @property
  673. def id(self):
  674. return self.__id
  675. ##
  676. # @brief checks a value corresponding to this option is valid
  677. #
  678. # @param value mixed
  679. # @return cast value
  680. # @throw ValueError
  681. def check_value(self, value):
  682. try:
  683. return self.__validator(value)
  684. except ValidationError:
  685. raise ValueError()