ixg_platform/apps/compliance/admin.py
ixgadmin 5d0b5fe757 Origin zones: draw the stored polygon on a map in the admin (Leaflet 1.9.4)
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.
2026-09-16 16:39:07 +00:00

111 lines
4 KiB
Python

import json
from django.conf import settings
from django.contrib import admin
from django.utils.html import format_html
from .models import (
ProductSpecification, SpecParameter,
Facility, OriginZone, CollectorGroup, HarvestIntake, IntakeBatchLineage,
DueDiligenceCase, EUDREvidence, RiskAssessment, MitigationAction,
DueDiligenceStatementReference,
)
class SpecParameterInline(admin.TabularInline):
model = SpecParameter
extra = 0
@admin.register(ProductSpecification)
class ProductSpecificationAdmin(admin.ModelAdmin):
list_display = ["product", "version", "status", "is_current", "effective_from", "effective_to", "approved_at"]
list_filter = ["status", "is_current", "product"]
inlines = [SpecParameterInline]
@admin.register(Facility)
class FacilityAdmin(admin.ModelAdmin):
list_display = ["name", "country", "region", "facility_type", "is_active"]
@admin.register(OriginZone)
class OriginZoneAdmin(admin.ModelAdmin):
list_display = ["zone_code", "facility", "country", "region", "verification_status", "verified_at"]
list_filter = ["verification_status", "country"]
readonly_fields = ["zone_map"]
class Media:
css = {"all": ("vendor/leaflet/leaflet.css",)}
js = ("vendor/leaflet/leaflet.js", "js/ixg_zone_map.js")
@admin.display(description="Area on a map")
def zone_map(self, obj):
"""Draw the stored polygon, so the shape can be checked by eye.
Display only. It does not validate the geometry and it does not verify
the zone — those are separate pieces of work.
"""
if obj is None:
return "Save this zone, then its area can be drawn here."
if not obj.geometry or not obj.geometry.get("coordinates"):
return format_html(
'<div style="max-width:900px;padding:1rem;border:1px dashed #cbd5e1;'
'border-radius:.375rem;color:#64748b">No polygon coordinates are stored '
'for this zone, so there is nothing to draw. Coordinates are captured in '
'the field, then entered on the Geometry field.</div>'
)
return format_html(
'<div id="zone-map" data-geometry="{}" data-tile-url="{}" '
'style="height:420px;max-width:900px;border:1px solid #cbd5e1;'
'border-radius:.375rem"></div>'
'<p style="max-width:900px;margin-top:.5rem;font-size:.8rem;color:#64748b">'
"Drawn from the stored polygon. A view, not a check: it does not validate the "
"geometry and it does not verify the zone.</p>",
json.dumps(obj.geometry),
settings.LEAFLET_TILE_URL,
)
@admin.register(CollectorGroup)
class CollectorGroupAdmin(admin.ModelAdmin):
list_display = ["name", "contact_reference", "origin_zone"]
@admin.register(HarvestIntake)
class HarvestIntakeAdmin(admin.ModelAdmin):
list_display = ["intake_reference", "intake_date", "weight_kg", "grade", "origin_zone", "status"]
list_filter = ["status"]
@admin.register(IntakeBatchLineage)
class IntakeBatchLineageAdmin(admin.ModelAdmin):
list_display = ["batch", "harvest_intake", "weight_allocated_kg"]
@admin.register(DueDiligenceCase)
class DueDiligenceCaseAdmin(admin.ModelAdmin):
list_display = ["batch", "status", "risk_level", "approved_at", "updated_at"]
list_filter = ["status", "risk_level"]
@admin.register(EUDREvidence)
class EUDREvidenceAdmin(admin.ModelAdmin):
list_display = ["case", "evidence_type", "source", "collected_at", "verified_at"]
@admin.register(RiskAssessment)
class RiskAssessmentAdmin(admin.ModelAdmin):
list_display = ["case", "risk_level", "assessed_by", "assessed_at"]
@admin.register(MitigationAction)
class MitigationActionAdmin(admin.ModelAdmin):
list_display = ["case", "action", "status", "owner", "due_date"]
@admin.register(DueDiligenceStatementReference)
class DueDiligenceStatementReferenceAdmin(admin.ModelAdmin):
list_display = ["case", "dds_reference", "submission_date", "submitted_by", "reconciliation_status"]