diff --git a/capeditor/blocks.py b/capeditor/blocks.py index a972be1..f42b521 100644 --- a/capeditor/blocks.py +++ b/capeditor/blocks.py @@ -13,7 +13,7 @@ from wagtail.models import Site from wagtailmodelchooser.blocks import ModelChooserBlock -from .forms.fields import PolygonField, MultiPolygonField, BoundaryMultiPolygonField +from .forms.fields import PolygonField, MultiPolygonField, BoundaryMultiPolygonField, PolygonOrMultiPolygonField from .forms.widgets import CircleWidget from .utils import file_path_mime @@ -58,6 +58,26 @@ def value_from_form(self, value): return value +class PolygonOrMultiPolygonFieldBlock(FieldBlock): + def __init__(self, required=True, help_text=None, srid=4326, **kwargs): + self.field_options = { + "required": required, + "help_text": help_text, + "srid": srid + } + + super().__init__(**kwargs) + + @cached_property + def field(self): + return PolygonOrMultiPolygonField(**self.field_options) + + def value_from_form(self, value): + if isinstance(value, GEOSGeometry): + value = value.json + return value + + class PolygonFieldBlock(FieldBlock): def __init__(self, required=True, help_text=None, srid=4326, **kwargs): self.field_options = { @@ -174,11 +194,6 @@ def geojson(self): polygon = self.get("boundary") return json.loads(polygon) - # @cached_property - # def aread_desc(self): - # area_desc = self.get("areaDesc") - # return json.loads(polygon) - class AlertAreaBoundaryBlock(blocks.StructBlock): class Meta: @@ -209,15 +224,25 @@ class Meta: class AlertAreaPolygonStructValue(StructValue): @cached_property def area(self): - polygon_geojson_str = self.get("polygon") - polygon_geojson_dict = json.loads(polygon_geojson_str) + geom_geojson_str = self.get("polygon") + geom_geojson_dict = json.loads(geom_geojson_str) + geom_shape = shape(geom_geojson_dict) + + polygons = [] + + if isinstance(geom_shape, Polygon): + polygons.append(geom_shape) + else: + polygons = list(geom_shape.geoms) - polygon = shape(polygon_geojson_dict) - coords = " ".join(["{},{}".format(y, x) for x, y in list(polygon.exterior.coords)]) + polygons_data = [] + for polygon in polygons: + coords = " ".join(["{},{}".format(y, x) for x, y in list(polygon.exterior.reverse().coords)]) + polygons_data.append(coords) area_data = { "areaDesc": self.get("areaDesc"), - "polygon": coords, + "polygons": polygons_data } if self.get("altitude"): @@ -239,9 +264,10 @@ class Meta: areaDesc = blocks.TextBlock(label=_("Affected areas / Regions"), help_text=_("The text describing the affected area of the alert message")) - polygon = PolygonFieldBlock(label=_("Polygon"), - help_text=_("The paired values of points defining a polygon that delineates " - "the affected area of the alert message")) + polygon = PolygonOrMultiPolygonFieldBlock(label=_("Polygon"), + help_text=_( + "The paired values of points defining a polygon that delineates " + "the affected area of the alert message")) altitude = blocks.CharBlock(max_length=100, required=False, label=_("Altitude"), help_text=_("The specific or minimum altitude of the affected " "area of the alert message")) @@ -576,7 +602,7 @@ class AlertInfo(blocks.StructBlock): ('Env', _("Pollution and other environmental")), ('Transport', _("Public and private transportation")), ('Infra', _("Utility, telecommunication, other non-transport infrastructure")), - ('Cbrne', _("Chemical, Biological, Radiological, Nuclear or High-Yield Explosive threat or attack")), + ('CBRNE', _("Chemical, Biological, Radiological, Nuclear or High-Yield Explosive threat or attack")), ('Other', _("Other events")), ) @@ -607,9 +633,10 @@ class AlertInfo(blocks.StructBlock): help_text=_("The text denoting the type of the subject event of the alert message. You " "can define hazards events monitored by your institution from CAP settings")) - category = blocks.ChoiceBlock(choices=CATEGORY_CHOICES, default="Met", label=_("Category"), - help_text=_("The code denoting the category of the subject" - " event of the alert message")) + category = blocks.MultipleChoiceBlock(choices=CATEGORY_CHOICES, default="Met", label=_("Category"), + help_text=_("The code denoting the category of the subject" + " event of the alert message"), + widget=forms.CheckboxSelectMultiple) language = blocks.ChoiceBlock(choices=LANGUAGE_CHOICES, default="en", required=False, label=_("Language"), help_text=_("The code denoting the language of the alert message"), ) diff --git a/capeditor/cap_settings.py b/capeditor/cap_settings.py index 8fba963..9ba5125 100644 --- a/capeditor/cap_settings.py +++ b/capeditor/cap_settings.py @@ -61,6 +61,24 @@ class Meta: ], heading=_("Predefined Areas"), classname="map-resize-trigger"), ]) + @property + def contact_list(self): + contacts = [] + for contact_block in self.contacts: + contact = contact_block.value.get("contact") + if contact: + contacts.append(contact) + return contacts + + @property + def audience_list(self): + audiences = [] + for audience_block in self.audience_types: + audience = audience_block.value.get("audience") + if audience: + audiences.append(audience) + return audiences + class HazardEventTypes(Orderable): setting = ParentalKey(CapSetting, on_delete=models.PROTECT, related_name="hazard_event_types") @@ -119,3 +137,15 @@ def get_default_sender(): if cap_setting and cap_setting.sender: return cap_setting.sender return None + + +def get_cap_contact_list(request): + cap_settings = CapSetting.for_request(request) + contacts_list = cap_settings.contact_list + return contacts_list + + +def get_cap_audience_list(request): + cap_settings = CapSetting.for_request(request) + audience_list = cap_settings.audience_list + return audience_list diff --git a/capeditor/caputils.py b/capeditor/caputils.py new file mode 100644 index 0000000..888bd8e --- /dev/null +++ b/capeditor/caputils.py @@ -0,0 +1,376 @@ +from datetime import datetime + +import pytz +from shapely import Point +from xmltodict import parse as parse_xml_to_dict + +from capeditor.errors import CAPImportError + +CAP_ALERT_ELEMENTS = [ + { + "name": "identifier", + "required": True, + }, + { + "name": "sender", + "required": True, + }, + { + "name": "sent", + "required": True, + "datetime": True, + }, + { + "name": "status", + "required": True, + }, + { + "name": "msgType", + "required": True, + }, + { + "name": "scope", + "required": True, + }, + { + "name": "restriction", + "required": False, + }, + { + "name": "addresses", + "required": False, + }, + { + "name": "code", + "required": False, + "many": True, + }, + { + "name": "note", + "required": False, + }, + { + "name": "references", + "required": False, + }, + { + "name": "incidents", + "required": False, + }, + { + "name": "info", + "required": True, + "many": True, + "elements": [ + { + "name": "language", + "required": False, + }, + { + "name": "category", + "required": True, + "many": True, + }, + { + "name": "event", + "required": True, + }, + { + "name": "responseType", + "required": False, + "many": True, + }, + { + "name": "urgency", + "required": True, + }, + { + "name": "severity", + "required": True, + }, + { + "name": "certainty", + "required": True, + }, + { + "name": "audience", + "required": False, + }, + { + "name": "eventCode", + "required": False, + "many": True, + "elements": [ + { + "name": "valueName", + "required": True, + }, + { + "name": "value", + "required": True, + } + ] + }, + { + "name": "effective", + "required": False, + "datetime": True, + }, + { + "name": "onset", + "required": False, + "datetime": True, + }, + { + "name": "expires", + "required": False, + "datetime": True, + }, + { + "name": "senderName", + "required": False, + }, + { + "name": "headline", + "required": True, + }, + { + "name": "description", + "required": True, + }, + { + "name": "instruction", + "required": False, + }, + { + "name": "web", + "required": False, + }, + { + "name": "contact", + "required": False, + }, + { + "name": "parameter", + "required": False, + "many": True, + "elements": [ + { + "name": "valueName", + "required": True, + }, + { + "name": "value", + "required": True, + } + ] + }, + { + "name": "resource", + "required": False, + "many": True, + "elements": [ + { + "name": "resourceDesc", + "required": True, + }, + { + "name": "mimeType", + "required": True, + }, + { + "name": "size", + "required": False, + }, + { + "name": "uri", + "required": False, + }, + { + "name": "derefUri", + "required": False, + }, + { + "name": "digest", + "required": False, + } + ] + }, + { + "name": "area", + "required": True, + "many": True, + "geo": True, + "elements": [ + { + "name": "areaDesc", + "required": True, + }, + { + "name": "polygon", + "required": False, + "many": True, + }, + { + "name": "circle", + "required": False, + "many": True, + }, + { + "name": "geocode", + "required": False, + "many": True, + "elements": [ + { + "name": "valueName", + "required": True, + }, + { + "name": "value", + "required": True, + } + ] + }, + { + "name": "altitude", + "required": False, + }, + { + "name": "ceiling", + "required": False, + } + ] + } + ] + } +] + + +def extract_element_data(element, data, validate=True): + """ + Extract element data. + + :param element: The element to extract. + :param data: The data to extract from. + :param validate: Whether to validate the data for required fields. + :return: The extracted element data. + """ + + element_name = element["name"] + element_data = data.get(element_name) or data.get(f"cap:{element_name}") + + if validate and element["required"] and not element_data: + raise CAPImportError(f"Missing required element: {element_name}") + + if not element_data: + return None + + if element.get("datetime"): + element_data = datetime.fromisoformat(element_data) + element_data = element_data.astimezone(pytz.utc) + element_data = element_data.isoformat() + + if element.get("many"): + if not isinstance(element_data, list): + element_data = [element_data] + + if element.get("elements"): + if isinstance(element_data, list): + for i, item in enumerate(element_data): + element_data[i] = {} + for sub_element in element["elements"]: + sub_element_data = extract_element_data(sub_element, item, validate=validate) + sub_element_name = sub_element["name"] + + if sub_element_data: + element_data[i][sub_element_name] = sub_element_data + + if sub_element_name == "polygon": + if sub_element_data: + polygons = [] + for polygon in sub_element_data: + coordinates = [] + for point in polygon.split(" "): + lat, lon = point.split(",") + coordinates.append([float(lon), float(lat)]) + polygons.append([coordinates]) + + if len(polygons) > 1: + geometry = { + "type": "MultiPolygon", + "coordinates": polygons + } + else: + geometry = { + "type": "Polygon", + "coordinates": polygons[0] + } + element_data[i]["geometry"] = geometry + + if sub_element_name == "circle": + if sub_element_data: + polygons = [] + for circle in sub_element_data: + parts = circle.split() + coords = parts[0].split(',') + # Extract the longitude, latitude, and radius + longitude, latitude, radius_km = float(coords[0]), float(coords[1]), float(parts[1]) + # Create a point for the center + center_point = Point(longitude, latitude) + + # Convert radius to degrees (approximation for small distances) + radius_deg = radius_km / 111.12 + + circle = center_point.buffer(radius_deg) + coordinates = [[y, x] for x, y in list(circle.exterior.coords)] + + polygons.append([coordinates]) + + if len(polygons) > 1: + geometry = { + "type": "MultiPolygon", + "coordinates": polygons + } + else: + geometry = { + "type": "Polygon", + "coordinates": polygons[0] + } + element_data[i]["geometry"] = geometry + else: + for sub_element in element["elements"]: + sub_element_data = extract_element_data(sub_element, element_data, validate=validate) + sub_element_name = sub_element["name"] + + if sub_element_data: + element_data[sub_element_name] = sub_element_data + + return element_data + + +def cap_xml_to_alert_data(cap_xml_string, validate=True): + """ + Convert a CAP XML string to a GeoJSON FeatureCollection. + + :param cap_xml_string: A string containing a CAP XML document. + :param validate: Whether to validate the CAP XML with the CAP schema for compulsory fields. + :return: Alert data as a dictionary. + """ + + xml_dict = parse_xml_to_dict(cap_xml_string) + + alert_element_data = xml_dict.get("alert") or xml_dict.get("cap:alert") + + if not alert_element_data: + raise CAPImportError("The loaded XML is not a valid CAP alert.") + + alert_data = {} + + for element in CAP_ALERT_ELEMENTS: + element_name = element["name"] + element_data = extract_element_data(element, alert_element_data, validate=validate) + + if element_data: + alert_data[element_name] = element_data + + return alert_data diff --git a/capeditor/errors.py b/capeditor/errors.py new file mode 100644 index 0000000..a2cb4b9 --- /dev/null +++ b/capeditor/errors.py @@ -0,0 +1,8 @@ +class Error(Exception): + + def __init__(self, message): + self.message = message + + +class CAPImportError(Error): + pass diff --git a/capeditor/forms/capimporter.py b/capeditor/forms/capimporter.py new file mode 100644 index 0000000..3de82bd --- /dev/null +++ b/capeditor/forms/capimporter.py @@ -0,0 +1,83 @@ +import requests +from django import forms +from django.utils.translation import gettext_lazy as _ + +from capeditor.caputils import cap_xml_to_alert_data +from capeditor.errors import CAPImportError + + +class CAPLoadForm(forms.Form): + LOAD_FROM_CHOICES = ( + ('text', _('Copy Paste XML')), + ('url', _('URL')), + ('file', _('File')), + ) + + load_from = forms.ChoiceField(choices=LOAD_FROM_CHOICES, initial="text", widget=forms.RadioSelect, + label=_('Load from'), ) + text = forms.CharField(required=False, widget=forms.Textarea, label=_('Paste your CAP XML here')) + url = forms.URLField(required=False, label=_('CAP Alert XML URL')) + file = forms.FileField(required=False, label=_('CAP File')) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['file'].widget.attrs.update({'accept': '.xml'}) + + def clean(self): + cleaned_data = super().clean() + load_from = cleaned_data.get('load_from') + text = cleaned_data.get('text') + url = cleaned_data.get('url') + file = cleaned_data.get('file') + + # field validation + if load_from == 'text' and not text: + self.add_error('text', _('This field is required.')) + if load_from == 'url' and not url: + self.add_error('url', _('This field is required.')) + if load_from == 'file' and not file: + self.add_error('file', _('This field is required.')) + + try: + if load_from == 'text': + content = cleaned_data.get('text') + alert_source = { + "type": "Copied XML", + } + elif load_from == 'file': + file = cleaned_data.get('file') + content = file.read().decode('utf-8') + alert_source = { + "name": file.name, + "type": "File", + } + else: + url = cleaned_data.get('url') + res = requests.get(url) + res.raise_for_status() + content = res.text + alert_source = { + "name": url, + "type": "URL", + } + + alert_data = cap_xml_to_alert_data(content) + + alert_data['alert_source'] = alert_source + + cleaned_data['alert_data'] = alert_data + + except CAPImportError as e: + self.add_error(None, e.message) + return cleaned_data + + except Exception as e: + self.add_error(None, _("An error occurred while trying to load the CAP XML. " + "Please check the data and try again.")) + return cleaned_data + + return cleaned_data + + +class CAPImportForm(forms.Form): + alert_data = forms.JSONField(widget=forms.HiddenInput) diff --git a/capeditor/forms/fields.py b/capeditor/forms/fields.py index 6ea01d4..892e1b4 100644 --- a/capeditor/forms/fields.py +++ b/capeditor/forms/fields.py @@ -1,4 +1,6 @@ from django.contrib.gis.forms import GeometryField as BaseGeometryField +from django.contrib.gis.geos import GEOSException +from django.core.exceptions import ValidationError from .widgets import PolygonWidget, BoundaryPolygonWidget @@ -27,5 +29,41 @@ class MultiPolygonField(MultiPolygonGeometryField): widget = PolygonWidget +class PolygonOrMultiPolygonField(BaseGeometryField): + widget = PolygonWidget + + def clean(self, value): + """ + Validate that the input value can be converted to a Geometry object + and return it. Raise a ValidationError if the value cannot be + instantiated as a Geometry. + """ + geom = super(BaseGeometryField, self).clean(value) + if geom is None: + return geom + + # Ensuring that the geometry is of the correct type (indicated + # using the OGC string label). + val_geom_type = str(geom.geom_type).upper() + + ALLOWED_GEOM_TYPES = ["POLYGON", "MULTIPOLYGON"] + + if val_geom_type not in ALLOWED_GEOM_TYPES: + raise ValidationError( + self.error_messages["invalid_geom_type"], code="invalid_geom_type" + ) + + # Transforming the geometry if the SRID was set. + if self.srid and self.srid != -1 and self.srid != geom.srid: + try: + geom.transform(self.srid) + except GEOSException: + raise ValidationError( + self.error_messages["transform_error"], code="transform_error" + ) + + return geom + + class PolygonField(PolygonGeometryField): widget = PolygonWidget diff --git a/capeditor/models.py b/capeditor/models.py index b3c4ae8..cb68095 100644 --- a/capeditor/models.py +++ b/capeditor/models.py @@ -1,4 +1,3 @@ -import json import os import uuid diff --git a/capeditor/static/capeditor/css/import_cap_preview.css b/capeditor/static/capeditor/css/import_cap_preview.css new file mode 100644 index 0000000..f215f17 --- /dev/null +++ b/capeditor/static/capeditor/css/import_cap_preview.css @@ -0,0 +1,47 @@ +.alert-source { + margin: 20px 0; + padding: 20px; +} + +.alert-source-item { + padding: 4px 0; + font-size: 14px; +} + +.alert-source-item-label { + font-weight: bold; +} + +.alert-meta-card { + margin: 20px 0; + padding: 20px; + +} + +.alert-meta-item { + padding: 4px 0; + font-size: 14px; +} + +.alert-meta-item-label { + font-weight: bold; +} + +.alert-detail { + padding: 40px 20px; +} + +.info-item { + padding: 4px 0; + font-size: 14px; +} + +.info-item-label { + font-weight: bold; +} + + +#cap-map { + height: 400px; + width: 100%; +} \ No newline at end of file diff --git a/capeditor/static/capeditor/js/widget/circle-widget.js b/capeditor/static/capeditor/js/widget/circle-widget.js index 6ede87a..8f69fea 100644 --- a/capeditor/static/capeditor/js/widget/circle-widget.js +++ b/capeditor/static/capeditor/js/widget/circle-widget.js @@ -277,6 +277,8 @@ CircleWidget.prototype.setSourceData = function (feature) { CircleWidget.prototype.initFromState = function () { const circeValue = this.getState() + console.log(circeValue) + if (circeValue) { const {lon, lat, radius} = this.parseCircleValue(circeValue) || {} diff --git a/capeditor/static/capeditor/js/widget/polygon-draw-widget.js b/capeditor/static/capeditor/js/widget/polygon-draw-widget.js index aa2d4a3..985c8be 100644 --- a/capeditor/static/capeditor/js/widget/polygon-draw-widget.js +++ b/capeditor/static/capeditor/js/widget/polygon-draw-widget.js @@ -173,10 +173,6 @@ class PolygonDrawWidget { if (combinedFeatures) { const feature = combinedFeatures.features[0] - if (feature.properties) { - delete feature.properties - } - this.setValue(JSON.stringify(feature.geometry)) } else { diff --git a/capeditor/static/capeditor/js/widget/polygon-widget.js b/capeditor/static/capeditor/js/widget/polygon-widget.js index b0fd83c..a9984ec 100644 --- a/capeditor/static/capeditor/js/widget/polygon-widget.js +++ b/capeditor/static/capeditor/js/widget/polygon-widget.js @@ -261,12 +261,18 @@ PolygonWidget.prototype.initDraw = function () { e.preventDefault() const isSaveButton = e.target.classList.contains("mapboxgl-draw-actions-btn_save") if (isSaveButton) { - const FC = this.draw.getAll(); - const feat = FC.features[0] - if (feat) { - const feature = feat.geometry - this.setDrawData(feature) + let combinedFeatures + + const featureCollection = this.draw.getAll() + if (featureCollection && featureCollection.features && !!featureCollection.features.length) { + combinedFeatures = turf.combine(featureCollection) + } + + if (combinedFeatures) { + const feature = combinedFeatures.features[0] + + this.setDrawData(feature.geometry) } else { this.setDrawData(null) } @@ -283,9 +289,18 @@ PolygonWidget.prototype.initDraw = function () { }) this.map.on("draw.create", (e) => { - const feat = e.features[0] - const feature = feat.geometry - this.setDrawData(feature) + let combinedFeatures + + const featureCollection = this.draw.getAll() + if (featureCollection && featureCollection.features && !!featureCollection.features.length) { + combinedFeatures = turf.combine(featureCollection) + } + + if (combinedFeatures) { + const feature = combinedFeatures.features[0] + + this.setDrawData(feature.geometry) + } }); } @@ -308,16 +323,17 @@ PolygonWidget.prototype.clearDraw = function () { } -PolygonWidget.prototype.setDrawData = function (feature) { - if (feature) { - const bbox = turf.bbox(feature) +PolygonWidget.prototype.setDrawData = function (geometry) { + if (geometry) { + const bbox = turf.bbox(geometry) const bounds = [[bbox[0], bbox[1]], [bbox[2], bbox[3]]] - this.setSourceData(feature) + this.setSourceData(geometry) this.map.setLayoutProperty("polygon", "visibility", "visible") this.map.fitBounds(bounds, {padding: 50}) - const geomString = JSON.stringify(feature) + const geomString = JSON.stringify(geometry) + this.setState(geomString) } else { diff --git a/capeditor/templates/capeditor/cap_alert_page.html b/capeditor/templates/capeditor/cap_alert_page.html index 0f9d6b1..1ba7684 100644 --- a/capeditor/templates/capeditor/cap_alert_page.html +++ b/capeditor/templates/capeditor/cap_alert_page.html @@ -314,11 +314,24 @@

'type': 'fill', 'source': 'polygon', 'layout': {}, - 'paint': { - 'fill-color': "red", - 'fill-opacity': 0.8, + 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", - } + }, }); // fit to bounds diff --git a/capeditor/templates/capeditor/load_cap_alert.html b/capeditor/templates/capeditor/load_cap_alert.html new file mode 100644 index 0000000..5e9cb01 --- /dev/null +++ b/capeditor/templates/capeditor/load_cap_alert.html @@ -0,0 +1,113 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% load l10n %} +{% load wagtailadmin_tags wagtailimages_tags static %} +{% block titletag %} + {% blocktranslate trimmed with title=page.get_admin_display_title %} + Import CAP Alert {{ title }} + {% endblocktranslate %} +{% endblock %} +{% block extra_css %} + {{ block.super }} +{% endblock %} + +{% block content %} + {% translate "Import CAP Alert" as header_str %} + + {% include "wagtailadmin/shared/header.html" with title=header_str icon="upload" %} + +
+
+ {% if form.non_field_errors %} +
+ {% include "wagtailadmin/shared/non_field_errors.html" with form=form %} +
+ {% endif %} + {% csrf_token %} + {% for field in form %} + {% if field.name != "text" and field.name != "load_from" %} + {% include "wagtailadmin/shared/field.html" with classname="w-hidden" %} + {% else %} + {% include "wagtailadmin/shared/field.html" %} + {% endif %} + {% endfor %} + + +
+
+{% endblock %} + +{% block extra_js %} + {{ block.super }} + + + + + + +{% endblock %} \ No newline at end of file diff --git a/capeditor/templates/capeditor/preview_cap_alert.html b/capeditor/templates/capeditor/preview_cap_alert.html new file mode 100644 index 0000000..2849969 --- /dev/null +++ b/capeditor/templates/capeditor/preview_cap_alert.html @@ -0,0 +1,430 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% load l10n %} +{% load wagtailadmin_tags wagtailimages_tags static %} +{% block titletag %} + {% blocktranslate trimmed with title=page.get_admin_display_title %} + Import CAP Alert {{ title }} + {% endblocktranslate %} +{% endblock %} +{% block extra_css %} + {{ block.super }} + + +{% endblock %} + +{% block content %} + {% translate "Import CAP Alert - Preview" as header_str %} + {% include "wagtailadmin/shared/header.html" with title=header_str icon="map" %} + +
+ {% if alert_data %} +
+
+ + {% translate "Import Type:" %} + + + {{ alert_data.alert_source.type }} + +
+
+ {% if alert_data.alert_source.type == "URL" %} + + {% translate "Source URL:" %} + + + {{ alert_data.alert_source.name }} + + {% else %} + {% if alert_data.alert_source.name %} + + {% translate "Source File Name:" %} + + + {{ alert_data.alert_source.name }} + + {% endif %} + {% endif %} +
+ + +
+ +
+
+ + {% translate "Sender:" %} + + + {{ alert_data.sender }} + +
+
+ + {% translate "Sent:" %} + + + {{ alert_data.sent }} UTC + +
+
+ + {% translate "Status:" %} + + + {{ alert_data.status }} + +
+
+ + {% translate "Message Type:" %} + + + {{ alert_data.scope }} + +
+
+
+ {% csrf_token %} + {% for field in form %} + {% if field.is_hidden %} + {{ field }} + {% endif %} + {% endfor %} + +
+
+
+
+
+
+
+
+
+
+
+ {% for info in alert_data.info %} + + {{ info.event }} + + {% endfor %} +
+
+
+ {% for info in alert_data.info %} +
+
+
+ + {% translate "Language:" %} + + + {{ info.language }} + +
+
+ + {% translate "Category:" %} + + + {% for category in info.category %} + {{ category }}{% if not forloop.last %}, {% endif %} + {% endfor %} + +
+
+ + {% translate "Event:" %} + + + {{ info.event }} + +
+
+ + {% translate "Urgency:" %} + + + {{ info.urgency }} + +
+
+ + {% translate "Severity:" %} + + + {{ info.severity }} + +
+
+ + {% translate "Certainty:" %} + + + {{ info.certainty }} + +
+ {% if info.effective or info.onset or info.expires %} +
+ {% if info.effective %} +
+ + {% translate "Effective:" %} + + + {{ info.effective }} UTC + +
+ {% endif %} + {% if info.onset %} +
+ + {% translate "Onset:" %} + + + {{ info.onset }} UTC + +
+ {% endif %} + {% if info.expires %} +
+ + {% translate "Expires:" %} + + + {{ info.expires }} UTC + +
+ {% endif %} +
+ {% endif %} + +
+ + {% translate "Headline:" %} + + + {{ info.headline }} + +
+
+ + {% translate "Description:" %} + + + {{ info.description }} + +
+ {% if info.instruction %} +
+ + {% translate "Instruction:" %} + + + {{ info.instruction }} + +
+ {% endif %} + +
+ + {% translate "Area:" %} + + + {% for area in info.area %} + {{ area.areaDesc }}{% if not forloop.last %}, {% endif %} + {% endfor %} + +
+
+
+ {% endfor %} +
+
+
+
+ {% endif %} +
+{% endblock %} + +{% block extra_js %} + {{ block.super }} + + + + + +{% endblock %} \ No newline at end of file diff --git a/capeditor/views.py b/capeditor/views.py new file mode 100644 index 0000000..7a8450e --- /dev/null +++ b/capeditor/views.py @@ -0,0 +1,51 @@ +from django.shortcuts import render, redirect +from wagtail import hooks + +from capeditor.forms.capimporter import CAPLoadForm, CAPImportForm + + +def load_cap_alert(request): + load_template_name = "capeditor/load_cap_alert.html" + preview_template_name = "capeditor/preview_cap_alert.html" + + context = {} + + if request.method == "POST": + form = CAPLoadForm(request.POST, request.FILES) + if form.is_valid(): + alert_data = form.cleaned_data["alert_data"] + form = CAPImportForm(initial={"alert_data": alert_data}) + context.update({ + "alert_data": alert_data, + "form": form, + }) + + return render(request, preview_template_name, context) + + else: + context.update({"form": form}) + return render(request, template_name=load_template_name, context=context) + + form = CAPLoadForm() + context.update({"form": form}) + + return render(request, load_template_name, context) + + +def import_cap_alert(request): + if request.method == "POST": + form = CAPImportForm(request.POST) + if form.is_valid(): + alert_data = form.cleaned_data["alert_data"] + + # run hook to import alert + for fn in hooks.get_hooks("before_import_cap_alert"): + result = fn(request, alert_data) + if hasattr(result, "status_code"): + return result + + return redirect("load_cap_alert") + else: + return redirect("load_cap_alert") + + return redirect("load_cap_alert") diff --git a/capeditor/wagtail_hooks.py b/capeditor/wagtail_hooks.py index f50c487..ad77eb7 100644 --- a/capeditor/wagtail_hooks.py +++ b/capeditor/wagtail_hooks.py @@ -1,6 +1,7 @@ from django.shortcuts import redirect from django.template.response import TemplateResponse from django.templatetags.static import static +from django.urls import path from django.utils.html import format_html from django.utils.translation import gettext as _ from wagtail import hooks @@ -10,6 +11,8 @@ from wagtail.admin.utils import get_valid_next_url_from_request from wagtail.models import Page +from capeditor.views import load_cap_alert, import_cap_alert + @hooks.register("insert_editor_js") def insert_editor_js(): @@ -26,6 +29,14 @@ def register_icons(icons): ] +@hooks.register('register_admin_urls') +def urlconf_stations(): + return [ + path('import-cap/', load_cap_alert, name='load_cap_alert'), + path('import-cap/import/', import_cap_alert, name='import_cap_alert'), + ] + + # @hooks.register("before_copy_page") def copy_cap_alert_page(request, page): if page.specific.__class__.__name__ == "CapAlertPage": diff --git a/sandbox/home/migrations/0020_alter_capalertpage_info.py b/sandbox/home/migrations/0020_alter_capalertpage_info.py new file mode 100644 index 0000000..021e9ec --- /dev/null +++ b/sandbox/home/migrations/0020_alter_capalertpage_info.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.11 on 2024-04-05 13:26 + +import capeditor.blocks +from django.db import migrations +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +import wagtailmodelchooser.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0019_capalertpage_search_image_alter_capalertpage_info'), + ] + + operations = [ + migrations.AlterField( + model_name='capalertpage', + name='info', + field=wagtail.fields.StreamField([('alert_info', wagtail.blocks.StructBlock([('event', wagtail.blocks.ChoiceBlock(choices=capeditor.blocks.get_hazard_types, help_text='The text denoting the type of the subject event of the alert message. You can define hazards events monitored by your institution from CAP settings', label='Event')), ('category', wagtail.blocks.MultipleChoiceBlock(choices=[('Geo', 'Geophysical'), ('Met', 'Meteorological'), ('Safety', 'General emergency and public safety'), ('Security', 'Law enforcement, military, homeland and local/private security'), ('Rescue', 'Rescue and recovery'), ('Fire', 'Fire suppression and rescue'), ('Health', 'Medical and public health'), ('Env', 'Pollution and other environmental'), ('Transport', 'Public and private transportation'), ('Infra', 'Utility, telecommunication, other non-transport infrastructure'), ('CBRNE', 'Chemical, Biological, Radiological, Nuclear or High-Yield Explosive threat or attack'), ('Other', 'Other events')], help_text='The code denoting the category of the subject event of the alert message', label='Category')), ('language', wagtail.blocks.ChoiceBlock(choices=[('en', 'English'), ('fr', 'French')], help_text='The code denoting the language of the alert message', label='Language', required=False)), ('urgency', wagtail.blocks.ChoiceBlock(choices=[('Immediate', 'Immediate - Responsive action SHOULD be taken immediately'), ('Expected', 'Expected - Responsive action SHOULD be taken soon (within next hour)'), ('Future', 'Future - Responsive action SHOULD be taken in the near future'), ('Past', 'Past - Responsive action is no longer required'), ('Unknown', 'Unknown - Urgency not known')], help_text='The code denoting the urgency of the subject event of the alert message', label='Urgency')), ('severity', wagtail.blocks.ChoiceBlock(choices=[('Extreme', 'Extreme - Extraordinary threat to life or property'), ('Severe', 'Severe - Significant threat to life or property'), ('Moderate', 'Moderate - Possible threat to life or property'), ('Minor', 'Minor - Minimal to no known threat to life or property'), ('Unknown', 'Unknown - Severity unknown')], help_text='The code denoting the severity of the subject event of the alert message', label='Severity')), ('certainty', wagtail.blocks.ChoiceBlock(choices=[('Observed', 'Observed - Determined to have occurred or to be ongoing'), ('Likely', 'Likely - Likely (percentage > ~50%)'), ('Possible', 'Possible - Possible but not likely (percentage <= ~50%)'), ('Unlikely', 'Unlikely - Not expected to occur (percentage ~ 0)'), ('Unknown', 'Unknown - Certainty unknown')], help_text='The code denoting the certainty of the subject event of the alert message', label='Certainty')), ('headline', wagtail.blocks.CharBlock(help_text='The text headline of the alert message. Make it direct and actionable as possible while remaining short', label='Headline', max_length=160, required=False)), ('description', wagtail.blocks.TextBlock(help_text='The text describing the subject event of the alert message. An extended description of the hazard or event that occasioned this message', label='Description', required=True)), ('instruction', wagtail.blocks.TextBlock(help_text='The text describing the recommended action to be taken by recipients of the alert message', label='Instruction', required=False)), ('effective', wagtail.blocks.DateTimeBlock(help_text='The effective time of the information of the alert message. If not set, the sent date will be used', label='Effective', required=False)), ('onset', wagtail.blocks.DateTimeBlock(help_text='The expected time of the beginning of the subject event of the alert message', label='Onset', required=False)), ('expires', wagtail.blocks.DateTimeBlock(help_text='The expiry time of the information of the alert message. If not set, each recipient is free to set its own policy as to when the message is no longer in effect.', label='Expires', required=True)), ('responseType', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('response_type', wagtail.blocks.ChoiceBlock(choices=[('Shelter', 'Shelter - Take shelter in place or per instruction'), ('Evacuate', 'Evacuate - Relocate as instructed in the instruction'), ('Prepare', 'Prepare - Relocate as instructed in the instruction'), ('Execute', 'Execute - Execute a pre-planned activity identified in instruction'), ('Avoid', 'Avoid - Avoid the subject event as per the instruction'), ('Monitor', 'Monitor - Attend to information sources as described in instruction'), ('Assess', 'Assess - Evaluate the information in this message - DONT USE FOR PUBLIC ALERTS'), ('AllClear', 'All Clear - The subject event no longer poses a threat or concern and any follow on action is described in instruction'), ('None', 'No action recommended')], help_text='The code denoting the type of action recommended for the target audience', label='Response type'))], label='Response Type'), default=[], label='Response Types')), ('senderName', wagtail.blocks.CharBlock(help_text='The human-readable name of the agency or authority issuing this alert.', label='Sender name', max_length=255, required=False)), ('contact', wagtail.blocks.CharBlock(help_text='The text describing the contact for follow-up and confirmation of the alert message', label='Contact', max_length=255, required=False)), ('audience', wagtail.blocks.CharBlock(help_text='The text describing the intended audience of the alert message', label='Audience', max_length=255, required=False)), ('area', wagtail.blocks.StreamBlock([('boundary_block', wagtail.blocks.StructBlock([('areaDesc', wagtail.blocks.TextBlock(help_text='The text describing the affected area of the alert message', label='Affected areas / Regions')), ('admin_level', wagtail.blocks.ChoiceBlock(choices=[(0, 'Level 0'), (1, 'Level 1'), (2, 'Level 2'), (3, 'Level 3')], label='Administrative Level')), ('boundary', capeditor.blocks.BoundaryFieldBlock(help_text='The paired values of points defining a polygon that delineates the affected area of the alert message', label='Boundary')), ('altitude', wagtail.blocks.CharBlock(help_text='The specific or minimum altitude of the affected area of the alert message', label='Altitude', max_length=100, required=False)), ('ceiling', wagtail.blocks.CharBlock(help_text='The maximum altitude of the affected area of the alert message.MUST NOT be used except in combination with the altitude element. ', label='Ceiling', max_length=100, required=False))], label='Admin Boundary')), ('polygon_block', wagtail.blocks.StructBlock([('areaDesc', wagtail.blocks.TextBlock(help_text='The text describing the affected area of the alert message', label='Affected areas / Regions')), ('polygon', capeditor.blocks.PolygonOrMultiPolygonFieldBlock(help_text='The paired values of points defining a polygon that delineates the affected area of the alert message', label='Polygon')), ('altitude', wagtail.blocks.CharBlock(help_text='The specific or minimum altitude of the affected area of the alert message', label='Altitude', max_length=100, required=False)), ('ceiling', wagtail.blocks.CharBlock(help_text='The maximum altitude of the affected area of the alert message.MUST NOT be used except in combination with the altitude element. ', label='Ceiling', max_length=100, required=False))], label='Draw Polygon')), ('circle_block', wagtail.blocks.StructBlock([('areaDesc', wagtail.blocks.TextBlock(help_text='The text describing the affected area of the alert message', label='Affected areas / Regions')), ('circle', capeditor.blocks.CircleFieldBlock(help_text='Drag the marker to change position', label='Circle')), ('altitude', wagtail.blocks.CharBlock(help_text='The specific or minimum altitude of the affected area of the alert message', label='Altitude', max_length=100, required=False)), ('ceiling', wagtail.blocks.CharBlock(help_text='The maximum altitude of the affected area of the alert message.MUST NOT be used except in combination with the altitude element. ', label='Ceiling', max_length=100, required=False))], label='Circle')), ('geocode_block', wagtail.blocks.StructBlock([('areaDesc', wagtail.blocks.TextBlock(help_text='The text describing the affected area of the alert message', label='Affected areas / Regions')), ('valueName', wagtail.blocks.TextBlock(label='Name')), ('value', wagtail.blocks.TextBlock(label='Value')), ('altitude', wagtail.blocks.CharBlock(help_text='The specific or minimum altitude of the affected area of the alert message', label='Altitude', max_length=100, required=False)), ('ceiling', wagtail.blocks.CharBlock(help_text='The maximum altitude of the affected area of the alert message.MUST NOT be used except in combination with the altitude element. ', label='Ceiling', max_length=100, required=False))], label='Geocode')), ('predefined_block', wagtail.blocks.StructBlock([('area', wagtailmodelchooser.blocks.ModelChooserBlock(label='Area', target_model='capeditor.predefinedalertarea')), ('altitude', wagtail.blocks.CharBlock(help_text='The specific or minimum altitude of the affected area of the alert message', label='Altitude', max_length=100, required=False)), ('ceiling', wagtail.blocks.CharBlock(help_text='The maximum altitude of the affected area of the alert message.MUST NOT be used except in combination with the altitude element. ', label='Ceiling', max_length=100, required=False))], label='Predefined Area'))], help_text='Admin Boundary, Polygon, Circle or Geocode', label='Alert Area')), ('resource', wagtail.blocks.StreamBlock([('file_resource', wagtail.blocks.StructBlock([('resourceDesc', wagtail.blocks.TextBlock(help_text='The text describing the type and content of the resource file', label='Resource Description')), ('file', wagtail.documents.blocks.DocumentChooserBlock())])), ('external_resource', wagtail.blocks.StructBlock([('resourceDesc', wagtail.blocks.TextBlock(help_text='The text describing the type and content of the resource file', label='Resource Description')), ('external_url', wagtail.blocks.URLBlock(help_text='Link to external resource. This can be for example a link to related websites. ', verbose_name='External Resource Link'))]))], help_text='Additional file with supplemental information related to this alert information', label='Resources', required=False)), ('parameter', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('valueName', wagtail.blocks.TextBlock(label='Name')), ('value', wagtail.blocks.TextBlock(label='Value'))], label='Parameter'), default=[], label='Parameters')), ('eventCode', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('valueName', wagtail.blocks.TextBlock(help_text='Name for the event code', label='Name')), ('value', wagtail.blocks.TextBlock(help_text='Value of the event code', label='Value'))], label='Event Code'), default=[], label='Event codes'))], label='Alert Information'))], blank=True, null=True, use_json_field=True, verbose_name='Alert Information'), + ), + ] diff --git a/sandbox/home/models.py b/sandbox/home/models.py index 37072f2..b130e92 100644 --- a/sandbox/home/models.py +++ b/sandbox/home/models.py @@ -4,7 +4,6 @@ from django.db import models from django.urls import reverse from django.utils.functional import cached_property -from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _, gettext_lazy from wagtail.admin.panels import MultiFieldPanel, FieldPanel from wagtail.admin.widgets.slug import SlugInput @@ -99,24 +98,30 @@ def xml_link(self): def on_publish_cap_alert(sender, **kwargs): instance = kwargs['instance'] - # publish to mqtt - topic = "cap/alerts/all" - publish_cap_mqtt_message(instance, topic) - - # create summary image - image_content_file = instance.generate_alert_card_image() - if image_content_file: - - # delete old image - if instance.search_image: - instance.search_image.delete() - - # create new image - instance.search_image = Image(title=instance.title, file=image_content_file) - instance.search_image.save() - - # save the instance - instance.save() + try: + # publish to mqtt + topic = "cap/alerts/all" + publish_cap_mqtt_message(instance, topic) + except Exception as e: + pass + + try: + # create summary image + image_content_file = instance.generate_alert_card_image() + if image_content_file: + + # delete old image + if instance.search_image: + instance.search_image.delete() + + # create new image + instance.search_image = Image(title=instance.title, file=image_content_file) + instance.search_image.save() + + # save the instance + instance.save() + except Exception as e: + pass page_published.connect(on_publish_cap_alert, sender=CapAlertPage) diff --git a/sandbox/home/wagtail_hooks.py b/sandbox/home/wagtail_hooks.py index 443352e..46495ab 100644 --- a/sandbox/home/wagtail_hooks.py +++ b/sandbox/home/wagtail_hooks.py @@ -1,6 +1,286 @@ +import json +from datetime import datetime + +import pytz from adminboundarymanager.wagtail_hooks import AdminBoundaryManagerAdminGroup +from django.shortcuts import redirect +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext as _ +from wagtail import hooks +from wagtail.admin import messages +from wagtail.admin.menu import Menu, MenuItem +from wagtail.blocks import StreamValue +from wagtail_modeladmin.menus import GroupMenuItem from wagtail_modeladmin.options import ( - modeladmin_register + modeladmin_register, ModelAdmin, ModelAdminGroup ) +from capeditor.cap_settings import CapSetting, get_cap_contact_list, get_cap_audience_list +from .models import CapAlertPage, HomePage + modeladmin_register(AdminBoundaryManagerAdminGroup) + + +class CAPAdmin(ModelAdmin): + model = CapAlertPage + menu_label = _('Alerts') + menu_icon = 'list-ul' + menu_order = 200 + add_to_settings_menu = False + exclude_from_explorer = False + + +class CAPMenuGroupAdminMenuItem(GroupMenuItem): + def is_shown(self, request): + return request.user.has_perm("base.can_view_alerts_menu") + + +class CAPMenuGroup(ModelAdminGroup): + menu_label = _('CAP Alerts') + menu_icon = 'warning' # change as required + menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd) + items = (CAPAdmin,) + + def get_menu_item(self, order=None): + if self.modeladmin_instances: + submenu = Menu(items=self.get_submenu_items()) + return CAPMenuGroupAdminMenuItem(self, self.get_menu_order(), submenu) + + def get_submenu_items(self): + menu_items = [] + item_order = 1 + + for modeladmin in self.modeladmin_instances: + menu_items.append(modeladmin.get_menu_item(order=item_order)) + item_order += 1 + + try: + + # add CAP import menu + settings_url = reverse("load_cap_alert") + import_cap_menu = MenuItem(label=_("Import CAP Alert"), url=settings_url, icon_name="upload") + menu_items.append(import_cap_menu) + + # add settings menu + settings_url = reverse( + "wagtailsettings:edit", + args=[CapSetting._meta.app_label, CapSetting._meta.model_name, ], + ) + gm_settings_menu = MenuItem(label=_("CAP Base Settings"), url=settings_url, icon_name="cog") + menu_items.append(gm_settings_menu) + + except Exception: + pass + + return menu_items + + +modeladmin_register(CAPMenuGroup) + + +@hooks.register('construct_settings_menu') +def hide_settings_menu_item(request, menu_items): + hidden_settings = ["cap-settings"] + menu_items[:] = [item for item in menu_items if item.name not in hidden_settings] + + +@hooks.register("before_import_cap_alert") +def import_cap_alert(request, alert_data): + cap_settings = CapSetting.for_request(request) + hazard_event_types = cap_settings.hazard_event_types.all() + + base_data = {} + + # an alert page requires a title + # here we use the headline of the first info block + title = None + + if "sender" in alert_data: + base_data["sender"] = alert_data["sender"] + if "sent" in alert_data: + sent = alert_data["sent"] + # convert dates to local timezone + sent = datetime.fromisoformat(sent).astimezone(pytz.utc) + sent_local = sent.astimezone(timezone.get_current_timezone()) + base_data["sent"] = sent_local + if "status" in alert_data: + base_data["status"] = alert_data["status"] + if "msgType" in alert_data: + base_data["msgType"] = alert_data["msgType"] + if "scope" in alert_data: + base_data["scope"] = alert_data["scope"] + if "restriction" in alert_data: + base_data["restriction"] = alert_data["restriction"] + if "note" in alert_data: + base_data["note"] = alert_data["note"] + + info_blocks = [] + + if "info" in alert_data: + for info in alert_data.get("info"): + info_base_data = {} + + if "language" in info: + info_base_data["language"] = info["language"] + if "category" in info: + info_base_data["category"] = info["category"] + if "event" in info: + event = info["event"] + + existing_hazard_event_type = hazard_event_types.filter(event__iexact=event).first() + if existing_hazard_event_type: + info_base_data["event"] = existing_hazard_event_type.event + else: + hazard_event_types.create(setting=cap_settings, is_in_wmo_event_types_list=False, event=event, + icon="warning") + info_base_data["event"] = event + + if "responseType" in info: + response_types = info["responseType"] + response_type_data = [] + for response_type in response_types: + response_type_data.append({"response_type": response_type}) + info_base_data["responseType"] = response_type_data + + if "urgency" in info: + info_base_data["urgency"] = info["urgency"] + if "severity" in info: + info_base_data["severity"] = info["severity"] + if "certainty" in info: + info_base_data["certainty"] = info["certainty"] + if "eventCode" in info: + event_codes = info["eventCode"] + event_code_data = [] + for event_code in event_codes: + event_code_data.append({"valueName": event_code["valueName"], "value": event_code["value"]}) + info_base_data["eventCode"] = event_code_data + if "effective" in info: + effective = info["effective"] + effective = datetime.fromisoformat(effective).astimezone(pytz.utc) + effective_local = effective.astimezone(timezone.get_current_timezone()) + info_base_data["effective"] = effective_local + if "onset" in info: + onset = info["onset"] + onset = datetime.fromisoformat(onset).astimezone(pytz.utc) + onset_local = onset.astimezone(timezone.get_current_timezone()) + info_base_data["onset"] = onset_local + if "expires" in info: + expires = info["expires"] + expires = datetime.fromisoformat(expires).astimezone(pytz.utc) + expires_local = expires.astimezone(timezone.get_current_timezone()) + info_base_data["expires"] = expires_local + if "senderName" in info: + info_base_data["senderName"] = info["senderName"] + if "headline" in info: + info_base_data["headline"] = info["headline"] + if not title: + title = info["headline"] + + if "description" in info: + info_base_data["description"] = info["description"] + if "instruction" in info: + info_base_data["instruction"] = info["instruction"] + if "contact" in info: + contact = info["contact"] + contact_list = get_cap_contact_list(request) + if contact not in contact_list: + cap_settings.contacts.append(("contact", {"contact": contact})) + cap_settings.save() + info_base_data["contact"] = contact + if "audience" in info: + audience = info["audience"] + audience_list = get_cap_audience_list(request) + if audience not in audience_list: + cap_settings.audience_types.append(("audience_type", {"audience": audience})) + cap_settings.save() + info_base_data["audience"] = audience + + if "parameter" in info: + parameters = info["parameter"] + parameter_data = [] + for parameter in parameters: + parameter_data.append({"valueName": parameter["valueName"], "value": parameter["value"]}) + info_base_data["parameter"] = parameter_data + if "resource" in info: + resources = info["resource"] + resource_data = [] + for resource in resources: + if resource.get("uri") and resource.get("resourceDesc"): + resource_data.append({ + "type": "external_resource", + "value": { + "external_url": resource["uri"], + "resourceDesc": resource["resourceDesc"] + } + }) + info_base_data["resource"] = resource_data + + areas_data = [] + if "area" in info: + for area in info.get("area"): + area_data = {} + areaDesc = area.get("areaDesc") + + if "geocode" in area: + area_data["type"] = "geocode_block" + geocode = area.get("geocode") + geocode_data = { + "areaDesc": areaDesc, + } + if "valueName" in geocode: + geocode_data["valueName"] = geocode["valueName"] + if "value" in geocode: + geocode_data["value"] = geocode["value"] + + area_data["value"] = geocode_data + + if "polygon" in area: + area_data["type"] = "polygon_block" + polygon_data = { + "areaDesc": areaDesc, + } + geometry = area.get("geometry") + polygon_data["polygon"] = json.dumps(geometry) + + area_data["value"] = polygon_data + + if "circle" in area: + area_data["type"] = "circle_block" + circle_data = { + "areaDesc": areaDesc, + } + circle = area.get("circle") + # take the first circle for now + # TODO: handle multiple circles ? Investigate use case + circle_data["circle"] = circle[0] + area_data["value"] = circle_data + + areas_data.append(area_data) + + stream_item = { + "type": "alert_info", + "value": { + **info_base_data, + "area": areas_data, + }, + } + + info_blocks.append(stream_item) + + if title: + base_data["title"] = title + new_cap_alert_page = CapAlertPage(**base_data, live=False) + new_cap_alert_page.info = StreamValue(new_cap_alert_page.info.stream_block, info_blocks, is_lazy=True) + + cap_list_page = HomePage.objects.live().first() + + if cap_list_page: + cap_list_page.add_child(instance=new_cap_alert_page) + cap_list_page.save_revision() + + messages.success(request, _("CAP Alert draft created. You can now edit the alert.")) + + return redirect(reverse("wagtailadmin_pages:edit", args=[new_cap_alert_page.id])) + + return None diff --git a/sandbox/sandbox/settings/base.py b/sandbox/sandbox/settings/base.py index 1ac722e..ea52830 100644 --- a/sandbox/sandbox/settings/base.py +++ b/sandbox/sandbox/settings/base.py @@ -166,7 +166,7 @@ ('sw', 'Swahili'), ] -TIME_ZONE = "UTC" +TIME_ZONE = "Africa/Nairobi" USE_I18N = True @@ -223,3 +223,5 @@ } CAP_BROKER_URI = env.str("CAP_BROKER_URI", default="") + +DATA_UPLOAD_MAX_MEMORY_SIZE = env.int("DATA_UPLOAD_MAX_MEMORY_SIZE", default=26214400) # 25MB diff --git a/setup.cfg b/setup.cfg index c0404eb..5af6410 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = capeditor -version = 0.5.1 +version = 0.5.2 description = Wagtail based CAP composer long_description = file:README.md long_description_content_type = text/markdown @@ -38,5 +38,6 @@ install_requires = geopandas matplotlib cartopy + xmltodict