Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes: #9583 - Add column specific search field to tables #15073

Open
wants to merge 34 commits into
base: feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4c39516
Preliminary work on 9583.
DanSheps Feb 2, 2024
e762755
HTMX work on 9583.
DanSheps Feb 7, 2024
f7294f7
Preliminary work on 9583.
DanSheps Feb 7, 2024
664a0eb
Preliminary work on 9583.
DanSheps Feb 7, 2024
cc423f5
Apply some quick fixes and add comments
DanSheps Feb 7, 2024
50557c0
Final work on #9583 for basic functionality
DanSheps Feb 9, 2024
03f67f3
CSS update for dropdown
DanSheps Feb 9, 2024
3243ebd
Merge remote-tracking branch 'origin/9583-add_column_specific_search_…
DanSheps Feb 9, 2024
b54cfd6
Update CSS
DanSheps Feb 12, 2024
5f69666
Change dropdown position and fix test failure
DanSheps Feb 12, 2024
06c1aff
Fix test failure
DanSheps Feb 12, 2024
4ae6683
Merge in recent feture changes
DanSheps Feb 12, 2024
c3f1a96
Update CSS after merge
DanSheps Feb 12, 2024
f81f76f
Optimizations
DanSheps Feb 12, 2024
0309796
Fix extraneous __all__ entry
DanSheps Feb 12, 2024
84151cb
Fix tom-select errors related to field id. Break out render_field fu…
DanSheps Feb 13, 2024
77bfd62
Merge branch 'feature' into 9583-add_column_specific_search_field_to_…
jeremystretch Mar 7, 2024
25a4e94
Merge branch 'feature' into 9583-add_column_specific_search_field_to_…
jeremystretch Mar 20, 2024
f257f4a
Apply suggestions from code review
DanSheps Mar 22, 2024
8a7df0b
Update netbox/utilities/templatetags/form_helpers.py
DanSheps Mar 22, 2024
a422a3c
Modify logic for table column filtering to further isolate the column…
DanSheps Mar 22, 2024
8ad79a6
Update CSS
DanSheps Mar 22, 2024
a9aa0cb
Merge in latest feature
DanSheps Apr 15, 2024
35cff12
Apply suggestions from Arthur
DanSheps Apr 15, 2024
1c995fa
Perform OOB swap for filter badges
DanSheps Apr 15, 2024
479c69b
Fix up duplication of template chits when rendering HTMX
DanSheps Apr 23, 2024
5830ae9
Rename variable for doing OOB swaps on the table
DanSheps Apr 25, 2024
fd38255
Rename swap variable back and remove join
DanSheps Apr 25, 2024
30e6531
Add a table form override and add a table column remap option
DanSheps Jun 13, 2024
f130678
Merge branch 'feature' of https://github.com/netbox-community/netbox …
DanSheps Jun 13, 2024
0ad1db9
Regnerate CSS
DanSheps Jun 13, 2024
b12ce97
Update to latest feature
DanSheps Jul 2, 2024
77cb8ac
Merge branch 'feature' into 9583-add_column_specific_search_field_to_…
DanSheps Oct 17, 2024
d6ab7b7
Update bundle
DanSheps Oct 17, 2024
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 netbox/netbox/tables/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
8 changes: 7 additions & 1 deletion netbox/netbox/views/generic/bulk_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ def get(self, request):
# Render the objects table
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='')
table.filterset_form = filterset_form
DanSheps marked this conversation as resolved.
Show resolved Hide resolved

# If this is an HTMX request, return only the rendered table HTML
if request.htmx:
if request.htmx.target != 'object_list':
Expand All @@ -176,7 +182,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),
}
Expand Down
2 changes: 1 addition & 1 deletion netbox/project-static/dist/netbox.css

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions netbox/project-static/styles/custom/_misc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ span.color-label {
visibility: hidden;
opacity: 0;
}

// Override column filter form dropdown
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
.column-filter {
position: static;
display: inline;
}
2 changes: 1 addition & 1 deletion netbox/templates/inc/table_controls_htmx.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div class="col-auto d-print-none">
<div class="input-group input-group-flat me-2 quicksearch">
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search"
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
hx-get="" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
Copy link
Member Author

@DanSheps DanSheps Feb 7, 2024

Choose a reason for hiding this comment

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

Removed full path, as this will allow taking into account filters on the form (to facilitate pushing of the filters from the column as well).

Should not impact functionality

Copy link
Member

Choose a reason for hiding this comment

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

Should not impact functionality

Do we know this for sure? IIRC we specify the full path for a reason, I just can't recall why.

Copy link
Member Author

Choose a reason for hiding this comment

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

From my testing, I haven't found any issues with it.

