diff --git a/djangoldp/__init__.py b/djangoldp/__init__.py index 61f1c6616668a3208fe17dc4a3a8b43ac511f42d..3ad85e21480b6ba5790bdb01d1d088067a45e0e0 100644 --- a/djangoldp/__init__.py +++ b/djangoldp/__init__.py @@ -5,4 +5,4 @@ __version__ = '0.0.0' options.DEFAULT_NAMES += ( 'lookup_field', 'rdf_type', 'rdf_context', 'auto_author', 'auto_author_field', 'owner_field', 'owner_urlid_field', 'view_set', 'container_path', 'permission_classes', 'serializer_fields', 'serializer_fields_exclude', 'empty_containers', - 'nested_fields', 'nested_fields_exclude', 'depth', 'permission_roles', 'inherit_permissions', 'public_field') + 'nested_fields', 'depth', 'permission_roles', 'inherit_permissions', 'public_field') diff --git a/djangoldp/apps.py b/djangoldp/apps.py index 70b35fb565cc6d9e5a0199c4d9da25a0e7c9a4f3..a4b4cc15cf29f878dc2d5f832c7e0c284fe8a897 100644 --- a/djangoldp/apps.py +++ b/djangoldp/apps.py @@ -34,7 +34,7 @@ class DjangoldpConfig(AppConfig): from django.conf import settings from django.contrib import admin from djangoldp.admin import DjangoLDPAdmin - from djangoldp.urls import get_all_non_abstract_subclasses_dict + from djangoldp.urls import get_all_non_abstract_subclasses from djangoldp.models import Model for package in settings.DJANGOLDP_PACKAGES: @@ -49,9 +49,6 @@ class DjangoldpConfig(AppConfig): except ModuleNotFoundError: pass - model_classes = get_all_non_abstract_subclasses_dict(Model) - - for class_name in model_classes: - model_class = model_classes[class_name] - if not admin.site.is_registered(model_class): - admin.site.register(model_class, DjangoLDPAdmin) + for model in get_all_non_abstract_subclasses(Model): + if not admin.site.is_registered(model): + admin.site.register(model, DjangoLDPAdmin) diff --git a/djangoldp/models.py b/djangoldp/models.py index 8bf32bf7d0a85ec4c1d51f5195d74ffef414dc84..3b977509b192f3dc44e63fb3a6d30cf38fd663d9 100644 --- a/djangoldp/models.py +++ b/djangoldp/models.py @@ -53,20 +53,6 @@ class Model(models.Model): from djangoldp.serializers import LDPSerializer return LDPSerializer - @classmethod - def nested_fields(cls): - '''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(cls).relations.items(): - if relation_info.to_many and field_name: - nested_fields.add(field_name) - # include all nested fields explicitly included on the model - nested_fields.update(set(getattr(cls._meta, 'nested_fields', set()))) - # exclude anything marked explicitly to be excluded - nested_fields = nested_fields.difference(set(getattr(cls._meta, 'nested_fields_exclude', set()))) - return list(nested_fields) - @classmethod def get_container_path(cls): '''returns the url path which is used to access actions on this model (e.g. /users/)''' diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 5f422d1db33dd3ea379f170932fa5c10dc4fbf59..4c5befdb6d5f74788d62918529a42fe8b804b5f9 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -436,7 +436,7 @@ class LDPSerializer(HyperlinkedModelSerializer, RDFSerializerMixin): lookup_field=getattr(model._meta, 'lookup_field', 'pk'), permission_classes=getattr(model._meta, 'permission_classes', []), fields=getattr(model._meta, 'serializer_fields', []), - nested_fields=model.nested_fields()) + nested_fields=getattr(model._meta, 'nested_fields', [])) parent_depth = max(getattr(self.parent.Meta, "depth", 0) - 1, 0) serializer_generator.depth = parent_depth serializer = serializer_generator.build_serializer()(context=self.parent.context) diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py index 6a6548ada34087dee709b086856df5122cca1679..4240ea58634b7efcdcdfd1ad041dd3bc595cf052 100644 --- a/djangoldp/tests/models.py +++ b/djangoldp/tests/models.py @@ -17,6 +17,7 @@ class User(AbstractUser, Model): 'conversation_set','groups', 'projects', 'owned_circles'] permission_classes = [ReadAndCreate|OwnerPermissions] rdf_type = 'foaf:user' + nested_fields = ['owned_circles'] class Skill(Model): @@ -32,6 +33,7 @@ class Skill(Model): ordering = ['pk'] permission_classes = [AnonymousReadOnly,ReadAndCreate|OwnerPermissions] serializer_fields = ["@id", "title", "recent_jobs", "slug", "obligatoire"] + nested_fields = ['joboffer_set'] lookup_field = 'slug' rdf_type = 'hd:skill' @@ -52,6 +54,7 @@ class JobOffer(Model): ordering = ['pk'] permission_classes = [AnonymousReadOnly, ReadOnly|OwnerPermissions] serializer_fields = ["@id", "title", "skills", "recent_skills", "resources", "slug", "some_skill", "urlid"] + nested_fields = ['skills', 'resources'] container_path = "job-offers/" lookup_field = 'slug' rdf_type = 'hd:joboffer' @@ -78,6 +81,7 @@ class Resource(Model): class Meta(Model.Meta): ordering = ['pk'] serializer_fields = ["@id", "joboffers"] + nested_fields = ['joboffers'] depth = 1 rdf_type = 'hd:Resource' @@ -93,6 +97,7 @@ class OwnedResource(Model): permission_classes = [OwnerPermissions] owner_field = 'user' serializer_fields = ['@id', 'description', 'user'] + nested_fields = ['owned_resources'] depth = 1 @@ -119,6 +124,7 @@ class OwnedResourceNestedOwnership(Model): permission_classes = [OwnerPermissions] owner_field = 'parent__user' serializer_fields = ['@id', 'description', 'parent'] + nested_fields = ['owned_resources'] depth = 1 @@ -183,6 +189,7 @@ class LDPDummy(Model): class Meta(Model.Meta): ordering = ['pk'] permission_classes = [AnonymousReadOnly,ReadAndCreate|OwnerPermissions] + nested_fields = ['anons'] # model used in django-guardian permission tests (no permission to anyone except suuperusers) @@ -251,6 +258,7 @@ class Invoice(Model): ordering = ['pk'] depth = 2 permission_classes = [AnonymousReadOnly,ReadAndCreate|OwnerPermissions] + nested_fields = ['batches'] class Circle(Model): @@ -345,6 +353,7 @@ class Project(Model): class Meta(Model.Meta): ordering = ['pk'] rdf_type = 'hd:project' + nested_fields = ['members'] class DateModel(Model): diff --git a/djangoldp/tests/tests_get.py b/djangoldp/tests/tests_get.py index 27c391624d649aa43b052105cf472d744372c699..87fd834860afc7556fb904af3f85db5a7b47c1f7 100644 --- a/djangoldp/tests/tests_get.py +++ b/djangoldp/tests/tests_get.py @@ -196,6 +196,7 @@ class TestGET(APITestCase): user = self._set_up_circle_and_user() response = self.client.get(f'/users/{user.pk}/owned_circles/', content_type='application/ld+json') + self.assertEqual(response.status_code, 200) self.assertEqual(response.data['@type'], 'ldp:Container') self.assertIn('@id', response.data) self.assertIn('ldp:contains', response.data) @@ -208,6 +209,7 @@ class TestGET(APITestCase): user = self._set_up_circle_and_user() response = self.client.get(f'/users/{user.pk}/owned_circles/', content_type='application/ld+json') + self.assertEqual(response.status_code, 200) self.assertEqual(response.data['@type'], 'ldp:Container') self.assertIn('@id', response.data) self.assertIn('ldp:contains', response.data) diff --git a/djangoldp/tests/tests_ldp_model.py b/djangoldp/tests/tests_ldp_model.py index c8a6b20f24608970a9b8853a98437a642a60f63d..51e5b94594441b2cc7084150cb396bd0f2df851f 100644 --- a/djangoldp/tests/tests_ldp_model.py +++ b/djangoldp/tests/tests_ldp_model.py @@ -43,20 +43,3 @@ class LDPModelTest(TestCase): self.assertEqual(local_queryset.count(), 1) self.assertIn(local, local_queryset) self.assertNotIn(external, local_queryset) - - def test_ldp_manager_nested_fields_auto(self): - nested_fields = JobOffer.nested_fields() - expected_nested_fields = ['skills', 'resources'] - self.assertEqual(len(nested_fields), len(expected_nested_fields)) - for expected in expected_nested_fields: - self.assertIn(expected, nested_fields) - - nested_fields = NoSuperUsersAllowedModel.nested_fields() - expected_nested_fields = [] - self.assertEqual(nested_fields, expected_nested_fields) - - def test_ldp_manager_nested_fields_exclude(self): - JobOffer._meta.nested_fields_exclude = ['skills'] - nested_fields = JobOffer.nested_fields() - expected_nested_fields = ['resources'] - self.assertEqual(nested_fields, expected_nested_fields) diff --git a/djangoldp/urls.py b/djangoldp/urls.py index 06722388e102a3229c3f9a11cf5db9b458467910..69df327188e71b0e664fa5b43c9dc876c0259c67 100644 --- a/djangoldp/urls.py +++ b/djangoldp/urls.py @@ -28,12 +28,7 @@ def get_all_non_abstract_subclasses(cls): return not getattr(sc._meta, 'abstract', False) return set(c for c in cls.__subclasses__() if valid_subclass(c)).union( - [s for c in cls.__subclasses__() for s in get_all_non_abstract_subclasses(c) if valid_subclass(s)]) - - -def get_all_non_abstract_subclasses_dict(cls): - '''returns a dict of class name -> class for all subclasses of given cls parameter (recursively)''' - return {cls.__name__: cls for cls in get_all_non_abstract_subclasses(cls)} + [subclass for c in cls.__subclasses__() for subclass in get_all_non_abstract_subclasses(c) if valid_subclass(subclass)]) urlpatterns = [ path('groups/', LDPViewSet.urls(model=Group, fields=['@id', 'name', 'user_set']),), @@ -66,22 +61,19 @@ for package in settings.DJANGOLDP_PACKAGES: except ModuleNotFoundError: pass -# fetch a list of all models which subclass DjangoLDP Model -model_classes = get_all_non_abstract_subclasses_dict(Model) - # append urls for all DjangoLDP Model subclasses -for class_name in model_classes: - model_class = model_classes[class_name] +for model in get_all_non_abstract_subclasses(Model): # the path is the url for this model - model_path = __clean_path(model_class.get_container_path()) + model_path = __clean_path(model.get_container_path()) # urls_fct will be a method which generates urls for a ViewSet (defined in LDPViewSetGenerator) - urls_fct = getattr(model_class, 'view_set', LDPViewSet).urls + urls_fct = getattr(model, 'view_set', LDPViewSet).urls urlpatterns.append(path('' + model_path, - urls_fct(model=model_class, - lookup_field=getattr(model_class._meta, 'lookup_field', 'pk'), - permission_classes=getattr(model_class._meta, 'permission_classes', []), - fields=getattr(model_class._meta, 'serializer_fields', []), - nested_fields=model_class.nested_fields()))) + urls_fct(model=model, + lookup_field=getattr(model._meta, 'lookup_field', 'pk'), + permission_classes=getattr(model._meta, 'permission_classes', []), + fields=getattr(model._meta, 'serializer_fields', []), + nested_fields=getattr(model._meta, 'nested_fields', []) + ))) # NOTE: this route will be ignored if a custom (subclass of Model) user model is used, or it is registered by a package # Django matches the first url it finds for a given path diff --git a/docs/create_model.md b/docs/create_model.md index f0a1fa8274049706b7fd0011e93717d8264800d8..a0ddc845f0b75f90e271ecbef8b1ae8acbc1d95f 100644 --- a/docs/create_model.md +++ b/docs/create_model.md @@ -183,10 +183,6 @@ 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 DjangoLDP automatically generates ViewSets for your models, and registers these at urls, according to the settings configured in the model Meta (see below for options) @@ -205,10 +201,13 @@ LDPViewSet.urls(model=User, lookup_field='username') list of ForeignKey, ManyToManyField, OneToOneField and their reverse relations. When a field is listed in this parameter, a container will be created inside each single element of the container. -In the following example, besides the urls `/members/` and `/members/<pk>/`, two other will be added to serve a container of the skills of the member: `/members/<pk>/skills/` and `/members/<pk>/skills/<pk>/` +In the following example, besides the urls `/members/` and `/members/<pk>/`, two others will be added to serve a container of the skills of the member: `/members/<pk>/skills/` and `/members/<pk>/skills/<pk>/`. + +ForeignKey, ManyToManyField, OneToOneField that are not listed in the `nested_fields` option will be rendered as a flat list and will not have their own container endpoint. ```python -<Model>._meta.nested_fields=["skills"] +Meta: + nested_fields=["skills"] ``` ### Improving Performance @@ -417,15 +416,6 @@ Only `deadline` will be serialized This is achieved when `LDPViewSet` sets the `exclude` property on the serializer in `build_serializer` method. Note that if you use a custom viewset which does not extend LDPSerializer then you will need to set this property yourself -### nested_fields_exclude - -```python - class 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 - ### empty_containers Slightly different from `serializer_fields` and `nested_fields` is the `empty_containers`, which allows for a list of nested containers which should be serialized, but without content, i.e. producing something like the following: