diff --git a/config/settings/base.py b/config/settings/base.py index 178a41c9..f70ab9c5 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -345,6 +345,7 @@ 'knox.auth.TokenAuthentication', ), 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', + 'PAGE_SIZE': env.int('SODAR_API_PAGE_SIZE', 100), } @@ -579,30 +580,11 @@ def set_logging(level=None): # General API settings -SODAR_API_DEFAULT_VERSION = '0.15.0' -SODAR_API_ALLOWED_VERSIONS = [ - '0.7.0', - '0.7.1', - '0.8.0', - '0.9.0', - '0.10.0', - '0.10.1', - '0.11.0', - '0.11.1', - '0.11.2', - '0.11.3', - '0.12.0', - '0.12.1', - '0.13.0', - '0.13.1', - '0.13.2', - '0.13.3', - '0.13.4', - '0.14.0', - '0.14.1', - '0.14.2', - '0.15.0', -] +# NOTE: All except SODAR_API_DEFAULT_HOST are deprecated +# TODO: Remove after upgrading to SODAR Core v1.0.3 +# (see bihealth/sodar-core#1495) +SODAR_API_DEFAULT_VERSION = '1.0' +SODAR_API_ALLOWED_VERSIONS = ['1.0'] SODAR_API_MEDIA_TYPE = 'application/vnd.bihealth.sodar+json' SODAR_API_DEFAULT_HOST = env.url( 'SODAR_API_DEFAULT_HOST', 'http://127.0.0.1:8000' diff --git a/docs_manual/source/api_irodsinfo.rst b/docs_manual/source/api_irodsinfo.rst index 19148d43..115f0299 100644 --- a/docs_manual/source/api_irodsinfo.rst +++ b/docs_manual/source/api_irodsinfo.rst @@ -6,20 +6,22 @@ Irods Info API The REST API for the iRODS Info app is described in this document. +Versioning +========== + +Media Type + ``application/vnd.bihealth.sodar.irodsinfo+json`` +Current Version + ``1.0`` +Accepted Versions + ``1.0`` +Header Example + ``Accept: application/vnd.bihealth.sodar.irodsinfo+json; version=x.y`` + + API Views ========= .. currentmodule:: irodsinfo.views_api .. autoclass:: IrodsEnvRetrieveAPIView - - -Versioning -========== - -For accept header versioning, the following header is expected in the current -SODAR version: - -.. code-block:: console - - Accept: application/vnd.bihealth.sodar+json; version=0.15.0 diff --git a/docs_manual/source/api_landingzones.rst b/docs_manual/source/api_landingzones.rst index de1f53eb..d0a564fa 100644 --- a/docs_manual/source/api_landingzones.rst +++ b/docs_manual/source/api_landingzones.rst @@ -6,6 +6,19 @@ Landing Zones API The REST API for landing zone operations is described in this document. +Versioning +========== + +Media Type + ``application/vnd.bihealth.sodar.landingzones+json`` +Current Version + ``1.0`` +Accepted Versions + ``1.0`` +Header Example + ``Accept: application/vnd.bihealth.sodar.landingzones+json; version=x.y`` + + API Views ========= @@ -22,14 +35,3 @@ API Views .. autoclass:: ZoneSubmitDeleteAPIView .. autoclass:: ZoneSubmitMoveAPIView - - -Versioning -========== - -For accept header versioning, the following header is expected in the current -SODAR version: - -.. code-block:: console - - Accept: application/vnd.bihealth.sodar+json; version=0.15.0 diff --git a/docs_manual/source/api_samplesheets.rst b/docs_manual/source/api_samplesheets.rst index b97d8cbf..d2df63b7 100644 --- a/docs_manual/source/api_samplesheets.rst +++ b/docs_manual/source/api_samplesheets.rst @@ -6,6 +6,19 @@ Sample Sheets API The REST API for sample sheet operations is described in this document. +Versioning +========== + +Media Type + ``application/vnd.bihealth.sodar.samplesheets+json`` +Current Version + ``1.0`` +Accepted Versions + ``1.0`` +Header Example + ``Accept: application/vnd.bihealth.sodar.samplesheets+json; version=x.y`` + + API Views ========= @@ -58,14 +71,3 @@ iRODS Data Requests .. autoclass:: IrodsDataRequestAcceptAPIView .. autoclass:: IrodsDataRequestRejectAPIView - - -Versioning -========== - -For accept header versioning, the following header is expected in the current -SODAR version: - -.. code-block:: console - - Accept: application/vnd.bihealth.sodar+json; version=0.15.0 diff --git a/irodsinfo/tests/test_permissions_api.py b/irodsinfo/tests/test_permissions_api.py index 03a04da1..1669dbbe 100644 --- a/irodsinfo/tests/test_permissions_api.py +++ b/irodsinfo/tests/test_permissions_api.py @@ -4,15 +4,26 @@ # Projectroles dependency from projectroles.tests.test_permissions import SiteAppPermissionTestBase +from projectroles.tests.test_permissions_api import SODARAPIPermissionTestMixin +from irodsinfo.views_api import ( + IRODSINFO_API_MEDIA_TYPE, + IRODSINFO_API_DEFAULT_VERSION, +) -class TestIrodsConfigRetrieveAPIView(SiteAppPermissionTestBase): + +class TestIrodsConfigRetrieveAPIView( + SODARAPIPermissionTestMixin, SiteAppPermissionTestBase +): """Tests for irodsinfo API""" + media_type = IRODSINFO_API_MEDIA_TYPE + api_version = IRODSINFO_API_DEFAULT_VERSION + def test_get_irods_config(self): """Test IrodsConfigRetrieveAPIView GET""" url = reverse('irodsinfo:api_env') good_users = [self.superuser, self.regular_user] bad_users = [self.anonymous] - self.assert_response(url, good_users, 200) - self.assert_response(url, bad_users, 401) + self.assert_response_api(url, good_users, 200) + self.assert_response_api(url, bad_users, 401) diff --git a/irodsinfo/tests/test_views_api.py b/irodsinfo/tests/test_views_api.py index 11f291ae..81d44833 100644 --- a/irodsinfo/tests/test_views_api.py +++ b/irodsinfo/tests/test_views_api.py @@ -7,11 +7,18 @@ from test_plus.test import TestCase from irodsinfo.tests.test_views import PLUGINS_DISABLE_IRODS +from irodsinfo.views_api import ( + IRODSINFO_API_MEDIA_TYPE, + IRODSINFO_API_DEFAULT_VERSION, +) class TestIrodsConfigRetrieveAPIView(TestCase): """Tests for IrodsConfigRetrieveAPIView""" + media_type = IRODSINFO_API_MEDIA_TYPE + api_version = IRODSINFO_API_DEFAULT_VERSION + def setUp(self): # Create users self.superuser = self.make_user('superuser') diff --git a/irodsinfo/views_api.py b/irodsinfo/views_api.py index 288283f1..090137e6 100644 --- a/irodsinfo/views_api.py +++ b/irodsinfo/views_api.py @@ -3,7 +3,9 @@ import logging from rest_framework.permissions import IsAuthenticated +from rest_framework.renderers import JSONRenderer from rest_framework.response import Response +from rest_framework.versioning import AcceptHeaderVersioning from rest_framework.views import APIView # Projectroles dependency @@ -15,7 +17,32 @@ logger = logging.getLogger(__name__) -class IrodsEnvRetrieveAPIView(IrodsConfigMixin, APIView): +# Local constants +IRODSINFO_API_MEDIA_TYPE = 'application/vnd.bihealth.sodar.irodsinfo+json' +IRODSINFO_API_ALLOWED_VERSIONS = ['1.0'] +IRODSINFO_API_DEFAULT_VERSION = '1.0' + + +class IrodsinfoAPIVersioningMixin: + """ + Irodsinfo API view versioning mixin for overriding media type and + accepted versions. + """ + + class IrodsinfoAPIRenderer(JSONRenderer): + media_type = IRODSINFO_API_MEDIA_TYPE + + class IrodsinfoAPIVersioning(AcceptHeaderVersioning): + allowed_versions = IRODSINFO_API_ALLOWED_VERSIONS + default_version = IRODSINFO_API_DEFAULT_VERSION + + renderer_classes = [IrodsinfoAPIRenderer] + versioning_class = IrodsinfoAPIVersioning + + +class IrodsEnvRetrieveAPIView( + IrodsConfigMixin, IrodsinfoAPIVersioningMixin, APIView +): """ Retrieve iRODS environment file for the current user. diff --git a/landingzones/tests/test_permissions_api.py b/landingzones/tests/test_permissions_api.py index 2a6c0851..15cd85ed 100644 --- a/landingzones/tests/test_permissions_api.py +++ b/landingzones/tests/test_permissions_api.py @@ -15,6 +15,10 @@ ZONE_TITLE, ZONE_DESC, ) +from landingzones.views_api import ( + LANDINGZONES_API_MEDIA_TYPE, + LANDINGZONES_API_DEFAULT_VERSION, +) # SODAR constants @@ -36,6 +40,9 @@ class ZoneAPIPermissionTestBase( ): """Base class for landingzones REST API view permission tests""" + media_type = LANDINGZONES_API_MEDIA_TYPE + api_version = LANDINGZONES_API_DEFAULT_VERSION + class TestZoneListAPIView(ZoneAPIPermissionTestBase): """Tests for ZoneListAPIView permissions""" diff --git a/landingzones/tests/test_permissions_api_taskflow.py b/landingzones/tests/test_permissions_api_taskflow.py index d73ae03c..75d424bc 100644 --- a/landingzones/tests/test_permissions_api_taskflow.py +++ b/landingzones/tests/test_permissions_api_taskflow.py @@ -26,6 +26,10 @@ ZONE_TITLE, ZONE_DESC, ) +from landingzones.views_api import ( + LANDINGZONES_API_MEDIA_TYPE, + LANDINGZONES_API_DEFAULT_VERSION, +) # Local constants @@ -41,6 +45,9 @@ class ZoneAPIPermissionTaskflowTestBase( ): """Base class for landing zone permission tests with Taskflow""" + media_type = LANDINGZONES_API_MEDIA_TYPE + api_version = LANDINGZONES_API_DEFAULT_VERSION + def setUp(self): super().setUp() # Import investigation diff --git a/landingzones/tests/test_views_api.py b/landingzones/tests/test_views_api.py index 5dab0c54..6282bcaa 100644 --- a/landingzones/tests/test_views_api.py +++ b/landingzones/tests/test_views_api.py @@ -20,6 +20,10 @@ ) from landingzones.tests.test_models import LandingZoneMixin from landingzones.tests.test_views_taskflow import ZONE_TITLE, ZONE_DESC +from landingzones.views_api import ( + LANDINGZONES_API_MEDIA_TYPE, + LANDINGZONES_API_DEFAULT_VERSION, +) # SODAR constants @@ -42,6 +46,9 @@ class TestLandingZoneAPIViewsBase( ): """Base class for Landingzones API view testing""" + media_type = LANDINGZONES_API_MEDIA_TYPE + api_version = LANDINGZONES_API_DEFAULT_VERSION + def setUp(self): super().setUp() # Init contributor user and assignment diff --git a/landingzones/tests/test_views_api_taskflow.py b/landingzones/tests/test_views_api_taskflow.py index 53df32e6..95091004 100644 --- a/landingzones/tests/test_views_api_taskflow.py +++ b/landingzones/tests/test_views_api_taskflow.py @@ -41,6 +41,10 @@ ZONE_ALL_COLLS, TEST_OBJ_NAME, ) +from landingzones.views_api import ( + LANDINGZONES_API_MEDIA_TYPE, + LANDINGZONES_API_DEFAULT_VERSION, +) # SODAR constants @@ -65,6 +69,9 @@ class ZoneAPIViewTaskflowTestBase( ): """Base landing zone API view test class with Taskflow enabled""" + media_type = LANDINGZONES_API_MEDIA_TYPE + api_version = LANDINGZONES_API_DEFAULT_VERSION + def setUp(self): super().setUp() # Get iRODS backend for session access diff --git a/landingzones/views_api.py b/landingzones/views_api.py index 3dda7365..74865264 100644 --- a/landingzones/views_api.py +++ b/landingzones/views_api.py @@ -11,9 +11,11 @@ CreateAPIView, UpdateAPIView, ) +from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework import status from rest_framework.serializers import ValidationError +from rest_framework.versioning import AcceptHeaderVersioning from rest_framework.views import APIView # Projectroles dependency @@ -41,11 +43,37 @@ logger = logging.getLogger(__name__) +# Local constants +LANDINGZONES_API_MEDIA_TYPE = 'application/vnd.bihealth.sodar.landingzones+json' +LANDINGZONES_API_ALLOWED_VERSIONS = ['1.0'] +LANDINGZONES_API_DEFAULT_VERSION = '1.0' + + # Mixins and Base Views -------------------------------------------------------- +class LandingzonesAPIVersioningMixin: + """ + Landingzones API view versioning mixin for overriding media type and + accepted versions. + """ + + class LandingzonesAPIRenderer(JSONRenderer): + media_type = LANDINGZONES_API_MEDIA_TYPE + + class LandingzonesAPIVersioning(AcceptHeaderVersioning): + allowed_versions = LANDINGZONES_API_ALLOWED_VERSIONS + default_version = LANDINGZONES_API_DEFAULT_VERSION + + renderer_classes = [LandingzonesAPIRenderer] + versioning_class = LandingzonesAPIVersioning + + class ZoneSubmitBaseAPIView( - ZoneModifyPermissionMixin, SODARAPIBaseProjectMixin, APIView + ZoneModifyPermissionMixin, + LandingzonesAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Base API view for initiating LandingZone operations via SODAR Taskflow. @@ -80,7 +108,9 @@ def _validate_zone_obj(cls, zone, allowed_status_types, action): # API Views -------------------------------------------------------------------- -class ZoneListAPIView(SODARAPIGenericProjectMixin, ListAPIView): +class ZoneListAPIView( + LandingzonesAPIVersioningMixin, SODARAPIGenericProjectMixin, ListAPIView +): """ List the landing zones in a project. @@ -120,7 +150,9 @@ def get_queryset(self): return ret -class ZoneRetrieveAPIView(SODARAPIGenericProjectMixin, RetrieveAPIView): +class ZoneRetrieveAPIView( + LandingzonesAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView +): """ Retrieve the details of a landing zone. @@ -163,7 +195,10 @@ def get_permission_required(self): class ZoneCreateAPIView( - ZoneModifyMixin, SODARAPIGenericProjectMixin, CreateAPIView + ZoneModifyMixin, + LandingzonesAPIVersioningMixin, + SODARAPIGenericProjectMixin, + CreateAPIView, ): """ Create a landing zone. @@ -231,7 +266,10 @@ def perform_create(self, serializer): class ZoneUpdateAPIView( - ZoneModifyMixin, SODARAPIGenericProjectMixin, UpdateAPIView + ZoneModifyMixin, + LandingzonesAPIVersioningMixin, + SODARAPIGenericProjectMixin, + UpdateAPIView, ): """ Update a landing zone description and user message. diff --git a/samplesheets/tests/test_permissions_api.py b/samplesheets/tests/test_permissions_api.py index fb815a7c..6f7089c6 100644 --- a/samplesheets/tests/test_permissions_api.py +++ b/samplesheets/tests/test_permissions_api.py @@ -31,6 +31,10 @@ REMOTE_SITE_SECRET, INVALID_SECRET, ) +from samplesheets.views_api import ( + SAMPLESHEETS_API_MEDIA_TYPE, + SAMPLESHEETS_API_DEFAULT_VERSION, +) # Local constants @@ -38,9 +42,16 @@ LABEL_CREATE = 'label' +class SamplesheetsAPIPermissionTestBase(ProjectAPIPermissionTestBase): + """Base class for samplesheets REST API view permission tests""" + + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + + class TestInvestigationRetrieveAPIView( SampleSheetIOMixin, - ProjectAPIPermissionTestBase, + SamplesheetsAPIPermissionTestBase, ): """Tests for InvestigationRetrieveAPIView permissions""" @@ -120,7 +131,9 @@ def test_get_archive(self): self.assert_response_api(url, self.anonymous, 401) -class TestSheetImportAPIView(SampleSheetIOMixin, ProjectAPIPermissionTestBase): +class TestSheetImportAPIView( + SampleSheetIOMixin, SamplesheetsAPIPermissionTestBase +): """Tests for SheetImportAPIView permissions""" def _cleanup_import(self): @@ -291,7 +304,7 @@ def test_post_archive(self): class TestSheetISAExportAPIView( SampleSheetIOMixin, - ProjectAPIPermissionTestBase, + SamplesheetsAPIPermissionTestBase, ): """Tests for SheetISAExportAPIView permissions""" @@ -364,7 +377,9 @@ def test_get_archive(self): class TestIrodsAccessTicketListAPIView( - SampleSheetIOMixin, IrodsAccessTicketMixin, ProjectAPIPermissionTestBase + SampleSheetIOMixin, + IrodsAccessTicketMixin, + SamplesheetsAPIPermissionTestBase, ): """Test permissions for IrodsAccessTicketListAPIView""" @@ -438,7 +453,9 @@ def test_get_archive(self): class TestIrodsAccessTicketRetrieveAPIView( - SampleSheetIOMixin, IrodsAccessTicketMixin, ProjectAPIPermissionTestBase + SampleSheetIOMixin, + IrodsAccessTicketMixin, + SamplesheetsAPIPermissionTestBase, ): """Test permissions for IrodsAccessTicketRetrieveAPIView""" @@ -509,7 +526,7 @@ def test_get_archive(self): class TestIrodsDataRequestRetrieveAPIView( - IrodsDataRequestMixin, ProjectAPIPermissionTestBase + IrodsDataRequestMixin, SamplesheetsAPIPermissionTestBase ): """Tests for TestIrodsDataRequestRetrieveAPIView permissions""" @@ -581,7 +598,7 @@ def test_get_archive(self): self.assert_response_api(self.url, self.anonymous, 401) -class TestIrodsDataRequestListAPIView(ProjectAPIPermissionTestBase): +class TestIrodsDataRequestListAPIView(SamplesheetsAPIPermissionTestBase): """Tests for TestIrodsDataRequestListAPIView permissions""" def setUp(self): @@ -646,7 +663,7 @@ def test_get_archive(self): class TestIrodsDataRequestRejectAPIView( - IrodsDataRequestMixin, ProjectAPIPermissionTestBase + IrodsDataRequestMixin, SamplesheetsAPIPermissionTestBase ): """Test permissions for TestIrodsDataRequestRejectAPIView""" @@ -728,7 +745,7 @@ def test_reject_archive(self): class TestIrodsDataRequestDestroyAPIView( - SampleSheetIOMixin, IrodsDataRequestMixin, ProjectAPIPermissionTestBase + SampleSheetIOMixin, IrodsDataRequestMixin, SamplesheetsAPIPermissionTestBase ): """Test permissions for IrodsDataRequestDestroyAPIView""" diff --git a/samplesheets/tests/test_permissions_api_taskflow.py b/samplesheets/tests/test_permissions_api_taskflow.py index d12f8217..b5d5fd42 100644 --- a/samplesheets/tests/test_permissions_api_taskflow.py +++ b/samplesheets/tests/test_permissions_api_taskflow.py @@ -33,6 +33,10 @@ SampleSheetTaskflowMixin, IRODS_FILE_NAME, ) +from samplesheets.views_api import ( + SAMPLESHEETS_API_MEDIA_TYPE, + SAMPLESHEETS_API_DEFAULT_VERSION, +) # Local constants @@ -43,11 +47,20 @@ # Base Classes and Mixins ------------------------------------------------------ -class IrodsAccessTicketAPIViewTestBase( +class SheetTaskflowAPIPermissionTestBase( SampleSheetIOMixin, - IrodsAccessTicketMixin, SampleSheetTaskflowMixin, TaskflowAPIPermissionTestBase, +): + """Base class for samplesheets REST API view permission tests""" + + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + + +class IrodsAccessTicketAPIViewTestBase( + IrodsAccessTicketMixin, + SheetTaskflowAPIPermissionTestBase, ): """Base class for iRODS access ticket API view permission tests""" @@ -65,9 +78,7 @@ def setUp(self): ) -class IrodsDataRequestAPIViewTestBase( - SampleSheetIOMixin, SampleSheetTaskflowMixin, TaskflowAPIPermissionTestBase -): +class IrodsDataRequestAPIViewTestBase(SheetTaskflowAPIPermissionTestBase): """Base class for iRODS data request API view permission tests""" def setUp(self): @@ -89,9 +100,7 @@ def setUp(self): # Test Classes ----------------------------------------------------------------- -class TestSampleDataFileExistsAPIView( - SampleSheetIOMixin, SampleSheetTaskflowMixin, TaskflowAPIPermissionTestBase -): +class TestSampleDataFileExistsAPIView(SheetTaskflowAPIPermissionTestBase): """Tests for SampleDataFileExistsAPIView permissions""" def setUp(self): @@ -434,9 +443,7 @@ def test_delete_archive(self): self.assert_response_api(self.url, self.anonymous, 401, method='DELETE') -class TestIrodsDataRequestListAPIView( - SampleSheetIOMixin, SampleSheetTaskflowMixin, TaskflowAPIPermissionTestBase -): +class TestIrodsDataRequestListAPIView(SheetTaskflowAPIPermissionTestBase): """Tests for IrodsDataRequestListAPIView permissions""" def setUp(self): diff --git a/samplesheets/tests/test_views_api.py b/samplesheets/tests/test_views_api.py index 0fd345db..4ebf26d3 100644 --- a/samplesheets/tests/test_views_api.py +++ b/samplesheets/tests/test_views_api.py @@ -67,6 +67,10 @@ IrodsAccessTicketViewTestMixin, ) from samplesheets.views import SheetImportMixin +from samplesheets.views_api import ( + SAMPLESHEETS_API_MEDIA_TYPE, + SAMPLESHEETS_API_DEFAULT_VERSION, +) app_settings = AppSettingAPI() @@ -96,6 +100,9 @@ class SampleSheetAPIViewTestBase(SampleSheetIOMixin, APIViewTestBase): """Base view for samplesheets API views tests""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + class IrodsAccessTicketAPITestBase( IrodsAccessTicketMixin, diff --git a/samplesheets/tests/test_views_api_taskflow.py b/samplesheets/tests/test_views_api_taskflow.py index 38e17b9d..841331aa 100644 --- a/samplesheets/tests/test_views_api_taskflow.py +++ b/samplesheets/tests/test_views_api_taskflow.py @@ -36,15 +36,6 @@ IRODS_REQUEST_STATUS_REJECTED, IRODS_REQUEST_ACTION_DELETE, ) -from samplesheets.views import ( - IRODS_REQUEST_EVENT_CREATE as CREATE_ALERT, - IRODS_REQUEST_EVENT_ACCEPT as ACCEPT_ALERT, - IRODS_REQUEST_EVENT_REJECT as REJECT_ALERT, -) -from samplesheets.views_api import ( - IRODS_QUERY_ERROR_MSG, -) - from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR from samplesheets.tests.test_models import ( IrodsAccessTicketMixin, @@ -60,6 +51,16 @@ TICKET_STR, TICKET_LABEL, ) +from samplesheets.views import ( + IRODS_REQUEST_EVENT_CREATE as CREATE_ALERT, + IRODS_REQUEST_EVENT_ACCEPT as ACCEPT_ALERT, + IRODS_REQUEST_EVENT_REJECT as REJECT_ALERT, +) +from samplesheets.views_api import ( + IRODS_QUERY_ERROR_MSG, + SAMPLESHEETS_API_MEDIA_TYPE, + SAMPLESHEETS_API_DEFAULT_VERSION, +) # SODAR constants PROJECT_TYPE_PROJECT = SODAR_CONSTANTS['PROJECT_TYPE_PROJECT'] @@ -84,6 +85,9 @@ class SampleSheetAPITaskflowTestBase( ): """Base samplesheets API view test class with Taskflow enabled""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + def setUp(self): super().setUp() # Make project with owner in Taskflow and Django @@ -109,6 +113,9 @@ class IrodsAccessTicketAPIViewTestBase( ): """Base samplesheets API view test class for iRODS access ticket requests""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + def assert_alert_count(self, alert_name, user, count, project=None): """ Assert expected app alert count. If project is not specified, default to @@ -171,6 +178,9 @@ class TestIrodsDataRequestAPIViewBase( ): """Base samplesheets API view test class for iRODS delete requests""" + media_type = SAMPLESHEETS_API_MEDIA_TYPE + api_version = SAMPLESHEETS_API_DEFAULT_VERSION + # TODO: Retrieve this from a common base/helper class instead of redef def assert_alert_count(self, alert_name, user, count, project=None): """ diff --git a/samplesheets/views_api.py b/samplesheets/views_api.py index 7b50f8ef..f33df8ad 100644 --- a/samplesheets/views_api.py +++ b/samplesheets/views_api.py @@ -25,7 +25,9 @@ UpdateAPIView, ) from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.renderers import JSONRenderer from rest_framework.response import Response +from rest_framework.versioning import AcceptHeaderVersioning from rest_framework.views import APIView # Projectroles dependency @@ -70,6 +72,10 @@ table_builder = SampleSheetTableBuilder() +# Local constants +SAMPLESHEETS_API_MEDIA_TYPE = 'application/vnd.bihealth.sodar.samplesheets+json' +SAMPLESHEETS_API_ALLOWED_VERSIONS = ['1.0'] +SAMPLESHEETS_API_DEFAULT_VERSION = '1.0' MD5_RE = re.compile(r'([a-fA-F\d]{32})') APP_NAME = 'samplesheets' IRODS_QUERY_ERROR_MSG = 'Exception querying iRODS objects' @@ -78,11 +84,31 @@ IRODS_TICKET_NO_UPDATE_FIELDS_MSG = 'No fields to update' +# Base Classes and Mixins ------------------------------------------------------ + + +class SamplesheetsAPIVersioningMixin: + """ + Samplesheets API view versioning mixin for overriding media type and + accepted versions. + """ + + class SamplesheetsAPIRenderer(JSONRenderer): + media_type = SAMPLESHEETS_API_MEDIA_TYPE + + class SamplesheetsAPIVersioning(AcceptHeaderVersioning): + allowed_versions = SAMPLESHEETS_API_ALLOWED_VERSIONS + default_version = SAMPLESHEETS_API_DEFAULT_VERSION + + renderer_classes = [SamplesheetsAPIRenderer] + versioning_class = SamplesheetsAPIVersioning + + # API Views -------------------------------------------------------------------- class InvestigationRetrieveAPIView( - SODARAPIGenericProjectMixin, RetrieveAPIView + SamplesheetsAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve metadata of an investigation with its studies and assays. @@ -116,7 +142,10 @@ class InvestigationRetrieveAPIView( class IrodsCollsCreateAPIView( - IrodsCollsCreateViewMixin, SODARAPIBaseProjectMixin, APIView + IrodsCollsCreateViewMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Create iRODS collections for a project. @@ -162,7 +191,10 @@ def post(self, request, *args, **kwargs): class SheetISAExportAPIView( - SheetISAExportMixin, SODARAPIBaseProjectMixin, APIView + SheetISAExportMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Export sample sheets as ISA-Tab TSV files, either packed in a zip archive or @@ -197,7 +229,12 @@ def get(self, request, *args, **kwargs): raise APIException('Unable to export ISA-Tab: {}'.format(ex)) -class SheetImportAPIView(SheetImportMixin, SODARAPIBaseProjectMixin, APIView): +class SheetImportAPIView( + SheetImportMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, +): """ Upload sample sheet as separate ISA-Tab TSV files or a zip archive. Will replace existing sheets if valid. @@ -315,7 +352,7 @@ def post(self, request, *args, **kwargs): class IrodsAccessTicketRetrieveAPIView( - SODARAPIGenericProjectMixin, RetrieveAPIView + SamplesheetsAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve an iRODS access ticket for a project. @@ -345,7 +382,9 @@ class IrodsAccessTicketRetrieveAPIView( queryset_project_field = 'study__investigation__project' -class IrodsAccessTicketListAPIView(SODARAPIBaseProjectMixin, ListAPIView): +class IrodsAccessTicketListAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseProjectMixin, ListAPIView +): """ List iRODS access tickets for a project. @@ -376,7 +415,10 @@ def get_queryset(self): class IrodsAccessTicketCreateAPIView( - IrodsAccessTicketModifyMixin, SODARAPIGenericProjectMixin, CreateAPIView + IrodsAccessTicketModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + CreateAPIView, ): """ Create an iRODS access ticket for a project. @@ -429,7 +471,10 @@ def perform_create(self, serializer): class IrodsAccessTicketUpdateAPIView( - IrodsAccessTicketModifyMixin, SODARAPIGenericProjectMixin, UpdateAPIView + IrodsAccessTicketModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + UpdateAPIView, ): """ Update an iRODS access ticket for a project. @@ -463,7 +508,10 @@ def perform_update(self, serializer): class IrodsAccessTicketDestroyAPIView( - IrodsAccessTicketModifyMixin, SODARAPIGenericProjectMixin, DestroyAPIView + IrodsAccessTicketModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + DestroyAPIView, ): """ Delete an iRODS access ticket. @@ -497,7 +545,7 @@ def perform_destroy(self, instance): class IrodsDataRequestRetrieveAPIView( - SODARAPIGenericProjectMixin, RetrieveAPIView + SamplesheetsAPIVersioningMixin, SODARAPIGenericProjectMixin, RetrieveAPIView ): """ Retrieve a iRODS data request. @@ -526,7 +574,9 @@ class IrodsDataRequestRetrieveAPIView( serializer_class = IrodsDataRequestSerializer -class IrodsDataRequestListAPIView(SODARAPIBaseProjectMixin, ListAPIView): +class IrodsDataRequestListAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseProjectMixin, ListAPIView +): """ List the iRODS data requests for a project. @@ -561,7 +611,10 @@ def get_queryset(self): class IrodsDataRequestCreateAPIView( - IrodsDataRequestModifyMixin, SODARAPIGenericProjectMixin, CreateAPIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + CreateAPIView, ): """ Create an iRODS delete request for a project. @@ -592,7 +645,10 @@ def perform_create(self, serializer): class IrodsDataRequestUpdateAPIView( - IrodsDataRequestModifyMixin, SODARAPIGenericProjectMixin, UpdateAPIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + UpdateAPIView, ): """ Update an iRODS data request for a project. @@ -622,7 +678,10 @@ def perform_update(self, serializer): class IrodsDataRequestDestroyAPIView( - IrodsDataRequestModifyMixin, SODARAPIGenericProjectMixin, DestroyAPIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIGenericProjectMixin, + DestroyAPIView, ): """ Delete an iRODS data request object. @@ -651,7 +710,10 @@ def perform_destroy(self, instance): class IrodsDataRequestAcceptAPIView( - IrodsDataRequestModifyMixin, SODARAPIBaseProjectMixin, APIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Accept an iRODS data request for a project. @@ -696,7 +758,10 @@ def post(self, request, *args, **kwargs): class IrodsDataRequestRejectAPIView( - IrodsDataRequestModifyMixin, SODARAPIBaseProjectMixin, APIView + IrodsDataRequestModifyMixin, + SamplesheetsAPIVersioningMixin, + SODARAPIBaseProjectMixin, + APIView, ): """ Reject an iRODS data request for a project. @@ -738,7 +803,9 @@ def post(self, request, *args, **kwargs): ) -class SampleDataFileExistsAPIView(SODARAPIBaseMixin, APIView): +class SampleDataFileExistsAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseMixin, APIView +): """ Return status of data object existing in SODAR iRODS by MD5 checksum. Includes all projects in search regardless of user permissions. @@ -812,7 +879,9 @@ def get(self, request, *args, **kwargs): return Response(ret, status=status.HTTP_200_OK) -class ProjectIrodsFileListAPIView(SODARAPIBaseProjectMixin, APIView): +class ProjectIrodsFileListAPIView( + SamplesheetsAPIVersioningMixin, SODARAPIBaseProjectMixin, APIView +): """ Return a list of files in the project sample data repository.