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