diff --git a/DataSource/dummy/leapidatasource.py b/DataSource/dummy/leapidatasource.py index 2e329f6..a0de7c5 100644 --- a/DataSource/dummy/leapidatasource.py +++ b/DataSource/dummy/leapidatasource.py @@ -20,7 +20,7 @@ class DummyDatasource(object): # @param limit int : Number of row to be returned # @param offset int : Used with limit to choose the start row # @return a list of LeCrud child classes - def select(self, target_cls, field_list, filters, rel_filters=None, order=None, group=None, limit=None, offset=0): + def select(self, target_cls, field_list, filters, rel_filters=None, order=None, group=None, limit=None, offset=0, instanciate=True): pass ## @brief delete lodel editorial components given filters diff --git a/EditorialModel/classtypes.py b/EditorialModel/classtypes.py index 6f21c6b..7c84bc8 100644 --- a/EditorialModel/classtypes.py +++ b/EditorialModel/classtypes.py @@ -4,15 +4,15 @@ common_fields = { 'lodel_id': { 'fieldtype': 'pk', - 'internal': 'automatic', + 'internal': 'autosql', }, 'class_id': { 'fieldtype': 'emuid', - 'class_id': True, + 'is_id_class': True, }, 'type_id': { 'fieldtype': 'emuid', - 'class_id': False, + 'is_id_class': False, }, 'string': { 'fieldtype': 'char', @@ -22,20 +22,20 @@ common_fields = { 'creation_date': { 'fieldtype': 'datetime', 'now_on_create': True, - 'internal': 'automatic', + 'internal': 'autosql', }, 'modification_date': { 'fieldtype': 'datetime', 'now_on_create': True, 'now_on_update': True, - 'internal': 'automatic', + 'internal': 'autosql', } } relations_common_fields = { 'id_relation': { 'fieldtype': 'pk', - 'internal': 'automatic', + 'internal': 'autosql', }, 'nature': { 'fieldtype': 'naturerelation', @@ -45,7 +45,7 @@ relations_common_fields = { 'internal': 'automatic', }, 'rank': { - 'fieldtype': 'integer', + 'fieldtype': 'rank', 'internal': 'automatic', }, 'superior': { diff --git a/EditorialModel/fields.py b/EditorialModel/fields.py index a07d5fd..9378315 100644 --- a/EditorialModel/fields.py +++ b/EditorialModel/fields.py @@ -39,8 +39,8 @@ class EmField(EmComponent): if not internal: self.internal = False else: - if internal.lower() not in ['automatic']: - raise ValueError("The internal arguments possible values are : [False, 'object', 'automatic']") + if internal.lower() not in ['automatic', 'autosql']: + raise ValueError("The internal arguments possible values are : [False, 'autosql', 'automatic']") self.internal = internal.lower() if self.internal == 'object' and name not in EditorialModel.classtypes.common_fields.keys(): diff --git a/EditorialModel/fieldtypes/emuid.py b/EditorialModel/fieldtypes/emuid.py index 1a54314..783052f 100644 --- a/EditorialModel/fieldtypes/emuid.py +++ b/EditorialModel/fieldtypes/emuid.py @@ -10,27 +10,30 @@ class EmFieldType(integer.EmFieldType): help = 'Fieldtypes designed to handle editorial model UID for LeObjects' - def __init__(self, id_class=True, **kwargs): + def __init__(self, is_id_class, **kwargs): + self._is_id_class = is_id_class kwargs['internal'] = 'automatic' - super(EmFieldType, self).__init__(id_class = id_class, **kwargs) + super(EmFieldType, self).__init__(is_id_class = is_id_class, **kwargs) def _check_data_value(self, value): - if not( value is None): - return ValueError("Cannot set this value. Only None is authorized") + return (value, None) def construct_data(self, lec, fname, datas): - if isinstance(lec, letype._LeType): - # if None try to fetch data from lec itself - fname[datas] = lec.em_uid()[ 0 if self.class_id else 1] + if self.is_id_class: + if lec.implements_leclass(): + datas[fname] = lec._class_id + else: + datas[fname] = None else: - raise RuntimeError("The LeObject is not a LeType") + if lec.implements_letype(): + datas[fname] = lec._type_id + else: + datas[fname] = None + return datas[fname] def check_data_consistency(self, lec, fname, datas): - if isinstance(lec, lecrud._LeCrud) and lec.implements_letype(): - if datas[fname] != (lec._class_id if self.class_id else lec._type_id): - return FieldTypeError("Given Editorial model uid doesn't fit with given LeObject") - else: - return FieldTypeError("You have to give a LeType !!!") + if datas[fname] != (lec._class_id if self.is_id_class else lec._type_id): + return FieldTypeError("Given Editorial model uid doesn't fit with given LeObject") diff --git a/EditorialModel/fieldtypes/generic.py b/EditorialModel/fieldtypes/generic.py index 3c6e1c4..71f7051 100644 --- a/EditorialModel/fieldtypes/generic.py +++ b/EditorialModel/fieldtypes/generic.py @@ -78,7 +78,13 @@ class GenericFieldType(object): # @param datas dict : dict storing fields values # @return constructed datas def construct_data(self, lec, fname, datas): - return datas[fname] + if fname in datas: + return datas[fname] + elif hasattr(lec.fieldtypes()[fname], 'default'): + return lec.fieldtypes()[fname].default + elif lec.fieldtypes()[fname].nullable: + return None + raise RuntimeError("Unable to construct data for field %s", fname) ## @brief Check datas consistency # @param leo LeCrud : A LeCrud child class instance diff --git a/EditorialModel/fieldtypes/pk.py b/EditorialModel/fieldtypes/pk.py index 2d47f94..863c2ec 100644 --- a/EditorialModel/fieldtypes/pk.py +++ b/EditorialModel/fieldtypes/pk.py @@ -13,7 +13,7 @@ class EmFieldType(integer.EmFieldType): allowed = { 'nullable': False, 'uniq': False, - 'internal': 'automatic', + 'internal': 'autosql', 'primary': True, } # Checking args diff --git a/EditorialModel/fieldtypes/rank.py b/EditorialModel/fieldtypes/rank.py new file mode 100644 index 0000000..1431f5f --- /dev/null +++ b/EditorialModel/fieldtypes/rank.py @@ -0,0 +1,25 @@ +#-*- coding utf-8 -*- + +import leapi.lerelation as lerelation + +from .generic import FieldTypeError +from . import integer + +class EmFieldType(integer.EmFieldType): + + help = 'Fieldtype designed to handle relations rank' + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def construct_data(self, lec, fname, datas): + superior_id = datas[lec._superior_field_name] + if lec.is_lerel2type(): + subordinate = lec._subordinate_cls + sub_em_type_id = subordinate._type_id + datas[fname] = lec.get_max_rank(superior_id, sub_em_type_id) + elif lec.is_lehierarch(): + datas[fname] = lec.get_max_rank(superior_id, datas['nature']) + else: + raise ValueError("Called with bad class : ", lec.__name__) + return datas[fname] diff --git a/EditorialModel/test/me.json b/EditorialModel/test/me.json index 2108112..1958e9c 100644 --- a/EditorialModel/test/me.json +++ b/EditorialModel/test/me.json @@ -269,10 +269,11 @@ "class_id": 1, "date_create": "Fri Oct 16 11:05:04 2015", "rank": 1, - "fieldtype": "integer", + "fieldtype": "emuid", "help_text": "", "internal": "automatic", "rel_field_id": null, + "is_id_class": true, "name": "class_id" }, "24": { @@ -304,10 +305,11 @@ "class_id": 1, "date_create": "Fri Oct 16 11:05:04 2015", "rank": 3, - "fieldtype": "integer", + "fieldtype": "emuid", "help_text": "", "internal": "automatic", "rel_field_id": null, + "is_id_class": false, "name": "type_id" }, "26": { @@ -323,7 +325,7 @@ "rank": 4, "fieldtype": "pk", "help_text": "", - "internal": "automatic", + "internal": "autosql", "rel_field_id": null, "name": "lodel_id" }, @@ -338,10 +340,11 @@ "class_id": 2, "date_create": "Fri Oct 16 11:05:04 2015", "rank": 1, - "fieldtype": "integer", + "fieldtype": "emuid", "help_text": "", "internal": "automatic", "rel_field_id": null, + "is_id_class": true, "name": "class_id" }, "29": { @@ -373,10 +376,11 @@ "class_id": 2, "date_create": "Fri Oct 16 11:05:04 2015", "rank": 3, - "fieldtype": "integer", + "fieldtype": "emuid", "help_text": "", "internal": "automatic", "rel_field_id": null, + "is_id_class": false, "name": "type_id" }, "31": { @@ -392,7 +396,7 @@ "rank": 4, "fieldtype": "pk", "help_text": "", - "internal": "automatic", + "internal": "autosql", "rel_field_id": null, "name": "lodel_id" }, @@ -407,10 +411,11 @@ "class_id": 13, "date_create": "Fri Oct 16 11:05:04 2015", "rank": 1, - "fieldtype": "integer", + "fieldtype": "emuid", "help_text": "", "internal": "automatic", "rel_field_id": null, + "is_id_class": true, "name": "class_id" }, "34": { @@ -442,10 +447,11 @@ "class_id": 13, "date_create": "Fri Oct 16 11:05:04 2015", "rank": 3, - "fieldtype": "integer", + "fieldtype": "emuid", "help_text": "", "internal": "automatic", "rel_field_id": null, + "is_id_class": false, "name": "type_id" }, "36": { @@ -461,7 +467,7 @@ "rank": 4, "fieldtype": "pk", "help_text": "", - "internal": "automatic", + "internal": "autosql", "rel_field_id": null, "name": "lodel_id" }, @@ -477,7 +483,7 @@ "class_id": 1, "date_create": "Wed Nov 4 10:52:13 2015", "now_on_create": true, - "internal": "automatic", + "internal": "autosql", "fieldtype": "datetime", "now_on_update": true, "help_text": "", @@ -497,7 +503,7 @@ "rank": 6, "string": "", "help_text": "", - "internal": "automatic", + "internal": "autosql", "optional": false, "name": "creation_date", "fieldtype": "datetime" @@ -516,7 +522,7 @@ "string": "", "now_on_update": true, "help_text": "", - "internal": "automatic", + "internal": "autosql", "optional": false, "name": "modification_date", "fieldtype": "datetime" @@ -534,7 +540,7 @@ "rank": 6, "string": "", "help_text": "", - "internal": "automatic", + "internal": "autosql", "optional": false, "name": "creation_date", "fieldtype": "datetime" @@ -553,7 +559,7 @@ "string": "", "now_on_update": true, "help_text": "", - "internal": "automatic", + "internal": "autosql", "optional": false, "name": "modification_date", "fieldtype": "datetime" @@ -571,9 +577,9 @@ "rank": 6, "string": "", "help_text": "", - "internal": "automatic", + "internal": "autosql", "optional": false, "name": "creation_date", "fieldtype": "datetime" } -} \ No newline at end of file +} diff --git a/leapi/lecrud.py b/leapi/lecrud.py index 693165c..e27817c 100644 --- a/leapi/lecrud.py +++ b/leapi/lecrud.py @@ -215,6 +215,9 @@ class _LeCrud(object): def is_lerel2type(cls): return cls.implements_lerel2type() + def uidget(self): + return getattr(self, self.uidname()) + ## @brief Returns object datas # @param # @return a dict of fieldname : value @@ -411,6 +414,7 @@ class _LeCrud(object): raise LeApiErrors("Error when inserting",[ValueError("The class '%s' was not found"%classname)]) if not callcls.implements_letype() and not callcls.implements_lerelation(): raise ValueError("You can only insert relations and LeTypes objects but tying to insert a '%s'"%callcls.__name__) + insert_datas = callcls.prepare_datas(datas, complete = True, allow_internal = False) return callcls._datasource.insert(callcls, **insert_datas) @@ -430,6 +434,7 @@ class _LeCrud(object): ret_datas = cls.check_datas_value(datas, complete, allow_internal) if isinstance(ret_datas, Exception): raise ret_datas + if complete: ret_datas = cls._construct_datas(ret_datas) cls._check_datas_consistency(ret_datas) @@ -458,7 +463,7 @@ class _LeCrud(object): def _construct_datas(cls, datas): res_datas = dict() for fname, ftype in cls.fieldtypes().items(): - if fname in datas: + if not ftype.is_internal() or ftype.internal != 'autosql': res_datas[fname] = ftype.construct_data(cls, fname, datas) return res_datas ## @brief Check datas consistency @@ -581,7 +586,7 @@ class _LeCrud(object): err_l[field] = ret else: res_filters.append((field,operator, value)) - + if len(err_l) > 0: raise LeApiDataCheckError("Error while preparing filters : ", err_l) return (res_filters, rel_filters) diff --git a/leapi/lefactory.py b/leapi/lefactory.py index 69661d7..cc53d11 100644 --- a/leapi/lefactory.py +++ b/leapi/lefactory.py @@ -210,7 +210,7 @@ import %s type_id = None for fname, finfo in EditorialModel.classtypes.common_fields.items(): if finfo['fieldtype'] == 'emuid': - if finfo['class_id']: + if finfo['is_id_class']: class_id = fname else: type_id = fname diff --git a/leapi/lerelation.py b/leapi/lerelation.py index 5bbf1c4..c4b5dc1 100644 --- a/leapi/lerelation.py +++ b/leapi/lerelation.py @@ -2,7 +2,9 @@ import copy import re +import warnings +import EditorialModel.classtypes import EditorialModel.fieldtypes.leo as ft_leo from . import lecrud from . import leobject @@ -18,7 +20,7 @@ class _LeRelation(lecrud._LeCrud): def __init__(self, id_relation, **kwargs): super().__init__(id_relation, **kwargs) - + ## @brief Forge a filter to match the superior @classmethod def sup_filter(self, leo): @@ -101,7 +103,11 @@ class _LeRelation(lecrud._LeCrud): # @return True in case of success, False in case of failure # @throw ValueError if step is not castable into an integer def set_rank(self, new_rank): - max_rank = self.get_max_rank() + raise NotImplemented("Abtract method") + + ## @brief Implements set_rank + def _set_rank(self, new_rank, **get_max_rank_args): + max_rank = self.get_max_rank(**get_max_rank_args) try: new_rank = int(new_rank) except ValueError: @@ -128,9 +134,7 @@ class _LeRelation(lecrud._LeCrud): ## @returns The maximum assignable rank for this relation # @todo implementation def get_max_rank(self): - max_rank_result = self.__class__.get(field_list=['rank'], order=[('rank', 'DESC')], limit=1) - max_rank = int(max_rank_result[0].rank) - return max_rank+1 + raise NotImplemented("Abstract method") ## @brief Abstract class to handle hierarchy relations class _LeHierarch(_LeRelation): @@ -138,6 +142,46 @@ class _LeHierarch(_LeRelation): ## @brief Delete current instance from DB def delete(self): lecrud._LeCrud._delete(self) + + ## @brief modify a LeHierarch rank + # @param new_rank int|str : The new rank can be an integer > 1 or strings 'first' or 'last' + # @return True in case of success, False in case of failure + # @throw ValueError if step is not castable into an integer + def set_rank(self, new_rank): + return self._set_rank( + new_rank, + id_superior=getattr(self, self.uidname()), + nature=self.nature + ) + + @classmethod + def insert(cls, datas): + # Checks if the relation exists + res = cls.get( + [(cls._subordinate_field_name, '=', datas['subordinate']), ('nature', '=', datas['nature'])], + [ cls.uidname() ] + ) + if not(res is None) and len(res) > 0: + return False + return super().insert(datas, 'LeHierarch') + + ## @brief Get maximum assignable rank given a superior id and a nature + # @return an integer > 1 + @classmethod + def get_max_rank(cls, id_superior, nature): + if nature not in EditorialModel.classtypes.EmNature.getall(): + raise ValueError("Unknow relation nature '%s'" % nature) + sql_res = cls.get( + query_filters=[ + ('nature','=', nature), + (cls._superior_field_name, '=', id_superior), + ], + field_list=['rank'], + order=[('rank', 'DESC')], + limit=1, + instanciate=False + ) + return sql_res[0]['rank']+1 if not(sql_res is None) and len(sql_res) > 0 else 1 ## @brief instanciate the relevant lodel object using a dict of datas @classmethod @@ -157,6 +201,23 @@ class _LeRel2Type(_LeRelation): ## @brief Delete current instance from DB def delete(self): lecrud._LeCrud._delete(self) + + ## @brief modify a LeRel2Type rank + # @param new_rank int|str : The new rank can be an integer > 1 or strings 'first' or 'last' + # @return True in case of success, False in case of failure + # @throw ValueError if step is not castable into an integer + def set_rank(self, new_rank): + return self._set_rank( + new_rank, + id_superior=getattr(self, self.uidname()), + type_em_id=self._subordinate_cls._type_id + ) + + @classmethod + def get_max_rank(cls, id_superior, type_em_id): + # SELECT rank FROM relation JOIN object ON object.lodel_id = id_subordinate WHERE object.type_id = + warnings.warn("LeRel2Type.get_max_rank() is not implemented yet and will always return 0") + return 0 ## @brief Implements insert for rel2type # @todo checks when autodetecing the rel2type class diff --git a/leapi/letype.py b/leapi/letype.py index d6ab7bd..4b48e61 100644 --- a/leapi/letype.py +++ b/leapi/letype.py @@ -74,9 +74,9 @@ class _LeType(_LeClass): # @return relation id if successfully created else returns false def add_superior(self, superior, nature, del_if_exists = False): lehierarch = self.name2class('LeHierarch') - if del_if_exists: + if del_if_exists: #Duplicated test with lehierarch.insert prev_sup = lehierarch.get( - [('subordinate', '=', self), ('nature', '=', nature)], + [(lehierarch._subordinate_field_name, '=', self), ('nature', '=', nature)], [ lehierarch.uidname() ] ) if len(prev_sup) > 0: diff --git a/leapi/test/test_lecrud.py b/leapi/test/test_lecrud.py index c1a9c94..5698136 100644 --- a/leapi/test/test_lecrud.py +++ b/leapi/test/test_lecrud.py @@ -2,6 +2,8 @@ Tests for _LeCrud and LeCrud """ +import copy + import unittest from unittest import TestCase from unittest.mock import patch @@ -188,11 +190,18 @@ class LeCrudTestCase(TestCase): ] for lecclass, ndats in ndatas: lecclass.insert(ndats) - dsmock.assert_called_once_with(lecclass, **ndats) - dsmock.reset_mock() + # Adding default values for internal fields + expt_dats = copy.copy(ndats) + expt_dats['string'] = None + expt_dats['class_id'] = lecclass._class_id + expt_dats['type_id'] = lecclass._type_id if lecclass.implements_letype() else None + + dsmock.assert_called_once_with(lecclass, **expt_dats) + dsmock.reset_mock() + lecclass.insert(ndats) - dsmock.assert_called_once_with(lecclass, **ndats) + dsmock.assert_called_once_with(lecclass, **expt_dats) dsmock.reset_mock() ## @todo try failing on inserting from LeClass child or LeObject @@ -308,7 +317,8 @@ class LeCrudTestCase(TestCase): order=None, group=None, limit=None, - offset=0 + offset=0, + instanciate=True ) dsmock.reset_mock() @@ -387,6 +397,7 @@ class LeCrudTestCase(TestCase): exptargs['filters'].append( ('type_id', '=', callcls._type_id) ) if callcls.implements_leclass: exptargs['filters'].append( ('class_id', '=', callcls._class_id) ) + exptargs['instanciate'] = True callcls.get(**getargs) dsmock.assert_called_once_with(**exptargs) dsmock.reset_mock() diff --git a/leapi/test/test_leobject.py b/leapi/test/test_leobject.py index 3ad7a5b..0aed6b8 100644 --- a/leapi/test/test_leobject.py +++ b/leapi/test/test_leobject.py @@ -2,6 +2,8 @@ Tests for _LeObject and LeObject """ +import copy + import unittest from unittest import TestCase from unittest.mock import patch @@ -114,12 +116,18 @@ class LeObjectMockDatasourceTestCase(TestCase): {'titre':'world'}, ] for ndats in ndatas: + # Adding values for internal fields + expt_dats = copy.copy(ndats) + expt_dats['string'] = None + expt_dats['class_id'] = Publication._class_id + expt_dats['type_id'] = Numero._type_id + Publication.insert(ndats, classname='Numero') - dsmock.assert_called_once_with(Numero, **ndats) + dsmock.assert_called_once_with(Numero, **expt_dats) dsmock.reset_mock() LeObject.insert(ndats, classname = 'Numero') - dsmock.assert_called_once_with(Numero, **ndats) + dsmock.assert_called_once_with(Numero, **expt_dats) dsmock.reset_mock() @unittest.skip("Wait FieldTypes modification (cf. #90) and classmethod capabilities for update") diff --git a/leapi/test/test_lerelation.py b/leapi/test/test_lerelation.py index dca457e..b20e95a 100644 --- a/leapi/test/test_lerelation.py +++ b/leapi/test/test_lerelation.py @@ -180,6 +180,9 @@ class LeHierarch(LeRelationTestCase): ] """ for query, equery in queries: + equery['rank'] = 1 + equery['depth'] = None + LeHierarch.insert(query) dsmock.assert_called_once_with(LeHierarch, **equery) dsmock.reset_mock() @@ -241,7 +244,7 @@ class LeRel2TypeTestCase(LeRelationTestCase): for query in queries: Rel_Textes2Personne.insert(query) - eres = {'nature': None} + eres = {'nature': None, 'depth': None, 'rank': 0} eres.update(query) for fname in ('superior', 'subordinate'): if isinstance(eres[fname], int):