Merge branch 'fix/docker-backend-workdir' into develop

This commit is contained in:
2026-04-28 11:30:09 +02:00
27 changed files with 235 additions and 21 deletions

View File

@@ -78,6 +78,7 @@ Configure the canonical test command for this repository:
```bash ```bash
docker compose --env-file .env.example -f infra/docker/compose.yml config docker compose --env-file .env.example -f infra/docker/compose.yml config
docker compose --env-file .env.example -f infra/docker/compose.yml run --rm --build backend python manage.py test
``` ```
Examples: Examples:

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git
.env
.env.*
!.env.example
__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/

View File

@@ -14,6 +14,8 @@ DJANGO_SECRET_KEY=change-me
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8080 DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8080
DJANGO_DEBUG=false DJANGO_DEBUG=false
CORS_ALLOWED_ORIGINS=http://localhost:4200,http://localhost:8080
TIME_ZONE=Europe/Rome
POSTGRES_DB=azionelab POSTGRES_DB=azionelab
POSTGRES_USER=azionelab POSTGRES_USER=azionelab

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,8 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "azionelab.settings")
application = get_asgi_application()

View File

@@ -0,0 +1,114 @@
import os
from pathlib import Path
import dj_database_url
from django.core.exceptions import ImproperlyConfigured
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
if not SECRET_KEY:
if DEBUG:
SECRET_KEY = "insecure-development-key"
else:
raise ImproperlyConfigured("DJANGO_SECRET_KEY must be set when DJANGO_DEBUG is false.")
def csv_env(name, default=""):
return [item.strip() for item in os.environ.get(name, default).split(",") if item.strip()]
ALLOWED_HOSTS = csv_env("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1")
CSRF_TRUSTED_ORIGINS = csv_env("DJANGO_CSRF_TRUSTED_ORIGINS")
CORS_ALLOWED_ORIGINS = csv_env("CORS_ALLOWED_ORIGINS")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"corsheaders",
"shows",
"bookings",
"checkins",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "azionelab.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "azionelab.wsgi.application"
DATABASES = {
"default": dj_database_url.config(
default=(
f"postgres://{os.environ.get('POSTGRES_USER', 'azionelab')}:"
f"{os.environ.get('POSTGRES_PASSWORD', 'azionelab')}"
f"@{os.environ.get('POSTGRES_HOST', 'postgres')}:"
f"{os.environ.get('POSTGRES_PORT', '5432')}/"
f"{os.environ.get('POSTGRES_DB', 'azionelab')}"
),
conn_max_age=60,
)
}
AUTH_PASSWORD_VALIDATORS = [
{
"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",
},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = os.environ.get("TIME_ZONE", "Europe/Rome")
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
],
}

View File

@@ -0,0 +1,10 @@
from django.test import SimpleTestCase
from django.urls import reverse
class HealthEndpointTests(SimpleTestCase):
def test_health_endpoint_returns_ok(self):
response = self.client.get(reverse("health"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})

15
backend/azionelab/urls.py Normal file
View File

@@ -0,0 +1,15 @@
from django.contrib import admin
from django.urls import path
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["GET"])
def health(request):
return Response({"status": "ok"})
urlpatterns = [
path("admin/", admin.site.urls),
path("api/health/", health, name="health"),
]

View File

@@ -0,0 +1,8 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "azionelab.settings")
application = get_wsgi_application()

View File

@@ -0,0 +1 @@

6
backend/bookings/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BookingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "bookings"

View File

@@ -0,0 +1 @@
# Domain models will be added when booking behavior is implemented.

View File

@@ -0,0 +1 @@

6
backend/checkins/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CheckinsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "checkins"

View File

@@ -0,0 +1 @@
# Domain models will be added when check-in behavior is implemented.

14
backend/manage.py Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "azionelab.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@

6
backend/shows/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ShowsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "shows"

1
backend/shows/models.py Normal file
View File

@@ -0,0 +1 @@
# Domain models will be added when show management is implemented.

View File

@@ -25,6 +25,8 @@ The frontend must not calculate authoritative availability. It may display avail
The backend is a Django 5.2 LTS application using Django REST Framework. The backend is a Django 5.2 LTS application using Django REST Framework.
The initial backend skeleton lives under `backend/` and includes the Django project, Django admin, the `shows`, `bookings`, and `checkins` apps, CORS configuration for the Angular frontend, PostgreSQL configuration through environment variables, and a health endpoint at `/api/health/`.
Responsibilities: Responsibilities:
- expose public read APIs for shows, venues, and performances; - expose public read APIs for shows, venues, and performances;

View File

