Skip to content

Commit

Permalink
add drf-spectacular support (#1508)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Feb 14, 2025
1 parent 22e7a1e commit fb630da
Show file tree
Hide file tree
Showing 14 changed files with 81 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/release_cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ TBA
- [ ] Update version number and date in `CHANGELOG`
- [ ] Update version number and date in `Major Changes` doc
- [ ] Ensure docs can be built without errors
- [ ] Ensure `generateschema` runs without errors or warnings (until in CI)
- [ ] Ensure `make spectacular` runs without errors or warnings (until in CI)

## Notes

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Unreleased
Added
-----

- **General**
- ``drf-spectacular`` support (#1508)
- **Projectroles**
- ``SODARUser.get_display_name()`` helper (#1487)
- App setting type constants (#1458)
Expand Down Expand Up @@ -104,6 +106,7 @@ Removed

- **General**
- Migrations squashed in v1.0 (#1455)
- DRF ``generateschema`` support (#1508)
- **Projectroles**
- Support for deprecated search results as dict (#1400)
- Support for deprecated app setting ``local`` parameter (#1394)
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ define USAGE=
@echo -e "\tmake collectstatic -- run collectstatic"
@echo -e "\tmake test [arg=<test_object>] -- run all tests or specify module/class/function"
@echo -e "\tmake manage_target arg=<target_command> -- run management command on target site, arg is mandatory"
@echo -e "\tmake spectacular -- generate OpenAPI schemas with drf-spectacular"
@echo -e
endef

Expand Down Expand Up @@ -67,6 +68,11 @@ else
endif


.PHONY: spectacular
spectacular:
$(MANAGE) spectacular --color $(arg)


.PHONY: usage
usage:
$(USAGE)
Expand Down
10 changes: 10 additions & 0 deletions config/drf_spectacular.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Config for drf-spectacular"""


def exclude_knox_hook(endpoints):
"""Exclude django-knox-auth endpoints from spectacular OpenAPI generation"""
filtered = []
for path, path_regex, method, callback in endpoints:
if not path.startswith('/api/auth/log'):
filtered.append((path, path_regex, method, callback))
return filtered
8 changes: 7 additions & 1 deletion config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
'dal', # For user search combo box
'dal_select2',
'dj_iconify.apps.DjIconifyConfig', # Iconify for SVG icons
'drf_spectacular', # OpenAPI schema generation
]

# Project apps
Expand Down Expand Up @@ -332,7 +333,6 @@
# Django REST framework
# ------------------------------------------------------------------------------


REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
Expand All @@ -343,8 +343,14 @@
'rest_framework.pagination.PageNumberPagination'
),
'PAGE_SIZE': SODAR_API_PAGE_SIZE,
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
'PREPROCESSING_HOOKS': ['config.drf_spectacular.exclude_knox_hook']
}


# Additional authentication settings
# ------------------------------------------------------------------------------

Expand Down
9 changes: 9 additions & 0 deletions docs/source/major_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Release Highlights
- Add app setting type constants
- Add app setting definition as objects
- Add API view to retrieve user details by user UUID
- Add drf-spectacular support for API documentation
- Update project list for flat list display
- Update owner transfer form to allow setting no role for old owner
- Update app settings API
Expand All @@ -37,6 +38,7 @@ Release Highlights
- Remove support for sodarcache REST API <v2.0
- Remove support for timeline REST API <v2.0
- Remove support for SODAR Core features deprecated in v1.0
- Remove support for generateschema
- Remove squashed migrations

Breaking Changes
Expand Down Expand Up @@ -157,6 +159,13 @@ App Settings Local Attribute
``local`` attribute is no longer supported. Instead, access the
``global_edit`` member of a ``PluginAppSettingDef`` object directly.

DRF-Spectacular Used for OpenAPI Schemas
----------------------------------------

This release adds support for ``drf-spectacular`` to generate OpenAPI schemas.
Use ``make spectacular`` to generate your schemas. Support for the DRF default
``generateschema`` command has been removed.

Squashed Migrations Removed
---------------------------

Expand Down
6 changes: 5 additions & 1 deletion filesfolders/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# NB: Creating abstract serializers is not easily possible as explained in the
# following StackOverflow post: https://stackoverflow.com/a/33137535

import uuid

from typing import Optional

from rest_framework import serializers
from rest_framework.generics import get_object_or_404

Expand All @@ -16,7 +20,7 @@
class FilesfoldersSerializerMixin:
"""Shared code that does not need metaprogramming."""

def get_folder(self, obj):
def get_folder(self, obj: File) -> Optional[uuid.UUID]:
if obj.folder:
return obj.folder.sodar_uuid
else:
Expand Down
7 changes: 0 additions & 7 deletions filesfolders/views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
GenericAPIView,
)
from rest_framework.renderers import JSONRenderer
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.versioning import AcceptHeaderVersioning

# Projectroles dependency
Expand Down Expand Up @@ -198,7 +197,6 @@ class FolderListCreateAPIView(

pagination_class = SODARPageNumberPagination
project_type = PROJECT_TYPE_PROJECT
schema = AutoSchema(operation_id_base='ListCreateFolder')
serializer_class = FolderSerializer


Expand Down Expand Up @@ -228,7 +226,6 @@ class FolderRetrieveUpdateDestroyAPIView(
lookup_field = 'sodar_uuid'
lookup_url_kwarg = 'folder'
project_type = PROJECT_TYPE_PROJECT
schema = AutoSchema(operation_id_base='UpdateDestroyFolder')
serializer_class = FolderSerializer


Expand Down Expand Up @@ -268,7 +265,6 @@ class FileListCreateAPIView(

pagination_class = SODARPageNumberPagination
project_type = PROJECT_TYPE_PROJECT
schema = AutoSchema(operation_id_base='ListCreateFile')
serializer_class = FileSerializer


Expand Down Expand Up @@ -300,7 +296,6 @@ class FileRetrieveUpdateDestroyAPIView(
lookup_field = 'sodar_uuid'
lookup_url_kwarg = 'file'
project_type = PROJECT_TYPE_PROJECT
schema = AutoSchema(operation_id_base='UpdateDestroyFile')
serializer_class = FileSerializer


Expand Down Expand Up @@ -358,7 +353,6 @@ class HyperLinkListCreateAPIView(

pagination_class = SODARPageNumberPagination
project_type = PROJECT_TYPE_PROJECT
schema = AutoSchema(operation_id_base='ListCreateHyperLink')
serializer_class = HyperLinkSerializer


Expand Down Expand Up @@ -389,5 +383,4 @@ class HyperLinkRetrieveUpdateDestroyAPIView(
lookup_field = 'sodar_uuid'
lookup_url_kwarg = 'hyperlink'
project_type = PROJECT_TYPE_PROJECT
schema = AutoSchema(operation_id_base='UpdateDestroyHyperLink')
serializer_class = HyperLinkSerializer
4 changes: 2 additions & 2 deletions projectroles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,15 @@ class Meta:
'sodar_uuid',
]

def get_additional_emails(self, obj):
def get_additional_emails(self, obj: User) -> list:
return [
e.email
for e in SODARUserAdditionalEmail.objects.filter(
user=obj, verified=True
).order_by('email')
]

def get_auth_type(self, obj):
def get_auth_type(self, obj: User) -> str:
return obj.get_auth_type()

def to_representation(self, instance):
Expand Down
23 changes: 20 additions & 3 deletions projectroles/views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@
)
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.versioning import AcceptHeaderVersioning
from rest_framework.views import APIView

from drf_spectacular.utils import extend_schema, inline_serializer

from projectroles.app_settings import AppSettingAPI
from projectroles.forms import INVITE_EXISTS_MSG
from projectroles.models import (
Expand Down Expand Up @@ -684,6 +685,7 @@ class RoleAssignmentOwnerTransferAPIView(
"""

permission_required = 'projectroles.update_project_owner'
serializer_class = RoleAssignmentSerializer

def post(self, request, *args, **kwargs):
"""Handle ownership transfer in a POST request"""
Expand Down Expand Up @@ -893,6 +895,7 @@ class ProjectInviteRevokeAPIView(
"""

permission_required = 'projectroles.invite_users'
serializer_class = ProjectInviteSerializer

def post(self, request, *args, **kwargs):
"""Handle invite revoking in a POST request"""
Expand Down Expand Up @@ -928,6 +931,7 @@ class ProjectInviteResendAPIView(
"""

permission_required = 'projectroles.invite_users'
serializer_class = ProjectInviteSerializer

def post(self, request, *args, **kwargs):
"""Handle invite resending in a POST request"""
Expand Down Expand Up @@ -1105,7 +1109,6 @@ class ProjectSettingRetrieveAPIView(

# NOTE: Update project settings perm is checked manually
permission_required = 'projectroles.view_project'
schema = AutoSchema(operation_id_base='AppSettingProject')
serializer_class = AppSettingSerializer

def get_object(self):
Expand Down Expand Up @@ -1160,6 +1163,7 @@ class ProjectSettingSetAPIView(
http_method_names = ['post']
# NOTE: Update project settings perm is checked manually
permission_required = 'projectroles.view_project'
serializer_class = AppSettingSerializer

@transaction.atomic
def post(self, request, *args, **kwargs):
Expand Down Expand Up @@ -1276,7 +1280,6 @@ class UserSettingRetrieveAPIView(
- ``setting_name``: Setting name (string)
"""

schema = AutoSchema(operation_id_base='AppSettingUser')
serializer_class = AppSettingSerializer

def get_object(self):
Expand Down Expand Up @@ -1313,6 +1316,7 @@ class UserSettingSetAPIView(
"""

http_method_names = ['post']
serializer_class = AppSettingSerializer

def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
Expand Down Expand Up @@ -1446,6 +1450,19 @@ def get_object(self):


# TODO: Update this for new API base classes
@extend_schema(
responses={
'200': inline_serializer(
'RemoteProjectGetResponse',
fields={
'users': serializers.JSONField(),
'projects': serializers.JSONField(),
'peer_sites': serializers.JSONField(),
'app_settings': serializers.JSONField(),
},
)
}
)
class RemoteProjectGetAPIView(RemoteSyncAPIVersioningMixin, APIView):
"""API view for retrieving remote projects from a source site"""

Expand Down
3 changes: 3 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,6 @@ celery>=5.4.0, <5.5

# Django autocomplete light (DAL)
django-autocomplete-light==3.11.0

# DRF-spectacular for OpenAPI schema generation
drf-spectacular>=0.28.0, <0.29
2 changes: 2 additions & 0 deletions sodarcache/tests/test_views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ def test_post_create(self):
response = self.request_knox(url, method='POST', data=post_data)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'detail': 'ok'})
self.assertEqual(JSONCacheItem.objects.all().count(), 2)
item = JSONCacheItem.objects.get(name='new_test_item')
expected = {
Expand Down Expand Up @@ -277,6 +278,7 @@ def test_post_update(self):
response = self.request_knox(url, method='POST', data=post_data)

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'detail': 'ok'})
self.assertEqual(JSONCacheItem.objects.all().count(), 1)
item = JSONCacheItem.objects.get(name=ITEM_NAME)
expected = {
Expand Down
14 changes: 13 additions & 1 deletion sodarcache/views_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""REST API views for the sodarcache app"""

from rest_framework import serializers
from rest_framework.exceptions import APIException, NotFound, ParseError
from rest_framework.generics import RetrieveAPIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.versioning import AcceptHeaderVersioning
from rest_framework.views import APIView

from drf_spectacular.utils import extend_schema, inline_serializer

# Projectroles dependency
from projectroles.models import SODAR_CONSTANTS
from projectroles.plugins import get_backend_api
Expand Down Expand Up @@ -113,6 +116,14 @@ def get_object(self):
return item


@extend_schema(
responses={
'200': inline_serializer(
'UpdateTimeResponse',
fields={'update_time': serializers.IntegerField()},
)
}
)
class CacheItemDateRetrieveAPIView(
SodarcacheAPIViewMixin, SODARAPIGenericProjectMixin, APIView
):
Expand Down Expand Up @@ -165,9 +176,10 @@ class CacheItemSetAPIView(
- ``data``: Full item data to be set (JSON)
"""

http_method_names = ['post']
permission_required = 'sodarcache.set_cache_value'
project_type = PROJECT_TYPE_PROJECT
http_method_names = ['post']
serializer_class = JSONCacheItemSerializer

def post(self, request, *args, **kwargs):
cache_backend = self.get_backend()
Expand Down
2 changes: 0 additions & 2 deletions timeline/views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.versioning import AcceptHeaderVersioning

# Projectroles dependency
Expand Down Expand Up @@ -64,7 +63,6 @@ class ProjectTimelineEventListAPIView(

pagination_class = SODARPageNumberPagination
permission_required = 'timeline.view_timeline'
schema = AutoSchema(operation_id_base='ListTimelineEvent')
serializer_class = TimelineEventSerializer

def get_queryset(self):
Expand Down

0 comments on commit fb630da

Please sign in to comment.