Files
Netbox-Utilities/netbox_utilities/site_prefix.py
T
MrBlakeandClaude Opus 5.5 337cd28c23 feat: filter device and rack lists by site prefix
Adds a site_prefix filter to the device and rack filtersets and filter
forms. It matches the first word of the site name case-insensitively
and accepts several comma-separated prefixes. Bumps version to 0.14.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:07:03 +02:00

113 lines
3.5 KiB
Python

import re
import django_tables2 as tables
from django import forms
from django.db.models import Q
COLUMN_NAME = "site_prefix"
def first_word(value):
"""Return the first whitespace-delimited word of a value."""
words = str(value or "").split()
return words[0] if words else None
def split_prefixes(values):
"""Split filter input such as ["Berlin, DC01", "HH"] into single prefixes."""
prefixes = []
for value in values or ():
for part in re.split(r"[,\s]+", str(value)):
if part and part not in prefixes:
prefixes.append(part)
return prefixes
def site_prefix_query(prefixes, field="site__name"):
"""Match records whose site name starts with one of the prefixes as its first word."""
query = Q()
for prefix in prefixes:
query |= Q(**{f"{field}__iexact": prefix})
query |= Q(**{f"{field}__iregex": rf"^{re.escape(prefix)}\s"})
return query
def filter_site_prefix(queryset, name, value):
prefixes = split_prefixes(value)
if not prefixes:
return queryset
return queryset.filter(site_prefix_query(prefixes))
class SitePrefixColumn(tables.Column):
"""Render the first word of a record's site name."""
def render(self, value):
if value is None:
return None
return first_word(value.name)
def prepend_to_default_columns(table):
"""Show the prefix column first for users without saved table preferences."""
defaults = tuple(table.Meta.default_columns)
table.Meta.default_columns = (
"pk",
COLUMN_NAME,
*(name for name in defaults if name not in {"pk", COLUMN_NAME}),
)
def insert_into_fieldsets(fieldsets, field_name, after="site_id"):
"""Place the field next to the site filter, or in its own fieldset as a fallback."""
from utilities.forms.rendering import FieldSet
result = []
inserted = False
for fieldset in fieldsets:
items = tuple(fieldset.items)
if not inserted and after in items:
index = items.index(after) + 1
fieldset = FieldSet(*items[:index], field_name, *items[index:], name=fieldset.name)
inserted = True
result.append(fieldset)
if not inserted:
result.append(FieldSet(field_name, name="Prefix"))
return tuple(result)
def install_site_prefix_filter():
from dcim.filtersets import DeviceFilterSet, RackFilterSet
from dcim.forms import DeviceFilterForm, RackFilterForm
from utilities.filters import MultiValueCharFilter
for filterset in (DeviceFilterSet, RackFilterSet):
filterset.base_filters[COLUMN_NAME] = MultiValueCharFilter(
method=filter_site_prefix,
label="Site prefix (first word of the site name)",
)
for form in (DeviceFilterForm, RackFilterForm):
form.base_fields[COLUMN_NAME] = forms.CharField(
required=False,
label="Prefix",
help_text="Erstes Wort des Standortnamens, mehrere durch Komma getrennt",
)
form.fieldsets = insert_into_fieldsets(form.fieldsets, COLUMN_NAME)
def install_site_prefix_column():
from dcim.tables import DeviceTable, RackTable
from utilities.tables import register_table_column
column = SitePrefixColumn(
accessor=tables.A("site"),
verbose_name="Prefix",
empty_values=(),
orderable=False,
)
register_table_column(column, COLUMN_NAME, DeviceTable, RackTable)
prepend_to_default_columns(DeviceTable)
prepend_to_default_columns(RackTable)
install_site_prefix_filter()