diff --git a/README.md b/README.md index 094de48e26f3352ec4f8be2855c69e2bbf6a6081..6d3c4299a75118e8370754bf7e73eacb37729ff2 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 736d53afff67ce74a147bf5d6d46ddf368523451..ac5b5247a204071adef15cd2ab6234e57d81cd98 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 65638a41034bf6d531f975ab699cbd14b8e94d75..5ebfd80b7c52d90de441447f7317c144a3daf2e6 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 b7da723952079ae7baa7c0fa436f700fbe0893ac..cfbbe047a9e527da693899b634406169e9f0af73 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 aec55aafd1fa04a7c8d4bad39280c5341464087c..9a3acce5526d308ce0f27369965a99e4a43d7cc5 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 bc316ed50914dea6fb6fc99847392fa9d09e2562..626fc967f85fc8adc5a28c036014705ec6028258 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 62b9a61aaf7cb7af49476e28aae88e3c9079d512..49db770cbe66344c89f35c0102653fb6342b35a0 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()))))