From b58aa3cbe922451230187380ce2aaa239041ebb6 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste <bleme@pm.me>
Date: Wed, 13 Mar 2019 12:29:44 +0100
Subject: [PATCH 1/3] syntax: issue #101 failed unit test

---
 djangoldp/__init__.py              |  2 +-
 djangoldp/tests/models.py          | 14 ++++++++++++--
 djangoldp/tests/tests_ldp_model.py |  6 +++---
 djangoldp/tests/tests_save.py      |  3 ++-
 djangoldp/tests/urls.py            |  4 +---
 djangoldp/urls.py                  |  1 +
 6 files changed, 20 insertions(+), 10 deletions(-)

diff --git a/djangoldp/__init__.py b/djangoldp/__init__.py
index 7461cf54..8c3166a1 100644
--- a/djangoldp/__init__.py
+++ b/djangoldp/__init__.py
@@ -1,4 +1,4 @@
 from django.db.models import options
 
 __version__ = '0.0.0'
-options.DEFAULT_NAMES += ('rdf_type', 'rdf_context', 'auto_author', 'view_set', 'container_path', 'permission_classes', 'serializer_fields', 'nested_fields')
\ No newline at end of file
+options.DEFAULT_NAMES += ('lookup_field', 'rdf_type', 'rdf_context', 'auto_author', 'view_set', 'container_path', 'permission_classes', 'serializer_fields', 'nested_fields')
\ No newline at end of file
diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py
index 981e5158..e29ee17c 100644
--- a/djangoldp/tests/models.py
+++ b/djangoldp/tests/models.py
@@ -4,15 +4,24 @@ from django.db import models
 from djangoldp.models import Model
 
 
-class Skill(models.Model):
+class Skill(Model):
     title = models.CharField(max_length=255, blank=True, null=True)
     obligatoire = models.CharField(max_length=255)
 
+    class Meta:
+        serializer_fields=["@id", "title"]
+        lookup_field = 'title'
+
 
-class JobOffer(models.Model):
+class JobOffer(Model):
     title = models.CharField(max_length=255, blank=True, null=True)
     skills = models.ManyToManyField(Skill, blank=True)
 
+    class Meta:
+        nested_fields=["skills"]
+        container_path="job-offers/"
+        lookup_field = 'title'
+
 
 class Thread(models.Model):
     description = models.CharField(max_length=255, blank=True, null=True)
@@ -27,6 +36,7 @@ class Message(models.Model):
 
 class Dummy(models.Model):
     some = models.CharField(max_length=255, blank=True, null=True)
+    slug = models.SlugField(blank=True, null=True, unique=True)
 
 
 class LDPDummy(Model):
diff --git a/djangoldp/tests/tests_ldp_model.py b/djangoldp/tests/tests_ldp_model.py
index 329899c0..3e1da6a7 100644
--- a/djangoldp/tests/tests_ldp_model.py
+++ b/djangoldp/tests/tests_ldp_model.py
@@ -11,7 +11,7 @@ class LDPModelTest(TestCase):
     def test_class_not_inheriting_ldp_model(self):
         dummy = Dummy.objects.create(some="text")
         self.assertEquals("/dummys/", Model.container_id(dummy))
-        self.assertEquals("/dummys/{}".format(dummy.pk), Model.resource_id(dummy))
+        self.assertEquals("/dummys/{}".format(dummy.slug), Model.resource_id(dummy))
 
     def test_class_inheriting_ldp_model(self):
         dummy = LDPDummy.objects.create(some="text")
@@ -21,8 +21,8 @@ class LDPModelTest(TestCase):
         self.assertEquals("/ldpdummys/{}".format(dummy.pk), Model.resource_id(dummy))
 
     def test_from_resolve_id(self):
-        saved_instance = Dummy.objects.create(some="text")
-        result = Model.resolve_id("/dummys/{}".format(saved_instance.pk))
+        saved_instance = Dummy.objects.create(some="text", slug="someid")
+        result = Model.resolve_id("/dummys/{}".format(saved_instance.slug))
         self.assertEquals(saved_instance, result)
 
     def test_resolve_container(self):
diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py
index 342448e4..161e45f5 100644
--- a/djangoldp/tests/tests_save.py
+++ b/djangoldp/tests/tests_save.py
@@ -1,7 +1,7 @@
 from django.test import TestCase
 
 from djangoldp.serializers import LDPSerializer
-from djangoldp.tests.models import Skill, JobOffer, Invoice
+from djangoldp.tests.models import Skill, JobOffer, Invoice, Dummy
 
 
 class Save(TestCase):
@@ -144,3 +144,4 @@ class Save(TestCase):
         self.assertIs(result.joboffer_set.count(), 1)
         self.assertEquals(result.joboffer_set.get(), job)
         self.assertIs(result.joboffer_set.get().skills.count(), 1)
