Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proposed action fields and validation #2371

7 changes: 7 additions & 0 deletions dref/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
IdentifiedNeed,
NationalSocietyAction,
PlannedIntervention,
ProposedAction,
RiskSecurity,
SourceInformation,
)
Expand Down Expand Up @@ -86,6 +87,7 @@ class DrefAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdmin):
"needs_identified",
"planned_interventions",
"risk_security",
"proposed_action",
)

def get_queryset(self, request):
Expand Down Expand Up @@ -216,3 +218,8 @@ def get_queryset(self, request):
"dref__needs_identified",
)
)


@admin.register(ProposedAction)
class ProposedActionAdmin(ReadOnlyMixin, admin.ModelAdmin):
search_fields = ["action"]
1 change: 1 addition & 0 deletions dref/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
"dref_onset_type": models.Dref.OnsetType,
"dref_disaster_category": models.Dref.DisasterCategory,
"dref_status": models.Dref.Status,
"proposed_action": models.ProposedAction.Action,
}
109 changes: 109 additions & 0 deletions dref/migrations/0076_dref_addressed_humanitarian_impacts_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Generated by Django 4.2.16 on 2025-01-22 06:19

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("deployments", "0090_sectortag_title_ar_sectortag_title_en_and_more"),
("dref", "0075_alter_dref_budget_file_preview_alter_dreffile_file_and_more"),
]

operations = [
migrations.AddField(
model_name="dref",
name="addressed_humanitarian_impacts",
field=models.TextField(
blank=True,
help_text=" Which of the expected severe humanitarian impacts of the hazard are your actions addressing?",
null=True,
verbose_name="Addressed Humanitarian Impacts",
),
),
migrations.AddField(
model_name="dref",
name="contingency_plans_supporting_document",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="dref_contingency_plans_supporting_document",
to="dref.dreffile",
verbose_name="Contingency Plans Supporting Document",
),
),
migrations.AddField(
model_name="dref",
name="hazard_date_and_location",
field=models.TextField(
blank=True,
help_text="When and where is the hazard expected to happen?",
max_length=255,
null=True,
verbose_name="Hazard Date and Location",
),
),
migrations.AddField(
model_name="dref",
name="hazard_vulnerabilities_and_risks",
field=models.TextField(
blank=True,
help_text="Explain the underlying vulnerabilities and risks the hazard poses for at-risk communities?",
null=True,
verbose_name="Hazard Vulnerabilities and Risks",
),
),
migrations.AddField(
model_name="dref",
name="indirect_cost",
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Indirect Cost"),
),
migrations.AddField(
model_name="dref",
name="scenario_analysis_supporting_document",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="dref_scenario_supporting_document",
to="dref.dreffile",
verbose_name="Scenario Analysis Supporting Document",
),
),
migrations.AddField(
model_name="dref",
name="sub_total",
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Sub total"),
),
migrations.AddField(
model_name="dref",
name="surge_deployment_cost",
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Surge Deployment Cost"),
),
migrations.AddField(
model_name="dref",
name="total",
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Total"),
),
migrations.CreateModel(
name="ProposedAction",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"proposed_type",
models.PositiveIntegerField(
choices=[(1, "Early Actions"), (2, "Early Response")], verbose_name="dref proposed action"
),
),
("budget", models.PositiveIntegerField(blank=True, null=True, verbose_name="Purpose Action Budgets")),
("activity", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="deployments.sector")),
],
),
migrations.AddField(
model_name="dref",
name="proposed_action",
field=models.ManyToManyField(blank=True, to="dref.proposedaction", verbose_name="Proposed Action"),
),
]
57 changes: 57 additions & 0 deletions dref/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pdf2image import convert_from_bytes

from api.models import Country, DisasterType, District, FieldReport
from deployments.models import Sector
from main.fields import SecureFileField


Expand Down Expand Up @@ -210,6 +211,22 @@ class SourceInformation(models.Model):
source_link = models.CharField(verbose_name=_("Source Link"), null=True, blank=True, max_length=255)


class ProposedAction(models.Model):
class Action(models.IntegerChoices):
EARLY_ACTION = 1, _("Early Actions")
EARLY_RESPONSE = 2, _("Early Response")

proposed_type = models.PositiveIntegerField(
choices=Action.choices,
verbose_name=_("dref proposed action"),
)
activity = models.ForeignKey(Sector, on_delete=models.CASCADE)
budget = models.PositiveIntegerField(verbose_name=_("Purpose Action Budgets"), blank=True, null=True)

