generated from bisco/codex-bootstrap
feat: add Django backend skeleton
This commit is contained in:
@@ -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
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
@@ -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
|
||||||
|
|||||||
1
backend/azionelab/__init__.py
Normal file
1
backend/azionelab/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
8
backend/azionelab/asgi.py
Normal file
8
backend/azionelab/asgi.py
Normal 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()
|
||||||
114
backend/azionelab/settings.py
Normal file
114
backend/azionelab/settings.py
Normal 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",
|
||||||
|
],
|
||||||
|
}
|
||||||
10
backend/azionelab/tests.py
Normal file
10
backend/azionelab/tests.py
Normal 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
15
backend/azionelab/urls.py
Normal 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"),
|
||||||
|
]
|
||||||
8
backend/azionelab/wsgi.py
Normal file
8
backend/azionelab/wsgi.py
Normal 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()
|
||||||
1
backend/bookings/__init__.py
Normal file
1
backend/bookings/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
6
backend/bookings/apps.py
Normal file
6
backend/bookings/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BookingsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "bookings"
|
||||||
1
backend/bookings/models.py
Normal file
1
backend/bookings/models.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Domain models will be added when booking behavior is implemented.
|
||||||
1
backend/checkins/__init__.py
Normal file
1
backend/checkins/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
6
backend/checkins/apps.py
Normal file
6
backend/checkins/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CheckinsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "checkins"
|
||||||
1
backend/checkins/models.py
Normal file
1
backend/checkins/models.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Domain models will be added when check-in behavior is implemented.
|
||||||
14
backend/manage.py
Normal file
14
backend/manage.py
Normal 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()
|
||||||
1
backend/shows/__init__.py
Normal file
1
backend/shows/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
6
backend/shows/apps.py
Normal file
6
backend/shows/apps.py
Normal 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
1
backend/shows/models.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Domain models will be added when show management is implemented.
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -5,18 +5,17 @@ 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/backend/
|
||||||
|
|
||||||
|
WORKDIR /app/backend
|
||||||
|
|
||||||
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"]
|
||||||
|
|||||||
@@ -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]
|
|
||||||
@@ -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
6
requirements/backend.txt
Normal 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
|
||||||
Reference in New Issue
Block a user