Skip to content

Commit

Permalink
H: Add dropdowns for filter and destination
Browse files Browse the repository at this point in the history
  • Loading branch information
hmpf committed Jan 14, 2025
1 parent f9b3e15 commit 8e7fa6f
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 55 deletions.
4 changes: 1 addition & 3 deletions src/argus/htmx/incident/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ class IncidentFilterForm(forms.Form):
source = forms.MultipleChoiceField(
widget=BadgeDropdownMultiSelect(
attrs={"placeholder": "select sources..."},
extra={
"hx_get": "htmx:incident-filter",
},
partial_get="htmx:incident-filter",
),
choices=tuple(SourceSystem.objects.values_list("id", "name")),
required=False,
Expand Down
2 changes: 2 additions & 0 deletions src/argus/htmx/notificationprofile/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
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"),
path("field/destinations/", views.destinations_form_view, name="notificationprofile-destinations-field"),
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"),
Expand Down
86 changes: 82 additions & 4 deletions src/argus/htmx/notificationprofile/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
"""

from django import forms
from django.shortcuts import redirect
from django.shortcuts import redirect, render
from django.urls import reverse
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 +21,17 @@ 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


class NotificationProfileForm(DestinationFieldMixin, NoColonMixin, forms.ModelForm):
class Meta:
model = NotificationProfile
fields = ["name", "timeslot", "filters", "active", "destinations"]
Expand All @@ -29,12 +42,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["name"].widget.attrs["class"] = "input input-bordered"

self.fields["filters"].queryset = Filter.objects.filter(user=user)
self.fields["filters"].widget = DropdownMultiSelect(
attrs={"placeholder": "select filter..."},
partial_get="htmx:notificationprofile-filters-field",
)
self.fields["filters"].choices = tuple(Filter.objects.filter(user=user).values_list("id", "name"))

self.fields["destinations"].queryset = DestinationConfig.objects.filter(user=user)
self.fields["destinations"].widget = DropdownMultiSelect(
attrs={"placeholder": "select destination..."},
partial_get="htmx:notificationprofile-destinations-field",
)
self.fields["destinations"].choices = self._get_destination_choices(user)


class NotificationProfileFilterForm(NoColonMixin, forms.ModelForm):
class Meta:
model = NotificationProfile
fields = ["filters"]

def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super().__init__(*args, **kwargs)
self.fields["filters"].widget = DropdownMultiSelect(
partial_get="htmx:notificationprofile-filters-field",
attrs={"placeholder": "select filter..."},
)
self.fields["filters"].choices = tuple(Filter.objects.filter(user=user).values_list("id", "name"))


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.fields["destinations"].widget = DropdownMultiSelect(
partial_get="htmx:notificationprofile-destinations-field",
attrs={"placeholder": "select destination..."},
)
self.fields["destinations"].choices = self._get_destination_choices(user)


def _render_form_field(request: HtmxHttpRequest, form, partial_template_name):
# Not a view!
form = form(request.GET or None, user=request.user)
context = {"form": form}
return render(request, partial_template_name, context=context)


def filters_form_view(request: HtmxHttpRequest):
return _render_form_field(
request, NotificationProfileFilterForm, "htmx/notificationprofile/_notificationprofile_form.html"
)


def destinations_form_view(request: HtmxHttpRequest):
return _render_form_field(
request, NotificationProfileDestinationForm, "htmx/notificationprofile/_notificationprofile_form.html"
)


class NotificationProfileMixin:
"Common functionality for all views"
Expand All @@ -54,6 +129,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 @@ -71,6 +148,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 Down
66 changes: 23 additions & 43 deletions src/argus/htmx/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
}

/*
! tailwindcss v3.4.16 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
*/

/*
Expand Down Expand Up @@ -1137,7 +1137,7 @@ html {
position: relative;
display: grid;
overflow: hidden;
grid-template-rows: auto 0fr;
grid-template-rows: max-content 0fr;
transition: grid-template-rows 0.2s;
width: 100%;
border-radius: var(--rounded-box, 1rem);
Expand All @@ -1159,6 +1159,13 @@ html {
opacity: 0;
}

:where(.collapse > input[type="checkbox"]),
:where(.collapse > input[type="radio"]) {
height: 100%;
width: 100%;
z-index: 1;
}

.collapse-content {
visibility: hidden;
grid-column-start: 1;
Expand All @@ -1175,12 +1182,12 @@ html {
.collapse[open],
.collapse-open,
.collapse:focus:not(.collapse-close) {
grid-template-rows: auto 1fr;
grid-template-rows: max-content 1fr;
}

.collapse:not(.collapse-close):has(> input[type="checkbox"]:checked),
.collapse:not(.collapse-close):has(> input[type="radio"]:checked) {
grid-template-rows: auto 1fr;
grid-template-rows: max-content 1fr;
}

.collapse[open] > .collapse-content,
Expand Down Expand Up @@ -2097,12 +2104,6 @@ input.tab:checked + .tab-content,
color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)));
}

.btm-nav > *:where(.active) {
border-top-width: 2px;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}

.btm-nav > * .label {
font-size: 1rem;
line-height: 1.5rem;
Expand Down Expand Up @@ -2604,15 +2605,9 @@ details.collapse summary::-webkit-details-marker {
position: relative;
}

:where(.collapse > input[type="checkbox"]),
:where(.collapse > input[type="radio"]) {
z-index: 1;
}

.collapse-title,
:where(.collapse > input[type="checkbox"]),
:where(.collapse > input[type="radio"]) {
width: 100%;
padding: 1rem;
padding-inline-end: 3rem;
min-height: 3.75rem;
Expand Down Expand Up @@ -2789,13 +2784,13 @@ details.collapse summary::-webkit-details-marker {
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
}

.loading-spinner {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
}

.loading-lg {
Expand Down Expand Up @@ -2978,6 +2973,10 @@ details.collapse summary::-webkit-details-marker {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}

.modal-action:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 1;
}

@keyframes modal-pop {
0% {
opacity: 0;
Expand Down Expand Up @@ -3327,13 +3326,6 @@ details.collapse summary::-webkit-details-marker {
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}

.table-zebra tr.active,
.table-zebra tr.active:nth-child(even),
.table-zebra-zebra tbody tr:nth-child(even) {
--tw-bg-opacity: 1;
background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
}

.table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) {
border-bottom-width: 1px;
--tw-border-opacity: 1;
Expand Down Expand Up @@ -3464,22 +3456,6 @@ details.collapse summary::-webkit-details-marker {
}
}

.btm-nav-xs > *:where(.active) {
border-top-width: 1px;
}

.btm-nav-sm > *:where(.active) {
border-top-width: 2px;
}

.btm-nav-md > *:where(.active) {
border-top-width: 2px;
}

.btm-nav-lg > *:where(.active) {
border-top-width: 4px;
}

.btn-xs {
height: 1.5rem;
min-height: 1.5rem;
Expand Down Expand Up @@ -4322,6 +4298,10 @@ details.collapse summary::-webkit-details-marker {
flex-grow: 1;
}

.border-collapse {
border-collapse: collapse;
}

.border-separate {
border-collapse: separate;
}
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="{% url 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
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 8e7fa6f

Please sign in to comment.