From 417cd5da04c1d74dc52f9f72e6b1ea00c33654a4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 11:15:03 +0100 Subject: [PATCH] Improve REST API with filters --- apps/activity/api/views.py | 9 ++++++- apps/api/urls.py | 5 ++++ apps/logs/api/views.py | 3 +++ apps/logs/signals.py | 2 +- apps/logs/urls.py | 8 ------- apps/member/api/views.py | 5 ++++ apps/note/api/serializers.py | 22 ++++++++++++++++- apps/note/api/urls.py | 3 +-- apps/note/api/views.py | 24 +++++++++---------- apps/note/fixtures/initial.json | 12 +++++----- note_kfet/settings/base.py | 6 +++-- note_kfet/urls.py | 3 +-- .../rest_framework/crispy_form.html | 5 ++++ .../django_filters/rest_framework/form.html | 6 +++++ .../django_filters/widgets/multiwidget.html | 1 + 15 files changed, 78 insertions(+), 36 deletions(-) delete mode 100644 apps/logs/urls.py create mode 100644 templates/django_filters/rest_framework/crispy_form.html create mode 100644 templates/django_filters/rest_framework/form.html create mode 100644 templates/django_filters/widgets/multiwidget.html diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 6a6c024e..4ee2194d 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -1,7 +1,8 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from rest_framework.filters import SearchFilter from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer from ..models import ActivityType, Activity, Guest @@ -15,6 +16,8 @@ class ActivityTypeViewSet(viewsets.ModelViewSet): """ queryset = ActivityType.objects.all() serializer_class = ActivityTypeSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['name', 'can_invite', ] class ActivityViewSet(viewsets.ModelViewSet): @@ -25,6 +28,8 @@ class ActivityViewSet(viewsets.ModelViewSet): """ queryset = Activity.objects.all() serializer_class = ActivitySerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['name', 'description', 'activity_type', ] class GuestViewSet(viewsets.ModelViewSet): @@ -35,3 +40,5 @@ class GuestViewSet(viewsets.ModelViewSet): """ queryset = Guest.objects.all() serializer_class = GuestSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] diff --git a/apps/api/urls.py b/apps/api/urls.py index bb1fdce2..8e0f6415 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,7 +3,9 @@ from django.conf.urls import url, include from django.contrib.auth.models import User +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import routers, serializers, viewsets +from rest_framework.filters import SearchFilter from activity.api.urls import register_activity_urls from member.api.urls import register_members_urls from note.api.urls import register_note_urls @@ -33,6 +35,9 @@ class UserViewSet(viewsets.ModelViewSet): """ queryset = User.objects.all() serializer_class = UserSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] + search_fields = ['$username', '$first_name', '$last_name', ] # Routers provide an easy way of automatically determining the URL conf. diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py index 60da612b..5b1b3ff6 100644 --- a/apps/logs/api/views.py +++ b/apps/logs/api/views.py @@ -1,6 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets from .serializers import ChangelogSerializer @@ -15,3 +16,5 @@ class ChangelogViewSet(viewsets.ModelViewSet): """ queryset = Changelog.objects.all() serializer_class = ChangelogSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip',] diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 41f87cda..ab196291 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -81,7 +81,7 @@ def save_object(sender, instance, **kwargs): model = instance.__class__ fields = '__all__' - previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") + previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else None instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8") if previous_json == instance_json: diff --git a/apps/logs/urls.py b/apps/logs/urls.py deleted file mode 100644 index 6d76674c..00000000 --- a/apps/logs/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -app_name = 'logs' - -# TODO User interface -urlpatterns = [ -] diff --git a/apps/member/api/views.py b/apps/member/api/views.py index 7e7dcd1d..c85df903 100644 --- a/apps/member/api/views.py +++ b/apps/member/api/views.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from rest_framework import viewsets +from rest_framework.filters import SearchFilter from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer from ..models import Profile, Club, Role, Membership @@ -25,6 +26,8 @@ class ClubViewSet(viewsets.ModelViewSet): """ queryset = Club.objects.all() serializer_class = ClubSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] class RoleViewSet(viewsets.ModelViewSet): @@ -35,6 +38,8 @@ class RoleViewSet(viewsets.ModelViewSet): """ queryset = Role.objects.all() serializer_class = RoleSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] class MembershipViewSet(viewsets.ModelViewSet): diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 61257ec4..73beead1 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -5,7 +5,8 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ + TemplateTransaction class NoteSerializer(serializers.ModelSerializer): @@ -111,6 +112,17 @@ class TransactionSerializer(serializers.ModelSerializer): fields = '__all__' +class TemplateTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Transactions. + The djangorestframework plugin will analyse the model `TemplateTransaction` and parse all fields in the API. + """ + + class Meta: + model = TemplateTransaction + fields = '__all__' + + class MembershipTransactionSerializer(serializers.ModelSerializer): """ REST API Serializer for Membership transactions. @@ -120,3 +132,11 @@ class MembershipTransactionSerializer(serializers.ModelSerializer): class Meta: model = MembershipTransaction fields = '__all__' + + +class TransactionPolymorphicSerializer(PolymorphicSerializer): + model_serializer_mapping = { + Transaction: TransactionSerializer, + TemplateTransaction: TemplateTransactionSerializer, + MembershipTransaction: MembershipTransactionSerializer, + } diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 5e176ec5..796a397f 100644 --- a/apps/note/api/urls.py +++ b/apps/note/api/urls.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from .views import NotePolymorphicViewSet, AliasViewSet, \ - TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet + TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet def register_note_urls(router, path): @@ -15,4 +15,3 @@ def register_note_urls(router, path): router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/transaction', TransactionViewSet) router.register(path + '/transaction/template', TransactionTemplateViewSet) - router.register(path + '/transaction/membership', MembershipTransactionViewSet) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 4fbb9481..14f64003 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -2,13 +2,15 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db.models import Q +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from rest_framework.filters import SearchFilter from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ NoteUserSerializer, AliasSerializer, \ - TemplateCategorySerializer, TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer + TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory +from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory class NoteViewSet(viewsets.ModelViewSet): @@ -139,6 +141,8 @@ class TemplateCategoryViewSet(viewsets.ModelViewSet): """ queryset = TemplateCategory.objects.all() serializer_class = TemplateCategorySerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] class TransactionTemplateViewSet(viewsets.ModelViewSet): @@ -149,6 +153,8 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet): """ queryset = TransactionTemplate.objects.all() serializer_class = TransactionTemplateSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['name', 'amount', 'display', 'category', ] class TransactionViewSet(viewsets.ModelViewSet): @@ -158,14 +164,6 @@ class TransactionViewSet(viewsets.ModelViewSet): then render it on /api/note/transaction/transaction/ """ queryset = Transaction.objects.all() - serializer_class = TransactionSerializer - - -class MembershipTransactionViewSet(viewsets.ModelViewSet): - """ - REST API View set. - The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer, - then render it on /api/note/transaction/membership/ - """ - queryset = MembershipTransaction.objects.all() - serializer_class = MembershipTransactionSerializer + serializer_class = TransactionPolymorphicSerializer + filter_backends = [SearchFilter] + search_fields = ['$reason', ] diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 01242b22..3654fa2f 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -3,7 +3,7 @@ "model": "note.note", "pk": 1, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -14,7 +14,7 @@ "model": "note.note", "pk": 2, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -25,7 +25,7 @@ "model": "note.note", "pk": 3, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -36,7 +36,7 @@ "model": "note.note", "pk": 4, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -47,7 +47,7 @@ "model": "note.note", "pk": 5, "fields": { - "polymorphic_ctype": 36, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", @@ -58,7 +58,7 @@ "model": "note.note", "pk": 6, "fields": { - "polymorphic_ctype": 36, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 84f07b6c..0694390d 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -137,12 +137,14 @@ REST_FRAMEWORK = { # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ # TODO Maybe replace it with our custom permissions system - 'rest_framework.permissions.DjangoModelPermissions' + 'rest_framework.permissions.DjangoModelPermissions', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', - ] + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, } # Internationalization diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 407659f8..da2f9d6c 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -20,8 +20,7 @@ urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), - path('logs/', include('logs.urls')), - path('api/', include('api.urls')), + path('api/', include('api.urls')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/templates/django_filters/rest_framework/crispy_form.html b/templates/django_filters/rest_framework/crispy_form.html new file mode 100644 index 00000000..171767c0 --- /dev/null +++ b/templates/django_filters/rest_framework/crispy_form.html @@ -0,0 +1,5 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +

{% trans "Field filters" %}

+{% crispy filter.form %} diff --git a/templates/django_filters/rest_framework/form.html b/templates/django_filters/rest_framework/form.html new file mode 100644 index 00000000..b116e353 --- /dev/null +++ b/templates/django_filters/rest_framework/form.html @@ -0,0 +1,6 @@ +{% load i18n %} +

{% trans "Field filters" %}

+
+ {{ filter.form.as_p }} + +
diff --git a/templates/django_filters/widgets/multiwidget.html b/templates/django_filters/widgets/multiwidget.html new file mode 100644 index 00000000..089ddb20 --- /dev/null +++ b/templates/django_filters/widgets/multiwidget.html @@ -0,0 +1 @@ +{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}