123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- #-*- coding: utf-8 -*-
-
- ## @package leobject API to access lodel datas
- #
- # This package contains abstract classes leapi.leclass.LeClass , leapi.letype.LeType, leapi.leapi._LeObject.
- # Those abstract classes are designed to be mother classes of dynamically generated classes ( see leapi.lefactory.LeFactory )
-
- ## @package leapi.leobject
- # @brief Abstract class designed to be implemented by LeObject
- #
- # @note LeObject will be generated by leapi.lefactory.LeFactory
-
- import re
- import copy
- import warnings
-
- import leapi
- from leapi.lecrud import _LeCrud
- from leapi.lefactory import LeFactory
- import EditorialModel
- from EditorialModel.types import EmType
-
- REL_SUP = 0
- REL_SUB = 1
-
- ## @brief Main class to handle objects defined by the types of an Editorial Model
- class _LeObject(_LeCrud):
-
- ## @brief maps em uid with LeType or LeClass keys are uid values are LeObject childs classes
- # @todo check if this attribute shouldn't be in _LeCrud
- _me_uid = dict()
-
- ## @brief Stores the fields name associated with fieldtype of the fields that are common to every LeObject
- _leo_fieldtypes = dict()
-
- ## @brief Instanciate a partial LeObject with a lodel_id
- # @note use the get_instance method to fetch datas and instanciate a concret LeObject
- def __init__(self, lodel_id):
- #Warning ! Handles only single pk
- uid_fname, uid_ft = list(self._uid_fieldtype.items())[0]
- new_id, err = uid_ft.check_data_value(lodel_id)
- if not (err is None):
- raise err
- setattr(self, uid_fname, lodel_id)
-
- ## @return Corresponding populated LeObject
- def get_instance(self):
- uid_fname = self.uidname()
- qfilter = '{uid_fname} = {uid}'.format(uid_fname = uid_fname, uid = getattr(self, uid_fname))
- return leobject.get([qfilter])[0]
-
- ## @return True if the LeObject is partially instanciated
- def is_partial(self):
- return not hasattr(self, '_classtype')
-
- ## @brief Check if a LeObject is the relation tree Root
- # @todo implementation
- def is_root(self):
- return False
-
- ## @brief Dirty & quick comparison implementation
- def __cmp__(self, other):
- return 0 if self == other else 1
- ## @brief Dirty & quick equality implementation
- # @todo check class
- def __eq__(self, other):
- uid_fname = self.uidname()
- if not hasattr(other, uid_fname):
- return False
- return getattr(self, uid_fname) == getattr(other, uid_fname)
-
-
- ## @brief Quick str cast method implementation
- def __str__(self):
- return "<%s lodel_id=%d>"%(self.__class__, getattr(self, self.uidname()))
-
- def __repr__(self):
- return self.__str__()
-
- ## @brief Given a ME uid return the corresponding LeClass or LeType class
- # @return a LeType or LeClass child class
- # @throw KeyError if no corresponding child classes
- # @todo check if this method shouldn't be in _LeCrud
- @classmethod
- def uid2leobj(cls, uid):
- uid = int(uid)
- if uid not in cls._me_uid:
- raise KeyError("No LeType or LeClass child classes with uid '%d'"%uid)
- return cls._me_uid[uid]
-
- @classmethod
- def fieldtypes(cls):
- if cls._fieldtypes_all is None:
- cls._fieldtypes_all = dict()
- cls._fieldtypes_all.update(cls._uid_fieldtype)
- cls._fieldtypes_all.update(cls._leo_fieldtypes)
- return cls._fieldtypes_all
-
- @classmethod
- def typefilter(cls):
- if hasattr(cls, '_type_id'):
- return ('type_id','=', cls._type_id)
- elif hasattr(cls, '_class_id'):
- return ('class_id', '=', cls._class_id)
- else:
- raise ValueError("Cannot generate a typefilter with %s class"%cls.__name__)
-
- ## @brief Delete LeObjects from db given filters and a classname
- # @note if no classname given, take the caller class
- # @param filters list :
- # @param classname None|str : the classname or None
- # @return number of deleted LeObjects
- # @see leapi.lecrud._LeCrud.delete()
- @classmethod
- def delete(cls, filters, classname = None):
- ccls = cls if classname is None else cls.name2class(classname)
- new_filters = copy.copy(filters)
- new_filters.append(ccls.typefilter())
- return _LeCrud.delete(ccls, new_filters)
-
- ## @brief Check that a relational field is valid
- # @param field str : a relational field
- # @return a nature
- @staticmethod
- def _prepare_relational_field(field):
- spl = field.split('.')
- if len(spl) != 2:
- return ValueError("The relationalfield '%s' is not valid"%field)
- nature = spl[-1]
- if nature not in EditorialModel.classtypes.EmNature.getall():
- return ValueError("'%s' is not a valid nature in the field %s"%(nature, field))
-
- if spl[0] == 'superior':
- return (REL_SUP, nature)
- elif spl[0] == 'subordinate':
- return (REL_SUB, nature)
- else:
- return ValueError("Invalid preffix for relationnal field : '%s'"%spl[0])
-
- ## @brief Check if a LeType is a hierarchy root
- @staticmethod
- def ___is_root(leo):
- if isinstance(leo, leapi.letype.LeType):
- return False
- elif isinstance(leo, LeRoot):
- return True
- raise ValueError("Invalid value for a LeType : %s"%leo)
-
- ## @brief Return a LeRoot instance
- @staticmethod
- def ___get_root():
- return LeRoot()
-
- ## @brief Link two leobject together using a rel2type field
- # @param lesup LeType : LeType child class instance linked as superior
- # @param lesub LeType : LeType child class instance linked as subordinate
- # @param **rel_attr : Relation attributes
- # @return True if linked without problems
- # @throw LeObjectError if the link is not valid
- # @throw LeObkectError if the link already exists
- # @throw AttributeError if an non existing relation attribute is given as argument
- # @throw ValueError if the relation attrivute value check fails
- #
- # @todo Code factorisation on relation check
- # @todo unit tests
- @classmethod
- def ___link_together(cls, lesup, lesub, rank = 'last', **rel_attr):
- if lesub.__class__ not in lesup._linked_types.keys():
- raise LeObjectError("Relation error : %s cannot be linked with %s"%(lesup.__class__.__name__, lesub.__class__.__name__))
-
- for attr_name in rel_attr.keys():
- if attr_name not in [ f for f,g in lesup._linked_types[lesub.__class__] ]:
- raise AttributeError("A rel2type between a %s and a %s doesn't have an attribute %s"%(lesup.__class__.__name__, lesub.__class__.__name__))
- if not sup._linked_types[lesub.__class__][1].check(rel_attr[attr_name]):
- raise ValueError("Wrong value '%s' for attribute %s"%(rel_attr[attr_name], attr_name))
-
- #Checks that attributes are uniq for this relation
- rels_attr = [ attrs for lesup, lesub, attrs in cls.links_get(lesup) if lesup == lesup ]
- for e_attrs in rels_attrs:
- if rel_attr == e_attrs:
- raise LeObjectError("Relation error : a relation with the same attributes already exists")
-
- return cls._datasource.add_related(lesup, lesub, rank, **rel_attr)
-
- ## @brief Get related objects
- # @param leo LeType(instance) : LeType child class instance
- # @param letype LeType(class) : the wanted LeType child class (not instance)
- # @param leo_is_superior bool : if True leo is the superior in the relation
- # @return A dict with LeType child class instance as key and dict {rel_attr_name:rel_attr_value, ...}
- # @throw LeObjectError if the relation is not possible
- #
- # @todo Code factorisation on relation check
- # @todo unit tests
- @classmethod
- def ___linked_together(cls, leo, letype, leo_is_superior = True):
- valid_link = letype in leo._linked_types.keys() if leo_is_superior else leo.__class__ in letype._linked_types.keys()
-
- if not valid_link:
- raise LeObjectError("Relation error : %s have no links with %s"%(
- leo.__class__ if leo_is_superior else letype,
- letype if leo_is_superior else leo.__class__
- ))
-
- return cls._datasource.get_related(leo, letype, leo_is_superior)
-
- ## @brief Fetch a relation and its attributes
- # @param id_relation int : the relation identifier
- # @return a tuple(lesup, lesub, dict_attr) or False if no relation exists with this id
- # @throw Exception if the relation is not a rel2type relation
- @classmethod
- def ___link_get(cls, id_relation):
- return cls._datasource.get_relation(id_relation)
-
- ## @brief Fetch all relations for an objects
- # @param leo LeType : LeType child class instance
- # @return a list of tuple (lesup, lesub, dict_attr)
- def ___links_get(cls, leo):
- return cls._datasource.get_relations(leo)
-
- ## @brief Remove a link (and attributes) between two LeObject
- # @param id_relation int : Relation identifier
- # @return True if a link has been deleted
- # @throw LeObjectError if the relation is not a rel2type
- #
- # @todo Code factorisation on relation check
- # @todo unit tests
- @classmethod
- def ___link_remove(cls, id_relation):
- if lesub.__class__ not in lesup._linked_types.keys():
- raise LeObjectError("Relation errorr : %s cannot be linked with %s"%(lesup.__class__.__name__, lesub.__class__.__name__))
-
- return cls._datasource.del_related(lesup, lesub)
-
- ## @brief Add a hierarchy relation between two LeObject
- # @param lesup LeType|LeRoot : LeType child class instance
- # @param lesub LeType : LeType child class instance
- # @param nature str : The nature of the relation @ref EditorialModel.classtypes
- # @param rank str|int : The relation rank. Can be 'last', 'first' or an integer
- # @param replace_if_exists bool : if True delete the old superior and set the new one. If False and there is a superior raise an LeObjectQueryError
- # @return The relation ID or False if fails
- # @throw LeObjectQueryError replace_if_exists == False and there is a superior
- @classmethod
- def ___hierarchy_add(cls, lesup, lesub, nature, rank = 'last', replace_if_exists = False):
- #Arguments check
- if nature not in EditorialModel.classtypes.EmClassType.natures(lesub._classtype):
- raise ValueError("Invalid nature '%s' for %s"%(nature, lesup.__class__.__name__))
-
- if not cls.leo_is_root(lesup):
- if nature not in EditorialModel.classtypes.EmClassType.natures(lesup._classtype):
- raise ValueError("Invalid nature '%s' for %s"%(nature, lesup.__class__.__name__))
- if lesup.__class__ not in lesub._superiors[nature]:
- raise ValueError("%s is not a valid superior for %s"%(lesup.__class__, lesub.__class__))
- #else:
- # lesup is not a LeType but a hierarchy root
-
- if rank not in ['first', 'last'] and not isinstance(rank, int):
- raise ValueError("Allowed values for rank are integers and 'first' or 'last' but '%s' found"%rank)
-
- superiors = cls.hierarchy_get(lesub, nature, leo_is_sup = False)
- if lesup in len(superiors) > 0:
- if not replace_if_exists:
- raise LeObjectQueryError("The subordinate allready has a superior")
- #remove existig superior
- if not cls.hierarchy_del(superiors[0], lesub, nature):
- raise RuntimeError("Unable to delete the previous superior")
-
- return self._datasource.add_superior(lesup, lesub, nature, rank)
-
- ## @brief Delete a hierarchy link between two LeObject
- # @param lesup LeType | LeRoot : LeType child class or hierarchy root
- # @param lesub LeType : LeType child class
- # @param nature str : The nature of the relation @ref EditorialModel.classtypes
- # @return True if deletion done successfully
- # @throw ValueError when bad arguments given
- @classmethod
- def ___hierarchy_del(cls, lesup, lesub, nature):
- if nature not in EditorialModel.classtypes.EmClassType.natures(lesub._classtype):
- raise ValueError("Invalid nature '%s' for %s"%(nature, lesup.__class__.__name__))
-
- if not cls.leo_is_root(lesup):
- if nature not in EditorialModel.classtypes.EmClassType.natures(lesup._classtype):
- raise ValueError("Invalid nature '%s' for %s"%(nature, lesup.__class__.__name__))
- if lesup.__class__ not in lesub._superiors[nature]:
- raise ValueError("%s is not a valid superior for %s"%(lesup.__class__, lesub.__class__))
- superiors = cls.hierarchy_get(lesub, nature, leo_is_sup = False)
- res = True
- for _lesup in superiors:
- if not cls._datasource.del_superior(_lesup, lesub, nature):
- #How to handler this ?
- res = False
- return res
-
- ## @brief Fetch neighbour in hierarchy relation
- # @param leo LeType | LeRoot : We want the neighbour of this LeObject (can be the root)
- # @param nature str : @ref EditorialModel.classtypes
- # @param leo_is_sup bool : if True leo is the superior and we want to fetch the subordinates else its the oposite
- # @return A list of LeObject ordered by depth if leo_is_sup, else a list of subordinates
- @classmethod
- def ___hierarchy_get(cls, leo, nature, leo_is_sup = True):
- #Checking arguments
- if not (nature is None) and not cls.is_root(leo):
- if nature not in EditorialModel.classtypes.EmClassType.natures(leo._classtype):
- raise ValueError("Invalid nature '%s' for %s"%(nature, lesup.__class__.__name__))
-
- if leo_is_sup:
- return cls._datasource.get_subordinates(leo, nature)
- else:
- return cls._datasource.get_superiors(leo, nature)
-
- ## @brief Preparing letype and leclass arguments
- #
- # This function will do multiple things :
- # - Convert string to LeType or LeClass child instances
- # - If both letype and leclass given, check that letype inherit from leclass
- # - If only a letype is given, fetch the parent leclass
- # @note If we give only a leclass as argument returned letype will be None
- # @note Its possible to give letype=None and leclass=None. In this case the method will return tuple(None,None)
- # @param letype LeType|str|None : LeType child instant or its name
- # @param leclass LeClass|str|None : LeClass child instant or its name
- # @return a tuple with 2 python classes (LeTypeChild, LeClassChild)
- @classmethod
- def ___prepare_targets(cls, letype = None , leclass = None):
- warnings.warn("_LeObject._prepare_targets is deprecated", DeprecationWarning)
- raise ValueError()
- if not(leclass is None):
- if isinstance(leclass, str):
- leclass = LeFactory.leobj_from_name(leclass)
-
- if not isinstance(leclass, type) or not (leapi.leclass.LeClass in leclass.__bases__) or leclass.__class__ == leapi.leclass.LeClass:
- raise ValueError("None | str | LeType child class excpected, but got : '%s' %s"%(leclass,type(leclass)))
-
- if not(letype is None):
- if isinstance(letype, str):
- letype = LeFactory.leobj_from_name(letype)
-
- if not isinstance(letype, type) or not leapi.letype.LeType in letype.__bases__ or letype.__class__ == leapi.letype.LeType:
- raise ValueError("None | str | LeType child class excpected, but got : %s"%type(letype))
-
- if leclass is None:
- leclass = letype._leclass
- elif leclass != letype._leclass:
- raise ValueError("LeType child class %s does'nt inherite from LeClass %s"%(letype.__name__, leclass.__name__))
-
- return (letype, leclass)
-
- ## @brief Class designed to represent the hierarchy roots
- # @see _LeObject.get_root() _LeObject.is_root()
- class LeRoot(object):
- pass
-
- class LeObjectError(Exception):
- pass
-
- class LeObjectQueryError(LeObjectError):
- pass
-
- ## @page leobject_filters LeObject query filters
- # The LeObject API provide methods that accept filters allowing the user
- # to query the database and fetch LodelEditorialObjects.
- #
- # The LeObject API translate those filters for the datasource.
- #
- # @section api_user_side API user side filters
- # Filters are string expressing a condition. The string composition
- # is as follow : "<FIELD> <OPERATOR> <VALUE>"
- # @subsection fpart FIELD
- # @subsubsection standart fields
- # Standart fields, represents a value of the LeObject for example "title", "lodel_id" etc.
- # @subsubsection rfields relationnal fields
- # relationnal fields, represents a relation with the object hierarchy. Those fields are composed as follow :
- # "<RELATION>.<NATURE>".
- #
- # - Relation can takes two values : superiors or subordinates
- # - Nature is a relation nature ( see EditorialModel.classtypes )
- # Examples : "superiors.parent", "subordinates.translation" etc.
- # @note The field_list arguement of leapi.leapi._LeObject.get() use the same syntax than the FIELD filter part
- # @subsection oppart OPERATOR
- # The OPERATOR part of a filter is a comparison operator. There is
- # - standart comparison operators : = , <, > , <=, >=, !=
- # - list operators : 'in' and 'not in'
- # The list of allowed operators is sotred at leapi.leapi._LeObject._query_operators .
- # @subsection valpart VALUE
- # The VALUE part of a filter is... just a value...
- #
- # @section datasource_side Datasource side filters
- # As said above the API "translate" filters before forwarding them to the datasource.
- #
- # The translation process transform filters in tuple composed of 3 elements
- # ( @ref fpart , @ref oppart , @ref valpart ). Each element is a string.
- #
- # There is a special case for @ref rfields : the field element is a tuple composed with two elements
- # ( RELATION, NATURE ) where NATURE is a string ( see EditorialModel.classtypes ) and RELATION is one of
- # the defined constant :
- #
- # - leapi.leapi.REL_SUB for "subordinates"
- # - leapi.leapi.REL_SUP for "superiors"
- #
- # @note The filters translation process also check if given field are valids compared to the concerned letype and/or the leclass
|