def __str__(self) -> str:
return f"{self.get_proposed_type_display()}-{self.budget}"


@reversion.register()
class Dref(models.Model):
class DrefType(models.IntegerChoices):
Expand Down Expand Up @@ -589,6 +606,46 @@ class Status(models.IntegerChoices):
__budget_file_id = None
is_active = models.BooleanField(verbose_name=_("Is Active"), null=True, blank=True)
source_information = models.ManyToManyField(SourceInformation, blank=True, verbose_name=_("Source Information"))
proposed_action = models.ManyToManyField(ProposedAction, verbose_name=_("Proposed Action"), blank=True)
sub_total = models.PositiveIntegerField(verbose_name=_("Sub total"), blank=True, null=True)
surge_deployment_cost = models.PositiveIntegerField(verbose_name=_("Surge Deployment Cost"), null=True, blank=True)
indirect_cost = models.PositiveIntegerField(verbose_name=_("Indirect Cost"), null=True, blank=True)
total = models.PositiveIntegerField(verbose_name=_("Total"), null=True, blank=True)
hazard_date_and_location = models.TextField(
verbose_name=_("Hazard Date and Location"),
max_length=255,
help_text=_("When and where is the hazard expected to happen?"),
null=True,
blank=True,
)
hazard_vulnerabilities_and_risks = models.TextField(
verbose_name=_("Hazard Vulnerabilities and Risks"),
help_text=_("Explain the underlying vulnerabilities and risks the hazard poses for at-risk communities?"),
null=True,
blank=True,
)
scenario_analysis_supporting_document = models.ForeignKey(
"DrefFile",
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_("Scenario Analysis Supporting Document"),
related_name="dref_scenario_supporting_document",
)
contingency_plans_supporting_document = models.ForeignKey(
"DrefFile",
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_("Contingency Plans Supporting Document"),
related_name="dref_contingency_plans_supporting_document",
)
addressed_humanitarian_impacts = models.TextField(
verbose_name=_("Addressed Humanitarian Impacts"),
help_text=_(" Which of the expected severe humanitarian impacts of the hazard are your actions addressing?"),
null=True,
blank=True,
)

class Meta:
verbose_name = _("dref")
Expand Down
119 changes: 115 additions & 4 deletions dref/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MiniDistrictSerializer,
UserNameSerializer,
)
from deployments.models import Sector
from dref.models import (
Dref,
DrefFile,
Expand All @@ -25,6 +26,7 @@
NationalSocietyAction,
PlannedIntervention,
PlannedInterventionIndicators,
ProposedAction,
RiskSecurity,
SourceInformation,
)
Expand Down Expand Up @@ -54,6 +56,17 @@ class Meta:
fields = "__all__"


class ProposedActionSerializer(serializers.ModelSerializer):

proposed_type_display = serializers.CharField(source="get_proposed_type_display", read_only=True)
activity = serializers.PrimaryKeyRelatedField(queryset=Sector.objects.all(), required=True)
budget = serializers.IntegerField(required=True)

class Meta:
model = ProposedAction
fields = "__all__"


class DrefFileInputSerializer(serializers.Serializer):
file = serializers.ListField(child=serializers.FileField())

Expand Down Expand Up @@ -332,6 +345,10 @@ class Meta:


class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer):
SUB_TOTAL = 75000
SURGE_DEPLOYMENT_COST = 10000
INDIRECT_COST_SURGE = 5800
INDIRECT_COST_NO_SURGE = 5000
MAX_NUMBER_OF_IMAGES = 2
ALLOWED_BUDGET_FILE_EXTENSIONS = ["pdf"]
ALLOWED_ASSESSMENT_REPORT_EXTENSIONS = ["pdf", "docx", "pptx"]
Expand Down Expand Up @@ -371,10 +388,22 @@ class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer):
modified_at = serializers.DateTimeField(required=False)
dref_access_user_list = serializers.SerializerMethodField()
source_information = SourceInformationSerializer(many=True, required=False)
scenario_analysis_supporting_document_details = DrefFileSerializer(
source="scenario_analysis_supporting_document", read_only=True, required=False, allow_null=True
)
contingency_plans_supporting_document_details = DrefFileSerializer(
source="contingency_plans_supporting_document", read_only=True, required=False, allow_null=True
)

