Skip to content

Commit ebe7199

Browse files
authored
Merge pull request #2596 from IFRCGo/feature/timeframe-validations
Feature/Timeframe and value validations
2 parents fea4344 + d7edba9 commit ebe7199

File tree

8 files changed

+297
-70
lines changed

8 files changed

+297
-70
lines changed

eap/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ class SimplifiedEAPAdmin(admin.ModelAdmin):
6464
)
6565
readonly_fields = (
6666
"cover_image",
67-
"hazard_impact_file",
68-
"risk_selected_protocols_file",
69-
"selected_early_actions_file",
67+
"hazard_impact_images",
68+
"risk_selected_protocols_images",
69+
"selected_early_actions_images",
7070
"planned_operations",
7171
"enable_approaches",
7272
"parent",

eap/enums.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@
55
"eap_type": models.EAPType,
66
"sector": models.PlannedOperation.Sector,
77
"timeframe": models.OperationActivity.TimeFrame,
8+
"years_timeframe_value": models.OperationActivity.YearsTimeFrameChoices,
9+
"months_timeframe_value": models.OperationActivity.MonthsTimeFrameChoices,
10+
"days_timeframe_value": models.OperationActivity.DaysTimeFrameChoices,
11+
"hours_timeframe_value": models.OperationActivity.HoursTimeFrameChoices,
812
"approach": models.EnableApproach.Approach,
913
}

eap/factories.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from random import random
2-
31
import factory
42
from factory import fuzzy
53

@@ -66,7 +64,6 @@ class Meta:
6664

6765
activity = fuzzy.FuzzyText(length=50, prefix="Activity-")
6866
timeframe = fuzzy.FuzzyChoice(OperationActivity.TimeFrame)
69-
time_value = factory.LazyFunction(lambda: [random.randint(1, 12) for _ in range(3)])
7067

7168

7269
class EnableApproachFactory(factory.django.DjangoModelFactory):
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Generated by Django 4.2.19 on 2025-11-25 10:51
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("eap", "0007_alter_eapfile_options_alter_eapregistration_options_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveField(
13+
model_name="simplifiedeap",
14+
name="hazard_impact_file",
15+
),
16+
migrations.RemoveField(
17+
model_name="simplifiedeap",
18+
name="risk_selected_protocols_file",
19+
),
20+
migrations.RemoveField(
21+
model_name="simplifiedeap",
22+
name="selected_early_actions_file",
23+
),
24+
migrations.AddField(
25+
model_name="simplifiedeap",
26+
name="hazard_impact_images",
27+
field=models.ManyToManyField(
28+
blank=True,
29+
related_name="simplified_eap_hazard_impact_images",
30+
to="eap.eapfile",
31+
verbose_name="Hazard Impact Images",
32+
),
33+
),
34+
migrations.AddField(
35+
model_name="simplifiedeap",
36+
name="risk_selected_protocols_images",
37+
field=models.ManyToManyField(
38+
blank=True,
39+
related_name="simplified_eap_risk_selected_protocols_images",
40+
to="eap.eapfile",
41+
verbose_name="Risk Selected Protocols Images",
42+
),
43+
),
44+
migrations.AddField(
45+
model_name="simplifiedeap",
46+
name="selected_early_actions_images",
47+
field=models.ManyToManyField(
48+
blank=True,
49+
related_name="simplified_eap_selected_early_actions_images",
50+
to="eap.eapfile",
51+
verbose_name="Selected Early Actions Images",
52+
),
53+
),
54+
]

eap/models.py

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,76 @@ class Meta:
233233

234234

235235
class OperationActivity(models.Model):
236+
# NOTE: `timeframe` and `time_value` together represent the time span for an activity.
237+
# Make sure to keep them in sync.
236238
class TimeFrame(models.IntegerChoices):
237239
YEARS = 10, _("Years")
238240
MONTHS = 20, _("Months")
239241
DAYS = 30, _("Days")
240242
HOURS = 40, _("Hours")
241243

