Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,7 @@ def test_dref_summary_fields_present_when_summary_exists(self):
dref=dref,
status=DrefSummary.SummaryStatus.SUCCESS,
situational_overview="overview text",
needs_identified="needs text",
operational_strategy="strategy text",
people_centered_approach="approach text",
challenges_identified="challenges text",
Expand All @@ -1619,6 +1620,7 @@ def test_dref_summary_fields_present_when_summary_exists(self):
self.assertIsNotNone(summary)
self.assertEqual(summary["status"], DrefSummary.SummaryStatus.SUCCESS)
self.assertEqual(summary["situational_overview"], "overview text")
self.assertEqual(summary["needs_identified"], "needs text")
self.assertEqual(summary["operational_strategy"], "strategy text")
self.assertEqual(summary["people_centered_approach"], "approach text")
self.assertEqual(summary["challenges_identified"], "challenges text")
Expand Down
2 changes: 1 addition & 1 deletion assets
Submodule assets updated 1 files
+4 −0 openapi-schema.yaml
2 changes: 2 additions & 0 deletions dref/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class DrefSummaryInline(admin.StackedInline, TranslationInlineModelAdmin):
"status",
"source_hash",
"situational_overview",
"needs_identified",
"operational_strategy",
"people_centered_approach",
"challenges_identified",
Expand Down Expand Up @@ -349,6 +350,7 @@ class DrefSummaryAdmin(TranslationAdmin, admin.ModelAdmin):
"status",
"source_hash",
"situational_overview",
"needs_identified",
"operational_strategy",
"people_centered_approach",
"challenges_identified",
Expand Down
1 change: 1 addition & 0 deletions dref/factories/dref.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ class Meta:
source_id = factory.SelfAttribute("dref.id")
status = DrefSummary.SummaryStatus.SUCCESS
situational_overview = fuzzy.FuzzyText(length=100)
needs_identified = fuzzy.FuzzyText(length=100)
operational_strategy = fuzzy.FuzzyText(length=100)
people_centered_approach = fuzzy.FuzzyText(length=100)
challenges_identified = fuzzy.FuzzyText(length=100)
Expand Down
38 changes: 38 additions & 0 deletions dref/migrations/0091_drefsummary_needs_identified_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.2.16 on 2026-08-14 06:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dref', '0090_drefsummary'),
]

operations = [
migrations.AddField(
model_name='drefsummary',
name='needs_identified',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='drefsummary',
name='needs_identified_ar',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='drefsummary',
name='needs_identified_en',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='drefsummary',
name='needs_identified_es',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='drefsummary',
name='needs_identified_fr',
field=models.TextField(blank=True, null=True),
),
]
5 changes: 5 additions & 0 deletions dref/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,11 @@ class SourceModel(models.IntegerChoices):
null=True,
)

needs_identified = models.TextField(
blank=True,
null=True,
)

