Skip to content

Commit 1046a13

Browse files
authored
Merge pull request #1 from wmo-raf/dev
update forecast manager
2 parents b97adba + 69c2c07 commit 1046a13

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2590
-522
lines changed

MANIFEST.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ include README.md
22
include LICENSE
33
recursive-include forecastmanager *.py
44
recursive-include forecastmanager/templates *
5-
recursive-include forecastmanager *.html
5+
recursive-include forecastmanager/static *
6+
recursive-include forecastmanager *.html

forecastmanager/blocks.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from wagtail import blocks
2+
from django.utils.translation import gettext_lazy as _
3+
4+
5+
class ExtremeMeasurementBlock(blocks.StructBlock):
6+
station_name = blocks.CharBlock(required=True)
7+
extreme_value = blocks.FloatBlock(required=True)
8+
9+
class Meta:
10+
template = 'blocks/extreme_block_item.html'
11+
12+
13+
class ExtremeBlock(blocks.StructBlock):
14+
title = blocks.CharBlock(required=True)
15+
measurements = blocks.StreamBlock([
16+
('measurements',ExtremeMeasurementBlock() )
17+
])
18+
19+
class Meta:
20+
template = 'blocks/extreme_block.html'

forecastmanager/management/commands/generate_forecast.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import requests
77

88
from wagtailgeowidget.helpers import geosgeometry_str_to_struct
9-
from forecastmanager.models import City,Forecast, ConditionCategory
9+
from forecastmanager.models import City,Forecast
1010

1111

1212
# Define the base URL for the Met Norway API
@@ -64,17 +64,12 @@ def extract_values(group):
6464
# Extract the minimum and maximum values of air temperature, wind speed, and wind direction
6565
min_temp = group['data.instant.details.air_temperature'].min()
6666
max_temp = group['data.instant.details.air_temperature'].max()
67-
wind_speed = group['data.instant.details.wind_speed'].mean()
68-
wind_dir = group['data.instant.details.wind_from_direction'].mean()
6967
# Extract the value of next_12_hours summary
7068
next_12_hours = group['data.next_12_hours.summary.symbol_code'].iloc[0]
7169
# Create a dictionary of the extracted values
72-
values = {'min_temp': min_temp, 'max_temp': max_temp, 'wind_speed': wind_speed,
73-
'wind_dir': wind_dir,
70+
values = {'min_temp': min_temp, 'max_temp': max_temp,
7471
'next_12_hours': next_12_hours}
7572
return pd.Series(values, index=['min_temp', 'max_temp',
76-
'wind_speed',
77-
'wind_dir',
7873
'next_12_hours'])
7974

8075
# Group the DataFrame by date and apply the extract_values() function to each group
@@ -90,17 +85,15 @@ def extract_values(group):
9085
time = index.to_pydatetime()
9186
min_temp = row['min_temp']
9287
max_temp = row['max_temp']
93-
wind_speed = row['wind_speed']
94-
wind_dir = row['wind_dir']
9588

9689
# Create or update the child object with the parent and the name from the second column
9790
# prioritize condition for the next 1 hour
9891
if 'next_1_hours' in row:
99-
condition = ConditionCategory.objects.get(short_name=row['next_1_hours'])
92+
condition = row['next_1_hours'].split("_")[0]
10093
elif 'next_6_hours' in row:
101-
condition = ConditionCategory.objects.get(short_name=row['next_6_hours'])
94+
condition = row['next_6_hours'].split("_")[0]
10295
else:
103-
condition = ConditionCategory.objects.get(short_name=row['next_12_hours'])
96+
condition = row['next_12_hours'].split("_")[0]
10497

10598
# use update_or_create to update existing data
10699
# and create new ones if the data does not exist
@@ -110,8 +103,6 @@ def extract_values(group):
110103
defaults={
111104
'min_temp': min_temp,
112105
'max_temp': max_temp,
113-
'wind_speed': wind_speed,
114-
'wind_direction': wind_dir,
115106
'condition': condition
116107
}
117108
)