244+
class YearsTimeFrameChoices(models.IntegerChoices):
245+
ONE_YEAR = 1, _("1")
246+
TWO_YEARS = 2, _("2")
247+
THREE_YEARS = 3, _("3")
248+
FOUR_YEARS = 4, _("4")
249+
FIVE_YEARS = 5, _("5")
250+
251+
class MonthsTimeFrameChoices(models.IntegerChoices):
252+
ONE_MONTH = 1, _("1")
253+
TWO_MONTHS = 2, _("2")
254+
THREE_MONTHS = 3, _("3")
255+
FOUR_MONTHS = 4, _("4")
256+
FIVE_MONTHS = 5, _("5")
257+
SIX_MONTHS = 6, _("6")
258+
SEVEN_MONTHS = 7, _("7")
259+
EIGHT_MONTHS = 8, _("8")
260+
NINE_MONTHS = 9, _("9")
261+
TEN_MONTHS = 10, _("10")
262+
ELEVEN_MONTHS = 11, _("11")
263+
TWELVE_MONTHS = 12, _("12")
264+
265+
class DaysTimeFrameChoices(models.IntegerChoices):
266+
ONE_DAY = 1, _("1")
267+
TWO_DAYS = 2, _("2")
268+
THREE_DAYS = 3, _("3")
269+
FOUR_DAYS = 4, _("4")
270+
FIVE_DAYS = 5, _("5")
271+
SIX_DAYS = 6, _("6")
272+
SEVEN_DAYS = 7, _("7")
273+
EIGHT_DAYS = 8, _("8")
274+
NINE_DAYS = 9, _("9")
275+
TEN_DAYS = 10, _("10")
276+
ELEVEN_DAYS = 11, _("11")
277+
TWELVE_DAYS = 12, _("12")
278+
THIRTEEN_DAYS = 13, _("13")
279+
FOURTEEN_DAYS = 14, _("14")
280+
FIFTEEN_DAYS = 15, _("15")
281+
SIXTEEN_DAYS = 16, _("16")
282+
SEVENTEEN_DAYS = 17, _("17")
283+
EIGHTEEN_DAYS = 18, _("18")
284+
NINETEEN_DAYS = 19, _("19")
285+
TWENTY_DAYS = 20, _("20")
286+
TWENTY_ONE_DAYS = 21, _("21")
287+
TWENTY_TWO_DAYS = 22, _("22")
288+
TWENTY_THREE_DAYS = 23, _("23")
289+
TWENTY_FOUR_DAYS = 24, _("24")
290+
TWENTY_FIVE_DAYS = 25, _("25")
291+
TWENTY_SIX_DAYS = 26, _("26")
292+
TWENTY_SEVEN_DAYS = 27, _("27")
293+
TWENTY_EIGHT_DAYS = 28, _("28")
294+
TWENTY_NINE_DAYS = 29, _("29")
295+
THIRTY_DAYS = 30, _("30")
296+
THIRTY_ONE_DAYS = 31, _("31")
297+
298+
class HoursTimeFrameChoices(models.IntegerChoices):
299+
ZERO_TO_FIVE_HOURS = 5, _("0-5")
300+
FIVE_TO_TEN_HOURS = 10, _("5-10")
301+
TEN_TO_FIFTEEN_HOURS = 15, _("10-15")
302+
FIFTEEN_TO_TWENTY_HOURS = 20, _("15-20")
303+
TWENTY_TO_TWENTY_FIVE_HOURS = 25, _("20-25")
304+
TWENTY_FIVE_TO_THIRTY_HOURS = 30, _("25-30")
305+
242306
activity = models.CharField(max_length=255, verbose_name=_("Activity"))
243307
timeframe = models.IntegerField(choices=TimeFrame.choices, verbose_name=_("Timeframe"))
244308
time_value = ArrayField(
@@ -732,10 +796,10 @@ class SimplifiedEAP(EAPBaseModel):
732796
null=True,
733797
blank=True,
734798
)
735-
hazard_impact_file = models.ManyToManyField(
799+
hazard_impact_images = models.ManyToManyField(
736800
EAPFile,
737-
verbose_name=_("Hazard Impact Files"),
738-
related_name="simplified_eap_hazard_impact_files",
801+
verbose_name=_("Hazard Impact Images"),
802+
related_name="simplified_eap_hazard_impact_images",
739803
blank=True,
740804
)
741805

@@ -745,10 +809,10 @@ class SimplifiedEAP(EAPBaseModel):
745809
blank=True,
746810
)
747811

