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.
This commit is contained in:
ixgadmin 2026-09-16 16:39:07 +00:00
parent 4667637265
commit 5d0b5fe757
13 changed files with 933 additions and 0 deletions

View file

@ -1,4 +1,8 @@
import json
from django.conf import settings
from django.contrib import admin
from django.utils.html import format_html
from .models import (
ProductSpecification, SpecParameter,
@ -29,6 +33,40 @@ class FacilityAdmin(admin.ModelAdmin):
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)

View file

@ -0,0 +1,68 @@
/* Draw the stored origin-zone polygon on a Leaflet map.
*
* Display only: this renders what is already stored on the record. It does not
* validate the geometry, and it does not approve the zone. Validation and the
* two-step verification are separate pieces of work.
*
* The geometry is passed in a data-* attribute so the browser decodes it (no
* JSON injected into a script block).
*/
(function () {
"use strict";
var OSM_ATTRIBUTION =
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
function init() {
var el = document.getElementById("zone-map");
if (!el || typeof L === "undefined") {
return;
}
var geometry;
try {
geometry = JSON.parse(el.dataset.geometry || "null");
} catch (err) {
el.innerHTML =
'<p style="padding:1rem;color:#b91c1c">Stored geometry is not valid JSON, so it cannot be drawn.</p>';
return;
}
if (!geometry || !geometry.coordinates) {
return;
}
var map = L.map(el, { scrollWheelZoom: false });
if (el.dataset.tileUrl) {
L.tileLayer(el.dataset.tileUrl, {
maxZoom: 19,
attribution: OSM_ATTRIBUTION,
}).addTo(map);
}
var layer = L.geoJSON(geometry, {
style: { color: "#0f766e", weight: 2, fillColor: "#14b8a6", fillOpacity: 0.25 },
}).addTo(map);
var bounds = layer.getBounds();
if (bounds && bounds.isValid()) {
map.fitBounds(bounds.pad(0.25));
} else {
map.setView([0, 0], 2);
}
// Grappelli lays the form out after load; a stale size leaves grey tiles.
setTimeout(function () {
map.invalidateSize();
if (bounds && bounds.isValid()) {
map.fitBounds(bounds.pad(0.25));
}
}, 250);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View file

@ -303,3 +303,63 @@ class PageRenderTests(TestCase):
self.assertEqual(resp.status_code, 302)
self.coa.refresh_from_db()
self.assertEqual(self.coa.status, CertificateOfAnalysis.Status.PUBLISHED)
class OriginZoneMapAdminTests(TestCase):
"""The admin map that closes the "no map view" gap, and its empty states.
The map is display only: these tests pin the fact that it draws what is
stored, and that a zone without coordinates explains itself instead of
rendering an empty box or raising.
"""
def setUp(self):
self.admin = User.objects.create_user(
email="zoneadmin@ixg.local",
password="pass1234",
role="admin",
is_staff=True,
is_superuser=True,
)
self.facility = Facility.objects.create(
name="IXG Processing Facility - Katsina", country="Nigeria",
region="Katsina State", facility_type=Facility.FacilityType.PROCESSING,
)
self.zone = OriginZone.objects.create(
facility=self.facility, zone_code="NG-KT-Z99", country="Nigeria",
region="Katsina State",
geometry={
"type": "Polygon",
"coordinates": [[[7.6012, 12.9905], [7.618, 12.9905],
[7.618, 13.0042], [7.6012, 13.0042],
[7.6012, 12.9905]]],
},
)
def test_zone_map_draws_the_stored_polygon(self):
self.client.force_login(self.admin)
resp = self.client.get(f"/admin/compliance/originzone/{self.zone.id}/change/")
self.assertEqual(resp.status_code, 200)
html = resp.content.decode()
self.assertIn('id="zone-map"', html)
self.assertIn("7.6012", html) # the stored coordinates
self.assertIn("vendor/leaflet/leaflet.js", html) # the library is loaded
self.assertIn("js/ixg_zone_map.js", html)
def test_zone_without_coordinates_says_so(self):
bare = OriginZone.objects.create(
facility=self.facility, zone_code="NG-KT-Z98", country="Nigeria",
geometry={},
)
self.client.force_login(self.admin)
resp = self.client.get(f"/admin/compliance/originzone/{bare.id}/change/")
self.assertEqual(resp.status_code, 200)
html = resp.content.decode()
self.assertIn("No polygon coordinates are stored", html)
self.assertNotIn('id="zone-map"', html)
def test_zone_map_on_the_add_page_is_not_an_error(self):
self.client.force_login(self.admin)
resp = self.client.get("/admin/compliance/originzone/add/")
self.assertEqual(resp.status_code, 200)
self.assertNotIn('id="zone-map"', resp.content.decode())

26
apps/core/static/vendor/leaflet/LICENSE vendored Normal file
View file

@ -0,0 +1,26 @@
BSD 2-Clause License
Copyright (c) 2010-2023, Volodymyr Agafonkin
Copyright (c) 2010-2011, CloudMade
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,64 @@
# Leaflet 1.9.4 — vendored front-end asset
Third-party code, self-hosted so the platform does not depend on a CDN for a
component that renders supplier-location data.
| | |
|---|---|
| Library | Leaflet (interactive maps) |
| Version | **1.9.4** (latest stable; the 1.9 line is mature — pin it) |
| Source | npm package `leaflet@1.9.4`, `dist/` |
| Vendored | 2026-09-16 |
| Licence | BSD-2-Clause — see `LICENSE` in this folder |
| Upstream | https://github.com/Leaflet/Leaflet |
## Integrity
Both shipped files were validated against the SRI hashes published by the
Leaflet project (leafletjs.com/download.html). They match, so these are the
authentic 1.9.4 artefacts:
```
sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo= leaflet.js
sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY= leaflet.css
```
Raw checksums (regenerate with `sha256sum leaflet.js leaflet.css images/*.png`):
```
db49d009c841f5ca34a888c96511ae936fd9f5533e90d8b2c4d57596f4e5641a leaflet.js
a7837102824184820dfa198d1ebcd109ff6d0ff9a2672a074b9a1b4d147d04c6 leaflet.css
066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf images/layers-2x.png
1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6 images/layers.png
00179c4c1ee830d3a108412ae0d294f55776cfeb085c60129a39aa6fc4ae2528 images/marker-icon-2x.png
574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437 images/marker-icon.png
264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da images/marker-shadow.png
```
## What is deliberately NOT here
Only `dist/` is vendored: `leaflet.js`, `leaflet.css` and `images/`. The source
tree, the `.map` source maps and `leaflet-src.js` are omitted to keep the
repository lean. Add a source map back only when debugging a specific issue.
## Upgrading
```bash
mkdir -p /tmp/lf && cd /tmp/lf && npm install leaflet@<new-version>
cp node_modules/leaflet/dist/leaflet.js node_modules/leaflet/dist/leaflet.css .
cp node_modules/leaflet/dist/images/*.png images/
cp node_modules/leaflet/LICENSE .
sha256sum leaflet.js leaflet.css images/*.png # and update this file
docker exec ixg-platform-app python manage.py collectstatic --noinput
```
Check the new version's published SRI hashes match before shipping, and read the
upstream changelog for breaking changes — a future major line is in development
upstream, so do not jump to it without testing.
## Usage
Loaded by the Django admin (see `apps/compliance/admin.py`, `OriginZoneAdmin`)
via the `Media` class, and drawn by
`apps/compliance/static/js/ixg_zone_map.js`. The tile source is configurable —
see the `LEAFLET_TILE_URL` setting in `dsp_platform/settings.py`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View file

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

View file

@ -133,6 +133,16 @@ MEDIA_ROOT = BASE_DIR / "media"
WIKI_PUBLIC_BASE = env("WIKI_PUBLIC_BASE", default="http://194.164.95.50")
# Tile source for the Leaflet map showing origin zones (apps/compliance/admin.py).
# Self-host this, or use a licensed provider, before it carries production
# traffic: OSM's public tile servers are governed by their usage policy and are
# only suitable for internal, low-volume use. Note also that any hosted tile
# source sees the areas being viewed — which, for this platform, are the places
# IXG sources shea.
LEAFLET_TILE_URL = env(
"LEAFLET_TILE_URL", default="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
)
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "users.User"