Skip to content

Commit 7afcab5

Browse files
authored
Merge pull request #15 from wmo-raf/dev
Refactoring
2 parents 22897b5 + 0de3e37 commit 7afcab5

File tree

173 files changed

+3854
-1392
lines changed

Some content is hidden

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

173 files changed

+3854
-1392
lines changed

forecastmanager/blocks.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from wagtail import blocks
2-
from django.utils.translation import gettext_lazy as _
32

43

54
class ExtremeMeasurementBlock(blocks.StructBlock):
@@ -13,7 +12,7 @@ class Meta:
1312
class ExtremeBlock(blocks.StructBlock):
1413
title = blocks.CharBlock(required=True)
1514
measurements = blocks.StreamBlock([
16-
('measurements',ExtremeMeasurementBlock() )
15+
('measurements', ExtremeMeasurementBlock())
1716
])
1817

1918
class Meta:

forecastmanager/constants.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from django.utils.translation import gettext_lazy as _
2+
3+
# extracted from https://nrkno.github.io/yr-weather-symbols/
4+
WEATHER_CONDITIONS = [
5+
{'id': 'clearsky_day', 'name': 'Clear sky Day'},
6+
{'id': 'clearsky_night', 'name': 'Clear sky Night'},
7+
{'id': 'clearsky_polartwilight', 'name': 'Clear sky Polar Twilight'},
8+
9+
{'id': 'fair_day', 'name': 'Fair Day'},
10+
{'id': 'fair_night', 'name': 'Fair Night'},
11+
{'id': 'fair_polartwilight', 'name': 'Fair Polar Twilight'},
12+
13+
{'id': 'partlycloudy_day', 'name': 'Partly Cloudy Day'},
14+
{'id': 'partlycloudy_night', 'name': 'Partly Cloudy Night'},
15+
{'id': 'partlycloudy_polartwilight', 'name': 'Partly Cloudy Polar Twilight'},
16+
17+
{'id': 'cloudy', 'name': 'Cloudy'},
18+
19+
{'id': 'rainshowers_day', 'name': 'Rain Showers Day'},
20+
{'id': 'rainshowers_night', 'name': 'Rain Showers Night'},
21+
{'id': 'rainshowers_polartwilight', 'name': 'Rain Showers Polar Twilight'},
22+
23+
{'id': 'rainshowersandthunder_day', 'name': 'Rain Showers and Thunder Day'},
24+
{'id': 'rainshowersandthunder_night', 'name': 'Rain Showers and Thunder Night'},
25+
{'id': 'rainshowersandthunder_polartwilight', 'name': 'Rain Showers and Thunder Polar Twilight'},
26+
27+
{'id': 'sleetshowers_day', 'name': 'Sleet Showers Day'},
28+
{'id': 'sleetshowers_night', 'name': 'Sleet Showers Night'},
29+
{'id': 'sleetshowers_polartwilight', 'name': 'Sleet Showers Polar Twilight'},
30+
31+
{'id': 'snowshowers_day', 'name': 'Snow Showers Day'},
32+
{'id': 'snowshowers_night', 'name': 'Snow Showers Night'},
33+
{'id': 'snowshowers_polartwilight', 'name': 'Snow Showers Polar Twilight'},
34+
35+
{'id': 'rain', 'name': 'Rain'}, {'id': 'heavyrain', 'name': 'Heavy Rain'},
36+
37+
{'id': 'heavyrainandthunder', 'name': 'Heavy Rain and Thunder'}, {'id': 'sleet', 'name': 'Sleet'},
38+
39+
{'id': 'snow', 'name': 'Snow'}, {'id': 'snowandthunder', 'name': 'Snow and Thunder'},
40+
41+
{'id': 'fog', 'name': 'Fog'},
42+
43+
{'id': 'sleetshowersandthunder_day', 'name': 'Sleet Showers and Thunder Day'},
44+
{'id': 'sleetshowersandthunder_night', 'name': 'Sleet Showers and Thunder Night'},
45+
{'id': 'sleetshowersandthunder_polartwilight', 'name': 'Sleet Showers and Thunder Polar Twilight'},
46+
47+
{'id': 'snowshowersandthunder_day', 'name': 'Snow Showers and Thunder Day'},
48+
{'id': 'snowshowersandthunder_night', 'name': 'Snow Showers and Thunder Night'},
49+
{'id': 'snowshowersandthunder_polartwilight', 'name': 'Snow Showers and Thunder Polar Twilight'},
50+
51+
{'id': 'rainandthunder', 'name': 'Rain and Thunder'},
52+
53+
{'id': 'sleetandthunder', 'name': 'Sleet and Thunder'},
54+
55+
{'id': 'lightrainshowersandthunder_day', 'name': 'Light Rain Showers and Thunder Day'},
56+
{'id': 'lightrainshowersandthunder_night', 'name': 'Light Rain Showers and Thunder Night'},
57+
{'id': 'lightrainshowersandthunder_polartwilight', 'name': 'Light Rain Showers and Thunder Polar Twilight'},
58+
59+
{'id': 'heavyrainshowersandthunder_day', 'name': 'Heavy Rain Showers and Thunder Day'},
60+
{'id': 'heavyrainshowersandthunder_night', 'name': 'Heavy Rain Showers and Thunder Night'},
61+
{'id': 'heavyrainshowersandthunder_polartwilight', 'name': 'Heavy Rain Showers and Thunder Polar Twilight'},
62+
63+
{'id': 'lightsleetshowersandthunder_day', 'name': 'Light Sleet Showers and Thunder Day'},
64+
{'id': 'lightsleetshowersandthunder_night', 'name': 'Light Sleet Showers and Thunder Night'},
65+
{'id': 'lightsleetshowersandthunder_polartwilight', 'name': 'Light Sleet Showers and Thunder Polar Twilight'},
66+
67+
{'id': 'heavysleetshowersandthunder_day', 'name': 'Heavy Sleet Showers and Thunder Day'},
68+
{'id': 'heavysleetshowersandthunder_night', 'name': 'Heavy Sleet Showers and Thunder Night'},
69+
{'id': 'heavysleetshowersandthunder_polartwilight', 'name': 'Heavy Sleet Showers and Thunder Polar Twilight'},
70+
71+
{'id': 'lightsnowshowersandthunder_day', 'name': 'Light Snow Showers and Thunder Day'},
72+
{'id': 'lightsnowshowersandthunder_night', 'name': 'Light Snow Showers and Thunder Night'},
73+
{'id': 'lightsnowshowersandthunder_polartwilight', 'name': 'Light Snow Showers and Thunder Polar Twilight'},
74+
75+
{'id': 'heavysnowshowersandthunder_day', 'name': 'Heavy Snow Showers and Thunder Day'},
76+
{'id': 'heavysnowshowersandthunder_night', 'name': 'Heavy Snow Showers and Thunder Night'},
77+
{'id': 'heavysnowshowersandthunder_polartwilight', 'name': 'Heavy Snow Showers and Thunder Polar Twilight'},
78+
79+
{'id': 'lightrainandthunder', 'name': 'Light Rain and Thunder'},
80+
{'id': 'lightsleetandthunder', 'name': 'Light Sleet and Thunder'},
81+
{'id': 'heavysleetandthunder', 'name': 'Heavy Sleet and Thunder'},
82+
{'id': 'lightsnowandthunder', 'name': 'Light Snow and Thunder'},
83+
{'id': 'heavysnowandthunder', 'name': 'Heavy Snow and Thunder'},
84+
{'id': 'lightrainshowers_day', 'name': 'Light Rain Showers Day'},
85+
{'id': 'lightrainshowers_night', 'name': 'Light Rain Showers Night'},
86+
{'id': 'lightrainshowers_polartwilight', 'name': 'Light Rain Showers Polar Twilight'},
87+
{'id': 'heavyrainshowers_day', 'name': 'Heavy Rain Showers Day'},
88+
{'id': 'heavyrainshowers_night', 'name': 'Heavy Rain Showers Night'},
89+
{'id': 'heavyrainshowers_polartwilight', 'name': 'Heavy Rain Showers Polar Twilight'},
90+
{'id': 'lightsleetshowers_day', 'name': 'Light Sleet Showers Day'},
91+
{'id': 'lightsleetshowers_night', 'name': 'Light Sleet Showers Night'},
92+
{'id': 'lightsleetshowers_polartwilight', 'name': 'Light Sleet Showers Polar Twilight'},
93+
{'id': 'heavysleetshowers_day', 'name': 'Heavy Sleet Showers Day'},
94+
{'id': 'heavysleetshowers_night', 'name': 'Heavy Sleet Showers Night'},
95+
{'id': 'heavysleetshowers_polartwilight', 'name': 'Heavy Sleet Showers Polar Twilight'},
96+
{'id': 'lightsnowshowers_day', 'name': 'Light Snow Showers Day'},
97+
{'id': 'lightsnowshowers_night', 'name': 'Light Snow Showers Night'},
98+
{'id': 'lightsnowshowers_polartwilight', 'name': 'Light Snow Showers Polar Twilight'},
99+
{'id': 'heavysnowshowers_day', 'name': 'Heavy Snow Showers Day'},
100+
{'id': 'heavysnowshowers_night', 'name': 'Heavy Snow Showers Night'},
101+
{'id': 'heavysnowshowers_polartwilight', 'name': 'Heavy Snow Showers Polar Twilight'},
102+
{'id': 'lightrain', 'name': 'Light Rain'}, {'id': 'lightsleet', 'name': 'Light Sleet'},
103+
{'id': 'heavysleet', 'name': 'Heavy Sleet'}, {'id': 'lightsnow', 'name': 'Light Snow'},
104+
{'id': 'heavysnow', 'name': 'Heavy Snow'}]
105+
106+
WEATHER_PARAMETERS = [
107+
{
108+
"name": "air_temperature_max",
109+
"label": "Maximum Air Temperature",
110+
"unit": "°C",
111+
"data_type": "int"
112+
},
113+
{
114+
"name": "air_temperature_min",
115+
"label": "Minimum Air Temperature",
116+
"unit": "°C",
117+
"data_type": "int"
118+
},
119+
{
120+
"name": "air_temperature",
121+
"label": "Air Temperature",
122+
"unit": "°C",
123+
"data_type": "int"
124+
},
125+
{
126+
"name": "dew_point_temperature",
127+
"label": "Dew Point Temperature",
128+
"unit": "°C",
129+
"data_type": "int"
130+
},
131+
{
132+
"name": "precipitation_amount",
133+
"label": "Precipitation Amount",
134+
"unit": "mm",
135+
"data_type": "float"
136+
},
137+
{
138+
"name": "air_pressure_at_sea_level",
139+
"label": "Air Pressure (Sea level)",
140+
"unit": "hPa",
141+
"data_type": "int"
142+
},
143+
{
144+
"name": "wind_speed",
145+
"label": "Wind Speed",
146+
"unit": "m/s",
147+
"data_type": "int"
148+
},
149+
{
150+
"name": "wind_from_direction",
151+
"label": "Wind Direction",
152+
"unit": "°",
153+
"data_type": "int"
154+
},
155+
{
156+
"name": "relative_humidity",
157+
"label": "Relative Humidity",
158+
"unit": "%",
159+
"data_type": "int"
160+
},
161+
{
162+
"name": "sunrise",
163+
"label": "Sunrise",
164+
},
165+
{
166+
"name": "sunset",
167+
"label": "Sunset",
168+
},
169+
{
170+
"name": "moonrise",
171+
"label": "Moonrise",
172+
},
173+
{
174+
"name": "moonset",
175+
"label": "Moonset",
176+
},
177+
]
178+
179+
WEATHER_PARAMETER_CHOICES = [(param['name'], _(param['label'])) for param in WEATHER_PARAMETERS]
180+
181+
WEATHER_PARAMETERS_AS_DICT = {param['name']: param for param in WEATHER_PARAMETERS}
182+
183+
WEATHER_CONDITION_CHOICES = [(condition['id'], _(condition['name'])) for condition in WEATHER_CONDITIONS]
184+
185+
WEATHER_CONDITIONS_AS_DICT = {condition['id']: condition for condition in WEATHER_CONDITIONS}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from django.db import models
2+
from django.templatetags.static import static
3+
from django.utils.functional import cached_property
4+
from django.utils.translation import gettext_lazy as _
5+
from modelcluster.fields import ParentalKey
6+
from modelcluster.models import ClusterableModel
7+
from wagtail.admin.panels import (
8+
FieldPanel,
9+
TabbedInterface,
10+
ObjectList, InlinePanel,
11+
)
12+
from wagtail.contrib.settings.models import BaseSiteSetting
13+
from wagtail.contrib.settings.registry import register_setting
14+
from wagtail.models import Orderable
15+
16+
from forecastmanager.constants import WEATHER_PARAMETER_CHOICES, WEATHER_PARAMETERS_AS_DICT
17+
from forecastmanager.widgets import WeatherSymbolChooserWidget
18+
19+
20+
@register_setting
21+
class ForecastSetting(ClusterableModel, BaseSiteSetting):
22+
enable_auto_forecast = models.BooleanField(default=False, verbose_name=_('Enable automated forecasts'))
23+
default_city = models.ForeignKey("City", blank=True, null=True, on_delete=models.CASCADE,
24+
verbose_name=_("Default City"))
25+
weather_detail_page = models.ForeignKey("wagtailcore.Page", blank=True, null=True, on_delete=models.SET_NULL, )
26+
27+
edit_handler = TabbedInterface([
28+
ObjectList([
29+
InlinePanel('periods', heading=_("Forecast Periods"), label=_("Forecast Period")),
30+
], heading=_("Forecast Periods")),
31+
ObjectList([
32+
InlinePanel('data_parameters', heading=_("Data Parameters"), label=_("Data Parameter")),
33+
], heading=_("Forecast Data Parameters")),
34+
ObjectList([
35+
InlinePanel('weather_conditions', heading=_("Weather Conditions"), label=_("Weather Condition")),
36+
], heading=_("Forecast Weather Conditions")),
37+
ObjectList([
38+
FieldPanel('enable_auto_forecast'),
39+
], heading=_("Forecast Source")),
40+
ObjectList([
41+
FieldPanel('default_city'),
42+
FieldPanel('weather_detail_page'),
43+
], heading=_("City")),
44+
])
45+
46+
@cached_property
47+
def data_parameter_values(self):
48+
data_parameters = self.data_parameters.all()
49+
params = []
50+
for param in data_parameters:
51+
params.append({"parameter": param.parameter, "name": param.name, "parameter_type": param.parameter_type,
52+
"parameter_unit": param.parameter_unit if param.parameter_unit else " "})
53+
return params
54+
55+
@property
56+
def periods_as_choices(self):
57+
return [(period.id, period.label) for period in self.periods.all()]
58+
59+
@property
60+
def weather_conditions_list(self):
61+
weather_conditions = self.weather_conditions.all()
62+
return [c.alias if c.alias else c.label for c in weather_conditions]
63+
64+
65+
class ForecastPeriod(Orderable):
66+
parent = ParentalKey(ForecastSetting, on_delete=models.CASCADE, related_name="periods")
67+
default = models.BooleanField(default=False, verbose_name=_("Is default"))
68+
forecast_effective_time = models.TimeField(verbose_name=_("Forecast Effective Time"))
69+
label = models.CharField(max_length=100, verbose_name=_("Label"))
70+
71+
class Meta:
72+
unique_together = ("default", "forecast_effective_time")
73+
74+
panels = [
75+
FieldPanel('forecast_effective_time'),
76+
FieldPanel('label'),
77+
FieldPanel('default'),
78+
]
79+
80+
def __str__(self):
81+
return self.label
82+
83+
def save(self, *args, **kwargs):
84+
if self.default:
85+
ForecastPeriod.objects.filter(default=True).update(default=False)
86+
super().save(*args, **kwargs)
87+
88+
89+
class ForecastDataParameters(Orderable):
90+
PARAMETER_TYPE_CHOICES = (
91+
("numeric", _("Number")),
92+
("time", _("Time")),
93+
("text", _("Text")),
94+
)
95+
parent = ParentalKey(ForecastSetting, on_delete=models.CASCADE, related_name="data_parameters")
96+
parameter = models.CharField(max_length=100, choices=WEATHER_PARAMETER_CHOICES, unique=True,
97+
verbose_name=_("Parameter"))
98+
name = models.CharField(max_length=100, verbose_name=_("Parameter Label"),
99+
help_text=_("Parameter name as locally labelled"))
100+
parameter_type = models.CharField(max_length=100, choices=PARAMETER_TYPE_CHOICES, verbose_name=_("Parameter Type"),
101+
default="numeric")
102+
parameter_unit = models.CharField(_("Unit of measurement"), max_length=100, null=True, blank=True,
103+
help_text="e.g °C, %, mm, hPa, etc ")
104+
105+
panels = [
106+
FieldPanel('parameter'),
107+
FieldPanel('name'),
108+
]
109+
110+
def __str__(self):
111+
return self.name
112+
113+
@property
114+
def parameter_info(self):
115+
return WEATHER_PARAMETERS_AS_DICT.get(self.parameter)
116+
117+
def parse_value(self, value):
118+
info = self.parameter_info
119+
120+
if not info.get("data_type"):
121+
return value
122+
123+
try:
124+
if info.get("data_type") == "int":
125+
return int(float(value))
126+
elif info.get("data_type") == "float":
127+
return float(value)
128+
except ValueError:
129+
pass
130+
131+
return value
132+
133+
134+
class WeatherCondition(Orderable):
135+
parent = ParentalKey(ForecastSetting, on_delete=models.CASCADE, related_name="weather_conditions")
136+
symbol = models.CharField(max_length=100, verbose_name=_("Weather Symbol"))
137+
label = models.CharField(max_length=100, unique=True, verbose_name=_("Label"))
138+
alias = models.CharField(max_length=100, blank=True, null=True, unique=True, verbose_name=_("Alias"))
139+
140+
panels = [
141+
FieldPanel('symbol', widget=WeatherSymbolChooserWidget),
142+
FieldPanel('label'),
143+
FieldPanel('alias'),
144+
]
145+
146+
def __str__(self):
147+
return self.label
148+
149+
@property
150+
def icon_url(self):
151+
return static('forecastmanager/weathericons/{}.png'.format(self.symbol))

0 commit comments

Comments
 (0)