From 0e8c78372f0d78c0ff805eaefd2a151b7b1079ee Mon Sep 17 00:00:00 2001 From: Erick Otenyo Date: Fri, 15 Mar 2024 11:15:08 +0300 Subject: [PATCH 1/3] Updates for integration --- sandbox/home/views.py | 55 -------------------------------- sandbox/sandbox/settings/base.py | 2 ++ sandbox/sandbox/urls.py | 14 +++----- 3 files changed, 6 insertions(+), 65 deletions(-) diff --git a/sandbox/home/views.py b/sandbox/home/views.py index 387db27..e69de29 100644 --- a/sandbox/home/views.py +++ b/sandbox/home/views.py @@ -1,55 +0,0 @@ -from itertools import groupby - -from datetime import datetime, timedelta -from django.shortcuts import render - -from forecastmanager.models import Forecast, DailyWeather, City - -def list_forecasts(request): - - start_date_param = datetime.today() - end_date_param = start_date_param + timedelta(days=6) - forecast_data = Forecast.objects.filter(forecast_date__gte=start_date_param.date(), forecast_date__lte=end_date_param.date(),effective_period__whole_day = True )\ - .order_by('forecast_date')\ - .values('id','city__name','forecast_date', 'data_value', 'condition', 'effective_period', 'effective_period__whole_day', 'effective_period__forecast_effective_time') - - # sort the data by city - data_sorted = sorted(forecast_data, key=lambda x: x['forecast_date']) - # group the data by city - grouped_forecast = {} - for dates, group in groupby(data_sorted, lambda x: x['forecast_date']): - dates_data = {'dates':dates, 'forecast_items': list(group)} - - # for item in sorted(city_data['forecast_items'], key=lambda x: x['forecast_date']): - # date_obj = datetime.strptime( item['forecast_date'], '%Y-%m-%d').date() - # item['forecast_date'] =item['forecast_date'] - - grouped_forecast[dates_data['dates']] = dates_data['forecast_items'] - - cities = list(set([d['city__name'] for d in data_sorted])) - dates = list(set([d['forecast_date'] for d in data_sorted])) - - return render(request, "integration/forecasts_include.html", { - "forecasts":grouped_forecast, - "cities":cities, - "dates":sorted(dates) - }) - -def daily_weather(request): - - report = DailyWeather.objects.all().order_by('issued_on').first() - - return render(request, "integration/dailyweather_include.html", { - "report":report - }) - - -def city_analysis(request, city_id): - - city_name = City.objects.get(pk=city_id).name - context = { - 'city_name':city_name, - 'city_id':city_id - } - - return render(request, "integration/city_analysis.html", context) \ No newline at end of file diff --git a/sandbox/sandbox/settings/base.py b/sandbox/sandbox/settings/base.py index a062c7f..dbfa18a 100644 --- a/sandbox/sandbox/settings/base.py +++ b/sandbox/sandbox/settings/base.py @@ -58,6 +58,8 @@ "wagtail_modeladmin", "wagtailfontawesomesvg", "wagtail.contrib.routable_page", + 'django_extensions', + ] MIDDLEWARE = [ diff --git a/sandbox/sandbox/urls.py b/sandbox/sandbox/urls.py index f505e52..88a5bb9 100644 --- a/sandbox/sandbox/urls.py +++ b/sandbox/sandbox/urls.py @@ -1,26 +1,20 @@ from django.conf import settings -from django.urls import include, path from django.contrib import admin - -from wagtail.admin import urls as wagtailadmin_urls +from django.urls import include, path +from search import views as search_views from wagtail import urls as wagtail_urls +from wagtail.admin import urls as wagtailadmin_urls from wagtail.documents import urls as wagtaildocs_urls -from home.views import list_forecasts, daily_weather,city_analysis -from forecastmanager import urls as forecastmanager_urls -from search import views as search_views +from forecastmanager import urls as forecastmanager_urls urlpatterns = [ path("django-admin/", admin.site.urls), path("admin/", include(wagtailadmin_urls)), path("documents/", include(wagtaildocs_urls)), path("search/", search_views.search, name="search"), - path("forecasts/", list_forecasts, name="list_forecasts"), - path("dailyweather/", daily_weather, name="daily_weather"), - path("city_analysis//", city_analysis, name="city_analysis"), path("", include(forecastmanager_urls)), - ] if settings.DEBUG: From f11d2f07b672f819f9630625acba49b5de0cfdf3 Mon Sep 17 00:00:00 2001 From: Erick Otenyo Date: Fri, 15 Mar 2024 11:15:17 +0300 Subject: [PATCH 2/3] Updates for integration --- forecastmanager/constants.py | 7 ++ forecastmanager/forecast_settings.py | 6 ++ forecastmanager/forms.py | 6 +- .../management/commands/generate_forecast.py | 1 - .../migrations/0018_delete_dailyweather.py | 16 +++ forecastmanager/migrations/0019_city_slug.py | 33 +++++++ forecastmanager/models.py | 45 ++------- forecastmanager/serializers.py | 6 +- .../forecastmanager/edit_forecast.html | 99 +++++++++++++++++++ forecastmanager/urls.py | 10 +- forecastmanager/views.py | 28 ++++++ forecastmanager/wagtail_hooks.py | 49 ++++----- forecastmanager/widgets.py | 13 +-- setup.cfg | 2 + 14 files changed, 242 insertions(+), 79 deletions(-) create mode 100644 forecastmanager/migrations/0018_delete_dailyweather.py create mode 100644 forecastmanager/migrations/0019_city_slug.py create mode 100644 forecastmanager/templates/forecastmanager/edit_forecast.html diff --git a/forecastmanager/constants.py b/forecastmanager/constants.py index 094e97b..0df85da 100644 --- a/forecastmanager/constants.py +++ b/forecastmanager/constants.py @@ -1,3 +1,4 @@ +from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ # extracted from https://nrkno.github.io/yr-weather-symbols/ @@ -183,3 +184,9 @@ WEATHER_CONDITION_CHOICES = [(condition['id'], _(condition['name'])) for condition in WEATHER_CONDITIONS] WEATHER_CONDITIONS_AS_DICT = {condition['id']: condition for condition in WEATHER_CONDITIONS} + +WEATHER_CONDITION_ICONS = [ + {"value": condition["id"], **condition, + 'icon_url': static("forecastmanager/weathericons/{0}.png".format(condition["id"]))} for condition in + WEATHER_CONDITIONS +] diff --git a/forecastmanager/forecast_settings.py b/forecastmanager/forecast_settings.py index 8f5f707..5d9b853 100644 --- a/forecastmanager/forecast_settings.py +++ b/forecastmanager/forecast_settings.py @@ -56,6 +56,12 @@ def data_parameter_values(self): def periods_as_choices(self): return [(period.id, period.label) for period in self.periods.all()] + @property + def effective_periods(self): + return [ + {"label": period.label, "time": period.forecast_effective_time, "default": period.default} + for period in self.periods.all()] + @property def weather_conditions_list(self): weather_conditions = self.weather_conditions.all() diff --git a/forecastmanager/forms.py b/forecastmanager/forms.py index 4c2459b..f676f0b 100644 --- a/forecastmanager/forms.py +++ b/forecastmanager/forms.py @@ -9,7 +9,7 @@ ) -class ForecastForm(WagtailAdminModelForm): +class ForecastCreateForm(WagtailAdminModelForm): data = forms.JSONField(widget=forms.HiddenInput) replace_existing = forms.BooleanField(required=False, initial=True, label=_("Replace existing data if found")) @@ -179,3 +179,7 @@ def clean(self): cleaned_data["data"] = cities return cleaned_data + + +class ForecastEditForm(WagtailAdminModelForm): + pass diff --git a/forecastmanager/management/commands/generate_forecast.py b/forecastmanager/management/commands/generate_forecast.py index 319cd02..026f2a8 100644 --- a/forecastmanager/management/commands/generate_forecast.py +++ b/forecastmanager/management/commands/generate_forecast.py @@ -129,7 +129,6 @@ def handle(self, *args, **options): city_forecast = CityForecast(city=city, condition=condition_obj) for key, value in data_values.get("parameters", {}).items(): if parameters_dict.get(key) is None: - logger.warning(f"parameter: {key} not mapped set in Forecast Settings") continue parameter = parameters_dict[key] diff --git a/forecastmanager/migrations/0018_delete_dailyweather.py b/forecastmanager/migrations/0018_delete_dailyweather.py new file mode 100644 index 0000000..e1253e2 --- /dev/null +++ b/forecastmanager/migrations/0018_delete_dailyweather.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.3 on 2024-03-15 06:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('forecastmanager', '0017_forecastsetting_weather_detail_page'), + ] + + operations = [ + migrations.DeleteModel( + name='DailyWeather', + ), + ] diff --git a/forecastmanager/migrations/0019_city_slug.py b/forecastmanager/migrations/0019_city_slug.py new file mode 100644 index 0000000..ea530a3 --- /dev/null +++ b/forecastmanager/migrations/0019_city_slug.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2024-03-15 07:48 + +import django_extensions.db.fields +from django.db import migrations + +from forecastmanager.models import City + + +def migrate_data_forward(apps, schema_editor): + for instance in City.objects.all(): + print(f"Generating slug for {instance}") + instance.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('forecastmanager', '0018_delete_dailyweather'), + ] + + operations = [ + migrations.AddField( + model_name='city', + name='slug', + field=django_extensions.db.fields.AutoSlugField(blank=True, default=None, editable=False, null=True, + populate_from='name', unique=True), + preserve_default=False + ), + migrations.RunPython( + migrate_data_forward, + migrations.RunPython.noop + ), + + ] diff --git a/forecastmanager/models.py b/forecastmanager/models.py index 64c3031..16c96bb 100644 --- a/forecastmanager/models.py +++ b/forecastmanager/models.py @@ -2,23 +2,22 @@ from django.contrib.gis.db import models from django.utils.translation import gettext_lazy as _ +from django_extensions.db.fields import AutoSlugField from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel -from wagtail.admin.panels import FieldPanel, MultiFieldPanel, InlinePanel +from wagtail.admin.panels import FieldPanel, InlinePanel from wagtail.api.v2.utils import get_full_url -from wagtail.fields import RichTextField, StreamField from wagtail.models import Orderable from wagtailgeowidget import geocoders from wagtailgeowidget.helpers import geosgeometry_str_to_struct from wagtailgeowidget.panels import LeafletPanel, GeoAddressPanel -from .blocks import ExtremeBlock from .forecast_settings import ( ForecastPeriod, WeatherCondition, ForecastDataParameters ) -from .forms import ForecastForm +from .forms import ForecastCreateForm class City(models.Model): @@ -29,6 +28,7 @@ class City(models.Model): help_text=_("Unique UUID. Auto generated on creation."), ) name = models.CharField(verbose_name=_("City Name"), max_length=255, null=True, blank=False, unique=True) + slug = AutoSlugField(populate_from='name', null=True, unique=True, default=None, editable=False) location = models.PointField(verbose_name=_("City Location (Lat, Lng)")) panels = [ GeoAddressPanel("name", geocoder=geocoders.NOMINATIM), @@ -43,10 +43,6 @@ class Meta: def __str__(self): return self.name - @property - def clean_name(self): - return self.name.replace(" ", "--") - @property def coordinates(self): location = geosgeometry_str_to_struct(str(self.location)) @@ -62,7 +58,7 @@ def y(self): class Forecast(ClusterableModel): - base_form_class = ForecastForm + base_form_class = ForecastCreateForm FORECAST_SOURCE_CHOICES = [ ("local", _("NMHSs Forecast")), @@ -95,6 +91,7 @@ def get_geojson(self, request=None): features.append(city_forecast.get_geojson_feature(request)) return { "type": "FeatureCollection", + "date": self.forecast_date, "features": features, } @@ -193,33 +190,3 @@ def parsed_value(self): @property def value_with_units(self): return f"{self.parsed_value}{self.parameter.parameter_info.get('unit')}" - - -class DailyWeather(models.Model): - issued_on = models.DateField(auto_now_add=True, null=True) - forecast_date = models.DateField(_("Forecast Date"), auto_now=False, auto_now_add=False) - forecast_desc = RichTextField(verbose_name=_('Weather Forecast Description')) - summary_date = models.DateField(_("Summary Date"), auto_now=False, auto_now_add=False) - summary_desc = RichTextField(verbose_name=_('Weather Summary Description')) - extreme_date = models.DateField(_("Extreme Date"), auto_now=False, auto_now_add=False, null=True, blank=True) - extremes = StreamField([ - ('extremes', ExtremeBlock()) - ], use_json_field=True) - - panels = [ - MultiFieldPanel([ - FieldPanel('summary_date'), - FieldPanel('summary_desc'), - ], heading="Weather Summary"), - MultiFieldPanel([ - FieldPanel('forecast_date'), - FieldPanel('forecast_desc'), - ], heading="Weather Forecast"), - MultiFieldPanel([ - FieldPanel('extreme_date'), - FieldPanel('extremes') - ], heading="Extremes") - ] - - def __str__(self): - return f'Daily Weather - Issued on {self.issued_on.strftime("%Y-%m-%d")}' diff --git a/forecastmanager/serializers.py b/forecastmanager/serializers.py index a7fa8d8..e15d3b5 100644 --- a/forecastmanager/serializers.py +++ b/forecastmanager/serializers.py @@ -5,18 +5,14 @@ class CitySerializer(serializers.ModelSerializer): coordinates = serializers.SerializerMethodField() - clean_name = serializers.SerializerMethodField() class Meta: model = City - fields = ('id', 'name', 'coordinates', "clean_name") + fields = ('id', 'name', 'coordinates', "slug") def get_coordinates(self, obj): return obj.coordinates - def get_clean_name(self, obj): - return obj.clean_name - class ForecastSerializer(serializers.ModelSerializer): class Meta: diff --git a/forecastmanager/templates/forecastmanager/edit_forecast.html b/forecastmanager/templates/forecastmanager/edit_forecast.html new file mode 100644 index 0000000..cfe6d96 --- /dev/null +++ b/forecastmanager/templates/forecastmanager/edit_forecast.html @@ -0,0 +1,99 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n wagtailadmin_tags static i18n %} +{% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}View {{ snippet_type_name }} +{% endblocktrans %}{% endblock %} +{% block content %} + {% include 'wagtailadmin/shared/headers/slim_header.html' %} + + {% trans "View" as new_str %} + {% include "wagtailadmin/shared/header.html" with title=new_str subtitle=model_opts.verbose_name icon=header_icon merged=1 only %} + + +
+
+ + +

