Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions src/argus/htmx/incident/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
QuerySetFilter = filter_backend.QuerySetFilter
LOG = logging.getLogger(__name__)

MIN_LEVEL = min(Level).value
MAX_LEVEL = max(Level).value


class RangeInput(forms.NumberInput):
template_name = "django/forms/widgets/range.html"
Expand Down Expand Up @@ -78,9 +81,9 @@ class IncidentFilterForm(TagFieldMixin, forms.Form):
help_text='Press "Enter" after each completed tag',
)
maxlevel = forms.IntegerField(
widget=RangeInput(attrs={"step": "1", "min": min(Level).value, "max": max(Level).value}),
widget=RangeInput(attrs={"step": "1", "min": MIN_LEVEL, "max": MAX_LEVEL}),
label="Level <=",
initial=max(Level).value,
initial=MAX_LEVEL,
required=False,
)

Expand All @@ -90,7 +93,7 @@ class IncidentFilterForm(TagFieldMixin, forms.Form):
"sourceSystemIds": [],
"source_types": [],
"tags": [],
"maxlevel": max(Level).value,
"maxlevel": MAX_LEVEL,
}

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -148,8 +151,13 @@ def to_filterblob(self):

filterblob = {}

filterblob["open"] = self._open_tristate()
filterblob["acked"] = self._acked_tristate()
open = self._open_tristate()
if open is not None:
filterblob["open"] = open

acked = self._acked_tristate()
if acked is not None:
filterblob["acked"] = acked

sourceSystemIds = self.cleaned_data.get("sourceSystemIds", [])
if sourceSystemIds:
Expand Down Expand Up @@ -199,7 +207,8 @@ def incident_list_filter(request, qs, use_empty_filter=False):
form = IncidentFilterForm(_convert_filterblob(filter_obj.filter))
LOG.debug("using stored filter: %s", filter_obj.filter)
else:
form_data = _normalize_form_data(request)
raw_data = request.POST if request.method == "POST" else request.GET
form_data = _normalize_form_data(raw_data)
if request.method == "POST":
form = IncidentFilterForm(form_data)
LOG.debug("using POST: %s", form_data)
Expand Down Expand Up @@ -227,6 +236,7 @@ def incident_list_filter(request, qs, use_empty_filter=False):

def _convert_filterblob(filterblob):
"""Converts values in filterblob so it can be used as valid input for IncidentFilterForm"""
filterblob = filterblob.copy()

if "open" in filterblob.keys():
open_state = filterblob["open"]
Expand All @@ -249,10 +259,9 @@ def _convert_filterblob(filterblob):
return filterblob


def _normalize_form_data(request):
def _normalize_form_data(raw_data):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, we can even add a type hint:

Suggested change
def _normalize_form_data(raw_data):
def _normalize_form_data(raw_data: QueryDict):

"""Normalizes form data from request, especially the 'tags' parameter."""

raw_data = request.POST if request.method == "POST" else request.GET
data = dict(raw_data.items())
for key in raw_data:
value = raw_data.getlist(key, [])
Expand Down
77 changes: 54 additions & 23 deletions src/argus/htmx/incident/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,26 @@ def incident_update(request: HtmxHttpRequest, action: str):
return HttpResponseClientRefresh()


def set_selected_filter(request, filter_obj):
if filter_obj:
request.session["selected_filter_pk"] = str(filter_obj.pk)
request.session["selected_filter_name"] = filter_obj.name
else:
request.session["selected_filter_pk"] = None
request.session.pop("selected_filter_name", None)


def get_selected_filter(request):
filter_id = request.session.get("selected_filter_pk", None)
if filter_id:
return get_object_or_404(Filter, pk=filter_id, user=request.user)
return None
Comment on lines +116 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful helper functions!