+
diff --git a/djangoldp/tests/urls.py b/djangoldp/tests/urls.py
index 5062860e..7ac1c38f 100644
--- a/djangoldp/tests/urls.py
+++ b/djangoldp/tests/urls.py
@@ -5,12 +5,10 @@ from djangoldp.tests.models import Skill, JobOffer, Message, Thread, Dummy
 from djangoldp.views import LDPViewSet
 
 urlpatterns = [
-    url(r'^skills/', LDPViewSet.urls(model=Skill, permission_classes=[], fields=["@id", "title"], nested_fields=[])),
-    url(r'^job-offers/', LDPViewSet.urls(model=JobOffer, nested_fields=["skills"], permission_classes=())),
     url(r'^messages/', LDPViewSet.urls(model=Message, permission_classes=[], fields=["@id", "text"], nested_fields=[])),
     url(r'^threads/', LDPViewSet.urls(model=Thread, nested_fields=["message_set"], permission_classes=())),
     url(r'^users/', LDPViewSet.urls(model=settings.AUTH_USER_MODEL, permission_classes=[])),
-    url(r'^dummys/', LDPViewSet.urls(model=Dummy, permission_classes=[])),
+    url(r'^dummys/', LDPViewSet.urls(model=Dummy, permission_classes=[], lookup_field='slug',)),
     url(r'^', include('djangoldp.urls')),
 ]
 
diff --git a/djangoldp/urls.py b/djangoldp/urls.py
index 59efe9c9..1dcce95e 100644
--- a/djangoldp/urls.py
+++ b/djangoldp/urls.py
@@ -30,6 +30,7 @@ for class_name in model_classes:
     urls_fct = model_class.get_view_set().urls
     urlpatterns.append(url(r'^' + path, include(
         urls_fct(model=model_class,
+                 lookup_field=getattr(model_class._meta, 'lookup_field', getattr(model_class.Meta, 'lookup_field', [])),
                  permission_classes=getattr(model_class._meta, 'permission_classes', getattr(model_class.Meta, 'permission_classes', [])),
                  fields=getattr(model_class._meta, 'serializer_fields', getattr(model_class.Meta, 'serializer_fields', [])),
                  nested_fields=getattr(model_class._meta, 'nested_fields', getattr(model_class.Meta, 'nested_fields', []))))))
-- 
GitLab


From 77e5f8fe32d3690b59fb570b41de6d0f3105fc47 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste <bleme@pm.me>
Date: Wed, 13 Mar 2019 13:11:59 +0100
Subject: [PATCH 2/3] update: manage slug on save and update

---
 djangoldp/models.py             |  6 +++-
 djangoldp/serializers.py        | 19 +++++++-----
 djangoldp/tests/models.py       | 12 ++++++--
 djangoldp/tests/tests_save.py   | 17 +++++------
 djangoldp/tests/tests_update.py | 52 ++++++++++++++++-----------------
 5 files changed, 61 insertions(+), 45 deletions(-)

diff --git a/djangoldp/models.py b/djangoldp/models.py
index fff174f8..e8a79806 100644
--- a/djangoldp/models.py
+++ b/djangoldp/models.py
@@ -29,11 +29,15 @@ class Model(models.Model):
 
     @classmethod
     def resource_id(cls, instance):
+        return "{}{}".format(cls.container_id(instance), getattr(instance, cls.slug_field(instance)))
+
+    @classmethod
+    def slug_field(cls, instance):
         view_name = '{}-detail'.format(instance._meta.object_name.lower())
         slug_field = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][1][0])
         if slug_field.startswith('/'):
             slug_field = slug_field[1:]
-        return "{}{}".format(cls.container_id(instance), getattr(instance, slug_field))
+        return slug_field
 
     @classmethod
     def container_id(cls, instance):
diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py
index e1ae36cb..f6a8c821 100644
--- a/djangoldp/serializers.py
+++ b/djangoldp/serializers.py
@@ -307,7 +307,8 @@ class LDPSerializer(HyperlinkedModelSerializer):
 
                     try:
                         match = resolve(uri_to_iri(uri))
-                        ret['pk'] = match.kwargs['pk']
+                        slug_field = Model.slug_field(self.__class__.Meta.model)
+                        ret[slug_field] = match.kwargs[slug_field]
                     except Resolver404:
                         pass
 
@@ -389,9 +390,11 @@ class LDPSerializer(HyperlinkedModelSerializer):
 
         for attr, value in validated_data.items():
             if isinstance(value, dict):
+                slug_field = Model.slug_field(instance)
                 manager = getattr(instance, attr)
-                if 'pk' in value:
-                    oldObj = manager._meta.model.objects.get(pk=value['pk'])
+                if slug_field in value:
+                    kwargs = {slug_field: value[slug_field]}
+                    oldObj = manager._meta.model.objects.get(**kwargs)
                     value = self.update(instance=oldObj, validated_data=value)
                 else:
                     value = self.internal_create(validated_data=value, model=manager._meta.model)