forecastmanager/migrations/0001_initial.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
# Generated by Django 4.2.2 on 2023-06-18 16:15
1+
# Generated by Django 4.2.2 on 2023-07-04 14:49
22

33
import django.contrib.gis.db.models.fields
44
from django.db import migrations, models
55
import django.db.models.deletion
66
import uuid
7+
import wagtail.blocks
8+
import wagtail.fields
79

810

911
class Migration(migrations.Migration):
@@ -12,7 +14,6 @@ class Migration(migrations.Migration):
1214

1315
dependencies = [
1416
('wagtailcore', '0083_workflowcontenttype'),
15-
('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
1617
]
1718

1819
operations = [
@@ -29,21 +30,20 @@ class Migration(migrations.Migration):
2930
},
3031
),
3132
migrations.CreateModel(
32-
name='ConditionCategory',
33+
name='DailyWeather',
3334
fields=[
3435
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
35-
('title', models.CharField(help_text='Weather Condition Title', max_length=50, verbose_name='Weather Condtion Title')),
36-
('short_name', models.CharField(blank=True, editable=False, help_text='Weather Condition Short Name (helpgul for yr.no weather api)', max_length=50, null=True, verbose_name='Weather Condtion Short Name')),
37-
('description', models.CharField(blank=True, max_length=250, null=True, verbose_name='Description')),
38-
('icon_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='Icon image')),
36+
('issued_on', models.DateField(auto_now_add=True, null=True)),
37+
('forecast_date', models.DateField(verbose_name='Forecast Date')),
38+
('forecast_desc', wagtail.fields.RichTextField(verbose_name='Weather Forecast Description')),
39+
('summary_date', models.DateField(verbose_name='Summary Date')),
40+
('summary_desc', wagtail.fields.RichTextField(verbose_name='Weather Summary Description')),
41+
('extreme_date', models.DateField(blank=True, null=True, verbose_name='Extreme Date')),
42+
('extremes', wagtail.fields.StreamField([('extremes', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('measurements', wagtail.blocks.StreamBlock([('measurements', wagtail.blocks.StructBlock([('station_name', wagtail.blocks.CharBlock(required=True)), ('extreme_value', wagtail.blocks.FloatBlock(required=True))]))]))]))], use_json_field=True)),
3943
],
40-
options={
41-
'verbose_name': 'Weather Condition Category',
42-
'verbose_name_plural': 'Weather Condition Categories',
43-
},
4444
),
4545
migrations.CreateModel(
46-
name='Setting',
46+
name='ForecastSetting',
4747
fields=[
4848
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
4949
('enable_auto_forecast', models.BooleanField(default=True, verbose_name='Enable automated forecasts')),
@@ -64,13 +64,12 @@ class Migration(migrations.Migration):
6464
('min_temp', models.IntegerField(blank=True, verbose_name='Minimum Temperaure')),
6565
('wind_direction', models.IntegerField(blank=True, null=True, verbose_name='Wind Direction')),
6666
('wind_speed', models.IntegerField(blank=True, null=True, verbose_name='Wind Speed')),
67+
('condition', models.CharField(choices=[('clearsky', 'Clear sky'), ('cloudy', 'Cloudy'), ('fair', 'Fair'), ('fog', 'Fog'), ('heavyrain', 'Heavy Rain'), ('heavyrainandthunder', 'Heavy Rain and Thunder'), ('heavyrainshowers', 'Heavy Rain Showers'), ('heavyrainshowersandthunder', 'Heavy Rain Showers and Thunder'), ('heavysleet', 'Heavy Sleet'), ('heavysleetandthunder', 'Heavy Sleet and Thunder'), ('heavysleetshowers', 'Heavy Sleet Showers'), ('heavysleetshowersandthunder', 'Heavy Sleet Showers and Thunder'), ('heavysnow', 'Heavy Snow'), ('heavysnowandthunder', 'Heavy Snow and Thunder'), ('heavysnowshowers', 'Heavy Snow Showers'), ('heavysnowshowersandthunder', 'Heavy Snow Showers and Thunder'), ('lightrain', 'Light Rain'), ('lightrainandthunder', 'Light Rain and Thunder'), ('lightrainshowers', 'Light Rain Showers'), ('lightrainshowersandthunder', 'Light Rain Showers and Thunder'), ('lightsleet', 'Light Sleet'), ('lightsleetandthunder', 'Light Sleet and Thunder'), ('lightsleetshowers', 'Light Sleet Showers'), ('lightsleetshowersandthunder', 'Light Sleet Showers and Thunder'), ('lightsnowshowersandthunder', 'Light Snow Showers and Thunder'), ('partlycloudy', 'Partly Cloudy'), ('rain', 'Rain'), ('rainandthunder', 'Rain and Thunder'), ('rainshowers', 'Rain showers'), ('rainshowersandthunder', 'Rain Showes and Thunder'), ('sleet', 'Sleet'), ('sleetandthunder', 'Sleet and Thunder'), ('sleetshowers', 'Sleet Showers'), ('sleetshowersandthunder', 'Sleet Showes and Thunder'), ('snow', 'Snow'), ('snowandthunder', 'Snow and Thunder'), ('snowshowers', 'Snow Showers'), ('snowshowersandthunder', 'Snow Showers and Thunder')], help_text='E.g Light Showers', max_length=255, null=True, verbose_name='General Weather Condition')),
6768
('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='forecastmanager.city', verbose_name='City')),
68-
('condition', models.ForeignKey(help_text='E.g Light Showers', null=True, on_delete=django.db.models.deletion.CASCADE, to='forecastmanager.conditioncategory', verbose_name='General Weather Condition')),
6969
],
7070
options={
7171
'verbose_name': 'Forecast',
7272
'verbose_name_plural': 'Forecasts',
73-
'unique_together': {('city', 'forecast_date')},
7473
},
7574
),
7675
]

