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.
This commit is contained in:
Aurélien Delobelle 2017-08-03 12:14:53 +02:00
parent 07d4c3ead1
commit fc4930a49e
4 changed files with 157 additions and 135 deletions

33
api/event/fields.py Normal file
View file

@ -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

View file

@ -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 rest_framework import serializers
from event.models import Event, ActivityTag, Place, ActivityTemplate from event.models import Event, ActivityTag, Place, ActivityTemplate
from .fields import EventHyperlinkedIdentityField
# Event Serializer # 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_by = serializers.ReadOnlyField(source='created_by.get_full_name')
created_at = serializers.ReadOnlyField()
class Meta: class Meta:
model = Event model = Event
fields = ('url', 'id', 'title', 'slug', 'created_by', 'created_at', fields = (
'description', 'beginning_date', 'ending_date') '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
# Serializers # Serializers
# TODO rajouter des permissions # TODO rajouter des permissions
class PlaceSerializer(EventSpecificSerializerMixin, class PlaceSerializer(serializers.ModelSerializer):
serializers.ModelSerializer): serializer_url_field = EventHyperlinkedIdentityField
class Meta: class Meta:
model = Place model = Place
@ -55,8 +32,9 @@ class PlaceSerializer(EventSpecificSerializerMixin,
# TODO rajouter des permissions # TODO rajouter des permissions
class ActivityTagSerializer(EventSpecificSerializerMixin, class ActivityTagSerializer(serializers.ModelSerializer):
serializers.ModelSerializer): serializer_url_field = EventHyperlinkedIdentityField
class Meta: class Meta:
model = ActivityTag model = ActivityTag
fields = ('url', 'id', 'name', 'is_public', 'color', 'event') fields = ('url', 'id', 'name', 'is_public', 'color', 'event')
@ -64,36 +42,30 @@ class ActivityTagSerializer(EventSpecificSerializerMixin,
# TODO rajouter des permissions # TODO rajouter des permissions
class ActivityTemplateSerializer(serializers.ModelSerializer): class ActivityTemplateSerializer(serializers.ModelSerializer):
event = EventSerializer(read_only=True)
tags = ActivityTagSerializer(many=True) tags = ActivityTagSerializer(many=True)
serializer_url_field = EventHyperlinkedIdentityField
class Meta: class Meta:
model = ActivityTemplate model = ActivityTemplate
fields = ('id', 'title', 'event', 'is_public', 'has_perm', fields = (
'min_perm', 'max_perm', 'description', 'remarks', 'tags',) 'url', 'id', 'title', 'event', 'is_public', 'has_perm', 'min_perm',
'max_perm', 'description', 'remarks', 'tags',
)
def update(self, instance, validated_data): def process_tags(self, instance, tags_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()
# TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag # 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 # 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 # 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 # été modifié entre temps dans la base de donnée (mais pas sur la
# classe backbone # classe backbone
tags = []
for tag_data in tags_data: for tag_data in tags_data:
tag_data.pop('event', None) tag, _ = ActivityTag.objects.get_or_create(**tag_data, defaults={
tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] 'event': instance.event,
for tag_data in tags_data] })
instance.tags.set(tags) tags.append(tag)
return instance instance.tags.add(*tags)
def create(self, validated_data): 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éé les autres champs et si l'id n'existe pas on le créé
""" """
tags_data = validated_data.pop('tags') tags_data = validated_data.pop('tags')
event_pk = validated_data.pop('event_pk') activity_template = super().create(validated_data)
event = event_pk and get_object_or_404(Event, id=event_pk) or None self.process_tags(activity_template, tags_data)
activity_template = ActivityTemplate.objects.create(event=event, return activity_template
**validated_data)
# TODO: en fonction de si backbone envoie un `id` ou non lorsque le tag def update(self, instance, validated_data):
# 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 @tags comportement attendu : si l'id existe déjà on ne change pas
# été modifié entre temps dans la base de donnée (mais pas sur la les autres champs et si l'id n'existe pas on le créé
# classe backbone """
for tag_data in tags_data: tags_data = validated_data.pop('tags')
tag_data.pop('event', None) activity_template = super().update(instance, validated_data)
tags = [ActivityTag.objects.get_or_create(event=event, **tag_data)[0] self.process_tags(activity_template, tags_data)
for tag_data in tags_data]
activity_template.tags = tags
return activity_template return activity_template

View file

@ -1,17 +1,54 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models import Q 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.viewsets import ModelViewSet
from rest_framework.filters import OrderingFilter
from api.event.serializers import EventSerializer, PlaceSerializer,\ from event.models import Activity, ActivityTag, ActivityTemplate, Event, Place
ActivityTagSerializer, ActivityTemplateSerializer, ActivitySerializer
from event.models import Event, Place, ActivityTag, ActivityTemplate, Activity from .serializers import (
ActivitySerializer, ActivityTagSerializer, ActivityTemplateSerializer,
EventSerializer, PlaceSerializer,
)
User = get_user_model() User = get_user_model()
# classes utilitaires # 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 : ViewSet that returns :
* rootlevel objects if no Event is specified * rootlevel objects if no Event is specified
@ -21,81 +58,64 @@ class EventSpecificViewSet(ModelViewSet):
to the save method. Works fine with serializers.EventSpecificSerializer to the save method. Works fine with serializers.EventSpecificSerializer
Useful for models that extends EventSpecificMixin Useful for models that extends EventSpecificMixin
""" """
def get_queryset(self): def get_queryset(self):
""" """
Warning : You may want to override this method Warning : You may want to override this method
and not call with super and not call with super
""" """
event_pk = self.kwargs.get('event_pk')
queryset = super().get_queryset() queryset = super().get_queryset()
if event_pk: filters = Q(event=None)
return queryset.filter(Q(event=event_pk) | Q(event=None)) if self.event:
return queryset.filter(event=None) filters |= Q(event=self.event)
return queryset.filter(filters)
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)
# ViewSets # ViewSets
class EventViewSet(ModelViewSet): class EventViewSet(ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
"""
queryset = Event.objects.all() queryset = Event.objects.all()
serializer_class = EventSerializer serializer_class = EventSerializer
filter_backends = (OrderingFilter,)
ordering_fields = ('title', 'creation_date', 'beginning_date',
'ending_date', )
ordering = ('beginning_date', )
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(created_by=self.request.user) serializer.save(created_by=self.request.user)
class PlaceViewSet(EventSpecificViewSet): class PlaceViewSet(EventSpecificModelViewSet):
queryset = Place.objects.all() queryset = Place.objects.all()
serializer_class = PlaceSerializer serializer_class = PlaceSerializer
filter_backends = (OrderingFilter,)
ordering_fields = ('name', )
ordering = ('name', )
class ActivityTagViewSet(EventSpecificViewSet):
class ActivityTagViewSet(EventSpecificModelViewSet):
queryset = ActivityTag.objects.all() queryset = ActivityTag.objects.all()
serializer_class = ActivityTagSerializer 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() queryset = ActivityTemplate.objects.all()
serializer_class = ActivityTemplateSerializer serializer_class = ActivityTemplateSerializer
def perform_create(self, serializer): filter_backends = (OrderingFilter,)
event_pk = self.kwargs.get('event_pk') ordering_fields = ('title', )
serializer.save(event_pk=event_pk) ordering = ('title', )
def perform_update(self, serializer):
event_pk = self.kwargs.get('event_pk')
serializer.save(event_pk=event_pk)
class ActivityViewSet(ModelViewSet): class ActivityViewSet(EventModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
"""
queryset = Activity.objects.all() queryset = Activity.objects.all()
serializer_class = ActivitySerializer serializer_class = ActivitySerializer
def perform_create(self, serializer): filter_backends = (OrderingFilter,)
event_pk = self.kwargs.get('event_pk') ordering_fields = ('title', )
serializer.save(event_pk=event_pk) ordering = ('title', )
def perform_update(self, serializer):
event_pk = self.kwargs.get('event_pk')
serializer.save(event_pk=event_pk)

View file

@ -1,27 +1,26 @@
from django.conf.urls import url, include from django.conf.urls import url, include
from rest_framework_nested.routers import SimpleRouter, NestedSimpleRouter 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 = SimpleRouter()
router.register(r'event', EventViewSet, 'event') router.register(r'event', views.EventViewSet)
router.register(r'place', PlaceViewSet, 'place') router.register(r'place', views.PlaceViewSet)
router.register(r'activitytag', ActivityTagViewSet, 'activitytag') router.register(r'tag', views.ActivityTagViewSet)
# Register nested router and register someviewsets vith it
# Views behind /event/<event_pk>/...
event_router = NestedSimpleRouter(router, r'event', lookup='event') event_router = NestedSimpleRouter(router, r'event', lookup='event')
event_router.register(r'place', PlaceViewSet, base_name='event-place') event_router.register(r'place', views.PlaceViewSet)
event_router.register(r'tag', ActivityTagViewSet, base_name='event-activitytag') event_router.register(r'tag', views.ActivityTagViewSet)
event_router.register(r'activitytemplate', ActivityTemplateViewSet, event_router.register(r'template', views.ActivityTemplateViewSet)
base_name='event-activitytemplate')
# The API URLs are now determined automatically by the router. # API URLconf: routers + auth for browsable API.
# Additionally, we include the login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^', include(event_router.urls)), url(r'^', include(event_router.urls)),
url(r'^api-auth/', include('rest_framework.urls', url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
namespace='rest_framework'))
] ]