diff --git a/.github/workflows/behave.yml b/.github/workflows/behave.yml index b72b0919..2cdb52b7 100644 --- a/.github/workflows/behave.yml +++ b/.github/workflows/behave.yml @@ -80,7 +80,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: coverage-report - path: coverage.xml + path: django/coverage.xml - name: Display Coverage Metrics if: matrix.python-version == '3.12' @@ -88,6 +88,7 @@ jobs: with: minimum_coverage: "50" report_name: "Django Pytest/Behave Coverage" + path: django/coverage.xml - name: Check Docker state (post-test) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 7139262f..7148d8d2 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -61,25 +61,26 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install uv pytest-github-actions-annotate-failures - uv pip install --system -r requirements/local.txt --prerelease=allow + uv pip install --system -r django/requirements/local.txt --prerelease=allow - name: Apply migrations env: DATABASE_URL: "postgres://scram:@localhost:5432/test_scram_${{ matrix.python-version }}" run: | + cd django/src python manage.py makemigrations --noinput python manage.py migrate --noinput - name: Check for missing migrations env: DATABASE_URL: "postgres://scram:@localhost:5432/test_scram_${{ matrix.python-version }}" - run: python manage.py makemigrations --check + run: cd django/src && python manage.py makemigrations --check - name: Run Pytest env: DATABASE_URL: "postgres://scram:@localhost:5432/test_scram_${{ matrix.python-version }}" REDIS_HOST: "localhost" - run: pytest + run: cd django/src && pytest - name: Install Scheduler Dependencies run: | diff --git a/.github/workflows/pytest_next_python.yml b/.github/workflows/pytest_next_python.yml index cb3e041f..59c7865b 100644 --- a/.github/workflows/pytest_next_python.yml +++ b/.github/workflows/pytest_next_python.yml @@ -56,7 +56,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install uv - uv pip install --system -r requirements/local.txt --prerelease=allow + uv pip install --system -r django/requirements/local.txt --prerelease=allow # https://github.com/pytest-dev/pytest-github-actions-annotate-failures/pull/68 isn't yet in a release uv pip install --system git+https://github.com/pytest-dev/pytest-github-actions-annotate-failures.git@6e66cd895fe05cd09be8bad58f5d79110a20385f @@ -64,11 +64,13 @@ jobs: env: DATABASE_URL: "postgres://scram:@localhost:5432/test_scram" run: | + cd django/src python manage.py makemigrations --noinput || true python manage.py migrate --fake-initial --noinput -v 2 - name: Check for duplicate migrations run: | + cd django/src if python manage.py makemigrations --dry-run | grep "No changes detected"; then echo "No duplicate migrations detected." else @@ -80,4 +82,4 @@ jobs: DATABASE_URL: "postgres://scram:@localhost:5432/test_scram" REDIS_HOST: "localhost" run: | - pytest || echo "::warning:: Failed on future Python version ${{ matrix.python-version }}." + cd django/src && pytest || echo "::warning:: Failed on future Python version ${{ matrix.python-version }}." diff --git a/.idea/runConfigurations/Django_Debugger.xml b/.idea/runConfigurations/Django_Debugger.xml index 83810494..850059c3 100644 --- a/.idea/runConfigurations/Django_Debugger.xml +++ b/.idea/runConfigurations/Django_Debugger.xml @@ -6,7 +6,7 @@ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 279e24a2..e5572b1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,13 +4,13 @@ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.12 hooks: - id: ruff-format - id: ruff-check @@ -26,7 +26,7 @@ repos: always_run: true - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.10.7 + rev: 0.11.8 hooks: - id: uv-lock args: [--check] diff --git a/.vscode/launch.json b/.vscode/launch.json index ccacdab1..d9abe396 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ }, "pathMappings": [ { - "localRoot": "${workspaceFolder}", + "localRoot": "${workspaceFolder}/django", "remoteRoot": "/app" } ] @@ -29,7 +29,7 @@ }, "pathMappings": [ { - "localRoot": "${workspaceFolder}", + "localRoot": "${workspaceFolder}/django", "remoteRoot": "/app" } ] diff --git a/Makefile b/Makefile index 061d7cac..75501f61 100644 --- a/Makefile +++ b/Makefile @@ -26,17 +26,17 @@ compose.override.yml: ## behave-all: runs behave inside the containers against all of your features .Phony: behave-all behave-all: compose.override.yml - @docker compose run --rm django coverage run -a manage.py behave --no-input --simple + @docker compose run --rm -w /app -e PYTHONPATH=/app/src django coverage run -a src/manage.py behave --no-input --simple ## behave: runs behave inside the containers against a specific feature (append FEATURE=feature_name_here) .Phony: behave behave: compose.override.yml - @docker compose run --rm django python manage.py behave --no-input --simple -i $(FEATURE) + @docker compose run --rm -w /app -e PYTHONPATH=/app/src django python src/manage.py behave --no-input --simple -i $(FEATURE) ## integration-tests: runs multi-instance system tests against docker compose running containers .Phony: integration-tests integration-tests: run - @docker compose exec -T django coverage run -a manage.py behave --no-input --use-existing-database scram/route_manager/tests/integration + @docker compose exec -T -w /app -e PYTHONPATH=/app/src django coverage run -a src/manage.py behave --no-input --use-existing-database src/scram/route_manager/tests/integration ## behave-translator .Phony: behave-translator @@ -52,8 +52,8 @@ build: compose.override.yml ## coverage.xml: generate coverage from test runs coverage.xml: pytest behave-all integration-tests behave-translator - @docker compose run --rm django coverage report - @docker compose run --rm django coverage xml + @docker compose run --rm -w /app django coverage report + @docker compose run --rm -w /app django coverage xml ## ci-test: runs all tests just like Github CI does .Phony: ci-test @@ -127,7 +127,7 @@ pass-reset: compose.override.yml ## pytest: runs pytest inside the containers .Phony: pytest pytest: compose.override.yml - @docker compose run --rm django coverage run -m pytest + @docker compose run --rm -w /app -e PYTHONPATH=/app/src django coverage run -m pytest ## pytest-scheduler: runs scheduler package tests with coverage .Phony: pytest-scheduler @@ -157,7 +157,7 @@ tail-log: compose.override.yml ## type-check: static type checking .Phony: type-check type-check: compose.override.yml - @docker compose run --rm django mypy scram + @docker compose run --rm -w /app -e PYTHONPATH=/app/src django mypy src/scram ## docs-build: build the documentation .Phony: docs-build diff --git a/compose.override.local.yml b/compose.override.local.yml index 44d7c4b6..2d6c754b 100644 --- a/compose.override.local.yml +++ b/compose.override.local.yml @@ -10,7 +10,7 @@ services: dockerfile: ./compose/local/django/Dockerfile image: scram_local_django volumes: - - $CI_PROJECT_DIR:/app:z + - $CI_PROJECT_DIR/django:/app:z - /tmp/profile_data:/tmp/profile_data env_file: - ./.envs/.local/.django @@ -31,7 +31,7 @@ services: dockerfile: ./compose/local/django/Dockerfile image: scram_local_django volumes: - - $CI_PROJECT_DIR:/app:z + - $CI_PROJECT_DIR/django:/app:z - /tmp/profile_data:/tmp/profile_data env_file: - ./.envs/.local/.django diff --git a/compose.override.production.yml b/compose.override.production.yml index 46e076b7..463320db 100644 --- a/compose.override.production.yml +++ b/compose.override.production.yml @@ -21,7 +21,7 @@ services: - path: '/etc/vault.d/secrets/kv_root_security.env' required: false volumes: - - ./staticfiles:/staticfiles + - ./django/staticfiles:/staticfiles healthcheck: test: ["CMD", "curl", "-f", "http://django:5000/process_updates/"] @@ -58,7 +58,7 @@ services: - ./compose/production/nginx/nginx.conf:/etc/nginx/conf.d/default.conf - /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem:/etc/ssl/server.crt - /etc/letsencrypt/live/${HOSTNAME}/privkey.pem:/etc/ssl/server.key - - ./staticfiles:/staticfiles + - ./django/staticfiles:/staticfiles ports: - "443:443" - "80:80" diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile index b2ba11c5..bf626e50 100644 --- a/compose/local/django/Dockerfile +++ b/compose/local/django/Dockerfile @@ -18,7 +18,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # Requirements are installed here to ensure they will be cached. -COPY ./requirements /requirements +COPY ./django/requirements /requirements RUN pip install uv RUN uv pip install --system -r /requirements/local.txt --prerelease=allow @@ -30,7 +30,7 @@ COPY ./compose/local/django/start /start RUN sed -i 's/\r$//g' /start RUN chmod +x /start -ENV PATH="$PATH:/app" -WORKDIR /app +ENV PATH="$PATH:/app/src" +WORKDIR /app/src ENTRYPOINT ["/entrypoint"] diff --git a/compose/local/docs/Dockerfile b/compose/local/docs/Dockerfile index a7ac21e6..b4890a71 100644 --- a/compose/local/docs/Dockerfile +++ b/compose/local/docs/Dockerfile @@ -24,7 +24,7 @@ RUN apt-get update \ # Only re-run the pip install if these files have changed -COPY requirements/base.txt requirements/local.txt requirements/production.txt /app/requirements/ +COPY django/requirements/base.txt django/requirements/local.txt django/requirements/production.txt /app/requirements/ RUN pip install uv RUN uv pip install --system -r /app/requirements/local.txt -r /app/requirements/production.txt --prerelease=allow diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile index a83f74d1..5d05ddc9 100644 --- a/compose/production/django/Dockerfile +++ b/compose/production/django/Dockerfile @@ -21,7 +21,7 @@ RUN addgroup --system django \ && adduser --system --ingroup django django # Requirements are installed here to ensure they will be cached. -COPY ./requirements/ /requirements +COPY ./django/requirements/ /requirements RUN pip install uv RUN uv pip install --system --no-cache-dir -r /requirements/production.txt \ && rm -rf /requirements @@ -34,10 +34,10 @@ COPY --chown=django:django ./compose/production/django/start /start RUN sed -i 's/\r$//g' /start RUN chmod +x /start -COPY --chown=django:django . /app +COPY --chown=django:django ./django /app USER django -WORKDIR /app +WORKDIR /app/src ENTRYPOINT ["/entrypoint"] diff --git a/compose/production/django/start b/compose/production/django/start index ba07ddd4..5d4fa09c 100644 --- a/compose/production/django/start +++ b/compose/production/django/start @@ -5,7 +5,7 @@ set -o pipefail set -o nounset -python /app/manage.py collectstatic --noinput +python /app/src/manage.py collectstatic --noinput -/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker +/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app/src -k uvicorn.workers.UvicornWorker diff --git a/config/routing.py b/config/routing.py deleted file mode 100644 index 10a63105..00000000 --- a/config/routing.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Define URLs for the WebSocket consumers.""" - -from django.urls import re_path - -from . import consumers - -websocket_urlpatterns = [ - re_path(r"ws/route_manager/translator_(?P\w+)/$", consumers.TranslatorConsumer.as_asgi()), - re_path(r"ws/route_manager/webui_(?P\w+)/$", consumers.WebUIConsumer.as_asgi()), -] diff --git a/locale/README.rst b/django/locale/README.rst similarity index 100% rename from locale/README.rst rename to django/locale/README.rst diff --git a/django/pyproject.toml b/django/pyproject.toml new file mode 100644 index 00000000..3556c13d --- /dev/null +++ b/django/pyproject.toml @@ -0,0 +1,83 @@ +[project] +name = "scram-django" +version = "1.5.1" + +# ==== pytest ==== +[tool.pytest.ini_options] +addopts = [ + "--ds=config.settings.test", + "--reuse-db", +] +minversion = "6.0" +pythonpath = ["src"] +python_files = [ + "tests.py", + "test_*.py", +] + +# ==== Coverage ==== +[tool.coverage.run] +branch = true +data_file = "coverage.coverage" +include = ["src/scram/*", "src/config/*"] +omit = ["**/migrations/*", "src/scram/contrib/*", "*/tests/*"] +plugins = ["django_coverage_plugin"] + +[tool.coverage.report] +exclude_also = [ + "if debug:", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] + +# ===== ruff ==== +# Base ruff config is inherited from the root pyproject.toml. +[tool.ruff] +exclude = ["migrations"] + +[tool.ruff.lint.per-file-ignores] +"src/scram/route_manager/**" = [ + "DOC201", # documenting return values +] +"src/scram/users/**" = [ + "DOC201", # documenting return values + "FBT001", # minimal issue; don't need to mess with in the User app + "PLR2004", # magic values when checking HTTP status codes +] +"test.py" = [ + "S105", # hardcoded password as argument +] + +# ==== mypy ==== +[tool.mypy] +check_untyped_defs = true +exclude = ["src/scram/route_manager/tests"] +ignore_missing_imports = true +plugins = [ + "mypy_django_plugin.main", + # We did used to have mypy_drf_plugin.main but it seems like it never actually worked... +] +python_version = "3.12" +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true + +[[tool.mypy.overrides]] +# Django migrations should not produce any errors: +ignore_errors = true +module = "*.migrations.*" + +[tool.django-stubs] +django_settings_module = "config.settings.test" + +# ==== behave ==== +[tool.behave] +paths = ["src/scram/route_manager/tests/acceptance"] +stderr_capture = false + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements/base.txt b/django/requirements/base.txt similarity index 100% rename from requirements/base.txt rename to django/requirements/base.txt diff --git a/requirements/local.txt b/django/requirements/local.txt similarity index 100% rename from requirements/local.txt rename to django/requirements/local.txt diff --git a/requirements/production.txt b/django/requirements/production.txt similarity index 100% rename from requirements/production.txt rename to django/requirements/production.txt diff --git a/config/__init__.py b/django/src/config/__init__.py similarity index 100% rename from config/__init__.py rename to django/src/config/__init__.py diff --git a/config/api_router.py b/django/src/config/api_router.py similarity index 100% rename from config/api_router.py rename to django/src/config/api_router.py diff --git a/config/asgi.py b/django/src/config/asgi.py similarity index 81% rename from config/asgi.py rename to django/src/config/asgi.py index 5e11d1fa..a93dcdbe 100644 --- a/config/asgi.py +++ b/django/src/config/asgi.py @@ -9,8 +9,6 @@ import logging import os -import sys -from pathlib import Path # TODO: from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter @@ -25,11 +23,15 @@ if debug: logger.info("Django is set to use a debugger. Provided debug mode: %s", debug) if debug == "pycharm-pydevd": - logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") + logger.info( + "Entering debug mode for pycharm, make sure the debug server is running in PyCharm!" + ) import pydevd_pycharm - pydevd_pycharm.settrace("host.docker.internal", port=56783, stdoutToServer=True, stderrToServer=True) + pydevd_pycharm.settrace( + "host.docker.internal", port=56783, stdoutToServer=True, stderrToServer=True + ) logger.info("Debugger started.") elif debug == "debugpy": @@ -43,11 +45,6 @@ else: logger.warning("Invalid debug mode given: %s. Debugger not started", debug) -# This allows easy placement of apps within the interior -# scram directory. -ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent -sys.path.append(str(ROOT_DIR / "scram")) - # If DJANGO_SETTINGS_MODULE is unset, default to the local settings os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") diff --git a/config/consumers.py b/django/src/config/consumers.py similarity index 92% rename from config/consumers.py rename to django/src/config/consumers.py index 1b47b6e7..73efd345 100644 --- a/config/consumers.py +++ b/django/src/config/consumers.py @@ -33,7 +33,9 @@ def update_connect_cache(): # Filter WebSocketSequenceElements by actiontype elements = await database_sync_to_async(list)( - WebSocketSequenceElement.objects.filter(action_type__name=self.actiontype).order_by("order_num"), + WebSocketSequenceElement.objects.filter( + action_type__name=self.actiontype + ).order_by("order_num"), ) if not elements: logger.warning("No elements found for actiontype=%s.", self.actiontype) @@ -46,7 +48,9 @@ def update_connect_cache(): for route in routes: for element in elements: - msg = await database_sync_to_async(lambda e: e.websocketmessage)(element) + msg = await database_sync_to_async(lambda e: e.websocketmessage)( + element + ) msg.msg_data[msg.msg_data_route_field] = str(route) await self.send_json({"type": msg.msg_type, "message": msg.msg_data}) @@ -76,7 +80,12 @@ async def receive_json(self, content): "last_seen": time.time(), } cache_key = f"translator_stats:{self.actiontype}" - logger.info("Received heartbeat for %s: %s (Key: %s)", self.actiontype, stats, cache_key) + logger.info( + "Received heartbeat for %s: %s (Key: %s)", + self.actiontype, + stats, + cache_key, + ) await database_sync_to_async(cache.set)(cache_key, stats, timeout=300) elif content["type"] == "translator_check_resp": # We received a check response from a translator, forward to web UI. diff --git a/django/src/config/routing.py b/django/src/config/routing.py new file mode 100644 index 00000000..9b1d1d2c --- /dev/null +++ b/django/src/config/routing.py @@ -0,0 +1,16 @@ +"""Define URLs for the WebSocket consumers.""" + +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path( + r"ws/route_manager/translator_(?P\w+)/$", + consumers.TranslatorConsumer.as_asgi(), + ), + re_path( + r"ws/route_manager/webui_(?P\w+)/$", + consumers.WebUIConsumer.as_asgi(), + ), +] diff --git a/config/settings/__init__.py b/django/src/config/settings/__init__.py similarity index 100% rename from config/settings/__init__.py rename to django/src/config/settings/__init__.py diff --git a/config/settings/base.py b/django/src/config/settings/base.py similarity index 94% rename from config/settings/base.py rename to django/src/config/settings/base.py index f60695be..71cf94f8 100644 --- a/config/settings/base.py +++ b/django/src/config/settings/base.py @@ -9,9 +9,8 @@ logger = logging.getLogger(__name__) -ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent -# scram/ -APPS_DIR = ROOT_DIR / "scram" +ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent.parent +APPS_DIR = ROOT_DIR / "src" / "scram" env = environ.Env() READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) @@ -112,7 +111,9 @@ ] # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, @@ -217,7 +218,9 @@ # EMAIL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend") +EMAIL_BACKEND = env( + "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend" +) # https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout EMAIL_TIMEOUT = 5 @@ -239,7 +242,9 @@ "version": 1, "disable_existing_loggers": False, "formatters": { - "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"}, + "verbose": { + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s" + }, }, "handlers": { "console": { @@ -258,7 +263,9 @@ "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "expiry": 86400 * 7, # expire messages after a week (default 60s) - "group_expiry": 86400 * 365 * 10, # effectively disable removing from a group (default 1d) + "group_expiry": 86400 + * 365 + * 10, # effectively disable removing from a group (default 1d) "hosts": [(os.environ.get("REDIS_HOST", "redis"), 6379)], }, }, @@ -317,7 +324,9 @@ MIDDLEWARE += ["mozilla_django_oidc.middleware.SessionRefresh"] # Extend middleware to add OIDC auth backend - AUTHENTICATION_BACKENDS += ["scram.route_manager.authentication_backends.ESnetAuthBackend"] + AUTHENTICATION_BACKENDS += [ + "scram.route_manager.authentication_backends.ESnetAuthBackend" + ] # https://docs.djangoproject.com/en/dev/ref/settings/#login-url LOGIN_URL = "oidc_authentication_init" @@ -350,7 +359,9 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#logout-url LOGOUT_URL = "local_auth:logout" else: - msg = f"Invalid authentication method: {AUTH_METHOD}. Please choose 'local' or 'oidc'" + msg = ( + f"Invalid authentication method: {AUTH_METHOD}. Please choose 'local' or 'oidc'" + ) raise ValueError(msg) @@ -378,7 +389,12 @@ SCRAM_DENIED_GROUPS = ["svc_scram_denied"] # This is the set of all the groups -SCRAM_GROUPS = SCRAM_ADMIN_GROUPS + SCRAM_READWRITE_GROUPS + SCRAM_READONLY_GROUPS + SCRAM_DENIED_GROUPS +SCRAM_GROUPS = ( + SCRAM_ADMIN_GROUPS + + SCRAM_READWRITE_GROUPS + + SCRAM_READONLY_GROUPS + + SCRAM_DENIED_GROUPS +) # How many entries to show PER Actiontype on the home page RECENT_LIMIT = 10 diff --git a/config/settings/local.py b/django/src/config/settings/local.py similarity index 94% rename from config/settings/local.py rename to django/src/config/settings/local.py index 5e4d7802..6ee28f09 100644 --- a/config/settings/local.py +++ b/django/src/config/settings/local.py @@ -30,7 +30,9 @@ # EMAIL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend") +EMAIL_BACKEND = env( + "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" +) # WhiteNoise # ------------------------------------------------------------------------------ @@ -70,7 +72,9 @@ # Your stuff... # ------------------------------------------------------------------------------ -REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ("rest_framework.permissions.IsAdminUser",) +REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = ( + "rest_framework.permissions.IsAdminUser", +) # Behave Django testing framework INSTALLED_APPS += ["behave_django"] diff --git a/config/settings/production.py b/django/src/config/settings/production.py similarity index 94% rename from config/settings/production.py rename to django/src/config/settings/production.py index c1592ab0..019a0dae 100644 --- a/config/settings/production.py +++ b/django/src/config/settings/production.py @@ -54,11 +54,15 @@ # TODO: set this to 60 seconds first and then to 518400 once you prove the former works SECURE_HSTS_SECONDS = 60 # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains -SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) +SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( + "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True +) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff -SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) +SECURE_CONTENT_TYPE_NOSNIFF = env.bool( + "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True +) # STATIC # ------------------------ @@ -116,7 +120,9 @@ "disable_existing_loggers": False, "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, "formatters": { - "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"}, + "verbose": { + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s" + }, }, "handlers": { "mail_admins": { diff --git a/config/settings/test.py b/django/src/config/settings/test.py similarity index 80% rename from config/settings/test.py rename to django/src/config/settings/test.py index e1201960..eb1ce936 100644 --- a/config/settings/test.py +++ b/django/src/config/settings/test.py @@ -41,10 +41,18 @@ # Your stuff... # ------------------------------------------------------------------------------ # These variables are required by the ESnetAuthBackend called in our OidcTest case -OIDC_OP_JWKS_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/certs" -OIDC_OP_AUTHORIZATION_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/auth" -OIDC_OP_TOKEN_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/token" -OIDC_OP_USER_ENDPOINT = "https://example.com/auth/realms/example/protocol/openid-connect/userinfo" +OIDC_OP_JWKS_ENDPOINT = ( + "https://example.com/auth/realms/example/protocol/openid-connect/certs" +) +OIDC_OP_AUTHORIZATION_ENDPOINT = ( + "https://example.com/auth/realms/example/protocol/openid-connect/auth" +) +OIDC_OP_TOKEN_ENDPOINT = ( + "https://example.com/auth/realms/example/protocol/openid-connect/token" +) +OIDC_OP_USER_ENDPOINT = ( + "https://example.com/auth/realms/example/protocol/openid-connect/userinfo" +) OIDC_RP_SIGN_ALGO = "RS256" OIDC_RP_CLIENT_ID = "" OIDC_RP_CLIENT_SECRET = "" diff --git a/config/urls.py b/django/src/config/urls.py similarity index 85% rename from config/urls.py rename to django/src/config/urls.py index 41a45d12..b02f6e7f 100644 --- a/config/urls.py +++ b/django/src/config/urls.py @@ -6,7 +6,11 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, path from django.views import defaults as default_views -from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) from rest_framework.authtoken.views import obtain_auth_token from .api_router import app_name @@ -33,7 +37,9 @@ urlpatterns += [path("oidc/", include("mozilla_django_oidc.urls"))] elif settings.AUTH_METHOD == "local": - urlpatterns += [path("auth/", include("scram.local_auth.urls", namespace="local_auth"))] + urlpatterns += [ + path("auth/", include("scram.local_auth.urls", namespace="local_auth")) + ] # API URLS api_version_urls = ( [ @@ -51,8 +57,14 @@ # Swagger OpenAPI URLs urlpatterns += [ path("schema/", SpectacularAPIView.as_view(), name="schema"), - path("schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), - path("schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), + path( + "schema/swagger-ui/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), + path( + "schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc" + ), ] if settings.DEBUG: diff --git a/config/websocket.py b/django/src/config/websocket.py similarity index 100% rename from config/websocket.py rename to django/src/config/websocket.py diff --git a/config/wsgi.py b/django/src/config/wsgi.py similarity index 86% rename from config/wsgi.py rename to django/src/config/wsgi.py index 1ebc25c0..9d10a91b 100644 --- a/config/wsgi.py +++ b/django/src/config/wsgi.py @@ -14,16 +14,9 @@ """ import os -import sys -from pathlib import Path from django.core.wsgi import get_wsgi_application -# This allows easy placement of apps within the interior -# scram directory. - -ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent -sys.path.append(str(ROOT_DIR / "scram")) # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use diff --git a/manage.py b/django/src/manage.py similarity index 77% rename from manage.py rename to django/src/manage.py index cbc80d8d..1ecbd66d 100755 --- a/manage.py +++ b/django/src/manage.py @@ -3,7 +3,6 @@ import os import sys -from pathlib import Path def main(): @@ -21,11 +20,6 @@ def main(): msg, ) from exc - # This allows easy placement of apps within the interior - # scram directory. - current_path = Path(__file__).parent.resolve() - sys.path.append(str(current_path / "scram")) - execute_from_command_line(sys.argv) diff --git a/django/src/scram/__init__.py b/django/src/scram/__init__.py new file mode 100644 index 00000000..bf9962a9 --- /dev/null +++ b/django/src/scram/__init__.py @@ -0,0 +1,7 @@ +"""The Django project for Security Catch and Release Automation Manager (SCRAM).""" + +__version__ = "1.5.1" +__version_info__ = tuple( + int(num) if num.isdigit() else num + for num in __version__.replace("-", ".", 1).split(".") +) # noqa: RUF067 diff --git a/scram/conftest.py b/django/src/scram/conftest.py similarity index 100% rename from scram/conftest.py rename to django/src/scram/conftest.py diff --git a/scram/contrib/__init__.py b/django/src/scram/contrib/__init__.py similarity index 100% rename from scram/contrib/__init__.py rename to django/src/scram/contrib/__init__.py diff --git a/scram/contrib/sites/__init__.py b/django/src/scram/contrib/sites/__init__.py similarity index 100% rename from scram/contrib/sites/__init__.py rename to django/src/scram/contrib/sites/__init__.py diff --git a/scram/contrib/sites/migrations/0001_initial.py b/django/src/scram/contrib/sites/migrations/0001_initial.py similarity index 100% rename from scram/contrib/sites/migrations/0001_initial.py rename to django/src/scram/contrib/sites/migrations/0001_initial.py diff --git a/scram/contrib/sites/migrations/0002_alter_domain_unique.py b/django/src/scram/contrib/sites/migrations/0002_alter_domain_unique.py similarity index 100% rename from scram/contrib/sites/migrations/0002_alter_domain_unique.py rename to django/src/scram/contrib/sites/migrations/0002_alter_domain_unique.py diff --git a/scram/contrib/sites/migrations/0003_set_site_domain_and_name.py b/django/src/scram/contrib/sites/migrations/0003_set_site_domain_and_name.py similarity index 100% rename from scram/contrib/sites/migrations/0003_set_site_domain_and_name.py rename to django/src/scram/contrib/sites/migrations/0003_set_site_domain_and_name.py diff --git a/scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py b/django/src/scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py similarity index 100% rename from scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py rename to django/src/scram/contrib/sites/migrations/0004_alter_options_ordering_domain.py diff --git a/scram/contrib/sites/migrations/__init__.py b/django/src/scram/contrib/sites/migrations/__init__.py similarity index 100% rename from scram/contrib/sites/migrations/__init__.py rename to django/src/scram/contrib/sites/migrations/__init__.py diff --git a/scram/local_auth/__init__.py b/django/src/scram/local_auth/__init__.py similarity index 100% rename from scram/local_auth/__init__.py rename to django/src/scram/local_auth/__init__.py diff --git a/scram/local_auth/urls.py b/django/src/scram/local_auth/urls.py similarity index 73% rename from scram/local_auth/urls.py rename to django/src/scram/local_auth/urls.py index 56c7b727..69094a2a 100644 --- a/scram/local_auth/urls.py +++ b/django/src/scram/local_auth/urls.py @@ -8,7 +8,9 @@ urlpatterns = [ path( "login/", - LoginView.as_view(template_name="local_auth/login.html", success_url="route_manager:home"), + LoginView.as_view( + template_name="local_auth/login.html", success_url="route_manager:home" + ), name="login", ), path("logout/", LogoutView.as_view(), name="logout"), diff --git a/scram/route_manager/__init__.py b/django/src/scram/route_manager/__init__.py similarity index 100% rename from scram/route_manager/__init__.py rename to django/src/scram/route_manager/__init__.py diff --git a/scram/route_manager/admin.py b/django/src/scram/route_manager/admin.py similarity index 100% rename from scram/route_manager/admin.py rename to django/src/scram/route_manager/admin.py diff --git a/scram/route_manager/api/__init__.py b/django/src/scram/route_manager/api/__init__.py similarity index 100% rename from scram/route_manager/api/__init__.py rename to django/src/scram/route_manager/api/__init__.py diff --git a/scram/route_manager/api/exceptions.py b/django/src/scram/route_manager/api/exceptions.py similarity index 92% rename from scram/route_manager/api/exceptions.py rename to django/src/scram/route_manager/api/exceptions.py index 297b4477..1793101a 100644 --- a/scram/route_manager/api/exceptions.py +++ b/django/src/scram/route_manager/api/exceptions.py @@ -18,7 +18,9 @@ class IgnoredRoute(APIException): """An operation attempted to add a route that overlaps with a route on the ignore list.""" status_code = 400 - default_detail = "This CIDR is on the ignore list. You are not allowed to add it here." + default_detail = ( + "This CIDR is on the ignore list. You are not allowed to add it here." + ) default_code = "ignored_route" diff --git a/scram/route_manager/api/serializers.py b/django/src/scram/route_manager/api/serializers.py similarity index 97% rename from scram/route_manager/api/serializers.py rename to django/src/scram/route_manager/api/serializers.py index be12bca4..10e4eafe 100644 --- a/scram/route_manager/api/serializers.py +++ b/django/src/scram/route_manager/api/serializers.py @@ -82,7 +82,9 @@ class EntrySerializer(serializers.HyperlinkedModelSerializer): else: who = serializers.CharField() comment = serializers.CharField() - originating_scram_instance = serializers.CharField(default="scram_hostname_not_set", read_only=True) + originating_scram_instance = serializers.CharField( + default="scram_hostname_not_set", read_only=True + ) is_active = serializers.BooleanField(default=True, read_only=True) def __init__(self, *args, **kwargs): diff --git a/scram/route_manager/api/views.py b/django/src/scram/route_manager/api/views.py similarity index 80% rename from scram/route_manager/api/views.py rename to django/src/scram/route_manager/api/views.py index 51ee4f89..11c6080a 100644 --- a/scram/route_manager/api/views.py +++ b/django/src/scram/route_manager/api/views.py @@ -23,8 +23,21 @@ from scram import __version__ as scram_version -from ..models import ActionType, Client, Entry, IgnoreEntry, Route, WebSocketSequenceElement -from .exceptions import ActiontypeNotAllowed, IgnoredRoute, NoActiveEntryFound, PrefixTooLarge + +from ..models import ( + ActionType, + Client, + Entry, + IgnoreEntry, + Route, + WebSocketSequenceElement, +) +from .exceptions import ( + ActiontypeNotAllowed, + IgnoredRoute, + NoActiveEntryFound, + PrefixTooLarge, +) from .serializers import ( ActionTypeSerializer, ClientSerializer, @@ -40,8 +53,13 @@ @extend_schema( description="API endpoint for actiontypes.", responses={ - 200: OpenApiResponse(response=ActionTypeSerializer, description="Successful retrieval of actiontype(s)."), - 403: OpenApiResponse(description="Authentication credentials were not provided."), + 200: OpenApiResponse( + response=ActionTypeSerializer, + description="Successful retrieval of actiontype(s).", + ), + 403: OpenApiResponse( + description="Authentication credentials were not provided." + ), 404: OpenApiResponse(description="The requested actiontype does not exist."), }, ) @@ -58,12 +76,18 @@ class ActionTypeViewSet(viewsets.ReadOnlyModelViewSet): description="API endpoint for ignore entries.", responses={ 200: OpenApiResponse( - response=IgnoreEntrySerializer, description="Successful retrieval or update of an ignore entry." + response=IgnoreEntrySerializer, + description="Successful retrieval or update of an ignore entry.", + ), + 201: OpenApiResponse( + response=IgnoreEntrySerializer, + description="Ignore entry successfully created.", ), - 201: OpenApiResponse(response=IgnoreEntrySerializer, description="Ignore entry successfully created."), 204: OpenApiResponse(description="Ignore entry successfully deleted."), 400: OpenApiResponse(description="Invalid data provided."), - 403: OpenApiResponse(description="Authentication credentials were not provided."), + 403: OpenApiResponse( + description="Authentication credentials were not provided." + ), 404: OpenApiResponse(description="The requested ignore entry does not exist."), }, ) @@ -83,7 +107,9 @@ class IgnoreEntryViewSet(viewsets.ModelViewSet): response=ClientSerializer, description="Client already existed and was retrieved successfully.", ), - 201: OpenApiResponse(response=ClientSerializer, description="Client successfully created."), + 201: OpenApiResponse( + response=ClientSerializer, description="Client successfully created." + ), 400: OpenApiResponse( description="Client with this name already exists with a different UUID, or client_name was not provided." ), @@ -105,7 +131,10 @@ def create(self, request, *args, **kwargs): request_uuid = request.data.get("uuid") if not client_name: - return Response({"detail": "client_name is required."}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"detail": "client_name is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) existing_client = self.queryset.filter(client_name=client_name).first() @@ -141,7 +170,8 @@ def create(self, request, *args, **kwargs): ], responses={ 200: OpenApiResponse( - response=IsActiveSerializer, description="The 'is_active' field indicates the status of the route." + response=IsActiveSerializer, + description="The 'is_active' field indicates the status of the route.", ), 400: OpenApiResponse(description="The 'cidr' parameter is missing or invalid."), }, @@ -164,25 +194,34 @@ def get_queryset(self): try: normalized_cidr = ipaddress.ip_network(cidr, strict=False) except ValueError: - raise ValidationError(detail={"error": "invalid ip address or network"}) from None + raise ValidationError( + detail={"error": "invalid ip address or network"} + ) from None self.normalization_warning = None self.normalized_cidr_for_response = normalized_cidr if str(cidr) != str(normalized_cidr): # save the warning so we can use it in the list response - self.normalization_warning = ( - f"Input CIDR '{cidr}' was not canonical and was normalized to '{normalized_cidr!s}' for the search." - ) + self.normalization_warning = f"Input CIDR '{cidr}' was not canonical and was normalized to '{normalized_cidr!s}' for the search." - return Entry.objects.filter(route__route__net_contained_or_equal=normalized_cidr, is_active=True) + return Entry.objects.filter( + route__route__net_contained_or_equal=normalized_cidr, is_active=True + ) def list(self, request): """Override the list function to just return a boolean instead of other metadata.""" queryset = self.get_queryset() if not queryset.exists() and hasattr(self, "normalized_cidr_for_response"): - response_data = {"results": [{"is_active": False, "route": str(self.normalized_cidr_for_response)}]} + response_data = { + "results": [ + { + "is_active": False, + "route": str(self.normalized_cidr_for_response), + } + ] + } else: serializer = self.get_serializer(queryset, many=True) response_data = {"results": serializer.data} @@ -194,11 +233,20 @@ def list(self, request): @extend_schema( description="API endpoint for entries.", responses={ - 200: OpenApiResponse(response=EntrySerializer, description="Successful retrieval of an entry/entries."), - 201: OpenApiResponse(response=EntrySerializer, description="Entry successfully created."), + 200: OpenApiResponse( + response=EntrySerializer, + description="Successful retrieval of an entry/entries.", + ), + 201: OpenApiResponse( + response=EntrySerializer, description="Entry successfully created." + ), 204: OpenApiResponse(description="Entry successfully deleted."), - 400: OpenApiResponse(description="The route is likely on the ignore list or the prefix is too large."), - 403: OpenApiResponse(description="The client is not authorized for this action."), + 400: OpenApiResponse( + description="The route is likely on the ignore list or the prefix is too large." + ), + 403: OpenApiResponse( + description="The client is not authorized for this action." + ), 404: OpenApiResponse(description="The requested entry does not exist."), }, ) @@ -232,15 +280,21 @@ def check_client_authorization(self, actiontype): raise PermissionDenied(msg) from client_dne # Check if client is authorized for the action type - if not client.is_authorized or actiontype not in client.authorized_actiontypes.values_list( - "name", flat=True + if ( + not client.is_authorized + or actiontype + not in client.authorized_actiontypes.values_list("name", flat=True) ): logger.debug( "Client: %s, actiontypes: %s", uuid, list(client.authorized_actiontypes.values_list("name", flat=True)), ) - logger.info("%s is not allowed to add an entry to the %s list.", uuid, actiontype) + logger.info( + "%s is not allowed to add an entry to the %s list.", + uuid, + actiontype, + ) raise ActiontypeNotAllowed elif not self.request.user.has_perm("route_manager.can_add_entry"): @@ -251,8 +305,15 @@ def check_ignore_list(route): """Ensure that we're not trying to block something from the ignore list.""" overlapping_ignore = IgnoreEntry.objects.filter(route__net_overlaps=route) if overlapping_ignore.count(): - ignore_entries = [str(ignore_entry["route"]) for ignore_entry in overlapping_ignore.values()] - logger.info("Cannot proceed adding %s. The ignore list contains %s.", route, ignore_entries) + ignore_entries = [ + str(ignore_entry["route"]) + for ignore_entry in overlapping_ignore.values() + ] + logger.info( + "Cannot proceed adding %s. The ignore list contains %s.", + route, + ignore_entries, + ) raise IgnoredRoute def perform_create(self, serializer): @@ -279,7 +340,9 @@ def perform_create(self, serializer): self.check_client_authorization(actiontype) self.check_ignore_list(route_instance) - elements = WebSocketSequenceElement.objects.filter(action_type__name=actiontype).order_by("order_num") + elements = WebSocketSequenceElement.objects.filter( + action_type__name=actiontype + ).order_by("order_num") if not elements: logger.warning("No elements found for actiontype: %s", actiontype) @@ -317,7 +380,10 @@ def perform_update(self, serializer): msg = "You can only update your own entries" raise PermissionDenied(msg) - serializer.save(who=serializer.instance.who, originating_scram_instance=settings.SCRAM_HOSTNAME) + serializer.save( + who=serializer.instance.who, + originating_scram_instance=settings.SCRAM_HOSTNAME, + ) entry = serializer.instance update_change_reason(entry, comment) @@ -418,8 +484,14 @@ def _get_translator_stats() -> dict[str, dict]: # Filter out stale heartbeats (e.g., > 90s) now = time.time() active_bgp_stat = {"v4": 0, "v6": 0} - if bgp_stats and now - bgp_stats["last_seen"] < translator_heartbeat_timeout: - active_bgp_stat = {"v4": bgp_stats["v4_count"], "v6": bgp_stats["v6_count"]} + if ( + bgp_stats + and now - bgp_stats["last_seen"] < translator_heartbeat_timeout + ): + active_bgp_stat = { + "v4": bgp_stats["v4_count"], + "v6": bgp_stats["v6_count"], + } translator_stats[at.name] = { "count": count, @@ -436,8 +508,7 @@ def _get_entries_stats() -> tuple[int, int]: entries_stats = {} try: counts = ( - Entry.objects - .filter(is_active=True) + Entry.objects.filter(is_active=True) .values("actiontype__name") .annotate(count=Count("id")) .order_by("actiontype__name") @@ -460,7 +531,11 @@ def get(request): if "error" in value: overall_status = "unhealthy" - http_status = status.HTTP_200_OK if overall_status == "healthy" else status.HTTP_503_SERVICE_UNAVAILABLE + http_status = ( + status.HTTP_200_OK + if overall_status == "healthy" + else status.HTTP_503_SERVICE_UNAVAILABLE + ) return Response( { diff --git a/scram/route_manager/apps.py b/django/src/scram/route_manager/apps.py similarity index 100% rename from scram/route_manager/apps.py rename to django/src/scram/route_manager/apps.py diff --git a/scram/route_manager/authentication_backends.py b/django/src/scram/route_manager/authentication_backends.py similarity index 100% rename from scram/route_manager/authentication_backends.py rename to django/src/scram/route_manager/authentication_backends.py diff --git a/scram/route_manager/context_processors.py b/django/src/scram/route_manager/context_processors.py similarity index 85% rename from scram/route_manager/context_processors.py rename to django/src/scram/route_manager/context_processors.py index e08a2fc5..89768dd9 100644 --- a/scram/route_manager/context_processors.py +++ b/django/src/scram/route_manager/context_processors.py @@ -27,5 +27,8 @@ def active_count(request): if "admin" not in request.META["PATH_INFO"]: active_block_entries = Entry.objects.filter(is_active=True).count() total_block_entries = Entry.objects.all().count() - return {"active_block_entries": active_block_entries, "total_block_entries": total_block_entries} + return { + "active_block_entries": active_block_entries, + "total_block_entries": total_block_entries, + } return {} diff --git a/scram/route_manager/migrations/0001_initial.py b/django/src/scram/route_manager/migrations/0001_initial.py similarity index 100% rename from scram/route_manager/migrations/0001_initial.py rename to django/src/scram/route_manager/migrations/0001_initial.py diff --git a/scram/route_manager/migrations/0002_ipaddress_uuid.py b/django/src/scram/route_manager/migrations/0002_ipaddress_uuid.py similarity index 100% rename from scram/route_manager/migrations/0002_ipaddress_uuid.py rename to django/src/scram/route_manager/migrations/0002_ipaddress_uuid.py diff --git a/scram/route_manager/migrations/0003_auto_20210408_0413.py b/django/src/scram/route_manager/migrations/0003_auto_20210408_0413.py similarity index 100% rename from scram/route_manager/migrations/0003_auto_20210408_0413.py rename to django/src/scram/route_manager/migrations/0003_auto_20210408_0413.py diff --git a/scram/route_manager/migrations/0004_actiontype.py b/django/src/scram/route_manager/migrations/0004_actiontype.py similarity index 100% rename from scram/route_manager/migrations/0004_actiontype.py rename to django/src/scram/route_manager/migrations/0004_actiontype.py diff --git a/scram/route_manager/migrations/0005_entry.py b/django/src/scram/route_manager/migrations/0005_entry.py similarity index 100% rename from scram/route_manager/migrations/0005_entry.py rename to django/src/scram/route_manager/migrations/0005_entry.py diff --git a/scram/route_manager/migrations/0006_history.py b/django/src/scram/route_manager/migrations/0006_history.py similarity index 100% rename from scram/route_manager/migrations/0006_history.py rename to django/src/scram/route_manager/migrations/0006_history.py diff --git a/scram/route_manager/migrations/0007_history_expiration.py b/django/src/scram/route_manager/migrations/0007_history_expiration.py similarity index 100% rename from scram/route_manager/migrations/0007_history_expiration.py rename to django/src/scram/route_manager/migrations/0007_history_expiration.py diff --git a/scram/route_manager/migrations/0008_default_block_actiontype.py b/django/src/scram/route_manager/migrations/0008_default_block_actiontype.py similarity index 100% rename from scram/route_manager/migrations/0008_default_block_actiontype.py rename to django/src/scram/route_manager/migrations/0008_default_block_actiontype.py diff --git a/scram/route_manager/migrations/0009_expiration_to_datetime.py b/django/src/scram/route_manager/migrations/0009_expiration_to_datetime.py similarity index 100% rename from scram/route_manager/migrations/0009_expiration_to_datetime.py rename to django/src/scram/route_manager/migrations/0009_expiration_to_datetime.py diff --git a/scram/route_manager/migrations/0010_actiontype_helptext.py b/django/src/scram/route_manager/migrations/0010_actiontype_helptext.py similarity index 100% rename from scram/route_manager/migrations/0010_actiontype_helptext.py rename to django/src/scram/route_manager/migrations/0010_actiontype_helptext.py diff --git a/scram/route_manager/migrations/0011_history_helptext.py b/django/src/scram/route_manager/migrations/0011_history_helptext.py similarity index 100% rename from scram/route_manager/migrations/0011_history_helptext.py rename to django/src/scram/route_manager/migrations/0011_history_helptext.py diff --git a/scram/route_manager/migrations/0012_unique_entries.py b/django/src/scram/route_manager/migrations/0012_unique_entries.py similarity index 100% rename from scram/route_manager/migrations/0012_unique_entries.py rename to django/src/scram/route_manager/migrations/0012_unique_entries.py diff --git a/scram/route_manager/migrations/0013_accept_cidrs.py b/django/src/scram/route_manager/migrations/0013_accept_cidrs.py similarity index 100% rename from scram/route_manager/migrations/0013_accept_cidrs.py rename to django/src/scram/route_manager/migrations/0013_accept_cidrs.py diff --git a/scram/route_manager/migrations/0014_create_groups.py b/django/src/scram/route_manager/migrations/0014_create_groups.py similarity index 100% rename from scram/route_manager/migrations/0014_create_groups.py rename to django/src/scram/route_manager/migrations/0014_create_groups.py diff --git a/scram/route_manager/migrations/0015_entry_is_active.py b/django/src/scram/route_manager/migrations/0015_entry_is_active.py similarity index 100% rename from scram/route_manager/migrations/0015_entry_is_active.py rename to django/src/scram/route_manager/migrations/0015_entry_is_active.py diff --git a/scram/route_manager/migrations/0016_auto_20211202_1933.py b/django/src/scram/route_manager/migrations/0016_auto_20211202_1933.py similarity index 100% rename from scram/route_manager/migrations/0016_auto_20211202_1933.py rename to django/src/scram/route_manager/migrations/0016_auto_20211202_1933.py diff --git a/scram/route_manager/migrations/0017_ignorelist.py b/django/src/scram/route_manager/migrations/0017_ignorelist.py similarity index 100% rename from scram/route_manager/migrations/0017_ignorelist.py rename to django/src/scram/route_manager/migrations/0017_ignorelist.py diff --git a/scram/route_manager/migrations/0018_auto_20220702_0203.py b/django/src/scram/route_manager/migrations/0018_auto_20220702_0203.py similarity index 100% rename from scram/route_manager/migrations/0018_auto_20220702_0203.py rename to django/src/scram/route_manager/migrations/0018_auto_20220702_0203.py diff --git a/scram/route_manager/migrations/0019_auto_20220708_1945.py b/django/src/scram/route_manager/migrations/0019_auto_20220708_1945.py similarity index 100% rename from scram/route_manager/migrations/0019_auto_20220708_1945.py rename to django/src/scram/route_manager/migrations/0019_auto_20220708_1945.py diff --git a/scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py b/django/src/scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py similarity index 100% rename from scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py rename to django/src/scram/route_manager/migrations/0020_historicalactiontype_historicalentry_historicalignoreentry.py diff --git a/scram/route_manager/migrations/0021_auto_20220929_2047.py b/django/src/scram/route_manager/migrations/0021_auto_20220929_2047.py similarity index 100% rename from scram/route_manager/migrations/0021_auto_20220929_2047.py rename to django/src/scram/route_manager/migrations/0021_auto_20220929_2047.py diff --git a/scram/route_manager/migrations/0022_auto_20230117_1930.py b/django/src/scram/route_manager/migrations/0022_auto_20230117_1930.py similarity index 100% rename from scram/route_manager/migrations/0022_auto_20230117_1930.py rename to django/src/scram/route_manager/migrations/0022_auto_20230117_1930.py diff --git a/scram/route_manager/migrations/0023_client.py b/django/src/scram/route_manager/migrations/0023_client.py similarity index 100% rename from scram/route_manager/migrations/0023_client.py rename to django/src/scram/route_manager/migrations/0023_client.py diff --git a/scram/route_manager/migrations/0024_alter_client_is_authorized.py b/django/src/scram/route_manager/migrations/0024_alter_client_is_authorized.py similarity index 100% rename from scram/route_manager/migrations/0024_alter_client_is_authorized.py rename to django/src/scram/route_manager/migrations/0024_alter_client_is_authorized.py diff --git a/scram/route_manager/migrations/0025_rename_uuid_client_uuid.py b/django/src/scram/route_manager/migrations/0025_rename_uuid_client_uuid.py similarity index 100% rename from scram/route_manager/migrations/0025_rename_uuid_client_uuid.py rename to django/src/scram/route_manager/migrations/0025_rename_uuid_client_uuid.py diff --git a/scram/route_manager/migrations/0026_alter_client_hostname.py b/django/src/scram/route_manager/migrations/0026_alter_client_hostname.py similarity index 100% rename from scram/route_manager/migrations/0026_alter_client_hostname.py rename to django/src/scram/route_manager/migrations/0026_alter_client_hostname.py diff --git a/scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py b/django/src/scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py similarity index 100% rename from scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py rename to django/src/scram/route_manager/migrations/0027_websocketmessage_websocketsequenceelement.py diff --git a/scram/route_manager/migrations/0028_default_websocket_messages.py b/django/src/scram/route_manager/migrations/0028_default_websocket_messages.py similarity index 100% rename from scram/route_manager/migrations/0028_default_websocket_messages.py rename to django/src/scram/route_manager/migrations/0028_default_websocket_messages.py diff --git a/scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py b/django/src/scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py similarity index 100% rename from scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py rename to django/src/scram/route_manager/migrations/0029_alter_websocketmessage_msg_data_route_field.py diff --git a/scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py b/django/src/scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py similarity index 100% rename from scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py rename to django/src/scram/route_manager/migrations/0030_alter_entry_comment_alter_entry_expiration_reason_and_more.py diff --git a/scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py b/django/src/scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py similarity index 100% rename from scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py rename to django/src/scram/route_manager/migrations/0031_alter_entry_expiration_and_more.py diff --git a/scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py b/django/src/scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py similarity index 100% rename from scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py rename to django/src/scram/route_manager/migrations/0032_entry_originating_scram_instance_and_more.py diff --git a/scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py b/django/src/scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py similarity index 100% rename from scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py rename to django/src/scram/route_manager/migrations/0033_fix_broken_entry_model_historical_defaults.py diff --git a/scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py b/django/src/scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py similarity index 100% rename from scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py rename to django/src/scram/route_manager/migrations/0034_alter_entry_originating_scram_instance_and_more.py diff --git a/scram/route_manager/migrations/0035_alter_client_uuid.py b/django/src/scram/route_manager/migrations/0035_alter_client_uuid.py similarity index 100% rename from scram/route_manager/migrations/0035_alter_client_uuid.py rename to django/src/scram/route_manager/migrations/0035_alter_client_uuid.py diff --git a/scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py b/django/src/scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py similarity index 100% rename from scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py rename to django/src/scram/route_manager/migrations/0035_rename_historicalactiontype_history_date_id_route_manag_history_ee6aeb_idx_and_more.py diff --git a/scram/route_manager/migrations/0036_rename_hostname_client_client_name.py b/django/src/scram/route_manager/migrations/0036_rename_hostname_client_client_name.py similarity index 100% rename from scram/route_manager/migrations/0036_rename_hostname_client_client_name.py rename to django/src/scram/route_manager/migrations/0036_rename_hostname_client_client_name.py diff --git a/scram/route_manager/migrations/0037_unique_client_uuid.py b/django/src/scram/route_manager/migrations/0037_unique_client_uuid.py similarity index 100% rename from scram/route_manager/migrations/0037_unique_client_uuid.py rename to django/src/scram/route_manager/migrations/0037_unique_client_uuid.py diff --git a/scram/route_manager/migrations/0038_merge_20260310_2137.py b/django/src/scram/route_manager/migrations/0038_merge_20260310_2137.py similarity index 100% rename from scram/route_manager/migrations/0038_merge_20260310_2137.py rename to django/src/scram/route_manager/migrations/0038_merge_20260310_2137.py diff --git a/scram/route_manager/migrations/__init__.py b/django/src/scram/route_manager/migrations/__init__.py similarity index 100% rename from scram/route_manager/migrations/__init__.py rename to django/src/scram/route_manager/migrations/__init__.py diff --git a/scram/route_manager/models.py b/django/src/scram/route_manager/models.py similarity index 89% rename from scram/route_manager/models.py rename to django/src/scram/route_manager/models.py index 98d70b9d..caba5550 100644 --- a/scram/route_manager/models.py +++ b/django/src/scram/route_manager/models.py @@ -33,8 +33,12 @@ def get_absolute_url(): class ActionType(models.Model): """Define a type of action that can be done with a given route. e.g. Block, shunt, redirect, etc.""" - name = models.CharField(help_text="One-word description of the action", max_length=30) - available = models.BooleanField(help_text="Is this a valid choice for new entries?", default=True) + name = models.CharField( + help_text="One-word description of the action", max_length=30 + ) + available = models.BooleanField( + help_text="Is this a valid choice for new entries?", default=True + ) history = HistoricalRecords() def __str__(self): @@ -48,7 +52,9 @@ class WebSocketMessage(models.Model): """Define a single message sent to downstream translators via WebSocket.""" msg_type = models.CharField("The type of the message", max_length=50) - msg_data = models.JSONField("The JSON payload. See also msg_data_route_field.", default=dict) + msg_data = models.JSONField( + "The JSON payload. See also msg_data_route_field.", default=dict + ) msg_data_route_field = models.CharField( "The key in the JSON payload whose value will contain the route being acted on.", default="route", @@ -98,8 +104,12 @@ class Entry(models.Model): history = HistoricalRecords() when = models.DateTimeField(auto_now_add=True) who = models.CharField("Username", default="Unknown", max_length=30) - originating_scram_instance = models.CharField(default="scram_hostname_not_set", max_length=255) - expiration = models.DateTimeField(default=datetime.datetime(9999, 12, 31, 0, 0, tzinfo=datetime.UTC)) + originating_scram_instance = models.CharField( + default="scram_hostname_not_set", max_length=255 + ) + expiration = models.DateTimeField( + default=datetime.datetime(9999, 12, 31, 0, 0, tzinfo=datetime.UTC) + ) expiration_reason = models.CharField( help_text="Optional reason for the expiration", max_length=200, @@ -115,7 +125,9 @@ class Meta: def __str__(self): """Summarize the most important fields to something easily readable.""" - desc = f"{self.route} ({self.actiontype}) from: {self.originating_scram_instance}" + desc = ( + f"{self.route} ({self.actiontype}) from: {self.originating_scram_instance}" + ) if not self.is_active: desc += " (inactive)" return desc diff --git a/scram/route_manager/tests/__init__.py b/django/src/scram/route_manager/tests/__init__.py similarity index 100% rename from scram/route_manager/tests/__init__.py rename to django/src/scram/route_manager/tests/__init__.py diff --git a/scram/route_manager/tests/acceptance/environment.py b/django/src/scram/route_manager/tests/acceptance/environment.py similarity index 100% rename from scram/route_manager/tests/acceptance/environment.py rename to django/src/scram/route_manager/tests/acceptance/environment.py diff --git a/scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature b/django/src/scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature rename to django/src/scram/route_manager/tests/acceptance/features/add_automated_block_entry.feature diff --git a/scram/route_manager/tests/acceptance/features/client.feature b/django/src/scram/route_manager/tests/acceptance/features/client.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/client.feature rename to django/src/scram/route_manager/tests/acceptance/features/client.feature diff --git a/scram/route_manager/tests/acceptance/features/expiration.feature b/django/src/scram/route_manager/tests/acceptance/features/expiration.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/expiration.feature rename to django/src/scram/route_manager/tests/acceptance/features/expiration.feature diff --git a/scram/route_manager/tests/acceptance/features/ignorelist.feature b/django/src/scram/route_manager/tests/acceptance/features/ignorelist.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/ignorelist.feature rename to django/src/scram/route_manager/tests/acceptance/features/ignorelist.feature diff --git a/scram/route_manager/tests/acceptance/features/initial_data.feature b/django/src/scram/route_manager/tests/acceptance/features/initial_data.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/initial_data.feature rename to django/src/scram/route_manager/tests/acceptance/features/initial_data.feature diff --git a/scram/route_manager/tests/acceptance/features/query.feature b/django/src/scram/route_manager/tests/acceptance/features/query.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/query.feature rename to django/src/scram/route_manager/tests/acceptance/features/query.feature diff --git a/scram/route_manager/tests/acceptance/features/remove_ip.feature b/django/src/scram/route_manager/tests/acceptance/features/remove_ip.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/remove_ip.feature rename to django/src/scram/route_manager/tests/acceptance/features/remove_ip.feature diff --git a/scram/route_manager/tests/acceptance/features/restrict_changes.feature b/django/src/scram/route_manager/tests/acceptance/features/restrict_changes.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/restrict_changes.feature rename to django/src/scram/route_manager/tests/acceptance/features/restrict_changes.feature diff --git a/scram/route_manager/tests/acceptance/features/search.feature b/django/src/scram/route_manager/tests/acceptance/features/search.feature similarity index 100% rename from scram/route_manager/tests/acceptance/features/search.feature rename to django/src/scram/route_manager/tests/acceptance/features/search.feature diff --git a/scram/route_manager/tests/acceptance/steps/common.py b/django/src/scram/route_manager/tests/acceptance/steps/common.py similarity index 100% rename from scram/route_manager/tests/acceptance/steps/common.py rename to django/src/scram/route_manager/tests/acceptance/steps/common.py diff --git a/scram/route_manager/tests/acceptance/steps/ip.py b/django/src/scram/route_manager/tests/acceptance/steps/ip.py similarity index 100% rename from scram/route_manager/tests/acceptance/steps/ip.py rename to django/src/scram/route_manager/tests/acceptance/steps/ip.py diff --git a/scram/route_manager/tests/acceptance/steps/translator.py b/django/src/scram/route_manager/tests/acceptance/steps/translator.py similarity index 84% rename from scram/route_manager/tests/acceptance/steps/translator.py rename to django/src/scram/route_manager/tests/acceptance/steps/translator.py index ff478a9a..0ba67d2b 100644 --- a/scram/route_manager/tests/acceptance/steps/translator.py +++ b/django/src/scram/route_manager/tests/acceptance/steps/translator.py @@ -9,11 +9,15 @@ async def query_translator(route, actiontype, is_announced): """Ensure the specified route is currently either blocked or unblocked.""" - communicator = WebsocketCommunicator(ws_application, f"/ws/route_manager/webui_{actiontype}/") + communicator = WebsocketCommunicator( + ws_application, f"/ws/route_manager/webui_{actiontype}/" + ) connected, _ = await communicator.connect() assert connected - await communicator.send_json_to({"type": "wui_check_req", "message": {"route": route}}) + await communicator.send_json_to( + {"type": "wui_check_req", "message": {"route": route}} + ) response = await communicator.receive_json_from(timeout=10) assert response["type"] == "wui_check_resp" assert response["message"]["is_blocked"] == is_announced diff --git a/scram/route_manager/tests/functional_tests.py b/django/src/scram/route_manager/tests/functional_tests.py similarity index 100% rename from scram/route_manager/tests/functional_tests.py rename to django/src/scram/route_manager/tests/functional_tests.py diff --git a/scram/route_manager/tests/integration/environment.py b/django/src/scram/route_manager/tests/integration/environment.py similarity index 100% rename from scram/route_manager/tests/integration/environment.py rename to django/src/scram/route_manager/tests/integration/environment.py diff --git a/scram/route_manager/tests/integration/features/multi_instance_sync.feature b/django/src/scram/route_manager/tests/integration/features/multi_instance_sync.feature similarity index 100% rename from scram/route_manager/tests/integration/features/multi_instance_sync.feature rename to django/src/scram/route_manager/tests/integration/features/multi_instance_sync.feature diff --git a/scram/route_manager/tests/integration/steps/common.py b/django/src/scram/route_manager/tests/integration/steps/common.py similarity index 100% rename from scram/route_manager/tests/integration/steps/common.py rename to django/src/scram/route_manager/tests/integration/steps/common.py diff --git a/scram/route_manager/tests/integration/steps/ip.py b/django/src/scram/route_manager/tests/integration/steps/ip.py similarity index 100% rename from scram/route_manager/tests/integration/steps/ip.py rename to django/src/scram/route_manager/tests/integration/steps/ip.py diff --git a/scram/route_manager/tests/integration/steps/multi_instance.py b/django/src/scram/route_manager/tests/integration/steps/multi_instance.py similarity index 91% rename from scram/route_manager/tests/integration/steps/multi_instance.py rename to django/src/scram/route_manager/tests/integration/steps/multi_instance.py index ddff80aa..1695c863 100644 --- a/scram/route_manager/tests/integration/steps/multi_instance.py +++ b/django/src/scram/route_manager/tests/integration/steps/multi_instance.py @@ -14,7 +14,11 @@ def get_auth_token(base_url: str = DJANGO_PRIMARY_URL): """Obtain an API authentication token for the test user.""" - response = requests.post(f"{base_url}/auth-token/", data={"username": "user", "password": "password"}, timeout=10) + response = requests.post( + f"{base_url}/auth-token/", + data={"username": "user", "password": "password"}, + timeout=10, + ) response.raise_for_status() return response.json()["token"] @@ -160,7 +164,9 @@ def check_entry_inactive_on_secondary(context, ip): entries = list_response.json().get("results", []) for entry in entries: if entry.get("route") == ip: - context.test.fail(f"Entry {ip} should be inactive but was found in active entries list") + context.test.fail( + f"Entry {ip} should be inactive but was found in active entries list" + ) # If we get here, the entry was not found in active entries, which is correct except requests.exceptions.RequestException as e: @@ -196,13 +202,19 @@ def check_announced_on_secondary(context, ip): # Verify process_updates actually processed this specific entry if hasattr(context, "secondary_process_data"): - reprocessed_list = context.secondary_process_data.get("entries_reprocessed_list", []) - secondary_hostname = context.secondary_process_data.get("scram_hostname", "UNKNOWN") + reprocessed_list = context.secondary_process_data.get( + "entries_reprocessed_list", [] + ) + secondary_hostname = context.secondary_process_data.get( + "scram_hostname", "UNKNOWN" + ) originating_instance = None for entry in entries: if entry.get("route") == ip: - originating_instance = entry.get("originating_scram_instance", "UNKNOWN") + originating_instance = entry.get( + "originating_scram_instance", "UNKNOWN" + ) break assert ip in reprocessed_list, ( f"Expected {ip} in reprocessed list, got {reprocessed_list}. " @@ -228,14 +240,18 @@ def check_removal_announced_on_secondary(context, ip): entries = list_response.json().get("results", []) for entry in entries: if entry.get("route") == ip: - context.test.fail(f"Entry {ip} should be inactive but was found in active entries list") + context.test.fail( + f"Entry {ip} should be inactive but was found in active entries list" + ) # Entry is not in active list, which is correct for a removed entry except requests.exceptions.RequestException as e: context.test.fail(f"Failed to call API: {e}") # Verify process_updates actually processed this specific entry - process_data = getattr(context, "secondary_process_data", None) or getattr(context, "primary_process_data", {}) + process_data = getattr(context, "secondary_process_data", None) or getattr( + context, "primary_process_data", {} + ) if process_data: reprocessed_list = process_data.get("entries_reprocessed_list", []) hostname = process_data.get("scram_hostname", "UNKNOWN") @@ -261,7 +277,9 @@ def check_removal_announced_on_primary(context, ip): entries = list_response.json().get("results", []) for entry in entries: if entry.get("route") == ip: - context.test.fail(f"Entry {ip} should be inactive but was found in active entries list") + context.test.fail( + f"Entry {ip} should be inactive but was found in active entries list" + ) except requests.exceptions.RequestException as e: context.test.fail(f"Failed to call API: {e}") diff --git a/scram/route_manager/tests/test_admin.py b/django/src/scram/route_manager/tests/test_admin.py similarity index 75% rename from scram/route_manager/tests/test_admin.py rename to django/src/scram/route_manager/tests/test_admin.py index 5063fc94..e9ddcad7 100644 --- a/scram/route_manager/tests/test_admin.py +++ b/django/src/scram/route_manager/tests/test_admin.py @@ -17,12 +17,18 @@ def setUp(self): route1 = Route.objects.create(route="192.168.1.1") route2 = Route.objects.create(route="192.168.1.2") - self.entry1 = Entry.objects.create(route=route1, actiontype=self.atype, who="admin") - self.entry2 = Entry.objects.create(route=route2, actiontype=self.atype, who="user1") + self.entry1 = Entry.objects.create( + route=route1, actiontype=self.atype, who="admin" + ) + self.entry2 = Entry.objects.create( + route=route2, actiontype=self.atype, who="user1" + ) def test_who_filter_lookups(self): """Test that the WhoFilter returns the correct users who have made entries.""" - who_filter = WhoFilter(request=None, params={}, model=Entry, model_admin=EntryAdmin) + who_filter = WhoFilter( + request=None, params={}, model=Entry, model_admin=EntryAdmin + ) mock_request = MagicMock() mock_model_admin = MagicMock(spec=EntryAdmin) @@ -35,7 +41,9 @@ def test_who_filter_lookups(self): def test_who_filter_queryset_with_value(self): """Test that the queryset is filtered correctly when a user is selected.""" - who_filter = WhoFilter(request=None, params={"who": ["admin"]}, model=Entry, model_admin=EntryAdmin) + who_filter = WhoFilter( + request=None, params={"who": ["admin"]}, model=Entry, model_admin=EntryAdmin + ) queryset = Entry.objects.all() filtered_queryset = who_filter.queryset(None, queryset) diff --git a/scram/route_manager/tests/test_api.py b/django/src/scram/route_manager/tests/test_api.py similarity index 96% rename from scram/route_manager/tests/test_api.py rename to django/src/scram/route_manager/tests/test_api.py index 047d5f4b..64a21d18 100644 --- a/scram/route_manager/tests/test_api.py +++ b/django/src/scram/route_manager/tests/test_api.py @@ -14,7 +14,9 @@ class TestAddRemoveIP(APITestCase): def setUp(self): """Set up the environment for our tests.""" self.url = reverse("api:v1:entry-list") - self.superuser = get_user_model().objects.create_superuser("admin", "admin@es.net", "admintestpassword") + self.superuser = get_user_model().objects.create_superuser( + "admin", "admin@es.net", "admintestpassword" + ) self.client.login(username="admin", password="admintestpassword") self.authorized_client = Client.objects.create( client_name="authorized_client.es.net", @@ -117,7 +119,9 @@ def test_unauthenticated_users_have_no_create_access(self): def test_unauthenticated_users_have_no_ignore_create_access(self): """Ensure an unauthenticated client can't add an IgnoreEntry.""" - response = self.client.post(self.ignore_url, {"route": "192.0.2.4"}, format="json") + response = self.client.post( + self.ignore_url, {"route": "192.0.2.4"}, format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_unauthenticated_users_have_no_list_access(self): @@ -138,7 +142,9 @@ def setUp(self): is_authorized=True, ) self.authorized_client.authorized_actiontypes.set([1]) - self.actiontype, _ = ActionType.objects.get_or_create(pk=1, defaults={"name": "block"}) + self.actiontype, _ = ActionType.objects.get_or_create( + pk=1, defaults={"name": "block"} + ) # Create some active entries diff --git a/scram/route_manager/tests/test_authorization.py b/django/src/scram/route_manager/tests/test_authorization.py similarity index 96% rename from scram/route_manager/tests/test_authorization.py rename to django/src/scram/route_manager/tests/test_authorization.py index 9f00dfbd..a8925c8d 100644 --- a/scram/route_manager/tests/test_authorization.py +++ b/django/src/scram/route_manager/tests/test_authorization.py @@ -29,7 +29,9 @@ def setUp(self): self.readwrite_user.groups.set([self.readwrite_group]) self.readwrite_user.save() - self.admin_user = User.objects.create(username="admin", is_staff=True, is_superuser=True) + self.admin_user = User.objects.create( + username="admin", is_staff=True, is_superuser=True + ) self.write_blocked_users = [None, self.unauthorized_user, self.readonly_user] self.write_allowed_users = [self.readwrite_user, self.admin_user] @@ -104,7 +106,9 @@ def test_unauthorized_detail_view(self): for user in self.detail_blocked_users: if user: self.client.force_login(user) - response = self.client.get(reverse("route_manager:detail", kwargs={"pk": pk})) + response = self.client.get( + reverse("route_manager:detail", kwargs={"pk": pk}) + ) self.assertIn(response.status_code, [302, 403], msg=f"username={user}") def test_authorized_detail_view(self): @@ -113,7 +117,9 @@ def test_authorized_detail_view(self): for user in self.detail_allowed_users: self.client.force_login(user) - response = self.client.get(reverse("route_manager:detail", kwargs={"pk": pk})) + response = self.client.get( + reverse("route_manager:detail", kwargs={"pk": pk}) + ) self.assertEqual(response.status_code, 200, msg=f"username={user}") def test_unauthorized_after_group_removal(self): diff --git a/scram/route_manager/tests/test_autocreate_admin.py b/django/src/scram/route_manager/tests/test_autocreate_admin.py similarity index 100% rename from scram/route_manager/tests/test_autocreate_admin.py rename to django/src/scram/route_manager/tests/test_autocreate_admin.py diff --git a/scram/route_manager/tests/test_common/__init__.py b/django/src/scram/route_manager/tests/test_common/__init__.py similarity index 100% rename from scram/route_manager/tests/test_common/__init__.py rename to django/src/scram/route_manager/tests/test_common/__init__.py diff --git a/scram/route_manager/tests/test_common/steps_common.py b/django/src/scram/route_manager/tests/test_common/steps_common.py similarity index 93% rename from scram/route_manager/tests/test_common/steps_common.py rename to django/src/scram/route_manager/tests/test_common/steps_common.py index 403767a7..8b966241 100644 --- a/scram/route_manager/tests/test_common/steps_common.py +++ b/django/src/scram/route_manager/tests/test_common/steps_common.py @@ -9,7 +9,12 @@ from django import conf from django.urls import reverse -from scram.route_manager.models import ActionType, Client, WebSocketMessage, WebSocketSequenceElement +from scram.route_manager.models import ( + ActionType, + Client, + WebSocketMessage, + WebSocketSequenceElement, +) @given("a {name} actiontype is defined") @@ -22,9 +27,13 @@ def create_actiontype(context, name): ) at, _ = ActionType.objects.get_or_create(name=name) - wsm, _ = WebSocketMessage.objects.get_or_create(msg_type="translator_add", msg_data_route_field="route") + wsm, _ = WebSocketMessage.objects.get_or_create( + msg_type="translator_add", msg_data_route_field="route" + ) wsm.save() - wsse, _ = WebSocketSequenceElement.objects.get_or_create(websocketmessage=wsm, verb="A", action_type=at) + wsse, _ = WebSocketSequenceElement.objects.get_or_create( + websocketmessage=wsm, verb="A", action_type=at + ) wsse.save() @@ -167,7 +176,9 @@ def add_ignore_entry(context, value): @when("we remove the {model} {value}") def remove_an_object(context, model, value): """Remove any model object with the matching value.""" - context.response = context.test.client.delete(reverse(f"api:v1:{model.lower()}-detail", args=[value])) + context.response = context.test.client.delete( + reverse(f"api:v1:{model.lower()}-detail", args=[value]) + ) @when("we list the {model}s") diff --git a/scram/route_manager/tests/test_common/steps_ip.py b/django/src/scram/route_manager/tests/test_common/steps_ip.py similarity index 90% rename from scram/route_manager/tests/test_common/steps_ip.py rename to django/src/scram/route_manager/tests/test_common/steps_ip.py index e51e0262..2c8c3a8e 100644 --- a/scram/route_manager/tests/test_common/steps_ip.py +++ b/django/src/scram/route_manager/tests/test_common/steps_ip.py @@ -27,7 +27,9 @@ def check_route(context, route, model): def check_ip(context, ip): """Find an Entry for the specified IP.""" try: - context.response = context.test.client.get(reverse("api:v1:entry-detail", args=[ip])) + context.response = context.test.client.get( + reverse("api:v1:entry-detail", args=[ip]) + ) context.queryException = None except ValueError as e: context.response = None @@ -57,7 +59,9 @@ def update_entry_comment(context, value, comment): data = {"comment": comment, "who": context.client.client_name} context.response = context.test.client.put( - reverse("api:v1:entry-detail", args=[value]), data=json.dumps(data), content_type="application/json" + reverse("api:v1:entry-detail", args=[value]), + data=json.dumps(data), + content_type="application/json", ) diff --git a/scram/route_manager/tests/test_history.py b/django/src/scram/route_manager/tests/test_history.py similarity index 94% rename from scram/route_manager/tests/test_history.py rename to django/src/scram/route_manager/tests/test_history.py index d2848e83..cdb328ac 100644 --- a/scram/route_manager/tests/test_history.py +++ b/django/src/scram/route_manager/tests/test_history.py @@ -41,7 +41,9 @@ def test_comments(self): for r in self.routes: route_old = Route.objects.get(route=r) e = Entry.objects.get(route=route_old) - self.assertEqual(e.get_change_reason(), "Zeek detected a scan from 192.0.2.1.") + self.assertEqual( + e.get_change_reason(), "Zeek detected a scan from 192.0.2.1." + ) route_new = str(route_old).replace("16", "32") e.route = Route.objects.create(route=route_new) diff --git a/scram/route_manager/tests/test_pagination.py b/django/src/scram/route_manager/tests/test_pagination.py similarity index 75% rename from scram/route_manager/tests/test_pagination.py rename to django/src/scram/route_manager/tests/test_pagination.py index dfa0ffaa..38ac3ee0 100644 --- a/scram/route_manager/tests/test_pagination.py +++ b/django/src/scram/route_manager/tests/test_pagination.py @@ -21,37 +21,60 @@ def setUp(self): """Set up the test environment.""" self.fake = Faker() self.fake.add_provider(internet) - get_user_model().objects.create_user(username="testuser", password="testpass123") + get_user_model().objects.create_user( + username="testuser", password="testpass123" + ) self.atype1 = ActionType.objects.create(name="Type1", available=True) self.atype2 = ActionType.objects.create(name="Type2", available=True) self.atype3 = ActionType.objects.create(name="Type3", available=False) # Create enough entries to test pagination - created_routes = Route.objects.bulk_create([ - Route(route=self.fake.unique.ipv4_public()) for x in range(self.TEST_PAGINATION_SIZE + 3) - ]) - entries_type1 = Entry.objects.bulk_create([ - Entry(route=route, actiontype=self.atype1, is_active=True) for route in created_routes - ]) + created_routes = Route.objects.bulk_create( + [ + Route(route=self.fake.unique.ipv4_public()) + for x in range(self.TEST_PAGINATION_SIZE + 3) + ] + ) + entries_type1 = Entry.objects.bulk_create( + [ + Entry(route=route, actiontype=self.atype1, is_active=True) + for route in created_routes + ] + ) # Create a second type of entries to test filtering per actiontype - created_routes = Route.objects.bulk_create([Route(route=self.fake.unique.ipv4_public()) for x in range(3)]) - entries_type2 = Entry.objects.bulk_create([ - Entry(route=route, actiontype=self.atype2, is_active=True) for route in created_routes - ]) + created_routes = Route.objects.bulk_create( + [Route(route=self.fake.unique.ipv4_public()) for x in range(3)] + ) + entries_type2 = Entry.objects.bulk_create( + [ + Entry(route=route, actiontype=self.atype2, is_active=True) + for route in created_routes + ] + ) # Create inactive entries to test filtering by available actiontypes - created_routes = Route.objects.bulk_create([Route(route=self.fake.unique.ipv4_public()) for x in range(3)]) - Entry.objects.bulk_create([ - Entry(route=route, actiontype=self.atype1, is_active=False) for route in created_routes - ]) + created_routes = Route.objects.bulk_create( + [Route(route=self.fake.unique.ipv4_public()) for x in range(3)] + ) + Entry.objects.bulk_create( + [ + Entry(route=route, actiontype=self.atype1, is_active=False) + for route in created_routes + ] + ) # Create entries for an invalid actiontype to test that - created_routes = Route.objects.bulk_create([Route(route=self.fake.unique.ipv4_public()) for x in range(3)]) - Entry.objects.bulk_create([ - Entry(route=route, actiontype=self.atype3, is_active=False) for route in created_routes - ]) + created_routes = Route.objects.bulk_create( + [Route(route=self.fake.unique.ipv4_public()) for x in range(3)] + ) + Entry.objects.bulk_create( + [ + Entry(route=route, actiontype=self.atype3, is_active=False) + for route in created_routes + ] + ) self.entries = { "type1": entries_type1, diff --git a/scram/route_manager/tests/test_process_updates.py b/django/src/scram/route_manager/tests/test_process_updates.py similarity index 90% rename from scram/route_manager/tests/test_process_updates.py rename to django/src/scram/route_manager/tests/test_process_updates.py index 592fa732..4c8a95cf 100644 --- a/scram/route_manager/tests/test_process_updates.py +++ b/django/src/scram/route_manager/tests/test_process_updates.py @@ -5,7 +5,13 @@ import pytest from django.conf import settings -from scram.route_manager.models import ActionType, Entry, Route, WebSocketMessage, WebSocketSequenceElement +from scram.route_manager.models import ( + ActionType, + Entry, + Route, + WebSocketMessage, + WebSocketSequenceElement, +) from scram.route_manager.views import check_for_orphaned_history, get_entries_to_process @@ -142,7 +148,9 @@ def test_multiple_entries_from_different_instances(self, actiontype): def test_reactivated_entry_found(self, actiontype, other_instance): """Reactivated entries are found for reprocessing.""" - entry = create_entry(actiontype, "192.0.2.30/32", other_instance, is_active=False) + entry = create_entry( + actiontype, "192.0.2.30/32", other_instance, is_active=False + ) cutoff = datetime.now(UTC) entry.is_active = True entry.save() @@ -155,7 +163,10 @@ def test_reactivated_entry_found(self, actiontype, other_instance): def test_future_expiration_entry_active(self, actiontype, other_instance): """Entries with future expiration are processed as active.""" _ = create_entry( - actiontype, "192.0.2.40/32", other_instance, expiration=datetime.now(UTC) + timedelta(hours=1) + actiontype, + "192.0.2.40/32", + other_instance, + expiration=datetime.now(UTC) + timedelta(hours=1), ) cutoff = datetime.now(UTC) - timedelta(minutes=2) @@ -167,7 +178,10 @@ def test_future_expiration_entry_active(self, actiontype, other_instance): def test_expired_entry_found_as_inactive(self, actiontype, other_instance): """Expired entries are found but marked inactive after process_updates expires them.""" entry = create_entry( - actiontype, "192.0.2.50/32", other_instance, expiration=datetime.now(UTC) - timedelta(hours=1) + actiontype, + "192.0.2.50/32", + other_instance, + expiration=datetime.now(UTC) - timedelta(hours=1), ) cutoff = datetime.now(UTC) - timedelta(minutes=2) @@ -176,7 +190,9 @@ def test_expired_entry_found_as_inactive(self, actiontype, other_instance): assert len(result) == 1 assert result[0].id == entry.id - def test_processes_entries_from_current_instance(self, actiontype, current_instance): + def test_processes_entries_from_current_instance( + self, actiontype, current_instance + ): """Verifies that entries from the current instance are processed (PR #193 fix).""" entry = create_entry(actiontype, "192.0.2.60/32", current_instance) cutoff = datetime.now(UTC) - timedelta(minutes=2) @@ -191,7 +207,9 @@ def test_processes_entries_from_current_instance(self, actiontype, current_insta class TestCheckForOrphanedHistory: """Tests for check_for_orphaned_history().""" - def test_logs_warning_for_orphaned_entries(self, caplog, actiontype, other_instance): + def test_logs_warning_for_orphaned_entries( + self, caplog, actiontype, other_instance + ): """Make sure we log a warning when history exists but Entry was deleted from underneath us.""" entry = create_entry(actiontype, "10.1.0.1/32", other_instance) orphaned_id = entry.id diff --git a/scram/route_manager/tests/test_swagger.py b/django/src/scram/route_manager/tests/test_swagger.py similarity index 100% rename from scram/route_manager/tests/test_swagger.py rename to django/src/scram/route_manager/tests/test_swagger.py diff --git a/scram/route_manager/tests/test_views.py b/django/src/scram/route_manager/tests/test_views.py similarity index 100% rename from scram/route_manager/tests/test_views.py rename to django/src/scram/route_manager/tests/test_views.py diff --git a/scram/route_manager/tests/test_websockets.py b/django/src/scram/route_manager/tests/test_websockets.py similarity index 91% rename from scram/route_manager/tests/test_websockets.py rename to django/src/scram/route_manager/tests/test_websockets.py index a37a3d84..b7882190 100644 --- a/scram/route_manager/tests/test_websockets.py +++ b/django/src/scram/route_manager/tests/test_websockets.py @@ -33,7 +33,8 @@ async def get_communicators(actiontypes, should_match, *args, **kwds): """ router = URLRouter(websocket_urlpatterns) communicators = [ - WebsocketCommunicator(router, f"/ws/route_manager/translator_{actiontype}/") for actiontype in actiontypes + WebsocketCommunicator(router, f"/ws/route_manager/translator_{actiontype}/") + for actiontype in actiontypes ] response = zip(communicators, should_match, strict=True) @@ -56,7 +57,9 @@ def setUp(self): """Set up our test environment.""" # TODO: This is copied from test_api; should de-dupe this. self.url = reverse("api:v1:entry-list") - self.superuser = get_user_model().objects.create_superuser("admin", "admin@example.net", "admintestpassword") + self.superuser = get_user_model().objects.create_superuser( + "admin", "admin@example.net", "admintestpassword" + ) self.client.force_login(self.superuser) self.uuid = "0e7e1cbd-7d73-4968-bc4b-ce3265dc2fd3" @@ -72,7 +75,9 @@ def setUp(self): ) self.authorized_client.authorized_actiontypes.set([self.actiontype]) - wsm, _ = WebSocketMessage.objects.get_or_create(msg_type="translator_add", msg_data_route_field="route") + wsm, _ = WebSocketMessage.objects.get_or_create( + msg_type="translator_add", msg_data_route_field="route" + ) _, _ = WebSocketSequenceElement.objects.get_or_create( websocketmessage=wsm, verb="A", @@ -109,7 +114,9 @@ async def get_nothings(self, communicator): async def add_ip(self, ip, mask): """Ensure we can add an IP to block.""" - async with get_communicators(self.actiontypes, self.should_match) as communicators: + async with get_communicators( + self.actiontypes, self.should_match + ) as communicators: await self.api_create_entry(ip) # A list of that many function calls to verify the response @@ -174,14 +181,18 @@ class TranslatorSequenceTestCase(TestTranslatorBaseCase): def local_setup(self): """Define the messages we want to send.""" - wsm2 = WebSocketMessage.objects.create(msg_type="translator_add", msg_data_route_field="foo") + wsm2 = WebSocketMessage.objects.create( + msg_type="translator_add", msg_data_route_field="foo" + ) _ = WebSocketSequenceElement.objects.create( websocketmessage=wsm2, verb="A", action_type=self.actiontype, order_num=20, ) - wsm3 = WebSocketMessage.objects.create(msg_type="translator_add", msg_data_route_field="bar") + wsm3 = WebSocketMessage.objects.create( + msg_type="translator_add", msg_data_route_field="bar" + ) _ = WebSocketSequenceElement.objects.create( websocketmessage=wsm3, verb="A", @@ -210,7 +221,9 @@ class TranslatorParametersTestCase(TestTranslatorBaseCase): def local_setup(self): """Define the message we want to send.""" - wsm = WebSocketMessage.objects.get(msg_type="translator_add", msg_data_route_field="route") + wsm = WebSocketMessage.objects.get( + msg_type="translator_add", msg_data_route_field="route" + ) wsm.msg_data = { "asn": 65550, "community": 100, diff --git a/scram/route_manager/urls.py b/django/src/scram/route_manager/urls.py similarity index 100% rename from scram/route_manager/urls.py rename to django/src/scram/route_manager/urls.py diff --git a/scram/route_manager/views.py b/django/src/scram/route_manager/views.py similarity index 89% rename from scram/route_manager/views.py rename to django/src/scram/route_manager/views.py index 43c64109..7628c47c 100644 --- a/scram/route_manager/views.py +++ b/django/src/scram/route_manager/views.py @@ -41,7 +41,9 @@ def home_page(request, prefilter=None): if User.objects.count() == 0: password = make_random_password(length=20) User.objects.create_superuser("admin", "admin@example.com", password) - authenticated_admin = authenticate(request, username="admin", password=password) + authenticated_admin = authenticate( + request, username="admin", password=password + ) login(request, authenticated_admin) messages.add_message( request, @@ -60,7 +62,9 @@ def home_page(request, prefilter=None): readwrite = False context = {"entries": {}, "readwrite": readwrite} for at in ActionType.objects.all(): - queryset_active = prefilter.filter(actiontype=at, is_active=True).order_by("-pk") + queryset_active = prefilter.filter(actiontype=at, is_active=True).order_by( + "-pk" + ) context["entries"][at] = { "objs": queryset_active[:num_entries], "active": queryset_active.count(), @@ -85,7 +89,9 @@ def search_entries(request): str_addr = str(request.POST.get("cidr")).strip() addr = ipaddress.ip_network(str_addr, strict=False) except ValueError: - messages.add_message(request, messages.ERROR, "Search query was not a valid CIDR address") + messages.add_message( + request, messages.ERROR, "Search query was not a valid CIDR address" + ) # Send a 400, but show the home page instead of an error page return HttpResponseBadRequest(render(request, "route_manager/home.html")) @@ -142,7 +148,9 @@ def add_entry(request): elif res.status_code == 403: # noqa: PLR2004 messages.add_message(request, messages.ERROR, "Permission Denied") else: - messages.add_message(request, messages.WARNING, f"Something went wrong: {res.status_code}") + messages.add_message( + request, messages.WARNING, f"Something went wrong: {res.status_code}" + ) return redirect("route_manager:home") @@ -161,7 +169,9 @@ def get_entries_to_process(cutoff_time: timedelta) -> list[Entry]: logger.debug("Looking for entries modified by any SCRAM instance") # Grab (only, via values_list) the Entry IDs that have had their history records touched since the cutoff time. - recently_touched_ids = set(Entry.history.filter(history_date__gt=cutoff_time).values_list("id", flat=True)) + recently_touched_ids = set( + Entry.history.filter(history_date__gt=cutoff_time).values_list("id", flat=True) + ) if not recently_touched_ids: logger.debug("No recently modified entries found") @@ -170,7 +180,11 @@ def get_entries_to_process(cutoff_time: timedelta) -> list[Entry]: logger.debug("Found recently touched entry IDs: %s", recently_touched_ids) # Using the ID's from above, fetch all matching entries and associated models. - entries_to_process = list(Entry.objects.filter(id__in=recently_touched_ids).select_related("actiontype", "route")) + entries_to_process = list( + Entry.objects.filter(id__in=recently_touched_ids).select_related( + "actiontype", "route" + ) + ) check_for_orphaned_history(recently_touched_ids, entries_to_process) @@ -178,7 +192,9 @@ def get_entries_to_process(cutoff_time: timedelta) -> list[Entry]: return entries_to_process -def check_for_orphaned_history(recently_touched_ids: set[int], entries_to_process: list[Entry]) -> None: +def check_for_orphaned_history( + recently_touched_ids: set[int], entries_to_process: list[Entry] +) -> None: """Check for orphaned history records where the Entry was deleted but history remains. This shouldn't happen in production since Entry.delete() is overridden on the model, @@ -193,7 +209,9 @@ def check_for_orphaned_history(recently_touched_ids: set[int], entries_to_proces # IDs with history but no corresponding Entry row = orphaned (hard-deleted outside of Entry.delete()) orphaned_ids = recently_touched_ids - found_ids if orphaned_ids: - logger.warning("Found history records with no corresponding Entry: %s", orphaned_ids) + logger.warning( + "Found history records with no corresponding Entry: %s", orphaned_ids + ) def reprocess_entries(entries_to_process: list[Entry]) -> None: @@ -215,8 +233,7 @@ def reprocess_entries(entries_to_process: list[Entry]) -> None: translator_group = f"translator_{entry.actiontype}" elements = ( - WebSocketSequenceElement.objects - .filter(action_type__name=entry.actiontype) + WebSocketSequenceElement.objects.filter(action_type__name=entry.actiontype) .order_by("order_num") .select_related("websocketmessage") ) @@ -258,12 +275,14 @@ def process_updates(request): else: logger.info("No new entries to process") - return JsonResponse({ - "entries_deleted": entries_start - entries_end, - "active_entries": entries_end, - "entries_reprocessed": len(entries_to_process), - "entries_reprocessed_list": entries_reprocessed_list, - }) + return JsonResponse( + { + "entries_deleted": entries_start - entries_end, + "active_entries": entries_end, + "entries_reprocessed": len(entries_to_process), + "entries_reprocessed_list": entries_reprocessed_list, + } + ) @require_GET @@ -293,7 +312,9 @@ def get_context_data(self, **kwargs): # Get all available action types for at in ActionType.objects.filter(available=True): - queryset = Entry.objects.filter(actiontype=at, is_active=True).order_by("-pk") + queryset = Entry.objects.filter(actiontype=at, is_active=True).order_by( + "-pk" + ) # Create a paginator for this action type paginator = Paginator(queryset, settings.PAGINATION_SIZE) diff --git a/scram/shared/__init__.py b/django/src/scram/shared/__init__.py similarity index 100% rename from scram/shared/__init__.py rename to django/src/scram/shared/__init__.py diff --git a/scram/shared/shared_code.py b/django/src/scram/shared/shared_code.py similarity index 96% rename from scram/shared/shared_code.py rename to django/src/scram/shared/shared_code.py index 926d7bb7..ddd9dba3 100644 --- a/scram/shared/shared_code.py +++ b/django/src/scram/shared/shared_code.py @@ -4,7 +4,9 @@ import string -def make_random_password(length: int = 20, min_digits: int = 5, max_attempts: int = 10000) -> str: +def make_random_password( + length: int = 20, min_digits: int = 5, max_attempts: int = 10000 +) -> str: """make_random_password replaces the deprecated django make_random_password function. Generates a random password of a specified length containing at least a specified number of digits using the diff --git a/scram/static/css/project.css b/django/src/scram/static/css/project.css similarity index 100% rename from scram/static/css/project.css rename to django/src/scram/static/css/project.css diff --git a/scram/static/fonts/.gitkeep b/django/src/scram/static/fonts/.gitkeep similarity index 100% rename from scram/static/fonts/.gitkeep rename to django/src/scram/static/fonts/.gitkeep diff --git a/scram/static/images/favicons/favicon.ico b/django/src/scram/static/images/favicons/favicon.ico similarity index 100% rename from scram/static/images/favicons/favicon.ico rename to django/src/scram/static/images/favicons/favicon.ico diff --git a/scram/static/js/project.js b/django/src/scram/static/js/project.js similarity index 100% rename from scram/static/js/project.js rename to django/src/scram/static/js/project.js diff --git a/scram/static/sass/custom_bootstrap_vars.scss b/django/src/scram/static/sass/custom_bootstrap_vars.scss similarity index 100% rename from scram/static/sass/custom_bootstrap_vars.scss rename to django/src/scram/static/sass/custom_bootstrap_vars.scss diff --git a/scram/static/sass/project.scss b/django/src/scram/static/sass/project.scss similarity index 100% rename from scram/static/sass/project.scss rename to django/src/scram/static/sass/project.scss diff --git a/scram/templates/403.html b/django/src/scram/templates/403.html similarity index 100% rename from scram/templates/403.html rename to django/src/scram/templates/403.html diff --git a/scram/templates/404.html b/django/src/scram/templates/404.html similarity index 100% rename from scram/templates/404.html rename to django/src/scram/templates/404.html diff --git a/scram/templates/500.html b/django/src/scram/templates/500.html similarity index 100% rename from scram/templates/500.html rename to django/src/scram/templates/500.html diff --git a/scram/templates/base.html b/django/src/scram/templates/base.html similarity index 100% rename from scram/templates/base.html rename to django/src/scram/templates/base.html diff --git a/scram/templates/local_auth/login.html b/django/src/scram/templates/local_auth/login.html similarity index 100% rename from scram/templates/local_auth/login.html rename to django/src/scram/templates/local_auth/login.html diff --git a/scram/templates/navbar.html b/django/src/scram/templates/navbar.html similarity index 100% rename from scram/templates/navbar.html rename to django/src/scram/templates/navbar.html diff --git a/scram/templates/route_manager/entry_detail.html b/django/src/scram/templates/route_manager/entry_detail.html similarity index 100% rename from scram/templates/route_manager/entry_detail.html rename to django/src/scram/templates/route_manager/entry_detail.html diff --git a/scram/templates/route_manager/entry_list.html b/django/src/scram/templates/route_manager/entry_list.html similarity index 100% rename from scram/templates/route_manager/entry_list.html rename to django/src/scram/templates/route_manager/entry_list.html diff --git a/scram/templates/route_manager/home.html b/django/src/scram/templates/route_manager/home.html similarity index 100% rename from scram/templates/route_manager/home.html rename to django/src/scram/templates/route_manager/home.html diff --git a/scram/templates/users/user_detail.html b/django/src/scram/templates/users/user_detail.html similarity index 100% rename from scram/templates/users/user_detail.html rename to django/src/scram/templates/users/user_detail.html diff --git a/scram/templates/users/user_form.html b/django/src/scram/templates/users/user_form.html similarity index 100% rename from scram/templates/users/user_form.html rename to django/src/scram/templates/users/user_form.html diff --git a/scram/users/__init__.py b/django/src/scram/users/__init__.py similarity index 100% rename from scram/users/__init__.py rename to django/src/scram/users/__init__.py diff --git a/scram/users/admin.py b/django/src/scram/users/admin.py similarity index 100% rename from scram/users/admin.py rename to django/src/scram/users/admin.py diff --git a/scram/users/api/serializers.py b/django/src/scram/users/api/serializers.py similarity index 77% rename from scram/users/api/serializers.py rename to django/src/scram/users/api/serializers.py index f8d22c88..3020193a 100644 --- a/scram/users/api/serializers.py +++ b/django/src/scram/users/api/serializers.py @@ -15,4 +15,6 @@ class Meta: model = User fields = ["username", "name", "url"] - extra_kwargs = {"url": {"view_name": "api:v1:user-detail", "lookup_field": "username"}} + extra_kwargs = { + "url": {"view_name": "api:v1:user-detail", "lookup_field": "username"} + } diff --git a/scram/users/api/views.py b/django/src/scram/users/api/views.py similarity index 100% rename from scram/users/api/views.py rename to django/src/scram/users/api/views.py diff --git a/scram/users/apps.py b/django/src/scram/users/apps.py similarity index 100% rename from scram/users/apps.py rename to django/src/scram/users/apps.py diff --git a/scram/users/forms.py b/django/src/scram/users/forms.py similarity index 85% rename from scram/users/forms.py rename to django/src/scram/users/forms.py index 3807fdac..0a74e252 100644 --- a/scram/users/forms.py +++ b/django/src/scram/users/forms.py @@ -24,4 +24,6 @@ class Meta(admin_forms.AdminUserCreationForm.Meta): model = User - error_messages = {"username": {"unique": _("This username has already been taken.")}} + error_messages = { + "username": {"unique": _("This username has already been taken.")} + } diff --git a/scram/users/migrations/0001_initial.py b/django/src/scram/users/migrations/0001_initial.py similarity index 100% rename from scram/users/migrations/0001_initial.py rename to django/src/scram/users/migrations/0001_initial.py diff --git a/scram/users/migrations/__init__.py b/django/src/scram/users/migrations/__init__.py similarity index 100% rename from scram/users/migrations/__init__.py rename to django/src/scram/users/migrations/__init__.py diff --git a/scram/users/models.py b/django/src/scram/users/models.py similarity index 100% rename from scram/users/models.py rename to django/src/scram/users/models.py diff --git a/scram/users/tests/__init__.py b/django/src/scram/users/tests/__init__.py similarity index 100% rename from scram/users/tests/__init__.py rename to django/src/scram/users/tests/__init__.py diff --git a/scram/users/tests/factories.py b/django/src/scram/users/tests/factories.py similarity index 100% rename from scram/users/tests/factories.py rename to django/src/scram/users/tests/factories.py diff --git a/scram/users/tests/test_admin.py b/django/src/scram/users/tests/test_admin.py similarity index 100% rename from scram/users/tests/test_admin.py rename to django/src/scram/users/tests/test_admin.py diff --git a/scram/users/tests/test_drf_urls.py b/django/src/scram/users/tests/test_drf_urls.py similarity index 84% rename from scram/users/tests/test_drf_urls.py rename to django/src/scram/users/tests/test_drf_urls.py index 8e587486..f4fe9375 100644 --- a/scram/users/tests/test_drf_urls.py +++ b/django/src/scram/users/tests/test_drf_urls.py @@ -10,7 +10,10 @@ def test_user_detail(user: User): """Ensure we can view details for a single User.""" - assert reverse("api:v1:user-detail", kwargs={"username": user.username}) == f"/api/v1/users/{user.username}/" + assert ( + reverse("api:v1:user-detail", kwargs={"username": user.username}) + == f"/api/v1/users/{user.username}/" + ) assert resolve(f"/api/v1/users/{user.username}/").view_name == "api:v1:user-detail" diff --git a/scram/users/tests/test_drf_views.py b/django/src/scram/users/tests/test_drf_views.py similarity index 100% rename from scram/users/tests/test_drf_views.py rename to django/src/scram/users/tests/test_drf_views.py diff --git a/scram/users/tests/test_forms.py b/django/src/scram/users/tests/test_forms.py similarity index 100% rename from scram/users/tests/test_forms.py rename to django/src/scram/users/tests/test_forms.py diff --git a/scram/users/tests/test_models.py b/django/src/scram/users/tests/test_models.py similarity index 100% rename from scram/users/tests/test_models.py rename to django/src/scram/users/tests/test_models.py diff --git a/scram/users/tests/test_urls.py b/django/src/scram/users/tests/test_urls.py similarity index 86% rename from scram/users/tests/test_urls.py rename to django/src/scram/users/tests/test_urls.py index aa5a57ad..15ecb1cd 100644 --- a/scram/users/tests/test_urls.py +++ b/django/src/scram/users/tests/test_urls.py @@ -10,7 +10,10 @@ def test_detail(user: User): """Ensure we can get the URL to view details about a single User.""" - assert reverse("users:detail", kwargs={"username": user.username}) == f"/users/{user.username}/" + assert ( + reverse("users:detail", kwargs={"username": user.username}) + == f"/users/{user.username}/" + ) assert resolve(f"/users/{user.username}/").view_name == "users:detail" diff --git a/scram/users/tests/test_views.py b/django/src/scram/users/tests/test_views.py similarity index 100% rename from scram/users/tests/test_views.py rename to django/src/scram/users/tests/test_views.py diff --git a/scram/users/urls.py b/django/src/scram/users/urls.py similarity index 100% rename from scram/users/urls.py rename to django/src/scram/users/urls.py diff --git a/scram/users/views.py b/django/src/scram/users/views.py similarity index 100% rename from scram/users/views.py rename to django/src/scram/users/views.py diff --git a/scram/utils/__init__.py b/django/src/scram/utils/__init__.py similarity index 100% rename from scram/utils/__init__.py rename to django/src/scram/utils/__init__.py diff --git a/scram/utils/context_processors.py b/django/src/scram/utils/context_processors.py similarity index 100% rename from scram/utils/context_processors.py rename to django/src/scram/utils/context_processors.py diff --git a/pyproject.toml b/pyproject.toml index d593bebb..b454de01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "SCRAM" -version = "1.5.1" requires-python = ">=3.12" +version = "1.5.1" # ==== pytest ==== [tool.pytest.ini_options] addopts = [ "--ds=config.settings.test", - "--ignore=scheduler" + "--ignore=scheduler", ] minversion = "6.0" python_files = [ @@ -112,42 +112,14 @@ max-complexity = 7 # our current code adheres to this without too much effort "**/views.py" = [ "DOC201", # documenting return values; it's fairly obvious in a View ] -"scram/route_manager/**" = [ - "DOC201", # documenting return values -] -"scram/users/**" = [ - "DOC201", # documenting return values - "FBT001", # minimal issue; don't need to mess with in the User app - "PLR2004", # magic values when checking HTTP status codes -] -"test.py" = [ - "S105", # hardcoded password as argument -] [tool.ruff.lint.pydocstyle] convention = "google" -# ==== mypy ==== -[tool.mypy] -check_untyped_defs = true -ignore_missing_imports = true -plugins = [ - "mypy_django_plugin.main", - "mypy_drf_plugin.main", -] -python_version = "3.11" -warn_redundant_casts = true -warn_unused_configs = true -warn_unused_ignores = true - -[[tool.mypy.overrides]] -# Django migrations should not produce any errors: -ignore_errors = true -module = "*.migrations.*" - -[tool.django-stubs] -django_settings_module = "config.settings.test" - - [tool.uv.workspace] -members = ["scheduler", "translator"] +members = ["django", "scheduler", "translator"] + +[dependency-groups] +dev = [ + "ruff~=0.14.14", +] diff --git a/scram/__init__.py b/scram/__init__.py deleted file mode 100644 index 806191d9..00000000 --- a/scram/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""The Django project for Security Catch and Release Automation Manager (SCRAM).""" - -__version__ = "1.5.1" -__version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")) # noqa: RUF067 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2c082581..00000000 --- a/setup.cfg +++ /dev/null @@ -1,29 +0,0 @@ -[flake8] -ignore = F811, W503 -max-line-length = 120 -exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv - -[pycodestyle] -max-line-length = 120 -exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv - -[mypy] -python_version = 3.8 -check_untyped_defs = True -ignore_missing_imports = True -warn_unused_ignores = True -warn_redundant_casts = True -warn_unused_configs = True -plugins = mypy_django_plugin.main -exclude = scram/route_manager/tests/* - -[mypy.plugins.django-stubs] -django_settings_module = config.settings.test - -[mypy-*.migrations.*] -# Django migrations should not produce any errors: -ignore_errors = True - -[behave] -paths = scram/route_manager/tests/acceptance -stderr_capture = no diff --git a/translator/src/translator/shared.py b/translator/src/translator/shared.py index 6a21b02d..0f6ccd1a 100644 --- a/translator/src/translator/shared.py +++ b/translator/src/translator/shared.py @@ -11,12 +11,12 @@ def asn_is_valid(asn: int) -> bool: Args: asn (int): The Autonomous System Number that we want to validate - Raises: - ASNError: If the ASN is not between 0 and 4294967295 or is not an integer. - Returns: bool: _description_ + Raises: + ASNError: If the ASN is not between 0 and 4294967295 or is not an integer. + """ if not isinstance(asn, int): msg = f"ASN {asn} is not an Integer, has type {type(asn)}" diff --git a/uv.lock b/uv.lock index 86d5dca9..cc4b2ba3 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,7 @@ requires-python = ">=3.12.9" members = [ "scheduler", "scram", + "scram-django", "translator", ] @@ -1025,6 +1026,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/98/1dd1a5c060916cf21d15e67b7d6a7078e26e2605d5c37cbc9f4f5454c478/redis-7.2.1-py3-none-any.whl", hash = "sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33", size = 396057, upload-time = "2026-02-25T20:05:16.533Z" }, ] +[[package]] +name = "ruff" +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + [[package]] name = "scheduler" version = "0.1.0" @@ -1057,6 +1084,21 @@ name = "scram" version = "1.5.1" source = { virtual = "." } +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = "~=0.14.14" }] + +[[package]] +name = "scram-django" +version = "1.5.1" +source = { editable = "django" } + [[package]] name = "setuptools" version = "82.0.0"