From 4c39516253b658f60a11597adae68edd75ee4b7d Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 2 Feb 2024 15:49:32 -0600 Subject: [PATCH 01/43] Preliminary work on 9583. --- netbox/netbox/tables/tables.py | 1 + netbox/netbox/views/generic/bulk_views.py | 7 ++++- .../inc/table_header_filter_dropdown.html | 11 ++++++++ netbox/templates/inc/table_htmx.html | 7 ++++- netbox/utilities/templatetags/form_helpers.py | 28 +++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 netbox/templates/inc/table_header_filter_dropdown.html diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 495e569915c..e82f61c6b19 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -38,6 +38,7 @@ class BaseTable(tables.Table): :param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed. """ exempt_columns = () + filterset_form = None class Meta: attrs = { diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index f5b605ccd6a..406ff049d77 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -160,6 +160,11 @@ def get(self, request): # Render the objects table table = self.get_table(self.queryset, request, has_bulk_actions) + filterset_form = self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None + + if hasattr(self, 'filterset_form'): + table.filterset_form = filterset_form + # If this is an HTMX request, return only the rendered table HTML if request.htmx: if request.htmx.target != 'object_list': @@ -175,7 +180,7 @@ def get(self, request): 'model': model, 'table': table, 'actions': actions, - 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, + 'filter_form': filterset_form, 'prerequisite_model': get_prerequisite_model(self.queryset), **self.get_extra_context(request), } diff --git a/netbox/templates/inc/table_header_filter_dropdown.html b/netbox/templates/inc/table_header_filter_dropdown.html new file mode 100644 index 00000000000..4d134c9ecde --- /dev/null +++ b/netbox/templates/inc/table_header_filter_dropdown.html @@ -0,0 +1,11 @@ +{% load form_helpers %} +{% if form_field %} + +{% endif %} \ No newline at end of file diff --git a/netbox/templates/inc/table_htmx.html b/netbox/templates/inc/table_htmx.html index 4eedce72dcf..6e9ebe874c9 100644 --- a/netbox/templates/inc/table_htmx.html +++ b/netbox/templates/inc/table_htmx.html @@ -1,4 +1,5 @@ {% load django_tables2 %} +{% load form_helpers %} {% if table.show_header %} @@ -21,9 +22,13 @@ hx-target="closest .htmx-container" {% if not table.embedded %}hx-push-url="true"{% endif %} >{{ column.header }} + {% include 'inc/table_header_filter_dropdown.html' with form_field=table.filterset_form|getfilterfield:column.name %} {% else %} - {{ column.header }} + + {{ column.header }} + {% include 'inc/table_header_filter_dropdown.html' with form_field=table.filterset_form|getfilterfield:column.name %} + {% endif %} {% endfor %} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index f4fd8b8198d..8b3979d3720 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -2,6 +2,7 @@ __all__ = ( 'getfield', + 'getfilterfield', 'render_custom_fields', 'render_errors', 'render_field', @@ -28,6 +29,15 @@ def getfield(form, fieldname): return None +@register.filter() +def getfilterfield(form, fieldname): + field = getfield(form, f'{fieldname}') + if field is not None: + return field + else: + return getfield(form, f'{fieldname}_id') + + @register.filter(name='widget_type') def widget_type(field): """ @@ -57,6 +67,24 @@ def render_field(field, bulk_nullable=False, label=None): } +@register.inclusion_tag('form_helpers/render_field.html') +def render_filter_field(field, bulk_nullable=False, label=None): + """ + Render a single form field from template + """ + if hasattr(field.field, 'widget'): + field.field.widget.attrs.update({ + 'hx-get': None, + 'hx-target': '#object_list', + 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' + }) + return { + 'field': field, + 'label': None, + 'bulk_nullable': bulk_nullable, + } + + @register.inclusion_tag('form_helpers/render_custom_fields.html') def render_custom_fields(form): """ From e762755e800c42d0ddeb4e77aead3a32100fd62a Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 7 Feb 2024 12:05:11 -0600 Subject: [PATCH 02/43] HTMX work on 9583. --- netbox/templates/generic/object_list.html | 2 +- netbox/utilities/templatetags/form_helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index bf077740678..043a55798c8 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -60,7 +60,7 @@ {% block content %} {# Object list tab #} -
+
{# Applied filters #} {% if filter_form %} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 8b3979d3720..dd4f1578f91 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -74,7 +74,7 @@ def render_filter_field(field, bulk_nullable=False, label=None): """ if hasattr(field.field, 'widget'): field.field.widget.attrs.update({ - 'hx-get': None, + 'hx-get': "", 'hx-target': '#object_list', 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' }) From f7294f7087275d3c7163ad94f565d442f7ffa338 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 7 Feb 2024 13:37:57 -0600 Subject: [PATCH 03/43] Preliminary work on 9583. --- netbox/templates/generic/object_list.html | 2 +- netbox/templates/inc/table_controls_htmx.html | 2 +- .../inc/table_header_filter_dropdown.html | 2 +- netbox/utilities/templatetags/form_helpers.py | 16 ++++++++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 043a55798c8..bf077740678 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -60,7 +60,7 @@ {% block content %} {# Object list tab #} -
+
{# Applied filters #} {% if filter_form %} diff --git a/netbox/templates/inc/table_controls_htmx.html b/netbox/templates/inc/table_controls_htmx.html index 3b1417cddb6..62c58732854 100644 --- a/netbox/templates/inc/table_controls_htmx.html +++ b/netbox/templates/inc/table_controls_htmx.html @@ -5,7 +5,7 @@
+ hx-get="" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" /> diff --git a/netbox/templates/inc/table_header_filter_dropdown.html b/netbox/templates/inc/table_header_filter_dropdown.html index 4d134c9ecde..36665841f9d 100644 --- a/netbox/templates/inc/table_header_filter_dropdown.html +++ b/netbox/templates/inc/table_header_filter_dropdown.html @@ -5,7 +5,7 @@
{% endif %} \ No newline at end of file diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index dd4f1578f91..6fe526f53a1 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -1,5 +1,6 @@ from django import template + __all__ = ( 'getfield', 'getfilterfield', @@ -10,6 +11,7 @@ 'widget_type', ) +from utilities.templatetags.helpers import querystring register = template.Library() @@ -68,13 +70,23 @@ def render_field(field, bulk_nullable=False, label=None): @register.inclusion_tag('form_helpers/render_field.html') -def render_filter_field(field, bulk_nullable=False, label=None): +def render_filter_field(field, bulk_nullable=False, table=None, request=None): """ Render a single form field from template """ + url = "" + kwargs = { + field.name: None + } + if request and table.htmx_url: + url = table.htmx_url + querystring(request, **kwargs) + elif request: + url = querystring(request, **kwargs) + if hasattr(field.field, 'widget'): field.field.widget.attrs.update({ - 'hx-get': "", + 'hx-get': url if url else '#', + 'hx-push-url': "true", 'hx-target': '#object_list', 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' }) From 664a0eba6db2d4baf3fc96597c5d39b2f408a0b3 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 7 Feb 2024 13:40:20 -0600 Subject: [PATCH 04/43] Preliminary work on 9583. --- netbox/utilities/templatetags/form_helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 6fe526f53a1..1bd01f38a2d 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -72,12 +72,16 @@ def render_field(field, bulk_nullable=False, label=None): @register.inclusion_tag('form_helpers/render_field.html') def render_filter_field(field, bulk_nullable=False, table=None, request=None): """ - Render a single form field from template + Render a single form field from template for use in column headers """ url = "" + + # Build kwargs for querystring function kwargs = { field.name: None } + + # Build request url if request and table.htmx_url: url = table.htmx_url + querystring(request, **kwargs) elif request: From cc423f5071dfa995eb5bf182ec4cfadd62c0f536 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 7 Feb 2024 13:48:32 -0600 Subject: [PATCH 05/43] Apply some quick fixes and add comments --- netbox/netbox/views/generic/bulk_views.py | 5 +++-- netbox/utilities/templatetags/form_helpers.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 406ff049d77..f2b62f7a814 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -160,9 +160,10 @@ def get(self, request): # Render the objects table table = self.get_table(self.queryset, request, has_bulk_actions) - filterset_form = self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None - + # Check for filterset_form on this view, if a form exists, apply to context and table, otherwise set to None + filterset_form = None if hasattr(self, 'filterset_form'): + filterset_form = self.filterset_form(request.GET, label_suffix='') table.filterset_form = filterset_form # If this is an HTMX request, return only the rendered table HTML diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 1bd01f38a2d..f2b3f8b3f69 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -7,6 +7,7 @@ 'render_custom_fields', 'render_errors', 'render_field', + 'render_filter_field', 'render_form', 'widget_type', ) From 50557c0f9d57ed7d37228e817372aab1cfb2b6b7 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 9 Feb 2024 16:35:09 -0600 Subject: [PATCH 06/43] Final work on #9583 for basic functionality --- netbox/templates/inc/table_header_filter_dropdown.html | 2 +- netbox/templates/inc/table_htmx.html | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/netbox/templates/inc/table_header_filter_dropdown.html b/netbox/templates/inc/table_header_filter_dropdown.html index 36665841f9d..e4292021240 100644 --- a/netbox/templates/inc/table_header_filter_dropdown.html +++ b/netbox/templates/inc/table_header_filter_dropdown.html @@ -1,6 +1,6 @@ {% load form_helpers %} {% if form_field %} - {% endif %} \ No newline at end of file diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index f2b3f8b3f69..5228fc9d3fe 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -59,45 +59,33 @@ def widget_type(field): # @register.inclusion_tag('form_helpers/render_field.html') -def render_field(field, bulk_nullable=False, label=None): +def render_field(field, bulk_nullable=False, label=None, table=None, request=None): """ Render a single form field from template """ - return { - 'field': field, - 'label': label or field.label, - 'bulk_nullable': bulk_nullable, - } - - -@register.inclusion_tag('form_helpers/render_field.html') -def render_filter_field(field, bulk_nullable=False, table=None, request=None): - """ - Render a single form field from template for use in column headers - """ url = "" - # Build kwargs for querystring function - kwargs = { - field.name: None - } + # Handle filter forms + if table: + # Build kwargs for querystring function + kwargs = {field.name: None} + # Build request url + if request and table.htmx_url: + url = table.htmx_url + querystring(request, **kwargs) + elif request: + url = querystring(request, **kwargs) + # Set HTMX args + if hasattr(field.field, 'widget'): + field.field.widget.attrs.update({ + 'hx-get': url if url else '#', + 'hx-push-url': "true", + 'hx-target': '#object_list', + 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' + }) - # Build request url - if request and table.htmx_url: - url = table.htmx_url + querystring(request, **kwargs) - elif request: - url = querystring(request, **kwargs) - - if hasattr(field.field, 'widget'): - field.field.widget.attrs.update({ - 'hx-get': url if url else '#', - 'hx-push-url': "true", - 'hx-target': '#object_list', - 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' - }) return { 'field': field, - 'label': None, + 'label': label or field.label if not table else None, 'bulk_nullable': bulk_nullable, } From 0309796bbbad159e7dc3ab1775b22595c41f8e05 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 12 Feb 2024 16:09:02 -0600 Subject: [PATCH 13/43] Fix extraneous __all__ entry --- netbox/utilities/templatetags/form_helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 5228fc9d3fe..6448f7d4624 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -7,7 +7,6 @@ 'render_custom_fields', 'render_errors', 'render_field', - 'render_filter_field', 'render_form', 'widget_type', ) From 84151cbc1a4c24b603878c099a293763247052fe Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 13 Feb 2024 15:23:14 -0600 Subject: [PATCH 14/43] Fix tom-select errors related to field id. Break out render_field function for column filters --- .../inc/table_header_filter_dropdown.html | 2 +- netbox/utilities/templatetags/form_helpers.py | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/netbox/templates/inc/table_header_filter_dropdown.html b/netbox/templates/inc/table_header_filter_dropdown.html index a0db03015c1..020ba6d4db1 100644 --- a/netbox/templates/inc/table_header_filter_dropdown.html +++ b/netbox/templates/inc/table_header_filter_dropdown.html @@ -5,7 +5,7 @@
{% endif %} \ No newline at end of file diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 6448f7d4624..79bc2634c27 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -58,10 +58,23 @@ def widget_type(field): # @register.inclusion_tag('form_helpers/render_field.html') -def render_field(field, bulk_nullable=False, label=None, table=None, request=None): +def render_field(field, bulk_nullable=False, label=None): """ Render a single form field from template """ + + return { + 'field': field, + 'label': label or field.label, + 'bulk_nullable': bulk_nullable, + } + + +@register.inclusion_tag('form_helpers/render_field.html') +def render_table_filter_field(field, table=None, request=None): + """ + Render a single form field for table column filters from template + """ url = "" # Handle filter forms @@ -74,18 +87,20 @@ def render_field(field, bulk_nullable=False, label=None, table=None, request=Non elif request: url = querystring(request, **kwargs) # Set HTMX args - if hasattr(field.field, 'widget'): - field.field.widget.attrs.update({ - 'hx-get': url if url else '#', - 'hx-push-url': "true", - 'hx-target': '#object_list', - 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' - }) + + if hasattr(field.field, 'widget'): + field.field.widget.attrs.update({ + 'id': f'table_filter_id_{field.name}', + 'hx-get': url if url else '#', + 'hx-push-url': "true", + 'hx-target': '#object_list', + 'hx-trigger': 'hidden.bs.dropdown from:closest .dropdown' + }) return { 'field': field, - 'label': label or field.label if not table else None, - 'bulk_nullable': bulk_nullable, + 'label': None, + 'bulk_nullable': False, } From f257f4aad401d23d4d758619b46446967cbe1462 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Thu, 21 Mar 2024 22:18:34 -0500 Subject: [PATCH 15/43] Apply suggestions from code review Co-authored-by: Jeremy Stretch --- netbox/netbox/views/generic/bulk_views.py | 5 ++--- netbox/templates/inc/table_header_filter_dropdown.html | 3 ++- netbox/templates/inc/table_htmx.html | 8 ++++---- netbox/utilities/templatetags/form_helpers.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index bfdb6d670eb..cf07797ddad 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -162,9 +162,8 @@ def get(self, request): table = self.get_table(self.queryset, request, has_bulk_actions) # Check for filterset_form on this view, if a form exists, apply to context and table, otherwise set to None - filterset_form = None - if hasattr(self, 'filterset_form') and self.filterset_form: - filterset_form = self.filterset_form(request.GET, label_suffix='') + if self.filterset_form: + filterset_form = self.filterset_form(request.GET) table.filterset_form = filterset_form # If this is an HTMX request, return only the rendered table HTML diff --git a/netbox/templates/inc/table_header_filter_dropdown.html b/netbox/templates/inc/table_header_filter_dropdown.html index 020ba6d4db1..4e76bcb547b 100644 --- a/netbox/templates/inc/table_header_filter_dropdown.html +++ b/netbox/templates/inc/table_header_filter_dropdown.html @@ -1,7 +1,8 @@ {% load form_helpers %} {% if form_field %}