forecastmanager/models.py

Lines changed: 86 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,47 @@
11
import uuid
2-
import json
3-
from django.utils.functional import cached_property
42
from django.contrib.gis.db import models
53
from django.utils.translation import gettext_lazy as _
64
from wagtailgeowidget.panels import LeafletPanel
75
from wagtailgeowidget.helpers import geosgeometry_str_to_struct
8-
from wagtail.admin.panels import FieldPanel
6+
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
7+
from wagtail.fields import RichTextField, StreamField
8+
from wagtail.snippets.models import register_snippet
9+
10+
from .blocks import ExtremeBlock
11+
12+
# @register_snippet
13+
class DailyWeather(models.Model):
14+
issued_on = models.DateField(auto_now_add=True, null=True)
15+
forecast_date = models.DateField(_("Forecast Date"), auto_now=False, auto_now_add=False)
16+
forecast_desc = RichTextField(verbose_name=_('Weather Forecast Description'))
17+
summary_date = models.DateField(_("Summary Date"), auto_now=False, auto_now_add=False)
18+
summary_desc = RichTextField(verbose_name=_('Weather Summary Description'))
19+
extreme_date = models.DateField(_("Extreme Date"), auto_now=False, auto_now_add=False, null=True, blank=True)
20+
extremes = StreamField([
21+
('extremes', ExtremeBlock())
22+
], use_json_field=True)
23+
24+
panels = [
25+
MultiFieldPanel([
26+
FieldPanel('summary_date'),
27+
FieldPanel('summary_desc'),
28+
], heading="Weather Summary"),
29+
MultiFieldPanel([
30+
FieldPanel('forecast_date'),
31+
FieldPanel('forecast_desc'),
32+
], heading="Weather Forecast"),
33+
MultiFieldPanel([
34+
FieldPanel('extreme_date'),
35+
FieldPanel('extremes')
36+
], heading="Extremes")
37+
38+
39+
]
40+
41+
def __str__(self) -> str:
42+
return f'Daily Weather - Issued on {self.issued_on.strftime("%Y-%m-%d")}'
43+
44+
945

1046
# Create your models here.
1147
class City(models.Model):
@@ -30,62 +66,66 @@ class Meta:
3066
def __str__(self) -> str:
3167
return self.name
3268

