mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #3894 from hSaria/2921-tags-select2
Fixes #2921: Replace tags filter with Select2 widget
This commit is contained in:
commit
67e427403f
@ -18,6 +18,10 @@
|
||||
* [#4071](https://github.com/netbox-community/netbox/issues/4071) - Enforce "view tag" permission on individual tag view
|
||||
* [#4079](https://github.com/netbox-community/netbox/issues/4079) - Fix assignment of power panel when bulk editing power feeds
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget
|
||||
|
||||
---
|
||||
|
||||
# v2.7.3 (2020-01-28)
|
||||
|
@ -8,8 +8,8 @@ from extras.forms import (
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
|
||||
DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
|
||||
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField
|
||||
)
|
||||
from .choices import CircuitStatusChoices
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@ -131,6 +131,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label='ASN'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -335,6 +336,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
min_value=0,
|
||||
label='Commit rate (Kbps)'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
|
@ -24,7 +24,8 @@ from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||
from .choices import *
|
||||
@ -367,6 +368,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -743,6 +745,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -1021,6 +1024,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -2108,6 +2112,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -2156,6 +2161,7 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
|
||||
|
||||
class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||
model = ConsolePort
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -2213,6 +2219,7 @@ class ConsolePortCSVForm(forms.ModelForm):
|
||||
|
||||
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||
model = ConsoleServerPort
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -2305,6 +2312,7 @@ class ConsoleServerPortCSVForm(forms.ModelForm):
|
||||
|
||||
class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||
model = PowerPort
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -2372,6 +2380,7 @@ class PowerPortCSVForm(forms.ModelForm):
|
||||
|
||||
class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||
model = PowerOutlet
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -2540,6 +2549,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
|
||||
|
||||
class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||
model = Interface
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
||||
@ -2865,6 +2875,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
|
||||
|
||||
class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||
model = FrontPort
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -3042,6 +3053,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm):
|
||||
|
||||
class RearPortFilterForm(DeviceComponentFilterForm):
|
||||
model = RearPort
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RearPortForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -3646,6 +3658,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
||||
|
||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
model = DeviceBay
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -3945,6 +3958,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -4131,6 +4145,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -4509,3 +4524,4 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
max_utilization = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
@ -12,7 +12,7 @@ from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
|
||||
CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
|
||||
SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
||||
SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .constants import *
|
||||
@ -105,6 +105,7 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -234,6 +235,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -580,6 +582,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
required=False,
|
||||
label='Expand prefix hierarchy'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -1019,6 +1022,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -1306,6 +1310,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
@ -1366,6 +1371,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
port = forms.IntegerField(
|
||||
required=False,
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
|
@ -9,7 +9,7 @@ from extras.forms import (
|
||||
)
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
|
||||
StaticSelect2Multiple
|
||||
StaticSelect2Multiple, TagFilterField
|
||||
)
|
||||
from .constants import *
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
@ -189,6 +189,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -14,7 +14,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -13,7 +13,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,13 +0,0 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="fa fa-tags" aria-hidden="true"></span>
|
||||
<strong>Tags</strong>
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
{% for tag in tags %}
|
||||
<a href="{% querystring request tag=tag.slug %}" class="btn btn-sm {% if tag.slug in request.GET.tag %}btn-primary{% else %}btn-link{% endif %}">{{ tag }} <span class="badge">{{ tag.count }}</span></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
@ -17,7 +17,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -21,7 +21,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -12,7 +12,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -15,7 +15,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -16,7 +16,6 @@
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -6,7 +6,7 @@ from extras.forms import (
|
||||
)
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
|
||||
FilterChoiceField, SlugField,
|
||||
FilterChoiceField, SlugField, TagFilterField
|
||||
)
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
@ -115,6 +115,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
|
@ -7,6 +7,7 @@ import yaml
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
||||
from django.db.models import Count
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
|
||||
from .choices import unpack_grouped_choices
|
||||
@ -561,6 +562,23 @@ class SlugField(forms.SlugField):
|
||||
self.widget.attrs['slug-source'] = slug_source
|
||||
|
||||
|
||||
class TagFilterField(forms.MultipleChoiceField):
|
||||
"""
|
||||
A filter field for the tags of a model. Only the tags used by a model are displayed.
|
||||
|
||||
:param model: The model of the filter
|
||||
"""
|
||||
widget = StaticSelect2Multiple
|
||||
|
||||
def __init__(self, model, *args, **kwargs):
|
||||
def get_choices():
|
||||
tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
|
||||
return [(str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags]
|
||||
|
||||
# Choices are fetched each time the form is initialized
|
||||
super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
|
||||
|
||||
|
||||
class FilterChoiceIterator(forms.models.ModelChoiceIterator):
|
||||
|
||||
def __iter__(self):
|
||||
|
@ -7,7 +7,8 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import Count, ManyToManyField, ProtectedError
|
||||
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||
from django.db.models.query import QuerySet
|
||||
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||
from django.http import HttpResponse, HttpResponseServerError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template import loader
|
||||
@ -166,12 +167,6 @@ class ObjectListView(View):
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.columns.show('pk')
|
||||
|
||||
# Construct queryset for tags list
|
||||
if is_taggable(model):
|
||||
tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
|
||||
else:
|
||||
tags = None
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
@ -184,7 +179,6 @@ class ObjectListView(View):
|
||||
'table': table,
|
||||
'permissions': permissions,
|
||||
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
||||
'tags': tags,
|
||||
}
|
||||
context.update(self.extra_context())
|
||||
|
||||
|
@ -16,7 +16,7 @@ from utilities.forms import (
|
||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
|
||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
|
||||
SmallTextarea, StaticSelect2, StaticSelect2Multiple
|
||||
SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField
|
||||
)
|
||||
from .choices import *
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
@ -232,6 +232,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||
@ -639,6 +640,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
|
||||
required=False,
|
||||
label='MAC address'
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user