@@ -406,18 +409,20 @@ class LDPSerializer(HyperlinkedModelSerializer):
     def save_or_update_nested_list(self, instance, nested_fields):
         for (field_name, data) in nested_fields:
             manager = getattr(instance, field_name)
+            slug_field = Model.slug_field(instance)
 
-            item_pk_to_keep = list(map(lambda e: int(e['pk']), filter(lambda x: 'pk' in x, data)))
+            item_pk_to_keep = list(map(lambda e: e[slug_field], filter(lambda x: slug_field in x, data)))
             for item in list(manager.all()):
-                if not item.pk in item_pk_to_keep:
+                if not str(getattr(item, slug_field)) in item_pk_to_keep:
                     if getattr(manager, 'through', None) is None:
                         item.delete()
                     else:
                         manager.remove(item)
 
             for item in data:
-                if 'pk' in item:
-                    oldObj = manager.model.objects.get(pk=item['pk'])
+                if slug_field in item:
+                    kwargs = {slug_field: item[slug_field]}
+                    oldObj = manager.model.objects.get(**kwargs)
                     savedItem = self.update(instance=oldObj, validated_data=item)
                 else:
                     rel = getattr(instance._meta.model, field_name).rel
diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py
index e29ee17c..43e56d66 100644
--- a/djangoldp/tests/models.py
+++ b/djangoldp/tests/models.py
@@ -1,5 +1,7 @@
 from django.conf import settings
 from django.db import models
+from django.db.models.signals import pre_save
+from django.dispatch import receiver
 
 from djangoldp.models import Model
 
@@ -7,20 +9,23 @@ from djangoldp.models import Model
 class Skill(Model):
     title = models.CharField(max_length=255, blank=True, null=True)
     obligatoire = models.CharField(max_length=255)
+    slug = models.SlugField(blank=True, null=True, unique=True)
+
 
     class Meta:
         serializer_fields=["@id", "title"]
-        lookup_field = 'title'
+        lookup_field = 'slug'
 
 
 class JobOffer(Model):
     title = models.CharField(max_length=255, blank=True, null=True)
     skills = models.ManyToManyField(Skill, blank=True)
+    slug = models.SlugField(blank=True, null=True, unique=True)
 
     class Meta:
         nested_fields=["skills"]
         container_path="job-offers/"
-        lookup_field = 'title'
+        lookup_field = 'slug'
 
 
 class Thread(models.Model):
@@ -61,3 +66,6 @@ class Task(models.Model):
 
     class Meta:
         serializer_fields = ['@id', 'title', 'batch']
+
+
+
diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py
index 161e45f5..0d7b70b9 100644
--- a/djangoldp/tests/tests_save.py
+++ b/djangoldp/tests/tests_save.py
@@ -1,7 +1,7 @@
 from django.test import TestCase
 
 from djangoldp.serializers import LDPSerializer
-from djangoldp.tests.models import Skill, JobOffer, Invoice, Dummy
+from djangoldp.tests.models import Skill, JobOffer, Invoice
 
 
 class Save(TestCase):
@@ -41,19 +41,19 @@ class Save(TestCase):
         self.assertEquals(result.batches.all()[0].tasks.all()[0].title, "Tache 1")
 
     def test_save_m2m(self):
-        skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire")
-        skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire")
+        skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire", slug="slug1")
+        skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire", slug="slug2")
 
         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), "title": "skill2 UP"},
-                       {"title": "skill3 NEW", "obligatoire": "obligatoire"},
+                       {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.slug)},
+                       {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.slug), "title": "skill2 UP"},
+                       {"title": "skill3", "obligatoire": "obligatoire", "slug": "slug3"},
                    ]}
                }
 
-        meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")}
+        meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills", "slug")}
 
         meta_class = type('Meta', (), meta_args)
         serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class})
@@ -65,7 +65,7 @@ class Save(TestCase):
         self.assertIs(result.skills.count(), 3)
         self.assertEquals(result.skills.all()[0].title, "skill1")  # no change
         self.assertEquals(result.skills.all()[1].title, "skill2 UP")  # title updated