@@ -55,7 +55,7 @@ Responsibilities:
The backend should run database migrations before or during deployment through an explicit operational command, not as hidden startup magic unless that choice is documented later. The backend should run database migrations before or during deployment through an explicit operational command, not as hidden startup magic unless that choice is documented later.
At the infrastructure placeholder stage, the `backend` service runs gunicorn against a minimal placeholder WSGI application. The real Django application will replace it later. The `backend` service runs gunicorn against the Django WSGI application. The current backend is an initial skeleton with Django admin, Django REST Framework, CORS configuration, the `shows`, `bookings`, and `checkins` apps, and a health endpoint.
### postgres ### postgres
@@ -97,6 +97,8 @@ Required backend configuration:
- `DJANGO_SECRET_KEY`; - `DJANGO_SECRET_KEY`;
- `DJANGO_ALLOWED_HOSTS`; - `DJANGO_ALLOWED_HOSTS`;
- `DJANGO_CSRF_TRUSTED_ORIGINS`; - `DJANGO_CSRF_TRUSTED_ORIGINS`;
- `CORS_ALLOWED_ORIGINS`;
- `TIME_ZONE`;
- `DATABASE_URL` or equivalent database settings; - `DATABASE_URL` or equivalent database settings;
- email host, port, username, password, TLS settings, and sender address; - email host, port, username, password, TLS settings, and sender address;
- public site URL used to build confirmation and QR verification links. - public site URL used to build confirmation and QR verification links.
@@ -147,14 +149,16 @@ Expected validation commands:
```bash ```bash
docker compose --env-file .env.example -f infra/docker/compose.yml config docker compose --env-file .env.example -f infra/docker/compose.yml config
docker compose --env-file .env.example -f infra/docker/compose.yml run --rm --build backend python manage.py test
docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py check --deploy docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py check --deploy
docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py test docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py test
``` ```
The canonical repository check for the current infrastructure stage is: The canonical repository check for the current stage is:
```bash ```bash
docker compose --env-file .env.example -f infra/docker/compose.yml config docker compose --env-file .env.example -f infra/docker/compose.yml config
docker compose --env-file .env.example -f infra/docker/compose.yml run --rm --build backend python manage.py test
``` ```
## Rollback ## Rollback

View File

@@ -80,6 +80,7 @@ Required controls:
- check-in verification preview and confirmation require authenticated staff or admin users; - check-in verification preview and confirmation require authenticated staff or admin users;
- staff permissions should separate content management from operational check-in when practical; - staff permissions should separate content management from operational check-in when practical;
- public APIs must not allow clients to set protected fields such as reservation status, token values, or check-in state. - public APIs must not allow clients to set protected fields such as reservation status, token values, or check-in state.
- CORS must allow only configured Angular frontend origins through `CORS_ALLOWED_ORIGINS`.
## Input Validation ## Input Validation

View File

@@ -8,6 +8,7 @@ All tests should run inside Docker containers.
```bash ```bash
docker compose --env-file .env.example -f infra/docker/compose.yml config docker compose --env-file .env.example -f infra/docker/compose.yml config
docker compose --env-file .env.example -f infra/docker/compose.yml run --rm --build backend python manage.py test
``` ```
## Test categories ## Test categories
@@ -21,3 +22,8 @@ Describe applicable categories:
- Ansible syntax checks; - Ansible syntax checks;
- Docker/Compose validation; - Docker/Compose validation;
- smoke tests. - smoke tests.
## Current coverage
- Docker Compose configuration validation;
- Django backend unit tests, including the initial health endpoint test.

View File

@@ -5,18 +5,15 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app WORKDIR /app
RUN pip install --no-cache-dir \
"Django==5.2.3" \
"djangorestframework==3.16.0" \
"gunicorn==23.0.0" \
"psycopg[binary]==3.2.9"
RUN useradd --create-home --shell /usr/sbin/nologin appuser RUN useradd --create-home --shell /usr/sbin/nologin appuser
COPY placeholder_wsgi.py /app/placeholder_wsgi.py COPY requirements/backend.txt /app/requirements/backend.txt
RUN pip install --no-cache-dir -r /app/requirements/backend.txt
COPY backend/ /app/
USER appuser USER appuser
EXPOSE 8000 EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "placeholder_wsgi:application"] CMD ["gunicorn", "--bind", "0.0.0.0:8000", "azionelab.wsgi:application"]

View File

@@ -1,10 +0,0 @@
def application(environ, start_response):
body = b"AzioneLab backend placeholder\n"
start_response(
"503 Service Unavailable",
[
("Content-Type", "text/plain; charset=utf-8"),
("Content-Length", str(len(body))),
],
)
return [body]

View File

@@ -1,13 +1,16 @@
services: services:
backend: backend:
build: build:
context: ./backend context: ../..
dockerfile: infra/docker/backend/Dockerfile
image: azionelab-backend:local image: azionelab-backend:local
environment: environment:
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY} DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS} DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS}
DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS} DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS}
DJANGO_DEBUG: ${DJANGO_DEBUG:-false} DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
TIME_ZONE: ${TIME_ZONE:-Europe/Rome}
DATABASE_URL: ${DATABASE_URL} DATABASE_URL: ${DATABASE_URL}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}

6
requirements/backend.txt Normal file
View File

@@ -0,0 +1,6 @@
Django==5.2.3
djangorestframework==3.16.0
django-cors-headers==4.7.0
dj-database-url==2.3.0
gunicorn==23.0.0
psycopg[binary]==3.2.9