diff --git a/gateway/.envs/example/django.env b/gateway/.envs/example/django.env index b8c36465..2d2f9830 100644 --- a/gateway/.envs/example/django.env +++ b/gateway/.envs/example/django.env @@ -41,8 +41,13 @@ SVI_SERVER_API_KEY= # BUSINESS LOGIC # ------------------------------------------------------------------------------ -# Recommended `False` in production -SDS_NEW_USERS_APPROVED_ON_CREATION=True +# Recommended `false` in production +SDS_NEW_USERS_APPROVED_ON_CREATION=true + +# VISUALIZATIONS +# ------------------------------------------------------------------------------ +# Enable or disable the visualizations feature (defaults to true if not set) +VISUALIZATIONS_ENABLED=true # MATPLOTLIB # ------------------------------------------------------------------------------ diff --git a/gateway/.envs/example/django.prod-example.env b/gateway/.envs/example/django.prod-example.env index a1117790..bca8bc43 100644 --- a/gateway/.envs/example/django.prod-example.env +++ b/gateway/.envs/example/django.prod-example.env @@ -2,18 +2,18 @@ # ====================== PRODUCTION ENV ====================== # GENERAL # ------------------------------------------------------------------------------ -# DJANGO_READ_DOT_ENV_FILE=True +# DJANGO_READ_DOT_ENV_FILE=true DJANGO_SETTINGS_MODULE=config.settings.production DJANGO_SECRET_KEY= DJANGO_ADMIN_URL= DJANGO_ALLOWED_HOSTS=localhost,sds.crc.nd.edu SITE_DOMAIN=sds.crc.nd.edu -USE_HTTPS=True +USE_HTTPS=true # SECURITY # ------------------------------------------------------------------------------ # Traefik already redirects users, so we can disable it here: -DJANGO_SECURE_SSL_REDIRECT=False +DJANGO_SECURE_SSL_REDIRECT=false # OAUTH # ------------------------------------------------------------------------------ @@ -35,7 +35,7 @@ SENTRY_ENVIRONMENT=production # DJANGO-ALLAUTH # ------------------------------------------------------------------------------ -DJANGO_ACCOUNT_ALLOW_REGISTRATION=True +DJANGO_ACCOUNT_ALLOW_REGISTRATION=true # GUNICORN # ------------------------------------------------------------------------------ @@ -81,7 +81,12 @@ OPENSEARCH_VERIFY_CERTS=false # BUSINESS LOGIC # ------------------------------------------------------------------------------ -SDS_NEW_USERS_APPROVED_ON_CREATION=False +SDS_NEW_USERS_APPROVED_ON_CREATION=false + +# VISUALIZATIONS +# ------------------------------------------------------------------------------ +# Enable or disable the visualizations feature (defaults to true if not set) +VISUALIZATIONS_ENABLED=true # MATPLOTLIB # ------------------------------------------------------------------------------ diff --git a/gateway/config/api_router.py b/gateway/config/api_router.py index 33ab42f9..cc9a80f4 100644 --- a/gateway/config/api_router.py +++ b/gateway/config/api_router.py @@ -17,7 +17,9 @@ router.register(r"assets/files", FileViewSet, basename="files") router.register(r"assets/captures", CaptureViewSet, basename="captures") router.register(r"assets/datasets", DatasetViewSet, basename="datasets") -router.register(r"visualizations", VisualizationViewSet, basename="visualizations") + +if settings.VISUALIZATIONS_ENABLED: + router.register(r"visualizations", VisualizationViewSet, basename="visualizations") app_name = "api" urlpatterns = [ diff --git a/gateway/config/settings/base.py b/gateway/config/settings/base.py index 8a744ad6..20c04a7e 100644 --- a/gateway/config/settings/base.py +++ b/gateway/config/settings/base.py @@ -523,6 +523,14 @@ def __get_random_token(length: int) -> str: default=False, ) +# Visualizations +# ------------------------------------------------------------------------------ +# Enable or disable the visualizations feature +VISUALIZATIONS_ENABLED: bool = env.bool( + "VISUALIZATIONS_ENABLED", + default=True, +) + # File upload limits # ------------------------------------------------------------------------------ # Maximum number of files that can be uploaded at once diff --git a/gateway/config/urls.py b/gateway/config/urls.py index 6f57a680..1c195c53 100644 --- a/gateway/config/urls.py +++ b/gateway/config/urls.py @@ -19,16 +19,20 @@ # User management path("users/", include("sds_gateway.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), - # Visualizations - path( - "visualizations/", - include("sds_gateway.visualizations.urls", namespace="visualizations"), - ), # Your stuff: custom urls includes go here # ... # Media files *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), ] + +# Conditionally include visualizations +if settings.VISUALIZATIONS_ENABLED: + urlpatterns += [ + path( + "visualizations/", + include("sds_gateway.visualizations.urls", namespace="visualizations"), + ), + ] if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development urlpatterns += staticfiles_urlpatterns() diff --git a/gateway/sds_gateway/conftest.py b/gateway/sds_gateway/conftest.py index b69a7f7b..47b5edd7 100644 --- a/gateway/sds_gateway/conftest.py +++ b/gateway/sds_gateway/conftest.py @@ -1,6 +1,7 @@ # ruff: noqa: E402 import os +from pathlib import Path import django import pytest @@ -11,7 +12,9 @@ # without calling setup we get the "apps aren't loaded yet" error django.setup() -# now we can import the models +# now we can import the models and settings +from django.conf import settings + from sds_gateway.users.models import User from sds_gateway.users.tests.factories import UserFactory @@ -24,3 +27,25 @@ def _media_storage(settings, tmpdir) -> None: @pytest.fixture def user(db) -> User: return UserFactory() + + +def pytest_collection_modifyitems(config, items): + """Deselect visualization tests entirely if feature is disabled.""" + if settings.VISUALIZATIONS_ENABLED: + return + + # Remove visualization tests from collection + deselected = [] + remaining = [] + + for item in items: + # Check if test is in visualizations module + item_path = Path(str(item.fspath)) + if "visualizations" in item_path.parts and "tests" in item_path.parts: + deselected.append(item) + else: + remaining.append(item) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining diff --git a/gateway/sds_gateway/templates/users/file_list.html b/gateway/sds_gateway/templates/users/file_list.html index eedd4392..60e70a4f 100644 --- a/gateway/sds_gateway/templates/users/file_list.html +++ b/gateway/sds_gateway/templates/users/file_list.html @@ -388,7 +388,9 @@ {% include "users/partials/capture_modal.html" %} - {% include "visualizations/partials/visualization_modal.html" with visualization_compatibility=visualization_compatibility %} + {% if VISUALIZATIONS_ENABLED %} + {% include "visualizations/partials/visualization_modal.html" with visualization_compatibility=visualization_compatibility %} + {% endif %} {% include "users/partials/web_download_modal.html" %} {% endblock body %} diff --git a/gateway/sds_gateway/templates/users/partials/capture_modal.html b/gateway/sds_gateway/templates/users/partials/capture_modal.html index e3a6bb4d..29366433 100644 --- a/gateway/sds_gateway/templates/users/partials/capture_modal.html +++ b/gateway/sds_gateway/templates/users/partials/capture_modal.html @@ -9,17 +9,19 @@ -{% include "visualizations/partials/visualization_modal.html" with visualization_compatibility=visualization_compatibility %} +{% if VISUALIZATIONS_ENABLED %} + {% include "visualizations/partials/visualization_modal.html" with visualization_compatibility=visualization_compatibility %} +{% endif %} diff --git a/gateway/sds_gateway/visualizations/tests/test_views.py b/gateway/sds_gateway/visualizations/tests/test_views.py index ea63d6f3..d7974875 100644 --- a/gateway/sds_gateway/visualizations/tests/test_views.py +++ b/gateway/sds_gateway/visualizations/tests/test_views.py @@ -3,9 +3,11 @@ import json from unittest.mock import patch +import pytest from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase +from django.urls import NoReverseMatch from django.urls import reverse from rest_framework import status @@ -39,10 +41,13 @@ def setUp(self) -> None: ) # Set up URL - self.waterfall_url = reverse( - "visualizations:waterfall", - kwargs={"capture_uuid": self.capture.uuid}, - ) + try: + self.waterfall_url = reverse( + "visualizations:waterfall", + kwargs={"capture_uuid": self.capture.uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") def test_waterfall_view_requires_login(self) -> None: """Test that the waterfall view requires login.""" @@ -72,10 +77,13 @@ def test_waterfall_view_capture_not_found_404(self) -> None: self.client.force_login(self.user) fake_uuid = "00000000-0000-0000-0000-000000000000" - fake_url = reverse( - "visualizations:waterfall", - kwargs={"capture_uuid": fake_uuid}, - ) + try: + fake_url = reverse( + "visualizations:waterfall", + kwargs={"capture_uuid": fake_uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") response = self.client.get(fake_url) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -142,20 +150,23 @@ def setUp(self) -> None: ) # Set up URL - self.create_waterfall_url = reverse( - "api:visualizations-create-waterfall", - kwargs={"pk": self.capture.uuid}, - ) + try: + self.create_waterfall_url = reverse( + "api:visualizations-create-waterfall", + kwargs={"pk": self.capture.uuid}, + ) - self.get_waterfall_status_url = reverse( - "api:visualizations-get-waterfall-status", - kwargs={"pk": self.capture.uuid}, - ) + self.get_waterfall_status_url = reverse( + "api:visualizations-get-waterfall-status", + kwargs={"pk": self.capture.uuid}, + ) - self.download_waterfall_url = reverse( - "api:visualizations-download-waterfall", - kwargs={"pk": self.capture.uuid}, - ) + self.download_waterfall_url = reverse( + "api:visualizations-download-waterfall", + kwargs={"pk": self.capture.uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") def test_create_waterfall_api_requires_authentication(self) -> None: """Test that create_waterfall API requires authentication.""" @@ -196,10 +207,13 @@ def test_create_waterfall_api_capture_not_found(self) -> None: self.client.force_login(self.user) fake_uuid = "00000000-0000-0000-0000-000000000000" - fake_url = reverse( - "api:visualizations-create-waterfall", - kwargs={"pk": fake_uuid}, - ) + try: + fake_url = reverse( + "api:visualizations-create-waterfall", + kwargs={"pk": fake_uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") response = self.client.post(fake_url) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -339,7 +353,6 @@ def setUp(self) -> None: password="testpassword", # noqa: S106 is_approved=True, ) - # Create test capture self.capture = Capture.objects.create( capture_type=CaptureType.DigitalRF, @@ -350,10 +363,13 @@ def setUp(self) -> None: ) # Set up URL - self.spectrogram_url = reverse( - "visualizations:spectrogram", - kwargs={"capture_uuid": self.capture.uuid}, - ) + try: + self.spectrogram_url = reverse( + "visualizations:spectrogram", + kwargs={"capture_uuid": self.capture.uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") def test_spectrogram_view_requires_login(self) -> None: """Test that the spectrogram view requires login.""" @@ -383,10 +399,13 @@ def test_spectrogram_view_capture_not_found_404(self) -> None: self.client.force_login(self.user) fake_uuid = "00000000-0000-0000-0000-000000000000" - fake_url = reverse( - "visualizations:spectrogram", - kwargs={"capture_uuid": fake_uuid}, - ) + try: + fake_url = reverse( + "visualizations:spectrogram", + kwargs={"capture_uuid": fake_uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") response = self.client.get(fake_url) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -453,20 +472,23 @@ def setUp(self) -> None: ) # Set up URLs - self.create_spectrogram_url = reverse( - "api:visualizations-create-spectrogram", - kwargs={"pk": self.capture.uuid}, - ) + try: + self.create_spectrogram_url = reverse( + "api:visualizations-create-spectrogram", + kwargs={"pk": self.capture.uuid}, + ) - self.get_spectrogram_status_url = reverse( - "api:visualizations-get-spectrogram-status", - kwargs={"pk": self.capture.uuid}, - ) + self.get_spectrogram_status_url = reverse( + "api:visualizations-get-spectrogram-status", + kwargs={"pk": self.capture.uuid}, + ) - self.download_spectrogram_url = reverse( - "api:visualizations-download-spectrogram", - kwargs={"pk": self.capture.uuid}, - ) + self.download_spectrogram_url = reverse( + "api:visualizations-download-spectrogram", + kwargs={"pk": self.capture.uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") def test_create_spectrogram_api_requires_authentication(self) -> None: """Test that create_spectrogram API requires authentication.""" @@ -519,10 +541,13 @@ def test_create_spectrogram_api_capture_not_found(self) -> None: self.client.force_login(self.user) fake_uuid = "00000000-0000-0000-0000-000000000000" - fake_url = reverse( - "api:visualizations-create-spectrogram", - kwargs={"pk": fake_uuid}, - ) + try: + fake_url = reverse( + "api:visualizations-create-spectrogram", + kwargs={"pk": fake_uuid}, + ) + except NoReverseMatch: + pytest.skip("Visualizations feature is disabled") response = self.client.post(fake_url) assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/sdk/docs/mkdocs/changelog.md b/sdk/docs/mkdocs/changelog.md index ec2eb54d..e05bfac8 100644 --- a/sdk/docs/mkdocs/changelog.md +++ b/sdk/docs/mkdocs/changelog.md @@ -2,7 +2,7 @@ ## `0.1.18` - YYYY-MM-DD -## `0.1.17` - 2025-12-19 +## `0.1.17` - YYYY-MM-DD + Fixes: + [**Improved file downloads**](https://github.com/spectrumx/sds-code/pull/236):