33-
@cached_property
34-
def point(self):
35-
return json.dumps(geosgeometry_str_to_struct(str(self.location)))
36-
3769
@property
38-
def lat(self):
39-
return self.point['y']
70+
def coordinates(self):
71+
location = geosgeometry_str_to_struct(str(self.location))
72+
return [location['x'],location['y'] ]
4073

41-
@property
42-
def lng(self):
43-
return self.point['x']
44-
45-
46-
class ConditionCategory(models.Model):
47-
title = models.CharField(max_length=50, help_text=_("Weather Condition Title"), verbose_name=_("Weather Condtion Title"))
48-
short_name = models.CharField(max_length=50, help_text=_("Weather Condition Short Name (helpgul for yr.no weather api)"), verbose_name=_("Weather Condtion Short Name"), null=True, blank=True, editable=False)
49-
description = models.CharField(max_length=250, blank=True, null=True, verbose_name=_("Description"))
50-
icon_image = models.ForeignKey(
51-
'wagtailimages.Image',
52-
null=True,
53-
blank=False,
54-
on_delete=models.SET_NULL,
55-
related_name='+',
56-
verbose_name=_("Icon image")
57-
)
58-
59-
60-
class Meta:
61-
verbose_name = _("Weather Condition Category")
62-
verbose_name_plural = _("Weather Condition Categories")
63-
64-
65-
def __str__(self):
66-
return self.title
67-
68-
69-
def save(self, *args, **kwargs):
70-
if not self.short_name:
71-
self.short_name = self.icon_image.file.name.split('/')[-1].split('.')[0]
72-
super(ConditionCategory, self).save(*args, **kwargs)
74+
class Forecast(models.Model):
75+
CONDITION_CHOICES = (
76+
('clearsky', 'Clear sky'),
77+
('cloudy','Cloudy'),
78+
('fair','Fair'),
79+
('fog','Fog'),
80+
('heavyrain','Heavy Rain'),
81+
('heavyrainandthunder','Heavy Rain and Thunder'),
82+
('heavyrainshowers','Heavy Rain Showers'),
83+
('heavyrainshowersandthunder','Heavy Rain Showers and Thunder'),
84+
('heavysleet','Heavy Sleet'),
85+
('heavysleetandthunder','Heavy Sleet and Thunder'),
86+
('heavysleetshowers','Heavy Sleet Showers'),
87+
('heavysleetshowersandthunder','Heavy Sleet Showers and Thunder'),
88+
('heavysnow','Heavy Snow'),
89+
('heavysnowandthunder','Heavy Snow and Thunder'),
90+
('heavysnowshowers','Heavy Snow Showers'),
91+
('heavysnowshowersandthunder','Heavy Snow Showers and Thunder'),
92+
('lightrain','Light Rain'),
93+
('lightrainandthunder','Light Rain and Thunder'),
94+
('lightrainshowers','Light Rain Showers'),
95+
('lightrainshowersandthunder','Light Rain Showers and Thunder'),
96+
('lightsleet','Light Sleet'),
97+
('lightsleetandthunder','Light Sleet and Thunder'),
98+
('lightsleetshowers','Light Sleet Showers'),
99+
('lightsleetshowersandthunder','Light Sleet Showers and Thunder'),
100+
('lightsnowshowersandthunder','Light Snow Showers and Thunder'),
101+
('partlycloudy','Partly Cloudy'),
102+
('rain','Rain'),
103+
('rainandthunder','Rain and Thunder'),
104+
('rainshowers','Rain showers'),
105+
('rainshowersandthunder','Rain Showes and Thunder'),
106+
('sleet','Sleet'),
107+
('sleetandthunder','Sleet and Thunder'),
108+
('sleetshowers','Sleet Showers'),
109+
('sleetshowersandthunder','Sleet Showes and Thunder'),
110+
('snow','Snow'),
111+
('snowandthunder','Snow and Thunder'),
112+
('snowshowers','Snow Showers'),
113+
('snowshowersandthunder','Snow Showers and Thunder'),
73114

115+
)
74116

