mirror of
https://github.com/yweber/lodel2.git
synced 2025-11-21 13:19:16 +01:00
414 lines
19 KiB
Python
414 lines
19 KiB
Python
#-*- coding: utf-8 -*-
|
||
|
||
## @package leobject API to access lodel datas
|
||
#
|
||
# This package contains abstract classes leobject.leclass.LeClass , leobject.letype.LeType, leobject.leobject._LeObject.
|
||
# Those abstract classes are designed to be mother classes of dynamically generated classes ( see leobject.lefactory.LeFactory )
|
||
|
||
## @package leobject.leobject
|
||
# @brief Abstract class designed to be implemented by LeObject
|
||
#
|
||
# @note LeObject will be generated by leobject.lefactory.LeFactory
|
||
|
||
import re
|
||
|
||
import leobject
|
||
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(object):
|
||
|
||
## @brief The editorial model
|
||
_model = None
|
||
## @brief The datasource
|
||
_datasource = None
|
||
## @brief maps em uid with LeType or LeClass keys are uid values are LeObject childs classes
|
||
_me_uid = dict()
|
||
|
||
_query_re = None
|
||
_query_operators = ['=', '<=', '>=', '!=', '<', '>', ' in ', ' not in ']
|
||
|
||
## @brief Instantiate with a Model and a DataSource
|
||
# @param **kwargs dict : datas usefull to instanciate a _LeObject
|
||
def __init__(self, **kwargs):
|
||
raise NotImplementedError("Abstract constructor")
|
||
|
||
## @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
|
||
@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]
|
||
|
||
## @brief Creates new entries in the datasource
|
||
# @param datas list : A list a dict with fieldname as key
|
||
# @param cls
|
||
# @return a list of inserted lodel_id
|
||
# @see leobject.datasources.dummy.DummyDatasource.insert(), leobject.letype.LeType.insert()
|
||
@classmethod
|
||
def insert(cls, letype, datas):
|
||
if isinstance(datas, dict):
|
||
datas = [datas]
|
||
|
||
if cls == _LeObject:
|
||
raise NotImplementedError("Abstract method")
|
||
letype,leclass = cls._prepare_targets(letype)
|
||
if letype is None:
|
||
raise ValueError("letype argument cannot be None")
|
||
|
||
for data in datas:
|
||
letype.check_datas_or_raise(data, complete = True)
|
||
return cls._datasource.insert(letype, leclass, datas)
|
||
|
||
## @brief Delete LeObjects given filters
|
||
# @param cls
|
||
# @param letype LeType|str : LeType child class or name
|
||
# @param leclass LeClass|str : LeClass child class or name
|
||
# @param filters list : list of filters (see @ref leobject_filters)
|
||
# @return bool
|
||
@classmethod
|
||
def delete(cls, letype, filters):
|
||
letype, leclass = cls._prepare_targets(letype)
|
||
filters,relationnal_filters = leobject.leobject._LeObject._prepare_filters(filters, letype, leclass)
|
||
return cls._datasource.delete(letype, leclass, filters, relationnal_filters)
|
||
|
||
## @brief Update LeObjects given filters and datas
|
||
# @param cls
|
||
# @param letype LeType|str : LeType child class or name
|
||
# @param filters list : list of filters (see @ref leobject_filters)
|
||
@classmethod
|
||
def update(cls, letype, filters, datas):
|
||
letype, leclass = cls._prepare_targets(letype)
|
||
filters,relationnal_filters = leobject.leobject._LeObject._prepare_filters(filters, letype, leclass)
|
||
if letype is None:
|
||
raise ValueError("Argument letype cannot be None")
|
||
letype.check_datas_or_raise(datas, False)
|
||
return cls._datasource.update(letype, leclass, filters, relationnal_filters, datas)
|
||
|
||
## @brief make a search to retrieve a collection of LeObject
|
||
# @param query_filters list : list of string of query filters (or tuple (FIELD, OPERATOR, VALUE) ) see @ref leobject_filters
|
||
# @param field_list list|None : list of string representing fields see @ref leobject_filters
|
||
# @param typename str : The name of the LeType we want
|
||
# @param classname str : The name of the LeClass we want
|
||
# @param cls
|
||
# @return responses ({string:*}): a list of dict with field:value
|
||
@classmethod
|
||
def get(cls, query_filters, field_list = None, typename = None, classname = None):
|
||
|
||
letype,leclass = cls._prepare_targets(typename, classname)
|
||
|
||
#Checking field_list
|
||
if field_list is None or len(field_list) == 0:
|
||
#default field_list
|
||
if not (letype is None):
|
||
field_list = letype._fields
|
||
elif not (leclass is None):
|
||
field_list = leclass._fieldtypes.keys()
|
||
else:
|
||
field_list = list(EditorialModel.classtypes.common_fields.keys())
|
||
|
||
#Fetching LeType
|
||
if letype is None:
|
||
if 'type_id' not in field_list:
|
||
field_list.append('type_id')
|
||
|
||
|
||
field_list = cls._prepare_field_list(field_list, letype, leclass)
|
||
|
||
#preparing filters
|
||
filters, relationnal_filters = cls._prepare_filters(query_filters, letype, leclass)
|
||
|
||
#Fetching datas from datasource
|
||
datas = cls._datasource.get(leclass, letype, field_list, filters, relationnal_filters)
|
||
|
||
#Instanciating corresponding LeType child classes with datas
|
||
result = list()
|
||
for leobj_datas in datas:
|
||
letype = self.uid2leobj(datas['type_id']) if letype is None else letype
|
||
result.append(letype(datas))
|
||
|
||
return result
|
||
|
||
## @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 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, **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))
|
||
return cls._datasource.add_related(lesup, lesub, **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 Remove a link (and attributes) between two LeObject
|
||
# @param lesup LeType : LeType child instance
|
||
# @param lesub LeType : LeType child instance
|
||
# @return True if a link has been deleted
|
||
# @throw LeObjectError if the relation between the two LeObject is not possible
|
||
#
|
||
# @todo Code factorisation on relation check
|
||
# @todo unit tests
|
||
@classmethod
|
||
def link_remove(cls, lesup, lesub):
|
||
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 Prepare a field_list
|
||
# @param field_list list : List of string representing fields
|
||
# @param letype LeType : LeType child class
|
||
# @param leclass LeClass : LeClass child class
|
||
# @return A well formated field list
|
||
@classmethod
|
||
def _prepare_field_list(cls, field_list, letype, leclass):
|
||
cls._check_fields(letype, leclass, [f for f in field_list if not cls._field_is_relational(f)])
|
||
for i, field in enumerate(field_list):
|
||
if cls._field_is_relational(field):
|
||
field_list[i] = cls._prepare_relational_field(field)
|
||
return field_list
|
||
|
||
## @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)
|
||
@staticmethod
|
||
def _prepare_targets(letype = None , leclass = None):
|
||
|
||
if not(leclass is None):
|
||
if isinstance(leclass, str):
|
||
leclass = leobject.lefactory.LeFactory.leobj_from_name(leclass)
|
||
|
||
if not isinstance(leclass, type) or not (leobject.leclass.LeClass in leclass.__bases__) or leclass.__class__ == leobject.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 = leobject.lefactory.LeFactory.leobj_from_name(letype)
|
||
|
||
if not isinstance(letype, type) or not leobject.letype.LeType in letype.__bases__ or letype.__class__ == leobject.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 Check if a fieldname is valid
|
||
# @param letype LeType|None : The concerned type (or None)
|
||
# @param leclass LeClass|None : The concerned class (or None)
|
||
# @param fields list : List of string representing fields
|
||
# @throw LeObjectQueryError if their is some problems
|
||
# @throw AttributeError if letype is not from the leclass class
|
||
# @todo Delete the checks of letype and leclass and ensure that this method is called with letype and leclass arguments from _prepare_targets()
|
||
#
|
||
# @see @ref leobject_filters
|
||
@staticmethod
|
||
def _check_fields(letype, leclass, fields):
|
||
#Checking that fields in the query_filters are correct
|
||
if letype is None and leclass is None:
|
||
#Only fields from the object table are allowed
|
||
for field in fields:
|
||
if field not in EditorialModel.classtypes.common_fields.keys():
|
||
raise LeObjectQueryError("Not typename and no classname given, but the field %s is not in the common_fields list"%field)
|
||
else:
|
||
if letype is None:
|
||
field_l = leclass._fieldtypes.keys()
|
||
else:
|
||
if not (leclass is None):
|
||
if letype._leclass != leclass:
|
||
raise AttributeError("The EmType %s is not a specialisation of the EmClass %s"%(typename, classname))
|
||
field_l = letype._fields
|
||
#Checks that fields are in this type
|
||
for field in fields:
|
||
if field not in field_l:
|
||
raise LeObjectQueryError("No field named '%s' in '%s'"%(field, letype.__name__))
|
||
pass
|
||
|
||
## @brief Prepare filters for datasource
|
||
#
|
||
# This method divide filters in two categories :
|
||
# - filters : standart FIELDNAME OP VALUE filter
|
||
# - relationnal_filters : filter on object relation RELATION_NATURE OP VALUE
|
||
#
|
||
# Both categories of filters are represented in the same way, a tuple with 3 elements (NAME|NAT , OP, VALUE )
|
||
#
|
||
# @warning This method assume that letype and leclass are returned from _LeObject._prepare_targets() method
|
||
# @param filters_l list : This list can contain str "FIELDNAME OP VALUE" and tuples (FIELDNAME, OP, VALUE)
|
||
# @param letype LeType|None : needed to check filters
|
||
# @param leclass LeClass|None : needed to check filters
|
||
# @return a tuple(FILTERS, RELATIONNAL_FILTERS
|
||
#
|
||
# @see @ref datasource_side
|
||
@staticmethod
|
||
def _prepare_filters(filters_l, letype = None, leclass = None):
|
||
filters = list()
|
||
for fil in filters_l:
|
||
if len(fil) == 3 and not isinstance(fil, str):
|
||
filters.append(tuple(fil))
|
||
else:
|
||
filters.append(_LeObject._split_filter(fil))
|
||
|
||
#Checking relational filters (for the moment fields like superior.NATURE)
|
||
relational_filters = [ (_LeObject._prepare_relational_field(field), operator, value) for field, operator, value in filters if _LeObject._field_is_relational(field)]
|
||
filters = [f for f in filters if not _LeObject._field_is_relational(f[0])]
|
||
#Checking the rest of the fields
|
||
_LeObject._check_fields(letype, leclass, [ f[0] for f in filters ])
|
||
|
||
return (filters, relational_filters)
|
||
|
||
|
||
## @brief Check if a field is relational or not
|
||
# @param field str : the field to test
|
||
# @return True if the field is relational else False
|
||
@staticmethod
|
||
def _field_is_relational(field):
|
||
return field.startswith('superior.') or field.startswith('subordinate')
|
||
|
||
## @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:
|
||
raise LeObjectQueryError("The relationalfield '%s' is not valid"%field)
|
||
nature = spl[-1]
|
||
if nature not in EditorialModel.classtypes.EmNature.getall():
|
||
raise LeObjectQueryError("'%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:
|
||
raise LeObjectQueryError("Invalid preffix for relationnal field : '%s'"%spl[0])
|
||
|
||
## @brief Check and split a query filter
|
||
# @note The query_filter format is "FIELD OPERATOR VALUE"
|
||
# @param query_filter str : A query_filter string
|
||
# @param cls
|
||
# @return a tuple (FIELD, OPERATOR, VALUE)
|
||
@classmethod
|
||
def _split_filter(cls, query_filter):
|
||
if cls._query_re is None:
|
||
cls._compile_query_re()
|
||
|
||
matches = cls._query_re.match(query_filter)
|
||
if not matches:
|
||
raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
|
||
|
||
result = (matches.group('field'), re.sub(r'\s', ' ', matches.group('operator'), count=0), matches.group('value').strip())
|
||
for r in result:
|
||
if len(r) == 0:
|
||
raise ValueError("The query_filter '%s' seems to be invalid"%query_filter)
|
||
return result
|
||
|
||
## @brief Compile the regex for query_filter processing
|
||
# @note Set _LeObject._query_re
|
||
@classmethod
|
||
def _compile_query_re(cls):
|
||
op_re_piece = '(?P<operator>(%s)'%cls._query_operators[0].replace(' ', '\s')
|
||
for operator in cls._query_operators[1:]:
|
||
op_re_piece += '|(%s)'%operator.replace(' ', '\s')
|
||
op_re_piece += ')'
|
||
cls._query_re = re.compile('^\s*(?P<field>(((superior)|(subordinate))\.)?[a-z_][a-z0-9\-_]*)\s*'+op_re_piece+'\s*(?P<value>[^<>=!].*)\s*$', flags=re.IGNORECASE)
|
||
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 leobject.leobject._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 leobject.leobject._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 :
|
||
#
|
||
# - leobject.leobject.REL_SUB for "subordinates"
|
||
# - leobject.leobject.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
|