From 7b457d31c1219d2ae873395324ad026e4a624abd Mon Sep 17 00:00:00 2001 From: Mateo Durante Date: Mon, 10 Nov 2025 23:50:36 +0100 Subject: [PATCH] sla_reminder --- ngen/models/announcement.py | 26 ++++++++ ngen/serializers/tools.py | 6 ++ ngen/tasks.py | 51 +++++++++++++++ .../templates/reports/event_sla_reminder.html | 44 +++++++++++++ ngen/views/tools.py | 63 +++++++++++++++++++ project/urls.py | 1 + 6 files changed, 191 insertions(+) create mode 100644 ngen/templates/reports/event_sla_reminder.html diff --git a/ngen/models/announcement.py b/ngen/models/announcement.py index f23df1bd..b3300322 100644 --- a/ngen/models/announcement.py +++ b/ngen/models/announcement.py @@ -234,6 +234,32 @@ def send_contact_check_submitted(contact, networks, check): }, ) + @staticmethod + def send_event_sla_reminder(events): + """ + Sends an email reminder for events approaching SLA deadlines. + """ + subject = "[%s] %s" % ( + config.TEAM_NAME, + gettext_lazy("SLA Reminder"), + ) + + template = "reports/event_sla_reminder.html" + + Communication.send_mail( + subject, + Communication.render_template( + template, + extra_params={ + "events": events, + }, + ), + { + "to": [config.TEAM_EMAIL], + "from": config.EMAIL_SENDER, + }, + ) + class Announcement( AuditModelMixin, diff --git a/ngen/serializers/tools.py b/ngen/serializers/tools.py index f6940cab..0dc55e46 100644 --- a/ngen/serializers/tools.py +++ b/ngen/serializers/tools.py @@ -141,3 +141,9 @@ class AddressInfoSerializer(serializers.Serializer): with_entity = serializers.BooleanField(default=True) with_events = serializers.BooleanField(default=True) with_cases = serializers.BooleanField(default=True) + + +class TaskRunSerializer(serializers.Serializer): + task_name = serializers.CharField(max_length=255) + params = serializers.DictField(child=serializers.CharField(), required=False) + async_run = serializers.BooleanField(default=True) diff --git a/ngen/tasks.py b/ngen/tasks.py index 79204756..4cc16e22 100644 --- a/ngen/tasks.py +++ b/ngen/tasks.py @@ -549,3 +549,54 @@ def send_contact_check_submitted(check_id): networks=check.contact.networks.all(), check=check, ) + + +@shared_task(ignore_result=True, store_errors_even_if_ignored=True) +def event_sla_reminder(minutes=None): + """ + Envia un mail al team con los eventos que estan por superar el SLA. + """ + + # Get the interval for the periodic task + if minutes is not None: + try: + minutes = int(minutes) + except ValueError: + raise TaskFailure("Minutes parameter must be an integer.") + timedeltavalue = timezone.timedelta(minutes=minutes) + else: + task = PeriodicTask.objects.filter(task="ngen.tasks.event_sla_reminder").first() + if task and task.interval: + interval = task.interval + period = ( + interval.period + ) # 'seconds', 'minutes', 'hours', 'minutes', 'weeks' + every = interval.every + + # Create a timedelta object based on the interval + timedelta_kwargs = {period: every} + timedeltavalue = timezone.timedelta(**timedelta_kwargs) + else: + default_value = 14 + timedeltavalue = timezone.timedelta(minutes=default_value) + logger.warning( + f"No interval found for the periodic task 'ngen.tasks.event_sla_reminder'. Using default value: {default_value} days." + ) + # search attend SLA + events = ( + ngen.models.Event.objects.filter( + case=None + # date__gt=timezone.now() - (F("priority__attend_time") - timedeltavalue), + # date__lte=timezone.now() - F("priority__attend_time"), + ) + .exclude(tags__slug="no_sla") + .order_by("priority__severity") + ) + if events.exists(): + Communication.send_event_sla_reminder(events) + return { + "status": "success", + "message": f"SLA reminder sent for {events.count()} events", + } + else: + return {"status": "success", "message": "No events approaching SLA deadlines"} diff --git a/ngen/templates/reports/event_sla_reminder.html b/ngen/templates/reports/event_sla_reminder.html new file mode 100644 index 00000000..abfee904 --- /dev/null +++ b/ngen/templates/reports/event_sla_reminder.html @@ -0,0 +1,44 @@ +{% extends 'reports/content_base.html' %} +{% load i18n %} +{% language lang %} + + {% block content_header %} +
+

{% translate 'Hello' %},

+

+ {% blocktranslate trimmed %} + Below are all your unattended events. + {% endblocktranslate %} +

+
+ {% endblock %} + + {% block content_body %} +
+
{% translate 'Unnatended Events' %}: {{ events|length }}
+ + + + + + + + + + + + + {% for event in events %} + + + + + + + + {% endfor %} + +
{% translate 'Date' %}{% translate 'Event' %}{% translate 'Priority' %}{% translate 'Taxonomy' %}{% translate 'Affected resources' %}
{{ event.date | date:"Y-m-d" }}{{ event.uuid|stringformat:"s"|slice:":8" }}{{ event.priority.name|stringformat:"s"|slice:":8" }}{{ event.taxonomy|stringformat:"s"|slice:":12" }}{{ event.address|stringformat:"s"|slice:":18" }}
+
+ {% endblock %} +{% endlanguage %} diff --git a/ngen/views/tools.py b/ngen/views/tools.py index a3f0e1cf..b60d3a75 100644 --- a/ngen/views/tools.py +++ b/ngen/views/tools.py @@ -368,3 +368,66 @@ def get(self, request): }, status=status.HTTP_200_OK, ) + + +class TaskRunView(APIView): + """ + View to trigger a specific task run manually. + """ + + permission_classes = [permissions.IsAdminUser] + serializer_class = serializers.TaskRunSerializer + + def post(self, request): + task_name = request.data.get("task_name") + async_run = request.data.get("async_run", True) + params = request.data.get("params", {}) + available_tasks = [ + "attend_cases", + "solve_cases", + "case_renotification", + "contact_summary", + "create_cases_for_matching_events", + "enrich_artifact", + "whois_lookup", + "internal_address_info", + "export_events_for_email_task", + "retest_event_kintun", + "async_send_email", + "retrieve_emails", + "send_contact_checks", + "send_contact_check_reminder", + "send_contact_check_submitted", + "event_sla_reminder", + ] + # get function form the module + task = None + if task_name in available_tasks: + print("Looking for task:", task_name) + task = getattr(models.tasks, task_name, None) + + if not task: + return Response( + {"message": f"Task {task_name} not found."}, + status=status.HTTP_404_NOT_FOUND, + ) + + # run the task + if async_run: + t = task.delay(**params) + return Response( + { + "task_id": t.id, + "message": f"Task {task_name} triggered successfully.", + }, + status=status.HTTP_200_OK, + ) + else: + res = task(**params) + return Response( + { + "message": f"Task {task_name} executed successfully.", + "result": res, + }, + status=status.HTTP_200_OK, + ) diff --git a/project/urls.py b/project/urls.py index 4a4a763f..7b1ec133 100644 --- a/project/urls.py +++ b/project/urls.py @@ -258,6 +258,7 @@ path( "api/result//", views.TaskStatusView.as_view(), name="task_status" ), + path("api/task/run", views.TaskRunView.as_view(), name="task_run"), path( "api/version/", views.VersionView.as_view(),