From fc4930a49eb8cbf12255068b1b0207b29f0a0945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 3 Aug 2017 12:14:53 +0200 Subject: [PATCH] Clean serializers and viewsets Event-based urls - Add viewset mixin 'EventUrlViewSetMixin' to get the event from the 'event_pk' url kwarg of a view. - Add url serializer fields for object which can be accessed with a root-level and/or event-specific url ('EventHyperlinked*Field). Update viewsets and serializers to use these + clean inheritance viewsets. --- api/event/fields.py | 33 +++++++++++ api/event/serializers.py | 114 ++++++++++++++------------------------ api/event/views.py | 116 +++++++++++++++++++++++---------------- api/urls.py | 29 +++++----- 4 files changed, 157 insertions(+), 135 deletions(-) create mode 100644 api/event/fields.py diff --git a/api/event/fields.py b/api/event/fields.py new file mode 100644 index 0000000..4df025c --- /dev/null +++ b/api/event/fields.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from rest_framework.reverse import reverse + + +class EventHyperlinkedFieldMixin: + + def get_url(self, obj, view_name, request, format): + url_kwargs = {'pk': obj.pk} + if getattr(obj, 'event', None): + url_kwargs['event_pk'] = obj.event.pk + return reverse( + view_name, kwargs=url_kwargs, request=request, format=format) + + def get_object(self, view_name, view_args, view_kwargs): + lookup_kwargs = { + 'pk': view_kwargs['pk'], + 'event_id': view_kwargs.get('event_pk'), + } + return self.get_queryset().get(**lookup_kwargs) + + +class EventHyperlinkedRelatedField( + EventHyperlinkedFieldMixin, + serializers.HyperlinkedRelatedField, + ): + pass + + +class EventHyperlinkedIdentityField( + EventHyperlinkedFieldMixin, + serializers.HyperlinkedIdentityField + ): + pass diff --git a/api/event/serializers.py b/api/event/serializers.py index 44461e6..a7ff849 100644 --- a/api/event/serializers.py +++ b/api/event/serializers.py @@ -1,53 +1,30 @@ -from django.shortcuts import get_object_or_404 +from django.db import transaction +from django.utils.decorators import method_decorator + from rest_framework import serializers + from event.models import Event, ActivityTag, Place, ActivityTemplate +from .fields import EventHyperlinkedIdentityField + # Event Serializer -class EventSerializer(serializers.HyperlinkedModelSerializer): +class EventSerializer(serializers.ModelSerializer): + # TODO: Change this to a nested serializer ~(url, full_name) of User created_by = serializers.ReadOnlyField(source='created_by.get_full_name') - created_at = serializers.ReadOnlyField() class Meta: model = Event - fields = ('url', 'id', 'title', 'slug', 'created_by', 'created_at', - 'description', 'beginning_date', 'ending_date') - - -# Classes utilitaires -class EventSpecificSerializerMixin(): - """ - Provide `update` and `create` methods for nested view with an Event - For example for Models which extends EventSpecificMixin - the event id has to be provided in the `save` method - Works fine with view.EventSpecificViewSet - Also provides : - event = eventserializer(allow_null=true, read_only=true) - """ - event = EventSerializer(allow_null=True, read_only=True) - - def update(self, instance, validated_data): - """ - Note : does NOT change the event value of the instance - """ - validated_data.pop('event_pk') - [setattr(instance, key, value) - for key, value in validated_data.items()] - instance.save() - return instance - - def create(self, validated_data): - ModelClass = self.Meta.model - event_pk = validated_data.pop('event_pk', None) - event = event_pk and get_object_or_404(Event, id=event_pk) or None - instance = ModelClass.objects.create(event=event, **validated_data) - return instance + fields = ( + 'url', 'id', 'title', 'slug', 'created_by', 'created_at', + 'description', 'beginning_date', 'ending_date', + ) # Serializers # TODO rajouter des permissions -class PlaceSerializer(EventSpecificSerializerMixin, - serializers.ModelSerializer): +class PlaceSerializer(serializers.ModelSerializer): + serializer_url_field = EventHyperlinkedIdentityField class Meta: model = Place @@ -55,8 +32,9 @@ class PlaceSerializer(EventSpecificSerializerMixin, # TODO rajouter des permissions -class ActivityTagSerializer(EventSpecificSerializerMixin, - serializers.ModelSerializer): +class ActivityTagSerializer(serializers.ModelSerializer): + serializer_url_field = EventHyperlinkedIdentityField + class Meta: model = ActivityTag fields = ('url', 'id', 'name', 'is_public', 'color', 'event') @@ -64,36 +42,30 @@ class ActivityTagSerializer(EventSpecificSerializerMixin, # TODO rajouter des permissions class ActivityTemplateSerializer(serializers.ModelSerializer): - event = EventSerializer(read_only=True) tags = ActivityTagSerializer(many=True) + serializer_url_field = EventHyperlinkedIdentityField + class Meta: model = ActivityTemplate - fields = ('id', 'title', 'event', 'is_public', 'has_perm', - 'min_perm', 'max_perm', 'description', 'remarks', 'tags',) + fields = ( + 'url', 'id', 'title', 'event', 'is_public', 'has_perm', 'min_perm', + 'max_perm', 'description', 'remarks', 'tags', + ) - def update(self, instance, validated_data): - """ - @tags comportement attendu : si l'id existe déjà on ne change pas - les autres champs et si l'id n'existe pas on le créé - """ - tags_data = validated_data.pop('tags') - validated_data.pop('event_pk') - event = instance.event - [setattr(instance, key, value) - for key, value in validated_data.items()] - instance.save() + def process_tags(self, instance, tags_data): # TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag # n'existe pas encore il faudra faire un premier passage sur `tags` i # pour s'assurer que le get ne foire pas le get si, par exemple, le tag # été modifié entre temps dans la base de donnée (mais pas sur la # classe backbone + tags = [] for tag_data in tags_data: - tag_data.pop('event', None) - tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] - for tag_data in tags_data] - instance.tags.set(tags) - return instance + tag, _ = ActivityTag.objects.get_or_create(**tag_data, defaults={ + 'event': instance.event, + }) + tags.append(tag) + instance.tags.add(*tags) def create(self, validated_data): """ @@ -101,20 +73,18 @@ class ActivityTemplateSerializer(serializers.ModelSerializer): les autres champs et si l'id n'existe pas on le créé """ tags_data = validated_data.pop('tags') - event_pk = validated_data.pop('event_pk') - event = event_pk and get_object_or_404(Event, id=event_pk) or None - activity_template = ActivityTemplate.objects.create(event=event, - **validated_data) - # TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag - # n'existe pas encore il faudra faire un premier passage sur `tags` i - # pour s'assurer que le get ne foire pas le get si, par exemple, le tag - # été modifié entre temps dans la base de donnée (mais pas sur la - # classe backbone - for tag_data in tags_data: - tag_data.pop('event', None) - tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] - for tag_data in tags_data] - activity_template.tags = tags + activity_template = super().create(validated_data) + self.process_tags(activity_template, tags_data) + return activity_template + + def update(self, instance, validated_data): + """ + @tags comportement attendu : si l'id existe déjà on ne change pas + les autres champs et si l'id n'existe pas on le créé + """ + tags_data = validated_data.pop('tags') + activity_template = super().update(instance, validated_data) + self.process_tags(activity_template, tags_data) return activity_template diff --git a/api/event/views.py b/api/event/views.py index b360583..3497f3a 100644 --- a/api/event/views.py +++ b/api/event/views.py @@ -1,17 +1,54 @@ from django.contrib.auth import get_user_model from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django.utils.functional import cached_property from rest_framework.viewsets import ModelViewSet +from rest_framework.filters import OrderingFilter -from api.event.serializers import EventSerializer, PlaceSerializer,\ - ActivityTagSerializer, ActivityTemplateSerializer, ActivitySerializer -from event.models import Event, Place, ActivityTag, ActivityTemplate, Activity +from event.models import Activity, ActivityTag, ActivityTemplate, Event, Place + +from .serializers import ( + ActivitySerializer, ActivityTagSerializer, ActivityTemplateSerializer, + EventSerializer, PlaceSerializer, +) User = get_user_model() # classes utilitaires -class EventSpecificViewSet(ModelViewSet): + +class EventUrlViewSetMixin: + """ + ViewSet mixin to handle the evenk_pk from url. + """ + + @cached_property + def event(self): + event_pk = self.kwargs.get('event_pk') + if event_pk: + return get_object_or_404(Event, pk=event_pk) + return None + + +class EventModelViewSetMixin: + + def perform_create(self, serializer): + serializer.save(event=self.event) + + def perform_update(self, serializer): + serializer.save(event=self.event) + + +class EventModelViewSet( + EventModelViewSetMixin, + EventUrlViewSetMixin, + ModelViewSet, + ): + pass + + +class EventSpecificModelViewSet(EventModelViewSet): """ ViewSet that returns : * rootlevel objects if no Event is specified @@ -21,81 +58,64 @@ class EventSpecificViewSet(ModelViewSet): to the save method. Works fine with serializers.EventSpecificSerializer Useful for models that extends EventSpecificMixin """ + def get_queryset(self): """ Warning : You may want to override this method and not call with super """ - event_pk = self.kwargs.get('event_pk') queryset = super().get_queryset() - if event_pk: - return queryset.filter(Q(event=event_pk) | Q(event=None)) - return queryset.filter(event=None) - - def perform_create(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) - - def perform_update(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) + filters = Q(event=None) + if self.event: + filters |= Q(event=self.event) + return queryset.filter(filters) # ViewSets class EventViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - - """ queryset = Event.objects.all() serializer_class = EventSerializer + filter_backends = (OrderingFilter,) + ordering_fields = ('title', 'creation_date', 'beginning_date', + 'ending_date', ) + ordering = ('beginning_date', ) + def perform_create(self, serializer): serializer.save(created_by=self.request.user) -class PlaceViewSet(EventSpecificViewSet): +class PlaceViewSet(EventSpecificModelViewSet): queryset = Place.objects.all() serializer_class = PlaceSerializer + filter_backends = (OrderingFilter,) + ordering_fields = ('name', ) + ordering = ('name', ) -class ActivityTagViewSet(EventSpecificViewSet): + +class ActivityTagViewSet(EventSpecificModelViewSet): queryset = ActivityTag.objects.all() serializer_class = ActivityTagSerializer + filter_backends = (OrderingFilter,) + ordering_fields = ('is_public', 'name', ) + ordering = ('is_public', 'name', ) -class ActivityTemplateViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - """ +class ActivityTemplateViewSet(EventModelViewSet): queryset = ActivityTemplate.objects.all() serializer_class = ActivityTemplateSerializer - def perform_create(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) - - def perform_update(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) + filter_backends = (OrderingFilter,) + ordering_fields = ('title', ) + ordering = ('title', ) -class ActivityViewSet(ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - - """ +class ActivityViewSet(EventModelViewSet): queryset = Activity.objects.all() serializer_class = ActivitySerializer - def perform_create(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) - - def perform_update(self, serializer): - event_pk = self.kwargs.get('event_pk') - serializer.save(event_pk=event_pk) + filter_backends = (OrderingFilter,) + ordering_fields = ('title', ) + ordering = ('title', ) diff --git a/api/urls.py b/api/urls.py index 0409143..1a704a0 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,27 +1,26 @@ from django.conf.urls import url, include + from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter -from api.event.views import EventViewSet, PlaceViewSet, ActivityTagViewSet,\ - ActivityTemplateViewSet -# Create a router and register our viewsets with it. +from api.event import views + + router = SimpleRouter() -router.register(r'event', EventViewSet, 'event') -router.register(r'place', PlaceViewSet, 'place') -router.register(r'activitytag', ActivityTagViewSet, 'activitytag') +router.register(r'event', views.EventViewSet) +router.register(r'place', views.PlaceViewSet) +router.register(r'tag', views.ActivityTagViewSet) -# Register nested router and register someviewsets vith it + +# Views behind /event//... event_router = NestedSimpleRouter(router, r'event', lookup='event') -event_router.register(r'place', PlaceViewSet, base_name='event-place') -event_router.register(r'tag', ActivityTagViewSet, base_name='event-activitytag') -event_router.register(r'activitytemplate', ActivityTemplateViewSet, - base_name='event-activitytemplate') +event_router.register(r'place', views.PlaceViewSet) +event_router.register(r'tag', views.ActivityTagViewSet) +event_router.register(r'template', views.ActivityTemplateViewSet) -# The API URLs are now determined automatically by the router. -# Additionally, we include the login URLs for the browsable API. +# API URLconf: routers + auth for browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^', include(event_router.urls)), - url(r'^api-auth/', include('rest_framework.urls', - namespace='rest_framework')) + url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), ]