diff --git a/pages/cap/views.py b/pages/cap/views.py index d3485484..7c5ed01d 100644 --- a/pages/cap/views.py +++ b/pages/cap/views.py @@ -1,13 +1,11 @@ import json from typing import List -from lxml import etree from capeditor.constants import SEVERITY_MAPPING from capeditor.models import CapSetting from capeditor.renderers import CapXMLRenderer from capeditor.serializers import AlertSerializer as BaseAlertSerializer from django.contrib.syndication.views import Feed -from base.cache import wagcache from django.core.validators import validate_email from django.db.models.base import Model from django.http import JsonResponse, HttpResponse @@ -17,10 +15,12 @@ from django.utils import timezone from django.utils.feedgenerator import Enclosure, rfc2822_date from django.utils.feedgenerator import Rss201rev2Feed +from lxml import etree from rest_framework.generics import get_object_or_404 from wagtail.api.v2.utils import get_full_url from wagtail.models import Site +from base.cache import wagcache from .models import CapAlertPage from .sign import sign_xml @@ -138,18 +138,28 @@ def get_cap_xml(request, identifier): xml = wagcache.get(f"cap_xml_{identifier}") if not xml: - data = AlertSerializer(alert).data + data = AlertSerializer(alert, context={ + "request": request, + }).data + xml = CapXMLRenderer().render(data) xml_bytes = bytes(xml, encoding='utf-8') + signed = False try: signed_xml = sign_xml(xml_bytes) if signed_xml: xml = signed_xml + signed = True except Exception as e: + print(e) pass - root = etree.fromstring(xml) + if signed: + root = etree.fromstring(xml) + else: + root = etree.fromstring(xml_bytes) + tree = etree.ElementTree(root) style_url = get_full_url(request, reverse("cap_stylesheet")) pi = etree.ProcessingInstruction('xml-stylesheet', f'type="text/xsl" href="{style_url}"') diff --git a/pages/cap/wagtail_hooks.py b/pages/cap/wagtail_hooks.py index f4ccf237..288a98ee 100644 --- a/pages/cap/wagtail_hooks.py +++ b/pages/cap/wagtail_hooks.py @@ -15,6 +15,7 @@ from wagtail.admin.forms.pages import CopyForm from wagtail.admin.menu import MenuItem, Menu from wagtail.blocks import StreamValue +from wagtail_modeladmin.helpers import PagePermissionHelper from wagtail.models import Page from wagtail_modeladmin.helpers import AdminURLHelper from wagtail_modeladmin.menus import GroupMenuItem @@ -32,6 +33,32 @@ from .utils import create_cap_geomanager_dataset +class CAPPagePermissionHelper(PagePermissionHelper): + def user_can_edit_obj(self, user, obj): + can_edit = super().user_can_edit_obj(user, obj) + + if obj.live and obj.status == "Actual": + return False + + return can_edit + + def user_can_delete_obj(self, user, obj): + can_delete = super().user_can_delete_obj(user, obj) + + if obj.live and obj.status == "Actual": + return False + + return can_delete + + def user_can_unpublish_obj(self, user, obj): + can_unpublish = super().user_can_unpublish_obj(user, obj) + + if obj.live and obj.status == "Actual": + return False + + return can_unpublish + + class CAPAdmin(ModelAdmin): model = CapAlertPage menu_label = _('Alerts') @@ -39,6 +66,7 @@ class CAPAdmin(ModelAdmin): menu_order = 200 add_to_settings_menu = False exclude_from_explorer = False + permission_helper_class = CAPPagePermissionHelper class CAPMenuGroupAdminMenuItem(GroupMenuItem): diff --git a/pages/home/static/js/weather_watch.js b/pages/home/static/js/weather_watch.js index 2c94f563..2cf92324 100644 --- a/pages/home/static/js/weather_watch.js +++ b/pages/home/static/js/weather_watch.js @@ -63,14 +63,29 @@ $((async function () { } = await fetch(homeMapSettingsUrl).then(response => response.json()) + // create map const map = new maplibregl.Map({ container: "home-map", // container ID style: defaultStyle, - center: [0, 0], // starting position [lng, lat] - zoom: 4, // starting zoom + center: [0, 0], + zoom: 4, scrollZoom: false, }); + + // Add zoom control to the map. + map.addControl(new maplibregl.NavigationControl({showCompass: false})); + + // add fullscreen control + map.addControl(new maplibregl.FullscreenControl()); + + // add attribution + map.addControl(new maplibregl.AttributionControl({ + customAttribution: '© OpenStreetMap © CARTO', + compact: false, + })); + + // fetch all weather icons and load to map if (weatherIconsUrl) { fetch(weatherIconsUrl).then(response => response.json()).then(icons => { icons.forEach(icon => { @@ -81,6 +96,7 @@ $((async function () { }); } + // fetch forecast settings if (forecastSettingsUrl) { fetch(forecastSettingsUrl).then(response => response.json()).then(settings => { forecastSettings = settings @@ -90,172 +106,7 @@ $((async function () { // wait for map to load await new Promise((resolve) => map.on("load", resolve)); - if (bounds) { - const mapBounds = [[bounds[0], bounds[1]], [bounds[2], bounds[3]]] - map.fitBounds(mapBounds, {padding: 20}); - } else { - if (countryInfo && countryInfo.bbox) { - const bbox = countryInfo.bbox - map.fitBounds(bbox, {padding: 20}); - } - } - - - fetch(homeMapAlertsUrl) - .then(response => { - if (!response.ok) { - throw new Error('Error fetching alerts'); - } - return response.text(); - }) - .then(alertsHTML => { - const $alerts = $(alertsHTML) - - if ($alerts.length) { - $alerts.appendTo("#alerts-container") - $("#alerts-legend").show() - - - // add cap alerts layer - map.addSource("alert-areas", { - type: "geojson", - data: capGeojsonUrl, - }); - - map.addLayer({ - id: "alert-areas-layer", - type: "fill", - source: "alert-areas", - paint: { - "fill-color": [ - "case", - ["==", ["get", "severity"], "Extreme"], - "#d72f2a", - ["==", ["get", "severity"], "Severe"], - "#f89904", - ["==", ["get", "severity"], "Moderate"], - "#e4e616", - ["==", ["get", "severity"], "Minor"], - "#53ffff", - ["==", ["get", "severity"], "Unknown"], - "#3366ff", - "black", - ], - "fill-opacity": 0.7, - "fill-outline-color": "#000", - }, - }); - - // CAP alerts layer on click - map.on("click", "alert-areas-layer", (e) => { - // Copy coordinates array. - - const description = e.features[0].properties.areaDesc; - const severity = e.features[0].properties.severity; - const event = e.features[0].properties.event; - - new maplibregl.Popup() - .setLngLat(e.lngLat) - .setHTML(`

${description}

${event}


${severity} severity

`) - .addTo(map); - - - }); - - // Change the cursor to a pointer when the mouse is over the alerts layer. - map.on("mouseenter", "alert-areas-layer", () => { - map.getCanvas().style.cursor = "pointer"; - }); - - // Change it back to a pointer when it leaves. - map.on("mouseleave", "alert-areas-layer", () => { - map.getCanvas().style.cursor = ""; - }); - } - }) - .catch(error => { - console.error("HOME_MAP_ALERTS_ERROR:", error) - }) - - - // Create a popup object - const popup = new maplibregl.Popup({ - closeButton: false, - closeOnClick: false - }); - - - // Add zoom and rotation controls to the map. - map.addControl(new maplibregl.NavigationControl({showCompass: false})); - - // add fullscreen control - map.addControl(new maplibregl.FullscreenControl()); - - map.addControl(new maplibregl.AttributionControl({ - customAttribution: '© OpenStreetMap © CARTO', - compact: false, - })); - - const getPopupHTML = (props) => { - const dataParams = forecastSettings?.parameters || [] - if (dataParams && dataParams.length === 0) { - return null - } - - const paramValues = dataParams.reduce((all, param) => { - if (props[param.parameter]) { - all[param.name] = `${props[param.parameter]} ${param.parameter_unit}` - } - return all - }, {}) - - const cityName = props.city; - const citySlug = props.city_slug; - const condition = props.condition_label; - - let values = Object.keys(paramValues).reduce((all, key) => { - all = all + `

${key}: ${paramValues[key]}

` - return all - }, "") - - let detailLink - if (cityDetailUrl) { - const detailUrl = cityDetailUrl + citySlug - detailLink = ` - Details - - - - ` - } - - - return `
-

${cityName}

-

${condition}

- ${detailLink ? detailLink : ""} -
- ${values} - - -
` - } - - - if (zoomLocations && !!zoomLocations.length) { - map.addControl(new ZoomToLocationsControl(zoomLocations), 'top-right'); - } - - const defaultZoomLocation = zoomLocations.find(loc => loc.default) - - if (defaultZoomLocation && defaultZoomLocation.bounds) { - map.fitBounds(defaultZoomLocation.bounds, { - padding: 20 - }); - } - - - // add country layer + // add boundary layer if (boundaryTilesUrl) { // add source map.addSource("admin-boundary-source", { @@ -301,7 +152,6 @@ $((async function () { }); } - // add city forecast source map.addSource("city-forecasts", { type: "geojson", @@ -323,6 +173,177 @@ $((async function () { source: "city-forecasts" }) + let zoomLocationsInit = false + const updateMapBounds = () => { + if (bounds) { + const mapBounds = [[bounds[0], bounds[1]], [bounds[2], bounds[3]]] + map.fitBounds(mapBounds, {padding: 50}); + } else { + if (countryInfo && countryInfo.bbox) { + const bbox = countryInfo.bbox + map.fitBounds(bbox, {padding: 50}); + } + } + + if (!zoomLocationsInit) { + initZoomLocations() + zoomLocationsInit = true + } + } + + const initZoomLocations = () => { + // Zoom Locations + if (zoomLocations && !!zoomLocations.length) { + map.addControl(new ZoomToLocationsControl(zoomLocations), 'top-right'); + } + + const defaultZoomLocation = zoomLocations.find(loc => loc.default) + + if (defaultZoomLocation && defaultZoomLocation.bounds) { + map.fitBounds(defaultZoomLocation.bounds, { + padding: 50 + }); + } + } + + if (homeMapAlertsUrl) { + // CAP Alerts + fetch(homeMapAlertsUrl) + .then(response => { + if (!response.ok) { + throw new Error('Error fetching alerts'); + } + return response.text(); + }) + .then(alertsHTML => { + const $alerts = $(alertsHTML) + + if ($alerts.length) { + $alerts.appendTo("#alerts-container") + $("#alerts-legend").show() + fetch(capGeojsonUrl).then(response => response.json()).then(geojsonAlertsData => { + if (geojsonAlertsData.features.length > 0) { + const bounds = turf.bbox(geojsonAlertsData); + + // fit map to alert bounds + map.fitBounds(bounds, {padding: 50}); + + // add cap alerts layer + map.addSource("alert-areas", { + type: "geojson", + data: geojsonAlertsData, + }); + + map.addLayer({ + id: "alert-areas-layer", + type: "fill", + source: "alert-areas", + paint: { + "fill-color": [ + "case", + ["==", ["get", "severity"], "Extreme"], + "#d72f2a", + ["==", ["get", "severity"], "Severe"], + "#f89904", + ["==", ["get", "severity"], "Moderate"], + "#e4e616", + ["==", ["get", "severity"], "Minor"], + "#53ffff", + ["==", ["get", "severity"], "Unknown"], + "#3366ff", + "black", + ], + "fill-opacity": 0.7, + "fill-outline-color": "#000", + }, + }, "city-forecasts"); + + // CAP alerts layer on click + map.on("click", "alert-areas-layer", (e) => { + // Copy coordinates array. + const description = e.features[0].properties.areaDesc; + const severity = e.features[0].properties.severity; + const event = e.features[0].properties.event; + + new maplibregl.Popup() + .setLngLat(e.lngLat) + .setHTML(`

${description}

${event}


${severity} severity

`) + .addTo(map); + }); + + // Change the cursor to a pointer when the mouse is over the alerts layer. + map.on("mouseenter", "alert-areas-layer", () => { + map.getCanvas().style.cursor = "pointer"; + }); + + // Change it back to a pointer when it leaves. + map.on("mouseleave", "alert-areas-layer", () => { + map.getCanvas().style.cursor = ""; + }); + } else { + updateMapBounds() + } + }).catch(error => { + console.error("HOME_MAP_ALERTS_GEOJSON_ERROR:", error) + updateMapBounds() + }) + } + }) + .catch(error => { + console.error("HOME_MAP_ALERTS_ERROR:", error) + updateMapBounds() + }) + } else { + updateMapBounds() + } + + + const getPopupHTML = (props) => { + const dataParams = forecastSettings?.parameters || [] + if (dataParams && dataParams.length === 0) { + return null + } + + const paramValues = dataParams.reduce((all, param) => { + if (props[param.parameter]) { + all[param.name] = `${props[param.parameter]} ${param.parameter_unit}` + } + return all + }, {}) + + const cityName = props.city; + const citySlug = props.city_slug; + const condition = props.condition_label; + + let values = Object.keys(paramValues).reduce((all, key) => { + all = all + `

${key}: ${paramValues[key]}

` + return all + }, "") + + let detailLink + if (cityDetailUrl) { + const detailUrl = cityDetailUrl + citySlug + detailLink = ` + Details + + + + ` + } + + + return `
+

${cityName}

+

${condition}

+ ${detailLink ? detailLink : ""} +
+ ${values} + + +
` + } + + // city forecast on click map.on("click", "city-forecasts", (e) => { // Get the feature that was hovered over @@ -344,8 +365,6 @@ $((async function () { .setHTML(popupHTML) .addTo(map); } - - }); // Change it back to a pointer when it leaves. diff --git a/pages/home/views.py b/pages/home/views.py index 4c5f5e3e..767c557f 100644 --- a/pages/home/views.py +++ b/pages/home/views.py @@ -2,10 +2,11 @@ from django.http import JsonResponse from django.urls import reverse from wagtail.api.v2.utils import get_full_url -from django.shortcuts import render + from base.models import OrganisationSetting from pages.home.models import HomeMapSettings + def home_map_settings(request): config = { "zoomLocations": []