diff --git a/addon_imps/redirect/__init__.py b/addon_imps/redirect/__init__.py new file mode 100644 index 00000000..a2f4aa93 --- /dev/null +++ b/addon_imps/redirect/__init__.py @@ -0,0 +1,2 @@ +"""addon_imps.redirect: imps that implement a redirect-like interface +""" diff --git a/addon_imps/redirect/redirect_dummy.py b/addon_imps/redirect/redirect_dummy.py new file mode 100644 index 00000000..53c3f3b8 --- /dev/null +++ b/addon_imps/redirect/redirect_dummy.py @@ -0,0 +1,9 @@ +from addon_toolkit.interfaces._base import BaseAddonInterface +from addon_toolkit.interfaces.redirect import RedirectAddonImp + + +class DummyRedirectImp(RedirectAddonImp): + """this is a dummy AddonImp for ALL redirect services. + redirect links will be specified in django admin configuration.""" + + ADDON_INTERFACE = BaseAddonInterface diff --git a/addon_service/admin/__init__.py b/addon_service/admin/__init__.py index 10389538..da1bfad0 100644 --- a/addon_service/admin/__init__.py +++ b/addon_service/admin/__init__.py @@ -93,6 +93,22 @@ class ExternalComputingServiceAdmin(GravyvaletModelAdmin): } +@admin.register(models.ExternalRedirectService) +class ExternalRedirectServiceAdmin(GravyvaletModelAdmin): + list_display = ("display_name", "created", "modified") + readonly_fields = ( + "id", + "created", + "modified", + ) + raw_id_fields = ("oauth2_client_config", "oauth1_client_config") + enum_choice_fields = { + "int_addon_imp": known_imps.RedirectAddonImpNumbers, + "int_credentials_format": CredentialsFormats, + "int_service_type": ServiceTypes, + } + + @admin.register(models.OAuth2ClientConfig) @linked_many_field("external_storage_services") @linked_many_field("external_citation_services") diff --git a/addon_service/common/known_imps.py b/addon_service/common/known_imps.py index edb3b7b0..9d6a7f87 100644 --- a/addon_service/common/known_imps.py +++ b/addon_service/common/known_imps.py @@ -11,6 +11,7 @@ ) from addon_imps.computing import boa from addon_imps.link import dataverse as link_dataverse +from addon_imps.redirect import redirect_dummy from addon_imps.storage import ( azure_blob_storage, bitbucket, @@ -30,6 +31,7 @@ from addon_toolkit.interfaces.citation import CitationAddonImp from addon_toolkit.interfaces.computing import ComputingAddonImp from addon_toolkit.interfaces.link import LinkAddonImp +from addon_toolkit.interfaces.redirect import RedirectAddonImp from addon_toolkit.interfaces.storage import StorageAddonImp @@ -102,6 +104,9 @@ class KnownAddonImps(enum.Enum): # Type: Link LINK_DATAVERSE = link_dataverse.DataverseLinkImp + # Type: Redirect + REDIRECT_DUMMY = redirect_dummy.DummyRedirectImp + if __debug__: BLARG = my_blarg.MyBlargStorage @@ -140,6 +145,9 @@ class AddonImpNumbers(enum.Enum): # Type: Link LINK_DATAVERSE = 1030 + # Type: Redirect + REDIRECT_DUMMY = 1040 + if __debug__: BLARG = -7 @@ -158,3 +166,4 @@ def filter_addons_by_type(addon_type): CitationAddonImpNumbers = filter_addons_by_type(CitationAddonImp) ComputingAddonImpNumbers = filter_addons_by_type(ComputingAddonImp) LinkAddonImpNumbers = filter_addons_by_type(LinkAddonImp) +RedirectAddonImpNumbers = filter_addons_by_type(RedirectAddonImp) diff --git a/addon_service/external_service/redirect/__init__.py b/addon_service/external_service/redirect/__init__.py new file mode 100644 index 00000000..713236df --- /dev/null +++ b/addon_service/external_service/redirect/__init__.py @@ -0,0 +1,2 @@ +"""addon_service.external_service.redirect: imps that implement a redirect service +""" diff --git a/addon_service/external_service/redirect/models.py b/addon_service/external_service/redirect/models.py new file mode 100644 index 00000000..12b1becb --- /dev/null +++ b/addon_service/external_service/redirect/models.py @@ -0,0 +1,15 @@ +from django.db import models + +from addon_service.external_service.models import ExternalService + + +class ExternalRedirectService(ExternalService): + redirect_url = models.URLField(blank=True, default="") + + class Meta: + verbose_name = "External Redirect Service" + verbose_name_plural = "External Redirect Services" + app_label = "addon_service" + + class JSONAPIMeta: + resource_name = "external-redirect-services" diff --git a/addon_service/external_service/redirect/serializers.py b/addon_service/external_service/redirect/serializers.py new file mode 100644 index 00000000..3cf8308c --- /dev/null +++ b/addon_service/external_service/redirect/serializers.py @@ -0,0 +1,46 @@ +from rest_framework_json_api import serializers +from rest_framework_json_api.utils import get_resource_type_from_model + +from addon_service.addon_imp.models import AddonImpModel +from addon_service.common import view_names +from addon_service.common.serializer_fields import DataclassRelatedDataField +from addon_service.external_service.serializers import ExternalServiceSerializer + +from .models import ExternalRedirectService + + +RESOURCE_TYPE = get_resource_type_from_model(ExternalRedirectService) + + +class ExternalRedirectServiceSerializer(ExternalServiceSerializer): + """api serializer for the `ExternalRedirectService` model""" + + url = serializers.HyperlinkedIdentityField( + view_name=view_names.detail_view(RESOURCE_TYPE) + ) + + addon_imp = DataclassRelatedDataField( + dataclass_model=AddonImpModel, + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + + redirect_url = serializers.CharField( + read_only=True, + ) + + class Meta: + model = ExternalRedirectService + fields = [ + "id", + "addon_imp", + "auth_uri", + "credentials_format", + "display_name", + "url", + "wb_key", + "external_service_name", + "configurable_api_root", + "icon_url", + "api_base_url_options", + "redirect_url", + ] diff --git a/addon_service/external_service/redirect/views.py b/addon_service/external_service/redirect/views.py new file mode 100644 index 00000000..e2e106d0 --- /dev/null +++ b/addon_service/external_service/redirect/views.py @@ -0,0 +1,21 @@ +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, +) +from rest_framework_json_api.views import ReadOnlyModelViewSet + +from .models import ExternalRedirectService +from .serializers import ExternalRedirectServiceSerializer + + +@extend_schema_view( + list=extend_schema( + description="Get the list of all available external redirect services" + ), + get=extend_schema( + description="Get particular external redirect service", + ), +) +class ExternalRedirectServiceViewSet(ReadOnlyModelViewSet): + queryset = ExternalRedirectService.objects.all() + serializer_class = ExternalRedirectServiceSerializer diff --git a/addon_service/migrations/0017_externalredirectservice.py b/addon_service/migrations/0017_externalredirectservice.py new file mode 100644 index 00000000..8620939a --- /dev/null +++ b/addon_service/migrations/0017_externalredirectservice.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.20 on 2025-09-11 18:14 + +import django.db.models.deletion +from django.db import ( + migrations, + models, +) + + +class Migration(migrations.Migration): + + dependencies = [ + ("addon_service", "0016_externallinkservice_int_supported_features_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ExternalRedirectService", + fields=[ + ( + "externalservice_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="addon_service.externalservice", + ), + ), + ("redirect_url", models.URLField(blank=True, default="")), + ], + options={ + "verbose_name": "External Redirect Service", + "verbose_name_plural": "External Redirect Services", + }, + bases=("addon_service.externalservice",), + ), + ] diff --git a/addon_service/models.py b/addon_service/models.py index b350e261..f381f961 100644 --- a/addon_service/models.py +++ b/addon_service/models.py @@ -15,6 +15,7 @@ from addon_service.external_service.citation.models import ExternalCitationService from addon_service.external_service.computing.models import ExternalComputingService from addon_service.external_service.link.models import ExternalLinkService +from addon_service.external_service.redirect.models import ExternalRedirectService from addon_service.external_service.storage.models import ExternalStorageService from addon_service.oauth1.models import OAuth1ClientConfig from addon_service.oauth2.models import ( @@ -47,4 +48,5 @@ "ExternalLinkService", "AuthorizedLinkAccount", "ConfiguredLinkAddon", + "ExternalRedirectService", ) diff --git a/addon_service/static/provider_icons/datapipe.png b/addon_service/static/provider_icons/datapipe.png new file mode 100644 index 00000000..2272662a Binary files /dev/null and b/addon_service/static/provider_icons/datapipe.png differ diff --git a/addon_service/urls.py b/addon_service/urls.py index ab89009d..adaa482f 100644 --- a/addon_service/urls.py +++ b/addon_service/urls.py @@ -68,6 +68,7 @@ def _register_viewset(viewset): _register_viewset(views.AddonOperationViewSet) _register_viewset(views.AddonImpViewSet) _register_viewset(views.UserReferenceViewSet) +_register_viewset(views.ExternalRedirectServiceViewSet) ### diff --git a/addon_service/views.py b/addon_service/views.py index f72ee948..7996234e 100644 --- a/addon_service/views.py +++ b/addon_service/views.py @@ -31,6 +31,7 @@ ExternalComputingServiceViewSet, ) from addon_service.external_service.link.views import ExternalLinkServiceViewSet +from addon_service.external_service.redirect.views import ExternalRedirectServiceViewSet from addon_service.external_service.storage.views import ExternalStorageServiceViewSet from addon_service.oauth1.views import oauth1_callback_view from addon_service.oauth2.views import oauth2_callback_view @@ -70,6 +71,7 @@ async def status(request): "AuthorizedStorageAccountViewSet", "ConfiguredStorageAddonViewSet", "ExternalStorageServiceViewSet", + "ExternalRedirectServiceViewSet", "ResourceReferenceViewSet", "UserReferenceViewSet", "oauth2_callback_view", diff --git a/addon_toolkit/interfaces/redirect.py b/addon_toolkit/interfaces/redirect.py new file mode 100644 index 00000000..4ca3c5fc --- /dev/null +++ b/addon_toolkit/interfaces/redirect.py @@ -0,0 +1,12 @@ +"""a static (and still in progress) definition of what composes a redirect addon""" + +import dataclasses + +from addon_toolkit.imp import AddonImp + + +@dataclasses.dataclass +class RedirectAddonImp(AddonImp): + """base class for redirect addon implementations""" + + pass