A UK buyer can now create their own account at /portal/signup/. The screen
creates a Keycloak user through a least-privilege service account
(ixg-provisioning: manage-users, view-users) and the matching Django row as
a buyer, then sends them through the normal sign-in flow to the portal.
Staff status is derived from the role in the OIDC backend rather than
asserted, so a self-registered buyer is not staff and does not become staff
on a later sign-in. The platform root and the RBAC middleware now route
each role to its own portal, which also fixes a redirect loop that the
staff fix exposed for non-staff buyers.
Cover access on GET and POST for a buyer, each required field when blank, an ETD
after the ETA, a batch that already has a shipment (queried, not blocked), a
status change, and an edit updating in place. Add the shipment routes to both
OPS_ROUTES sweeps so the detail and edit screens render clean markup, and assert
the native date inputs carry ISO values — the defect that left the intake and
batch edit dates blank.
A buyer account is a legitimate authenticated user, so IsAuthenticated alone
lets it through. Sweep every ModelViewSet endpoint and assert a buyer gets 403
on write, and on read where the API is ops-only; assert ops is not forbidden;
and keep an explicit allowlist for the chat, user, notification and subscription
reads a buyer may still make. Also assert the escalation is closed: a buyer
cannot PATCH their own role to admin, sees only themselves in the user API, and
cannot reach another user's notification or subscription.
Replace the Bootstrap-prefix allowlist with a default-to-leak rule for bare
hyphenated tokens, parse tags quote-aware so a '>' inside an earlier attribute
cannot truncate the tag, render the batch edit and lineage screens in the sweep
with a seeded batch, and cover stale evidence in the seed re-run test.
Evidence rows are keyed on the filename, so reassigning the in-flight batch's
intakes left the old harvest_document row behind. Delete anything the case no
longer references, mirroring the lineage cleanup in _batch.
The ops templates shipped with 346 of them: an unquoted attribute value ends at
the first whitespace, so the styles were never applied and status badges rendered
invisible. Assert it on the rendered HTML rather than the template source, because
a template may hold {{ }} inside an unquoted attribute that renders harmlessly —
the source is ambiguous, the response is not.
Harvest intakes could only be recorded in the administration area or through the
API, so the first record the business actually creates was entered outside the
portal it works in. Add list, create and correct screens under Harvest Intakes.
The form is deliberately stricter than the model. HarvestIntake allows almost
everything to be blank because records also arrive by API, and the platform
copes by flagging the gaps later — the EUDR completeness check has an intake
data test whose whole job is to catch a missing date or weight. The form
therefore requires every field those checks read: reference, collector group,
origin zone, date, weight and grade.
Three things it does beyond capturing the fields: it suggests the next intake
reference for the year (editable, since it may have to match a weighbridge
ticket), fills the origin zone in from the collector group while leaving it
editable, and refuses a duplicate reference — including one differing only in
case. It queries, rather than blocks, when the chosen zone is not the one the
group is registered to, and it marks unverified zones in the dropdown, because
geolocation passes only for a verified zone.
Correcting amends the record in place: the reference a buyer or auditor quotes
does not change.
The origin-zone map drew its background from a public tile server, which made it
the only part of the admin page that depended on a third-party domain — and the
part that breaks. Where a viewer's network, proxy or browser filter refuses
tile.openstreetmap.org (the refusal arrives as a 403) the map never draws, while
the page, the Leaflet bundle and the polygon data are all same-origin and fine.
Fetch the tiles server-side and serve them from /tiles/{z}/{x}/{y}.png: cached
on disk, so a page opened repeatedly does not re-fetch, and gated on being
signed in, so the route is not an open tile relay. The upstream stays
configurable via LEAFLET_TILE_URL, and LEAFLET_TILE_PROXY_ENABLED=False puts the
browser back on direct fetches.
When a tile still cannot be fetched, answer an empty tile rather than an error
page and say so under the map, instead of leaving a viewer with a blank grid and
no explanation.
Media was unreachable from either portal, which is why the certificate button on
the buyer compliance page returned page-not-found and no document row had
anything to click. The cause was structural: the portal and ops hosts proxy
location / to the app with a path prefix, so a stored /media/... URL arrives as
/portal/media/... and matches no route.
Add a download view per portal behind the same role gate as the page that links
to it — buyer-visible documents and published certificates for buyers, anything
the ops list shows for operations — resolving the stored path safely under
MEDIA_ROOT so a traversal or an absolute path is refused. Point the seven
templates that list files at the new routes.
Also add a portal-wide render sweep: nothing in the suite was fetching the ops
or portal pages, so a view could lose its template_name and 500 in front of a
user with every test still green.
Closes the "no map view" gap for origin zones: the polygon was stored as
coordinates and shown as raw JSON, so the shape could not be checked by eye.
- vendor Leaflet 1.9.4 into apps/core/static/vendor/leaflet. Self-hosted rather
than pulled from a CDN, because this renders supplier-location data.
PROVENANCE.md records the version, source, licence (BSD-2-Clause), checksums
and upgrade steps; the checksums match the SRI hashes Leaflet publishes
- OriginZoneAdmin gains an "Area on a map" read-only row and its Media assets;
apps/compliance/static/js/ixg_zone_map.js draws the polygon from the stored
geometry, passed through a data attribute rather than injected into a script
- the tile source is a setting (LEAFLET_TILE_URL), so moving to self-hosted
tiles is a config change, not a code change
Deliberately display only: it draws what is stored. It does not validate the
geometry, and it does not verify the zone - both remain open work, and the page
says so where the map appears.
Tests cover the drawn map, a zone with no coordinates, and the add page.
Builds one coherent, fully fictional batch chain for ops training, so the same
batch reference appears on every screen a trainer walks through: origin zone and
collector groups, harvest intakes and lineage, processing batch, QC result and
four-eyes verification, certificate of analysis with its supporting documents,
EUDR due-diligence case, shipment and ESG report.
Three batches on purpose: a completed happy path, one mid-flight, and one QC
failure with a CAPA so the exception branch has something to show.
Strictly synthetic data - no real buyer, supplier, collector or producer data.