From 3df810c3f1d8aa8d4a6e1b7f3253c141e12a362b Mon Sep 17 00:00:00 2001
From: Jean-Baptiste <bleme@pm.me>
Date: Wed, 18 Sep 2019 09:45:02 +0200
Subject: [PATCH] update: fix lot of federated issues

---
 djangoldp/models.py             |   2 +
 djangoldp/serializers.py        |  93 +++++++++++++----
 djangoldp/tests/tests_temp.py   |   2 +
 djangoldp/tests/tests_update.py | 170 ++++++++++++++++++++++++--------
 4 files changed, 204 insertions(+), 63 deletions(-)

diff --git a/djangoldp/models.py b/djangoldp/models.py
index 0d89db5b..3340dc4f 100644
--- a/djangoldp/models.py
+++ b/djangoldp/models.py
@@ -106,6 +106,8 @@ class Model(models.Model):
 
     @classonlymethod
     def resolve(cls, path):
+        if settings.BASE_URL in path:
+            path = path[len(settings.BASE_URL):]
         container = cls.resolve_container(path)
         try:
             resolve_id = cls.resolve_id(path)
diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py
index 8fbd6f2d..8294c042 100644
--- a/djangoldp/serializers.py
+++ b/djangoldp/serializers.py
@@ -4,6 +4,7 @@ from urllib import parse
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AbstractUser
 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
@@ -33,6 +34,8 @@ class LDListMixin:
             data = data['ldp:contains']
         except (TypeError, KeyError):
             pass
+        if len(data) == 0:
+            return []
         if isinstance(data, dict):
             data = [data]
         if isinstance(data, str) and data.startswith("http"):
@@ -186,7 +189,7 @@ class JsonLdRelatedField(JsonLdField):
     def to_representation(self, value):
         try:
             if Model.is_external(value):
-                return {'@id': value.urlid }
+                return {'@id': value.urlid}
             else:
                 return {'@id': super().to_representation(value)}
         except ImproperlyConfigured:
@@ -465,7 +468,8 @@ class LDPSerializer(HyperlinkedModelSerializer):
         return serializer
 
     def to_internal_value(self, data):
-        user_case = self.Meta.model is get_user_model() and '@id' in data and not data['@id'].startswith(settings.BASE_URL)
+        user_case = self.Meta.model is get_user_model() and '@id' in data and not data['@id'].startswith(
+            settings.BASE_URL)
         if user_case:
             data['username'] = 'external'
         ret = super().to_internal_value(data)
@@ -521,18 +525,7 @@ class LDPSerializer(HyperlinkedModelSerializer):
                     getattr(instance, field_name).add(related)
 
     def internal_create(self, validated_data, model):
-
-        nested_fk_fields_name = list(filter(lambda key: isinstance(validated_data[key], dict), validated_data))
-        for field_name in nested_fk_fields_name:
-            field_dict = validated_data[field_name]
-            field_model = getattr(model, field_name).field.rel.model
-            slug_field = Model.slug_field(field_model)
-            if slug_field in field_dict:
-                kwargs = {slug_field: field_dict[slug_field]}
-                sub_inst = field_model.objects.get(**kwargs)
-            else:
-                sub_inst = self.internal_create(field_dict, field_model)
-            validated_data[field_name] = sub_inst
+        validated_data = self.resolve_fk_instances(model, validated_data)
 
         nested_fields = []
         nested_list_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data))
@@ -564,6 +557,9 @@ class LDPSerializer(HyperlinkedModelSerializer):
         return validated_data
 
     def update(self, instance, validated_data):
