Wagtail CMS and API backend for the Cheminova site, providing:
- A single “Home” page with image support
- Secure image upload and per-request authorization
- Custom image model with renditions and live-page tracking
- JSON API endpoints for pages and images
- Containerized development and deployment with Docker & Docker Compose
- Prerequisites
- Installation
- Local Development (Docker Compose)
- Running Tests
- Deployment
- Dump Database and Backup to S3
- Restore Database
- Image Upload Flow
- Image Request Flow (with Authentication)
- Image API Request Flow (Metadata)
- uv
- Docker & Docker Compose (for containerized workflows)
Scripts are defined in tasks.py and can be run with uv:
uv run invoke $TASK_NAMEList available tasks:
uv run invoke --listAvailable tasks:
- admin-user: Create or update an admin user.
- bump: Bump version using uv version and create a git tag.
- dev: Run the development server with docker compose.
- export-dump: Dump database and export dump to S3.
- format: Format code using ruff.
- import-dump: Import dump from S3 and load it into the database.
- init-site: Initialize the default Wagtail site with the given URL.
- sync-assets: Sync static and media assets from S3 to local storage.
- test: Run tests using django test framework.
Clone the repo:
git clone ***REMOVED***
cd cheminova-backend-
Configure environment variables
cp .env.example .env
and edit
.envto set variables as needed. -
Configure the Minio client
cp config/mc/config.json.example config/mc/config.json
and edit it to set access key and secret key for the different environments.
-
Bring up the full stack (Postgres, Wagtail, Nginx, Minio)
uv run invoke dev
-
Apply database migrations
docker compose exec wagtail uv run manage.py migrate -
Create a superuser
uv run invoke admin-user --password $ADMIN_PASSWORD -
Create a default site
uv run invoke init-site --site-url http://localhost
-
Import site data from a live environment (optional)
Get the latest dump filename from the S3 bucket.
uv run invoke import-dump --file-name $LATEST_DUMP_FILENAME -
Sync media assets from S3 (optional)
uv run invoke sync-assets
-
Copy the frontend site to the
frontenddirectory (optional) -
Wagtail runs at http://localhost:8000/cms
-
Frontend at http://localhost:8080/
-
Wagtail CMS admin at http://localhost:8080/cms/admin/
-
Backend API at http://localhost:8080/cms/api/
-
Minio web UI at http://localhost:9001/
```bash
uv run invoke test
```
-
Build and push Docker images.
-
In production, ensure the following env vars are set: • SECRET_KEY • POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST • (Optional) BASE_PATH if serving under a subpath
-
The production.py settings module will configure: • Secure proxy headers • CSRF trusted origins • Wagtail admin base URL
docker compose exec wagtail uv run manage.py export_dumpChoose a dump file in the S3 bucket. To restore the database from a dump file, you can use the following command:
docker compose exec wagtail uv run manage.py import_dump $DUMP_FILENAMEUsers and site data will be restored from the local database unless the --no-restore-local-data flag is provided.
sequenceDiagram
participant Client
participant nginx as nginx:8080
participant wagtail as Wagtail:8000
participant upload_view as image_upload.views
participant serializer as ImageModelSerializer
participant model as CustomImage Model
participant db as PostgreSQL
participant fs as File System
Client->>nginx: POST /api/upload/<br/>(multipart/form-data with image)
nginx->>wagtail: proxy_pass to wagtail:8000/api/upload/
wagtail->>upload_view: upload_image_view(request)
upload_view->>upload_view: Extract file from request.data
upload_view->>upload_view: Generate UUID filename<br/>(original-{uuid}.ext)
upload_view->>serializer: ImageModelSerializer(data={file, title})
serializer->>serializer: validate()
alt validation successful
serializer->>model: CustomImage.objects.create()
model->>db: INSERT image record
model->>fs: Save image file to /media/original_images/
model->>serializer: return created instance
serializer->>upload_view: return serialized data
upload_view->>wagtail: Response(data, status=201)
wagtail->>nginx: HTTP 201 with image data
nginx->>Client: HTTP 201 with image metadata
else validation failed
serializer->>upload_view: return errors
upload_view->>wagtail: Response(errors, status=400)
wagtail->>nginx: HTTP 400
nginx->>Client: HTTP 400 with error details
end
sequenceDiagram
participant Client
participant nginx as nginx:8080
participant wagtail as Wagtail:8000
participant auth_view as image_auth.views
participant model as CustomImage Model
participant db as PostgreSQL
participant fs as File System
Client->>nginx: GET /media/images/some-image.jpg
nginx->>nginx: auth_request /api/image-auth/
nginx->>wagtail: GET /api/image-auth/<br/>(internal, X-Original-URI header)
wagtail->>auth_view: check_permissions(request)
alt user is authenticated
auth_view->>wagtail: Response(200, "OK")
wagtail->>nginx: HTTP 200
nginx->>fs: serve file from /media/
fs->>nginx: image file content
nginx->>Client: HTTP 200 with image
else user not authenticated
auth_view->>auth_view: get_image_file(X-Original-URI)
auth_view->>auth_view: get_image_type(image_path)
alt image type is "rendition"
auth_view->>model: RenditionModel.objects.get(file=path)
model->>db: SELECT rendition
db->>model: rendition record
model->>auth_view: rendition.image
else image type is "original"
auth_view->>model: CustomImage.objects.get(file=path)
model->>db: SELECT image
db->>model: image record
model->>auth_view: image
end
auth_view->>model: image.get_referenced_live_pages()
model->>db: Query page references
db->>model: live page references
alt image has live page references
model->>auth_view: [live_pages]
auth_view->>wagtail: Response(200, "OK")
wagtail->>nginx: HTTP 200
nginx->>fs: serve file from /media/
fs->>nginx: image file content
nginx->>Client: HTTP 200 with image
else no live page references
model->>auth_view: []
auth_view->>wagtail: Response(401, "Unauthorized")
wagtail->>nginx: HTTP 401
nginx->>Client: HTTP 401 Unauthorized
end
end
sequenceDiagram
participant Client
participant nginx as nginx:8080
participant wagtail as Wagtail:8000
participant api_view as custom_images.views
participant serializer as CustomImageModelSerializer
participant model as CustomImage Model
participant db as PostgreSQL
Client->>nginx: GET /api/images/<br/>(with Authorization header)
nginx->>wagtail: proxy_pass to wagtail:8000/api/images/
wagtail->>api_view: CustomImageViewSet.list(request)
api_view->>api_view: Check IsAuthenticated permission
alt user is authenticated
api_view->>model: CustomImage.objects.all()
model->>db: SELECT * FROM custom_images
db->>model: image records
loop for each image
model->>serializer: CustomImageModelSerializer(image)
serializer->>model: get_referenced_live_pages()
model->>db: Query page references
db->>model: live page references
model->>serializer: live pages list
serializer->>serializer: set 'live' field (len > 0)
serializer->>api_view: serialized image data
end
api_view->>wagtail: Response with image list
wagtail->>nginx: HTTP 200 with JSON
nginx->>Client: HTTP 200 with image metadata
else user not authenticated
api_view->>wagtail: Response(401, "Unauthorized")
wagtail->>nginx: HTTP 401
nginx->>Client: HTTP 401 Unauthorized
end