Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# BMA documentation

This directory describes the repository as it exists on 30 August 2026. It is
implementation documentation, not a target design or a refactoring proposal.

- [Architecture overview](architecture/overview.md) — application boundaries,
data flow, storage, and the dependency/Django 6 status report.
- [File and image model](architecture/file-and-image-model.md) — the persisted
media, rendition, and job relationships.
- [Image processing](architecture/image-processing.md) — how BMA creates and
accepts client-produced media work.
- [Frontend](architecture/frontend.md) — Django templates, custom template
tags, JavaScript, and media presentation.

The code is the source of truth. Paths in these documents are repository-root
relative.
122 changes: 122 additions & 0 deletions docs/architecture/file-and-image-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# File and image model

## Core archive model

`files.models.BaseFile` is the polymorphic root for archive items. Its concrete
subclasses are `Image`, `Video`, `Audio`, and `Document`; callers querying the
root normally receive the real subtype through `django-polymorphic`.

It owns the shared archive metadata: UUID primary key and short UUID display
form, uploader, upload job, timestamps, title/description/source, original
filename/size/MIME type, CC license and attribution, moderation/publication/
soft-delete flags, and user-tagged `django-taggit` tags. It also has the file
object permissions used by `django-guardian`. A file is visible either when the
requester has `view_basefile` on it or when it is both approved and published.

```mermaid
classDiagram
class BaseFile {
UUID uuid PK
User uploader
FileUploadJob job
title, description, original_filename
file_size, mimetype, license, attribution
approved, published, deleted
create_jobs()
create_thumbnail_jobs()
}
class Image {
PictureField original
width, height, aspect_ratio, pixels
crop_center_x, crop_center_y
JSON exif
}
class Video { FileField original }
class Audio { FileField original }
class Document { FileField original }
class ImageVersion {
ImageConversionJob job
PictureField imagefile
width, height, aspect_ratio, mimetype
}
class ThumbnailSource {
ThumbnailSourceJob job
PictureField source
crop_center_x, crop_center_y
}
class Thumbnail {
ThumbnailJob job
PictureField imagefile
width, height, aspect_ratio, mimetype
}
BaseFile <|-- Image
BaseFile <|-- Video
BaseFile <|-- Audio
BaseFile <|-- Document
BaseFile "1" --> "0..*" ImageVersion : image_versions (Image only)
BaseFile "1" --> "0..1" ThumbnailSource : thumbnailsource
BaseFile "1" --> "0..*" Thumbnail : thumbnails
ThumbnailSource "0..1" --> "0..*" Thumbnail : source
```

The original field lives on each concrete subtype. `Image.original` is a local
`PictureField`; the other concrete subtypes use their own original file fields.
`BaseFile.filename` derives the basename from that subtype field. An upload
first persists the subtype, then creates a completed `FileUploadJob`, associates
it back to `BaseFile.job`, adds tags/object permissions, and invokes
`create_jobs()`.

## Image and rendition records

`images.models.Image` adds image dimensions, a stored aspect-ratio string,
database-persisted `pixels`, crop-center percentages, JSON EXIF, and the
original image. Its `create_jobs()` creates EXIF extraction, full-size alternate
format, smaller responsive-version, and thumbnail jobs as applicable.

`ImageVersion` is a non-polymorphic derivative record. It has a one-to-one
`ImageConversionJob` and points to `files.BaseFile` rather than `Image` so the
prefetch helpers can fetch versions while querying the polymorphic root. It is
unique by `(image, width, height, mimetype)`, ordered widest first, and keeps an
advertised aspect ratio separate from minor rounding in actual dimensions.

`Image.get_versions()` and `get_fullsize_version()` deliberately consume the
prefetched `image_version_list` rather than issuing a query. File list/detail
views call `prefetch_image_version_list()` for this reason. If another caller
uses these methods without that setup, the expected attribute is absent rather
than transparently queried.

## Thumbnail records

`ThumbnailSource` is a one-to-one image input used to generate thumbnails. It
is required for video, audio, and document files; it is optional for an image,
where the original may be used directly. Its field configuration asks for WEBP
versions at 1:1, 4:3, 16:9, and 2:3, up to 200 CSS pixels across four grid
columns at 1x and 2x. The record keeps its own crop center.

`Thumbnail` is the generated artifact. It points to its `BaseFile` and to the
`ThumbnailSource` when one exists; image-direct thumbnails have `source=NULL`.
It is one-to-one with `ThumbnailJob`, unique by `(basefile, width, height,
mimetype)`, and ordered widest first. `BaseFile.get_thumbnails()` groups
prefetched thumbnail records by aspect ratio, MIME type, and width for template
rendering.

Both rendition classes share `ImageModel`: file size, reported MIME type,
width/height, advertised ratio, and persisted `pixels`. These are separate
models rather than subclassing `Image` because the code explicitly avoids mixing
the non-polymorphic image mixin into the polymorphic hierarchy.

