Skip to content

Commit

Permalink
Add dropdowns to profiles page
Browse files Browse the repository at this point in the history
  • Loading branch information
hmpf authored Jan 22, 2025
1 parent 86b8f53 commit 249b681
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 22 deletions.
10 changes: 7 additions & 3 deletions src/argus/htmx/incident/filter.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/argus/htmx/notificationprofile/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<pk>/", views.NotificationProfileDetailView.as_view(), name="notificationprofile-detail"),
path("<pk>/update/", views.NotificationProfileUpdateView.as_view(), name="notificationprofile-update"),
path("<pk>/delete/", views.NotificationProfileDeleteView.as_view(), name="notificationprofile-delete"),
path("<pk>/field/filters/", views.filters_form_view, name="notificationprofile-filters-field-update"),
path(
"<pk>/field/destinations/", views.destinations_form_view, name="notificationprofile-destinations-field-update"
),
]
131 changes: 125 additions & 6 deletions src/argus/htmx/notificationprofile/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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"]
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{% endif %}
<input type="{{ widget.type }}"
name="{{ widget.name }}"
autocomplete="off"
{% if widget.value != None %} class="checkbox checkbox-sm checkbox-primary border" value="{{ widget.value|stringformat:'s' }}"{% endif %}
{% include "django/forms/widgets/attrs.html" %}>
{% if widget.wrap_label %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<div class="dropdown dropdown-bottom "
<div class="dropdown dropdown-bottom"
{% block field_control %}
hx-trigger="change from:#{{ widget.attrs.id }}"
id="dropdown-{{ widget.attrs.id }}"
hx-trigger="change from:(find #{{ widget.attrs.id }})"
hx-swap="outerHTML"
hx-target="find .show-selected-box"
hx-select=".show-selected-box"
{% if widget.extra.hx_get %}hx-get="{% url widget.extra.hx_get %}"{% endif %}
hx-select="#dropdown-{{ widget.attrs.id }} .show-selected-box"
hx-get="{{ widget.partial_get }}"
hx-include="find #{{ widget.attrs.id }}"
{% endblock field_control %}>
<div tabindex="0"
role="button"
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">
<p class="text-base-content/50">{{ widget.attrs.placeholder }}</p>
{% if not widget.has_selected %}<p class="text-base-content/50">{{ widget.attrs.placeholder }}</p>{% endif %}
{% for _, options, _ in widget.optgroups %}
{% for option in options %}
{% if option.selected %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<div class="card-actions justify-end">
<input class="btn btn-primary" type="submit" value="Save">
<button class="contents">
<a class="btn btn-primary"
href="{% url "htmx:notificationprofile-delete" pk=object.pk %}">Delete</a>
</button>
{% if object.pk %}
<button class="contents">
<a class="btn btn-primary"
href="{% url "htmx:notificationprofile-delete" pk=object.pk %}">Delete</a>
</button>
{% endif %}
</div>
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<section class="card-body">
<form method="post"
action="{% url "htmx:notificationprofile-update" pk=form.instance.pk %}"
class="flex flex-row gap-4">
<form method="post" action="{{ form.action }}" class="flex flex-row gap-4">
{% csrf_token %}
{{ form.as_div }}
{% include "./_notificationprofile_buttons.html" with object=form.instance %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
href="{% url "htmx:notificationprofile-create" %}">Create new profile</a>
</button>
{% for form in form_list %}
<div class="card my-4 bg-base-100 glass shadow-2xl">{% include "./_notificationprofile_form.html" %}</div>
<div class="card my-4 bg-base-100 shadow-2xl">{% include "./_notificationprofile_form.html" %}</div>
{% endfor %}
</div>
{% endblock profile_main %}
11 changes: 11 additions & 0 deletions src/argus/htmx/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,19 @@ class DropdownMultiSelect(ExtraWidgetMixin, forms.CheckboxSelectMultiple):
template_name = "htmx/forms/dropdown_select_multiple.html"
option_template_name = "htmx/forms/checkbox_select_multiple.html"

def __init__(self, partial_get, **kwargs):
super().__init__(**kwargs)
self.partial_get = partial_get

def __deepcopy__(self, memo):
obj = super().__deepcopy__(memo)
obj.partial_get = self.partial_get
memo[id(self)] = obj
return obj

def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context["widget"]["partial_get"] = self.partial_get
widget_value = context["widget"]["value"]
context["widget"]["has_selected"] = self.has_selected(name, widget_value, attrs)
return context
Expand Down

0 comments on commit 249b681

Please sign in to comment.