75-
class Forecast(models.Model):
76117
city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name=_("City"))
77118
forecast_date = models.DateField( auto_now=False, auto_now_add=False, verbose_name=_("Forecasts Date"))
78119
max_temp = models.IntegerField(verbose_name=_("Maximum Temperature"), blank=True)
79120
min_temp = models.IntegerField(verbose_name=_("Minimum Temperaure"), blank=True)
80121
wind_direction = models.IntegerField(verbose_name=_("Wind Direction"), blank=True, null=True)
81122
wind_speed = models.IntegerField(verbose_name=_("Wind Speed"), blank=True, null=True)
82-
# condition = models.CharField(verbose_name=_("General Weather Condition", max_length=255, blank=True, help_text="E.g Light Showers", default="Light Showers")
83-
condition = models.ForeignKey(ConditionCategory, verbose_name=_("General Weather Condition"), on_delete=models.CASCADE, help_text=_("E.g Light Showers"), null=True)
123+
condition = models.CharField(choices=CONDITION_CHOICES, verbose_name=_("General Weather Condition"), help_text=_("E.g Light Showers"), null=True, max_length=255)
84124

85125
class Meta:
86126
verbose_name = _("Forecast")
87127
verbose_name_plural = _("Forecasts")
88-
unique_together = ('city', 'forecast_date')
128+
# unique_together = (('city', 'forecast_date'))
89129

90130
panels = [
91131
FieldPanel("city"),
@@ -100,3 +140,4 @@ class Meta:
100140
# def __str__(self):
101141
# return f"{self.city} - {self.forecast_date}"
102142

143+

forecastmanager/serializers.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from rest_framework import serializers, status
2+
from .models import City, Forecast
3+
from rest_framework.response import Response
4+
5+
6+
class CitySerializer(serializers.ModelSerializer):
7+
8+
coordinates = serializers.SerializerMethodField()
9+
10+
class Meta:
11+
model = City
12+
fields = ('name', 'coordinates')
13+
14+
def get_coordinates(self, obj):
15+
# Implement the logic to compute the property value here
16+
17+
return obj.coordinates
18+
19+
20+
class ForecastSerializer(serializers.ModelSerializer):
21+
22+
city_detail = serializers.SerializerMethodField()
23+
condition_display = serializers.SerializerMethodField()
24+
list_serializer_class = serializers.ListSerializer
25+
26+
class Meta:
27+
model = Forecast
28+
fields =['id','forecast_date','max_temp','min_temp', 'condition_display', 'city', 'city_detail', 'condition']
29+
30+
31+
@staticmethod
32+
def get_city_detail(obj):
33+
serializer = CitySerializer(obj.city)
34+
return serializer.data
35+
36+
def get_condition_display(self, obj):
37+
return obj.get_condition_display()
38+
39+
def to_representation(self, instance):
40+
representation = super().to_representation(instance)
41+
42+
forecast_feature = {
43+
"type": "Feature",
44+
"properties": {
45+
'id': representation['id'],
46+
'city_name': representation['city_detail']['name'],
47+
'forecast_date': representation['forecast_date'],
48+
'max_temp': representation['max_temp'],
49+
'min_temp': representation['min_temp'],
50+
'condition':representation['condition_display'],
51+
'condition_icon': f'/static/forecastmanager/img/{representation["condition"]}.png',
52+
},
53+
"geometry": {
54+
"coordinates": representation['city_detail']['coordinates'],
55+
"type": "Point"
56+
}
57+
}
58+
59+
return forecast_feature
60+
61+
62+
def create(self, validated_data):
63+
forecast, created = Forecast.objects.update_or_create(
64+
forecast_date=validated_data['forecast_date'],
65+
city=validated_data['city'],
66+
defaults={
67+
'min_temp': validated_data['min_temp'],
68+
'max_temp': validated_data['max_temp'],
69+
'condition': validated_data['condition'],
70+
}
71+
)
72+
73+
return forecast
74+
75+
76+
77+
78+

0 commit comments

Comments
 (0)