From 43e8145dab84f1fe868fe994f93830d1a5c3502d Mon Sep 17 00:00:00 2001 From: Yann Date: Mon, 11 Jan 2016 14:03:01 +0100 Subject: [PATCH] Implements get_max_rank() in LeApi. Add a fieldtype rank. + A lot of bugfix The me.json was not up to date (Models don't forward classtypes modifications to loaded json file) Some bugfix on fieldtypes that was not used (because of old me.json) --- DataSource/dummy/leapidatasource.py | 2 +- EditorialModel/classtypes.py | 14 +++--- EditorialModel/fields.py | 4 +- EditorialModel/fieldtypes/emuid.py | 29 +++++++----- EditorialModel/fieldtypes/generic.py | 8 +++- EditorialModel/fieldtypes/pk.py | 2 +- EditorialModel/fieldtypes/rank.py | 25 ++++++++++ EditorialModel/test/me.json | 38 ++++++++------- leapi/lecrud.py | 9 +++- leapi/lefactory.py | 2 +- leapi/lerelation.py | 71 ++++++++++++++++++++++++++-- leapi/letype.py | 4 +- leapi/test/test_lecrud.py | 19 ++++++-- leapi/test/test_leobject.py | 12 ++++- leapi/test/test_lerelation.py | 5 +- 15 files changed, 186 insertions(+), 58 deletions(-) create mode 100644 EditorialModel/fieldtypes/rank.py 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):