## Local django-pictures derivative

The `pictures` app is local source adapted from django-pictures, not a PyPI
dependency. `PictureField` extends Django's `ImageField` and uses a
`PictureFieldFile` to calculate the expected output files. `SimplePicture`
represents one expected rendition and derives a predictable storage name from
the parent filename, optional custom aspect ratio, width, and output type.

The shared `PICTURES` configuration is one 4,000-pixel-wide 12-column container,
native aspect ratio by default (`None`), 1x/2x densities, and WEBP output. The
field-specific thumbnail-source settings override that shared size/grid/ratio
policy. `get_picture_files_list()` therefore tells BMA which image-version or
thumbnail artifacts should exist; it does not generate them. The processor is
configured as `images.picture_processor.dummy_processor`, consistent with the
external-processing boundary.
72 changes: 72 additions & 0 deletions docs/architecture/frontend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Frontend and templates

## Rendering model

BMA is a server-rendered Django application with progressive JavaScript. Views
select templates under `src/templates/` and each app's `templates/` directory;
Bootstrap 5 supplies layout/components, `django-tables2` supplies tables,
`django-filter` supplies filter forms, and HTMX middleware is installed. The
repository also ships direct JavaScript integrations for uploads, jobs,
PhotoSwipe, Splide, Dropzone, Compressor.js, EXIF parsing, and the image editor.

```mermaid
flowchart TD
V[Django views] --> T[Base and app templates]
T --> Tags[Custom template tags]
Tags --> P[Picture / thumbnail HTML]
P --> Media[Authorised short-UUID media URLs]
T --> JS[Static JavaScript]
JS --> API[JSON API]
API --> T
```

`src/templates/base.html` is the shared shell. App templates extend it or a
feature-level template; examples include `files/templates/file_show.html`,
`file_list_grid.html`, `albums/templates/`, `tags/templates/`, and
`jobs/templates/grinder.html`. `widgets/templates/` renders embeddable
gallery/PhotoSwipe/Splide fragments, and `templates/allauth/` overrides
allauth's layouts/elements.

## Media rendering

`utils.templatetags.bma_utils` is the BMA presentation bridge.

| Tag | Current behavior |
| --- | --- |
| `{% render_file %}` | Selects the image `picture` tag or audio/video/document include according to `filetype`. |
| `{% thumbnail %}` | Finds a prefetched WEBP thumbnail at a supported width/ratio, emits 1x/2x markup, or returns the configured file-type placeholder. |
| `{% render_source_set %}` | Groups prefetched `ImageVersion` records and produces a width-descriptor `srcset` for an image/MIME/aspect-ratio request. |
| `{% media_query %}` | Converts grid breakpoint arguments into a responsive `sizes` string. |
| `{% noscript_embed %}` / `{% photoswipe_embed %}` | Produces simple linked fallback markup or widget script markup. |

`pictures.templatetags.pictures.picture` renders a `<picture>` element using
the local `pictures/picture.html` template. It gathers version querysets by MIME
type for a requested aspect ratio and emits `<source srcset>` elements followed
by an `<img>` pointing at the original. The image list and detail views prefetch
versions and thumbnails to support this pattern.

File detail views use PhotoSwipe markup and pass original, full-size WEBP,
dimensions, and `srcset` data. Grid/gallery templates use the thumbnail tag and
the same PhotoSwipe integration. If a rendition is not finished, templates can
still fall back to the original or a static file-type thumbnail, depending on
the tag/path used.

## Browser-side flows

The upload page uses Dropzone and `upload.js`; it builds multipart API requests
with original metadata and may create/client-crop a thumbnail source before
uploading. `uploadClient.js` is shared by upload and grinder flows. It owns an
OAuth client, file/job queues, job polling/assignment, in-browser conversion
through Compressor.js, and result submission.

The grinder template (`jobs/templates/grinder.html`) exposes Start/Stop controls,
progress, and a log. It loads the browser worker's dependencies and
`grinder.js`, which continuously asks for work while running. The UI is thus a
worker implementation as well as an administration page; see
[image processing](image-processing.md) for its supported job boundary.

Static source assets live in `src/static_src/` and are served from `STATIC_URL`;
`src/assets/css/` contains the CSS package metadata. `STATIC_ROOT` is the
collection destination. Media is different: templates use the short media URLs
created by `BmaFileSystemStorage`, and every such request re-enters BMA's
authorized media view before the development server or nginx delivers bytes.
99 changes: 99 additions & 0 deletions docs/architecture/image-processing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Image processing and jobs

## Processing contract

BMA persists work as polymorphic `jobs.models.BaseJob` records. A job contains
its UUID, parent `BaseFile`, source URL, optional assigned worker user/client
UUID/version, timestamps, and completion state. The API is the contract with
the processor; a worker need not share BMA's filesystem or process space.

