From 9d6611d96a267750cac1155aa481b28a5bbe67eb Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Thu, 7 Feb 2019 14:49:09 +0100 Subject: [PATCH 01/12] update: add a test for PUT. But it's passing... --- djangoldp/tests/runner.py | 5 +++- djangoldp/tests/{tests.py => tests_save.py} | 27 ++++++++++++++++- djangoldp/tests/tests_update.py | 32 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) rename djangoldp/tests/{tests.py => tests_save.py} (61%) create mode 100644 djangoldp/tests/tests_update.py diff --git a/djangoldp/tests/runner.py b/djangoldp/tests/runner.py index 8e043144..1cf8604d 100644 --- a/djangoldp/tests/runner.py +++ b/djangoldp/tests/runner.py @@ -24,6 +24,9 @@ from django.test.runner import DiscoverRunner test_runner = DiscoverRunner(verbosity=1) -failures = test_runner.run_tests(['djangoldp.tests.tests']) +failures = test_runner.run_tests([ + 'djangoldp.tests.tests_save', + 'djangoldp.tests.tests_update']) if failures: sys.exit(failures) + diff --git a/djangoldp/tests/tests.py b/djangoldp/tests/tests_save.py similarity index 61% rename from djangoldp/tests/tests.py rename to djangoldp/tests/tests_save.py index 647b26c3..e4108d1f 100644 --- a/djangoldp/tests/tests.py +++ b/djangoldp/tests/tests_save.py @@ -6,7 +6,7 @@ from djangoldp.tests.models import Skill, JobOffer class Serializer(TestCase): - def test_container_serializer_save(self): + def test_save_m2m(self): skill1 = Skill.objects.create(title="skill1") skill2 = Skill.objects.create(title="skill2") job = {"title": "job test", @@ -43,3 +43,28 @@ class Serializer(TestCase): self.assertEquals(result.title, "job test") self.assertIs(result.skills.count(), 0) + + def test_update(self): + skill1 = Skill.objects.create(title="skill1") + skill2 = Skill.objects.create(title="skill2") + job1 = JobOffer.objects.create(title="job test") + + job = {"@id": "https://happy-dev.fr/skills/{}/".format(job1.pk), + "title": "job test updated", + "skills": { + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + ]} + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job) + serializer.is_valid() + result = serializer.save() + + self.assertEquals(result.title, "job test updated") + self.assertIs(result.skills.count(), 0) diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py new file mode 100644 index 00000000..f2606c03 --- /dev/null +++ b/djangoldp/tests/tests_update.py @@ -0,0 +1,32 @@ +from django.test import TestCase + +from djangoldp.serializers import LDPSerializer +from djangoldp.tests.models import Skill, JobOffer + + +class Serializer(TestCase): + + def test_update(self): + skill1 = Skill.objects.create(title="skill1") + skill2 = Skill.objects.create(title="skill2") + job1 = JobOffer.objects.create(title="job test") + + job = {"@id": "https://happy-dev.fr/skills/{}/".format(job1.pk), + "title": "job test updated", + "skills": { + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + ]} + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job) + serializer.is_valid() + result = serializer.save() + + self.assertEquals(result.title, "job test updated") + self.assertIs(result.skills.count(), 2) -- GitLab From e9a0ec9e8b852313e2c31fc5403d0355025b1962 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Sun, 10 Feb 2019 11:16:36 +0100 Subject: [PATCH 02/12] fix: PUT should work with flatten JSonLD input --- djangoldp/serializers.py | 55 ++++++++++++++++++++++++++++++++- djangoldp/tests/tests_save.py | 24 -------------- djangoldp/tests/tests_update.py | 30 ++++++++++++++++-- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 6ce27477..603ac7b3 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -65,6 +65,12 @@ class JsonLdField(HyperlinkedRelatedField): except MultiValueDictKeyError: pass + def to_internal_value(self, data): + return super().to_internal_value(data) + + def get_value(self, dictionary): + return super().get_value(dictionary) + class JsonLdRelatedField(JsonLdField): def to_representation(self, value): @@ -103,12 +109,19 @@ class JsonLdIdentityField(JsonLdField): except: return super().to_internal_value(data) + def get_value(self, dictionary): + return super().get_value(dictionary) + class LDPSerializer(HyperlinkedModelSerializer): url_field_name = "@id" serializer_related_field = JsonLdRelatedField serializer_url_field = JsonLdIdentityField + @property + def data(self): + return super().data + def get_default_field_names(self, declared_fields, model_info): try: fields = list(self.Meta.model._meta.serializer_fields) @@ -130,6 +143,27 @@ class LDPSerializer(HyperlinkedModelSerializer): get_perms(self.context['request'].user, obj)] return data + def build_standard_field(self, field_name, model_field): + class JSonLDStandardField: + parent_view_name = None + + def __init__(self, **kwargs): + self.parent_view_name = kwargs.pop('parent_view_name') + super().__init__(**kwargs) + + def get_value(self, dictionary): + try: + object_list = dictionary["@graph"] + part_id = '/{}'.format(get_resolver().reverse_dict[self.parent_view_name][0][0][0], self.parent.instance.pk) + obj = next(filter(lambda o: part_id in o['@id'], object_list)) + return super().get_value(obj) + except KeyError: + return super().get_value(dictionary) + + field_class, field_kwargs = super().build_standard_field(field_name, model_field) + field_kwargs['parent_view_name'] = '{}-list'.format(model_field.model._meta.object_name.lower()) + return type(field_class.__name__ + 'Valued', (JSonLDStandardField, field_class), {}), field_kwargs + def build_nested_field(self, field_name, relation_info, nested_depth): class NestedLDPSerializer(self.__class__): @@ -147,6 +181,9 @@ class LDPSerializer(HyperlinkedModelSerializer): view_name='{}-detail'.format(model._meta.object_name.lower()), queryset=model.objects.all()).to_internal_value(data) + def get_value(self, dictionary): + return super().get_value(dictionary) + kwargs = get_nested_relation_kwargs(relation_info) kwargs['read_only'] = False kwargs['required'] = False @@ -154,7 +191,7 @@ class LDPSerializer(HyperlinkedModelSerializer): @classmethod def many_init(cls, *args, **kwargs): - kwargs['child'] = cls() + kwargs['child'] = cls(**kwargs) try: cls.Meta.depth = kwargs['context']['view'].many_depth except KeyError: @@ -174,3 +211,19 @@ class LDPSerializer(HyperlinkedModelSerializer): getattr(obj, field_name).add(item) return obj + + def update(self, instance, validated_data): + nested_fields = [] + nested_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data)) + for field_name in nested_fields_name: + nested_fields.append((field_name, validated_data.pop(field_name))) + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + + for (field_name, data) in nested_fields: + for item in data: + getattr(instance, field_name).add(item) + + return instance diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py index e4108d1f..53f48e21 100644 --- a/djangoldp/tests/tests_save.py +++ b/djangoldp/tests/tests_save.py @@ -44,27 +44,3 @@ class Serializer(TestCase): self.assertEquals(result.title, "job test") self.assertIs(result.skills.count(), 0) - def test_update(self): - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") - job1 = JobOffer.objects.create(title="job test") - - job = {"@id": "https://happy-dev.fr/skills/{}/".format(job1.pk), - "title": "job test updated", - "skills": { - "ldp:contains": [ - {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, - {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, - ]} - } - - meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} - - meta_class = type('Meta', (), meta_args) - serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) - serializer = serializer_class(data=job) - serializer.is_valid() - result = serializer.save() - - self.assertEquals(result.title, "job test updated") - self.assertIs(result.skills.count(), 0) diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index f2606c03..c3b65b5c 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -11,7 +11,7 @@ class Serializer(TestCase): skill2 = Skill.objects.create(title="skill2") job1 = JobOffer.objects.create(title="job test") - job = {"@id": "https://happy-dev.fr/skills/{}/".format(job1.pk), + job = {"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), "title": "job test updated", "skills": { "ldp:contains": [ @@ -24,9 +24,35 @@ class Serializer(TestCase): meta_class = type('Meta', (), meta_args) serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) - serializer = serializer_class(data=job) + serializer = serializer_class(data=job, instance=job1) serializer.is_valid() result = serializer.save() self.assertEquals(result.title, "job test updated") self.assertIs(result.skills.count(), 2) + + def test_update_graph(self): + skill1 = Skill.objects.create(title="skill1") + skill2 = Skill.objects.create(title="skill2") + job1 = JobOffer.objects.create(title="job test") + + job = {"@graph": [{"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), + "title": "job test updated", + "skills": { + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + ]} + }] + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job, instance=job1) + serializer.is_valid() + result = serializer.save() + + self.assertEquals(result.title, "job test updated") + #self.assertIs(result.skills.count(), 2) -- GitLab From 7417cb7d1160d6a590743a20531f2a26c82f13fa Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Mon, 11 Feb 2019 11:20:03 +0100 Subject: [PATCH 03/12] fix: PUT should save nested relations --- djangoldp/serializers.py | 13 +++++++++++++ djangoldp/tests/tests_update.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 603ac7b3..740a8b37 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -30,6 +30,16 @@ class LDListMixin: self.id = parent_id + self.field_name + "/" return super().get_attribute(instance) + def get_value(self, dictionary): + try: + object_list = dictionary["@graph"] + view_name = '{}-list'.format(self.parent.Meta.model._meta.object_name.lower()) + part_id = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][0], self.parent.instance.pk) + obj = next(filter(lambda o: part_id in o['@id'], object_list)) + return super().get_value(obj) + except KeyError: + return super().get_value(dictionary) + class ContainerSerializer(LDListMixin, ListSerializer): id = '' @@ -198,6 +208,9 @@ class LDPSerializer(HyperlinkedModelSerializer): pass return ContainerSerializer(*args, **kwargs) + def get_value(self, dictionary): + return super().get_value(dictionary) + def create(self, validated_data): nested_fields = [] nested_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data)) diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index c3b65b5c..300f2eed 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -55,4 +55,4 @@ class Serializer(TestCase): result = serializer.save() self.assertEquals(result.title, "job test updated") - #self.assertIs(result.skills.count(), 2) + self.assertIs(result.skills.count(), 2) -- GitLab From 70607de5748d1fb6bcf5aa50b73cc503d216642f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Mon, 11 Feb 2019 21:37:07 +0100 Subject: [PATCH 04/12] update: add failing test for nested updates --- djangoldp/serializers.py | 4 ++-- djangoldp/tests/tests_update.py | 35 +++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 740a8b37..0f6d711a 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -92,7 +92,7 @@ class JsonLdRelatedField(JsonLdField): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except: + except KeyError: return super().to_internal_value(data) @classmethod @@ -116,7 +116,7 @@ class JsonLdIdentityField(JsonLdField): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except: + except KeyError: return super().to_internal_value(data) def get_value(self, dictionary): diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index 300f2eed..df79b949 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -15,8 +15,9 @@ class Serializer(TestCase): "title": "job test updated", "skills": { "ldp:contains": [ + # {"title": "new skill"}, {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, - {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), "title": "skill2 UP"}, ]} } @@ -30,6 +31,9 @@ class Serializer(TestCase): self.assertEquals(result.title, "job test updated") self.assertIs(result.skills.count(), 2) + # self.assertEquals(result.skills[0].title, "new skill") # new skill + self.assertEquals(result.skills.all()[0].title, "skill1") # no change + self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated def test_update_graph(self): skill1 = Skill.objects.create(title="skill1") @@ -37,13 +41,25 @@ class Serializer(TestCase): job1 = JobOffer.objects.create(title="job test") job = {"@graph": [{"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), - "title": "job test updated", - "skills": { - "ldp:contains": [ - {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, - {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, - ]} - }] + "title": "job test updated", + "skills": { + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + # {"@id": "_.123"}, + ]} + }, + # { + # "@id": "_.123", + # "title": "new skill" + # }, + { + "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk), + }, + { + "@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), + "title": "skill2 UP" + }] } meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} @@ -56,3 +72,6 @@ class Serializer(TestCase): self.assertEquals(result.title, "job test updated") self.assertIs(result.skills.count(), 2) + #self.assertEquals(result.skills[0].title, "new skill") # new skill + self.assertEquals(result.skills.all()[0].title, "skill1") # no change + self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated -- GitLab From 28d878916032a4fb487b5489bfd61997945adbe2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Mon, 11 Feb 2019 23:37:09 +0100 Subject: [PATCH 05/12] fix: allow update on nested fields on POST or PUT --- djangoldp/serializers.py | 13 +++++++++++-- djangoldp/tests/tests_save.py | 5 ++++- djangoldp/tests/tests_update.py | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 0f6d711a..02b96e28 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -14,7 +14,7 @@ class LDListMixin: # data = json.loads(data) try: data = data['ldp:contains'] - except TypeError: + except (TypeError, KeyError): pass if isinstance(data, dict): data = [data] @@ -187,9 +187,12 @@ class LDPSerializer(HyperlinkedModelSerializer): def to_internal_value(self, data): model = self.Meta.model - return self.serializer_related_field( + instance = self.serializer_related_field( view_name='{}-detail'.format(model._meta.object_name.lower()), queryset=model.objects.all()).to_internal_value(data) + for key in data: + setattr(instance, key, data[key]) + return instance def get_value(self, dictionary): return super().get_value(dictionary) @@ -221,6 +224,7 @@ class LDPSerializer(HyperlinkedModelSerializer): for (field_name, data) in nested_fields: for item in data: + item.save() getattr(obj, field_name).add(item) return obj @@ -236,7 +240,12 @@ class LDPSerializer(HyperlinkedModelSerializer): instance.save() for (field_name, data) in nested_fields: + try: + getattr(instance, field_name).clear() + except AttributeError: + pass for item in data: + item.save() getattr(instance, field_name).add(item) return instance diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py index 53f48e21..4375f943 100644 --- a/djangoldp/tests/tests_save.py +++ b/djangoldp/tests/tests_save.py @@ -9,11 +9,12 @@ class Serializer(TestCase): def test_save_m2m(self): skill1 = Skill.objects.create(title="skill1") skill2 = Skill.objects.create(title="skill2") + job = {"title": "job test", "skills": { "ldp:contains": [ {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, - {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), "title": "skill2 UP"}, ]} } @@ -27,6 +28,8 @@ class Serializer(TestCase): self.assertEquals(result.title, "job test") self.assertIs(result.skills.count(), 2) + self.assertEquals(result.skills.all()[0].title, "skill1") # no change + self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated def test_save_without_nested_fields(self): skill1 = Skill.objects.create(title="skill1") diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index df79b949..1b84325c 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -7,9 +7,11 @@ from djangoldp.tests.models import Skill, JobOffer class Serializer(TestCase): def test_update(self): + skill = Skill.objects.create(title="to drop") skill1 = Skill.objects.create(title="skill1") skill2 = Skill.objects.create(title="skill2") job1 = JobOffer.objects.create(title="job test") + job1.skills.add(skill) job = {"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), "title": "job test updated", -- GitLab From fcd84309de868f5bde8ce6c1d23d1b879e308354 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 13:00:32 +0100 Subject: [PATCH 06/12] update: allow nested object creation on PUT request --- djangoldp/serializers.py | 50 +++++++++++++++++++++++---------- djangoldp/tests/tests_update.py | 13 +++++---- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 02b96e28..96f2c82e 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -1,8 +1,10 @@ +from urllib import parse + from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import get_resolver +from django.core.urlresolvers import get_resolver, resolve, get_script_prefix from django.utils.datastructures import MultiValueDictKeyError +from django.utils.encoding import uri_to_iri from guardian.shortcuts import get_perms -from rest_framework.fields import empty from rest_framework.relations import HyperlinkedRelatedField, ManyRelatedField, MANY_RELATION_KWARGS from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer from rest_framework.utils.field_mapping import get_nested_relation_kwargs @@ -164,7 +166,8 @@ class LDPSerializer(HyperlinkedModelSerializer): def get_value(self, dictionary): try: object_list = dictionary["@graph"] - part_id = '/{}'.format(get_resolver().reverse_dict[self.parent_view_name][0][0][0], self.parent.instance.pk) + part_id = '/{}'.format(get_resolver().reverse_dict[self.parent_view_name][0][0][0], + self.parent.instance.pk) obj = next(filter(lambda o: part_id in o['@id'], object_list)) return super().get_value(obj) except KeyError: @@ -172,7 +175,7 @@ class LDPSerializer(HyperlinkedModelSerializer): field_class, field_kwargs = super().build_standard_field(field_name, model_field) field_kwargs['parent_view_name'] = '{}-list'.format(model_field.model._meta.object_name.lower()) - return type(field_class.__name__ + 'Valued', (JSonLDStandardField, field_class), {}), field_kwargs + return type(field_class.__name__ + 'Valued', (JSonLDStandardField, field_class), {}), field_kwargs def build_nested_field(self, field_name, relation_info, nested_depth): class NestedLDPSerializer(self.__class__): @@ -186,13 +189,21 @@ class LDPSerializer(HyperlinkedModelSerializer): fields = '__all__' def to_internal_value(self, data): - model = self.Meta.model - instance = self.serializer_related_field( - view_name='{}-detail'.format(model._meta.object_name.lower()), - queryset=model.objects.all()).to_internal_value(data) - for key in data: - setattr(instance, key, data[key]) - return instance + value = super().to_internal_value(data) + if '@id' in data: + uri = data['@id'] + http_prefix = uri.startswith(('http:', 'https:')) + + if http_prefix: + uri = parse.urlparse(uri).path + prefix = get_script_prefix() + if uri.startswith(prefix): + uri = '/' + uri[len(prefix):] + + match = resolve(uri_to_iri(uri)) + value['pk'] = match.kwargs['pk'] + + return value def get_value(self, dictionary): return super().get_value(dictionary) @@ -215,12 +226,15 @@ class LDPSerializer(HyperlinkedModelSerializer): return super().get_value(dictionary) def create(self, validated_data): + return self.internal_create(validated_data, model=self.Meta.model) + + def internal_create(self, validated_data, model): nested_fields = [] nested_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data)) for field_name in nested_fields_name: nested_fields.append((field_name, validated_data.pop(field_name))) - obj = self.Meta.model.objects.create(**validated_data) + obj = model.objects.create(**validated_data) for (field_name, data) in nested_fields: for item in data: @@ -236,7 +250,7 @@ class LDPSerializer(HyperlinkedModelSerializer): nested_fields.append((field_name, validated_data.pop(field_name))) for attr, value in validated_data.items(): - setattr(instance, attr, value) + setattr(instance, attr, value) instance.save() for (field_name, data) in nested_fields: @@ -245,7 +259,13 @@ class LDPSerializer(HyperlinkedModelSerializer): except AttributeError: pass for item in data: - item.save() - getattr(instance, field_name).add(item) + manager = getattr(instance, field_name) + if 'pk' in item: + oldObj = manager.model.objects.get(pk=item['pk']) + savedItem = self.update(instance=oldObj, validated_data=item) + else: + savedItem = self.internal_create(validated_data=item, model=manager.model) + + getattr(instance, field_name).add(savedItem) return instance diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index 1b84325c..7e9e6815 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -17,7 +17,7 @@ class Serializer(TestCase): "title": "job test updated", "skills": { "ldp:contains": [ - # {"title": "new skill"}, + {"title": "new skill"}, {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), "title": "skill2 UP"}, ]} @@ -32,15 +32,18 @@ class Serializer(TestCase): result = serializer.save() self.assertEquals(result.title, "job test updated") - self.assertIs(result.skills.count(), 2) - # self.assertEquals(result.skills[0].title, "new skill") # new skill - self.assertEquals(result.skills.all()[0].title, "skill1") # no change - self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated + self.assertIs(result.skills.count(), 3) + skills = result.skills.all().order_by('title'); + self.assertEquals(skills[0].title, "new skill") # new skill + self.assertEquals(skills[1].title, "skill1") # no change + self.assertEquals(skills[2].title, "skill2 UP") # title updated def test_update_graph(self): + skill = Skill.objects.create(title="to drop") skill1 = Skill.objects.create(title="skill1") skill2 = Skill.objects.create(title="skill2") job1 = JobOffer.objects.create(title="job test") + job1.skills.add(skill) job = {"@graph": [{"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), "title": "job test updated", -- GitLab From 9e98986d25d139f479506d00ba255082602660ff Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 13:05:06 +0100 Subject: [PATCH 07/12] update: allow nested object creation on POST --- djangoldp/serializers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 96f2c82e..ab70d648 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -234,14 +234,11 @@ class LDPSerializer(HyperlinkedModelSerializer): for field_name in nested_fields_name: nested_fields.append((field_name, validated_data.pop(field_name))) - obj = model.objects.create(**validated_data) + instance = model.objects.create(**validated_data) - for (field_name, data) in nested_fields: - for item in data: - item.save() - getattr(obj, field_name).add(item) + self.save_or_update_nested(instance, nested_fields) - return obj + return instance def update(self, instance, validated_data): nested_fields = [] @@ -253,6 +250,11 @@ class LDPSerializer(HyperlinkedModelSerializer): setattr(instance, attr, value) instance.save() + self.save_or_update_nested(instance, nested_fields) + + return instance + + def save_or_update_nested(self, instance, nested_fields): for (field_name, data) in nested_fields: try: getattr(instance, field_name).clear() @@ -267,5 +269,3 @@ class LDPSerializer(HyperlinkedModelSerializer): savedItem = self.internal_create(validated_data=item, model=manager.model) getattr(instance, field_name).add(savedItem) - - return instance -- GitLab From 20a9b245f784e93f4b1fb4cef7fccf65bf515a94 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 14:39:36 +0100 Subject: [PATCH 08/12] update: allow update and create on PUT or POST with @graph input --- djangoldp/serializers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index ab70d648..70dc8ac7 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -38,7 +38,21 @@ class LDListMixin: view_name = '{}-list'.format(self.parent.Meta.model._meta.object_name.lower()) part_id = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][0], self.parent.instance.pk) obj = next(filter(lambda o: part_id in o['@id'], object_list)) - return super().get_value(obj) + list = super().get_value(obj); + try: + list= list['ldp:contains'] + except KeyError: + pass + + ret=[] + for item in list: + fullItem = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + if fullItem is None: + ret.append(item) + else: + ret.append(fullItem) + + return ret except KeyError: return super().get_value(dictionary) -- GitLab From 2217a972f29896cf60c1b104d996b112a64dfc4d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 15:28:41 +0100 Subject: [PATCH 09/12] fix: add new nested object with @graph representation --- djangoldp/serializers.py | 18 ++++++++++++++---- djangoldp/tests/tests_update.py | 22 ++++++++++++---------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 70dc8ac7..857d5f1a 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -1,7 +1,7 @@ from urllib import parse from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import get_resolver, resolve, get_script_prefix +from django.core.urlresolvers import get_resolver, resolve, get_script_prefix, Resolver404 from django.utils.datastructures import MultiValueDictKeyError from django.utils.encoding import uri_to_iri from guardian.shortcuts import get_perms @@ -44,9 +44,16 @@ class LDListMixin: except KeyError: pass + if isinstance(list, dict): + list = [list] + ret=[] for item in list: - fullItem = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + fullItem=None + try: + fullItem = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + except StopIteration: + pass if fullItem is None: ret.append(item) else: @@ -214,8 +221,11 @@ class LDPSerializer(HyperlinkedModelSerializer): if uri.startswith(prefix): uri = '/' + uri[len(prefix):] - match = resolve(uri_to_iri(uri)) - value['pk'] = match.kwargs['pk'] + try: + match = resolve(uri_to_iri(uri)) + value['pk'] = match.kwargs['pk'] + except Resolver404: + pass return value diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index 7e9e6815..e81a4eaf 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -33,7 +33,7 @@ class Serializer(TestCase): self.assertEquals(result.title, "job test updated") self.assertIs(result.skills.count(), 3) - skills = result.skills.all().order_by('title'); + skills = result.skills.all().order_by('title') self.assertEquals(skills[0].title, "new skill") # new skill self.assertEquals(skills[1].title, "skill1") # no change self.assertEquals(skills[2].title, "skill2 UP") # title updated @@ -51,13 +51,13 @@ class Serializer(TestCase): "ldp:contains": [ {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, - # {"@id": "_.123"}, + {"@id": "_.123"}, ]} }, - # { - # "@id": "_.123", - # "title": "new skill" - # }, + { + "@id": "_.123", + "title": "new skill" + }, { "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk), }, @@ -75,8 +75,10 @@ class Serializer(TestCase): serializer.is_valid() result = serializer.save() + skills = result.skills.all().order_by('title') + self.assertEquals(result.title, "job test updated") - self.assertIs(result.skills.count(), 2) - #self.assertEquals(result.skills[0].title, "new skill") # new skill - self.assertEquals(result.skills.all()[0].title, "skill1") # no change - self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated + self.assertIs(result.skills.count(), 3) + self.assertEquals(skills[0].title, "new skill") # new skill + self.assertEquals(skills[1].title, "skill1") # no change + self.assertEquals(skills[2].title, "skill2 UP") # title updated -- GitLab From 7602097912c738f5d4a581c7829518ab01845284 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 15:57:06 +0100 Subject: [PATCH 10/12] update: deal with required value on partial update --- djangoldp/serializers.py | 20 +++++++++++--------- djangoldp/tests/models.py | 1 + djangoldp/tests/tests_save.py | 8 ++++---- djangoldp/tests/tests_update.py | 17 +++++++++-------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 857d5f1a..82c031d8 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -49,15 +49,15 @@ class LDListMixin: ret=[] for item in list: - fullItem=None + full_item=None try: - fullItem = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + full_item = next(filter(lambda o: item['@id'] == o['@id'], object_list)) except StopIteration: pass - if fullItem is None: + if full_item is None: ret.append(item) else: - ret.append(fullItem) + ret.append(full_item) return ret except KeyError: @@ -210,9 +210,8 @@ class LDPSerializer(HyperlinkedModelSerializer): fields = '__all__' def to_internal_value(self, data): - value = super().to_internal_value(data) - if '@id' in data: - uri = data['@id'] + if self.url_field_name in data: + uri = data[self.url_field_name] http_prefix = uri.startswith(('http:', 'https:')) if http_prefix: @@ -223,11 +222,14 @@ class LDPSerializer(HyperlinkedModelSerializer): try: match = resolve(uri_to_iri(uri)) - value['pk'] = match.kwargs['pk'] + instance = self.Meta.model.objects.get(pk=match.kwargs['pk']) + for key in self.data: + if not key in data: + data[key] = getattr(instance, key) except Resolver404: pass - return value + return super().to_internal_value(data) def get_value(self, dictionary): return super().get_value(dictionary) diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py index d8e0e76b..968ffbbb 100644 --- a/djangoldp/tests/models.py +++ b/djangoldp/tests/models.py @@ -4,6 +4,7 @@ from django.db import models class Skill(models.Model): title = models.CharField(max_length=255, blank=True, null=True) + obligatoire = models.CharField(max_length=255) class JobOffer(models.Model): diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py index 4375f943..1117c928 100644 --- a/djangoldp/tests/tests_save.py +++ b/djangoldp/tests/tests_save.py @@ -7,8 +7,8 @@ from djangoldp.tests.models import Skill, JobOffer class Serializer(TestCase): def test_save_m2m(self): - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") job = {"title": "job test", "skills": { @@ -32,8 +32,8 @@ class Serializer(TestCase): self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated def test_save_without_nested_fields(self): - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") job = {"title": "job test"} meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index e81a4eaf..a4033acb 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -7,9 +7,9 @@ from djangoldp.tests.models import Skill, JobOffer class Serializer(TestCase): def test_update(self): - skill = Skill.objects.create(title="to drop") - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") + skill = Skill.objects.create(title="to drop", obligatoire="obligatoire") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") job1 = JobOffer.objects.create(title="job test") job1.skills.add(skill) @@ -17,7 +17,7 @@ class Serializer(TestCase): "title": "job test updated", "skills": { "ldp:contains": [ - {"title": "new skill"}, + {"title": "new skill", "obligatoire": "okay"}, {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), "title": "skill2 UP"}, ]} @@ -39,9 +39,9 @@ class Serializer(TestCase): self.assertEquals(skills[2].title, "skill2 UP") # title updated def test_update_graph(self): - skill = Skill.objects.create(title="to drop") - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") + skill = Skill.objects.create(title="to drop", obligatoire="obligatoire") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") job1 = JobOffer.objects.create(title="job test") job1.skills.add(skill) @@ -56,7 +56,8 @@ class Serializer(TestCase): }, { "@id": "_.123", - "title": "new skill" + "title": "new skill", + "obligatoire": "okay" }, { "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk), -- GitLab From 2b02e3e96875e455afb8cda1574a1d1763d45cb0 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 18:21:39 +0100 Subject: [PATCH 11/12] fix: manage simple nested field (non list) --- djangoldp/serializers.py | 92 +++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 82c031d8..4f8ead62 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -1,3 +1,4 @@ +from collections import OrderedDict, Mapping from urllib import parse from django.core.exceptions import ImproperlyConfigured @@ -5,8 +6,13 @@ from django.core.urlresolvers import get_resolver, resolve, get_script_prefix, R from django.utils.datastructures import MultiValueDictKeyError from django.utils.encoding import uri_to_iri from guardian.shortcuts import get_perms +from django.core.exceptions import ValidationError as DjangoValidationError +from rest_framework.exceptions import ValidationError +from rest_framework.fields import SkipField +from rest_framework.fields import get_error_detail, set_value from rest_framework.relations import HyperlinkedRelatedField, ManyRelatedField, MANY_RELATION_KWARGS from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer +from rest_framework.settings import api_settings from rest_framework.utils.field_mapping import get_nested_relation_kwargs from rest_framework.utils.serializer_helpers import ReturnDict @@ -40,16 +46,16 @@ class LDListMixin: obj = next(filter(lambda o: part_id in o['@id'], object_list)) list = super().get_value(obj); try: - list= list['ldp:contains'] + list = list['ldp:contains'] except KeyError: pass if isinstance(list, dict): list = [list] - ret=[] + ret = [] for item in list: - full_item=None + full_item = None try: full_item = next(filter(lambda o: item['@id'] == o['@id'], object_list)) except StopIteration: @@ -77,7 +83,7 @@ class ContainerSerializer(LDListMixin, ListSerializer): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except: + except (KeyError, TypeError): return super().to_internal_value(data) @@ -115,7 +121,7 @@ class JsonLdRelatedField(JsonLdField): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except KeyError: + except (KeyError, TypeError): return super().to_internal_value(data) @classmethod @@ -211,6 +217,38 @@ class LDPSerializer(HyperlinkedModelSerializer): def to_internal_value(self, data): if self.url_field_name in data: + if not isinstance(data, Mapping): + message = self.error_messages['invalid'].format( + datatype=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='invalid') + + ret = OrderedDict() + errors = OrderedDict() + fields = list(filter(lambda x: x.field_name in data, self._writable_fields)) + + for field in fields: + validate_method = getattr(self, 'validate_' + field.field_name, None) + primitive_value = field.get_value(data) + try: + validated_value = field.run_validation(primitive_value) + if validate_method is not None: + validated_value = validate_method(validated_value) + except ValidationError as exc: + errors[field.field_name] = exc.detail + except DjangoValidationError as exc: + errors[field.field_name] = get_error_detail(exc) + except SkipField: + pass + else: + set_value(ret, field.source_attrs, validated_value) + + if errors: + raise ValidationError(errors) + + uri = data[self.url_field_name] http_prefix = uri.startswith(('http:', 'https:')) @@ -222,17 +260,14 @@ class LDPSerializer(HyperlinkedModelSerializer): try: match = resolve(uri_to_iri(uri)) - instance = self.Meta.model.objects.get(pk=match.kwargs['pk']) - for key in self.data: - if not key in data: - data[key] = getattr(instance, key) + ret['pk'] = match.kwargs['pk'] except Resolver404: pass - return super().to_internal_value(data) + return ret + else: + return super().to_internal_value(data) - def get_value(self, dictionary): - return super().get_value(dictionary) kwargs = get_nested_relation_kwargs(relation_info) kwargs['read_only'] = False @@ -249,7 +284,25 @@ class LDPSerializer(HyperlinkedModelSerializer): return ContainerSerializer(*args, **kwargs) def get_value(self, dictionary): - return super().get_value(dictionary) + try: + object_list = dictionary["@graph"] + view_name = '{}-list'.format(self.parent.Meta.model._meta.object_name.lower()) + part_id = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][0], + self.parent.instance.pk) + obj = next(filter(lambda o: part_id in o[self.url_field_name], object_list)) + item = super().get_value(obj) + full_item = None + try: + full_item = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + except StopIteration: + pass + if full_item is None: + return item + else: + return full_item + + except KeyError: + return super().get_value(dictionary) def create(self, validated_data): return self.internal_create(validated_data, model=self.Meta.model) @@ -262,7 +315,7 @@ class LDPSerializer(HyperlinkedModelSerializer): instance = model.objects.create(**validated_data) - self.save_or_update_nested(instance, nested_fields) + self.save_or_update_nested_list(instance, nested_fields) return instance @@ -273,14 +326,21 @@ class LDPSerializer(HyperlinkedModelSerializer): nested_fields.append((field_name, validated_data.pop(field_name))) for attr, value in validated_data.items(): + if isinstance(value, dict): + manager = getattr(instance, attr) + if 'pk' in value: + oldObj = manager._meta.model.objects.get(pk=value['pk']) + value = self.update(instance=oldObj, validated_data=value) + else: + value = self.internal_create(validated_data=value, model=manager._meta.model) setattr(instance, attr, value) instance.save() - self.save_or_update_nested(instance, nested_fields) + self.save_or_update_nested_list(instance, nested_fields) return instance - def save_or_update_nested(self, instance, nested_fields): + def save_or_update_nested_list(self, instance, nested_fields): for (field_name, data) in nested_fields: try: getattr(instance, field_name).clear() -- GitLab From f37f5e0ce3cbec27052620b351f93c438b8a4b15 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Tue, 12 Feb 2019 20:31:54 +0100 Subject: [PATCH 12/12] syntax: reformat --- djangoldp/serializers.py | 4 +--- djangoldp/views.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 4f8ead62..80510749 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -2,11 +2,11 @@ from collections import OrderedDict, Mapping from urllib import parse from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ValidationError as DjangoValidationError from django.core.urlresolvers import get_resolver, resolve, get_script_prefix, Resolver404 from django.utils.datastructures import MultiValueDictKeyError from django.utils.encoding import uri_to_iri from guardian.shortcuts import get_perms -from django.core.exceptions import ValidationError as DjangoValidationError from rest_framework.exceptions import ValidationError from rest_framework.fields import SkipField from rest_framework.fields import get_error_detail, set_value @@ -248,7 +248,6 @@ class LDPSerializer(HyperlinkedModelSerializer): if errors: raise ValidationError(errors) - uri = data[self.url_field_name] http_prefix = uri.startswith(('http:', 'https:')) @@ -268,7 +267,6 @@ class LDPSerializer(HyperlinkedModelSerializer): else: return super().to_internal_value(data) - kwargs = get_nested_relation_kwargs(relation_info) kwargs['read_only'] = False kwargs['required'] = False diff --git a/djangoldp/views.py b/djangoldp/views.py index 8ddcbb3e..0aac9e24 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -109,8 +109,8 @@ class LDPViewSet(LDPViewSetGenerator): lookup_field = get_resolver().reverse_dict[model_name + '-detail'][0][0][1][0] meta_args = {'model': self.model, 'extra_kwargs': { '@id': {'lookup_field': lookup_field}}, - 'depth': self.depth, - 'extra_fields': self.nested_fields} + 'depth': self.depth, + 'extra_fields': self.nested_fields} if self.fields: meta_args['fields'] = self.fields else: -- GitLab