diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 92c0dc9a6..2a6ae088c 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -6,7 +6,7 @@ from django.db.models import Q from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet from tenancy.models import Tenant, TenantGroup -from utilities.filters import ContentTypeFilter +from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter from virtualization.models import Cluster, ClusterGroup from .choices import * from .models import * @@ -114,6 +114,12 @@ class TagFilterSet(ChangeLoggedModelFilterSet): method='search', label='Search', ) + content_type = MultiValueCharFilter( + method='_content_type' + ) + content_type_id = MultiValueNumberFilter( + method='_content_type_id' + ) class Meta: model = Tag @@ -127,6 +133,32 @@ class TagFilterSet(ChangeLoggedModelFilterSet): Q(slug__icontains=value) ) + def _content_type(self, queryset, name, values): + ct_filter = Q() + + # Compile list of app_label & model pairings + for value in values: + try: + app_label, model = value.lower().split('.') + ct_filter |= Q( + app_label=app_label, + model=model + ) + except ValueError: + pass + + # Get ContentType instances + content_types = ContentType.objects.filter(ct_filter) + + return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct() + + def _content_type_id(self, queryset, name, values): + + # Get ContentType instances + content_types = ContentType.objects.filter(pk__in=values) + + return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct() + class ConfigContextFilterSet(ChangeLoggedModelFilterSet): q = django_filters.CharFilter( diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 977ad9d68..ab1c5aded 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou from tenancy.models import Tenant, TenantGroup from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2, - BOOLEAN_WITH_BLANK_CHOICES, + CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, + JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, ) from virtualization.models import Cluster, ClusterGroup from .choices import * from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag +from .utils import FeatureQuery # @@ -180,6 +181,11 @@ class TagFilterForm(BootstrapMixin, forms.Form): required=False, label=_('Search') ) + content_type_id = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()), + required=False, + label=_('Tagged object type') + ) class TagBulkEditForm(BootstrapMixin, BulkEditForm): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index eb08f5930..656c3efdc 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from circuits.models import Provider from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices from extras.filtersets import * @@ -537,6 +538,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): ) Tag.objects.bulk_create(tags) + # Apply some tags so we can filter by content type + site = Site.objects.create(name='Site 1', slug='site-1') + provider = Provider.objects.create(name='Provider 1', slug='provider-1') + + site.tags.set(tags[0]) + provider.tags.set(tags[1]) + def test_name(self): params = {'name': ['Tag 1', 'Tag 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -549,6 +557,14 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'color': ['ff0000', '00ff00']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_content_type(self): + params = {'content_type': ['dcim.site', 'circuits.provider']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + site_ct = ContentType.objects.get_for_model(Site).pk + provider_ct = ContentType.objects.get_for_model(Provider).pk + params = {'content_type_id': [site_ct, provider_ct]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ObjectChangeTestCase(TestCase, BaseFilterSetTests): queryset = ObjectChange.objects.all()