From 082f061ae3c7e0f6c6110bc75731a03cb885690e Mon Sep 17 00:00:00 2001 From: Klink Date: Sun, 30 Aug 2026 09:32:20 +0200 Subject: [PATCH] docs: map current BMA architecture Documents the current BMA architecture and image pipeline. --- docs/README.md | 16 +++ docs/architecture/file-and-image-model.md | 122 ++++++++++++++++++ docs/architecture/frontend.md | 72 +++++++++++ docs/architecture/image-processing.md | 99 ++++++++++++++ docs/architecture/overview.md | 149 ++++++++++++++++++++++ 5 files changed, 458 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/architecture/file-and-image-model.md create mode 100644 docs/architecture/frontend.md create mode 100644 docs/architecture/image-processing.md create mode 100644 docs/architecture/overview.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..29edc99 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/architecture/file-and-image-model.md b/docs/architecture/file-and-image-model.md new file mode 100644 index 0000000..8a6cfbe --- /dev/null +++ b/docs/architecture/file-and-image-model.md @@ -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. diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md new file mode 100644 index 0000000..fd49989 --- /dev/null +++ b/docs/architecture/frontend.md @@ -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 `` element using +the local `pictures/picture.html` template. It gathers version querysets by MIME +type for a requested aspect ratio and emits `` elements followed +by an `` 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. diff --git a/docs/architecture/image-processing.md b/docs/architecture/image-processing.md new file mode 100644 index 0000000..67bc941 --- /dev/null +++ b/docs/architecture/image-processing.md @@ -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. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..8d72c1c --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,149 @@ +# BMA architecture overview + +## Scope and shape + +BMA is a Django 5.2 media archive. It stores uploaded originals and metadata, +controls access at the object level, records work that must be done on media, +and serves the resulting files. It does **not** encode images, extract EXIF, or +create thumbnail-source frames in Django. Those operations occur in an +authenticated external client (the browser worker is bundled here; the CLI is +an external consumer of the same API). + +```mermaid +flowchart LR + U[Creator / browser / CLI] -->|upload original + metadata| API[Django Ninja API] + API --> DB[(PostgreSQL)] + API --> FS[(MEDIA_ROOT filesystem)] + API --> J[FileUploadJob and pending jobs] + W[Browser grinder or external CLI worker] -->|claim jobs| API + W -->|fetch authorised source URL| M[Media view] + M --> FS + W -->|derived file / EXIF result| API + API --> DB + API --> FS + V[Browser visitor] --> T[Django templates] + T --> M +``` + +The Django project is configured in `src/bma/settings.py` and routes in +`src/bma/urls.py`. Its local applications provide users, files, image/video/ +audio/document subtypes, jobs, albums, tags, widgets, permissions, hit counts, +and presentation utilities. The JSON API is mounted at `/api/v1/json/`; the +browser UI uses Django templates under `src/**/templates/`. + +### Major boundaries + +| Boundary | Current responsibility | +| --- | --- | +| Django application | Validation, persistence, authorization, job creation/assignment/result acceptance, HTML/API responses. | +| PostgreSQL | Model state, polymorphic type metadata, object permissions, tags, albums, and jobs. The development service is PostGIS 14, although the inspected model code uses PostgreSQL range types rather than GIS fields. | +| `MEDIA_ROOT` | Original uploads and accepted derivative files. `BmaFileSystemStorage` makes opaque short-UUID URLs for these paths. | +| Web server | Production media is authorized by Django then internally handed to nginx through `X-Accel-Redirect`; development streams through Django. | +| External processor | Reads the server-issued job description, obtains the source through the authorized media endpoint, does conversion/cropping/EXIF work, and POSTs the result. Django does not invoke ffmpeg, Pillow, ImageMagick, or a queue worker. | + +## Storage and delivery + +Every media field uses `utils.storage.BmaFileSystemStorage`. Physical names are +deterministic beneath `MEDIA_ROOT`, while public URLs hide that directory layout: + +| Asset | Physical path pattern | Public URL prefix | +| --- | --- | --- | +| Original | `user_//bma__.` | `oi`, `ov`, `oa`, or `od` | +| ThumbnailSource | `user_//bma__/thumbnailsource_.` | `ts` | +| ImageVersion | `user_/image/bma_image_//imageversion_w_.` | `iv` | +| Thumbnail | `user_//bma__/thumbnails//thumbnail_w_.` | `t` | + +`files.views.bma_media_view()` decodes the short UUID, finds the database +object, checks that the parent `BaseFile` is permitted to the requester, then +either returns an nginx internal redirect or a development `FileResponse`. +Consequently, media URLs are stable handles but not public-object storage URLs: +authorization remains in the request path. + +`django-cleanup` deletes files following model deletion; the `cleanup_post_delete` +hook in `utils.apps` removes empty `MEDIA_ROOT` directories afterwards. Model +relationships use `NP_CASCADE` for the archive's dependent records, so a media +object's jobs and rendition records are also removed with it. + +## Current dependency status and Django 6 assessment + +Status below is an inspection of `pyproject.toml`, imports/settings, and the +current repository configuration on 30 August 2026. “Needed” means the checked +source directly uses it or enables it in Django; it does not claim an alternative +could not be built. “Update” means an upgrade is needed for the Django 6 move, +or that the exact pin is behind a currently published compatible release. + +### Immediate Django 6 roadblocks + +1. **Python 3.11 blocks the upgrade.** Django 6 supports Python 3.12–3.14 and + Django 5.2 is the last release supporting 3.11. This repository declares + `requires-python >=3.11`, uses `python:3.11-slim-bullseye` in + `docker/Dockerfile`, and has a `py311` tox environment. Move the supported + baseline and test/deployment image to Python 3.12 or later before resolving + Django 6. [Django 6 release notes](https://docs.djangoproject.com/en/6.0/releases/6.0/) + confirm this requirement. +2. **The Django pin must change.** `Django==5.2.14` cannot resolve to Django 6. + The project already sets `DEFAULT_AUTO_FIELD`, uses keyword `save()` calls, + and the inspected source does not call Django 6's removed APIs. A full test + run is still required after the dependency resolution because permissions, + polymorphic querying, API schemas, and templates are integration-heavy. +3. **Polymorphic querying is an upgrade hotspot.** `utils/polymorphic_related.py` + copies internal `django-polymorphic`/Django queryset state and is based on an + upstream pull request. It is local compatibility-sensitive code, so it needs + focused list/detail/API prefetch tests under Django 6 even if package + installation succeeds. +4. **Use modern compatible pins for the framework-facing packages.** In + particular, update `django-oauth-toolkit` from 3.2.0 (3.4.0 explicitly lists + Django 6.0), `django-polymorphic` from 4.11.2 (newer 4.11 releases exist), + and `django-ninja` from 1.6.0 (newer 1.6 releases list Django 6). The current + `django-filter` 25.2 already added Django 6.0 testing. Verify + `django-guardian` against its selected release: the current PyPI classifiers + do not list Django 6, and object permissions are core BMA behavior. + +Django 6 introduces a task framework, but that does not remove BMA's existing +external worker boundary: Django's own documentation likewise says task +execution belongs to external worker infrastructure. This is an observation, +not a migration recommendation. [Django tasks](https://docs.djangoproject.com/en/6.0/releases/6.0/) + +### Runtime dependencies + +| Dependency (pinned) | Purpose in BMA | Needed now? | Update/Django 6 status | +| --- | --- | --- | --- | +| `Django==5.2.14` | Web framework, ORM, templates, auth, storage base classes. | Yes | Required upgrade target; Django 6 also requires Python 3.12+. | +| `django-allauth[socialaccount]==65.16.1` | Local account flow and BornHack OpenID Connect social login. | Yes | Update to a current 65.19.x release when resolving; current releases list Django 6. | +| `django-bootstrap5==26.2` | Template tag library and Bootstrap form rendering. | Yes | Check its resolved Django 6 support during the upgrade; no local compatibility shim. | +| `django-cleanup==9.0.0` | Deletes model-backed files; BMA also hooks its post-delete signal. | Yes | Re-resolve/test with Django 6. | +| `django-cors-headers==4.9.0` | CORS middleware and origin settings for browser/API clients. | Yes | Re-resolve/test with Django 6. | +| `django-decorator-include==3.3` | Applies OAuth decorators to OAuth Toolkit endpoint patterns. | Yes | Small but framework-facing; verify under Django 6. | +| `django-filter==25.2` | FilterSets for file, tag, album, and job UIs/APIs. | Yes | No mandatory version change for 6.0: 25.2 added Django 6 testing. | +| `django-guardian==3.3.0` | Per-object file/album permissions and backend. | Yes | Update to current 3.3.x and treat compatibility as a release gate; latest PyPI classifiers inspected do not yet advertise Django 6. | +| `django-htmx==1.27.0` | HTMX request middleware. | Yes | Re-resolve/test with Django 6. | +| `django-ninja==1.6.0` | Typed JSON API, schemas, routers, uploaded-file handling. | Yes | Update to current 1.6.x; recent releases list Django 6. | +| `django-stubs-ext==5.2.9` | Runtime `monkeypatch()` for django-stubs typing support. | Yes, as currently imported in settings | Align to a Django 6-compatible stubs release; it is not merely a development extra here. | +| `django-tables2==2.8.0` | Server-rendered list tables/pagination. | Yes | Re-resolve/test with Django 6. | +| `django-oauth-toolkit==3.2.0` | OAuth2/OIDC server, tokens, worker/browser API auth. | Yes | Update to 3.4.0 or current compatible release; 3.4.0 explicitly supports Django 6.0. | +| `django-polymorphic==4.11.2` | `BaseFile` and `BaseJob` subtype loading. | Yes | Update to a current 4.11.x, then exercise the local related-polymorphic extension. | +| `django-taggit==6.1.0` | Tags and tag manager base classes. | Yes | Re-resolve/test with Django 6. | +| `demoji==1.1.0` | Converts emoji to descriptions while generating tag slugs. | Yes | No Django coupling; retain unless tag-slug behavior changes. | +| `environs[django]==14.6.0` | Reads environment configuration and database URL. | Yes | No direct Django 6 concern; retain. | +| `fontawesomefree==6.6.0` | Installed Font Awesome assets/template integration. | Yes | Asset dependency; optional only if templates/assets are changed, which is outside this assessment. | +| `orjson==3.11.7` | Ninja request parser and response renderer. | Yes | No Django coupling; retain and re-resolve for Python 3.12. | +| `psycopg2-binary==2.9.11` | PostgreSQL driver and `DateTimeTZRange` import. | Yes | Django 6 documents psycopg2 2.9.9+ as Python-3.12-compatible; this pin meets that floor. | +| `shortuuid==1.0.13` | Compact UUIDs for paths, media URLs, and display. | Yes | No Django coupling; retain. | + +`pictures` is deliberately **not** an external dependency: `src/pictures/` is a +local implementation derived from django-pictures. It supplies `PictureField`, +the rendition-path calculation, the `{% picture %}` tag, and its templates. It +must be included in the Django 6 test matrix as BMA code. + +### Development and test dependencies + +| Dependency (pinned) | Purpose | Needed now? | Django 6 action | +| --- | --- | --- | --- | +| `pre-commit`, `setuptools_scm` | Local checks and package versioning. | Yes for development/release workflow | Refresh independently as desired; not runtime blockers. | +| `beautifulsoup4` | HTML assertions in tests. | Yes for test suite | Re-resolve on Python 3.12. | +| `coverage`, `pytest-cov` | Coverage measurement. | Yes for test suite | Update/re-resolve for the new interpreter. | +| `django-debug-toolbar` | Optional development debug UI. | Yes when `DEBUG_TOOLBAR` is enabled | Choose a Django 6-compatible release before enabling it under the new stack. | +| `factory-boy`, `pytest-django`, `pytest-randomly`, `tox` | Test data, Django/pytest integration, order randomization, and environment orchestration. | Yes for the current test workflow | Add Python 3.12+ environments and update pins as resolution requires. | + +The report intentionally does not propose removing dependencies or replacing the +job design. It identifies the current use sites and upgrade checks only.