Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions changelog.d/+shrink-stored-filter-widget.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Made the controls for storing filters take up less horizontal space.
81 changes: 49 additions & 32 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 @@
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 @@
"sourceSystemIds": [],
"source_types": [],
"tags": [],
"maxlevel": max(Level).value,
"maxlevel": MAX_LEVEL,
}

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

filterblob = {}

filterblob["open"] = self._open_tristate()
filterblob["acked"] = self._acked_tristate()
open = self._open_tristate()

Check warning on line 154 in src/argus/htmx/incident/filter.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this variable; it shadows a builtin.

See more on https://sonarcloud.io/project/issues?id=Uninett_Argus&issues=AZq01bD9TDDKExqz1PCk&open=AZq01bD9TDDKExqz1PCk&pullRequest=1621
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 @@ -190,44 +198,54 @@
return reverse("htmx:filter-list")


# Not a view!
def incident_list_filter(request, qs, use_empty_filter=False):
LOG = logging.getLogger(__name__ + ".incident_list_filter")
LOG.debug("GET at start: %s", request.GET)
filter_pk, filter_obj = request.session.get("selected_filter", None), None
LOG.debug("<<<<FORM<<<<")
LOG.debug("GET: %s", request.GET)

filter_pk = request.session.get("selected_filter_pk", None)
converted_filterblob = {}
if filter_pk:
filter_obj = Filter.objects.get(pk=filter_pk)
if filter_obj:
form = IncidentFilterForm(_convert_filterblob(filter_obj.filter))
LOG.debug("using stored filter: %s", filter_obj.filter)
else:
form_data = _normalize_form_data(request)
if request.method == "POST":
form = IncidentFilterForm(form_data)
LOG.debug("using POST: %s", form_data)
else:
if use_empty_filter:
filterblob = IncidentFilterForm.EMPTY_FILTERBLOB
form = IncidentFilterForm(filterblob)
LOG.debug("using empty filter: %s", filterblob)
else:
form = IncidentFilterForm(form_data or None)
LOG.debug("using GET: %s", form_data)
LOG.debug('found stored filter "%s": %s', filter_obj.name, filter_obj.filter)
converted_filterblob = _convert_filterblob(filter_obj.filter)

if converted_filterblob:
form_data = converted_filterblob
LOG.debug("using converted stored filter: %s", form_data)
elif request.POST:
form_data = _normalize_form_data(request.POST)
LOG.debug("using POST: %s", form_data)
elif use_empty_filter:
form_data = IncidentFilterForm.EMPTY_FILTERBLOB
LOG.debug("using empty filter: %s", form_data)
else: # request.GET
form_data = _normalize_form_data(request.GET) or None
LOG.debug("using GET: %s", form_data)

form = IncidentFilterForm(form_data)
form.is_valid() # fill cleaned_data
if use_empty_filter:
LOG.debug(">>>>FORM>>>> early return")
return form, qs

if form.is_valid():
LOG.debug("Cleaned data: %s", form.cleaned_data)
LOG.debug("Cleaned data: %s, getting query set", form.cleaned_data)
filterblob = form.to_filterblob()
LOG.debug("using filterblob: %s", filterblob)
qs = QuerySetFilter.filtered_incidents(filterblob, qs)
else:
if not request.GET:
LOG.debug("empty form")
else:
LOG.debug("Dirty form: %s", form.errors)
for field, error_messages in form.errors.items():
messages.error(request, f"{field}: {','.join(error_messages)}")
LOG.debug("Dirty form: %s", form.errors)
for field, error_messages in form.errors.items():
messages.error(request, f"{field}: {','.join(error_messages)}")
LOG.debug(">>>>FORM>>>>")
return form, qs


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 @@ -250,10 +268,9 @@
return filterblob


