From cf7a1990e508b581ea6c6333dc775f25ff482694 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 28 Apr 2016 16:03:56 +0200 Subject: [PATCH 01/16] [WIP] Relation supp in LIB --- syncano/models/fields.py | 47 ++++++++++++++++++++++++++++++++ syncano/models/manager_mixins.py | 9 ++++++ 2 files changed, 56 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 12f10f8..e6bc254 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -622,6 +622,7 @@ class SchemaField(JSONField): 'datetime', 'file', 'reference', + 'relation', 'array', 'object', 'geopoint', @@ -745,12 +746,58 @@ def to_python(self, value): return GeoPointStruct(latitude, longitude) +class RelationValidatorMixin(object): + + def validate(self, value, model_instance): + super(RelationValidatorMixin, self).validate(value, model_instance) + self._validate(value) + + @classmethod + def _validate(cls, value): + value = cls._make_list(value) + all_ints = all([isinstance(x, int) for x in value]) + from archetypes import Model + all_objects = all([isinstance(obj, Model) for obj in value]) + object_types = [type(obj) for obj in value] + if len(set(object_types)) != 1: + raise SyncanoValueError("All objects should be the same type.") + + if (all_ints and all_objects) or (not all_ints and not all_objects): + raise SyncanoValueError("List elements should be objects or integers.") + + @classmethod + def _make_list(cls, value): + if not isinstance(value, (list, tuple)): + value = [value] + return value + + +class RelationField(RelationValidatorMixin, WritableField): + + def to_python(self, value): + if isinstance(value, dict) and 'type' in value and 'value' in value: + value = value['value'] + + if not isinstance(value, (list, tuple)): + return [value] + + return value + + def to_native(self, value): + if not isinstance(value, (list, tuple)): + value = [value] + + self._validate(value) + return value + + MAPPING = { 'string': StringField, 'text': StringField, 'file': FileField, 'ref': StringField, 'reference': ReferenceField, + 'relation': RelationField, 'integer': IntegerField, 'float': FloatField, 'boolean': BooleanField, diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py index 7f0e0e7..f574cfd 100644 --- a/syncano/models/manager_mixins.py +++ b/syncano/models/manager_mixins.py @@ -110,3 +110,12 @@ def _check_field_type_for_increment(cls, model, field_name): return True return False + + +class RelationMixin(object): + + def add(self): + pass + + def remove(self): + pass From 6133c2598fffdfc555df3da944e36acf90365dd9 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 12:31:57 +0200 Subject: [PATCH 02/16] [LIB-678] add relation manager with add and remove methods; --- syncano/models/archetypes.py | 3 ++ syncano/models/fields.py | 30 ++------------- syncano/models/relations.py | 71 ++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 syncano/models/relations.py diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index ce42147..984ebde 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -235,6 +235,9 @@ def to_python(self, data): value = data[field_name] setattr(self, field.name, value) + if isinstance(field, fields.RelationField): + setattr(self, "{}_set".format(field_name), field(instance=self, field_name=field_name)) + def to_native(self): """Converts the current instance to raw data which can be serialized to JSON and send to API. diff --git a/syncano/models/fields.py b/syncano/models/fields.py index e6bc254..4d7093b 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -11,6 +11,7 @@ from .manager import SchemaManager from .registry import registry +from .relations import RelationManager, RelationValidatorMixin class JSONToPythonMixin(object): @@ -746,34 +747,11 @@ def to_python(self, value): return GeoPointStruct(latitude, longitude) -class RelationValidatorMixin(object): - - def validate(self, value, model_instance): - super(RelationValidatorMixin, self).validate(value, model_instance) - self._validate(value) - - @classmethod - def _validate(cls, value): - value = cls._make_list(value) - all_ints = all([isinstance(x, int) for x in value]) - from archetypes import Model - all_objects = all([isinstance(obj, Model) for obj in value]) - object_types = [type(obj) for obj in value] - if len(set(object_types)) != 1: - raise SyncanoValueError("All objects should be the same type.") - - if (all_ints and all_objects) or (not all_ints and not all_objects): - raise SyncanoValueError("List elements should be objects or integers.") - - @classmethod - def _make_list(cls, value): - if not isinstance(value, (list, tuple)): - value = [value] - return value - - class RelationField(RelationValidatorMixin, WritableField): + def __call__(self, instance, field_name): + return RelationManager(instance=instance, field_name=field_name) + def to_python(self, value): if isinstance(value, dict) and 'type' in value and 'value' in value: value = value['value'] diff --git a/syncano/models/relations.py b/syncano/models/relations.py new file mode 100644 index 0000000..31b25be --- /dev/null +++ b/syncano/models/relations.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from syncano.exceptions import SyncanoValueError + + +class RelationValidatorMixin(object): + + def validate(self, value, model_instance): + super(RelationValidatorMixin, self).validate(value, model_instance) + self._validate(value) + + @classmethod + def _validate(cls, value): + value = cls._make_list(value) + all_ints = all([isinstance(x, int) for x in value]) + from archetypes import Model + all_objects = all([isinstance(obj, Model) for obj in value]) + object_types = [type(obj) for obj in value] + if len(set(object_types)) != 1: + raise SyncanoValueError("All objects should be the same type.") + + if (all_ints and all_objects) or (not all_ints and not all_objects): + raise SyncanoValueError("List elements should be objects or integers.") + + if all_objects: + return True + return False + + @classmethod + def _make_list(cls, value): + if not isinstance(value, (list, tuple)): + value = [value] + return value + + +class RelationManager(RelationValidatorMixin): + + def __init__(self, instance, field_name): + super(RelationManager, self).__init__() + self.instance = instance + self.model = instance._meta + self.field_name = field_name + + def add(self, *args): + if self._validate(args): + value_ids = [obj.id for obj in args] + else: + value_ids = args + + connection = self.instance._meta.connection + + data = {self.field_name: {'_add': value_ids}} + meta = self.instance._meta + update_path = meta.get_endpoint(name='detail')['path'] + update_path = update_path.format(**self.instance.get_endpoint_data()) + response = connection.request('PATCH', update_path, data=data) + self.instance.to_python(response) + + def remove(self, *args): + if self._validate(args): + value_ids = [obj.id for obj in args] + else: + value_ids = args + + connection = self.instance._meta.connection + + data = {self.field_name: {'_remove': value_ids}} + meta = self.instance._meta + update_path = meta.get_endpoint(name='detail')['path'] + update_path = update_path.format(**self.instance.get_endpoint_data()) + response = connection.request('PATCH', update_path, data=data) + self.instance.to_python(response) From 604a75831e5f13efaabc67ff9d1aa388164d82d9 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 13:21:09 +0200 Subject: [PATCH 03/16] [LIB-678] add integration test for relations --- syncano/models/fields.py | 3 +- tests/integration_test_relations.py | 59 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/integration_test_relations.py diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 4d7093b..4f44412 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -765,7 +765,8 @@ def to_native(self, value): if not isinstance(value, (list, tuple)): value = [value] - self._validate(value) + if self._validate(value): + value = [obj.id for obj in value] return value diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py new file mode 100644 index 0000000..9b6e3a2 --- /dev/null +++ b/tests/integration_test_relations.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from syncano.models import Class +from tests.integration_test import InstanceMixin, IntegrationTest + + +class ResponseTemplateApiTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(ResponseTemplateApiTest, cls).setUpClass() + + # prapare data + cls.author = Class.please.create(name="author", schema=[ + {"name": "name", "type": "string", "filter_index": True}, + {"name": "birthday", "type": "integer"}, + ]) + + cls.book = Class.please.create(name="book", schema=[ + {"name": "title", "type": "string", "filter_index": True}, + {"name": "authors", "type": "relation", "target": "author"}, + ]) + + cls.prus = cls.author.objects.create(name='Bolesław Prus', birthday=1847) + cls.lem = cls.author.objects.create(name='Stanisław Lem', birthday=1921) + cls.coehlo = cls.author.objects.create(name='Paulo Coehlo', birthday=1947) + + cls.lalka = cls.book.objects.create(authors=[cls.prus.id], title='Lalka') + cls.niezwyciezony = cls.book.objects.create(authors=[cls.lem.id], title='Niezwyciężony') + cls.brida = cls.book.objects.create(authors=[cls.coehlo.id], title='Brida') + + def test_integers_list(self): + authors_list_ids = [self.prus.id, self.coehlo.id] + book = self.book.object.create(authors=authors_list_ids, title='Strange title') + self.assertListEqual(book.authors, authors_list_ids) + + def test_object_list(self): + authors_list_ids = [self.prus, self.coehlo] + book = self.book.object.create(authors=authors_list_ids, title='Strange title') + self.assertListEqual(book.authors, authors_list_ids) + + def test_object_assign(self): + self.lalka.authors = [self.lem, self.coehlo] + self.lalka.save() + + self.assertListEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + + def test_related_field_add(self): + self.lalka.authors_set.add(self.coehlo) + self.assertListEqual(self.lalka.authors, [self.prus.id, self.coehlo.id]) + + self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) + self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + + def test_related_field_remove(self): + self.lalka.authors_set.remove(self.prus) + self.assertListEqual(self.lalka.authors, []) + + self.lalka.authors_set.remove(self.prus, self.lem, self.coehlo) + self.assertListEqual(self.lalka.authors, [self.prus.id, self.lem.id, self.coehlo.id]) From 720852a396ae5065d4680d7746f7859a5eaf35df Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 13:27:54 +0200 Subject: [PATCH 04/16] [LIB-678] add integration test for relations --- tests/integration_test_relations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 9b6e3a2..d54ab67 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -30,12 +30,12 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] - book = self.book.object.create(authors=authors_list_ids, title='Strange title') + book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(book.authors, authors_list_ids) def test_object_list(self): authors_list_ids = [self.prus, self.coehlo] - book = self.book.object.create(authors=authors_list_ids, title='Strange title') + book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(book.authors, authors_list_ids) def test_object_assign(self): From 3d0bc216ac2621614897196bdc5b7780f1260424 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 14:40:11 +0200 Subject: [PATCH 05/16] [LIB-678] add integration test for relations --- syncano/models/relations.py | 2 +- tests/integration_test_relations.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/syncano/models/relations.py b/syncano/models/relations.py index 31b25be..03a4e33 100644 --- a/syncano/models/relations.py +++ b/syncano/models/relations.py @@ -12,7 +12,7 @@ def validate(self, value, model_instance): def _validate(cls, value): value = cls._make_list(value) all_ints = all([isinstance(x, int) for x in value]) - from archetypes import Model + from .archetypes import Model all_objects = all([isinstance(obj, Model) for obj in value]) object_types = [type(obj) for obj in value] if len(set(object_types)) != 1: diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index d54ab67..4c46a51 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -34,7 +34,7 @@ def test_integers_list(self): self.assertListEqual(book.authors, authors_list_ids) def test_object_list(self): - authors_list_ids = [self.prus, self.coehlo] + authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(book.authors, authors_list_ids) @@ -45,15 +45,15 @@ def test_object_assign(self): self.assertListEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) def test_related_field_add(self): - self.lalka.authors_set.add(self.coehlo) - self.assertListEqual(self.lalka.authors, [self.prus.id, self.coehlo.id]) + self.niezwyciezony.authors_set.add(self.coehlo) + self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) def test_related_field_remove(self): - self.lalka.authors_set.remove(self.prus) - self.assertListEqual(self.lalka.authors, []) + self.brida.authors_set.remove(self.coehlo) + self.assertListEqual(self.brida.authors, []) - self.lalka.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertListEqual(self.lalka.authors, [self.prus.id, self.lem.id, self.coehlo.id]) + self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) + self.assertListEqual(self.niezwyciezony.authors, []) From 6a02d4507213b0b3677a826126f90589e0381a7b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 14:58:54 +0200 Subject: [PATCH 06/16] [LIB-678] correct test and to_naive and to_python methods --- syncano/models/fields.py | 6 ++++++ tests/integration_test_relations.py | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 4f44412..3bf787f 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -753,6 +753,9 @@ def __call__(self, instance, field_name): return RelationManager(instance=instance, field_name=field_name) def to_python(self, value): + if not value: + return None + if isinstance(value, dict) and 'type' in value and 'value' in value: value = value['value'] @@ -762,6 +765,9 @@ def to_python(self, value): return value def to_native(self, value): + if not value: + return None + if not isinstance(value, (list, tuple)): value = [value] diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 4c46a51..8815876 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -31,29 +31,29 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertListEqual(book.authors, authors_list_ids) + self.assertItemsEqual(book.authors, authors_list_ids) def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertListEqual(book.authors, authors_list_ids) + self.assertItemsEqual(book.authors, authors_list_ids) def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() - self.assertListEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + self.assertItemsEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) - self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) + self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) - self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) - self.assertListEqual(self.brida.authors, []) + self.assertItemsEqual(self.brida.authors, None) self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertListEqual(self.niezwyciezony.authors, []) + self.assertItemsEqual(self.niezwyciezony.authors, None) From 9719122d8694db881dfae008324af4c8201dff56 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 15:09:07 +0200 Subject: [PATCH 07/16] [LIB-678] correct test and to_naive and to_python methods --- tests/integration_test_relations.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 8815876..f84bb8a 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -31,29 +31,29 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertItemsEqual(book.authors, authors_list_ids) + self.assertCountEqual(book.authors, authors_list_ids) def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertItemsEqual(book.authors, authors_list_ids) + self.assertCountEqual(book.authors, authors_list_ids) def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() - self.assertItemsEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + self.assertCountEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) - self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) + self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) - self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) - self.assertItemsEqual(self.brida.authors, None) + self.assertEqual(self.brida.authors, None) self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertItemsEqual(self.niezwyciezony.authors, None) + self.assertEqual(self.niezwyciezony.authors, None) From b13d164d92c888e539799ceee77eb97aa512b26e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 15:21:21 +0200 Subject: [PATCH 08/16] [LIB-678] correct test and to_naive and to_python methods --- tests/integration_test_relations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index f84bb8a..ec3d2e9 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -31,25 +31,25 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertCountEqual(book.authors, authors_list_ids) + self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertCountEqual(book.authors, authors_list_ids) + self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() - self.assertCountEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + self.assertListEqual(sorted(self.lalka.authors), sorted([self.lem.id, self.coehlo.id])) def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) - self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) + self.assertListEqual(sorted(self.niezwyciezony.authors), sorted([self.lem.id, self.coehlo.id])) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) - self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + self.assertListEqual(sorted(self.niezwyciezony.authors), sorted([self.lem.id, self.prus.id, self.coehlo.id])) def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) From 023fe00d7bec8a2c3f8bc795d04d6e557e652067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 3 May 2016 11:07:59 +0200 Subject: [PATCH 09/16] [LIB-678] remove uneeded mixin and make code more DRY; --- syncano/models/manager_mixins.py | 9 --------- syncano/models/relations.py | 29 ++++++++++------------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py index f574cfd..7f0e0e7 100644 --- a/syncano/models/manager_mixins.py +++ b/syncano/models/manager_mixins.py @@ -110,12 +110,3 @@ def _check_field_type_for_increment(cls, model, field_name): return True return False - - -class RelationMixin(object): - - def add(self): - pass - - def remove(self): - pass diff --git a/syncano/models/relations.py b/syncano/models/relations.py index 03a4e33..2307d6e 100644 --- a/syncano/models/relations.py +++ b/syncano/models/relations.py @@ -41,30 +41,21 @@ def __init__(self, instance, field_name): self.field_name = field_name def add(self, *args): - if self._validate(args): - value_ids = [obj.id for obj in args] - else: - value_ids = args - - connection = self.instance._meta.connection - - data = {self.field_name: {'_add': value_ids}} - meta = self.instance._meta - update_path = meta.get_endpoint(name='detail')['path'] - update_path = update_path.format(**self.instance.get_endpoint_data()) - response = connection.request('PATCH', update_path, data=data) - self.instance.to_python(response) + self._add_or_remove(args) def remove(self, *args): - if self._validate(args): - value_ids = [obj.id for obj in args] - else: - value_ids = args + self._add_or_remove(args, operation='_remove') - connection = self.instance._meta.connection + def _add_or_remove(self, id_list, operation='_add'): + if self._validate(id_list): + value_ids = [obj.id for obj in id_list] + else: + value_ids = id_list - data = {self.field_name: {'_remove': value_ids}} meta = self.instance._meta + connection = meta.connection + + data = {self.field_name: {operation: value_ids}} update_path = meta.get_endpoint(name='detail')['path'] update_path = update_path.format(**self.instance.get_endpoint_data()) response = connection.request('PATCH', update_path, data=data) From 6a71c83debcd76fb48e69ae74308903e0334d89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 16:00:31 +0200 Subject: [PATCH 10/16] [LIB-678] start to implement filters; --- syncano/models/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 3bf787f..2c0a659 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -748,6 +748,7 @@ def to_python(self, value): class RelationField(RelationValidatorMixin, WritableField): + query_allowed = True def __call__(self, instance, field_name): return RelationManager(instance=instance, field_name=field_name) @@ -764,6 +765,9 @@ def to_python(self, value): return value + def to_query(self, value, lookup_type): + pass + def to_native(self, value): if not value: return None From d8768d59852acec3e89be74a8484b1d716721a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:14:08 +0200 Subject: [PATCH 11/16] [LIB-678] add contains and is filtering on relation field --- syncano/models/fields.py | 27 ++++++++++++--- syncano/models/manager.py | 54 ++++++++++++++++++++++------- tests/integration_test_relations.py | 19 ++++++++++ 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 2980d8b..944bed1 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -138,7 +138,7 @@ def to_native(self, value): """ return value - def to_query(self, value, lookup_type): + def to_query(self, value, lookup_type, **kwargs): """ Returns field's value prepared for usage in HTTP request query. """ @@ -725,11 +725,11 @@ def to_native(self, value): return geo_struct - def to_query(self, value, lookup_type): + def to_query(self, value, lookup_type, **kwargs): """ Returns field's value prepared for usage in HTTP request query. """ - super(GeoPointField, self).to_query(value, lookup_type) + super(GeoPointField, self).to_query(value, lookup_type, **kwargs) if lookup_type not in ['near', 'exists']: raise SyncanoValueError('Lookup {} not supported for geopoint field'.format(lookup_type)) @@ -816,8 +816,25 @@ def to_python(self, value): return value - def to_query(self, value, lookup_type): - pass + def to_query(self, value, lookup_type, related_field_name=None, related_field_lookup=None, **kwargs): + + if not self.query_allowed: + raise self.ValidationError('Query on this field is not supported.') + + if lookup_type not in ['contains', 'is']: + raise SyncanoValueError('Lookup {} not supported for relation field.'.format(lookup_type)) + + query_dict = {} + + if lookup_type == 'contains': + if self._validate(value): + value = [obj.id for obj in value] + query_dict = value + + if lookup_type == 'is': + query_dict = {related_field_name: {"_{0}".format(related_field_lookup): value}} + + return query_dict def to_native(self, value): if not value: diff --git a/syncano/models/manager.py b/syncano/models/manager.py index cd72e0f..c3dd04e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -882,7 +882,8 @@ class for :class:`~syncano.models.base.Object` model. LOOKUP_SEPARATOR = '__' ALLOWED_LOOKUPS = [ 'gt', 'gte', 'lt', 'lte', - 'eq', 'neq', 'exists', 'in', 'near' + 'eq', 'neq', 'exists', 'in', 'startswith', + 'near', 'is', 'contains', ] def __init__(self): @@ -955,28 +956,57 @@ def filter(self, **kwargs): lookup = 'eq' if self.LOOKUP_SEPARATOR in field_name: - field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 1) - - if field_name not in model._meta.field_names: - allowed = ', '.join(model._meta.field_names) - raise SyncanoValueError('Invalid field name "{0}" allowed are {1}.'.format(field_name, allowed)) - - if lookup not in self.ALLOWED_LOOKUPS: - allowed = ', '.join(self.ALLOWED_LOOKUPS) - raise SyncanoValueError('Invalid lookup type "{0}" allowed are {1}.'.format(lookup, allowed)) + model_name, field_name, lookup = self._get_lookup_attributes(field_name) for field in model._meta.fields: if field.name == field_name: break - query.setdefault(field_name, {}) - query[field_name]['_{0}'.format(lookup)] = field.to_query(value, lookup) + self._validate_lookup(model, model_name, field_name, lookup, field) + + query_main_lookup, query_main_field = self._get_main_lookup(model_name, field_name, lookup) + + query.setdefault(query_main_field, {}) + query[query_main_field]['_{0}'.format(query_main_lookup)] = field.to_query( + value, + query_main_lookup, + related_field_name=field_name, + related_field_lookup=lookup, + ) self.query['query'] = json.dumps(query) self.method = 'GET' self.endpoint = 'list' return self + def _get_lookup_attributes(self, field_name): + try: + model_name, field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 2) + except ValueError: + model_name = None + field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 1) + + return model_name, field_name, lookup + + def _validate_lookup(self, model, model_name, field_name, lookup, field): + if not model_name and field_name not in model._meta.field_names: + allowed = ', '.join(model._meta.field_names) + raise SyncanoValueError('Invalid field name "{0}" allowed are {1}.'.format(field_name, allowed)) + + if lookup not in self.ALLOWED_LOOKUPS: + allowed = ', '.join(self.ALLOWED_LOOKUPS) + raise SyncanoValueError('Invalid lookup type "{0}" allowed are {1}.'.format(lookup, allowed)) + + if model_name and field.__class__.__name__ != 'RelationField': + raise SyncanoValueError('Lookup supported only for RelationField.') + + @classmethod + def _get_main_lookup(cls, model_name, field_name, lookup): + if model_name: + return 'is', model_name + else: + return lookup, field_name + def bulk_create(self, *objects): """ Creates many new objects. diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index ec3d2e9..13f9c24 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -57,3 +57,22 @@ def test_related_field_remove(self): self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) self.assertEqual(self.niezwyciezony.authors, None) + + def test_related_field_lookup_contains(self): + filtered_books = self.book.objects.list().filter(author__contains=[self.prus]) + + self.assertEqual(len(filtered_books), 1) + + for book in filtered_books: + self.assertEqual(book.title, self.lalka.title) + + def test_related_field_lookup_contains_fail(self): + filtered_books = self.book.objects.list().filter(author__contains=[self.prus, self.lem]) + self.assertEqual(len(filtered_books), 0) + + def test_related_field_lookup_is(self): + filtered_books = self.book.objects.list().filter(author__name__startswith='Stan') + + self.assertEqual(len(filtered_books), 1) + for book in filtered_books: + self.assertEqual(book.title, self.niezwyciezony.title) From 3994735bc63ca36dd23d43eb8855b89dc5b97dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:16:00 +0200 Subject: [PATCH 12/16] [LIB-678] add filter index in test correct spell error - in names --- tests/integration_test_relations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 13f9c24..7b9672d 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -17,7 +17,7 @@ def setUpClass(cls): cls.book = Class.please.create(name="book", schema=[ {"name": "title", "type": "string", "filter_index": True}, - {"name": "authors", "type": "relation", "target": "author"}, + {"name": "authors", "type": "relation", "target": "author", "filter_index": True}, ]) cls.prus = cls.author.objects.create(name='Bolesław Prus', birthday=1847) @@ -59,7 +59,7 @@ def test_related_field_remove(self): self.assertEqual(self.niezwyciezony.authors, None) def test_related_field_lookup_contains(self): - filtered_books = self.book.objects.list().filter(author__contains=[self.prus]) + filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) self.assertEqual(len(filtered_books), 1) @@ -67,11 +67,11 @@ def test_related_field_lookup_contains(self): self.assertEqual(book.title, self.lalka.title) def test_related_field_lookup_contains_fail(self): - filtered_books = self.book.objects.list().filter(author__contains=[self.prus, self.lem]) + filtered_books = self.book.objects.list().filter(authors__contains=[self.prus, self.lem]) self.assertEqual(len(filtered_books), 0) def test_related_field_lookup_is(self): - filtered_books = self.book.objects.list().filter(author__name__startswith='Stan') + filtered_books = self.book.objects.list().filter(authors__name__startswith='Stan') self.assertEqual(len(filtered_books), 1) for book in filtered_books: From 25db2dec73b519e0d0a8b594cf7a0cd17c3de63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:25:48 +0200 Subject: [PATCH 13/16] [LIB-678] correct model_name initialization --- syncano/models/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index c3dd04e..8ce1606 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -954,6 +954,7 @@ def filter(self, **kwargs): for field_name, value in six.iteritems(kwargs): lookup = 'eq' + model_name = None if self.LOOKUP_SEPARATOR in field_name: model_name, field_name, lookup = self._get_lookup_attributes(field_name) From fbfb410dc233c9fb584e75689b2fcde871abc8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:32:58 +0200 Subject: [PATCH 14/16] [LIB-678] add list() in tests asserts --- tests/integration_test_relations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 7b9672d..2ae0a85 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -61,18 +61,18 @@ def test_related_field_remove(self): def test_related_field_lookup_contains(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) - self.assertEqual(len(filtered_books), 1) + self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: self.assertEqual(book.title, self.lalka.title) def test_related_field_lookup_contains_fail(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus, self.lem]) - self.assertEqual(len(filtered_books), 0) + self.assertEqual(len(list(filtered_books)), 0) def test_related_field_lookup_is(self): filtered_books = self.book.objects.list().filter(authors__name__startswith='Stan') - self.assertEqual(len(filtered_books), 1) + self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: self.assertEqual(book.title, self.niezwyciezony.title) From 81a5e3e3a65303f61c6239fd55b5a51bd9054871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:44:46 +0200 Subject: [PATCH 15/16] [LIB-678] clean up test a little --- tests/integration_test_relations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 2ae0a85..3bfac92 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -33,11 +33,15 @@ def test_integers_list(self): book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) + book.delete() + def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) + book.delete() + def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() @@ -51,6 +55,9 @@ def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) self.assertListEqual(sorted(self.niezwyciezony.authors), sorted([self.lem.id, self.prus.id, self.coehlo.id])) + self.niezwyciezony.authors = [self.lem] + self.niezwyciezony.save() + def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) self.assertEqual(self.brida.authors, None) @@ -58,6 +65,9 @@ def test_related_field_remove(self): self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) self.assertEqual(self.niezwyciezony.authors, None) + self.niezwyciezony.authors_set.add(self.lem) + self.brida.authors_set.add(self.coehlo) + def test_related_field_lookup_contains(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) From 8175bd60e772b00a9f08ffe843cbfa6687a3335f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 5 May 2016 01:09:04 +0200 Subject: [PATCH 16/16] [LIB-678] correct tests --- tests/integration_test_relations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 3bfac92..37348b6 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -47,6 +47,8 @@ def test_object_assign(self): self.lalka.save() self.assertListEqual(sorted(self.lalka.authors), sorted([self.lem.id, self.coehlo.id])) + self.lalka.authors = [self.prus] + self.lalka.save() def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) @@ -65,8 +67,10 @@ def test_related_field_remove(self): self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) self.assertEqual(self.niezwyciezony.authors, None) - self.niezwyciezony.authors_set.add(self.lem) - self.brida.authors_set.add(self.coehlo) + self.niezwyciezony.authors = [self.lem] + self.niezwyciezony.save() + self.brida.authors = [self.coehlo] + self.brida.save() def test_related_field_lookup_contains(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus])