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

Fixes #2921: Replace tags filter with Select2 widget
This commit is contained in:
Jeremy Stretch 2020-02-04 09:38:14 -05:00 committed by GitHub
commit 67e427403f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 59 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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