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 @@
Upload Result
{% 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):