From 591f9b9a1d40af46183bc884079d12bff1c339f4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 27 Oct 2020 17:12:48 +0100 Subject: [PATCH 1/6] Added drf-spectacular. fix #2071 --- modoboa/admin/api.py | 11 ++-- modoboa/admin/serializers.py | 3 + .../core/commands/templates/settings.py.tpl | 7 +++ modoboa/core/viewsets.py | 5 ++ modoboa/templates/swagger-ui.html | 57 ------------------- modoboa/urls.py | 18 ++---- requirements.txt | 1 + 7 files changed, 26 insertions(+), 76 deletions(-) delete mode 100644 modoboa/templates/swagger-ui.html diff --git a/modoboa/admin/api.py b/modoboa/admin/api.py index 2c7ee274c..e9a334758 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api.py @@ -18,14 +18,13 @@ from . import lib, models, serializers class DomainViewSet(viewsets.ModelViewSet): """ - retrieve: - Return the given domain. + Domain viewset. - list: - Return a list of all existing domains. + retrieve: Return the given domain. - create: - Create a new domain instance. + list: Return a list of all existing domains. + + create: Create a new domain instance. """ permission_classes = [IsAuthenticated, DjangoModelPermissions, ] diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index 8a68d184e..2a66a020e 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -7,6 +7,8 @@ from django.shortcuts import get_object_or_404 from django.utils.encoding import force_text from django.utils.translation import ugettext as _, ugettext_lazy +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from modoboa.admin import models as admin_models @@ -186,6 +188,7 @@ class AccountSerializer(serializers.ModelSerializer): if not user.is_superuser: del self.fields["master_user"] + @extend_schema_field(OpenApiTypes.STR) def get_role(self, account): """Return role.""" return account.role diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index c604ca92b..243658309 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -64,6 +64,7 @@ INSTALLED_APPS = ( 'ckeditor_uploader', 'rest_framework', 'rest_framework.authtoken', + 'drf_spectacular', 'phonenumber_field', 'django_otp', 'django_otp.plugins.otp_totp', @@ -192,6 +193,12 @@ REST_FRAMEWORK = { ), } +SPECTACULAR_SETTINGS = { + 'SCHEMA_PATH_PREFIX': r'/api/v1', + 'TITLE': 'Modoboa API', + 'VERSION': '1.0.0' +} + # Modoboa settings # MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") diff --git a/modoboa/core/viewsets.py b/modoboa/core/viewsets.py index 177160688..1014c2e4d 100644 --- a/modoboa/core/viewsets.py +++ b/modoboa/core/viewsets.py @@ -7,6 +7,7 @@ from django_otp.plugins.otp_static.models import StaticDevice, StaticToken from django_otp.plugins.otp_totp.models import TOTPDevice from rest_framework import permissions, response, viewsets from rest_framework.decorators import action +from drf_spectacular.utils import extend_schema from . import serializers @@ -28,6 +29,9 @@ class AccountViewSet(viewsets.ViewSet): ) return response.Response() + @extend_schema( + request=serializers.CheckTFASetupSerializer + ) @action(methods=["post"], detail=False, url_path="tfa/setup/check") def tfa_setup_check(self, request): """Check TFA setup.""" @@ -57,6 +61,7 @@ class AccountViewSet(viewsets.ViewSet): request.user.save() return response.Response() + @extend_schema(tags=['account']) @action(methods=["post"], detail=False, url_path="tfa/reset_codes") def tfa_reset_codes(self, request, *args, **kwargs): """Reset recovery codes.""" diff --git a/modoboa/templates/swagger-ui.html b/modoboa/templates/swagger-ui.html deleted file mode 100644 index efbd02f35..000000000 --- a/modoboa/templates/swagger-ui.html +++ /dev/null @@ -1,57 +0,0 @@ - -{% load static %} - - - - - Modoboa API - - - - -
- - - - diff --git a/modoboa/urls.py b/modoboa/urls.py index d5a5390de..5e9e1deda 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -6,11 +6,9 @@ from django.conf import settings from django.contrib.auth import views as auth_views from django.contrib.auth.decorators import login_required from django.urls import include, path -from django.views.generic import TemplateView from django.views.i18n import JavaScriptCatalog -from rest_framework.renderers import JSONOpenAPIRenderer -from rest_framework.schemas import get_schema_view +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from modoboa.admin.views import user as user_views from modoboa.core import signals as core_signals, views as core_views @@ -59,17 +57,11 @@ if extra_routes: urlpatterns += extra_routes # API urls -schema_view = get_schema_view( - title="Modoboa API", - version="1.0.0", - public=False, - renderer_classes=[JSONOpenAPIRenderer], -) urlpatterns += [ - path('docs/openapi.json', schema_view, name="openapi_schema"), - path('docs/api/', login_required( - TemplateView.as_view(template_name="swagger-ui.html")), - name="docs-index"), + path('docs/openapi.json', SpectacularAPIView.as_view(), name='schema'), + path('docs/api/', + SpectacularSwaggerView.as_view(url_name='schema'), + name='docs-index'), path('api/v1/', include("modoboa.urls_api")), ] diff --git a/requirements.txt b/requirements.txt index 8242a5aea..485d9cf23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ django-otp==1.0.2 dj-database-url djangorestframework>=3.11,<3.12 +drf-spectacular uritemplate passlib~=1.7.4 From 78a772eaf182c6c16d773922695199f11d98d438 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Nov 2020 21:20:18 +0100 Subject: [PATCH 2/6] Added description for domain viewset routes. --- modoboa/admin/api.py | 25 ++++++++++++++++--------- modoboa/urls.py | 6 +++++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/modoboa/admin/api.py b/modoboa/admin/api.py index e9a334758..7a1b04e6c 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api.py @@ -4,6 +4,7 @@ from django import http from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ +from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import filters, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ParseError @@ -16,16 +17,22 @@ from modoboa.core import sms_backends from . import lib, models, serializers +@extend_schema_view( + retrieve=extend_schema( + description="Retrieve a particular domain", + summary="Retrieve a particular domain" + ), + list=extend_schema( + description="Retrieve a list of domains", + summary="Retrieve a list of domains" + ), + create=extend_schema( + description="Create a new domain", + summary="Create a new domain" + ) +) class DomainViewSet(viewsets.ModelViewSet): - """ - Domain viewset. - - retrieve: Return the given domain. - - list: Return a list of all existing domains. - - create: Create a new domain instance. - """ + """Domain viewset.""" permission_classes = [IsAuthenticated, DjangoModelPermissions, ] serializer_class = serializers.DomainSerializer diff --git a/modoboa/urls.py b/modoboa/urls.py index 5e9e1deda..3198d8844 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -8,7 +8,9 @@ from django.contrib.auth.decorators import login_required from django.urls import include, path from django.views.i18n import JavaScriptCatalog -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from drf_spectacular.views import ( + SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView +) from modoboa.admin.views import user as user_views from modoboa.core import signals as core_signals, views as core_views @@ -62,6 +64,8 @@ urlpatterns += [ path('docs/api/', SpectacularSwaggerView.as_view(url_name='schema'), name='docs-index'), + path('docs/api/redoc/', SpectacularRedocView.as_view(url_name='schema'), + name='redoc'), path('api/v1/', include("modoboa.urls_api")), ] From a15a35a62589c8d1b25c4c964508fa077ed70df3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Nov 2020 21:22:19 +0100 Subject: [PATCH 3/6] Added missing setting. --- modoboa/core/commands/templates/settings.py.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 243658309..a404ca01d 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -191,6 +191,7 @@ REST_FRAMEWORK = { 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } SPECTACULAR_SETTINGS = { From e4f5a25e64da86239bb083b1588f235f3a9f3d23 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Nov 2020 21:56:48 +0100 Subject: [PATCH 4/6] Added missing test config. --- test_project/test_project/settings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index bd94c34a7..29f83a8ff 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -63,6 +63,7 @@ INSTALLED_APPS = ( 'ckeditor_uploader', 'rest_framework', 'rest_framework.authtoken', + 'drf_spectacular', 'django_otp', 'django_otp.plugins.otp_totp', 'django_otp.plugins.otp_static', @@ -184,6 +185,13 @@ REST_FRAMEWORK = { 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'SCHEMA_PATH_PREFIX': r'/api/v1', + 'TITLE': 'Modoboa API', + 'VERSION': '1.0.0' } # Modoboa settings From b9720c1837cebfea845860d36a99d32e268d0bd2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 10 Nov 2020 16:07:34 +0100 Subject: [PATCH 5/6] Fixed unit tests. --- modoboa/core/commands/templates/settings.py.tpl | 3 ++- modoboa/core/tests/test_api.py | 11 ++--------- test_project/test_project/settings.py | 3 ++- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index a404ca01d..cab8a30e3 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -197,7 +197,8 @@ REST_FRAMEWORK = { SPECTACULAR_SETTINGS = { 'SCHEMA_PATH_PREFIX': r'/api/v1', 'TITLE': 'Modoboa API', - 'VERSION': '1.0.0' + 'VERSION': '1.0.0', + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], } # Modoboa settings diff --git a/modoboa/core/tests/test_api.py b/modoboa/core/tests/test_api.py index ef1fe5b77..043222304 100644 --- a/modoboa/core/tests/test_api.py +++ b/modoboa/core/tests/test_api.py @@ -4,16 +4,13 @@ from modoboa.lib.tests import ModoTestCase class OpenAPITestCase(ModoTestCase): - openapi_schema_url = reverse_lazy('openapi_schema') + openapi_schema_url = reverse_lazy('schema') def test_unauthorized(self): self.client.logout() - response = self.client.get("/docs/api/") - self.assertRedirects(response, "/accounts/login/?next=/docs/api/") - response = self.client.get(self.openapi_schema_url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 401) def test_get_schema(self): self.assertEqual(self.openapi_schema_url, "/docs/openapi.json") @@ -24,7 +21,3 @@ class OpenAPITestCase(ModoTestCase): 'title': "Modoboa API", 'version': "1.0.0", }) - - def test_get_swagger_ui(self): - response = self.client.get("/docs/api/") - self.assertContains(response, "Modoboa API") diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 29f83a8ff..a2579f81b 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -191,7 +191,8 @@ REST_FRAMEWORK = { SPECTACULAR_SETTINGS = { 'SCHEMA_PATH_PREFIX': r'/api/v1', 'TITLE': 'Modoboa API', - 'VERSION': '1.0.0' + 'VERSION': '1.0.0', + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], } # Modoboa settings From 304ee7e8fb3fc75650d93eae39008a00d4a18eb9 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 10 Nov 2020 16:25:22 +0100 Subject: [PATCH 6/6] Fixed unit test. --- modoboa/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modoboa/urls.py b/modoboa/urls.py index 3198d8844..8c3813902 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -9,7 +9,7 @@ from django.urls import include, path from django.views.i18n import JavaScriptCatalog from drf_spectacular.views import ( - SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView + SpectacularJSONAPIView, SpectacularSwaggerView, SpectacularRedocView ) from modoboa.admin.views import user as user_views @@ -60,7 +60,7 @@ if extra_routes: # API urls urlpatterns += [ - path('docs/openapi.json', SpectacularAPIView.as_view(), name='schema'), + path('docs/openapi.json', SpectacularJSONAPIView.as_view(), name='schema'), path('docs/api/', SpectacularSwaggerView.as_view(url_name='schema'), name='docs-index'),