@require_GET
def filter_form(request: HtmxHttpRequest):
set_selected_filter(request, None)
LOG = logging.getLogger(__name__ + ".filter_form")
request.session["selected_filter"] = None
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None)
context = {"filter_form": filter_form}
Expand All @@ -135,7 +151,7 @@ def create_filter(request: HtmxHttpRequest):
filterblob = filter_form.to_filterblob()
_, filter_obj = create_named_filter(request, filter_name, filterblob)
if filter_obj:
request.session["selected_filter"] = str(filter_obj.id)
set_selected_filter(request, filter_obj)
return HttpResponseClientRefresh()
messages.error(request, "Failed to create filter")
return HttpResponseBadRequest()
Expand All @@ -152,11 +168,12 @@ def update_filter(request: HtmxHttpRequest, pk: int):
filter_obj.save()

# Immediately select the newly updated filter - keep or not?
# request.session["selected_filter"] = str(filter_obj.id)
# set_selected_filter(request, filter_obj)

messages.success(request, f"Updated filter '{filter_obj.name}'.")
return HttpResponseClientRefresh()
messages.error(request, f"Failed to update filter '{filter_obj.name}'.")
errors = f": {filter_form.errors}" if filter_form.errors else ""
messages.error(request, f'Failed to update filter "{filter_obj.name}": {errors}')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
messages.error(request, f'Failed to update filter "{filter_obj.name}": {errors}')
messages.error(request, f'Failed to update filter "{filter_obj.name}"{errors}')

return HttpResponseBadRequest()


Expand All @@ -166,8 +183,8 @@ def delete_filter(request: HtmxHttpRequest, pk: int):
deleted_id = filter_obj.delete()
if deleted_id:
messages.success(request, f"Deleted filter {filter_obj.name}.")
if request.session.get("selected_filter") == str(pk):
request.session["selected_filter"] = None
if request.session.get("selected_filter_pk") == str(pk):
set_selected_filter(request, None)
return HttpResponseClientRefresh()


Expand All @@ -185,22 +202,25 @@ def get_existing_filters(request: HtmxHttpRequest):

@require_GET
def filter_select(request: HtmxHttpRequest):
context = {}
template_name = "htmx/incident/_incident_list_filter_incidents.html"

filter_id = request.GET.get("filter", None)
if filter_id and get_object_or_404(Filter, id=filter_id):
request.session["selected_filter"] = filter_id
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None)
context = {"filter_form": filter_form}
return render(request, "htmx/incident/_incident_filterbox.html", context=context)
if filter_id:
use_empty_filter = False
filter_obj = get_object_or_404(Filter, id=filter_id)
set_selected_filter(request, filter_obj)
else:
request.session["selected_filter"] = None
if request.htmx.trigger:
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None, use_empty_filter=True)
context = {"filter_form": filter_form}
return render(request, "htmx/incident/_incident_filterbox.html", context=context)
else:
return retarget(HttpResponse(), "#incident-filter-select")
use_empty_filter = True
set_selected_filter(request, None)

if request.htmx.trigger:
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None, use_empty_filter=use_empty_filter)
context["filter_form"] = filter_form
return render(request, template_name, context=context)

return retarget(HttpResponse(), "#incident-filter-select")


def dedupe_querydict(querydict: QueryDict):
Expand Down Expand Up @@ -276,6 +296,10 @@ def incident_list(request: HtmxHttpRequest) -> HttpResponse:
total_count = qs.count()
last_refreshed = make_aware(datetime.now())

# Stored filters
existing_filters = Filter.objects.filter(user=request.user)

# Get filters storable in Filter.filter
incident_list_filter = get_filter_function()
filter_form, qs = incident_list_filter(request, qs)

Expand Down Expand Up @@ -327,12 +351,19 @@ def incident_list(request: HtmxHttpRequest) -> HttpResponse:

LOG.debug("GET at end: %s", request.GET)
context = {
"columns": columns,
"page_title": "Incidents",
"base": base_template,
# filter box
"filter_form": filter_form,
# storing filters
"stored_filters": existing_filters,
# table
"columns": columns,
# refresh info
"refresh_info": refresh_info,
"refresh_info_forms": GET_forms,
"page_title": "Incidents",
"base": base_template,
"filtered_count": filtered_count,
"count": total_count,
"page": page,
"last_page_num": last_page_num,
"second_to_last_page": last_page_num - 1,
Expand Down
1 change: 1 addition & 0 deletions src/argus/htmx/templates/htmx/_base_confirm_dialog.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- htmx/_base_confirm_dialog -->
<!-- This confirmation dialog is a special case of the base form modal that needs its own template.
It can be used as one of several confirm dialogs in DOM, for example as an item in HTML list.
In this case the item_id attribute must be provided when extending from this template.
Expand Down

This file was deleted.

16 changes: 13 additions & 3 deletions src/argus/htmx/templates/htmx/incident/_filter_controls.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<!-- htmx/incident/_filter_controls.html -->
<div class="join join-horizontal items-center">
{% include "htmx/incident/_filter_select.html" %}
{% include "htmx/incident/_filter_create_modal.html" with dialog_id="create-filter-dialog" button_title="Create filter" button_class="btn-sm text-xs join-item" header="Create new filter" explanation="Create new filter from currently selected filter parameters" cancel_text="Cancel" submit_text="Submit" %}
{% include "htmx/incident/_filter_update_dropdown.html" %}
{% include "htmx/incident/_filter_delete_dropdown.html" %}
{% include "htmx/incident/_filter_create_modal.html" with dialog_id="create-filter-dialog" button_title="Save new" button_class="btn-sm text-xs join-item" header="Create new filter" explanation="Create new filter from currently selected filter parameters" cancel_text="Cancel" submit_text="Submit" %}
{% with item_id=request.session.selected_filter_pk item_title=request.session.selected_filter_name item_class="btn btn-sm text-xs join-item rounded-l-none! ml-1" %}
{% if request.session.selected_filter_pk %}
{% url 'htmx:filter-update' pk=request.session.selected_filter_pk as update_filter_url %}
{% include "htmx/incident/_filter_controls_confirm_dialog.html" with filter_url=update_filter_url modal_button_name="Update" dialog_id="filter-update-confirm" action="Update filter" confirmation_message="Are you sure you want to override this filter?" %}
{% url 'htmx:filter-delete' pk=request.session.selected_filter_pk as delete_filter_url %}
{% include "htmx/incident/_filter_controls_confirm_dialog.html" with filter_url=delete_filter_url modal_button_name="Delete" dialog_id="filter-delete-confirm" action="Delete filter" confirmation_message="Are you sure you want to delete this filter?" %}
{% else %}
<button class="{{ item_class }}" disabled>Update</button>
<button class="{{ item_class }}" disabled>Delete</button>
{% endif %}
{% endwith %}
</div>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!-- htmx/incident/_filter_alter_confirm_dialog.html -->
{% extends "htmx/_base_confirm_dialog.html" %}
{% block confirm_action_control %}
hx-post="{% url 'htmx:filter-update' pk=filter.id %}"
hx-post="{{ filter_url }}"
hx-include="#incident-filter-box fieldset"
{% endblock confirm_action_control %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- htmx/incident/_filter_create_modal.html -->
{% extends "htmx/_base_form_modal.html" %}
{% block form_control %}
hx-post="{% url 'htmx:filter-create' %}"
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- htmx/incident/_incident_filterbox.html -->
{% load widget_tweaks %}
<form id="incident-filter-box"
class="incident-list-param"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- htmx/incident/_incident_list_filter_incidents.html -->
<div id="incident-filter-box" class="flex flex-wrap items-center">
{% include "htmx/incident/_incident_filterbox.html" %}
{% include "htmx/incident/_filter_controls.html" %}
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- htmx/incident/_incident_list_menubar.html -->
<div role="tablist" class="tabs tabs-lifted">
{% block menu_tabs %}
<input type="radio"
Expand All @@ -9,10 +10,7 @@
checked="checked" />
<div role="tabpanel"
class="filterbox tab-content border-primary [--tab-border:theme(borderWidth.DEFAULT)] rounded-box p-2">
<div class="flex flex-wrap items-center">
{% include "htmx/incident/_incident_filterbox.html" %}
{% include "htmx/incident/_filter_controls.html" %}
</div>
{% include "htmx/incident/_incident_list_filter_incidents.html" %}
</div>
<input type="radio"
name="incident_menus"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- htmx/incident/_incident_table_paginator_pageitem.html -->
<!--
For each link we use hx-get to tell htmx to fetch that URL and
swap it in. We also repeat the URL in the href attribute so the
Expand Down
8 changes: 5 additions & 3 deletions src/argus/htmx/templates/htmx/incident/filter_list.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<!-- htmx/incident/filter_list.html -->
<label class="form-control w-full max-w-xs">
<span class="block label pt-0 pb-0.5">
<span class="label-text">Filter</span>
</span>
<select class="select select-bordered select-sm join-item w-full max-w-xs"
<select class="incident-list-param select select-bordered select-sm join-item w-full max-w-xs"
id="incident-filter-select"
name="filter"
hx-get="{% url 'htmx:select-filter' %}"
hx-include=".incident-list-param"
hx-target="#incident-filter-box"
hx-swap="outerHTML"
hx-on::after-swap="htmx.trigger('#filter-selector-form', 'unselect')">
<option {% if not request.session.selected_filter %}selected{% endif %}
<option {% if not request.session.selected_filter_pk %}selected{% endif %}
value="">---</option>
{% for object in object_list %}
<option value="{{ object.id }}"
{% if request.session.selected_filter == object.id|stringformat:"i" %}selected{% endif %}>
{% if request.session.selected_filter_pk == object.id|stringformat:"i" %}selected{% endif %}>
{{ object.name }}
</option>
{% endfor %}
Expand Down
12 changes: 6 additions & 6 deletions tests/htmx/incident/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from argus.auth.factories import PersonUserFactory
from argus.filter.factories import FilterFactory
from argus.htmx.incident.filter import IncidentFilterForm, NamedFilterForm, create_named_filter, incident_list_filter
from argus.htmx.incident.views import search_tags
from argus.htmx.incident.views import search_tags, set_selected_filter
from argus.incident.constants import AckedStatus, OpenStatus
from argus.incident.factories import IncidentFactory, SourceSystemFactory
from argus.incident.models import Incident, Tag
Expand Down Expand Up @@ -104,29 +104,29 @@ def teardown(self):
connect_signals()

def test_valid_request_should_return_filtered_queryset(self):
self.request.session["selected_filter"] = self.valid_filter.pk
set_selected_filter(self.request, self.valid_filter)
_, qs = incident_list_filter(self.request, self.qs)
assert self.incident in self.qs
assert self.incident not in qs

def test_invalid_request_should_return_unfiltered_queryset(self):
self.request.session["selected_filter"] = self.invalid_filter.pk
set_selected_filter(self.request, self.invalid_filter)
_, qs = incident_list_filter(self.request, self.qs)
assert qs == self.qs

def test_valid_request_should_return_form_with_correct_values(self):
self.request.session["selected_filter"] = self.valid_filter.pk
set_selected_filter(self.request, self.valid_filter)
form, _ = incident_list_filter(self.request, self.qs)
assert form.to_filterblob()["maxlevel"] == self.valid_filter.filter["maxlevel"]

def test_invalid_request_should_return_form_with_errors(self):
self.request.session["selected_filter"] = self.invalid_filter.pk
set_selected_filter(self.request, self.invalid_filter)
form, _ = incident_list_filter(self.request, self.qs)
assert form.errors

def test_get_request_without_selected_filter_should_use_get_parameters_as_form_data(self):
maxlevel = 3
self.request.session["selected_filter"] = None
set_selected_filter(self.request, None)
self.request.GET = QueryDict(f"tags=&maxlevel={maxlevel}")
form, _ = incident_list_filter(self.request, self.qs)
assert form.to_filterblob()["maxlevel"] == maxlevel
Expand Down