diff --git a/api/admin.py b/api/admin.py
index 09de0002f..10754a214 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -1,4 +1,5 @@
import csv
+import json
import time
from django.conf import settings
@@ -11,6 +12,8 @@
from django.db.models.functions import Concat
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
+from django.utils import timezone
+from django.utils.formats import date_format
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
@@ -199,6 +202,11 @@ class EventLinkInline(admin.TabularInline, TranslationInlineModelAdmin):
class EventAdmin(CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin):
+
+ @admin.display(ordering="ifrc_severity_level_update_date")
+ def level_updated_at(self, obj):
+ return obj.ifrc_severity_level_update_date
+
country_in = "countries__pk__in"
region_in = "regions__pk__in"
@@ -212,7 +220,9 @@ class EventAdmin(CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin):
]
list_display = (
"name",
- "ifrc_severity_level",
+ "severity_level_link",
+ "level_updated_at",
+ "cc_status",
"glide",
"auto_generated",
"auto_generated_source",
@@ -229,6 +239,45 @@ class EventAdmin(CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin):
"parent_event",
)
+ def severity_level_link(self, obj):
+ """Display severity level as a link to Crisis Categorisation"""
+ # Check if there's an existing crisis categorisation for this event
+ first_crisis_cat = models.CrisisCategorisationByCountry.objects.filter(event=obj).first()
+
+ if first_crisis_cat:
+ # Link to existing record
+ url = reverse("admin:api_crisiscategorisationbycountry_change", args=[first_crisis_cat.pk])
+ title = "Edit crisis categorisation"
+ else:
+ # Link to add new record with event pre-filled
+ url = (
+ reverse("admin:api_crisiscategorisationbycountry_add")
+ + f"?event={obj.pk}"
+ + f"&crisis_categorisation={obj.ifrc_severity_level}"
+ )
+ title = "Edit crisis categorisation"
+
+ severity_display = obj.get_ifrc_severity_level_display()
+ severity_emoji_map = {
+ 0: "🟡",
+ 1: "🟠",
+ 2: "🔴",
+ }
+ severity_emoji = severity_emoji_map.get(obj.ifrc_severity_level)
+ if severity_display and severity_emoji:
+ severity_display = f"{severity_emoji} {severity_display}"
+ return format_html('{}', url, title, severity_display)
+
+ severity_level_link.short_description = "Crisis Categorisation"
+ severity_level_link.admin_order_field = "ifrc_severity_level"
+
+ @admin.display(description="CC Status")
+ def cc_status(self, obj):
+ latest_cc = models.CrisisCategorisationByCountry.objects.filter(event=obj).order_by("-updated_at").first()
+ if not latest_cc or latest_cc.status is None:
+ return "-"
+ return latest_cc.get_status_display()
+
def appeals(self, instance):
if getattr(instance, "appeals").exists():
return format_html_join(
@@ -241,10 +290,10 @@ def appeals(self, instance):
# To add the 'Notify subscribers now' button
# WikiJS links added
change_form_template = "admin/emergency_change_form.html"
- change_list_template = "admin/emergency_change_list.html"
+ change_list_template = "admin/emergency_change_list_with_history.html"
# Overwriting readonly fields for Edit mode
- def changeform_view(self, request, *args, **kwargs):
+ def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if not request.user.is_superuser:
self.readonly_fields = (
"appeals",
@@ -259,7 +308,37 @@ def changeform_view(self, request, *args, **kwargs):
"auto_generated_source",
)
- return super(EventAdmin, self).changeform_view(request, *args, **kwargs)
+ # Set severity level from GET parameter
+ if object_id and request.GET.get("set_ifrc_severity_level"):
+ # Not good, because this way no history will be saved:
+ try:
+ severity_level = int(request.GET.get("set_ifrc_severity_level"))
+ if severity_level in [0, 1, 2]: # Validate it's a valid choice
+ obj = self.get_object(request, object_id)
+ if obj:
+ original = models.Event.objects.get(pk=obj.pk)
+ severity_changed = original.ifrc_severity_level != severity_level
+ if severity_changed:
+ new_update_date = timezone.now()
+ if (
+ original.ifrc_severity_level_update_date is not None
+ and original.ifrc_severity_level_update_date > new_update_date
+ ):
+ messages.error(request, "A severity level update date can not be earlier than the previous one.")
+ else:
+ obj.ifrc_severity_level = severity_level
+ obj.ifrc_severity_level_update_date = new_update_date
+ obj.save(update_fields=["ifrc_severity_level", "ifrc_severity_level_update_date"])
+ models.EventSeverityLevelHistory.objects.create(
+ event=obj,
+ ifrc_severity_level=original.ifrc_severity_level,
+ ifrc_severity_level_update_date=original.ifrc_severity_level_update_date,
+ created_by=request.user,
+ )
+ except (ValueError, TypeError):
+ pass
+
+ return super(EventAdmin, self).changeform_view(request, object_id, form_url, extra_context)
# Evaluate if the regular 'Save' or 'Notify subscribers now' button was pushed
def response_change(self, request, obj):
@@ -314,6 +393,53 @@ def field_reports(self, instance):
field_reports.short_description = "Field Reports"
+ def changelist_view(self, request, extra_context=None):
+ response = super().changelist_view(request, extra_context)
+
+ # Check if we have a result list to process
+ if hasattr(response, "context_data") and "cl" in response.context_data:
+ cl = response.context_data["cl"]
+ result_list = list(cl.result_list)
+ expanded_results = []
+
+ for event in result_list:
+ # Add the main event row
+ expanded_results.append(
+ {
+ "object": event,
+ "is_history": False,
+ "history_record": None,
+ }
+ )
+
+ # Add history rows if they exist
+ history_records = models.EventSeverityLevelHistory.objects.filter(event=event).order_by(
+ "-ifrc_severity_level_update_date"
+ )
+
+ for history in history_records:
+ expanded_results.append(
+ {
+ "object": event,
+ "is_history": True,
+ "history_record": {
+ "date": (
+ date_format(history.ifrc_severity_level_update_date, "N j, Y, g:i a")
+ if history.ifrc_severity_level_update_date
+ else ""
+ ),
+ "severity": history.get_ifrc_severity_level_display(),
+ "severity_value": history.ifrc_severity_level,
+ },
+ }
+ )
+
+ # Convert to JSON for JavaScript
+
+ response.context_data["expanded_results"] = json.dumps(expanded_results, default=str)
+
+ return response
+
class GdacsAdmin(CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin):
country_in = "countries__pk__in"
@@ -1063,6 +1189,396 @@ def updated_at(self, obj):
)
+@admin.register(models.CrisisCategorisationByCountry)
+class CrisisCategorisationByCountryAdmin(admin.ModelAdmin):
+ list_display = [
+ "event",
+ "country",
+ "crisis_categorisation",
+ "crisis_score",
+ "status",
+ "updated_at",
+ ]
+ list_filter = ["crisis_categorisation", "event"]
+ list_select_related = ["event", "country"]
+ search_fields = ["event__name", "country__name"]
+ autocomplete_fields = ["event"]
+ readonly_fields = [
+ "created_at",
+ "updated_at",
+ "event_countries_overview",
+ "pre_crisis_vulnerability",
+ "crisis_complexity",
+ "scope_and_scale",
+ "humanitarian_conditions",
+ "capacity_and_response",
+ # "pre_crisis_vulnerability_hazard_exposure_intermediate",
+ # "pre_crisis_vulnerability_vulnerability_intermediate",
+ # "pre_crisis_vulnerability_coping_mechanism_intermediate",
+ # "crisis_complexity_humanitarian_access_acaps",
+ # "scope_and_scale_number_of_affected_population",
+ # "scope_and_scale_total_population_of_the_affected_area",
+ # "scope_and_scale_percentage_affected_population",
+ # "scope_and_scale_impact_index_inform",
+ # "humanitarian_conditions_casualties_injrd_deaths_missing",
+ # "humanitarian_conditions_severity",
+ # "humanitarian_conditions_people_in_need",
+ # "capacity_and_response_ifrc_international_staff",
+ # "capacity_and_response_ifrc_national_staff",
+ # "capacity_and_response_ifrc_total_staff",
+ # "capacity_and_response_regional_office",
+ # "capacity_and_response_ops_capacity_ranking",
+ # "capacity_and_response_number_of_ns_staff",
+ # "capacity_and_response_ratio_staff_volunteer",
+ # "capacity_and_response_number_of_ns_volunteer",
+ # "capacity_and_response_number_of_dref_ea_last_3_years",
+ ]
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj: # This is a "Change" – we do not allow to change the country
+ return self.readonly_fields + ["event", "country"]
+ return self.readonly_fields
+
+ class Media:
+ js = ("js/crisis_categorisation_headers.js",)
+
+ def event_countries_overview(self, obj):
+ """Display a table of all countries for this event with their crisis scores"""
+ if not obj.event:
+ return ""
+
+ # Get all crisis categorisations for this event
+ categorisations = (
+ models.CrisisCategorisationByCountry.objects.filter(event=obj.event)
+ .select_related("country")
+ .order_by("country__name")
+ )
+
+ if not categorisations.exists():
+ return mark_safe('
No countries categorised for this event yet.
')
+
+ # Build HTML table
+ html = """
+
+
+
+
+ | Country |
+ CC |
+ Crisis score |
+ 1. Pre-crisis vulnerability |
+ 2. Crisis complexity |
+ 3. Scope & scale |
+ 4. Humanitarian conditions |
+ 5. Capacity & response |
+
+
+
+ """
+
+ for cat in categorisations:
+ cc_display = cat.get_crisis_categorisation_display() if cat.crisis_categorisation is not None else "-"
+ cc_emoji_map = {
+ 0: "🟡",
+ 1: "🟠",
+ 2: "🔴",
+ }
+ cc_emoji = cc_emoji_map.get(cat.crisis_categorisation)
+ if cc_display != "-" and cc_emoji:
+ cc_display = f"{cc_emoji} {cc_display}"
+
+ # Highlight current country
+ row_class = "current-country" if cat.country == obj.country else ""
+ change_url = f"../../{cat.pk}/change/"
+
+ html += f"""
+
+ | {cat.country.name} |
+ {cc_display} |
+ {cat.crisis_score if cat.crisis_score else "-"} |
+ {cat.pre_crisis_vulnerability if cat.pre_crisis_vulnerability else "-"} |
+ {cat.crisis_complexity if cat.crisis_complexity else "-"} |
+ {cat.scope_and_scale if cat.scope_and_scale else "-"} |
+ {cat.humanitarian_conditions if cat.humanitarian_conditions else "-"} |
+ {cat.capacity_and_response if cat.capacity_and_response else "-"} |
+
+ """
+
+ # Add final row with averages if there are multiple countries
+ if categorisations.count() > 1:
+ # Calculate averages
+ def calc_avg(field_name):
+ values = [getattr(cat, field_name) for cat in categorisations if getattr(cat, field_name) is not None]
+ if values:
+ return round(sum(values) / len(values), 2)
+ return "-"
+
+ avg_crisis_score = calc_avg("crisis_score")
+ avg_pre_crisis = calc_avg("pre_crisis_vulnerability")
+ avg_crisis_complexity = calc_avg("crisis_complexity")
+ avg_scope_scale = calc_avg("scope_and_scale")
+ avg_humanitarian = calc_avg("humanitarian_conditions")
+ avg_capacity = calc_avg("capacity_and_response")
+
+ # Create links for Y, O, R to event page with severity level filters
+ event_url_base = f"../../../event/{obj.event.pk}/change/"
+ y_link = (
+ f'Yellow'
+ )
+ o_link = (
+ f'Orange'
+ )
+ r_link = (
+ f'Red'
+ )
+
+ if obj.status > 2:
+ finalize = f"Categorise emergency as {y_link} / {o_link} / {r_link}"
+ else:
+ finalize = "Get this categorisation approved."
+
+ html += f"""
+
+ | Summary |
+ {finalize} |
+ {avg_crisis_score} |
+ {avg_pre_crisis} |
+ {avg_crisis_complexity} |
+ {avg_scope_scale} |
+ {avg_humanitarian} |
+ {avg_capacity} |
+
+ """
+
+ html += """
+
+
+ """
+
+ return mark_safe(html)
+
+ event_countries_overview.short_description = mark_safe("" + _("Crisis Categorisation Overview") + "")
+
+ fieldsets = (
+ (
+ None,
+ {
+ "fields": (
+ "event",
+ "event_countries_overview",
+ "country",
+ "crisis_categorisation",
+ "crisis_score",
+ )
+ },
+ ),
+ (
+ "1. Pre-Crisis Vulnerability",
+ {
+ "fields": (
+ "pre_crisis_vulnerability",
+ ("pre_crisis_vulnerability_hazard_exposure", "pre_crisis_vulnerability_hazard_exposure_comment"),
+ (
+ "pre_crisis_vulnerability_hazard_exposure_intermediate",
+ "pre_crisis_vulnerability_hazard_exposure_intermediate_comment",
+ ),
+ ("pre_crisis_vulnerability_vulnerability", "pre_crisis_vulnerability_vulnerability_comment"),
+ (
+ "pre_crisis_vulnerability_vulnerability_intermediate",
+ "pre_crisis_vulnerability_vulnerability_intermediate_comment",
+ ),
+ ("pre_crisis_vulnerability_coping_mechanism", "pre_crisis_vulnerability_coping_mechanism_comment"),
+ (
+ "pre_crisis_vulnerability_coping_mechanism_intermediate",
+ "pre_crisis_vulnerability_coping_mechanism_intermediate_comment",
+ ),
+ )
+ },
+ ),
+ (
+ "2. Crisis Complexity",
+ {
+ "fields": (
+ "crisis_complexity",
+ ("crisis_complexity_humanitarian_access_score", "crisis_complexity_humanitarian_access_score_comment"),
+ ("crisis_complexity_humanitarian_access_acaps", "crisis_complexity_humanitarian_access_acaps_comment"),
+ ("crisis_complexity_government_response", "crisis_complexity_government_response_comment"),
+ ("crisis_complexity_media_attention", "crisis_complexity_media_attention_comment"),
+ ("crisis_complexity_ifrc_security_phase", "crisis_complexity_ifrc_security_phase_comment"),
+ )
+ },
+ ),
+ (
+ "3. Scope & Scale",
+ {
+ "fields": (
+ "scope_and_scale",
+ (
+ "scope_and_scale_number_of_affected_population_score",
+ "scope_and_scale_number_of_affected_population_score_comment",
+ ),
+ (
+ "scope_and_scale_number_of_affected_population",
+ "scope_and_scale_number_of_affected_population_comment",
+ ),
+ (
+ "scope_and_scale_percentage_affected_population_score",
+ "scope_and_scale_percentage_affected_population_score_comment",
+ ),
+ (
+ "scope_and_scale_total_population_of_the_affected_area",
+ "scope_and_scale_total_population_of_the_affected_area_comment",
+ ),
+ (
+ "scope_and_scale_percentage_affected_population",
+ "scope_and_scale_percentage_affected_population_comment",
+ ),
+ ("scope_and_scale_impact_index_score", "scope_and_scale_impact_index_score_comment"),
+ ("scope_and_scale_impact_index_inform", "scope_and_scale_impact_index_inform_comment"),
+ )
+ },
+ ),
+ (
+ "4. Humanitarian Conditions",
+ {
+ "fields": (
+ "humanitarian_conditions",
+ ("humanitarian_conditions_casualties_score", "humanitarian_conditions_casualties_score_comment"),
+ (
+ "humanitarian_conditions_casualties_injrd_deaths_missing",
+ "humanitarian_conditions_casualties_injrd_deaths_missing_comment",
+ ),
+ ("humanitarian_conditions_severity_score", "humanitarian_conditions_severity_score_comment"),
+ ("humanitarian_conditions_severity", "humanitarian_conditions_severity_comment"),
+ ("humanitarian_conditions_people_in_need_score", "humanitarian_conditions_people_in_need_score_comment"),
+ ("humanitarian_conditions_people_in_need", "humanitarian_conditions_people_in_need_comment"),
+ )
+ },
+ ),
+ (
+ "5. Capacity & Response",
+ {
+ "fields": (
+ "capacity_and_response",
+ ("capacity_and_response_ifrc_capacity_score", "capacity_and_response_ifrc_capacity_score_comment"),
+ ("capacity_and_response_ifrc_international_staff", "capacity_and_response_ifrc_international_staff_comment"),
+ ("capacity_and_response_ifrc_national_staff", "capacity_and_response_ifrc_national_staff_comment"),
+ ("capacity_and_response_ifrc_total_staff", "capacity_and_response_ifrc_total_staff_comment"),
+ ("capacity_and_response_regional_office", "capacity_and_response_regional_office_comment"),
+ ("capacity_and_response_ops_capacity_score", "capacity_and_response_ops_capacity_score_comment"),
+ ("capacity_and_response_ops_capacity_ranking", "capacity_and_response_ops_capacity_ranking_comment"),
+ ("capacity_and_response_ns_staff_score", "capacity_and_response_ns_staff_score_comment"),
+ ("capacity_and_response_number_of_ns_staff", "capacity_and_response_number_of_ns_staff_comment"),
+ (
+ "capacity_and_response_ratio_staff_to_volunteer_score",
+ "capacity_and_response_ratio_staff_to_volunteer_score_comment",
+ ),
+ ("capacity_and_response_ratio_staff_volunteer", "capacity_and_response_ratio_staff_volunteer_comment"),
+ ("capacity_and_response_number_of_ns_volunteer", "capacity_and_response_number_of_ns_volunteer_comment"),
+ ("capacity_and_response_number_of_dref_score", "capacity_and_response_number_of_dref_score_comment"),
+ (
+ "capacity_and_response_number_of_dref_ea_last_3_years",
+ "capacity_and_response_number_of_dref_ea_last_3_years_comment",
+ ),
+ (
+ "capacity_and_response_presence_support_pns_in_country",
+ "capacity_and_response_presence_support_pns_in_country_comment",
+ ),
+ )
+ },
+ ),
+ (
+ "––––––––––––––––––––",
+ {"fields": ("commentary", "general_document", "status")},
+ ),
+ (
+ "Timestamps",
+ {
+ "fields": ("created_at", "updated_at"),
+ "classes": ("collapse",),
+ },
+ ),
+ )
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related("event", "country")
+
+
@admin.register(models.Export)
class ExportTokenAdmin(admin.ModelAdmin):
pass
diff --git a/api/migrations/0228_crisiscategorisationbycountry.py b/api/migrations/0228_crisiscategorisationbycountry.py
new file mode 100644
index 000000000..c220397fc
--- /dev/null
+++ b/api/migrations/0228_crisiscategorisationbycountry.py
@@ -0,0 +1,120 @@
+# Generated by Django 4.2.27 on 2026-02-05 13:34
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import smart_selects.db_fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0227_alter_eventseveritylevelhistory_options'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CrisisCategorisationByCountry',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('crisis_categorisation', models.IntegerField(blank=True, choices=[(0, 'Yellow'), (1, 'Orange'), (2, 'Red')], null=True, verbose_name='crisis categorisation')),
+ ('crisis_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='crisis score')),
+ ('pre_crisis_vulnerability', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='pre-crisis vulnerability')),
+ ('pre_crisis_vulnerability_hazard_exposure', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='hazard exposure')),
+ ('pre_crisis_vulnerability_hazard_exposure_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('pre_crisis_vulnerability_hazard_exposure_intermediate', models.DecimalField(blank=True, decimal_places=1, help_text='Not counted for the score, just FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='hazard exposure (intermediate score)')),
+ ('pre_crisis_vulnerability_hazard_exposure_intermediate_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('pre_crisis_vulnerability_vulnerability', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='vulnerability')),
+ ('pre_crisis_vulnerability_vulnerability_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('pre_crisis_vulnerability_vulnerability_intermediate', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='vulnerability (intermediate score)')),
+ ('pre_crisis_vulnerability_vulnerability_intermediate_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('pre_crisis_vulnerability_coping_mechanism', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='coping mechanism')),
+ ('pre_crisis_vulnerability_coping_mechanism_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('pre_crisis_vulnerability_coping_mechanism_intermediate', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='coping mechanism (intermediate score)')),
+ ('pre_crisis_vulnerability_coping_mechanism_intermediate_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('crisis_complexity', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='crisis complexity')),
+ ('crisis_complexity_humanitarian_access_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='humanitarian access score')),
+ ('crisis_complexity_humanitarian_access_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('crisis_complexity_humanitarian_access_acaps', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Humanitarian access ACAPS (complex crisis)')),
+ ('crisis_complexity_humanitarian_access_acaps_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('crisis_complexity_government_response', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='government response')),
+ ('crisis_complexity_government_response_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('crisis_complexity_media_attention', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='media attention')),
+ ('crisis_complexity_media_attention_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('crisis_complexity_ifrc_security_phase', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC security phase')),
+ ('crisis_complexity_ifrc_security_phase_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='scope & scale')),
+ ('scope_and_scale_number_of_affected_population_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# Affected population score')),
+ ('scope_and_scale_number_of_affected_population_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale_number_of_affected_population', models.IntegerField(blank=True, help_text='FYI', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000)], verbose_name='# Affected population')),
+ ('scope_and_scale_number_of_affected_population_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale_percentage_affected_population_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='% Affected population score')),
+ ('scope_and_scale_percentage_affected_population_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale_total_population_of_the_affected_area', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='total population of the affected area *')),
+ ('scope_and_scale_total_population_of_the_affected_area_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale_percentage_affected_population', models.DecimalField(blank=True, decimal_places=2, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='% of Affected population')),
+ ('scope_and_scale_percentage_affected_population_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale_impact_index_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='impact index score')),
+ ('scope_and_scale_impact_index_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('scope_and_scale_impact_index_inform', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='impact index INFORM')),
+ ('scope_and_scale_impact_index_inform_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('humanitarian_conditions', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='humanitarian conditions')),
+ ('humanitarian_conditions_casualties_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='casualties score')),
+ ('humanitarian_conditions_casualties_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('humanitarian_conditions_casualties_injrd_deaths_missing', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='casualties injured + deaths + missing')),
+ ('humanitarian_conditions_casualties_injrd_deaths_missing_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('humanitarian_conditions_severity_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='severity score *')),
+ ('humanitarian_conditions_severity_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('humanitarian_conditions_severity', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='severity')),
+ ('humanitarian_conditions_severity_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('humanitarian_conditions_people_in_need_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# People in need score *')),
+ ('humanitarian_conditions_people_in_need_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('humanitarian_conditions_people_in_need', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# People in need')),
+ ('humanitarian_conditions_people_in_need_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='capacity & response')),
+ ('capacity_and_response_ifrc_capacity_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC capacity score')),
+ ('capacity_and_response_ifrc_capacity_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ifrc_international_staff', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC International staff')),
+ ('capacity_and_response_ifrc_international_staff_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ifrc_national_staff', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC National staff')),
+ ('capacity_and_response_ifrc_national_staff_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ifrc_total_staff', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC Total staff')),
+ ('capacity_and_response_ifrc_total_staff_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_regional_office', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Regional Office')),
+ ('capacity_and_response_regional_office_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ops_capacity_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='OPS Capacity score')),
+ ('capacity_and_response_ops_capacity_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ops_capacity_ranking', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Ops capacity Ranking (IFRC Global Risk - 2021)')),
+ ('capacity_and_response_ops_capacity_ranking_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ns_staff_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='NS staff score')),
+ ('capacity_and_response_ns_staff_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_number_of_ns_staff', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Number of NS staff (FDRS) 2025')),
+ ('capacity_and_response_number_of_ns_staff_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ratio_staff_to_volunteer_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Ratio staff to volunteer score')),
+ ('capacity_and_response_ratio_staff_to_volunteer_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_ratio_staff_volunteer', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Ratio staff volunteer')),
+ ('capacity_and_response_ratio_staff_volunteer_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_number_of_ns_volunteer', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Number of NS volunteer (FDRS)')),
+ ('capacity_and_response_number_of_ns_volunteer_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_number_of_dref_score', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# DREF Score')),
+ ('capacity_and_response_number_of_dref_score_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_number_of_dref_ea_last_3_years', models.DecimalField(blank=True, decimal_places=1, help_text='FYI', max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# DREF and EA in the last 3 years')),
+ ('capacity_and_response_number_of_dref_ea_last_3_years_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('capacity_and_response_presence_support_pns_in_country', models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Expert Judgement: on active presence and support of PNS in country')),
+ ('capacity_and_response_presence_support_pns_in_country_comment', models.CharField(blank=True, max_length=999, null=True, verbose_name='')),
+ ('commentary', models.TextField(blank=True, null=True, verbose_name='Add commentary')),
+ ('status', models.IntegerField(choices=[(0, 'In progress'), (1, 'Draft'), (2, 'Pending validation'), (3, 'Validated'), (4, 'Published'), (5, 'Merged')], default=0, verbose_name='status')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
+ ('country', smart_selects.db_fields.ChainedForeignKey(auto_choose=True, chained_field='event', chained_model_field='event', on_delete=django.db.models.deletion.CASCADE, to='api.country')),
+ ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crisis_categorisations', to='api.event', verbose_name='event')),
+ ('general_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='crisis_categorisations', to='api.generaldocument', verbose_name='File attachment')),
+ ],
+ options={
+ 'verbose_name': 'crisis categorisation by country',
+ 'verbose_name_plural': 'crisis categorisations by country',
+ 'ordering': ('event', 'country'),
+ 'unique_together': {('event', 'country')},
+ },
+ ),
+ ]
diff --git a/api/migrations/0229_alter_crisiscategorisationbycountry_capacity_and_response_ifrc_international_staff_and_more.py b/api/migrations/0229_alter_crisiscategorisationbycountry_capacity_and_response_ifrc_international_staff_and_more.py
new file mode 100644
index 000000000..76c1b0ce5
--- /dev/null
+++ b/api/migrations/0229_alter_crisiscategorisationbycountry_capacity_and_response_ifrc_international_staff_and_more.py
@@ -0,0 +1,114 @@
+# Generated by Django 4.2.27 on 2026-02-05 16:58
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0228_crisiscategorisationbycountry'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_ifrc_international_staff',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC International staff'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_ifrc_national_staff',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC National staff'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_ifrc_total_staff',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='IFRC Total staff'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_number_of_dref_ea_last_3_years',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# DREF and EA in the last 3 years'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_number_of_ns_staff',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Number of NS staff (FDRS) 2025'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_number_of_ns_volunteer',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Number of NS volunteer (FDRS)'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_ops_capacity_ranking',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Ops capacity Ranking (IFRC Global Risk - 2021)'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_ratio_staff_volunteer',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Ratio staff volunteer'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='capacity_and_response_regional_office',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Regional Office'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='crisis_complexity_humanitarian_access_acaps',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='Humanitarian access ACAPS (complex crisis)'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='humanitarian_conditions_casualties_injrd_deaths_missing',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='casualties injured + deaths + missing'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='humanitarian_conditions_people_in_need',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='# People in need'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='humanitarian_conditions_severity',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='severity'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='pre_crisis_vulnerability_coping_mechanism_intermediate',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='coping mechanism (intermediate score)'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='pre_crisis_vulnerability_hazard_exposure_intermediate',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='hazard exposure (intermediate score)'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='pre_crisis_vulnerability_vulnerability_intermediate',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='vulnerability (intermediate score)'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='scope_and_scale_impact_index_inform',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='impact index INFORM'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='scope_and_scale_number_of_affected_population',
+ field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000)], verbose_name='# Affected population'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='scope_and_scale_percentage_affected_population',
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='% of Affected population'),
+ ),
+ migrations.AlterField(
+ model_name='crisiscategorisationbycountry',
+ name='scope_and_scale_total_population_of_the_affected_area',
+ field=models.DecimalField(blank=True, decimal_places=1, max_digits=3, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(9)], verbose_name='total population of the affected area *'),
+ ),
+ ]
diff --git a/api/models.py b/api/models.py
index 9e00c3aca..8d0c4a40e 100644
--- a/api/models.py
+++ b/api/models.py
@@ -1,5 +1,6 @@
import uuid
from datetime import datetime, timedelta
+from decimal import ROUND_HALF_UP, Decimal
import pytz
import reversion
@@ -13,7 +14,13 @@
# from django.db import models
from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField
-from django.core.validators import FileExtensionValidator, RegexValidator, validate_slug
+from django.core.validators import (
+ FileExtensionValidator,
+ MaxValueValidator,
+ MinValueValidator,
+ RegexValidator,
+ validate_slug,
+)
from django.db.models import Q
# from django.db.models import Prefetch
@@ -22,6 +29,7 @@
from django.utils.translation import gettext_lazy as _
from django.utils.translation import override as translation_override
from modeltranslation.utils import build_localized_fieldname
+from smart_selects.db_fields import ChainedForeignKey
from tinymce.models import HTMLField
from lang.translation import AVAILABLE_LANGUAGES
@@ -936,6 +944,767 @@ class Meta:
verbose_name_plural = _("emergency severity level histories")
+class CrisisCategorisationStatus(models.IntegerChoices):
+ IN_PROGRESS = 0, _("In progress")
+ DRAFT = 1, _("Draft")
+ PENDING = 2, _("Pending validation")
+ VALIDATED = 3, _("Validated")
+ PUBLISHED = 4, _("Published")
+ MERGED = 5, _("Merged")
+
+
+@reversion.register()
+class CrisisCategorisationByCountry(models.Model):
+ """Crisis categorisation for a specific country within an event"""
+
+ event = models.ForeignKey(
+ Event,
+ verbose_name=_("event"),
+ on_delete=models.CASCADE,
+ related_name="crisis_categorisations",
+ )
+ country = ChainedForeignKey(
+ "Country",
+ chained_field="event", # The field on THIS model (CrisisCategorisationByCountry)
+ chained_model_field="event", # The field on the TARGET model (Country) that refers to Event
+ show_all=False, # Only show filtered results
+ auto_choose=True, # If only one option exists, select it automatically. GREAT!
+ sort=True,
+ )
+ # country = models.ForeignKey(
+ # Country,
+ # verbose_name=_("country"),
+ # on_delete=models.CASCADE,
+ # related_name="crisis_categorisations",
+ # )
+ crisis_categorisation = models.IntegerField(
+ choices=AlertLevel.choices,
+ verbose_name=_("crisis categorisation"),
+ null=True,
+ blank=True,
+ )
+ crisis_score = models.DecimalField(
+ verbose_name=_("crisis score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+
+ # Pre-Crisis Vulnerability section
+ pre_crisis_vulnerability = models.DecimalField(
+ verbose_name=_("pre-crisis vulnerability"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ pre_crisis_vulnerability_hazard_exposure = models.DecimalField(
+ verbose_name=_("hazard exposure"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ pre_crisis_vulnerability_hazard_exposure_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ pre_crisis_vulnerability_hazard_exposure_intermediate = models.DecimalField(
+ verbose_name=_("hazard exposure (intermediate score)"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # help_text=_("Not counted for the score, just FYI"),
+ )
+ pre_crisis_vulnerability_hazard_exposure_intermediate_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ pre_crisis_vulnerability_vulnerability = models.DecimalField(
+ verbose_name=_("vulnerability"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ pre_crisis_vulnerability_vulnerability_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ pre_crisis_vulnerability_vulnerability_intermediate = models.DecimalField(
+ verbose_name=_("vulnerability (intermediate score)"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ pre_crisis_vulnerability_vulnerability_intermediate_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ pre_crisis_vulnerability_coping_mechanism = models.DecimalField(
+ verbose_name=_("coping mechanism"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ pre_crisis_vulnerability_coping_mechanism_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ pre_crisis_vulnerability_coping_mechanism_intermediate = models.DecimalField(
+ verbose_name=_("coping mechanism (intermediate score)"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ pre_crisis_vulnerability_coping_mechanism_intermediate_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+
+ # Crisis Complexity section
+ crisis_complexity = models.DecimalField(
+ verbose_name=_("crisis complexity"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ crisis_complexity_humanitarian_access_score = models.DecimalField(
+ verbose_name=_("humanitarian access score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ crisis_complexity_humanitarian_access_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ crisis_complexity_humanitarian_access_acaps = models.DecimalField(
+ verbose_name=_("Humanitarian access ACAPS (complex crisis)"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ crisis_complexity_humanitarian_access_acaps_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ crisis_complexity_government_response = models.DecimalField(
+ verbose_name=_("government response"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ crisis_complexity_government_response_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ crisis_complexity_media_attention = models.DecimalField(
+ verbose_name=_("media attention"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ crisis_complexity_media_attention_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ crisis_complexity_ifrc_security_phase = models.DecimalField(
+ verbose_name=_("IFRC security phase"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ crisis_complexity_ifrc_security_phase_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+
+ # Scope & Scale section
+ scope_and_scale = models.DecimalField(
+ verbose_name=_("scope & scale"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ scope_and_scale_number_of_affected_population_score = models.DecimalField(
+ verbose_name=_("# Affected population score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ scope_and_scale_number_of_affected_population_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ scope_and_scale_number_of_affected_population = models.IntegerField(
+ verbose_name=_("# Affected population"),
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(1000000000)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ scope_and_scale_number_of_affected_population_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ scope_and_scale_percentage_affected_population_score = models.DecimalField(
+ verbose_name=_("% Affected population score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ scope_and_scale_percentage_affected_population_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ scope_and_scale_total_population_of_the_affected_area = models.DecimalField(
+ verbose_name=_("total population of the affected area *"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ scope_and_scale_total_population_of_the_affected_area_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ scope_and_scale_percentage_affected_population = models.DecimalField(
+ verbose_name=_("% of Affected population"),
+ max_digits=3,
+ decimal_places=2,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(100)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ scope_and_scale_percentage_affected_population_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ scope_and_scale_impact_index_score = models.DecimalField(
+ verbose_name=_("impact index score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ scope_and_scale_impact_index_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ scope_and_scale_impact_index_inform = models.DecimalField(
+ verbose_name=_("impact index INFORM"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ scope_and_scale_impact_index_inform_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+
+ # Humanitarian Conditions section
+ humanitarian_conditions = models.DecimalField(
+ verbose_name=_("humanitarian conditions"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ humanitarian_conditions_casualties_score = models.DecimalField(
+ verbose_name=_("casualties score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ humanitarian_conditions_casualties_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ humanitarian_conditions_casualties_injrd_deaths_missing = models.DecimalField(
+ verbose_name=_("casualties injured + deaths + missing"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ humanitarian_conditions_casualties_injrd_deaths_missing_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ humanitarian_conditions_severity_score = models.DecimalField(
+ verbose_name=_("severity score *"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ humanitarian_conditions_severity_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ humanitarian_conditions_severity = models.DecimalField(
+ verbose_name=_("severity"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ humanitarian_conditions_severity_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ humanitarian_conditions_people_in_need_score = models.DecimalField(
+ verbose_name=_("# People in need score *"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ humanitarian_conditions_people_in_need_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ humanitarian_conditions_people_in_need = models.DecimalField(
+ verbose_name=_("# People in need"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ humanitarian_conditions_people_in_need_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+
+ # Capacity & Response section
+ capacity_and_response = models.DecimalField(
+ verbose_name=_("capacity & response"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_ifrc_capacity_score = models.DecimalField(
+ verbose_name=_("IFRC capacity score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_ifrc_capacity_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ifrc_international_staff = models.DecimalField(
+ verbose_name=_("IFRC International staff"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_ifrc_international_staff_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ifrc_national_staff = models.DecimalField(
+ verbose_name=_("IFRC National staff"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_ifrc_national_staff_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ifrc_total_staff = models.DecimalField(
+ verbose_name=_("IFRC Total staff"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_ifrc_total_staff_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_regional_office = models.DecimalField(
+ verbose_name=_("Regional Office"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_regional_office_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ops_capacity_score = models.DecimalField(
+ verbose_name=_("OPS Capacity score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_ops_capacity_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ops_capacity_ranking = models.DecimalField(
+ verbose_name=_("Ops capacity Ranking (IFRC Global Risk - 2021)"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_ops_capacity_ranking_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ns_staff_score = models.DecimalField(
+ verbose_name=_("NS staff score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_ns_staff_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_number_of_ns_staff = models.DecimalField(
+ verbose_name=_("Number of NS staff (FDRS) 2025"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_number_of_ns_staff_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ratio_staff_to_volunteer_score = models.DecimalField(
+ verbose_name=_("Ratio staff to volunteer score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_ratio_staff_to_volunteer_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_ratio_staff_volunteer = models.DecimalField(
+ verbose_name=_("Ratio staff volunteer"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_ratio_staff_volunteer_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_number_of_ns_volunteer = models.DecimalField(
+ verbose_name=_("Number of NS volunteer (FDRS)"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_number_of_ns_volunteer_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_number_of_dref_score = models.DecimalField(
+ verbose_name=_("# DREF Score"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_number_of_dref_score_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_number_of_dref_ea_last_3_years = models.DecimalField(
+ verbose_name=_("# DREF and EA in the last 3 years"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ # # help_text=_("Not counted for the score, just FYI"),
+ )
+ capacity_and_response_number_of_dref_ea_last_3_years_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+ capacity_and_response_presence_support_pns_in_country = models.DecimalField(
+ verbose_name=_("Expert Judgement: on active presence and support of PNS in country"),
+ max_digits=3,
+ decimal_places=1,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(9)],
+ )
+ capacity_and_response_presence_support_pns_in_country_comment = models.CharField(
+ max_length=999,
+ verbose_name="",
+ null=True,
+ blank=True,
+ )
+
+ # Commentary
+ commentary = models.TextField(
+ verbose_name=_("Add commentary"),
+ null=True,
+ blank=True,
+ )
+
+ # Attachment
+ general_document = models.ForeignKey(
+ "GeneralDocument",
+ verbose_name=_("File attachment"),
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="crisis_categorisations",
+ )
+
+ status = models.IntegerField(choices=CrisisCategorisationStatus.choices, verbose_name=_("status"), default=0)
+ created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True)
+ updated_at = models.DateTimeField(verbose_name=_("updated at"), auto_now=True)
+
+ class Meta:
+ unique_together = ("event", "country")
+ verbose_name = _("crisis categorisation by country")
+ verbose_name_plural = _("crisis categorisations by country")
+ ordering = ("event", "country")
+
+ def __str__(self):
+ return f"{self.event.name} - {self.country.name}"
+
+ def clean(self):
+ """Validate that the country is associated with the event"""
+ from django.core.exceptions import ValidationError
+
+ if self.event and self.country:
+ if not self.event.countries.filter(pk=self.country.pk).exists():
+ raise ValidationError(
+ {"country": _("The selected country must be one of the countries associated with this event.")}
+ )
+
+ def _average_fields(self, field_names):
+ values = []
+ for field_name in field_names:
+ value = getattr(self, field_name)
+ if value is None:
+ continue
+ values.append(Decimal(str(value)))
+
+ if not values:
+ return None
+
+ avg = sum(values) / Decimal(len(values))
+ return avg.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
+
+ def save(self, *args, **kwargs):
+ """Override save to always validate before saving"""
+ self.pre_crisis_vulnerability = self._average_fields(
+ [
+ "pre_crisis_vulnerability_hazard_exposure",
+ "pre_crisis_vulnerability_vulnerability",
+ "pre_crisis_vulnerability_coping_mechanism",
+ ]
+ )
+ self.crisis_complexity = self._average_fields(
+ [
+ "crisis_complexity_humanitarian_access_score",
+ "crisis_complexity_government_response",
+ "crisis_complexity_media_attention",
+ "crisis_complexity_ifrc_security_phase",
+ ]
+ )
+ self.scope_and_scale = self._average_fields(
+ [
+ "scope_and_scale_number_of_affected_population_score",
+ "scope_and_scale_percentage_affected_population_score",
+ "scope_and_scale_impact_index_score",
+ ]
+ )
+ self.humanitarian_conditions = self._average_fields(
+ [
+ "humanitarian_conditions_casualties_score",
+ "humanitarian_conditions_severity_score",
+ "humanitarian_conditions_people_in_need_score",
+ ]
+ )
+ self.capacity_and_response = self._average_fields(
+ [
+ "capacity_and_response_ifrc_capacity_score",
+ "capacity_and_response_ops_capacity_score",
+ "capacity_and_response_ns_staff_score",
+ "capacity_and_response_ratio_staff_to_volunteer_score",
+ "capacity_and_response_number_of_dref_score",
+ "capacity_and_response_presence_support_pns_in_country",
+ ]
+ )
+ self.full_clean()
+ return super().save(*args, **kwargs)
+
+
@reversion.register()
class EventFeaturedDocument(models.Model):
event = models.ForeignKey(
diff --git a/api/templates/admin/emergency_change_list_with_history.html b/api/templates/admin/emergency_change_list_with_history.html
new file mode 100644
index 000000000..acbde77f2
--- /dev/null
+++ b/api/templates/admin/emergency_change_list_with_history.html
@@ -0,0 +1,151 @@
+{% extends "admin/emergency_change_list.html" %}
+{% load i18n static %}
+
+{% block extrahead %}
+ {{ block.super }}
+
+
+{% endblock %}
+
+
diff --git a/assets b/assets
index ecf93b474..1faa98273 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit ecf93b474a86f62038e0d9330dc3935834130d0f
+Subproject commit 1faa982732d421f810e8c429f1b2b09d7831dc50
diff --git a/go-static/css/after.css b/go-static/css/after.css
index 0941e212d..31db15a3b 100644
--- a/go-static/css/after.css
+++ b/go-static/css/after.css
@@ -5,3 +5,256 @@
p.deletelink-box {
margin-top: 0;
}
+
+/* Crisis Categorisation By Country - Event field width */
+#id_event {
+ min-width: 400px;
+}
+
+/* Crisis Categorisation By Country - Inline field layout */
+
+/* Make form rows display fields side by side */
+.change-form .form-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ align-items: flex-start;
+}
+
+/* First field in a row (score fields) - wider for longer labels */
+.change-form .form-row > div:first-child:not(:only-child) {
+ flex: 0 0 30%;
+ min-width: 200px;
+}
+
+/* Second field in a row (comment fields) - 68% width */
+.change-form .form-row > div:last-child:not(:only-child) {
+ flex: 0 0 68%;
+ min-width: 300px;
+}
+
+/* Single fields (not in tuples) take full width */
+.change-form .form-row > div:only-child {
+ flex: 1 1 100%;
+ max-width: 100%;
+}
+
+/* Ensure labels don't break the layout and don't wrap */
+.change-form .form-row label {
+ display: block;
+ margin-bottom: 5px;
+ white-space: nowrap;
+ overflow: visible;
+}
+
+/* Input fields should not exceed their container */
+.change-form .form-row input[type="text"],
+.change-form .form-row input[type="number"],
+.change-form .form-row select,
+.change-form .form-row textarea {
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+/* Crisis Categorisation: ensure IntegerField inputs match DecimalField width but aren't oversized */
+.model-crisiscategorisationbycountry.change-form .vIntegerField {
+ width: 14em;
+}
+
+/* Comment fields (second column) should fill their container */
+.change-form .form-row > div:last-child:not(:only-child) input[type="text"] {
+ width: 100%;
+}
+
+/* Fieldset with positioned header field */
+.change-form fieldset.module {
+ position: relative;
+}
+
+.change-form fieldset.module > h2 {
+ padding-right: 200px; /* Space for the input field */
+}
+
+/* Table header row for fieldsets - custom HTML version */
+.change-form .fieldset-table-header {
+ display: flex;
+ gap: 15px;
+ background: #f8f8f8;
+ padding: 8px 10px;
+ margin: 4px 0 0; /* Gap between header and subheader */
+ border-bottom: 1px solid #ddd;
+ font-weight: 600;
+ font-size: 11px;
+ color: #666;
+}
+
+.change-form .fieldset-table-header .header-indicator {
+ flex: 0 0 30%;
+ min-width: 200px;
+}
+
+.change-form .fieldset-table-header .header-comment {
+ flex: 0 0 68%;
+ min-width: 300px;
+}
+
+/* Dark theme support for subheaders */
+@media (prefers-color-scheme: dark) {
+ .change-form .fieldset-table-header {
+ background: #2b2b2b;
+ border-bottom-color: #444;
+ color: #b0b0b0;
+ }
+}
+
+
+/* Position first form-row with single field to the header area */
+.change-form fieldset.module > .form-row:first-of-type > div:only-child {
+ position: absolute;
+ top: 3px;
+ right: 15px;
+ margin: 0;
+ z-index: 10;
+}
+
+/* Exception: commentary and event fields should NOT be positioned in header */
+.change-form fieldset.module > .form-row:first-of-type > div:only-child:has(textarea[name="commentary"]),
+.change-form fieldset.module > .form-row:first-of-type > div:only-child:has(input[name="commentary"]),
+.change-form fieldset.module > .form-row:first-of-type > div:only-child:has(select[name="event"]) {
+ position: static;
+}
+
+.change-form fieldset.module > .form-row:first-of-type > div:only-child label {
+ display: none;
+}
+
+/* Show label for commentary and event fields */
+.change-form fieldset.module > .form-row:first-of-type > div:only-child:has(textarea[name="commentary"]) label,
+.change-form fieldset.module > .form-row:first-of-type > div:only-child:has(input[name="commentary"]) label,
+.change-form fieldset.module > .form-row:first-of-type > div:only-child:has(select[name="event"]) label {
+ display: block;
+}
+
+.change-form fieldset.module > .form-row:first-of-type > div:only-child input,
+.change-form fieldset.module > .form-row:first-of-type > div:only-child select {
+ width: 100px;
+ max-width: 100px;
+ margin: 0;
+ padding: 4px 6px;
+ font-size: 13px;
+}
+
+/* Reset width for commentary and event fields */
+.change-form fieldset.module > .form-row:first-of-type > div:only-child textarea[name="commentary"],
+.change-form fieldset.module > .form-row:first-of-type > div:only-child input[name="commentary"],
+.change-form fieldset.module > .form-row:first-of-type > div:only-child select[name="event"] {
+ width: 100%;
+ max-width: 100%;
+ padding: initial;
+ font-size: initial;
+}
+
+/* Hide the form-row container but show its content via absolute positioning */
+.change-form fieldset.module > .form-row:first-of-type:has(> div:only-child) {
+ height: 0;
+ margin: 0;
+ padding: 0;
+ overflow: visible;
+}
+
+/* Show normal height for commentary and event form-rows */
+.change-form fieldset.module > .form-row:first-of-type:has(textarea[name="commentary"]),
+.change-form fieldset.module > .form-row:first-of-type:has(input[name="commentary"]),
+.change-form fieldset.module > .form-row:first-of-type:has(select[name="event"]) {
+ height: auto;
+ margin: initial;
+ padding: initial;
+}
+
+/* Did not work to align Event left
+.change-form .form-row.field-event > div {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+} */
+
+/* Clickable help icon + tooltip attached to Crisis Categorisation headers */
+.precrisis-help-icon,
+.cc-help-icon {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ line-height: 16px;
+ text-align: center;
+ border-radius: 50%;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ cursor: pointer;
+ margin-left: 5px;
+ vertical-align: middle;
+ user-select: none;
+ position: relative;
+ top: -0.3rem;
+}
+
+/* Tooltip bubble */
+.precrisis-help-tooltip,
+.cc-help-tooltip {
+ display: none;
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 0;
+ background: #fff;
+ color: #333;
+ padding: 8px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 12px;
+ max-width: 90vw;
+ max-height: 80vh;
+ overflow: auto;
+ z-index: 1000;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ margin-top: 0;
+}
+
+.precrisis-help-tooltip img,
+.cc-help-tooltip img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+.precrisis-help-tooltip::before,
+.cc-help-tooltip::before {
+ content: '';
+ position: absolute;
+ bottom: -5px;
+ left: 20px;
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid #ccc;
+}
+
+.precrisis-help-tooltip.visible,
+.cc-help-tooltip.visible {
+ display: block;
+}
+
+/* Dark theme support */
+@media (prefers-color-scheme: dark) {
+ .precrisis-help-tooltip,
+ .cc-help-tooltip {
+ background: #1a1a1a;
+ color: #eee;
+ border: 1px solid #444;
+ }
+
+ .precrisis-help-tooltip::before,
+ .cc-help-tooltip::before {
+ border-top-color: #1a1a1a;
+ }
+}
diff --git a/go-static/images/cc/capacity_and_response.gif b/go-static/images/cc/capacity_and_response.gif
new file mode 100644
index 000000000..1bc4c2143
Binary files /dev/null and b/go-static/images/cc/capacity_and_response.gif differ
diff --git a/go-static/images/cc/crisis_complexity.gif b/go-static/images/cc/crisis_complexity.gif
new file mode 100644
index 000000000..5a336b5f4
Binary files /dev/null and b/go-static/images/cc/crisis_complexity.gif differ
diff --git a/go-static/images/cc/humanitarian_conditions.gif b/go-static/images/cc/humanitarian_conditions.gif
new file mode 100644
index 000000000..b41243315
Binary files /dev/null and b/go-static/images/cc/humanitarian_conditions.gif differ
diff --git a/go-static/images/cc/pre_crisis_vulnerability.gif b/go-static/images/cc/pre_crisis_vulnerability.gif
new file mode 100644
index 000000000..fd353a516
Binary files /dev/null and b/go-static/images/cc/pre_crisis_vulnerability.gif differ
diff --git a/go-static/images/cc/scope_and_scale.gif b/go-static/images/cc/scope_and_scale.gif
new file mode 100644
index 000000000..2051f21c0
Binary files /dev/null and b/go-static/images/cc/scope_and_scale.gif differ
diff --git a/go-static/js/crisis_categorisation_headers.js b/go-static/js/crisis_categorisation_headers.js
new file mode 100644
index 000000000..2215ac908
--- /dev/null
+++ b/go-static/js/crisis_categorisation_headers.js
@@ -0,0 +1,324 @@
+// Add custom HTML header row to Crisis Categorisation fieldsets
+document.addEventListener('DOMContentLoaded', function() {
+ // Minimal styling for injected icons/tooltips
+ const style = document.createElement('style');
+ style.textContent = `
+ .external-link-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 6px;
+ vertical-align: middle;
+ color: inherit;
+ text-decoration: none;
+ opacity: 0.85;
+ }
+ .external-link-icon:hover { opacity: 1; }
+ .external-link-icon svg {
+ width: 14px;
+ height: 14px;
+ fill: currentColor;
+ }
+
+ /* Field help icons color adjustment */
+ .field-help-icon {
+ color: #444 !important;
+ }
+ @media (prefers-color-scheme: dark) {
+ .field-help-icon {
+ color: #b0b0b0 !important;
+ }
+ }
+
+ /* Position field tooltips below the icon */
+ .field-help-tooltip {
+ top: 100% !important;
+ bottom: auto !important;
+ margin-top: 8px !important;
+ }
+
+ /* Arrow adjustment for top-pointing (tooltip below) */
+ .field-help-tooltip::before {
+ top: -5px !important;
+ bottom: auto !important;
+ border-top: none !important;
+ border-bottom: 5px solid #ccc !important;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .field-help-tooltip::before {
+ border-bottom-color: #1a1a1a !important;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+
+ const fieldsets = document.querySelectorAll('.change-form fieldset.module');
+
+ const headerNodesByText = new Map();
+ let escKeyHandlerAdded = false;
+
+ // Target fieldsets that should have the custom header
+ const targetHeaders = [
+ '1. Pre-Crisis Vulnerability',
+ '2. Crisis Complexity',
+ '3. Scope & Scale',
+ '4. Humanitarian Conditions',
+ '5. Capacity & Response'
+ ];
+
+ fieldsets.forEach(function(fieldset) {
+ const h2 = fieldset.querySelector('h2');
+ const firstFormRow = fieldset.querySelector('.form-row:first-of-type');
+
+ // Only add header if h2 exists and matches target headers
+ if (h2 && firstFormRow) {
+ const headerText = h2.textContent.trim();
+
+ headerNodesByText.set(headerText, h2);
+
+ // Check if this fieldset is one of the target fieldsets
+ if (targetHeaders.includes(headerText)) {
+ // Create the custom HTML header
+ const headerRow = document.createElement('div');
+ headerRow.className = 'fieldset-table-header';
+ headerRow.innerHTML = `
+
+
+ `;
+
+ // Insert after the first form-row (which contains the header field)
+ firstFormRow.parentNode.insertBefore(headerRow, firstFormRow.nextSibling);
+ }
+ }
+ });
+
+ function addHeaderHelpTooltip(headerText, imageFilename, imageAlt, extraIconClass) {
+ const headerNode = headerNodesByText.get(headerText);
+ if (!headerNode) {
+ return;
+ }
+
+ if (headerNode.querySelector('.cc-help-icon')) {
+ return;
+ }
+
+ const fallbackHelpTextContent = 'Help image failed to load.';
+
+ const helpIcon = document.createElement('span');
+ helpIcon.className = `help-icon cc-help-icon ${extraIconClass || ''}`.trim();
+ helpIcon.innerHTML = 'ⓘ';
+ helpIcon.title = 'Click for more information';
+
+ const tooltip = document.createElement('div');
+ tooltip.className = 'help-tooltip cc-help-tooltip';
+
+ const helpImage = document.createElement('img');
+ helpImage.src = `/static/images/cc/${imageFilename}`;
+ helpImage.alt = imageAlt;
+ helpImage.loading = 'lazy';
+ helpImage.addEventListener('error', function() {
+ tooltip.textContent = fallbackHelpTextContent;
+ });
+
+ tooltip.appendChild(helpImage);
+
+ headerNode.appendChild(document.createTextNode(' '));
+ headerNode.appendChild(helpIcon);
+
+ headerNode.style.position = 'relative';
+ headerNode.appendChild(tooltip);
+
+ helpIcon.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const shouldShow = !tooltip.classList.contains('visible');
+ document.querySelectorAll('.cc-help-tooltip.visible').forEach(function(openTooltip) {
+ openTooltip.classList.remove('visible');
+ });
+ tooltip.classList.toggle('visible', shouldShow);
+ });
+
+ document.addEventListener('click', function(e) {
+ if (!headerNode.contains(e.target)) {
+ tooltip.classList.remove('visible');
+ }
+ });
+
+ if (!escKeyHandlerAdded) {
+ escKeyHandlerAdded = true;
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' || e.key === 'Esc') {
+ document.querySelectorAll('.cc-help-tooltip.visible').forEach(function(openTooltip) {
+ openTooltip.classList.remove('visible');
+ });
+ }
+ });
+ }
+ }
+
+ addHeaderHelpTooltip(
+ '1. Pre-Crisis Vulnerability',
+ 'pre_crisis_vulnerability.gif',
+ 'Pre-Crisis Vulnerability help',
+ 'precrisis-help-icon'
+ );
+ addHeaderHelpTooltip(
+ '2. Crisis Complexity',
+ 'crisis_complexity.gif',
+ 'Crisis Complexity help',
+ 'crisiscomplexity-help-icon'
+ );
+ addHeaderHelpTooltip(
+ '3. Scope & Scale',
+ 'scope_and_scale.gif',
+ 'Scope & Scale help',
+ 'scopeandscale-help-icon'
+ );
+ addHeaderHelpTooltip(
+ '4. Humanitarian Conditions',
+ 'humanitarian_conditions.gif',
+ 'Humanitarian Conditions help',
+ 'humanitarianconditions-help-icon'
+ );
+ addHeaderHelpTooltip(
+ '5. Capacity & Response',
+ 'capacity_and_response.gif',
+ 'Capacity & Response help',
+ 'capacityandresponse-help-icon'
+ );
+
+ const informMapExplorerUrl = 'https://drmkc.jrc.ec.europa.eu/inform-index/INFORM-Risk/Map-Explorer';
+
+ function addExternalLinkIcon(fieldSelector, url) {
+ const field = document.querySelector(fieldSelector);
+ if (!field) {
+ return;
+ }
+
+ const label = field.querySelector('label');
+ if (!label) {
+ return;
+ }
+
+ if (label.querySelector('.external-link-icon')) {
+ return;
+ }
+
+ const link = document.createElement('a');
+ link.className = 'external-link-icon';
+ link.href = url;
+ link.target = '_blank';
+ link.rel = 'noopener noreferrer';
+ link.title = 'Open INFORM Risk Map Explorer';
+ link.setAttribute('aria-label', 'Open INFORM Risk Map Explorer');
+
+ // Inline SVG external-link icon
+ link.innerHTML = `
+
+ `;
+
+ label.appendChild(link);
+ }
+
+
+ function addFieldHelpTooltip(label) {
+ if (label.querySelector('.field-help-icon')) {
+ return;
+ }
+
+ const helpIcon = document.createElement('span');
+ helpIcon.className = 'help-icon cc-help-icon field-help-icon';
+ helpIcon.textContent = 'ⓘ';
+ helpIcon.title = 'Click for more information';
+ helpIcon.style.fontStyle = 'normal';
+ helpIcon.style.marginLeft = '5px';
+ helpIcon.style.cursor = 'pointer';
+
+ const tooltip = document.createElement('div');
+ tooltip.className = 'help-tooltip cc-help-tooltip field-help-tooltip';
+ tooltip.textContent = 'This indicator will not be counted for the score but is needed for the overall tracking and estimations.';
+ tooltip.style.fontStyle = 'normal';
+
+ // Ensure label can position the absolute tooltip
+ label.style.position = 'relative';
+
+ label.appendChild(helpIcon);
+ label.appendChild(tooltip);
+
+ helpIcon.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const isVisible = tooltip.classList.contains('visible');
+
+ // Close all open tooltips first
+ document.querySelectorAll('.cc-help-tooltip.visible').forEach(function(openTooltip) {
+ openTooltip.classList.remove('visible');
+ });
+
+ if (!isVisible) {
+ tooltip.classList.add('visible');
+ }
+ });
+
+ document.addEventListener('click', function(e) {
+ if (!label.contains(e.target)) {
+ tooltip.classList.remove('visible');
+ }
+ });
+ }
+
+ function italicizeFieldLabels(fieldNames) {
+ fieldNames.forEach(function(fieldName) {
+ const label = document.querySelector(`label[for="id_${fieldName}"]`);
+ if (!label) {
+ return;
+ }
+
+ label.style.fontStyle = 'italic';
+ addFieldHelpTooltip(label);
+ });
+ }
+
+ italicizeFieldLabels([
+ 'pre_crisis_vulnerability_hazard_exposure_intermediate',
+ 'pre_crisis_vulnerability_vulnerability_intermediate',
+ 'pre_crisis_vulnerability_coping_mechanism_intermediate',
+ 'crisis_complexity_humanitarian_access_acaps',
+ 'scope_and_scale_number_of_affected_population',
+ 'scope_and_scale_total_population_of_the_affected_area',
+ 'scope_and_scale_percentage_affected_population',
+ 'scope_and_scale_impact_index_inform',
+ 'humanitarian_conditions_casualties_injrd_deaths_missing',
+ 'humanitarian_conditions_severity',
+ 'humanitarian_conditions_people_in_need',
+ 'capacity_and_response_ifrc_international_staff',
+ 'capacity_and_response_ifrc_national_staff',
+ 'capacity_and_response_ifrc_total_staff',
+ 'capacity_and_response_regional_office',
+ 'capacity_and_response_ops_capacity_ranking',
+ 'capacity_and_response_number_of_ns_staff',
+ 'capacity_and_response_ratio_staff_volunteer',
+ 'capacity_and_response_number_of_ns_volunteer',
+ 'capacity_and_response_number_of_dref_ea_last_3_years'
+ ]);
+
+ // Add external URL icons after labels
+ addExternalLinkIcon('.field-pre_crisis_vulnerability_hazard_exposure', informMapExplorerUrl);
+ addExternalLinkIcon('.field-pre_crisis_vulnerability_vulnerability', informMapExplorerUrl);
+ addExternalLinkIcon('.field-pre_crisis_vulnerability_coping_mechanism', informMapExplorerUrl);
+ addExternalLinkIcon('.field-crisis_complexity_humanitarian_access_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-crisis_complexity_humanitarian_access_acaps', informMapExplorerUrl);
+ addExternalLinkIcon('.field-scope_and_scale_impact_index_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-humanitarian_conditions_severity_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-capacity_and_response_ifrc_capacity_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-capacity_and_response_ops_capacity_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-capacity_and_response_ns_staff_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-capacity_and_response_ratio_staff_to_volunteer_score', informMapExplorerUrl);
+ addExternalLinkIcon('.field-capacity_and_response_number_of_dref_score', informMapExplorerUrl);
+});
diff --git a/main/settings.py b/main/settings.py
index 6fede598b..d2e2daf1a 100644
--- a/main/settings.py
+++ b/main/settings.py
@@ -276,6 +276,8 @@ def parse_domain(*env_keys: str) -> str:
"debug_toolbar",
# GIS
"django.contrib.gis",
+ # chained select
+ "smart_selects",
]
REST_FRAMEWORK = {
diff --git a/main/urls.py b/main/urls.py
index 0733965ad..d9d6cc676 100644
--- a/main/urls.py
+++ b/main/urls.py
@@ -267,6 +267,7 @@
path("docs/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path("api-docs/", SpectacularAPIView.as_view(), name="schema"),
path("api-docs/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
+ path("chaining/", include("smart_selects.urls")),
]
if settings.OIDC_ENABLE:
diff --git a/pyproject.toml b/pyproject.toml
index f617456d7..6cfafedcd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,7 @@ dependencies = [
"django-read-only==1.12.0",
"django-reversion-compare==0.16.2",
"django-reversion==5.0.12",
+ "django-smart-selects>=1.7.2",
"django-storages[s3,azure]==1.14.5",
"django-tinymce==4.1.0",
"django-oauth-toolkit==3.0.1",
@@ -92,7 +93,7 @@ dev = [
"pytest-ordering",
"pytest-django",
"snapshottest==0.6.0",
- "django-debug-toolbar==4.1.0",
+ "django-debug-toolbar==6.2.0",
"django-stubs",
]
celery = [
diff --git a/uv.lock b/uv.lock
index a5528acb0..47692fbfd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -641,15 +641,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/83/19/9d53c3a330b121d04
[[package]]
name = "django-debug-toolbar"
-version = "4.1.0"
+version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "sqlparse" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9b/e0/9a439dd58ac27ceaecce015fe57916bbd84dbbaf7ca3208c122b46c7d3ff/django_debug_toolbar-4.1.0.tar.gz", hash = "sha256:f57882e335593cb8e74c2bda9f1116bbb9ca8fc0d81b50a75ace0f83de5173c7", size = 116377, upload-time = "2023-05-16T06:46:00.074Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/4d/6acf660500d3d581bfc19460d9605cdf14c275640f35825da1329eaafafa/django_debug_toolbar-6.2.0.tar.gz", hash = "sha256:dc1c174d8fb0ea01435e02d9ceef735cf62daf37c1a6a5692d33b4127327679b", size = 313779, upload-time = "2026-01-20T12:38:25.268Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/11/22ae178ae870945970250bdad22530583384b3438be87e08c075016f3b07/django_debug_toolbar-4.1.0-py3-none-any.whl", hash = "sha256:a0b532ef5d52544fd745d1dcfc0557fa75f6f0d1962a8298bd568427ef2fa436", size = 222480, upload-time = "2023-05-16T06:46:23.879Z" },
+ { url = "https://files.pythonhosted.org/packages/88/04/e24611299a5ee0d4edfacf935b09cfb7d5d9cb653bd7b7883c3b43a6f90d/django_debug_toolbar-6.2.0-py3-none-any.whl", hash = "sha256:1575461954e6befa720e999dec13fe4f1cc8baf40b6c3ac2aec5f340c0f9c85f", size = 271354, upload-time = "2026-01-20T12:38:23.608Z" },
]
[[package]]
@@ -813,6 +813,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/b9/40963fd61f766d6570415a5e82a544824bc62f865ecbc935b41e986bb149/django_reversion_compare-0.16.2-py3-none-any.whl", hash = "sha256:5629f226fc73bd7b95de47b2e21e2eba2fa39f004ba0fee6d460e96676c0dc9b", size = 97508, upload-time = "2023-05-08T14:03:51.018Z" },
]
+[[package]]
+name = "django-smart-selects"
+version = "1.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "django" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/33/3b512f6b38e0581be812f470424e9b819688edba03b581931534d5fcfc11/django_smart_selects-1.7.2.tar.gz", hash = "sha256:c8116718ecf2d0d0fd4cd1e52c26d2dcc586e25319aa65af7af2d51a1012df3a", size = 27931, upload-time = "2024-11-17T19:06:57.09Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/83/4ae33d1518409ac65891cc5f739cface4d8f27117fbf1eda3a515e90e86c/django_smart_selects-1.7.2-py3-none-any.whl", hash = "sha256:08c774d96285e6d169043cd548860d74d085be8d349c42437232c42bc43cd79a", size = 31651, upload-time = "2024-11-17T19:06:55.279Z" },
+]
+
[[package]]
name = "django-storages"
version = "1.14.5"
@@ -1043,6 +1055,7 @@ dependencies = [
{ name = "django-redis" },
{ name = "django-reversion" },
{ name = "django-reversion-compare" },
+ { name = "django-smart-selects" },
{ name = "django-storages", extra = ["azure", "s3"] },
{ name = "django-tinymce" },
{ name = "djangorestframework" },
@@ -1136,6 +1149,7 @@ requires-dist = [
{ name = "django-redis", specifier = "==5.0.0" },
{ name = "django-reversion", specifier = "==5.0.12" },
{ name = "django-reversion-compare", specifier = "==0.16.2" },
+ { name = "django-smart-selects", specifier = ">=1.7.2" },
{ name = "django-storages", extras = ["s3", "azure"], specifier = "==1.14.5" },
{ name = "django-tinymce", specifier = "==4.1.0" },
{ name = "djangorestframework", specifier = "==3.15.2" },
@@ -1188,7 +1202,7 @@ requires-dist = [
[package.metadata.requires-dev]
celery = [{ name = "playwright", specifier = "==1.50.0" }]
dev = [
- { name = "django-debug-toolbar", specifier = "==4.1.0" },
+ { name = "django-debug-toolbar", specifier = "==6.2.0" },
{ name = "django-stubs" },
{ name = "pytest" },
{ name = "pytest-django" },