+ {{ object.forecast_date|date:"l j F" }} - {{ object.effective_period.label }} +

+ + +
+ + +
+ +
+ + + + + + {% for param in weather_parameters %} + + {% endfor %} + + + + {% for city_forecast in object.city_forecasts.all %} + + + + {% for param in weather_parameters %} + {% for data in city_forecast.data_values.all %} + {% if data.parameter.parameter == param.parameter %} + + {% endif %} + {% endfor %} + {% endfor %} + + {% endfor %} + +
{% trans "City" %} {% trans "Condition" %} {{ param.name }}
+

{{ city_forecast.city.name }}

+
+ + {{ data.value_with_units }}
+
+
+
+ + + +{% endblock %} + +{% block extra_css %} + {{ block.super }} + {{ media.css }} + + + +{% endblock %} +{% block extra_js %} + {{ block.super }} + {% include "wagtailadmin/pages/_editor_js.html" %} + {{ media.js }} + + + + +{% endblock %} diff --git a/forecastmanager/urls.py b/forecastmanager/urls.py index b0b806e..1e313ca 100644 --- a/forecastmanager/urls.py +++ b/forecastmanager/urls.py @@ -1,9 +1,17 @@ from django.urls import path -from .views import CityListView, ForecastListView, download_forecast_template +from .views import ( + CityListView, + ForecastListView, + download_forecast_template, + weather_icons, + forecast_settings +) urlpatterns = [ path('api/cities', CityListView.as_view(), name='cities-list'), path('api/forecasts', ForecastListView.as_view(), name='forecast-list'), + path('api/forecast-settings', forecast_settings, name='forecast-settings'), + path('api/weather-icons', weather_icons, name='weather-icons'), path('api/forecast_templace.csv', download_forecast_template, name='download-forecast-template'), ] diff --git a/forecastmanager/views.py b/forecastmanager/views.py index 22825b3..0b17435 100644 --- a/forecastmanager/views.py +++ b/forecastmanager/views.py @@ -3,13 +3,16 @@ from django.contrib.gis.geos import Point from django.http import HttpResponse +from django.http import JsonResponse from django.shortcuts import render, redirect from django.urls import reverse from django_filters.rest_framework import DjangoFilterBackend from rest_framework.generics import ListAPIView from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS +from wagtail.api.v2.utils import get_full_url from forecastmanager.models import City, Forecast +from .constants import WEATHER_CONDITION_ICONS from .forecast_settings import ForecastSetting from .forms import CityLoaderForm from .serializers import CitySerializer, ForecastSerializer @@ -46,6 +49,12 @@ def get_queryset(self): start_date = self.request.query_params.get('start_date') end_date = self.request.query_params.get('end_date') + effective_time = self.request.query_params.get('effective_time') + + if effective_time: + queryset = queryset.filter(effective_period__forecast_effective_time=effective_time) + else: + queryset = queryset.filter(effective_period__default=True) if start_date: queryset = queryset.filter(forecast_date__gte=start_date) @@ -129,3 +138,22 @@ def load_cities(request): context.update({"form": form}) return render(request, template_name=template, context=context) + + +def forecast_settings(request): + context = {} + + fm_settings = ForecastSetting.for_request(request) + data_parameters = fm_settings.data_parameter_values + effective_periods = fm_settings.effective_periods + + context.update({"parameters": data_parameters, "periods": effective_periods}) + + return JsonResponse(context) + + +def weather_icons(request): + options = WEATHER_CONDITION_ICONS + icons = [{"id": icon["id"], "name": icon["name"], "url": get_full_url(request, icon["icon_url"])} for icon in + options] + return JsonResponse(icons, safe=False) diff --git a/forecastmanager/wagtail_hooks.py b/forecastmanager/wagtail_hooks.py index ca99eee..663f18e 100644 --- a/forecastmanager/wagtail_hooks.py +++ b/forecastmanager/wagtail_hooks.py @@ -6,12 +6,12 @@ from wagtail.snippets.views.snippets import ( SnippetViewSet, SnippetViewSetGroup, - CreateView, + CreateView, EditView, ) from forecastmanager.forecast_settings import ForecastSetting -from forecastmanager.forms import ForecastForm -from forecastmanager.models import City, DailyWeather, Forecast +from forecastmanager.forms import ForecastCreateForm, ForecastEditForm +from forecastmanager.models import City, Forecast from forecastmanager.views import load_cities @@ -41,25 +41,9 @@ class CityViewSet(SnippetViewSet): icon = "globe" menu_label = _("Cities") - def get_queryset(self, request): - queryset = super().get_queryset(request) - - name = request.GET.get("name") - - print(name) - - return queryset - - -class DailyWeatherViewSet(SnippetViewSet): - model = DailyWeather - - icon = 'site' - menu_label = _('Daily Weather') - class ForecastCreateView(CreateView): - form_class = ForecastForm + form_class = ForecastCreateForm template_name = "forecastmanager/create_forecast.html" def get_context_data(self, **kwargs): @@ -86,21 +70,42 @@ def get_context_data(self, **kwargs): return context +class ForecastEditView(EditView): + form_class = ForecastEditForm + template_name = "forecastmanager/edit_forecast.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + fm_settings = ForecastSetting.for_request(self.request) + + weather_parameters = fm_settings.data_parameter_values + context.update({ + "weather_parameters": weather_parameters, + }) + + return context + + class ForecastViewSet(SnippetViewSet): model = Forecast + list_filter = ["forecast_date", "effective_period"] + add_view_class = ForecastCreateView + edit_view_class = ForecastEditView create_template_name = "forecastmanager/create_forecast.html" + edit_template_name = "forecastmanager/edit_forecast.html" icon = 'table' - menu_label = _('Daily Forecast') + menu_label = _('Forecasts') class ForecastViewSetGroup(SnippetViewSetGroup): - items = (CityViewSet, ForecastViewSet, DailyWeatherViewSet) + items = (CityViewSet, ForecastViewSet) menu_icon = "table" menu_label = _("City Forecast") menu_name = "city_forecast" + menu_order = 200 def get_submenu_items(self): menu_items = super().get_submenu_items() diff --git a/forecastmanager/widgets.py b/forecastmanager/widgets.py index 4006033..ce37f4f 100644 --- a/forecastmanager/widgets.py +++ b/forecastmanager/widgets.py @@ -6,7 +6,7 @@ from wagtail.utils.widgets import WidgetWithScript from wagtail.widget_adapters import WidgetAdapter -from forecastmanager.constants import WEATHER_CONDITION_CHOICES +from forecastmanager.constants import WEATHER_CONDITION_CHOICES, WEATHER_CONDITION_ICONS class WeatherSymbolChooserWidget(WidgetWithScript, widgets.TextInput): @@ -26,16 +26,9 @@ def get_context(self, name, value, attrs): return context def render_js_init(self, id_, name, value): - symbol_options = [] - for symbol in WEATHER_CONDITION_CHOICES: - symbol_options.append({ - "value": symbol[0], - "label": str(symbol[1]), - "icon_url": static("forecastmanager/weathericons/{0}.png".format(symbol[0])), - }) - + options = WEATHER_CONDITION_ICONS return "$(document).ready(() => new WeatherSymbolChooserWidget({0},{1}));".format(json.dumps(id_), - json.dumps(symbol_options)) + json.dumps(options)) class Media: css = { diff --git a/setup.cfg b/setup.cfg index ba69eb9..9bb98de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,4 +30,6 @@ install_requires = django-filter wagtail-icon-chooser>=0.0.6 wagtail-font-awesome-svg>=1.0.1 + django-extensions + From 78e54f989a68241d4007ee17729d12aff6c034b2 Mon Sep 17 00:00:00 2001 From: Erick Otenyo Date: Fri, 15 Mar 2024 11:16:01 +0300 Subject: [PATCH 3/3] Bump version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 9bb98de..6fa4d0f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = forecastmanager -version = 0.3.0 +version = 0.4.0 description = Integration of Weather City Forecasts Manager in Wagtail Projects. long_description = file:README.md long_description_content_type = text/markdown