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

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
 * [#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)

+ 4 - 2
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)
 
 
 #

+ 17 - 1
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)

+ 7 - 1
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):

+ 2 - 1
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)
 
 
 #

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 2 - 1
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)
 
 
 #

+ 18 - 0
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):

+ 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.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())
 

+ 3 - 1
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)
 
 
 #