def _normalize_form_data(request):
def _normalize_form_data(raw_data):
"""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
97 changes: 74 additions & 23 deletions src/argus/htmx/incident/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,27 @@ 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


@require_GET
def filter_form(request: HtmxHttpRequest):
set_selected_filter(request, None)
LOG = logging.getLogger(__name__ + ".filter_form")
request.session["selected_filter"] = None
LOG.debug("filter_form calling incident_list_filter")
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None)
context = {"filter_form": filter_form}
Expand All @@ -129,13 +146,14 @@ def create_filter(request: HtmxHttpRequest):
from argus.htmx.incident.filter import create_named_filter

filter_name = request.POST.get("filter_name", None)
LOG.debug("create_filter calling incident_list_filter")
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None)
if filter_name and filter_form.is_valid():
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 @@ -144,6 +162,7 @@ def create_filter(request: HtmxHttpRequest):
@require_POST
def update_filter(request: HtmxHttpRequest, pk: int):
filter_obj = get_object_or_404(Filter, id=pk)
LOG.debug("update_filter calling incident_list_filter")
incident_list_filter = get_filter_function()
filter_form, _ = incident_list_filter(request, None)
if filter_form.is_valid():
Expand All @@ -152,11 +171,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}')
return HttpResponseBadRequest()


Expand All @@ -166,8 +186,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 +205,33 @@ def get_existing_filters(request: HtmxHttpRequest):

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

LOG.debug("GET when selecting filter: %s", request.GET)
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)
LOG.debug("Selecting filter: %s", filter_obj.name)
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)
LOG.debug("Selecting empty filter")

if request.htmx.trigger:
LOG.debug("filter_select calling incident_list_filter")
incident_list_filter = get_filter_function()
LOG.debug("nao 1 %s", request.GET)
filter_form, _ = incident_list_filter(request, None, use_empty_filter=use_empty_filter)
context["filter_form"] = filter_form
LOG.debug("nao 2 %s", request.GET)
return render(request, template_name, context=context)

LOG.debug("boo %s", request.GET)
return retarget(HttpResponse(), "#incident-filter-select")


def dedupe_querydict(querydict: QueryDict):
Expand All @@ -219,12 +250,16 @@ def dedupe_querydict(querydict: QueryDict):

def add_param_to_querydict(querydict: QueryDict, key: str, value: Any):
"Set key to value if missing from querydict"
LOG = logging.getLogger(__name__ + ".add_param_to_querydict")
qd = querydict.copy()
LOG.debug("Current QueryDict: %s, about to alter %s", qd, key)
if value is None:
LOG.debug("Unchanged QueryDict: %s, value is None", querydict)
return querydict
if key not in qd:
if isinstance(value, Iterable):
if not value:
LOG.debug("Unchanged QueryDict: %s, value is falsey Iterable", querydict)
return querydict
if isinstance(value, str):
value = [value]
Expand All @@ -234,7 +269,9 @@ def add_param_to_querydict(querydict: QueryDict, key: str, value: Any):
value = [str(value)]
qd.setlist(key, value)
qd._mutable = False
LOG.debug("Altered QueryDict: %s", qd)
return qd
LOG.debug("Unchanged QueryDict: %s, key not found", querydict)
return querydict


Expand Down Expand Up @@ -263,6 +300,7 @@ def search_tags(request):
@require_GET
def incident_list(request: HtmxHttpRequest) -> HttpResponse:
LOG = logging.getLogger(__name__ + ".incident_list")
LOG.debug("<=====VIEW=====<")
LOG.debug("GET at start: %s", request.GET)
request.GET = dedupe_querydict(request.GET)
LOG.debug("after dedupe: %s", request.GET)
Expand All @@ -276,6 +314,11 @@ 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
LOG.debug("calling incident_list_filter")
incident_list_filter = get_filter_function()
filter_form, qs = incident_list_filter(request, qs)

Expand Down Expand Up @@ -327,14 +370,22 @@ 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,
}
LOG.debug(">=====VIEW=====>")
return render(request, "htmx/incident/incident_list.html", context=context)
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.

Loading
Loading