-        self.assertEquals(result.skills.all()[2].title, "skill3 NEW")  # creation on the fly
+        self.assertEquals(result.skills.all()[2].title, "skill3")  # creation on the fly
 
     def test_save_m2m_graph_simple(self):
         job = {"@graph": [
@@ -144,4 +144,3 @@ class Save(TestCase):
         self.assertIs(result.joboffer_set.count(), 1)
         self.assertEquals(result.joboffer_set.get(), job)
         self.assertIs(result.joboffer_set.get().skills.count(), 1)
-
diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py
index 39083e20..19c96eff 100644
--- a/djangoldp/tests/tests_update.py
+++ b/djangoldp/tests/tests_update.py
@@ -8,19 +8,19 @@ from djangoldp.tests.models import Skill, JobOffer, Thread, Message
 class Update(TestCase):
 
     def test_update(self):
-        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")
+        skill = Skill.objects.create(title="to drop", obligatoire="obligatoire", slug="slug1")
+        skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire", slug="slug2")
+        skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire", slug="slug3")
         job1 = JobOffer.objects.create(title="job test")
         job1.skills.add(skill)
 
-        job = {"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk),
+        job = {"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.slug),
                "title": "job test updated",
                "skills": {
                    "ldp:contains": [
                        {"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"},
+                       {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.slug)},
+                       {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.slug), "title": "skill2 UP"},
                    ]}
                }
 
@@ -40,21 +40,21 @@ class Update(TestCase):
         self.assertEquals(skills[2].title, "skill2 UP")  # title updated
 
     def test_update_graph(self):
-        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")
+        skill = Skill.objects.create(title="to drop", obligatoire="obligatoire", slug="slug1")
+        skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire", slug="slug2")
+        skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire", slug="slug3")
+        job1 = JobOffer.objects.create(title="job test", slug="slug4")
         job1.skills.add(skill)
 
         job = {"@graph":
             [
                 {
-                    "@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk),
+                    "@id": "https://happy-dev.fr/job-offers/{}/".format(job1.slug),
                     "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": "https://happy-dev.fr/skills/{}/".format(skill1.slug)},
+                            {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.slug)},
                             {"@id": "_.123"},
                         ]}
                 },
@@ -64,10 +64,10 @@ class Update(TestCase):
                     "obligatoire": "okay"
                 },
                 {
-                    "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk),
+                    "@id": "https://happy-dev.fr/skills/{}/".format(skill1.slug),
                 },
                 {
-                    "@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk),
+                    "@id": "https://happy-dev.fr/skills/{}/".format(skill2.slug),
                     "title": "skill2 UP"
                 }
             ]
@@ -90,19 +90,19 @@ class Update(TestCase):
         self.assertEquals(skills[2].title, "skill2 UP")  # title updated
 
     def test_update_graph_2(self):
-        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")
+        skill = Skill.objects.create(title="to drop", obligatoire="obligatoire", slug="slug")
+        skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire", slug="slug1")
+        skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire", slug="slug2")
+        job1 = JobOffer.objects.create(title="job test", slug="slug1")
         job1.skills.add(skill)
 
         job = {"@graph":
             [
                 {
-                    "@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk),
+                    "@id": "https://happy-dev.fr/job-offers/{}/".format(job1.slug),
                     "title": "job test updated",
                     "skills": {
-                        "@id": "https://happy-dev.fr/job-offers/{}/skills/".format(job1.pk)
+                        "@id": "https://happy-dev.fr/job-offers/{}/skills/".format(job1.slug)
                     }
                 },
                 {
@@ -111,17 +111,17 @@ class Update(TestCase):
                     "obligatoire": "okay"
                 },
                 {
-                    "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk),
+                    "@id": "https://happy-dev.fr/skills/{}/".format(skill1.slug),
                 },
                 {
-                    "@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk),
+                    "@id": "https://happy-dev.fr/skills/{}/".format(skill2.slug),
                     "title": "skill2 UP"
                 },
                 {
-                    '@id': "https://happy-dev.fr/job-offers/{}/skills/".format(job1.pk),
+                    '@id': "https://happy-dev.fr/job-offers/{}/skills/".format(job1.slug),
                     "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(skill1.slug)},
+                        {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.slug)},
                         {"@id": "_.123"},
                     ]
                 }
-- 
GitLab


From 2706cc44bbd2d2d7e7df1c5a98d0b4734bf60246 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste <bleme@pm.me>
Date: Wed, 13 Mar 2019 16:54:25 +0100
Subject: [PATCH 3/3] bugfix: fieldname can be None

---
 djangoldp/serializers.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py
index f6a8c821..539ec668 100644
--- a/djangoldp/serializers.py
+++ b/djangoldp/serializers.py
@@ -364,7 +364,7 @@ class LDPSerializer(HyperlinkedModelSerializer):
         info = model_meta.get_field_info(ModelClass)
         many_to_many = {}
         for field_name, relation_info in info.relations.items():
-            if relation_info.to_many and relation_info.reverse and not (field_name in validated_data):
+            if relation_info.to_many and relation_info.reverse and not (field_name in validated_data) and not field_name is None:
                 rel = getattr(instance._meta.model, field_name).rel
                 if rel.name in validated_data:
                     related = validated_data[rel.name]
-- 
GitLab