diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 6f286ed96..162523e0f 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -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) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 643359b74..caf8d9d36 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -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) # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 8619d6844..0320eefce 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e166136dd..24f044f79 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -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): diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 46b2c12f4..2b5e059ca 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -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) # diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index d686bdf7a..169aab072 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index e4ee7fb2b..4126f75ec 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/device_component_list.html b/netbox/templates/dcim/device_component_list.html index 3936a1c19..28322973e 100644 --- a/netbox/templates/dcim/device_component_list.html +++ b/netbox/templates/dcim/device_component_list.html @@ -14,7 +14,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index 623d69aa2..8b991689f 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index 3b8988ed8..75f587f5d 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/powerfeed_list.html b/netbox/templates/dcim/powerfeed_list.html index cfe2c989c..e384cb2c2 100644 --- a/netbox/templates/dcim/powerfeed_list.html +++ b/netbox/templates/dcim/powerfeed_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index 72da3048e..2724e4427 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 64948a6f9..ef9e0e411 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/dcim/virtualchassis_list.html b/netbox/templates/dcim/virtualchassis_list.html index 8c26f3c3e..55cfc1691 100644 --- a/netbox/templates/dcim/virtualchassis_list.html +++ b/netbox/templates/dcim/virtualchassis_list.html @@ -13,7 +13,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/inc/tags_panel.html b/netbox/templates/inc/tags_panel.html deleted file mode 100644 index a7923fbed..000000000 --- a/netbox/templates/inc/tags_panel.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load helpers %} - -
-
- - Tags -
-
- {% for tag in tags %} - {{ tag }} {{ tag.count }} - {% endfor %} -
-
diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index aad747b2d..27363a56d 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -17,7 +17,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
Statistics diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index 12f227301..b7920a434 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index b80af8e1d..f0754d37b 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -21,7 +21,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/service_list.html b/netbox/templates/ipam/service_list.html index a39bec22e..4aac520d9 100644 --- a/netbox/templates/ipam/service_list.html +++ b/netbox/templates/ipam/service_list.html @@ -12,7 +12,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index b4d313a8c..24d538f88 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 566e2f3e6..975c73a37 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index b6d792765..ee631b439 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -15,7 +15,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index 91463c52c..a77636a5b 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/cluster_list.html b/netbox/templates/virtualization/cluster_list.html index 3fef90c03..6f5f058ad 100644 --- a/netbox/templates/virtualization/cluster_list.html +++ b/netbox/templates/virtualization/cluster_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html index b10341547..821f956a2 100644 --- a/netbox/templates/virtualization/virtualmachine_list.html +++ b/netbox/templates/virtualization/virtualmachine_list.html @@ -16,7 +16,6 @@
{% include 'inc/search_panel.html' %} - {% include 'inc/tags_panel.html' %}
{% endblock %} diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 22a4809de..b0468b37a 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -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) # diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index a14ec9305..a6eca7382 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -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): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index f62ebe6cf..d4a0c1289 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -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()) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 066e162d3..b470bace2 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -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) #