```mermaid
sequenceDiagram
participant C as Creator/client
participant B as BMA API
participant S as Media storage/view
participant W as Worker (browser grinder or CLI)
C->>B: POST /files/upload/ original + metadata
B->>S: persist original
B->>B: create completed FileUploadJob and pending derivative jobs
W->>B: POST /jobs/assign/ with client identity
B-->>W: all unassigned unfinished jobs for one BaseFile
W->>S: GET source_url with session/OAuth token
W->>W: resize, crop, make source frame, or parse EXIF
W->>B: POST /jobs/{uuid}/result/ result + metadata
B->>S: persist accepted derivative when applicable
B->>B: create/delete rendition record, mark job finished
```

`POST /jobs/assign/` is restricted to users with the worker role. It clears
unfinished assignments older than 24 hours, then assigns every unassigned,
unfinished job for the first available file to one requester. This grouping lets
one processor retain/fetch the source once for a file. Job result submission is
also worker-only and requires an unfinished job UUID. The server validates
image metadata before it writes a rendition, then records the worker identity
and marks the job finished.

The browser grinder (`/jobs/grinder/`) uses `static_src/js/uploadClient.js`.
It obtains OAuth credentials, claims jobs, caches fetched input blobs by source
URL, uses Compressor.js to resize or crop, and uploads results. Its EXIF branch
is presently commented out; an external CLI that supports the API can perform
that job. The JavaScript worker only processes sources that are images. For
non-image source media, it cannot fulfil the corresponding thumbnail-source
job and unassigns it, which is why the external CLI boundary is operationally
significant.

## Job types and results

| Job class | Created for | Worker output | Server-side result handling |
| --- | --- | --- | --- |
| `FileUploadJob` | Every accepted original upload. | None; it records the upload. | Already finished; `result_url()` is the original. |
| `ImageExifExtractionJob` | Images whose `exif` field is `NULL`. | JSON EXIF file. | Loads JSON into `Image.exif`. |
| `ImageConversionJob` | Images: full-size selected output formats and required smaller responsive outputs. | Image at requested width/height/type, crop behavior, and metadata. | Validates and writes an `ImageVersion`; deletes a pre-existing record at the same image/dimensions/MIME first. |
| `ThumbnailSourceJob` | Video, audio, and document files lacking a `ThumbnailSource`. | Representative image/frame plus dimensions/MIME metadata. | Replaces the existing `ThumbnailSource` in a transaction, then creates needed thumbnail jobs. |
| `ThumbnailJob` | All files once an image or thumbnail source is available. | Requested thumbnail image plus metadata. | Replaces the same `(file, width, height, MIME)` thumbnail transactionally; attaches its source when present. |

`ImageJob` supplies the conversion specification used by the two image-output
jobs: requested width, height, output extension, optional custom aspect ratio,
and 0–100 crop-center coordinates. It derives the output MIME type from
`ALLOWED_IMAGE_TYPES`. The server's image result schema receives width, height,
and MIME type from the worker; model validation and uniqueness constraints
protect the persisted result.

## Image workflow

For an image upload, BMA stores the original and calculates `aspect_ratio` from
the upload's dimensions. `Image.create_jobs()` then:

1. creates an EXIF job when metadata is missing;
2. creates a full-size `ImageConversionJob` for each configured output type if
the matching full-size version is absent;
3. calculates smaller native-ratio `PictureField` candidates and creates jobs
for files not present on disk; and
4. calculates thumbnail candidates from the original image, using the image's
crop center.

For video/audio/document uploads, `BaseFile.create_jobs()` first requests a
thumbnail source unless one accompanied the upload. Once the source arrives,
the server calculates the thumbnail candidates from it. An upload may include
`thumbnail_data` and thumbnail metadata, in which case it creates a completed
`ThumbnailSourceJob` and source during the original upload path.

`get_or_create()` prevents duplicate unfinished work for the same configured
job fields, and physical-file existence skips creating some image/thumbnail
jobs. Result handling deletes a competing existing derivative only after the
new object has passed validation (inside the documented transaction for
thumbnails and thumbnail sources).

## Processor settings and limits

`GET /jobs/settings/` publishes supported input MIME type mappings, licenses,
and `IMAGE_ENCODING` (currently WEBP quality 90, lossy) to clients. The
repository's development environment permits JPEG, BMP, GIF, SVG, TIFF, PNG,
and WEBP originals, selected video/audio formats, and plain text/PDF documents.
`PICTURES["FILE_TYPES"]` currently contains only `WEBP`, so the configured
derivative target is WEBP even though the accepted original formats are wider.

No task queue, broker, subprocess runner, or server-side image encoder is
present in the inspected project. The durable job table is the hand-off and
recovery mechanism; successful processing depends on a compatible, authorized
worker being available.
Loading
Loading