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.
116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
import json
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
|
|
from apps.core.tiles import browser_tile_url
|
|
|
|
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.
|
|
|
|
The tile URL is the same-origin path from apps/core/tiles.py, so the map
|
|
keeps working when a viewer's network cannot reach a third-party tile
|
|
server.
|
|
"""
|
|
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;background:#eef2f5"></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),
|
|
browser_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"]
|