1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2025-11-21 13:19:16 +01:00
lodel2_mirror/leobject/leobject.py

414 lines
19 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#-*- 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