1+ from django .db import transaction
2+ from django .db .models import Q
3+ from uuid import UUID
14from rest_framework import serializers
25
3- from .models import City , Forecast
6+ from .forecast_settings import ForecastPeriod , WeatherCondition , ForecastDataParameters
7+ from .models import City , Forecast , CityForecast , DataValue
48
59
610class CitySerializer (serializers .ModelSerializer ):
@@ -24,3 +28,157 @@ def to_representation(self, instance):
2428 request = self .context .get ("request" )
2529
2630 return instance .get_geojson (request )
31+
32+
33+ class CityForecastPostSerializer (serializers .Serializer ):
34+ city = serializers .CharField ()
35+ condition = serializers .CharField ()
36+ data_values = serializers .DictField (
37+ child = serializers .JSONField (),
38+ required = False ,
39+ default = dict ,
40+ )
41+
42+ def validate (self , attrs ):
43+ city_ref = attrs .get ("city" )
44+ condition_ref = attrs .get ("condition" )
45+ data_values = attrs .get ("data_values" ) or {}
46+
47+ city_query = Q (name__iexact = city_ref ) | Q (slug__iexact = city_ref )
48+ try :
49+ city_uuid = UUID (str (city_ref ))
50+ city_query = city_query | Q (id = city_uuid )
51+ except (ValueError , TypeError ):
52+ pass
53+
54+ city_obj = City .objects .filter (city_query ).first ()
55+
56+ if not city_obj :
57+ raise serializers .ValidationError ({"city" : f"Unknown city: { city_ref } " })
58+
59+ condition_obj = WeatherCondition .objects .filter (
60+ Q (symbol__iexact = condition_ref ) |
61+ Q (label__iexact = condition_ref ) |
62+ Q (alias__iexact = condition_ref )
63+ ).first ()
64+
65+ if not condition_obj :
66+ raise serializers .ValidationError ({"condition" : f"Unknown weather condition: { condition_ref } " })
67+
68+ normalized_data_values = []
69+ for parameter_key , value in data_values .items ():
70+ parameter_obj = ForecastDataParameters .objects .filter (
71+ Q (parameter__iexact = parameter_key ) |
72+ Q (name__iexact = parameter_key )
73+ ).first ()
74+
75+ if not parameter_obj :
76+ raise serializers .ValidationError (
77+ {"data_values" : f"Unknown parameter: { parameter_key } " }
78+ )
79+
80+ if value is None or value == "" :
81+ continue
82+
83+ if parameter_obj .parameter_type == "numeric" :
84+ try :
85+ float (value )
86+ except (TypeError , ValueError ):
87+ raise serializers .ValidationError (
88+ {
89+ "data_values" : (
90+ f"Invalid numeric value for '{ parameter_key } ': { value } "
91+ )
92+ }
93+ )
94+
95+ normalized_data_values .append ({
96+ "parameter" : parameter_obj ,
97+ "value" : str (value ),
98+ })
99+
100+ attrs ["city_obj" ] = city_obj
101+ attrs ["condition_obj" ] = condition_obj
102+ attrs ["resolved_data_values" ] = normalized_data_values
103+ return attrs
104+
105+
106+ class ForecastPostSerializer (serializers .Serializer ):
107+ forecast_date = serializers .DateField ()
108+ effective_time = serializers .TimeField (help_text = "Forecast effective time, e.g. '06:00:00'" )
109+ source = serializers .ChoiceField (
110+ choices = [choice [0 ] for choice in Forecast .FORECAST_SOURCE_CHOICES ],
111+ required = False ,
112+ default = "local" ,
113+ )
114+ replace_existing = serializers .BooleanField (required = False , default = True )
115+ city_forecasts = CityForecastPostSerializer (many = True )
116+
117+ def validate_city_forecasts (self , value ):
118+ if not value :
119+ raise serializers .ValidationError ("At least one city forecast is required." )
120+
121+ seen = set ()
122+ for city_forecast in value :
123+ city_id = str (city_forecast ["city_obj" ].id )
124+ if city_id in seen :
125+ raise serializers .ValidationError (
126+ f"Duplicate city in payload: { city_forecast ['city_obj' ].name } "
127+ )
128+ seen .add (city_id )
129+
130+ return value
131+
132+ def validate (self , attrs ):
133+ effective_time = attrs .get ("effective_time" )
134+ period_obj = ForecastPeriod .objects .filter (forecast_effective_time = effective_time ).first ()
135+ if not period_obj :
136+ raise serializers .ValidationError ({"effective_time" : f"No ForecastPeriod found for time { effective_time } " })
137+ attrs ["effective_period" ] = period_obj
138+ return attrs
139+
140+ @transaction .atomic
141+ def create (self , validated_data ):
142+ city_forecasts = validated_data .pop ("city_forecasts" )
143+ replace_existing = validated_data .pop ("replace_existing" , True )
144+ effective_period = validated_data .pop ("effective_period" )
145+
146+ existing_forecasts = Forecast .objects .filter (
147+ forecast_date = validated_data ["forecast_date" ],
148+ effective_period = effective_period ,
149+ )
150+
151+ if existing_forecasts .exists ():
152+ if not replace_existing :
153+ raise serializers .ValidationError (
154+ "Forecast already exists for this date and effective period. "
155+ "Set replace_existing=true to overwrite it."
156+ )
157+ existing_forecasts .delete ()
158+
159+ forecast = Forecast .objects .create (
160+ forecast_date = validated_data ["forecast_date" ],
161+ effective_period = effective_period ,
162+ source = validated_data .get ("source" , "local" )
163+ )
164+
165+ for city_forecast_data in city_forecasts :
166+ city_forecast = CityForecast .objects .create (
167+ parent = forecast ,
168+ city = city_forecast_data ["city_obj" ],
169+ condition = city_forecast_data ["condition_obj" ],
170+ )
171+
172+ data_values = [
173+ DataValue (
174+ parent = city_forecast ,
175+ parameter = data_value ["parameter" ],
176+ value = data_value ["value" ],
177+ )
178+ for data_value in city_forecast_data ["resolved_data_values" ]
179+ ]
180+
181+ if data_values :
182+ DataValue .objects .bulk_create (data_values )
183+
184+ return forecast
0 commit comments