diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 6919ff16f..3c52c973c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -5,21 +5,25 @@ labels: ["type: bug"] body: - type: markdown attributes: - value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a - current NetBox installation. If you're having trouble with installation or just - looking for assistance with using NetBox, please visit our - [discussion forum](https://github.com/netbox-community/netbox/discussions) instead." + value: > + **NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox + installation. If you're having trouble with installation or just looking for + assistance with using NetBox, please visit our + [discussion forum](https://github.com/netbox-community/netbox/discussions) instead. - type: input attributes: label: NetBox version - description: "What version of NetBox are you currently running?" - placeholder: v2.10.4 + description: > + What version of NetBox are you currently running? (If you don't have access to the most + recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) + before opening a bug report to see if your issue has already been addressed.) + placeholder: v2.11.3 validations: required: true - type: dropdown attributes: label: Python version - description: "What version of Python are you currently running?" + description: What version of Python are you currently running? options: - 3.6 - 3.7 @@ -30,12 +34,13 @@ body: - type: textarea attributes: label: Steps to Reproduce - description: "Describe in detail the exact steps that someone else can take to - reproduce this bug using the current stable release of NetBox. Begin with the - creation of any necessary database objects and call out every operation being - performed explicitly. If reporting a bug in the REST API, be sure to reconstruct - the raw HTTP request(s) being made: Don't rely on a client library such as - pynetbox." + description: > + Describe in detail the exact steps that someone else can take to + reproduce this bug using the current stable release of NetBox. Begin with the + creation of any necessary database objects and call out every operation being + performed explicitly. If reporting a bug in the REST API, be sure to reconstruct + the raw HTTP request(s) being made: Don't rely on a client library such as + pynetbox." placeholder: | 1. Click on "create widget" 2. Set foo to 12 and bar to G @@ -45,14 +50,14 @@ body: - type: textarea attributes: label: Expected Behavior - description: "What did you expect to happen?" - placeholder: "A new widget should have been created with the specified attributes" + description: What did you expect to happen? + placeholder: A new widget should have been created with the specified attributes validations: required: true - type: textarea attributes: label: Observed Behavior - description: "What happened instead?" - placeholder: "A TypeError exception was raised" + description: What happened instead? + placeholder: A TypeError exception was raised validations: required: true diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index 19d9696ad..0f87115fc 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -30,6 +30,6 @@ body: - type: textarea attributes: label: Proposed Changes - description: "Describe the proposed changes and why they are necessary" + description: Describe the proposed changes and why they are necessary. validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 7d7bde225..9181f7ce4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -5,15 +5,16 @@ labels: ["type: feature"] body: - type: markdown attributes: - value: "**NOTE:** This form is only for submitting well-formed proposals to extend or - modify NetBox in some way. If you're trying to solve a problem but can't figure out how, - or if you still need time to work on the details of a proposed new feature, please start - a [discussion](https://github.com/netbox-community/netbox/discussions) instead." + value: > + **NOTE:** This form is only for submitting well-formed proposals to extend or modify + NetBox in some way. If you're trying to solve a problem but can't figure out how, or if + you still need time to work on the details of a proposed new feature, please start a + [discussion](https://github.com/netbox-community/netbox/discussions) instead. - type: input attributes: label: NetBox version - description: "What version of NetBox are you currently running?" - placeholder: v2.10.4 + description: What version of NetBox are you currently running? + placeholder: v2.11.3 validations: required: true - type: dropdown @@ -28,26 +29,29 @@ body: - type: textarea attributes: label: Proposed functionality - description: "Describe in detail the new feature or behavior you'd like to propose. - Include any specific changes to work flows, data models, or the user interface." + description: > + Describe in detail the new feature or behavior you'd like to propose. Include any specific + changes to work flows, data models, or the user interface. validations: required: true - type: textarea attributes: label: Use case - description: "Explain how adding this functionality would benefit NetBox users. What - need does it address?" + description: > + Explain how adding this functionality would benefit NetBox users. What need does it address? validations: required: true - type: textarea attributes: label: Database changes - description: "Note any changes to the database schema necessary to support the new - feature. For example, does the proposal require adding a new model or field? (Not - all new features require database changes.)" + description: > + Note any changes to the database schema necessary to support the new feature. For example, + does the proposal require adding a new model or field? (Not all new features require database + changes.) - type: textarea attributes: label: External dependencies - description: "List any new dependencies on external libraries or services that this - new feature would introduce. For example, does the proposal require the installation - of a new Python package? (Not all new features introduce new dependencies.)" + description: > + List any new dependencies on external libraries or services that this new feature would + introduce. For example, does the proposal require the installation of a new Python package? + (Not all new features introduce new dependencies.) diff --git a/.github/ISSUE_TEMPLATE/housekeeping.yaml b/.github/ISSUE_TEMPLATE/housekeeping.yaml index 5e675583e..777871395 100644 --- a/.github/ISSUE_TEMPLATE/housekeeping.yaml +++ b/.github/ISSUE_TEMPLATE/housekeeping.yaml @@ -5,18 +5,20 @@ labels: ["type: housekeeping"] body: - type: markdown attributes: - value: "**NOTE:** This template is for use by maintainers only. Please do not submit - an issue using this template unless you have been specifically asked to do so." + value: > + **NOTE:** This template is for use by maintainers only. Please do not submit + an issue using this template unless you have been specifically asked to do so. - type: textarea attributes: label: Proposed Changes - description: "Describe in detail the new feature or behavior you'd like to propose. - Include any specific changes to work flows, data models, or the user interface." + description: > + Describe in detail the new feature or behavior you'd like to propose. + Include any specific changes to work flows, data models, or the user interface. validations: required: true - type: textarea attributes: label: Justification - description: "Please provide justification for the proposed change(s)." + description: Please provide justification for the proposed change(s). validations: required: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8fc85ead6..0f617e8aa 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,9 +17,10 @@ jobs: necessary. close-pr-message: > This PR has been automatically closed due to lack of activity. - days-before-stale: 45 - days-before-close: 15 + days-before-stale: 60 + days-before-close: 30 exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone' + operations-per-run: 100 remove-stale-when-updated: false stale-issue-label: 'pending closure' stale-issue-message: > diff --git a/README.md b/README.md index e35b72c2e..ad0595782 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ - +
VRFs
Aggregates
+Prefixes
diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html index 9b2085189..badb2b40c 100644 --- a/netbox/templates/virtualization/clustergroup.html +++ b/netbox/templates/virtualization/clustergroup.html @@ -47,7 +47,7 @@ {% include 'inc/table.html' with table=clusters_table %} {% if perms.virtualization.add_cluster %} diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 3b57e1a02..2e049135d 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -4,7 +4,7 @@ from circuits.models import Circuit from dcim.models import Device, Rack, Site from extras.api.views import CustomFieldModelViewSet from ipam.models import IPAddress, Prefix, VLAN, VRF -from tenancy import filters +from tenancy import filtersets from tenancy.models import Tenant, TenantGroup from utilities.utils import count_related from virtualization.models import VirtualMachine @@ -32,7 +32,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet): cumulative=True ) serializer_class = serializers.TenantGroupSerializer - filterset_class = filters.TenantGroupFilterSet + filterset_class = filtersets.TenantGroupFilterSet # @@ -54,4 +54,4 @@ class TenantViewSet(CustomFieldModelViewSet): vrf_count=count_related(VRF, 'tenant') ) serializer_class = serializers.TenantSerializer - filterset_class = filters.TenantFilterSet + filterset_class = filtersets.TenantFilterSet diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filtersets.py similarity index 86% rename from netbox/tenancy/filters.py rename to netbox/tenancy/filtersets.py index 0581866a4..d00b78629 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filtersets.py @@ -1,8 +1,9 @@ import django_filters from django.db.models import Q -from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet -from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter +from extras.filters import TagFilter +from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet +from utilities.filters import TreeNodeMultipleChoiceFilter from .models import Tenant, TenantGroup @@ -13,7 +14,7 @@ __all__ = ( ) -class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class TenantGroupFilterSet(OrganizationalModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=TenantGroup.objects.all(), label='Tenant group (ID)', @@ -30,7 +31,7 @@ class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdate fields = ['id', 'name', 'slug', 'description'] -class TenantFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class TenantFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index cad1b3c20..c9f55ec84 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -14,7 +14,7 @@ __all__ = ( ) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class TenantGroup(NestedGroupModel): """ An arbitrary collection of Tenants. diff --git a/netbox/tenancy/tests/test_filters.py b/netbox/tenancy/tests/test_filtersets.py similarity index 88% rename from netbox/tenancy/tests/test_filters.py rename to netbox/tenancy/tests/test_filtersets.py index c78b25083..fd4a0bd76 100644 --- a/netbox/tenancy/tests/test_filters.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -1,10 +1,11 @@ from django.test import TestCase -from tenancy.filters import * +from tenancy.filtersets import * from tenancy.models import Tenant, TenantGroup +from utilities.testing import ChangeLoggedFilterSetTests -class TenantGroupTestCase(TestCase): +class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = TenantGroup.objects.all() filterset = TenantGroupFilterSet @@ -27,10 +28,6 @@ class TenantGroupTestCase(TestCase): for tenantgroup in tenant_groups: tenantgroup.save() - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:2]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_name(self): params = {'name': ['Tenant Group 1', 'Tenant Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -51,7 +48,7 @@ class TenantGroupTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class TenantTestCase(TestCase): +class TenantTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Tenant.objects.all() filterset = TenantFilterSet @@ -73,10 +70,6 @@ class TenantTestCase(TestCase): ) Tenant.objects.bulk_create(tenants) - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:2]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_name(self): params = {'name': ['Tenant 1', 'Tenant 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 206ff6c7a..b4a29a2e6 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,10 +1,10 @@ from circuits.models import Circuit from dcim.models import Site, Rack, Device, RackReservation -from ipam.models import IPAddress, Prefix, VLAN, VRF +from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from netbox.views import generic from utilities.tables import paginate_table from virtualization.models import VirtualMachine, Cluster -from . import filters, forms, tables +from . import filtersets, forms, tables from .models import Tenant, TenantGroup @@ -63,7 +63,7 @@ class TenantGroupBulkEditView(generic.BulkEditView): 'tenant_count', cumulative=True ) - filterset = filters.TenantGroupFilterSet + filterset = filtersets.TenantGroupFilterSet table = tables.TenantGroupTable form = forms.TenantGroupBulkEditForm @@ -85,7 +85,7 @@ class TenantGroupBulkDeleteView(generic.BulkDeleteView): class TenantListView(generic.ObjectListView): queryset = Tenant.objects.all() - filterset = filters.TenantFilterSet + filterset = filtersets.TenantFilterSet filterset_form = forms.TenantFilterForm table = tables.TenantTable @@ -101,6 +101,7 @@ class TenantView(generic.ObjectView): 'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'vrf_count': VRF.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(), + 'aggregate_count': Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(), @@ -130,12 +131,12 @@ class TenantBulkImportView(generic.BulkImportView): class TenantBulkEditView(generic.BulkEditView): queryset = Tenant.objects.prefetch_related('group') - filterset = filters.TenantFilterSet + filterset = filtersets.TenantFilterSet table = tables.TenantTable form = forms.TenantBulkEditForm class TenantBulkDeleteView(generic.BulkDeleteView): queryset = Tenant.objects.prefetch_related('group') - filterset = filters.TenantFilterSet + filterset = filtersets.TenantFilterSet table = tables.TenantTable diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index 7773e54f4..b0443b87e 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -6,7 +6,7 @@ from rest_framework.routers import APIRootView from rest_framework.viewsets import ViewSet from netbox.api.views import ModelViewSet -from users import filters +from users import filtersets from users.models import ObjectPermission, UserConfig from utilities.querysets import RestrictedQuerySet from utilities.utils import deepmerge @@ -28,13 +28,13 @@ class UsersRootView(APIRootView): class UserViewSet(ModelViewSet): queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username') serializer_class = serializers.UserSerializer - filterset_class = filters.UserFilterSet + filterset_class = filtersets.UserFilterSet class GroupViewSet(ModelViewSet): queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name') serializer_class = serializers.GroupSerializer - filterset_class = filters.GroupFilterSet + filterset_class = filtersets.GroupFilterSet # @@ -44,7 +44,7 @@ class GroupViewSet(ModelViewSet): class ObjectPermissionViewSet(ModelViewSet): queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users') serializer_class = serializers.ObjectPermissionSerializer - filterset_class = filters.ObjectPermissionFilterSet + filterset_class = filtersets.ObjectPermissionFilterSet # diff --git a/netbox/users/filters.py b/netbox/users/filtersets.py similarity index 98% rename from netbox/users/filters.py rename to netbox/users/filtersets.py index 359cf9cc7..6625cba36 100644 --- a/netbox/users/filters.py +++ b/netbox/users/filtersets.py @@ -2,8 +2,8 @@ import django_filters from django.contrib.auth.models import Group, User from django.db.models import Q +from netbox.filtersets import BaseFilterSet from users.models import ObjectPermission -from utilities.filters import BaseFilterSet __all__ = ( 'GroupFilterSet', diff --git a/netbox/users/tests/test_filters.py b/netbox/users/tests/test_filtersets.py similarity index 89% rename from netbox/users/tests/test_filters.py rename to netbox/users/tests/test_filtersets.py index c3774927c..32a6b6cd9 100644 --- a/netbox/users/tests/test_filters.py +++ b/netbox/users/tests/test_filtersets.py @@ -2,11 +2,12 @@ from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from users.filters import GroupFilterSet, ObjectPermissionFilterSet, UserFilterSet +from users.filtersets import GroupFilterSet, ObjectPermissionFilterSet, UserFilterSet from users.models import ObjectPermission +from utilities.testing import BaseFilterSetTests -class UserTestCase(TestCase): +class UserTestCase(TestCase, BaseFilterSetTests): queryset = User.objects.all() filterset = UserFilterSet @@ -59,10 +60,6 @@ class UserTestCase(TestCase): users[1].groups.set([groups[1]]) users[2].groups.set([groups[2]]) - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:2]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_username(self): params = {'username': ['User1', 'User2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -95,7 +92,7 @@ class UserTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class GroupTestCase(TestCase): +class GroupTestCase(TestCase, BaseFilterSetTests): queryset = Group.objects.all() filterset = GroupFilterSet @@ -109,16 +106,12 @@ class GroupTestCase(TestCase): ) Group.objects.bulk_create(groups) - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:2]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_name(self): params = {'name': ['Group 1', 'Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ObjectPermissionTestCase(TestCase): +class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): queryset = ObjectPermission.objects.all() filterset = ObjectPermissionFilterSet @@ -160,10 +153,6 @@ class ObjectPermissionTestCase(TestCase): permissions[i].users.set([users[i]]) permissions[i].object_types.set([object_types[i]]) - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:2]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_name(self): params = {'name': ['Permission 1', 'Permission 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 6305c0bba..ed71afc1b 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -1,17 +1,9 @@ import django_filters -from django_filters.constants import EMPTY_VALUES -from copy import deepcopy -from dcim.forms import MACAddressField from django import forms from django.conf import settings -from django.db import models -from django_filters.utils import get_model_field, resolve_field +from django_filters.constants import EMPTY_VALUES -from extras.models import Tag -from utilities.constants import ( - FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, - FILTER_NUMERIC_BASED_LOOKUP_MAP -) +from dcim.forms import MACAddressField def multivalue_field_factory(field_class): @@ -91,21 +83,6 @@ class NullableCharFieldFilter(django_filters.CharFilter): return qs.distinct() if self.distinct else qs -class TagFilter(django_filters.ModelMultipleChoiceFilter): - """ - Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered - to objects matching all tags. - """ - def __init__(self, *args, **kwargs): - - kwargs.setdefault('field_name', 'tags__slug') - kwargs.setdefault('to_field_name', 'slug') - kwargs.setdefault('conjoined', True) - kwargs.setdefault('queryset', Tag.objects.all()) - - super().__init__(*args, **kwargs) - - class NumericArrayFilter(django_filters.NumberFilter): """ Filter based on the presence of an integer within an ArrayField. @@ -134,182 +111,3 @@ class ContentTypeFilter(django_filters.CharFilter): f'{self.field_name}__model': model } ) - - -# -# FilterSets -# - -class BaseFilterSet(django_filters.FilterSet): - """ - A base filterset which provides common functionaly to all NetBox filtersets - """ - FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS) - FILTER_DEFAULTS.update({ - models.AutoField: { - 'filter_class': MultiValueNumberFilter - }, - models.CharField: { - 'filter_class': MultiValueCharFilter - }, - models.DateField: { - 'filter_class': MultiValueDateFilter - }, - models.DateTimeField: { - 'filter_class': MultiValueDateTimeFilter - }, - models.DecimalField: { - 'filter_class': MultiValueNumberFilter - }, - models.EmailField: { - 'filter_class': MultiValueCharFilter - }, - models.FloatField: { - 'filter_class': MultiValueNumberFilter - }, - models.IntegerField: { - 'filter_class': MultiValueNumberFilter - }, - models.PositiveIntegerField: { - 'filter_class': MultiValueNumberFilter - }, - models.PositiveSmallIntegerField: { - 'filter_class': MultiValueNumberFilter - }, - models.SlugField: { - 'filter_class': MultiValueCharFilter - }, - models.SmallIntegerField: { - 'filter_class': MultiValueNumberFilter - }, - models.TimeField: { - 'filter_class': MultiValueTimeFilter - }, - models.URLField: { - 'filter_class': MultiValueCharFilter - }, - MACAddressField: { - 'filter_class': MultiValueMACAddressFilter - }, - }) - - @staticmethod - def _get_filter_lookup_dict(existing_filter): - # Choose the lookup expression map based on the filter type - if isinstance(existing_filter, ( - MultiValueDateFilter, - MultiValueDateTimeFilter, - MultiValueNumberFilter, - MultiValueTimeFilter - )): - lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP - - elif isinstance(existing_filter, ( - TreeNodeMultipleChoiceFilter, - )): - # TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression - lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP - - elif isinstance(existing_filter, ( - django_filters.ModelChoiceFilter, - django_filters.ModelMultipleChoiceFilter, - TagFilter - )) or existing_filter.extra.get('choices'): - # These filter types support only negation - lookup_map = FILTER_NEGATION_LOOKUP_MAP - - elif isinstance(existing_filter, ( - django_filters.filters.CharFilter, - django_filters.MultipleChoiceFilter, - MultiValueCharFilter, - MultiValueMACAddressFilter - )): - lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP - - else: - lookup_map = None - - return lookup_map - - @classmethod - def get_filters(cls): - """ - Override filter generation to support dynamic lookup expressions for certain filter types. - - For specific filter types, new filters are created based on defined lookup expressions in - the form `