Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gateway/.envs/example/django.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# ====================== LOCAL ENV ======================
# GENERAL
# ------------------------------------------------------------------------------
DJANGO_DEBUG=true
USE_DOCKER=yes
HOSTNAME=localhost:8000
API_VERSION=v1
Expand Down
1 change: 1 addition & 0 deletions gateway/.envs/example/django.prod-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# GENERAL
# ------------------------------------------------------------------------------
# DJANGO_READ_DOT_ENV_FILE=true
DJANGO_DEBUG=false
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=
DJANGO_ADMIN_URL=
Expand Down
19 changes: 19 additions & 0 deletions gateway/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from environs import env

from config.settings.logs import ColoredFormatter
from config.settings.utils import guess_admin_console_env
from config.settings.utils import guess_max_web_download_size

__rng = random.SystemRandom()
Expand Down Expand Up @@ -591,6 +592,24 @@ def __get_random_token(length: int) -> str:
SDS_PROGRAMMATIC_SITE_NAME: str = env.str("SDS_PROGRAMMATIC_SITE_NAME", default="sds")
SDS_SITE_FQDN: str = env.str("SDS_SITE_FQDN", default="localhost")

# ADMIN_CONSOLE_ENV is used to visually distinguish between different environments
# (production, staging, local) in the admin console and error emails. It does not affect
# any functionality and it is meant to prevent changes in production meant for testing
# or development environments.
ADMIN_CONSOLE_ENV: str = (
env.str(
"SDS_ADMIN_CONSOLE_ENV",
default=guess_admin_console_env(is_debug=DEBUG),
)
.strip()
.lower()
)
# cast to known values
if ADMIN_CONSOLE_ENV in {"dev", "development"}:
ADMIN_CONSOLE_ENV = "local"
elif ADMIN_CONSOLE_ENV not in {"production", "staging", "local"}:
ADMIN_CONSOLE_ENV = "local" if DEBUG else "production"


def _get_brand_image_url() -> str | None:
"""Resolve brand image path from static files only.
Expand Down
45 changes: 39 additions & 6 deletions gateway/config/settings/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import os
from socket import gethostname

from loguru import logger as log
from sentry_sdk.types import Event
from sentry_sdk.types import Hint


def _is_staging_guess() -> bool:
"""Determines if the current environment is staging based on hostname."""
# any of these substrings in hostname or SENTRY_ENVIRONMENT
# is enough to hint a staging environment
staging_hints_lower = {
"staging",
"-qa",
".qa",
}
hostname: str = gethostname().lower()
is_staging_hostname = any(hint in hostname for hint in staging_hints_lower)
sentry_env = os.getenv("SENTRY_ENVIRONMENT", "").lower()
is_staging_sentry = any(hint in sentry_env for hint in staging_hints_lower)

is_staging = is_staging_hostname or is_staging_sentry
log.debug(f"{is_staging=}")

return is_staging


def guess_best_sentry_env() -> str:
_hostname: str = gethostname()
_is_staging: bool = "-qa" in _hostname or "-dev" in _hostname
return "staging" if _is_staging else "production"
return "staging" if _is_staging_guess() else "production"


def before_send(event: Event, hint: Hint) -> Event | None:
Expand Down Expand Up @@ -39,7 +58,21 @@ def guess_max_web_download_size() -> int:
- Production: 20GB
- Dev/QA (staging): 5GB
"""
_hostname: str = gethostname()
_is_staging: bool = "-qa" in _hostname or "-dev" in _hostname
# Production: 20GB, Staging (dev/qa): 5GB
return 20 * 1024 * 1024 * 1024 if not _is_staging else 5 * 1024 * 1024 * 1024
return (
20 * 1024 * 1024 * 1024 if not _is_staging_guess() else 5 * 1024 * 1024 * 1024
)


def guess_admin_console_env(*, is_debug: bool) -> str:
"""Determine the admin console environment label.

Returns:
str: One of "production", "staging", or "local".
"""
if _is_staging_guess():
return "staging"
if is_debug:
return "local"
# safer default
return "production"
31 changes: 17 additions & 14 deletions gateway/sds_gateway/api_methods/tests/test_capture_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,21 +275,25 @@ def _stub(*_args, **_kwargs):

return _stub

def _delete_owned_files(self) -> None:
"""Remove files owned by the primary test user after clearing relations."""

owned_files = list(File.objects.filter(owner=self.user))
for owned_file in owned_files:
owned_file.captures.clear()
owned_file.datasets.clear()

File.objects.filter(owner=self.user).update(
capture=None,
dataset=None,
)
File.objects.filter(owner=self.user).delete()

def tearDown(self) -> None:
"""Clean up test data."""

# remove temporary files linked during tests
for temp_file in getattr(self, "_temp_files", []):
temp_file_obj = File.objects.get(pk=temp_file.pk)
# Clear M2M relationships
temp_file_obj.captures.clear()
temp_file_obj.datasets.clear()
# Clear FK relationships
File.objects.filter(pk=temp_file.pk).update(
capture=None,
dataset=None,
)
File.objects.filter(pk=temp_file.pk).delete()
# remove files linked during tests, including ones created directly by tests
self._delete_owned_files()
self._temp_files.clear()

