Skip to content

Commit 249b681

Browse files
authored
Add dropdowns to profiles page
1 parent 86b8f53 commit 249b681

File tree

9 files changed

+165
-22
lines changed

9 files changed

+165
-22
lines changed

src/argus/htmx/incident/filter.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import forms
2+
from django.urls import reverse
23

34
from argus.filter import get_filter_backend
45
from argus.incident.models import SourceSystem
@@ -18,9 +19,7 @@ class IncidentFilterForm(forms.Form):
1819
source = forms.MultipleChoiceField(
1920
widget=BadgeDropdownMultiSelect(
2021
attrs={"placeholder": "select sources..."},
21-
extra={
22-
"hx_get": "htmx:incident-filter",
23-
},
22+
partial_get=None,
2423
),
2524
choices=tuple(SourceSystem.objects.values_list("id", "name")),
2625
required=False,
@@ -35,6 +34,11 @@ class IncidentFilterForm(forms.Form):
3534
required=False,
3635
)
3736

37+
def __init__(self, *args, **kwargs):
38+
super().__init__(*args, **kwargs)
39+
# mollify tests
40+
self.fields["source"].widget.partial_get = reverse("htmx:incident-filter")
41+
3842
def _tristate(self, onkey, offkey):
3943
on = self.cleaned_data.get(onkey, None)
4044
off = self.cleaned_data.get(offkey, None)

src/argus/htmx/notificationprofile/urls.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
urlpatterns = [
88
path("", views.NotificationProfileListView.as_view(), name="notificationprofile-list"),
99
path("create/", views.NotificationProfileCreateView.as_view(), name="notificationprofile-create"),
10+
path("field/filters/", views.filters_form_view, name="notificationprofile-filters-field-create"),
11+
path("field/destinations/", views.destinations_form_view, name="notificationprofile-destinations-field-create"),
1012
path("<pk>/", views.NotificationProfileDetailView.as_view(), name="notificationprofile-detail"),
1113
path("<pk>/update/", views.NotificationProfileUpdateView.as_view(), name="notificationprofile-update"),
1214
path("<pk>/delete/", views.NotificationProfileDeleteView.as_view(), name="notificationprofile-delete"),
15+
path("<pk>/field/filters/", views.filters_form_view, name="notificationprofile-filters-field-update"),
16+
path(
17+
"<pk>/field/destinations/", views.destinations_form_view, name="notificationprofile-destinations-field-update"
18+
),
1319
]

src/argus/htmx/notificationprofile/views.py

