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
|
||||
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:
|
||||
|
||||
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_CSRF_TRUSTED_ORIGINS=http://localhost:8080
|
||||
DJANGO_DEBUG=false
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:4200,http://localhost:8080
|
||||
TIME_ZONE=Europe/Rome
|
||||
|
||||
POSTGRES_DB=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 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:
|
||||
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -97,6 +97,8 @@ Required backend configuration:
|
||||
- `DJANGO_SECRET_KEY`;
|
||||
- `DJANGO_ALLOWED_HOSTS`;
|
||||
- `DJANGO_CSRF_TRUSTED_ORIGINS`;
|
||||
- `CORS_ALLOWED_ORIGINS`;
|
||||
- `TIME_ZONE`;
|
||||
- `DATABASE_URL` or equivalent database settings;
|
||||
- email host, port, username, password, TLS settings, and sender address;
|
||||
- public site URL used to build confirmation and QR verification links.
|
||||
@@ -147,14 +149,16 @@ Expected validation commands:
|
||||
|
||||
```bash
|
||||
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 test
|
||||
```
|
||||
|
||||
The canonical repository check for the current infrastructure stage is:
|
||||
The canonical repository check for the current stage is:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
@@ -80,6 +80,7 @@ Required controls:
|
||||
- check-in verification preview and confirmation require authenticated staff or admin users;
|
||||
- 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.
|
||||
- CORS must allow only configured Angular frontend origins through `CORS_ALLOWED_ORIGINS`.
|
||||
|
||||
## Input Validation
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ All tests should run inside Docker containers.
|
||||
|
||||
```bash
|
||||
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
|
||||
@@ -21,3 +22,8 @@ Describe applicable categories:
|
||||
- Ansible syntax checks;
|
||||
- Docker/Compose validation;
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
context: ../..
|
||||
dockerfile: infra/docker/backend/Dockerfile
|
||||
image: azionelab-backend:local
|
||||
environment:
|
||||
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
|
||||
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS}
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS}
|
||||
DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
|
||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
||||
TIME_ZONE: ${TIME_ZONE:-Europe/Rome}
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
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