+        model = self.Meta.model
+        validated_data = self.resolve_fk_instances(model, 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:
@@ -577,12 +573,38 @@ class LDPSerializer(HyperlinkedModelSerializer):
             else:
                 setattr(instance, attr, value)
 
-
         self.save_or_update_nested_list(instance, nested_fields)
         instance.save()
 
         return instance
 
+    def resolve_fk_instances(self, model, validated_data):
+        nested_fk_fields_name = list(filter(lambda key: isinstance(validated_data[key], dict), validated_data))
+        for field_name in nested_fk_fields_name:
+            field_dict = validated_data[field_name]
+            try:
+                field_model = getattr(model, field_name).field.rel.model
+            except:
+                # not fk
+                continue
+            slug_field = Model.slug_field(field_model)
+            sub_inst = None
+            if slug_field in field_dict:
+                kwargs = {slug_field: field_dict[slug_field]}
+                sub_inst = field_model.objects.get(**kwargs)
+            elif 'urlid' in field_dict and settings.BASE_URL in field_dict['urlid']:
+                model, sub_inst = Model.resolve(field_dict['urlid'])
+            elif 'urlid' in field_dict and issubclass(field_model, AbstractUser):
+                kwargs = {'username': field_dict['urlid']}
+                sub_inst = field_model.objects.get(**kwargs)
+            elif 'urlid' in field_dict:
+                kwargs = {'urlid': field_dict['urlid']}
+                sub_inst = field_model.objects.get(**kwargs)
+            if sub_inst is None:
+                sub_inst = self.internal_create(field_dict, field_model)
+            validated_data[field_name] = sub_inst
+        return validated_data
+
     def update_dict_value(self, attr, instance, value):
         info = model_meta.get_field_info(instance)
         slug_field = Model.slug_field(instance)
@@ -597,8 +619,20 @@ class LDPSerializer(HyperlinkedModelSerializer):
         if relation_info.to_many:
             value = self.internal_create(validated_data=value, model=relation_info.related_model)
         else:
-            value[instance._meta.fields_map[attr].remote_field.name] = instance
-            oldObj = getattr(instance, attr, None)
+            try:
+                reverse_attr_name = instance._meta.fields_map[attr].remote_field.name
+                many = False
+            except:
+                rel = list(filter(lambda field: field.name == attr, instance._meta.fields))[0].rel
+                many = rel.one_to_many
+                reverse_attr_name = rel.related_name
+            if many:
+                value[reverse_attr_name] = [instance]
+                oldObj = rel.model.object.get(id=value['urlid'])
+            else:
+                value[reverse_attr_name] = instance
+                oldObj = getattr(instance, attr, None)
+
             if oldObj is None:
                 value = self.internal_create(validated_data=value, model=relation_info.related_model)
             else:
@@ -619,6 +653,7 @@ 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)
+            field_model = manager.model
             slug_field = Model.slug_field(manager.model)
             try:
                 item_pk_to_keep = list(map(lambda e: e[slug_field], filter(lambda x: slug_field in x, data)))
@@ -639,11 +674,19 @@ class LDPSerializer(HyperlinkedModelSerializer):
                     saved_item = item
                 elif slug_field in item:
                     kwargs = {slug_field: item[slug_field]}
-                    try:
-                        old_obj = manager.model.objects.get(**kwargs)
+                    saved_item = self.get_or_create(field_model, item, kwargs)
+                elif 'urlid' in item and settings.BASE_URL in item['urlid']:
+                    model, old_obj = Model.resolve(item['urlid'])
+                    if old_obj is not None:
                         saved_item = self.update(instance=old_obj, validated_data=item)
-                    except manager.model.DoesNotExist:
-                        saved_item = self.internal_create(validated_data=item, model=manager.model)
+                    else:
+                        saved_item = self.internal_create(validated_data=item, model=field_model)
+                elif 'urlid' in item and issubclass(field_model, AbstractUser):
+                    kwargs = {'username': item['urlid']}
+                    saved_item = self.get_or_create(field_model, item, kwargs)
+                elif 'urlid' in item:
+                    kwargs = {'urlid': item['urlid']}
+                    saved_item = self.get_or_create(field_model, item, kwargs)
                 else:
                     rel = getattr(instance._meta.model, field_name).rel
                     try:
@@ -657,3 +700,11 @@ class LDPSerializer(HyperlinkedModelSerializer):
                 if getattr(manager, 'through', None) is not None and manager.through._meta.auto_created:
                     manager.remove(saved_item)
                     manager.add(saved_item)
