From 249b681153064c005f78b4cb5293aeb0fd780979 Mon Sep 17 00:00:00 2001 From: Hanne Moa Date: Wed, 22 Jan 2025 13:11:03 +0100 Subject: [PATCH] Add dropdowns to profiles page --- src/argus/htmx/incident/filter.py | 10 +- src/argus/htmx/notificationprofile/urls.py | 6 + src/argus/htmx/notificationprofile/views.py | 131 +++++++++++++++++- .../htmx/forms/checkbox_select_multiple.html | 1 + .../htmx/forms/dropdown_select_multiple.html | 12 +- .../_notificationprofile_buttons.html | 10 +- .../_notificationprofile_form.html | 4 +- .../notificationprofile_list.html | 2 +- src/argus/htmx/widgets.py | 11 ++ 9 files changed, 165 insertions(+), 22 deletions(-) diff --git a/src/argus/htmx/incident/filter.py b/src/argus/htmx/incident/filter.py index 7ede7cb19..1a26ce46f 100644 --- a/src/argus/htmx/incident/filter.py +++ b/src/argus/htmx/incident/filter.py @@ -1,4 +1,5 @@ from django import forms +from django.urls import reverse from argus.filter import get_filter_backend from argus.incident.models import SourceSystem @@ -18,9 +19,7 @@ class IncidentFilterForm(forms.Form): source = forms.MultipleChoiceField( widget=BadgeDropdownMultiSelect( attrs={"placeholder": "select sources..."}, - extra={ - "hx_get": "htmx:incident-filter", - }, + partial_get=None, ), choices=tuple(SourceSystem.objects.values_list("id", "name")), required=False, @@ -35,6 +34,11 @@ class IncidentFilterForm(forms.Form): required=False, ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # mollify tests + self.fields["source"].widget.partial_get = reverse("htmx:incident-filter") + def _tristate(self, onkey, offkey): on = self.cleaned_data.get(onkey, None) off = self.cleaned_data.get(offkey, None) diff --git a/src/argus/htmx/notificationprofile/urls.py b/src/argus/htmx/notificationprofile/urls.py index c3867c1ff..6dd696d1e 100644 --- a/src/argus/htmx/notificationprofile/urls.py +++ b/src/argus/htmx/notificationprofile/urls.py @@ -7,7 +7,13 @@ urlpatterns = [ path("", views.NotificationProfileListView.as_view(), name="notificationprofile-list"), path("create/", views.NotificationProfileCreateView.as_view(), name="notificationprofile-create"), + path("field/filters/", views.filters_form_view, name="notificationprofile-filters-field-create"), + path("field/destinations/", views.destinations_form_view, name="notificationprofile-destinations-field-create"), path("/", views.NotificationProfileDetailView.as_view(), name="notificationprofile-detail"), path("/update/", views.NotificationProfileUpdateView.as_view(), name="notificationprofile-update"), path("/delete/", views.NotificationProfileDeleteView.as_view(), name="notificationprofile-delete"), + path("/field/filters/", views.filters_form_view, name="notificationprofile-filters-field-update"), + path( + "/field/destinations/", views.destinations_form_view, name="notificationprofile-destinations-field-update" + ), ] diff --git a/src/argus/htmx/notificationprofile/views.py b/src/argus/htmx/notificationprofile/views.py index 9d8f0c98f..cd036d45a 100644 --- a/src/argus/htmx/notificationprofile/views.py +++ b/src/argus/htmx/notificationprofile/views.py @@ -5,10 +5,14 @@ """ from django import forms -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.urls import reverse +from django.views.decorators.http import require_GET from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView +from argus.htmx.request import HtmxHttpRequest +from argus.htmx.widgets import DropdownMultiSelect +from argus.notificationprofile.media import MEDIA_CLASSES_DICT from argus.notificationprofile.models import NotificationProfile, Timeslot, Filter, DestinationConfig @@ -18,7 +22,52 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -class NotificationProfileForm(NoColonMixin, forms.ModelForm): +class DestinationFieldMixin: + def _get_destination_choices(self, user): + choices = [] + for dc in DestinationConfig.objects.filter(user=user): + MediaPlugin = MEDIA_CLASSES_DICT[dc.media.slug] + label = MediaPlugin.get_label(dc) + choices.append((dc.id, f"{dc.media.name}: {label}")) + return choices + + def _init_destinations(self, user): + qs = DestinationConfig.objects.filter(user=user) + self.fields["destinations"].queryset = qs + if self.instance.id: + partial_get = reverse( + "htmx:notificationprofile-destinations-field-update", + kwargs={"pk": self.instance.pk}, + ) + else: + partial_get = reverse("htmx:notificationprofile-destinations-field-create") + self.fields["destinations"].widget = DropdownMultiSelect( + partial_get=partial_get, + attrs={"placeholder": "select destination..."}, + ) + self.fields["destinations"].choices = self._get_destination_choices(user) + + +class FilterFieldMixin: + def _init_filters(self, user): + qs = Filter.objects.filter(user=user) + self.fields["filters"].queryset = qs + + if self.instance.id: + partial_get = reverse( + "htmx:notificationprofile-filters-field-update", + kwargs={"pk": self.instance.pk}, + ) + else: + partial_get = reverse("htmx:notificationprofile-filters-field-create") + self.fields["filters"].widget = DropdownMultiSelect( + partial_get=partial_get, + attrs={"placeholder": "select filter..."}, + ) + self.fields["filters"].choices = tuple(qs.values_list("id", "name")) + + +class NotificationProfileForm(DestinationFieldMixin, FilterFieldMixin, NoColonMixin, forms.ModelForm): class Meta: model = NotificationProfile fields = ["name", "timeslot", "filters", "active", "destinations"] @@ -29,12 +78,74 @@ class Meta: def __init__(self, *args, **kwargs): user = kwargs.pop("user") super().__init__(*args, **kwargs) + self.fields["timeslot"].queryset = Timeslot.objects.filter(user=user) - self.fields["filters"].queryset = Filter.objects.filter(user=user) - self.fields["destinations"].queryset = DestinationConfig.objects.filter(user=user) self.fields["active"].widget.attrs["class"] = "checkbox checkbox-sm checkbox-accent border" + self.fields["active"].widget.attrs["autocomplete"] = "off" self.fields["name"].widget.attrs["class"] = "input input-bordered" + self.action = self.get_action() + + self._init_filters(user) + self._init_destinations(user) + + def get_action(self): + if self.instance and self.instance.pk: + return reverse("htmx:notificationprofile-update", kwargs={"pk": self.instance.pk}) + else: + return reverse("htmx:notificationprofile-create") + + +class NotificationProfileFilterForm(FilterFieldMixin, NoColonMixin, forms.ModelForm): + class Meta: + model = NotificationProfile + fields = ["filters"] + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user") + super().__init__(*args, **kwargs) + self._init_filters(user) + + +class NotificationProfileDestinationForm(DestinationFieldMixin, NoColonMixin, forms.ModelForm): + class Meta: + model = NotificationProfile + fields = ["destinations"] + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user") + super().__init__(*args, **kwargs) + self._init_destinations(user) + + +def _render_form_field(request: HtmxHttpRequest, form, partial_template_name, prefix=None): + # Not a view! + form = form(request.GET or None, user=request.user, prefix=prefix) + context = {"form": form} + return render(request, partial_template_name, context=context) + + +@require_GET +def filters_form_view(request: HtmxHttpRequest, pk: int = None): + prefix = f"npf{pk}" if pk else None + return _render_form_field( + request, + NotificationProfileFilterForm, + "htmx/notificationprofile/_notificationprofile_form.html", + prefix=prefix, + ) + + +@require_GET +def destinations_form_view(request: HtmxHttpRequest, pk: int = None): + prefix = f"npf{pk}" if pk else None + return _render_form_field( + request, + NotificationProfileDestinationForm, + "htmx/notificationprofile/_notificationprofile_form.html", + prefix=prefix, + ) + class NotificationProfileMixin: "Common functionality for all views" @@ -54,6 +165,8 @@ def get_queryset(self): return qs.filter(user_id=self.request.user.id) def get_template_names(self): + if self.request.htmx and self.partial_template_name: + return [self.partial_template_name] orig_app_label = self.model._meta.app_label orig_model_name = self.model._meta.model_name self.model._meta.app_label = "htmx/notificationprofile" @@ -76,6 +189,7 @@ class ChangeMixin: "Common functionality for create and update views" form_class = NotificationProfileForm + partial_template_name = "htmx/notificationprofile/_notificationprofile_form.html" def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -85,16 +199,21 @@ def get_form_kwargs(self): def form_valid(self, form): self.object = form.save(commit=False) self.object.user = self.request.user - self.object.save() return super().form_valid(form) + def get_prefix(self): + if self.object and self.object.pk: + prefix = f"npf{self.object.pk}" + return prefix + return self.prefix + class NotificationProfileListView(NotificationProfileMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) forms = [] for obj in self.get_queryset(): - form = NotificationProfileForm(None, user=self.request.user, instance=obj) + form = NotificationProfileForm(None, prefix=f"npf{obj.pk}", user=self.request.user, instance=obj) forms.append(form) context["form_list"] = forms return context diff --git a/src/argus/htmx/templates/htmx/forms/checkbox_select_multiple.html b/src/argus/htmx/templates/htmx/forms/checkbox_select_multiple.html index 36a57eb10..0b1d56fb1 100644 --- a/src/argus/htmx/templates/htmx/forms/checkbox_select_multiple.html +++ b/src/argus/htmx/templates/htmx/forms/checkbox_select_multiple.html @@ -4,6 +4,7 @@ {% endif %} {% if widget.wrap_label %} diff --git a/src/argus/htmx/templates/htmx/forms/dropdown_select_multiple.html b/src/argus/htmx/templates/htmx/forms/dropdown_select_multiple.html index 3abc553c5..c9ca60e8b 100644 --- a/src/argus/htmx/templates/htmx/forms/dropdown_select_multiple.html +++ b/src/argus/htmx/templates/htmx/forms/dropdown_select_multiple.html @@ -1,15 +1,17 @@ -