From bb97c71fe4d1ab232ae59bf63136bf68f6c34d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerem=C3=ADas=20Pretto?= Date: Sat, 4 Apr 2026 01:22:08 +0200 Subject: [PATCH] feat: make Event.date nullable (no default) and add Case.notification_delay_seconds for MTTR - Event.date: removed default=timezone.now, now null/blank. Set by IntelMQ (time.observation) or GUI - Case.notification_delay_seconds: calculated on first transition to attended state as (attend_date - event.date) - CaseSerializer: expose notification_delay_seconds as read-only - Migration 0024: AlterField event.date + AddField case.notification_delay_seconds --- ...t_date_nullable_case_notification_delay.py | 30 +++++++++++++++++++ ngen/models/case.py | 27 +++++++++++++++++ ngen/serializers/case.py | 2 ++ 3 files changed, 59 insertions(+) create mode 100644 ngen/migrations/0024_event_date_nullable_case_notification_delay.py diff --git a/ngen/migrations/0024_event_date_nullable_case_notification_delay.py b/ngen/migrations/0024_event_date_nullable_case_notification_delay.py new file mode 100644 index 00000000..d5db9ce9 --- /dev/null +++ b/ngen/migrations/0024_event_date_nullable_case_notification_delay.py @@ -0,0 +1,30 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ngen", "0023_alter_casetemplate_case_tlp_and_more_squashed_0025_remove_casetemplate_priority_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="date", + field=models.DateTimeField( + blank=True, + default=None, + null=True, + ), + ), + migrations.AddField( + model_name="case", + name="notification_delay_seconds", + field=models.FloatField( + blank=True, + default=None, + help_text="Seconds between the event date and when the case was first attended. Used to measure MTTR.", + null=True, + ), + ), + ] diff --git a/ngen/models/case.py b/ngen/models/case.py index 495595f1..30fb13c3 100644 --- a/ngen/models/case.py +++ b/ngen/models/case.py @@ -102,6 +102,15 @@ class Case( attend_date = models.DateTimeField(null=True, blank=True, default=None) solve_date = models.DateTimeField(null=True, blank=True, default=None) + notification_delay_seconds = models.FloatField( + null=True, + blank=True, + default=None, + help_text=gettext_lazy( + "Seconds between the event date and when the case was first attended. " + "Used to measure MTTR." + ), + ) report_message_id = models.CharField( max_length=255, null=True, blank=True, default=None @@ -242,6 +251,22 @@ def delete_events(self): for event in self.events.all(): event.delete() + def _calculate_notification_delay(self): + """Calculate seconds between the earliest event date and the current + attend_date (when the case is first opened/responded). + Only calculated once (first transition to attended state).""" + if self.notification_delay_seconds is not None: + return + if not self.attend_date: + return + events = self._temp_events if self._temp_events else self.events.all() + for event in events: + if event.date: + self.notification_delay_seconds = ( + self.attend_date - event.date + ).total_seconds() + return + @hook(BEFORE_CREATE) def before_create(self): self.report_message_id = make_msgid(domain=DNS_NAME) @@ -250,6 +275,7 @@ def before_create(self): if self.state.attended: self.attend_date = timezone.now() self.solve_date = None + self._calculate_notification_delay() elif self.state.solved: self.solve_date = timezone.now() @@ -285,6 +311,7 @@ def before_update(self): if self.state.attended: self.attend_date = timezone.now() self.solve_date = None + self._calculate_notification_delay() self.communicate_open() elif self.state.solved: self.solve_date = timezone.now() diff --git a/ngen/serializers/case.py b/ngen/serializers/case.py index d1bd66db..b8b9cf9c 100644 --- a/ngen/serializers/case.py +++ b/ngen/serializers/case.py @@ -284,6 +284,7 @@ class Meta: "blocked", "merged", "tags", + "notification_delay_seconds", ) read_only_fields = [ "attend_date", @@ -293,6 +294,7 @@ class Meta: "created_by", "notification_count", "blocked", + "notification_delay_seconds", ] def get_evidence(self, obj):