From 56c78ee68566216513bd4587567319469757bcb1 Mon Sep 17 00:00:00 2001 From: Calum Mackervoy <c.mackervoy@gmail.com> Date: Thu, 9 Jul 2020 21:57:41 +0000 Subject: [PATCH] minor: nested_fields via manager --- README.md | 28 ++++++++++++++++++++++++++-- djangoldp/__init__.py | 4 ++-- djangoldp/models.py | 20 +++++++++++++++++++- djangoldp/serializers.py | 2 +- djangoldp/tests/models.py | 8 +------- djangoldp/tests/tests_ldp_model.py | 20 +++++++++++++++++++- djangoldp/urls.py | 2 +- 7 files changed, 69 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 094de48e..6d3c4299 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ See "Custom Meta options" below to see some helpful ways you can tweak the behav Your model will be automatically detected and registered with an LDPViewSet and corresponding URLs, as well as being registered with the Django admin panel. If you register your model with the admin panel manually, make sure to extend djangoldp.DjangoLDPAdmin so that the model is registered with [Django-Guardian object permissions](https://django-guardian.readthedocs.io/en/stable/userguide/admin-integration.html). An alternative version which extends Django's `UserAdmin` is available as djangoldp.DjangoLDPUserAdmin -### Model Federation +#### Model Federation Model `urlid`s can be **local** (matching `settings.SITE_URL`), or **external** @@ -205,7 +205,14 @@ It can also be disabled on a model instance instance.allow_create_backlinks = False ``` -For situations where you don't want to include federated resources in a queryset, DjangoLDP Models override `models.Manager`, allowing you to write `Todo.objects.local()`, for example: +### LDPManager + +DjangoLDP Models override `models.Manager`, accessible by `Model.objects` + +#### local() + +For situations where you don't want to include federated resources in a queryset e.g. + ```python Todo.objects.create(name='Local Todo') Todo.objects.create(name='Distant Todo', urlid='https://anotherserversomewhere.com/todos/1/') @@ -216,6 +223,9 @@ Todo.objects.local() # { Local Todo } only For Views, we also define a FilterBackend to achieve the same purpose. See the section on ViewSets for this purpose +#### nested_fields() + +returns a list of all nested field names for the model, built of a union of the model class' `nested_fields` setting, the to-many relations on the model, excluding all fields detailed by `nested_fields_exclude` ## LDPViewSet @@ -401,6 +411,20 @@ class Todo(Model): Only `name` will be serialized +### nested_fields -- DEPRECIATED + +Set on a model to auto-generate viewsets and containers for nested relations (e.g. `/circles/<pk>/members/`) + +Depreciated in DjangoLDP 0.8.0, as all to-many fields are included as nested fields by default + +### nested_fields_exclude + +```python +<Model>._meta.nested_fields_exclude=["skills"] +``` + +Will exclude the field `skills` from the model's nested fields, and prevent a container `/model/<pk>/skills/` from being generated + ## Custom urls To add customs urls who can not be add through the `Model` class, it's possible de create a file named `djangoldp_urls.py`. It will be executed like an `urls.py` file diff --git a/djangoldp/__init__.py b/djangoldp/__init__.py index 736d53af..ac5b5247 100644 --- a/djangoldp/__init__.py +++ b/djangoldp/__init__.py @@ -3,6 +3,6 @@ from django.db.models import options __version__ = '0.0.0' options.DEFAULT_NAMES += ( 'lookup_field', 'rdf_type', 'rdf_context', 'auto_author', 'auto_author_field', 'owner_field', 'view_set', - 'container_path', 'permission_classes', 'serializer_fields', 'nested_fields', 'depth', 'anonymous_perms', - 'authenticated_perms', 'owner_perms') + 'container_path', 'permission_classes', 'serializer_fields', 'nested_fields', 'nested_fields_exclude', 'depth', + 'anonymous_perms', 'authenticated_perms', 'owner_perms') default_app_config = 'djangoldp.apps.DjangoldpConfig' diff --git a/djangoldp/models.py b/djangoldp/models.py index 65638a41..5ebfd80b 100644 --- a/djangoldp/models.py +++ b/djangoldp/models.py @@ -12,6 +12,7 @@ from django.dispatch import receiver from django.urls import reverse_lazy, get_resolver from django.utils.datastructures import MultiValueDictKeyError from django.utils.decorators import classonlymethod +from rest_framework.utils import model_meta from djangoldp.fields import LDPUrlField from djangoldp.permissions import LDPPermissions @@ -22,12 +23,28 @@ logger = logging.getLogger('djangoldp') class LDPModelManager(models.Manager): - # an alternative to all() which exlcudes external resources def local(self): + '''an alternative to all() which exlcudes external resources''' queryset = super(LDPModelManager, self).all() internal_ids = [x.pk for x in queryset if not Model.is_external(x)] return queryset.filter(pk__in=internal_ids) + def nested_fields(self): + '''parses the relations on the model, and returns a list of nested field names''' + nested_fields = set() + # include all many-to-many relations + for field_name, relation_info in model_meta.get_field_info(self.model).relations.items(): + if relation_info.to_many: + nested_fields.add(field_name) + # include all nested fields explicitly included on the model + nested_fields.update(set(Model.get_meta(self.model, 'nested_fields', set()))) + # exclude anything marked explicitly to be excluded + nested_fields = nested_fields.difference(set(Model.get_meta(self.model, 'nested_fields_exclude', set()))) + return list(nested_fields) + + def fields(self): + return self.nested_fields() + class Model(models.Model): urlid = LDPUrlField(blank=True, null=True, unique=True) @@ -35,6 +52,7 @@ class Model(models.Model): allow_create_backlink = models.BooleanField(default=True, help_text='set to False to disable backlink creation after Model save') objects = LDPModelManager() + nested = LDPModelManager() def __init__(self, *args, **kwargs): super(Model, self).__init__(*args, **kwargs) diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index b7da7239..cfbbe047 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -320,7 +320,7 @@ class LDPSerializer(HyperlinkedModelSerializer): 'permission_classes', [LDPPermissions]), fields=Model.get_meta(model_class, 'serializer_fields', []), - nested_fields=Model.get_meta(model_class, 'nested_fields', [])) + nested_fields=model_class.nested.nested_fields()) parent_depth = max(getattr(self.parent.Meta, "depth", 0) - 1, 0) serializer_generator.depth = parent_depth serializer = serializer_generator.build_read_serializer()(context=self.parent.context) diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py index aec55aaf..9a3acce5 100644 --- a/djangoldp/tests/models.py +++ b/djangoldp/tests/models.py @@ -17,7 +17,6 @@ class User(AbstractUser, Model): anonymous_perms = ['view', 'add'] authenticated_perms = ['inherit', 'change'] owner_perms = ['inherit'] - nested_fields = ['circles', 'projects'] class Skill(Model): @@ -53,7 +52,6 @@ class JobOffer(Model): anonymous_perms = ['view'] authenticated_perms = ['inherit', 'change', 'add'] owner_perms = ['inherit', 'delete', 'control'] - nested_fields = ["skills"] serializer_fields = ["@id", "title", "skills", "recent_skills", "resources", "slug", "some_skill", "urlid"] container_path = "job-offers/" lookup_field = 'slug' @@ -80,7 +78,6 @@ class Resource(Model): authenticated_perms = ['inherit'] owner_perms = ['inherit'] serializer_fields = ["@id", "joboffers"] - nested_fields = ["joboffers"] depth = 1 @@ -148,7 +145,6 @@ class Invoice(Model): anonymous_perms = ['view'] authenticated_perms = ['inherit', 'add'] owner_perms = ['inherit', 'change', 'delete', 'control'] - nested_fields = ["batches"] class Batch(Model): @@ -160,7 +156,6 @@ class Batch(Model): anonymous_perms = ['view', 'add'] authenticated_perms = ['inherit', 'add'] owner_perms = ['inherit', 'change', 'delete', 'control'] - nested_fields = ["tasks", 'invoice'] depth = 1 @@ -196,7 +191,7 @@ class Circle(Model): null=True, blank=True) class Meta(Model.Meta): - nested_fields = ["team"] + nested_fields_exclude = ["team"] anonymous_perms = ['view', 'add', 'delete', 'add', 'change', 'control'] authenticated_perms = ["inherit"] rdf_type = 'hd:circle' @@ -221,7 +216,6 @@ class Project(Model): team = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='projects') class Meta(Model.Meta): - nested_fields = ["team"] anonymous_perms = ['view', 'add', 'delete', 'add', 'change', 'control'] authenticated_perms = ["inherit"] rdf_type = 'hd:project' diff --git a/djangoldp/tests/tests_ldp_model.py b/djangoldp/tests/tests_ldp_model.py index bc316ed5..626fc967 100644 --- a/djangoldp/tests/tests_ldp_model.py +++ b/djangoldp/tests/tests_ldp_model.py @@ -3,7 +3,7 @@ import unittest from django.test import TestCase from djangoldp.models import Model -from djangoldp.tests.models import Dummy, LDPDummy +from djangoldp.tests.models import Dummy, LDPDummy, Circle, CircleMember class LDPModelTest(TestCase): @@ -36,3 +36,21 @@ class LDPModelTest(TestCase): path = 'http://happy-dev.fr/{}{}/'.format(get_resolver().reverse_dict[view_name][0][0][0], dummy.pk) self.assertEquals(path, dummy.get_absolute_url()) + + def test_ldp_manager_local_objects(self): + local = LDPDummy.objects.create(some='text') + external = LDPDummy.objects.create(some='text', urlid='https://distant.com/ldpdummys/1/') + self.assertEqual(LDPDummy.objects.count(), 2) + local_queryset = LDPDummy.objects.local() + self.assertEqual(local_queryset.count(), 1) + self.assertIn(local, local_queryset) + self.assertNotIn(external, local_queryset) + + def test_ldp_manager_nested_fields(self): + nested_fields = Circle.objects.nested_fields() + expected_nested_fields = ['members'] + self.assertEqual(nested_fields, expected_nested_fields) + + nested_fields = CircleMember.objects.nested_fields() + expected_nested_fields = [] + self.assertEqual(nested_fields, expected_nested_fields) diff --git a/djangoldp/urls.py b/djangoldp/urls.py index 62b9a61a..49db770c 100644 --- a/djangoldp/urls.py +++ b/djangoldp/urls.py @@ -50,4 +50,4 @@ for class_name in model_classes: lookup_field=Model.get_meta(model_class, 'lookup_field', 'pk'), permission_classes=Model.get_meta(model_class, 'permission_classes', [LDPPermissions]), fields=Model.get_meta(model_class, 'serializer_fields', []), - nested_fields=Model.get_meta(model_class, 'nested_fields', []))))) + nested_fields=model_class.nested.fields())))) -- GitLab