diff --git a/README.md b/README.md index 264545b7ce9dd8133fe7e2b28636ccf2b3c14dbe..404d022e5f0b79fbcb214b88ca00380694cc6409 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This module is an add-on for Django REST Framework that serves a django model re It aims at enabling people with little development skills to serve their own data, to be used with a LDP application. +Building a Startin' Blox application? Read this: https://git.happy-dev.fr/startinblox/devops/doc + ## Requirements * Django (known to work with django 1.11) @@ -26,7 +28,18 @@ $ pip install djangoldp $ django-admin startproject myldpserver ``` -3. Create your django model inside a file myldpserver/myldpserver/models.py +3. Add DjangoLDP to INSTALLED_APPS +```python +INSTALLED_APPS = [ + ... + # make sure all of your own apps are installed BEFORE DjangoLDP + 'djangoldp.apps.DjangoldpConfig', +] +``` + +IMPORTANT: DjangoLDP will register any models which haven't been registered, with the admin. As such it is important to add your own apps above DjangoLDP, so that you can use custom Admin classes if you wish + +4. Create your django model inside a file myldpserver/myldpserver/models.py Note that container_path will be use to resolve instance iri and container iri In the future it could also be used to auto configure django router (e.g. urls.py) @@ -38,14 +51,14 @@ class Todo(Model): deadline = models.DateTimeField() ``` -3.1. Configure container path (optional) +4.1. Configure container path (optional) By default it will be "todos/" with an S for model called Todo ```python <Model>._meta.container_path = "/my-path/" ``` -3.2. Configure field visibility (optional) +4.2. Configure field visibility (optional) Note that at this stage you can limit access to certain fields of models using ```python @@ -64,7 +77,7 @@ User._meta.serializer_fields = ('username','first_name','last_name') Note that this will be overridden if you explicitly set the fields= parameter as an argument to LDPViewSet.urls(), and filtered if you set the excludes= parameter. -4. Add a url in your urls.py: +5. Add a url in your urls.py: ```python from django.conf.urls import url @@ -86,14 +99,23 @@ You could also only use this line in settings.py instead: ROOT_URLCONF = 'djangoldp.urls' ``` -5. In the settings.py file, add your application name at the beginning of the application list, and add the following lines +6. In the settings.py file, add your application name at the beginning of the application list, and add the following lines ```python STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'static') LDP_RDF_CONTEXT = 'https://cdn.happy-dev.fr/owl/hdcontext.jsonld' +DJANGOLDP_PACKAGES = [] +SITE_URL = 'http://localhost:8000' +BASE_URL = SITE_URL ``` -6. You can also register your model for the django administration site +* `LDP_RDF_CONTEXT` tells DjangoLDP where our RDF [ontology](https://www.w3.org/standards/semanticweb/ontology) is defined, which will be returned as part of our views in the 'context' field. This is a web URL and you can visit the value to view the full ontology online +* `DJANGOLDP_PACKAGES` defines which other [DjangoLDP packages](https://git.happy-dev.fr/startinblox/djangoldp-packages) we're using in this installation +* `SITE_URL` is the URL serving the site, e.g. `https://example.com/` +* `BASE_URL` may be different from SITE_URL, e.g. `https://example.com/app/` + + +7. You can also register your model for the django administration site ```python from django.contrib import admin @@ -102,15 +124,15 @@ from .models import Todo admin.site.register(Todo) ``` -7. You then need to have your WSGI server pointing on myldpserver/myldpserver/wsgi.py +8. You then need to have your WSGI server pointing on myldpserver/myldpserver/wsgi.py -8. You will probably need to create a super user +9. You will probably need to create a super user ```bash $ ./manage.py createsuperuser ``` -9. If you have no CSS on the admin screens : +10. If you have no CSS on the admin screens : ```bash $ ./manage.py collectstatic diff --git a/djangoldp/admin.py b/djangoldp/admin.py index dffdc236ff97205748b9dcaa3626bbb1f35adcfd..2627925705f5bb2cfb378a73eb099ff4779dc565 100644 --- a/djangoldp/admin.py +++ b/djangoldp/admin.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib import admin from .models import LDPSource, Model +# automatically import selected DjangoLDP packages from settings for package in settings.DJANGOLDP_PACKAGES: try: import_module('{}.admin'.format(package)) @@ -18,6 +19,7 @@ for package in settings.DJANGOLDP_PACKAGES: model_classes = {cls.__name__: cls for cls in Model.__subclasses__()} +# automatically register models with the admin panel (which have not been added manually) for class_name in model_classes: model_class = model_classes[class_name] if not admin.site.is_registered(model_class): diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 5e886e15ef59fdbd3b33ad3727fa75e06c074e23..3a19bbf35d9d8f8e4aaa4083f53888453ce51428 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -27,13 +27,17 @@ from djangoldp.permissions import LDPPermissions class LDListMixin: + '''A Mixin used by the custom Serializers in this file''' child_attr = 'child' + # converts primitive data representation to the representation used within our application def to_internal_value(self, data): try: + # if this is a container, the data will be stored in ldp:contains data = data['ldp:contains'] except (TypeError, KeyError): pass + if len(data) == 0: return [] if isinstance(data, dict): @@ -43,6 +47,7 @@ class LDListMixin: return [getattr(self, self.child_attr).to_internal_value(item) for item in data] + # converts internal representation to primitive data representation def to_representation(self, value): ''' Permission on container : @@ -53,7 +58,9 @@ class LDListMixin: child_model = getattr(self, self.child_attr).Meta.model except AttributeError: child_model = value.model + parent_model = None + if isinstance(value, QuerySet): value = list(value) @@ -61,11 +68,13 @@ class LDListMixin: filtered_values = value container_permissions = Model.get_permissions(child_model, self.context['request'].user, ['view', 'add']) else: + # this is a container. Parent model is the containing object, child the model contained try: parent_model = Model.resolve_parent(self.context['request'].path) except: parent_model = child_model + # remove objects from the list which I don't have permission to view filtered_values = list( filter(lambda v: Model.get_permission_classes(v, [LDPPermissions])[0]().has_object_permission( self.context['request'], self.context['view'], v), value)) diff --git a/djangoldp/views.py b/djangoldp/views.py index d9fc066fc58f4e6a4b5b4b82b6ea2a2106ed8b5e..8889220f557fe22240e3e56cd4658a4a2aea0511 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -24,6 +24,8 @@ from djangoldp.permissions import LDPPermissions get_user_model()._meta.rdf_context = {"get_full_name": "rdfs:label"} +# renders into JSONLD format by applying context to the data +# https://github.com/digitalbazaar/pyld class JSONLDRenderer(JSONRenderer): media_type = 'application/ld+json' @@ -38,15 +40,18 @@ class JSONLDRenderer(JSONRenderer): data["@context"] = settings.LDP_RDF_CONTEXT return super(JSONLDRenderer, self).render(data, accepted_media_type, renderer_context) - +# https://github.com/digitalbazaar/pyld class JSONLDParser(JSONParser): media_type = 'application/ld+json' def parse(self, stream, media_type=None, parser_context=None): data = super(JSONLDParser, self).parse(stream, media_type, parser_context) + # compact applies the context to the data and makes it a format which is easier to work with + # see: http://json-ld.org/spec/latest/json-ld/#compacted-document-form return jsonld.compact(data, ctx=settings.LDP_RDF_CONTEXT) +# an authentication class which exempts CSRF authentication class NoCSRFAuthentication(SessionAuthentication): def enforce_csrf(self, request): return @@ -81,6 +86,7 @@ class LDPViewSetGenerator(ModelViewSet): @classonlymethod def urls(cls, **kwargs): + '''constructs urls list for model passed in kwargs''' kwargs['model'] = cls.get_model(**kwargs) model_name = kwargs['model']._meta.object_name.lower() if kwargs.get('model_prefix'): @@ -93,12 +99,14 @@ class LDPViewSetGenerator(ModelViewSet): name='{}-detail'.format(model_name)), ] + # append nested fields to the urls list for field in kwargs.get('nested_fields') or cls.nested_fields: urls.append(url('^' + detail_expr + field + '/', LDPNestedViewSet.nested_urls(field, **kwargs))) return include(urls) +# LDPViewSetGenerator is a ModelViewSet (DRF) with methods to automatically generate model urls class LDPViewSet(LDPViewSetGenerator): """An automatically generated viewset that serves models following the Linked Data Platform convention""" fields = None @@ -109,6 +117,8 @@ class LDPViewSet(LDPViewSetGenerator): def __init__(self, **kwargs): super().__init__(**kwargs) + # attach filter backends based on permissions classes, to reduce the queryset based on these permissions + # https://www.django-rest-framework.org/api-guide/filtering/#generic-filtering if self.permission_classes: for p in self.permission_classes: if hasattr(p, 'filter_class') and p.filter_class: @@ -136,11 +146,13 @@ class LDPViewSet(LDPViewSetGenerator): return self.build_serializer(meta_args, 'Write') def build_serializer(self, meta_args, name_prefix): + # create the Meta class to associate to LDPSerializer, using meta_args param if self.fields: meta_args['fields'] = self.fields else: meta_args['exclude'] = self.exclude or () meta_class = type('Meta', (), meta_args) + from djangoldp.serializers import LDPSerializer return type(LDPSerializer)(self.model._meta.object_name.lower() + name_prefix + 'Serializer', (LDPSerializer,), {'Meta': meta_class}) @@ -148,6 +160,7 @@ class LDPViewSet(LDPViewSetGenerator): def create(self, request, *args, **kwargs): serializer = self.get_write_serializer(data=request.data) serializer.is_valid(raise_exception=True) + self.perform_create(serializer) response_serializer = self.get_serializer() data = response_serializer.to_representation(serializer.instance) @@ -210,6 +223,7 @@ class LDPViewSet(LDPViewSetGenerator): return super(LDPView, self).get_queryset(*args, **kwargs) def dispatch(self, request, *args, **kwargs): + '''overriden dispatch method to append some custom headers''' response = super(LDPViewSet, self).dispatch(request, *args, **kwargs) response["Access-Control-Allow-Origin"] = request.META.get('HTTP_ORIGIN') response["Access-Control-Allow-Methods"] = "GET,POST,PUT,PATCH,DELETE"