+
+    def get_or_create(self, field_model, item, kwargs):
+        try:
+            old_obj = field_model.objects.get(**kwargs)
+            saved_item = self.update(instance=old_obj, validated_data=item)
+        except field_model.DoesNotExist:
+            saved_item = self.internal_create(validated_data=item, model=field_model)
+        return saved_item
diff --git a/djangoldp/tests/tests_temp.py b/djangoldp/tests/tests_temp.py
index db29aaf1..ccde5edd 100644
--- a/djangoldp/tests/tests_temp.py
+++ b/djangoldp/tests/tests_temp.py
@@ -1,3 +1,5 @@
+import json
+
 from django.contrib.auth.models import User
 from django.test import TestCase
 from rest_framework.test import APIRequestFactory, APIClient
diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py
index 9ba9a525..39b7b233 100644
--- a/djangoldp/tests/tests_update.py
+++ b/djangoldp/tests/tests_update.py
@@ -272,31 +272,7 @@ class Update(TestCase):
         self.assertEquals(response.data['content'], "post content")
         self.assertIn('location', response._headers)
 
-    def test_create_sub_object_in_existing_object_with_reverse_1to1_relation(self):
-        """
-        Doesn't work with depth = 0 on UserProfile Model. Should it be ?
-        """
-        user = User.objects.create(username="alex", password="test")
-        body = [
-            {
-                '@id': "_:b975",
-                'http://happy-dev.fr/owl/#description': "user description",
-                'http://happy-dev.fr/owl/#dummy': {
-                    '@id': './'
-                }
-            },
-            {
-                '@id': '/users/{}/'.format(user.pk),
-                "http://happy-dev.fr/owl/#first_name": "Alexandre",
-                "http://happy-dev.fr/owl/#last_name": "Bourlier",
-                "http://happy-dev.fr/owl/#username": "alex",
-                'http://happy-dev.fr/owl/#userprofile': {'@id': "_:b975"}
-            }
-        ]
-        response = self.client.put('/users/{}/'.format(user.pk), data=json.dumps(body),
-                                   content_type='application/ld+json')
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('userprofile', response.data)
+
 
     def test_create_sub_object_in_existing_object_with_existing_reverse_1to1_relation(self):
         user = User.objects.create(username="alex", password="test")
@@ -396,23 +372,6 @@ class Update(TestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.data['peer_user'], None)
 
-    def test_m2m_new_link(self):
-        resource = Resource.objects.create()
-        job = JobOffer.objects.create(title="first title", slug="job")
-        body = {
-            'http://happy-dev.fr/owl/#joboffers': {
-                '@id': 'http://testserver.com/job-offers/{}/'.format(job.slug),
-            }
-        }
-
-        response = self.client.put('/resources/{}/'.format(resource.pk),
-                                   data=json.dumps(body),
-                                   content_type='application/ld+json')
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.data['joboffers']['ldp:contains'][0]['@id'],
-                         "http://testserver.com/job-offers/{}/".format(job.slug))
-        self.assertEqual(response.data['joboffers']['ldp:contains'][0]['title'], "first title")
-
     def test_m2m_new_link_bis(self):
         resource = Resource.objects.create()
         job = JobOffer.objects.create(title="first title", slug="job")
@@ -493,6 +452,62 @@ class Update(TestCase):
         self.assertEqual(response.data['joboffers']['ldp:contains'][0]['@id'],
                          "http://external.job/job/1")
 