+125-6
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
"""
66

77
from django import forms
8-
from django.shortcuts import redirect
8+
from django.shortcuts import redirect, render
99
from django.urls import reverse
10+
from django.views.decorators.http import require_GET
1011
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
1112

13+
from argus.htmx.request import HtmxHttpRequest
14+
from argus.htmx.widgets import DropdownMultiSelect
15+
from argus.notificationprofile.media import MEDIA_CLASSES_DICT
1216
from argus.notificationprofile.models import NotificationProfile, Timeslot, Filter, DestinationConfig
1317

1418

@@ -18,7 +22,52 @@ def __init__(self, *args, **kwargs):
1822
super().__init__(*args, **kwargs)
1923

2024

21-
class NotificationProfileForm(NoColonMixin, forms.ModelForm):
25+
class DestinationFieldMixin:
26+
def _get_destination_choices(self, user):
27+
choices = []
28+
for dc in DestinationConfig.objects.filter(user=user):
29+
MediaPlugin = MEDIA_CLASSES_DICT[dc.media.slug]
30+
label = MediaPlugin.get_label(dc)
31+
choices.append((dc.id, f"{dc.media.name}: {label}"))
32+
return choices
33+
34+
def _init_destinations(self, user):
35+
qs = DestinationConfig.objects.filter(user=user)
36+
self.fields["destinations"].queryset = qs
37+
if self.instance.id:
38+
partial_get = reverse(
39+
"htmx:notificationprofile-destinations-field-update",
40+
kwargs={"pk": self.instance.pk},
41+
)
42+
else:
43+
partial_get = reverse("htmx:notificationprofile-destinations-field-create")
44+
self.fields["destinations"].widget = DropdownMultiSelect(
45+
partial_get=partial_get,
46+
attrs={"placeholder": "select destination..."},
47+
)
48+
self.fields["destinations"].choices = self._get_destination_choices(user)
49+
50+
51+
class FilterFieldMixin:
52+
def _init_filters(self, user):
53+
qs = Filter.objects.filter(user=user)
54+
self.fields["filters"].queryset = qs
55+
56+
if self.instance.id:
57+
partial_get = reverse(
58+
"htmx:notificationprofile-filters-field-update",
59+
kwargs={"pk": self.instance.pk},
60+
)
61+
else:
62+
partial_get = reverse("htmx:notificationprofile-filters-field-create")
63+
self.fields["filters"].widget = DropdownMultiSelect(
64+
partial_get=partial_get,
65+
attrs={"placeholder": "select filter..."},
66+
)
67+
self.fields["filters"].choices = tuple(qs.values_list("id", "name"))
68+
69+
70+
class NotificationProfileForm(DestinationFieldMixin, FilterFieldMixin, NoColonMixin, forms.ModelForm):
2271
class Meta:
2372
model = NotificationProfile
2473
fields = ["name", "timeslot", "filters", "active", "destinations"]
@@ -29,12 +78,74 @@ class Meta:
2978
def __init__(self, *args, **kwargs):
3079
user = kwargs.pop("user")
3180
super().__init__(*args, **kwargs)
81+
3282
self.fields["timeslot"].queryset = Timeslot.objects.filter(user=user)
33-
self.fields["filters"].queryset = Filter.objects.filter(user=user)
34-
self.fields["destinations"].queryset = DestinationConfig.objects.filter(user=user)
3583
self.fields["active"].widget.attrs["class"] = "checkbox checkbox-sm checkbox-accent border"
84+
self.fields["active"].widget.attrs["autocomplete"] = "off"
3685
self.fields["name"].widget.attrs["class"] = "input input-bordered"
3786

87+
self.action = self.get_action()
88+
89+
self._init_filters(user)
90+
self._init_destinations(user)
91+
92+
def get_action(self):
93+
if self.instance and self.instance.pk:
94+
return reverse("htmx:notificationprofile-update", kwargs={"pk": self.instance.pk})
95+
else:
96+
return reverse("htmx:notificationprofile-create")
97+
98+
99+
class NotificationProfileFilterForm(FilterFieldMixin, NoColonMixin, forms.ModelForm):
100+
class Meta:
101+
model = NotificationProfile
102+
fields = ["filters"]
103+
104+
def __init__(self, *args, **kwargs):
105+
user = kwargs.pop("user")
106+
super().__init__(*args, **kwargs)
107+
self._init_filters(user)
108+
109+
110+
class NotificationProfileDestinationForm(DestinationFieldMixin, NoColonMixin, forms.ModelForm):
111+
class Meta:
112+
model = NotificationProfile
113+
fields = ["destinations"]
114+
115+
def __init__(self, *args, **kwargs):
116+
user = kwargs.pop("user")
117+
super().__init__(*args, **kwargs)
118+
self._init_destinations(user)
119+
120+
121+
def _render_form_field(request: HtmxHttpRequest, form, partial_template_name, prefix=None):
122+
# Not a view!
123+
form = form(request.GET or None, user=request.user, prefix=prefix)
124+
context = {"form": form}
125+
return render(request, partial_template_name, context=context)
126+
127+
128+
@require_GET
129+
def filters_form_view(request: HtmxHttpRequest, pk: int = None):
130+
prefix = f"npf{pk}" if pk else None
131+
return _render_form_field(
132+
request,
133+
NotificationProfileFilterForm,
134+
"htmx/notificationprofile/_notificationprofile_form.html",
135+
prefix=prefix,
136+
)
137+
138+
139+
@require_GET
140+
def destinations_form_view(request: HtmxHttpRequest, pk: int = None):
141+
prefix = f"npf{pk}" if pk else None
142+
return _render_form_field(
143+
request,
144+
NotificationProfileDestinationForm,
145+
"htmx/notificationprofile/_notificationprofile_form.html",
146+
prefix=prefix,
147+
)
148+
38149

39150
class NotificationProfileMixin:
40151
"Common functionality for all views"
@@ -54,6 +165,8 @@ def get_queryset(self):
54165
return qs.filter(user_id=self.request.user.id)
55166

56167
def get_template_names(self):
168+
if self.request.htmx and self.partial_template_name:
169+
return [self.partial_template_name]
57170
orig_app_label = self.model._meta.app_label
58171
orig_model_name = self.model._meta.model_name
59172
self.model._meta.app_label = "htmx/notificationprofile"
@@ -76,6 +189,7 @@ class ChangeMixin:
76189
"Common functionality for create and update views"
77190

78191
form_class = NotificationProfileForm
192+
partial_template_name = "htmx/notificationprofile/_notificationprofile_form.html"
79193

80194
def get_form_kwargs(self):
81195
kwargs = super().get_form_kwargs()
@@ -85,16 +199,21 @@ def get_form_kwargs(self):
85199
def form_valid(self, form):
86200
self.object = form.save(commit=False)
87201
self.object.user = self.request.user
88-
self.object.save()
89202
return super().form_valid(form)
90203

204+
def get_prefix(self):
205+
if self.object and self.object.pk:
206+
prefix = f"npf{self.object.pk}"
207+
return prefix
208+
return self.prefix
209+
91210

92211
class NotificationProfileListView(NotificationProfileMixin, ListView):
93212
def get_context_data(self, **kwargs):
94213
context = super().get_context_data(**kwargs)
95214
forms = []
96215
for obj in self.get_queryset():
97-
form = NotificationProfileForm(None, user=self.request.user, instance=obj)
216+
form = NotificationProfileForm(None, prefix=f"npf{obj.pk}", user=self.request.user, instance=obj)
98217
forms.append(form)
99218
context["form_list"] = forms
100219
return context

src/argus/htmx/templates/htmx/forms/checkbox_select_multiple.html

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{% endif %}
55
<input type="{{ widget.type }}"
66
name="{{ widget.name }}"
7+
autocomplete="off"
78
{% if widget.value != None %} class="checkbox checkbox-sm checkbox-primary border" value="{{ widget.value|stringformat:'s' }}"{% endif %}
89
{% include "django/forms/widgets/attrs.html" %}>
910
{% if widget.wrap_label %}

src/argus/htmx/templates/htmx/forms/dropdown_select_multiple.html

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
<div class="dropdown dropdown-bottom "
1+
<div class="dropdown dropdown-bottom"
22
{% block field_control %}
3-
hx-trigger="change from:#{{ widget.attrs.id }}"
3+
id="dropdown-{{ widget.attrs.id }}"
4+
hx-trigger="change from:(find #{{ widget.attrs.id }})"
45
hx-swap="outerHTML"
56
hx-target="find .show-selected-box"
6-
hx-select=".show-selected-box"
7-
{% if widget.extra.hx_get %}hx-get="{% url widget.extra.hx_get %}"{% endif %}
7+
hx-select="#dropdown-{{ widget.attrs.id }} .show-selected-box"
8+
hx-get="{{ widget.partial_get }}"
9+
hx-include="find #{{ widget.attrs.id }}"
810
{% endblock field_control %}>
911
<div tabindex="0"
1012
role="button"
1113
class="show-selected-box input input-accent input-bordered input-md border overflow-y-auto min-h-8 h-auto max-h-16 max-w-xs leading-tight flex flex-wrap items-center gap-0.5">
12-
<p class="text-base-content/50">{{ widget.attrs.placeholder }}</p>
14+
{% if not widget.has_selected %}<p class="text-base-content/50">{{ widget.attrs.placeholder }}</p>{% endif %}
1315
{% for _, options, _ in widget.optgroups %}
1416
{% for option in options %}
1517
{% if option.selected %}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<div class="card-actions justify-end">
22
<input class="btn btn-primary" type="submit" value="Save">
3-
<button class="contents">
4-
<a class="btn btn-primary"
5-
href="{% url "htmx:notificationprofile-delete" pk=object.pk %}">Delete</a>
6-
</button>
3+
{% if object.pk %}
4+
<button class="contents">
5+
<a class="btn btn-primary"
6+
href="{% url "htmx:notificationprofile-delete" pk=object.pk %}">Delete</a>
7+
</button>
8+
{% endif %}
79
</div>

src/argus/htmx/templates/htmx/notificationprofile/_notificationprofile_form.html

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<section class="card-body">
2-
<form method="post"
3-
action="{% url "htmx:notificationprofile-update" pk=form.instance.pk %}"
4-
class="flex flex-row gap-4">
2+
<form method="post" action="{{ form.action }}" class="flex flex-row gap-4">
53
{% csrf_token %}
64
{{ form.as_div }}
75
{% include "./_notificationprofile_buttons.html" with object=form.instance %}

src/argus/htmx/templates/htmx/notificationprofile/notificationprofile_list.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
href="{% url "htmx:notificationprofile-create" %}">Create new profile</a>
77
</button>
88
{% for form in form_list %}
9-
<div class="card my-4 bg-base-100 glass shadow-2xl">{% include "./_notificationprofile_form.html" %}</div>
9+
<div class="card my-4 bg-base-100 shadow-2xl">{% include "./_notificationprofile_form.html" %}</div>
1010
{% endfor %}
1111
</div>
1212
{% endblock profile_main %}

src/argus/htmx/widgets.py

+11
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,19 @@ class DropdownMultiSelect(ExtraWidgetMixin, forms.CheckboxSelectMultiple):
2222
template_name = "htmx/forms/dropdown_select_multiple.html"
2323
option_template_name = "htmx/forms/checkbox_select_multiple.html"
2424

25+
def __init__(self, partial_get, **kwargs):
26+
super().__init__(**kwargs)
27+
self.partial_get = partial_get
28+
29+
def __deepcopy__(self, memo):
30+
obj = super().__deepcopy__(memo)
31+
obj.partial_get = self.partial_get
32+
memo[id(self)] = obj
33+
return obj
34+
2535
def get_context(self, name, value, attrs):
2636
context = super().get_context(name, value, attrs)
37+
context["widget"]["partial_get"] = self.partial_get
2738
widget_value = context["widget"]["value"]
2839
context["widget"]["has_selected"] = self.has_selected(name, widget_value, attrs)
2940
return context

0 commit comments

Comments
 (0)