748-
risk_selected_protocols_file = models.ManyToManyField(
812+
risk_selected_protocols_images = models.ManyToManyField(
749813
EAPFile,
750-
verbose_name=_("Risk Selected Protocols Files"),
751-
related_name="simplified_eap_risk_selected_protocols_files",
814+
verbose_name=_("Risk Selected Protocols Images"),
815+
related_name="simplified_eap_risk_selected_protocols_images",
752816
blank=True,
753817
)
754818

@@ -758,10 +822,10 @@ class SimplifiedEAP(EAPBaseModel):
758822
null=True,
759823
blank=True,
760824
)
761-
selected_early_actions_file = models.ManyToManyField(
825+
selected_early_actions_images = models.ManyToManyField(
762826
EAPFile,
763-
verbose_name=_("Selected Early Actions Files"),
764-
related_name="simplified_eap_selected_early_actions_files",
827+
verbose_name=_("Selected Early Actions Images"),
828+
related_name="simplified_eap_selected_early_actions_images",
765829
blank=True,
766830
)
767831

eap/serializers.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,67 @@ def validate_file(self, file):
193193
return file
194194

195195

196+
# NOTE: Separate serializer for partial updating EAPFile instance
197+
class EAPFileUpdateSerializer(BaseEAPSerializer):
198+
id = serializers.IntegerField(required=True)
199+
file = serializers.FileField(required=False)
200+
201+
class Meta:
202+
model = EAPFile
203+
fields = "__all__"
204+
read_only_fields = (
205+
"created_by",
206+
"modified_by",
207+
)
208+
209+
def validate_file(self, file):
210+
validate_file_type(file)
211+
return file
212+
213+
214+
ALLOWED_MAP_TIMEFRAMES_VALUE = {
215+
OperationActivity.TimeFrame.YEARS: list(OperationActivity.YearsTimeFrameChoices.values),
216+
OperationActivity.TimeFrame.MONTHS: list(OperationActivity.MonthsTimeFrameChoices.values),
217+
OperationActivity.TimeFrame.DAYS: list(OperationActivity.DaysTimeFrameChoices.values),
218+
OperationActivity.TimeFrame.HOURS: list(OperationActivity.HoursTimeFrameChoices.values),
219+
}
220+
221+
196222
class OperationActivitySerializer(
197223
serializers.ModelSerializer,
198224
):
199225
id = serializers.IntegerField(required=False)
226+
timeframe = serializers.ChoiceField(
227+
choices=OperationActivity.TimeFrame.choices,
228+
required=True,
229+
)
230+
time_value = serializers.ListField(
231+
child=serializers.IntegerField(),
232+
required=True,
233+
)
200234

201235
class Meta:
202236
model = OperationActivity
203237
fields = "__all__"
204238

239+
# NOTE: Custom validation for `timeframe` and `time_value`
240+
# Make sure time_value is within the allowed range for the selected timeframe
241+
def validate(self, validated_data: dict[str, typing.Any]) -> dict[str, typing.Any]:
242+
timeframe = validated_data["timeframe"]
243+
time_value = validated_data["time_value"]
244+
245+
allowed_values = ALLOWED_MAP_TIMEFRAMES_VALUE.get(timeframe, [])
246+
invalid_values = [value for value in time_value if value not in allowed_values]
247+
248+
if invalid_values:
249+
raise serializers.ValidationError(
250+
{
251+
"time_value": gettext("Invalid time_value(s) %s for the selected timeframe %s.")
252+
% (invalid_values, OperationActivity.TimeFrame(timeframe).label)
253+
}
254+
)
255+
return validated_data
256+
205257

206258
class PlannedOperationSerializer(
207259
NestedUpdateMixin,
@@ -252,35 +304,35 @@ class SimplifiedEAPSerializer(
252304
enable_approaches = EnableApproachSerializer(many=True, required=False)
253305

254306
# FILES
255-
cover_image_details = EAPFileSerializer(source="cover_image", read_only=True)
256-
hazard_impact_file_details = EAPFileSerializer(source="hazard_impact_file", many=True, read_only=True)
257-
selected_early_actions_file_details = EAPFileSerializer(source="selected_early_actions_file", many=True, read_only=True)
258-
risk_selected_protocols_file_details = EAPFileSerializer(source="risk_selected_protocols_file", many=True, read_only=True)
307+
cover_image_file = EAPFileUpdateSerializer(source="cover_image", required=False, allow_null=True)
308+
hazard_impact_images_details = EAPFileSerializer(source="hazard_impact_images", many=True, read_only=True)
309+
selected_early_actions_file_details = EAPFileSerializer(source="selected_early_actions_images", many=True, read_only=True)
310+
risk_selected_protocols_file_details = EAPFileSerializer(source="risk_selected_protocols_images", many=True, read_only=True)
259311

260312
# Admin2
261313
admin2_details = Admin2Serializer(source="admin2", many=True, read_only=True)
262314

263315
class Meta:
264316
model = SimplifiedEAP
265-
fields = "__all__"
266317
read_only_fields = [
267318
"version",
268319
"is_locked",
269320
]
321+
exclude = ("cover_image",)
270322

271-
def validate_hazard_impact_file(self, images):
323+
def validate_hazard_impact_images(self, images):
272324
if images and len(images) > self.MAX_NUMBER_OF_IMAGES:
273325
raise serializers.ValidationError(f"Maximum {self.MAX_NUMBER_OF_IMAGES} images are allowed to upload.")
274326
validate_file_type(images)
275327
return images
276328

277-
def validate_risk_selected_protocols_file(self, images):
329+
def validate_risk_selected_protocols_images(self, images):
278330
if images and len(images) > self.MAX_NUMBER_OF_IMAGES:
279331
raise serializers.ValidationError(f"Maximum {self.MAX_NUMBER_OF_IMAGES} images are allowed to upload.")
280332
validate_file_type(images)
281333
return images
282334

283-
def validate_selected_early_actions_file(self, images):
335+
def validate_selected_early_actions_images(self, images):
284336
if images and len(images) > self.MAX_NUMBER_OF_IMAGES:
285337
raise serializers.ValidationError(f"Maximum {self.MAX_NUMBER_OF_IMAGES} images are allowed to upload.")
286338
validate_file_type(images)

0 commit comments

Comments
 (0)