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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. # -*- coding: utf-8 -*-
  2. ## @package lodel.leapi.datahandlers.base_classes Define all base/abstract class for data handlers
  3. #
  4. # Contains custom exceptions too
  5. import copy
  6. import importlib
  7. import inspect
  8. from lodel import logger
  9. class FieldValidationError(Exception):
  10. pass
  11. ##@brief Base class for all data handlers
  12. class DataHandler(object):
  13. __HANDLERS_MODULES = ('datas_base', 'datas', 'references')
  14. ##@brief Stores the DataHandler childs classes indexed by name
  15. __base_handlers = None
  16. ##@brief Stores custom datahandlers classes indexed by name
  17. # @todo do it ! (like plugins, register handlers... blablabla)
  18. __custom_handlers = dict()
  19. help_text = 'Generic Field Data Handler'
  20. ##@brief List fields that will be exposed to the construct_data_method
  21. _construct_datas_deps = []
  22. ##@brief constructor
  23. # @param internal False | str : define whether or not a field is internal
  24. # @param immutable bool : indicates if the fieldtype has to be defined in child classes of LeObject or if it is
  25. # designed globally and immutable
  26. # @param **args
  27. # @throw NotImplementedError if it is instanciated directly
  28. def __init__(self, **kwargs):
  29. if self.__class__ == DataHandler:
  30. raise NotImplementedError("Abstract class")
  31. self.__arguments = kwargs
  32. self.nullable = True
  33. self.uniq = False
  34. self.immutable = False
  35. self.primary_key = False
  36. if 'defaults' in kwargs:
  37. self.default, error = self.check_data_value(kwargs['default'])
  38. if error:
  39. raise error
  40. del(args['default'])
  41. for argname, argval in kwargs.items():
  42. setattr(self, argname, argval)
  43. ## Fieldtype name
  44. @staticmethod
  45. def name(cls):
  46. return cls.__module__.split('.')[-1]
  47. @classmethod
  48. def is_reference(cls):
  49. return issubclass(cls, Reference)
  50. def is_primary_key(self):
  51. return self.primary_key
  52. ##@brief checks if a fieldtype is internal
  53. # @return bool
  54. def is_internal(self):
  55. return self.internal is not False
  56. ##@brief calls the data_field defined _check_data_value() method
  57. # @return tuple (value, error|None)
  58. def check_data_value(self, value):
  59. if value is None:
  60. if not self.nullable:
  61. return None, TypeError("'None' value but field is not nullable")
  62. return None, None
  63. return self._check_data_value(value)
  64. ##@brief checks if this class can override the given data handler
  65. # @param data_handler DataHandler
  66. # @return bool
  67. def can_override(self, data_handler):
  68. if data_handler.__class__.base_type != self.__class__.base_type:
  69. return False
  70. return True
  71. ##@brief Build field value
  72. # @param emcomponent EmComponent : An EmComponent child class instance
  73. # @param fname str : The field name
  74. # @param datas dict : dict storing fields values (from the component)
  75. # @param cur_value : the value from the current field (identified by fieldname)
  76. # @return the value
  77. # @throw RunTimeError if data construction fails
  78. def construct_data(self, emcomponent, fname, datas, cur_value):
  79. emcomponent_fields = emcomponent.fields()
  80. fname_data_handler = None
  81. if fname in emcomponent_fields:
  82. fname_data_handler = DataHandler.from_name(emcomponent_fields[fname])
  83. if fname in datas.keys():
  84. return cur_value
  85. elif fname_data_handler is not None and hasattr(fname_data_handler, 'default'):
  86. return fname_data_handler.default
  87. elif fname_data_handler is not None and fname_data_handler.nullable:
  88. return None
  89. return RuntimeError("Unable to construct data for field %s", fname)
  90. ##@brief Check datas consistency
  91. # @param emcomponent EmComponent : An EmComponent child class instance
  92. # @param fname : the field name
  93. # @param datas dict : dict storing fields values
  94. # @return an Exception instance if fails else True
  95. # @todo A implémenter
  96. def check_data_consistency(self, emcomponent, fname, datas):
  97. return True
  98. ##@brief This method is use by plugins to register new data handlers
  99. @classmethod
  100. def register_new_handler(cls, name, data_handler):
  101. if not inspect.isclass(data_handler):
  102. raise ValueError("A class was expected but %s given" % type(data_handler))
  103. if not issubclass(data_handler, DataHandler):
  104. raise ValueError("A data handler HAS TO be a child class of DataHandler")
  105. cls.__custom_handlers[name] = data_handler
  106. @classmethod
  107. def load_base_handlers(cls):
  108. if cls.__base_handlers is None:
  109. cls.__base_handlers = dict()
  110. for module_name in cls.__HANDLERS_MODULES:
  111. module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name)
  112. for name, obj in inspect.getmembers(module):
  113. if inspect.isclass(obj):
  114. logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__))
  115. cls.__base_handlers[name.lower()] = obj
  116. return copy.copy(cls.__base_handlers)
  117. ##@brief given a field type name, returns the associated python class
  118. # @param fieldtype_name str : A field type name (not case sensitive)
  119. # @return DataField child class
  120. # @todo implements custom handlers fetch
  121. # @note To access custom data handlers it can be cool to preffix the handler name by plugin name for example ? (to ensure name unicity)
  122. @classmethod
  123. def from_name(cls, name):
  124. name = name.lower()
  125. if name not in cls.__base_handlers:
  126. raise NameError("No data handlers named '%s'" % (name,))
  127. return cls.__base_handlers[name]
  128. ##@brief Return the module name to import in order to use the datahandler
  129. # @param data_handler_name str : Data handler name
  130. # @return a str
  131. @classmethod
  132. def module_name(cls, name):
  133. name = name.lower()
  134. handler_class = cls.from_name(name)
  135. return '{module_name}.{class_name}'.format(
  136. module_name = handler_class.__module__,
  137. class_name = handler_class.__name__
  138. )
  139. ##@brief __hash__ implementation for fieldtypes
  140. def __hash__(self):
  141. hash_dats = [self.__class__.__module__]
  142. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  143. hash_dats.append((kdic, getattr(self, kdic)))
  144. return hash(tuple(hash_dats))
  145. ##@brief Base class for datas data handler (by opposition with references)
  146. class DataField(DataHandler):
  147. pass
  148. ##@brief Abstract class for all references
  149. #
  150. # References are fields that stores a reference to another
  151. # editorial object
  152. class Reference(DataHandler):
  153. ##@brief Instanciation
  154. # @param allowed_classes list | None : list of allowed em classes if None no restriction
  155. # @param back_reference tuple | None : tuple containing (LeObject child class, fieldname)
  156. # @param internal bool : if False, the field is not internal
  157. # @param **kwargs : other arguments
  158. def __init__(self, allowed_classes = None, back_reference = None, internal=False, **kwargs):
  159. self.__allowed_classes = None if allowed_classes is None else set(allowed_classes)
  160. if back_reference is not None:
  161. if len(back_reference) != 2:
  162. raise ValueError("A tuple (classname, fieldname) expected but got '%s'" % back_reference)
  163. #if not issubclass(back_reference[0], LeObject) or not isinstance(back_reference[1], str):
  164. # raise TypeError("Back reference was expected to be a tuple(<class LeObject>, str) but got : (%s, %s)" % (back_reference[0], back_reference[1]))
  165. self.__back_reference = back_reference
  166. super().__init__(internal=internal, **kwargs)
  167. @property
  168. def back_reference(self):
  169. return copy.copy(self.__back_reference)
  170. ##@brief Set the back reference for this field.
  171. def _set_back_reference(self, back_reference):
  172. self.__back_reference = back_reference
  173. ##@brief Check value
  174. # @param value *
  175. # @return tuple(value, exception)
  176. # @todo implement the check when we have LeObject to check value
  177. def _check_data_value(self, value):
  178. return value, None
  179. if isinstance(value, lodel.editorial_model.components.EmClass):
  180. value = [value]
  181. for elt in value:
  182. if not issubclass(elt.__class__, EmClass):
  183. return None, FieldValidationError("Some elements of this references are not EmClass instances")
  184. if self.__allowed_classes is not None:
  185. if not isinstance(elt, self.__allowed_classes):
  186. return None, FieldValidationError("Some element of this references are not valids (don't fit with allowed_classes")
  187. return value
  188. ##@brief This class represent a data_handler for single reference to another object
  189. #
  190. # The fields using this data handlers are like "foreign key" on another object
  191. class SingleRef(Reference):
  192. def __init__(self, allowed_classes = None, **kwargs):
  193. super().__init__(allowed_classes = allowed_classes)
  194. def _check_data_value(self, value):
  195. val, expt = super()._check_data_value(value)
  196. if not isinstance(expt, Exception):
  197. if len(val) > 1:
  198. return None, FieldValidationError("Only single values are allowed for SingleRef fields")
  199. return val, expt
  200. ##@brief This class represent a data_handler for multiple references to another object
  201. #
  202. # The fields using this data handlers are like SingleRef but can store multiple references in one field
  203. # @note SQL implementation could be tricky
  204. class MultipleRef(Reference):
  205. ##
  206. # @param max_item int | None : indicate the maximum number of item referenced by this field, None mean no limit
  207. def __init__(self, max_item = None, **kwargs):
  208. super().__init__(**kwargs)
  209. def _check_data_value(self, value):
  210. if self.max_item is not None:
  211. if self.max_item < len(value):
  212. return None, FieldValidationError("To many items")