proposed_action = ProposedActionSerializer(many=True, required=False)

class Meta:
model = Dref
read_only_fields = ("modified_by", "created_by", "budget_file_preview")
read_only_fields = (
"modified_by",
"created_by",
"budget_file_preview",
)
exclude = (
"cover_image",
"event_map",
Expand Down Expand Up @@ -428,6 +457,74 @@ def validate(self, data):
raise serializers.ValidationError(
gettext("Operation timeframe can't be greater than %s for assessment_report" % self.MAX_OPERATION_TIMEFRAME)
)

# NOTE: Validation for type DREF Imminent
if data.get("type_of_dref") == Dref.DrefType.IMMINENT:
is_surge_personnel_deployed = data.get("is_surge_personnel_deployed")
sub_total = data.get("sub_total")
surge_deployment_cost = data.get("surge_deployment_cost")
indirect_cost = data.get("indirect_cost")
total = data.get("total")
proposed_actions = data.get("proposed_action", [])

if not proposed_actions:
raise serializers.ValidationError(
{"proposed_action": gettext("Proposed Action is required for type DREF Imminent")}
)
if not sub_total:
raise serializers.ValidationError({"sub_total": gettext("Sub-total is required for Imminent DREF")})
if sub_total != self.SUB_TOTAL:
raise serializers.ValidationError(
{"sub_total": gettext("Sub-total should be equal to %s for Imminent DREF" % self.SUB_TOTAL)}
)
if is_surge_personnel_deployed and not surge_deployment_cost:
raise serializers.ValidationError(
{"surge_deployment_cost": gettext("Surge Deployment is required for Imminent DREF")}
)
if not indirect_cost:
raise serializers.ValidationError({"indirect_cost": gettext("Indirect Cost is required for Imminent DREF")})
if not total:
raise serializers.ValidationError({"total": gettext("Total is required for Imminent DREF")})

proposed_budget = sum(action.get("budget", 0) for action in proposed_actions)
if proposed_budget != sub_total:
raise serializers.ValidationError("Sub-total should be equal to proposed budget")

if is_surge_personnel_deployed:
if surge_deployment_cost != self.SURGE_DEPLOYMENT_COST:
raise serializers.ValidationError(
{
"surge_deployment_cost": gettext(
"Surge Deployment Cost should be equal to %s for Surge Personnel Deployed"
% self.SURGE_DEPLOYMENT_COST
)
}
)
if indirect_cost != self.INDIRECT_COST_SURGE:
raise serializers.ValidationError(
{
"indirect_cost": gettext(
"Indirect Cost should be equal to %s for Surge Personnel Deployed" % self.INDIRECT_COST_SURGE
)
}
)
expected_total = surge_deployment_cost + indirect_cost + sub_total
else:
if indirect_cost != self.INDIRECT_COST_NO_SURGE:
raise serializers.ValidationError(
{
"indirect_cost": gettext(
"Indirect Cost should be equal to %s for No Surge Personnel Deployed"
% self.INDIRECT_COST_NO_SURGE
)
}
)
expected_total = indirect_cost + sub_total

if expected_total != total:
raise serializers.ValidationError(
{"total": gettext("Total should be equal to sum of Sub-total, Surge Deployment Cost and Indirect Cost")}
)
Comment on lines +461 to +527
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets make it clear about total variable weather it is total_cost or something else. Same for other variable.. like expected_total, etc, Also update the validationError after renaming the respective variable.

return data

def validate_images(self, images):
Expand Down Expand Up @@ -590,8 +687,18 @@ class DrefOperationalUpdateSerializer(NestedUpdateMixin, NestedCreateMixin, Mode

class Meta:
model = DrefOperationalUpdate
read_only_fields = ("operational_update_number", "modified_by", "created_by")
exclude = ("images", "photos", "event_map", "cover_image", "users")
read_only_fields = (
"operational_update_number",
"modified_by",
"created_by",
)
exclude = (
"images",
"photos",
"event_map",
"cover_image",
"users",
)

def validate(self, data):
dref = data.get("dref")
Expand Down Expand Up @@ -904,7 +1011,11 @@ class DrefFinalReportSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSeria

class Meta:
model = DrefFinalReport
read_only_fields = ("modified_by", "created_by", "financial_report_preview")
read_only_fields = (
"modified_by",
"created_by",
"financial_report_preview",
)
exclude = (
"images",
"photos",
Expand Down
Loading
Loading