# Clean up OpenSearch documents
Expand Down Expand Up @@ -2294,8 +2298,7 @@ def test_waterfall_slices_stream_success(
assert data["end_index"] == 5 # noqa: PLR2004
mock_reconstruct_drf_files.assert_called_once()
mock_compute_slices_on_demand.assert_called_once()
# Avoid unused variable linter warning
assert stream_file.capture_id == capture.id
assert stream_file.capture_id == capture.pk


class OpenSearchErrorTestCases(APITestCase):
Expand Down
3 changes: 2 additions & 1 deletion gateway/sds_gateway/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from django.http import HttpRequest


def app_settings(_request: HttpRequest) -> dict[str, bool]:
def app_settings(_request: HttpRequest) -> dict[str, bool | str]:
"""Expose application-wide settings in templates."""
return {
"VISUALIZATIONS_ENABLED": settings.VISUALIZATIONS_ENABLED,
"ADMIN_CONSOLE_ENV": settings.ADMIN_CONSOLE_ENV,
}


Expand Down
27 changes: 27 additions & 0 deletions gateway/sds_gateway/static/css/admin_environment.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
body.admin-env-production #header {
background: #800020;
}

body.admin-env-staging #header {
background: #ff8c00;
}

body.admin-env-local #header {
background: #198754;
}

body.admin-env-production #header,
body.admin-env-production #header a:link,
body.admin-env-production #header a:visited,
body.admin-env-staging #header,
body.admin-env-staging #header a:link,
body.admin-env-staging #header a:visited,
body.admin-env-local #header,
body.admin-env-local #header a:link,
body.admin-env-local #header a:visited {
color: #ffffff;
}

.mono {
font-family: monospace;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* Manages the control panel for spectrogram generation settings
*/

import { DEFAULT_SPECTROGRAM_SETTINGS, INPUT_RANGES } from "./constants.js";

export class SpectrogramControls {
constructor() {
this.settings = { ...DEFAULT_SPECTROGRAM_SETTINGS };
this.settings = null;
this.defaultSettings = null;
this.inputRanges = {};
this.onSettingsChange = null;
this.onGenerateClick = null;
this.initializeControls();
Expand Down Expand Up @@ -40,6 +40,9 @@ export class SpectrogramControls {
return;
}

this.settings = this.getSettingsFromControls();
this.defaultSettings = { ...this.settings };
this.inputRanges = this.getInputRanges();
this.setupEventListeners();
this.updateControlValues();
}
Expand All @@ -57,7 +60,7 @@ export class SpectrogramControls {
// Standard Deviation change
this.stdDevInput.addEventListener("change", (e) => {
const value = Number.parseInt(e.target.value);
if (this.validateInput(value, INPUT_RANGES.stdDev)) {
if (this.validateInput(value, this.inputRanges.stdDev)) {
this.settings.stdDev = value;
this.notifySettingsChange();
} else {
Expand All @@ -68,7 +71,7 @@ export class SpectrogramControls {
// Hop Size change
this.hopSizeInput.addEventListener("change", (e) => {
const value = Number.parseInt(e.target.value);
if (this.validateInput(value, INPUT_RANGES.hopSize)) {
if (this.validateInput(value, this.inputRanges.hopSize)) {
this.settings.hopSize = value;
this.notifySettingsChange();
} else {
Expand All @@ -83,23 +86,50 @@ export class SpectrogramControls {
});
}

/**
* Read the initial settings rendered by the server
*/
getSettingsFromControls() {
return {
fftSize: Number.parseInt(this.fftSizeSelect.value, 10),
stdDev: Number.parseInt(this.stdDevInput.value, 10),
hopSize: Number.parseInt(this.hopSizeInput.value, 10),
colorMap: this.colorMapSelect.value,
};
}

/**
* Read numeric validation ranges from the current form inputs
*/
getInputRanges() {
return {
stdDev: {
min: Number.parseInt(this.stdDevInput.min, 10),
max: Number.parseInt(this.stdDevInput.max, 10),
},
hopSize: {
min: Number.parseInt(this.hopSizeInput.min, 10),
max: Number.parseInt(this.hopSizeInput.max, 10),
},
};
}

/**
* Validate input value against specified range
*/
validateInput(value, range) {
return value >= range.min && value <= range.max;
return Number.isInteger(value) && value >= range.min && value <= range.max;
}

/**
* Reset input value to last valid setting
*/
resetInputValue(input, value) {
input.value = value;
const range = this.inputRanges[input.id];
this.showValidationError(
input,
`Value must be between ${INPUT_RANGES[input.id].min} and ${
INPUT_RANGES[input.id].max
}`,
`Value must be between ${range.min} and ${range.max}`,
);
}

Expand Down Expand Up @@ -201,7 +231,7 @@ export class SpectrogramControls {
* Reset controls to default values
*/
resetToDefaults() {
this.settings = { ...DEFAULT_SPECTROGRAM_SETTINGS };
this.settings = { ...this.defaultSettings };
this.updateControlValues();
this.notifySettingsChange();
}
Expand Down
Loading
Loading