<span class="input-group-text py-1">
<a href="#" id="quicksearch_clear" class="d-none text-secondary"><i class="mdi mdi-close-circle"></i></a>
</span>
Expand Down
11 changes: 11 additions & 0 deletions netbox/templates/inc/table_header_filter_dropdown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% load form_helpers %}
{% if form_field %}
<div class="column-filter dropdown">
<a href="#" classs="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
<i class="mdi mdi-filter-settings" style="font-size: 1.25rem;"> </i>
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
</a>
<div class="dropdown-menu">
{% render_table_filter_field form_field table=table request=request %}
</div>
</div>
{% endif %}
11 changes: 10 additions & 1 deletion netbox/templates/inc/table_htmx.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% load django_tables2 %}
{% load form_helpers %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
{% if table.show_header %}
<thead>
Expand All @@ -16,14 +17,22 @@
><i class="mdi mdi-close"></i></a>
</div>
{% endif %}
{% if table.filterset_form %}
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
{% include 'inc/table_header_filter_dropdown.html' with form_field=table.filterset_form|getfilterfield:column.name %}
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
{% endif %}
<a href="#"
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
hx-target="closest .htmx-container"
{% if not table.embedded %}hx-push-url="true"{% endif %}
>{{ column.header }}</a>
</th>
{% else %}
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
<th {{ column.attrs.th.as_html }}>
{% if table.filterset_form %}
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
{% include 'inc/table_header_filter_dropdown.html' with form_field=table.filterset_form|getfilterfield:column.name %}
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
{% endif %}
{{ column.header }}
</th>
{% endif %}
{% endfor %}
</tr>
Expand Down
46 changes: 46 additions & 0 deletions netbox/utilities/templatetags/form_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

__all__ = (
'getfield',
'getfilterfield',
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
'render_custom_fields',
'render_errors',
'render_field',
'render_form',
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
'widget_type',
)

from utilities.templatetags.helpers import querystring

register = template.Library()

Expand All @@ -32,6 +34,15 @@ def getfield(form, fieldname):
return None


@register.filter()
def getfilterfield(form, fieldname):
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
field = getfield(form, f'{fieldname}')
if field is not None:
return field
else:
return getfield(form, f'{fieldname}_id')
DanSheps marked this conversation as resolved.
Show resolved Hide resolved


@register.filter(name='widget_type')
def widget_type(field):
"""
Expand Down Expand Up @@ -120,13 +131,48 @@ 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 or getattr(field, '_nullable', False),
}


@register.inclusion_tag('form_helpers/render_field.html')
def render_table_filter_field(field, table=None, request=None):
DanSheps marked this conversation as resolved.
Show resolved Hide resolved
"""
Render a single form field for table column filters from template
"""
url = ""

# 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
DanSheps marked this conversation as resolved.
Show resolved Hide resolved

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'
})
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this approach is tenable: We're using the same form field for both the column and the regular filter form (behind the second tab). Setting HTMX attributes on a non-HTMX form field should be avoided, and overriding its ID may cause problems. IMO we should try to identify a cleaner approach.

Copy link
Member Author

@DanSheps DanSheps Mar 22, 2024

Choose a reason for hiding this comment

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

I have went ahead and made a possible modification to this.

In bulk views, I initialize two copies of the form:

  1. A copy of the form for the filtering tab
  2. A copy of the form for the table column filtering

Since this render_table_filter_field tag is only used by the table coumn filtering logic, this shouldn't be a problem anymore.

Let me know if this approach is a more "safer" one while allowing for some code re-use. I can go a little deeper and perhaps modify all filtersets to add a "column filter" tuple to allow a more clean approach to limiting the columns as well.

Copy link
Member

Choose a reason for hiding this comment

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

AFAICT this should work fine, but do we need a full form instance for the columns? It might suffice to make a copy of the relevant fields from the filter form and attach them to their respective columns.

Though with either approach, I feel like we're missing a step. The current implementation depends on the column name and the field name matching (form_field=table.filterset_form|get_filter_field:column.name). This should work ~95% of the time but inevitably we're going to find exceptions. I'd like to see if we can devise a more robust approach, short of duplicating the field definitions.

Copy link
Member Author

Choose a reason for hiding this comment

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

AFAICT this should work fine, but do we need a full form instance for the columns? It might suffice to make a copy of the relevant fields from the filter form and attach them to their respective columns.

Instead of initializing the form right away, we could copy the form definition and then remove fields that not present on the table before intitialization. This feels like excess code for very little gain however

Though with either approach, I feel like we're missing a step. The current implementation depends on the column name and the field name matching (form_field=table.filterset_form|get_filter_field:column.name). This should work ~95% of the time but inevitably we're going to find exceptions. I'd like to see if we can devise a more robust approach, short of duplicating the field definitions.

I think we should handle this on a case-by-case. The only other option would be looking at the table and building a completely new form class dynamically based on the table field types, however we then would likely lose all the custom filtering unless we tie it with the existing filter form somehow.


return {
'field': field,
'label': None,
'bulk_nullable': False,
}


@register.inclusion_tag('form_helpers/render_custom_fields.html')
def render_custom_fields(form):
"""
Expand Down
Loading