Просмотр исходного кода

Merge pull request #3894 from hSaria/2921-tags-select2

Fixes #2921: Replace tags filter with Select2 widget
Jeremy Stretch 6 лет назад
Родитель
Сommit
67e427403f

+ 4 - 0
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
 * [#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
 * [#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)
 # v2.7.3 (2020-01-28)

+ 4 - 2
netbox/circuits/forms.py

@@ -8,8 +8,8 @@ from extras.forms import (
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 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 .choices import CircuitStatusChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -131,6 +131,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
         required=False,
         required=False,
         label='ASN'
         label='ASN'
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -335,6 +336,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
         min_value=0,
         min_value=0,
         label='Commit rate (Kbps)'
         label='Commit rate (Kbps)'
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #

+ 17 - 1
netbox/dcim/forms.py

@@ -24,7 +24,8 @@ from utilities.forms import (
     APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
     APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
     BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
     BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
     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 virtualization.models import Cluster, ClusterGroup, VirtualMachine
 from .choices import *
 from .choices import *
@@ -367,6 +368,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -743,6 +745,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
             null_option=True,
             null_option=True,
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -1021,6 +1024,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -2108,6 +2112,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -2156,6 +2161,7 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
 
 
 class ConsolePortFilterForm(DeviceComponentFilterForm):
 class ConsolePortFilterForm(DeviceComponentFilterForm):
     model = ConsolePort
     model = ConsolePort
+    tag = TagFilterField(model)
 
 
 
 
 class ConsolePortForm(BootstrapMixin, forms.ModelForm):
 class ConsolePortForm(BootstrapMixin, forms.ModelForm):
@@ -2213,6 +2219,7 @@ class ConsolePortCSVForm(forms.ModelForm):
 
 
 class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
 class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
     model = ConsoleServerPort
     model = ConsoleServerPort
+    tag = TagFilterField(model)
 
 
 
 
 class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
 class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
@@ -2305,6 +2312,7 @@ class ConsoleServerPortCSVForm(forms.ModelForm):
 
 
 class PowerPortFilterForm(DeviceComponentFilterForm):
 class PowerPortFilterForm(DeviceComponentFilterForm):
     model = PowerPort
     model = PowerPort
+    tag = TagFilterField(model)
 
 
 
 
 class PowerPortForm(BootstrapMixin, forms.ModelForm):
 class PowerPortForm(BootstrapMixin, forms.ModelForm):
@@ -2372,6 +2380,7 @@ class PowerPortCSVForm(forms.ModelForm):
 
 
 class PowerOutletFilterForm(DeviceComponentFilterForm):
 class PowerOutletFilterForm(DeviceComponentFilterForm):
     model = PowerOutlet
     model = PowerOutlet
+    tag = TagFilterField(model)
 
 
 
 
 class PowerOutletForm(BootstrapMixin, forms.ModelForm):
 class PowerOutletForm(BootstrapMixin, forms.ModelForm):
@@ -2540,6 +2549,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
 
 
 class InterfaceFilterForm(DeviceComponentFilterForm):
 class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     model = Interface
+    tag = TagFilterField(model)
 
 
 
 
 class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
 class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
@@ -2865,6 +2875,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
 
 
 class FrontPortFilterForm(DeviceComponentFilterForm):
 class FrontPortFilterForm(DeviceComponentFilterForm):
     model = FrontPort
     model = FrontPort
+    tag = TagFilterField(model)
 
 
 
 
 class FrontPortForm(BootstrapMixin, forms.ModelForm):
 class FrontPortForm(BootstrapMixin, forms.ModelForm):
@@ -3042,6 +3053,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm):
 
 
 class RearPortFilterForm(DeviceComponentFilterForm):
 class RearPortFilterForm(DeviceComponentFilterForm):
     model = RearPort
     model = RearPort
+    tag = TagFilterField(model)
 
 
 
 
 class RearPortForm(BootstrapMixin, forms.ModelForm):
 class RearPortForm(BootstrapMixin, forms.ModelForm):
@@ -3646,6 +3658,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
 
 
 class DeviceBayFilterForm(DeviceComponentFilterForm):
 class DeviceBayFilterForm(DeviceComponentFilterForm):
     model = DeviceBay
     model = DeviceBay
+    tag = TagFilterField(model)
 
 
 
 
 class DeviceBayForm(BootstrapMixin, forms.ModelForm):
 class DeviceBayForm(BootstrapMixin, forms.ModelForm):
@@ -3945,6 +3958,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -4131,6 +4145,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
             null_option=True,
             null_option=True,
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -4509,3 +4524,4 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
     max_utilization = forms.IntegerField(
     max_utilization = forms.IntegerField(
         required=False
         required=False
     )
     )
+    tag = TagFilterField(model)

+ 7 - 1
netbox/ipam/forms.py

@@ -12,7 +12,7 @@ from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
     CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
     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 virtualization.models import VirtualMachine
 from .constants import *
 from .constants import *
@@ -105,6 +105,7 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         required=False,
         required=False,
         label='Search'
         label='Search'
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -234,6 +235,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -580,6 +582,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
         required=False,
         required=False,
         label='Expand prefix hierarchy'
         label='Expand prefix hierarchy'
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -1019,6 +1022,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -1306,6 +1310,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
             null_option=True,
             null_option=True,
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #
@@ -1366,6 +1371,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     port = forms.IntegerField(
     port = forms.IntegerField(
         required=False,
         required=False,
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

+ 2 - 1
netbox/secrets/forms.py

@@ -9,7 +9,7 @@ from extras.forms import (
 )
 )
 from utilities.forms import (
 from utilities.forms import (
     APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
     APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
-    StaticSelect2Multiple
+    StaticSelect2Multiple, TagFilterField
 )
 )
 from .constants import *
 from .constants import *
 from .models import Secret, SecretRole, UserKey
 from .models import Secret, SecretRole, UserKey
@@ -189,6 +189,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
             value_field="slug",
             value_field="slug",
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #

+ 0 - 1
netbox/templates/circuits/circuit_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/circuits/provider_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/device_component_list.html

@@ -14,7 +14,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-        {% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/device_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/devicetype_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/powerfeed_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/rack_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/site_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/dcim/virtualchassis_list.html

@@ -13,7 +13,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 13
netbox/templates/inc/tags_panel.html

@@ -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>

+ 0 - 1
netbox/templates/ipam/aggregate_list.html

@@ -17,7 +17,6 @@
 	</div>
 	</div>
 	<div class="col-md-3 noprint">
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong><i class="fa fa-bar-chart"></i> Statistics</strong>
                 <strong><i class="fa fa-bar-chart"></i> Statistics</strong>

+ 0 - 1
netbox/templates/ipam/ipaddress_list.html

@@ -16,7 +16,6 @@
 	</div>
 	</div>
 	<div class="col-md-3 noprint">
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/ipam/prefix_list.html

@@ -21,7 +21,6 @@
 	</div>
 	</div>
 	<div class="col-md-3 noprint">
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/ipam/service_list.html

@@ -12,7 +12,6 @@
 	</div>
 	</div>
 	<div class="col-md-3 noprint">
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/ipam/vlan_list.html

@@ -16,7 +16,6 @@
 	</div>
 	</div>
 	<div class="col-md-3 noprint">
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/ipam/vrf_list.html

@@ -16,7 +16,6 @@
 	</div>
 	</div>
 	<div class="col-md-3 noprint">
 	<div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/secrets/secret_list.html

@@ -15,7 +15,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/tenancy/tenant_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/virtualization/cluster_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 1
netbox/templates/virtualization/virtualmachine_list.html

@@ -16,7 +16,6 @@
     </div>
     </div>
     <div class="col-md-3 noprint">
     <div class="col-md-3 noprint">
 		{% include 'inc/search_panel.html' %}
 		{% include 'inc/search_panel.html' %}
-		{% include 'inc/tags_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 2 - 1
netbox/tenancy/forms.py

@@ -6,7 +6,7 @@ from extras.forms import (
 )
 )
 from utilities.forms import (
 from utilities.forms import (
     APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
     APISelect, APISelectMultiple, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField,
-    FilterChoiceField, SlugField,
+    FilterChoiceField, SlugField, TagFilterField
 )
 )
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
 
 
@@ -115,6 +115,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
             null_option=True,
             null_option=True,
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #

+ 18 - 0
netbox/utilities/forms.py

@@ -7,6 +7,7 @@ import yaml
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
 from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
+from django.db.models import Count
 from mptt.forms import TreeNodeMultipleChoiceField
 from mptt.forms import TreeNodeMultipleChoiceField
 
 
 from .choices import unpack_grouped_choices
 from .choices import unpack_grouped_choices
@@ -561,6 +562,23 @@ class SlugField(forms.SlugField):
         self.widget.attrs['slug-source'] = slug_source
         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):
 class FilterChoiceIterator(forms.models.ModelChoiceIterator):
 
 
     def __iter__(self):
     def __iter__(self):

+ 2 - 8
netbox/utilities/views.py

@@ -7,7 +7,8 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.db import transaction, IntegrityError
 from django.db import transaction, IntegrityError
 from django.db.models import Count, ManyToManyField, ProtectedError
 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.http import HttpResponse, HttpResponseServerError
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template import loader
 from django.template import loader
@@ -166,12 +167,6 @@ class ObjectListView(View):
         if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
         if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
             table.columns.show('pk')
             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
         # Apply the request context
         paginate = {
         paginate = {
             'paginator_class': EnhancedPaginator,
             'paginator_class': EnhancedPaginator,
@@ -184,7 +179,6 @@ class ObjectListView(View):
             'table': table,
             'table': table,
             'permissions': permissions,
             'permissions': permissions,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
-            'tags': tags,
         }
         }
         context.update(self.extra_context())
         context.update(self.extra_context())
 
 

+ 3 - 1
netbox/virtualization/forms.py

@@ -16,7 +16,7 @@ from utilities.forms import (
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
     add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
     ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
     ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
     ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
-    SmallTextarea, StaticSelect2, StaticSelect2Multiple
+    SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField
 )
 )
 from .choices import *
 from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -232,6 +232,7 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
             null_option=True,
             null_option=True,
         )
         )
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
 class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
@@ -639,6 +640,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
         required=False,
         required=False,
         label='MAC address'
         label='MAC address'
     )
     )
+    tag = TagFilterField(model)
 
 
 
 
 #
 #