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.
111 lines
4 KiB
Python
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"]
|