From 37400bdd5aabdee3e7d2fc476d847b6d0b6f998f Mon Sep 17 00:00:00 2001
From: Sylvain Le Bon <sylvain@startinblox.com>
Date: Thu, 5 Oct 2023 18:09:23 +0200
Subject: [PATCH] bugfix: many-to-many are also callable

---
 djangoldp/models.py                | 4 ++--
 djangoldp/tests/models.py          | 5 ++++-
 djangoldp/tests/tests_get.py       | 3 +++
 djangoldp/tests/tests_ldp_model.py | 4 ++--
 djangoldp/views.py                 | 4 ++--
 docs/create_model.md               | 7 +++++++
 6 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/djangoldp/models.py b/djangoldp/models.py
index 013cc5d9..d7ecf17c 100644
--- a/djangoldp/models.py
+++ b/djangoldp/models.py
@@ -361,7 +361,7 @@ class DynamicNestedField:
     Used to define a method as a nested_field.
     Usage:
         LDPUser.circles = lambda self: Circle.objects.filter(members__user=self)
-        LDPUser.circles.field = DynamicField(Circle, 'circles')
+        LDPUser.circles.field = DynamicNestedField(Circle, 'circles')
     '''
     related_query_name = None
     one_to_many = False
@@ -370,7 +370,7 @@ class DynamicNestedField:
     one_to_one = False
     def __init__(self, model, name) -> None:
         self.model = model
-        self.remote_field = type('Field', (object,), {'name': 'circles'})
+        self.remote_field = type('Field', (object,), {'name': name})
 
 @receiver([post_save])
 def auto_urlid(sender, instance, **kwargs):
diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py
index 6a6548ad..3e8c0bf7 100644
--- a/djangoldp/tests/models.py
+++ b/djangoldp/tests/models.py
@@ -3,7 +3,7 @@ from django.contrib.auth.models import AbstractUser, Group
 from django.db import models
 from django.utils.datetime_safe import date
 
-from djangoldp.models import Model
+from djangoldp.models import Model, DynamicNestedField
 from djangoldp.permissions import ACLPermissions, AuthenticatedOnly, ReadOnly, \
     ReadAndCreate, AnonymousReadOnly, OwnerPermissions, InheritPermissions
 
@@ -52,10 +52,13 @@ class JobOffer(Model):
         ordering = ['pk']
         permission_classes = [AnonymousReadOnly, ReadOnly|OwnerPermissions]
         serializer_fields = ["@id", "title", "skills", "recent_skills", "resources", "slug", "some_skill", "urlid"]
+        nested_fields = ['resources', 'recent_skills']
         container_path = "job-offers/"
         lookup_field = 'slug'
         rdf_type = 'hd:joboffer'
 
+JobOffer.recent_skills.field = DynamicNestedField(Skill, 'recent_skills')
+
 
 class Conversation(models.Model):
     description = models.CharField(max_length=255, blank=True, null=True)
diff --git a/djangoldp/tests/tests_get.py b/djangoldp/tests/tests_get.py
index 27c39162..032df707 100644
--- a/djangoldp/tests/tests_get.py
+++ b/djangoldp/tests/tests_get.py
@@ -104,6 +104,9 @@ class TestGET(APITestCase):
         self.assertEqual(response.status_code, 200)
         self.assertIn('some_skill', response.data)
         self.assertEqual(response.data['some_skill']['@id'], skill.urlid)
+        response = self.client.get('/job-offers/{}/recent_skills/'.format(job.slug), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.data['ldp:contains']), 2)
 
     def test_get_nested(self):
         invoice = Invoice.objects.create(title="invoice")
diff --git a/djangoldp/tests/tests_ldp_model.py b/djangoldp/tests/tests_ldp_model.py
index c8a6b20f..4432d643 100644
--- a/djangoldp/tests/tests_ldp_model.py
+++ b/djangoldp/tests/tests_ldp_model.py
@@ -46,7 +46,7 @@ class LDPModelTest(TestCase):
 
     def test_ldp_manager_nested_fields_auto(self):
         nested_fields = JobOffer.nested_fields()
-        expected_nested_fields = ['skills', 'resources']
+        expected_nested_fields = ['skills', 'resources', 'recent_skills']
         self.assertEqual(len(nested_fields), len(expected_nested_fields))
         for expected in expected_nested_fields:
             self.assertIn(expected, nested_fields)
@@ -58,5 +58,5 @@ class LDPModelTest(TestCase):
     def test_ldp_manager_nested_fields_exclude(self):
         JobOffer._meta.nested_fields_exclude = ['skills']
         nested_fields = JobOffer.nested_fields()
-        expected_nested_fields = ['resources']
+        expected_nested_fields = ['resources', 'recent_skills']
         self.assertEqual(nested_fields, expected_nested_fields)
diff --git a/djangoldp/views.py b/djangoldp/views.py
index 4c0fc44c..113df15a 100644
--- a/djangoldp/views.py
+++ b/djangoldp/views.py
@@ -25,7 +25,7 @@ from rest_framework.views import APIView
 from rest_framework.viewsets import ModelViewSet
 
 from djangoldp.endpoints.webfinger import WebFingerEndpoint, WebFingerError
-from djangoldp.models import LDPSource, Model, Follower
+from djangoldp.models import LDPSource, Model, Follower, DynamicNestedField
 from djangoldp.filters import LocalObjectOnContainerPathBackend, SearchByQueryParamFilterBackend
 from djangoldp.related import get_prefetch_fields
 from djangoldp.utils import is_authenticated_user
@@ -596,7 +596,7 @@ class LDPNestedViewSet(LDPViewSet):
     def get_queryset(self, *args, **kwargs):
         related = getattr(self.get_parent(), self.nested_field)
         if self.related_field.many_to_many or self.related_field.many_to_one or self.related_field.one_to_many:
-            if callable(related):
+            if isinstance(self.related_field, DynamicNestedField):
                 return related()
             return related.all()
         if self.related_field.one_to_one:
diff --git a/docs/create_model.md b/docs/create_model.md
index f0a1fa82..200e4d85 100644
--- a/docs/create_model.md
+++ b/docs/create_model.md
@@ -211,6 +211,13 @@ In the following example, besides the urls `/members/` and `/members/<pk>/`, two
 <Model>._meta.nested_fields=["skills"]
 ```
 
+Methods can be used to create custom read-only fields, by adding the name of the method in the `serializer_fields`. The same can be done for nested fields, but the method must be decorated with a `DynamicNestedField`.
+
+```python
+LDPUser.circles = lambda self: Circle.objects.filter(members__user=self)
+LDPUser.circles.field = DynamicNestedField(Circle, 'circles')
+```
+
 ### Improving Performance
 
 On certain endpoints, you may find that you only need a subset of fields on a model, and serializing them all is expensive (e.g. if I only need the `name` and `id` of each group chat, then why serialize all of their members?). To optimise the fields serialized, you can pass a custom header in the request, `Accept-Model-Fields`, with a `list` value of desired fields e.g. `['@id', 'name']`
-- 
GitLab