+    def test_m2m_new_link_external(self):
+        resource = Resource.objects.create()
+        body = {
+            'http://happy-dev.fr/owl/#joboffers': {
+                '@id': 'http://testserver.com/job-offers/stuff/',
+            }
+        }
+
+        response = self.client.put('/resources/{}/'.format(resource.pk),
+                                   data=json.dumps(body),
+                                   content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.data['joboffers']['ldp:contains'][0]['@id'],
+                         "http://testserver.com/job-offers/stuff/")
+
+    def test_m2m_new_link_local(self):
+        resource = Resource.objects.create()
+        job = JobOffer.objects.create(title="first title", slug="job")
+        body = {
+            'http://happy-dev.fr/owl/#joboffers': {
+                '@id': 'http://happy-dev.fr/job-offers/{}/'.format(job.slug),
+            }
+        }
+
+        response = self.client.put('/resources/{}/'.format(resource.pk),
+                                   data=json.dumps(body),
+                                   content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.data['joboffers']['ldp:contains'][0]['@id'],
+                         "http://happy-dev.fr/job-offers/{}/".format(job.slug))
+        self.assertEqual(response.data['joboffers']['ldp:contains'][0]['title'], "first title")
+
+    def test_update_with_new_fk_relation(self):
+        user = User.objects.create(username="alex", password="test")
+        conversation = Conversation.objects.create(author_user=user,
+                                                   description="conversation description")
+        body = [
+            {
+                '@id': "/conversations/{}/".format(conversation.pk),
+                'http://happy-dev.fr/owl/#description': "conversation update",
+                'http://happy-dev.fr/owl/#peer_user': {
+                    '@id': 'http://happy-dev.fr/users/{}'.format(user.pk),
+                }
+            }
+        ]
+        response = self.client.put('/conversations/{}/'.format(conversation.pk), data=json.dumps(body),
+                                   content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('peer_user', response.data)
+
+        conversation = Conversation.objects.get(pk=conversation.pk)
+        self.assertIsNotNone(conversation.peer_user)
+
+        user = User.objects.get(pk=user.pk)
+        self.assertEqual(user.peers_conv.count(), 1)
+
     def test_m2m_user_link_federated(self):
         circle = Circle.objects.create(description="cicle name")
         body = {
@@ -508,3 +523,74 @@ class Update(TestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.data['team']['ldp:contains'][0]['@id'],
                          "http://external.user/user/1")
+
+    def test_m2m_user_link_existing_external(self):
+        circle = Circle.objects.create(description="cicle name")
+        ext_user = User.objects.create(username='http://external.user/user/1')
+        body = {
+            'http://happy-dev.fr/owl/#description': 'circle name',
+            'http://happy-dev.fr/owl/#team': {
+                'http://happy-dev.fr/owl/#@id': ext_user.username,
+            }
+        }
+
+        response = self.client.put('/circles/{}/'.format(circle.pk),
+                                   data=json.dumps(body),
+                                   content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.data['team']['ldp:contains'][0]['@id'],
+                         ext_user.username)
+
+        circle = Circle.objects.get(pk=circle.pk)
+        self.assertEqual(circle.team.count(), 1)
+
+        user = User.objects.get(pk=ext_user.pk)
+        self.assertEqual(user.circle_set.count(), 1)
+
+    def test_create_sub_object_in_existing_object_with_reverse_1to1_relation(self):
+        """
+        Doesn't work with depth = 0 on UserProfile Model. Should it be ?
+        """
+        user = User.objects.create(username="alex", password="test")
+        body = [
+            {
+                '@id': "_:b975",
+                'http://happy-dev.fr/owl/#description': "user description",
+                'http://happy-dev.fr/owl/#dummy': {
+                    '@id': './'
+                }
+            },
+            {
+                '@id': '/users/{}/'.format(user.pk),
+                "http://happy-dev.fr/owl/#first_name": "Alexandre",
+                "http://happy-dev.fr/owl/#last_name": "Bourlier",
+                "http://happy-dev.fr/owl/#username": "alex",
+                'http://happy-dev.fr/owl/#userprofile': {'@id': "_:b975"}
+            }
+        ]
+        response = self.client.put('/users/{}/'.format(user.pk), data=json.dumps(body),
+                                   content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('userprofile', response.data)
+
+    def test_m2m_user_link_remove_existing_link(self):
+        ext_user = User.objects.create(username='http://external.user/user/1')
+        circle = Circle.objects.create(description="cicle name")
+        circle.team.add(ext_user)
+        circle.save()
+        body = {
+            'http://happy-dev.fr/owl/#description': 'circle name',
+            'http://happy-dev.fr/owl/#team': {
+            }
+        }
+
+        response = self.client.put('/circles/{}/'.format(circle.pk),
+                                   data=json.dumps(body),
+                                   content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+
+        circle = Circle.objects.get(pk=circle.pk)
+        self.assertEqual(circle.team.count(), 0)
+
+        user = User.objects.get(pk=ext_user.pk)
+        self.assertEqual(user.circle_set.count(), 0)
-- 
GitLab