operational_strategy = models.TextField(
blank=True,
null=True,
Expand Down
1 change: 1 addition & 0 deletions dref/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2454,6 +2454,7 @@ class Meta:
"source_display",
"source_id",
"situational_overview",
"needs_identified",
"operational_strategy",
"people_centered_approach",
"challenges_identified",
Expand Down
65 changes: 49 additions & 16 deletions dref/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
# DrefSummary fields — order is the iteration order for prompt assembly.
SUMMARY_FIELDS: List[str] = [
"situational_overview",
"needs_identified",
"operational_strategy",
"people_centered_approach",
"challenges_identified",
Expand All @@ -60,6 +61,11 @@ def _build_situational_overview_prompt(**kwargs) -> str:
return f'Data for "situational_overview" — the disaster situation and rationale for the operation:\n{data_json}'


def _build_needs_identified_prompt(**kwargs) -> str:
data_json = _section_data_json(kwargs)
return f'Data for "needs_identified" — the needs and gaps identified for the affected population:\n{data_json}'


def _build_operational_strategy_prompt(**kwargs) -> str:
data_json = _section_data_json(kwargs)
return f'Data for "operational_strategy" — the objective and strategy of the response:\n{data_json}'
Expand All @@ -83,19 +89,22 @@ def _build_lessons_learned_prompt(**kwargs) -> str:
# Registry
SECTION_PROMPT_BUILDERS: Dict[str, Callable[..., str]] = {
"situational_overview": _build_situational_overview_prompt,
"needs_identified": _build_needs_identified_prompt,
"operational_strategy": _build_operational_strategy_prompt,
"people_centered_approach": _build_people_centered_approach_prompt,
"challenges_identified": _build_challenges_identified_prompt,
"lessons_learned": _build_lessons_learned_prompt,
}

GLOBAL_PROMPT = (
"The DREF data above is organised by summary section. Using ONLY that data, write five concise "
"summary sections. Return a single JSON object (and nothing else) with exactly these keys, each "
"summarising the block of the same name:\n"
"The DREF data above is organised by summary section. Using ONLY that data, write six "
"summary sections. Return a single JSON object (and nothing else) with exactly these "
"keys, each summarising the block of the same name:\n"
"\n"
' "situational_overview": The disaster situation and the rationale for the operation. Use the '
'data under the "situational_overview" key.\n'
' "needs_identified": The needs identified per sector, and any gaps or limitations in the '
'assessment. Use the data under the "needs_identified" key.\n'
' "operational_strategy": The overall objective and strategic approach of the response. Use the '
'data under the "operational_strategy" key.\n'
' "people_centered_approach": Who is targeted and how they are selected and engaged. Use the '
Expand Down Expand Up @@ -138,6 +147,9 @@ def _extract_fields(obj, field_names: List[str]) -> dict:

SITUATIONAL_COMMON_FIELDS: List[str] = ["event_description", "event_scope"]

# Imminent DREF applications created on the v2 use hazard_date_and_location.
IMMINENT_SITUATIONAL_FIELDS: List[str] = ["hazard_date_and_location"]

OPERATIONAL_COMMON_FIELDS: List[str] = ["operation_objective", "response_strategy"]

PEOPLE_COMMON_FIELDS: List[str] = ["people_assisted", "selection_criteria"]
Expand All @@ -151,14 +163,32 @@ def __init__(self):

@staticmethod
def _situational_overview_kwargs(source_doc) -> dict:
"""Build situational_overview kwargs — common across all document types.
"""Imminent v2 applications describe the situation in the scenario analysis fields; others use the common ones."""
if isinstance(source_doc, Dref) and source_doc.type_of_dref == Dref.DrefType.IMMINENT and source_doc.is_dref_imminent_v2:
return _extract_fields(source_doc, IMMINENT_SITUATIONAL_FIELDS)
return _extract_fields(source_doc, SITUATIONAL_COMMON_FIELDS) # event_scope is empty for Assessment; dropped

``event_scope`` is one of the common fields; when it is empty (e.g. an
Imminent DREF Application where the scope is not yet known)
``_extract_fields`` drops it automatically, while by the Final Report
stage the event has materialized and the field feeds the summary.
"""
return _extract_fields(source_doc, SITUATIONAL_COMMON_FIELDS)
@staticmethod
def _needs_identified_kwargs(source_doc) -> dict:
"""Collect the needs from the ``needs_identified`` M2M, plus the gaps text."""

def need_title(need):
display = getattr(need, "get_title_display", None)
return display() if callable(display) else need.title

# Order explicitly: needs_identified has no Meta.ordering, so an unordered
# .all() can return rows in different orders across queries, which would
# change the source hash and trigger needless regeneration.
needs = sorted(source_doc.needs_identified.all(), key=lambda need: need.id)
return {
# A need with no description is still meaningful: it names a sector
# where a need was identified, so keep it and drop the empty text.
"needs_identified": [
{"title": need_title(need), **({"description": need.description} if need.description else {})} for need in needs
]
or None,
"identified_gaps": _field_val(source_doc, "identified_gaps"),
}

@staticmethod
def _challenges_and_lessons_kwargs(source_doc) -> Dict[str, dict]:
Expand All @@ -173,7 +203,7 @@ def pi_title(pi):
# Order explicitly: planned_interventions has no Meta.ordering, so an
# unordered .all() can return rows in different orders across queries,
# which would change the source hash and trigger needless regeneration.
planned = list(source_doc.planned_interventions.order_by("id"))
planned = sorted(source_doc.planned_interventions.all(), key=lambda pi: pi.id)
return {
"challenges_identified": {
"planned_interventions": [{"title": pi_title(pi), "challenges": pi.challenges} for pi in planned if pi.challenges]
Expand All @@ -189,39 +219,42 @@ def pi_title(pi):

@classmethod
def _extract_dref_kwargs(cls, dref) -> Dict[str, dict]:
"""Dref Application / Assessment / Imminent — three sections only.
"""Dref Application / Assessment / Imminent — four sections.

Challenges and lessons are not applicable at the application stage;
they are formally recorded only in the Final Report.
"""
return {
"situational_overview": cls._situational_overview_kwargs(dref),
"needs_identified": cls._needs_identified_kwargs(dref),
"operational_strategy": _extract_fields(dref, OPERATIONAL_COMMON_FIELDS),
"people_centered_approach": _extract_fields(dref, PEOPLE_COMMON_FIELDS),
}

@classmethod
def _extract_dref_ops_kwargs(cls, ops) -> Dict[str, dict]:
"""DrefOperationalUpdate — three sections.
"""DrefOperationalUpdate — four sections.

Challenges and lessons are not generated for Operational Updates.
"""
return {
"situational_overview": cls._situational_overview_kwargs(ops),
"needs_identified": cls._needs_identified_kwargs(ops),
"operational_strategy": _extract_fields(ops, OPERATIONAL_COMMON_FIELDS),
"people_centered_approach": _extract_fields(ops, PEOPLE_COMMON_FIELDS),
}

@classmethod
def _extract_dref_final_kwargs(cls, final) -> Dict[str, dict]:
"""DrefFinalReport — all five sections.
"""DrefFinalReport — all six sections.

Challenges and lessons come from ``planned_interventions`` M2M via
``_challenges_and_lessons_kwargs``; this is the only document type
where those two sections are generated.
"""
kwargs = {
"situational_overview": cls._situational_overview_kwargs(final),
"needs_identified": cls._needs_identified_kwargs(final),
"operational_strategy": _extract_fields(final, OPERATIONAL_COMMON_FIELDS),
"people_centered_approach": _extract_fields(final, PEOPLE_COMMON_FIELDS),
}
Expand Down Expand Up @@ -251,7 +284,7 @@ def get_latest_approved_source(dref: Dref) -> Optional[tuple[DrefSummary.SourceM
Priority: Final Report > latest Operational Update > Dref itself.
"""
final_report = (
DrefFinalReport.objects.select_related("country", "disaster_type")
DrefFinalReport.objects.prefetch_related("needs_identified", "planned_interventions")
.filter(dref=dref, status=Dref.Status.APPROVED)
.order_by("-created_at")
.first()
Expand All @@ -260,7 +293,7 @@ def get_latest_approved_source(dref: Dref) -> Optional[tuple[DrefSummary.SourceM
return SOURCE_BY_MODEL[DrefFinalReport], final_report

latest_ops_update = (
DrefOperationalUpdate.objects.select_related("country", "disaster_type")
DrefOperationalUpdate.objects.prefetch_related("needs_identified")
.filter(dref=dref, status=Dref.Status.APPROVED)
.order_by(F("operational_update_number").desc(nulls_last=True), "-created_at")
.first()
Expand Down
3 changes: 2 additions & 1 deletion dref/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def generate_dref_summary(dref_id: int, overwrite: bool = False) -> DrefSummaryG
specific source passed in, so it self-corrects no matter which
approval triggered it or the order concurrent runs execute in.
"""
dref = Dref.objects.filter(id=dref_id).first()
# needs_identified feeds the summary when the Dref itself is the latest approved source.
dref = Dref.objects.prefetch_related("needs_identified").filter(id=dref_id).first()
if not dref:
logger.error("Dref not found for summary generation", extra=logger_context({"dref_id": dref_id}))
return DrefSummaryGenerationResult.SOURCE_NOT_FOUND
Expand Down
Loading
Loading