diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 6919ff16f..073e8dc5c 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.2 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..6ea8b6597 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -5,14 +5,15 @@ 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?" + description: What version of NetBox are you currently running? placeholder: v2.10.4 validations: required: true @@ -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..45f233a55 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,6 +20,7 @@ jobs: days-before-stale: 45 days-before-close: 15 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 237673e6b..f1821f78a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox). -The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). +The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev. ### Discussion diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 4ed3d946e..927bf9f37 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -515,6 +515,14 @@ The file path to the location where custom scripts will be kept. By default, thi --- +## SESSION_COOKIE_NAME + +Default: `sessionid` + +The name used for the session cookie. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-name) for more detail. + +--- + ## SESSION_FILE_PATH Default: None diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index a43e354de..4b5ababbc 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,5 +1,24 @@ # NetBox v2.11 +## v2.11.3 (FUTURE) + +### Enhancements + +* [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter +* [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type + +### Bug Fixes + +* [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view +* [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view +* [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM +* [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view +* [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view +* [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key +* [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master + +--- + ## v2.11.2 (2021-04-27) ### Enhancements diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 0ea8d1973..3bceb2de0 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,6 +1,6 @@ from rest_framework.routers import APIRootView -from circuits import filters +from circuits import filtersets from circuits.models import * from dcim.api.views import PassThroughPortMixin from extras.api.views import CustomFieldModelViewSet @@ -26,7 +26,7 @@ class ProviderViewSet(CustomFieldModelViewSet): circuit_count=count_related(Circuit, 'provider') ) serializer_class = serializers.ProviderSerializer - filterset_class = filters.ProviderFilterSet + filterset_class = filtersets.ProviderFilterSet # @@ -38,7 +38,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet): circuit_count=count_related(Circuit, 'type') ) serializer_class = serializers.CircuitTypeSerializer - filterset_class = filters.CircuitTypeFilterSet + filterset_class = filtersets.CircuitTypeFilterSet # @@ -50,7 +50,7 @@ class CircuitViewSet(CustomFieldModelViewSet): 'type', 'tenant', 'provider', 'termination_a', 'termination_z' ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer - filterset_class = filters.CircuitFilterSet + filterset_class = filtersets.CircuitFilterSet # @@ -62,7 +62,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet): 'circuit', 'site', 'provider_network', 'cable' ) serializer_class = serializers.CircuitTerminationSerializer - filterset_class = filters.CircuitTerminationFilterSet + filterset_class = filtersets.CircuitTerminationFilterSet brief_prefetch_fields = ['circuit'] @@ -73,4 +73,4 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet): class ProviderNetworkViewSet(CustomFieldModelViewSet): queryset = ProviderNetwork.objects.prefetch_related('tags') serializer_class = serializers.ProviderNetworkSerializer - filterset_class = filters.ProviderNetworkFilterSet + filterset_class = filtersets.ProviderNetworkFilterSet diff --git a/netbox/circuits/filters.py b/netbox/circuits/filtersets.py similarity index 89% rename from netbox/circuits/filters.py rename to netbox/circuits/filtersets.py index 034a99ac9..066178685 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filtersets.py @@ -1,13 +1,12 @@ import django_filters from django.db.models import Q -from dcim.filters import CableTerminationFilterSet +from dcim.filtersets import CableTerminationFilterSet from dcim.models import Region, Site, SiteGroup -from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet -from tenancy.filters import TenancyFilterSet -from utilities.filters import ( - BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter -) +from extras.filters import TagFilter +from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet +from tenancy.filtersets import TenancyFilterSet +from utilities.filters import TreeNodeMultipleChoiceFilter from .choices import * from .models import * @@ -20,7 +19,7 @@ __all__ = ( ) -class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class ProviderFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -80,7 +79,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated ) -class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class ProviderNetworkFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -110,14 +109,14 @@ class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, Created ).distinct() -class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class CircuitTypeFilterSet(OrganizationalModelFilterSet): class Meta: model = CircuitType fields = ['id', 'name', 'slug'] -class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): +class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -207,7 +206,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe ).distinct() -class CircuitTerminationFilterSet(BaseFilterSet, CreatedUpdatedFilterSet, CableTerminationFilterSet): +class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -233,7 +232,7 @@ class CircuitTerminationFilterSet(BaseFilterSet, CreatedUpdatedFilterSet, CableT class Meta: model = CircuitTermination - fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id'] + fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filtersets.py similarity index 95% rename from netbox/circuits/tests/test_filters.py rename to netbox/circuits/tests/test_filtersets.py index 448e42368..4880a8388 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -1,13 +1,14 @@ from django.test import TestCase from circuits.choices import * -from circuits.filters import * +from circuits.filtersets import * from circuits.models import * from dcim.models import Cable, Region, Site, SiteGroup from tenancy.models import Tenant, TenantGroup +from utilities.testing import ChangeLoggedFilterSetTests -class ProviderTestCase(TestCase): +class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Provider.objects.all() filterset = ProviderFilterSet @@ -61,10 +62,6 @@ class ProviderTestCase(TestCase): CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'), )) - 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': ['Provider 1', 'Provider 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -103,7 +100,7 @@ class ProviderTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class CircuitTypeTestCase(TestCase): +class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CircuitType.objects.all() filterset = CircuitTypeFilterSet @@ -116,10 +113,6 @@ class CircuitTypeTestCase(TestCase): CircuitType(name='Circuit Type 3', slug='circuit-type-3'), )) - def test_id(self): - params = {'id': [self.queryset.first().pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - def test_name(self): params = {'name': ['Circuit Type 1']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) @@ -129,7 +122,7 @@ class CircuitTypeTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class CircuitTestCase(TestCase): +class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Circuit.objects.all() filterset = CircuitFilterSet @@ -213,10 +206,6 @@ class CircuitTestCase(TestCase): )) CircuitTermination.objects.bulk_create(circuit_terminations) - 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_cid(self): params = {'cid': ['Test Circuit 1', 'Test Circuit 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -288,7 +277,7 @@ class CircuitTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class CircuitTerminationTestCase(TestCase): +class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CircuitTermination.objects.all() filterset = CircuitTerminationFilterSet @@ -382,7 +371,7 @@ class CircuitTerminationTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ProviderNetworkTestCase(TestCase): +class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ProviderNetwork.objects.all() filterset = ProviderNetworkFilterSet @@ -403,10 +392,6 @@ class ProviderNetworkTestCase(TestCase): ) ProviderNetwork.objects.bulk_create(provider_networks) - 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': ['Provider Network 1', 'Provider Network 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index c05ea4622..612602316 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -7,7 +7,7 @@ from netbox.views import generic from utilities.forms import ConfirmationForm from utilities.tables import paginate_table from utilities.utils import count_related -from . import filters, forms, tables +from . import filtersets, forms, tables from .choices import CircuitTerminationSideChoices from .models import * @@ -20,7 +20,7 @@ class ProviderListView(generic.ObjectListView): queryset = Provider.objects.annotate( count_circuits=count_related(Circuit, 'provider') ) - filterset = filters.ProviderFilterSet + filterset = filtersets.ProviderFilterSet filterset_form = forms.ProviderFilterForm table = tables.ProviderTable @@ -63,7 +63,7 @@ class ProviderBulkEditView(generic.BulkEditView): queryset = Provider.objects.annotate( count_circuits=count_related(Circuit, 'provider') ) - filterset = filters.ProviderFilterSet + filterset = filtersets.ProviderFilterSet table = tables.ProviderTable form = forms.ProviderBulkEditForm @@ -72,7 +72,7 @@ class ProviderBulkDeleteView(generic.BulkDeleteView): queryset = Provider.objects.annotate( count_circuits=count_related(Circuit, 'provider') ) - filterset = filters.ProviderFilterSet + filterset = filtersets.ProviderFilterSet table = tables.ProviderTable @@ -82,7 +82,7 @@ class ProviderBulkDeleteView(generic.BulkDeleteView): class ProviderNetworkListView(generic.ObjectListView): queryset = ProviderNetwork.objects.all() - filterset = filters.ProviderNetworkFilterSet + filterset = filtersets.ProviderNetworkFilterSet filterset_form = forms.ProviderNetworkFilterForm table = tables.ProviderNetworkTable @@ -125,14 +125,14 @@ class ProviderNetworkBulkImportView(generic.BulkImportView): class ProviderNetworkBulkEditView(generic.BulkEditView): queryset = ProviderNetwork.objects.all() - filterset = filters.ProviderNetworkFilterSet + filterset = filtersets.ProviderNetworkFilterSet table = tables.ProviderNetworkTable form = forms.ProviderNetworkBulkEditForm class ProviderNetworkBulkDeleteView(generic.BulkDeleteView): queryset = ProviderNetwork.objects.all() - filterset = filters.ProviderNetworkFilterSet + filterset = filtersets.ProviderNetworkFilterSet table = tables.ProviderNetworkTable @@ -183,7 +183,7 @@ class CircuitTypeBulkEditView(generic.BulkEditView): queryset = CircuitType.objects.annotate( circuit_count=count_related(Circuit, 'type') ) - filterset = filters.CircuitTypeFilterSet + filterset = filtersets.CircuitTypeFilterSet table = tables.CircuitTypeTable form = forms.CircuitTypeBulkEditForm @@ -203,7 +203,7 @@ class CircuitListView(generic.ObjectListView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'termination_a', 'termination_z' ) - filterset = filters.CircuitFilterSet + filterset = filtersets.CircuitFilterSet filterset_form = forms.CircuitFilterForm table = tables.CircuitTable @@ -252,7 +252,7 @@ class CircuitBulkEditView(generic.BulkEditView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations' ) - filterset = filters.CircuitFilterSet + filterset = filtersets.CircuitFilterSet table = tables.CircuitTable form = forms.CircuitBulkEditForm @@ -261,7 +261,7 @@ class CircuitBulkDeleteView(generic.BulkDeleteView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations' ) - filterset = filters.CircuitFilterSet + filterset = filtersets.CircuitFilterSet table = tables.CircuitTable diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index cb46c1eca..9d402227f 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -16,7 +16,7 @@ from rest_framework.routers import APIRootView from rest_framework.viewsets import GenericViewSet, ViewSet from circuits.models import Circuit -from dcim import filters +from dcim import filtersets from dcim.models import * from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet from ipam.models import Prefix, VLAN @@ -103,7 +103,7 @@ class RegionViewSet(CustomFieldModelViewSet): cumulative=True ) serializer_class = serializers.RegionSerializer - filterset_class = filters.RegionFilterSet + filterset_class = filtersets.RegionFilterSet # @@ -119,7 +119,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet): cumulative=True ) serializer_class = serializers.SiteGroupSerializer - filterset_class = filters.SiteGroupFilterSet + filterset_class = filtersets.SiteGroupFilterSet # @@ -138,7 +138,7 @@ class SiteViewSet(CustomFieldModelViewSet): virtualmachine_count=count_related(VirtualMachine, 'cluster__site') ) serializer_class = serializers.SiteSerializer - filterset_class = filters.SiteFilterSet + filterset_class = filtersets.SiteFilterSet # @@ -160,7 +160,7 @@ class LocationViewSet(CustomFieldModelViewSet): cumulative=True ).prefetch_related('site') serializer_class = serializers.LocationSerializer - filterset_class = filters.LocationFilterSet + filterset_class = filtersets.LocationFilterSet # @@ -172,7 +172,7 @@ class RackRoleViewSet(CustomFieldModelViewSet): rack_count=count_related(Rack, 'role') ) serializer_class = serializers.RackRoleSerializer - filterset_class = filters.RackRoleFilterSet + filterset_class = filtersets.RackRoleFilterSet # @@ -187,7 +187,7 @@ class RackViewSet(CustomFieldModelViewSet): powerfeed_count=count_related(PowerFeed, 'rack') ) serializer_class = serializers.RackSerializer - filterset_class = filters.RackFilterSet + filterset_class = filtersets.RackFilterSet @swagger_auto_schema( responses={200: serializers.RackUnitSerializer(many=True)}, @@ -244,7 +244,7 @@ class RackViewSet(CustomFieldModelViewSet): class RackReservationViewSet(ModelViewSet): queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') serializer_class = serializers.RackReservationSerializer - filterset_class = filters.RackReservationFilterSet + filterset_class = filtersets.RackReservationFilterSet # Assign user from request def perform_create(self, serializer): @@ -262,7 +262,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet): platform_count=count_related(Platform, 'manufacturer') ) serializer_class = serializers.ManufacturerSerializer - filterset_class = filters.ManufacturerFilterSet + filterset_class = filtersets.ManufacturerFilterSet # @@ -274,7 +274,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): device_count=count_related(Device, 'device_type') ) serializer_class = serializers.DeviceTypeSerializer - filterset_class = filters.DeviceTypeFilterSet + filterset_class = filtersets.DeviceTypeFilterSet brief_prefetch_fields = ['manufacturer'] @@ -285,49 +285,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): class ConsolePortTemplateViewSet(ModelViewSet): queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.ConsolePortTemplateSerializer - filterset_class = filters.ConsolePortTemplateFilterSet + filterset_class = filtersets.ConsolePortTemplateFilterSet class ConsoleServerPortTemplateViewSet(ModelViewSet): queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.ConsoleServerPortTemplateSerializer - filterset_class = filters.ConsoleServerPortTemplateFilterSet + filterset_class = filtersets.ConsoleServerPortTemplateFilterSet class PowerPortTemplateViewSet(ModelViewSet): queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.PowerPortTemplateSerializer - filterset_class = filters.PowerPortTemplateFilterSet + filterset_class = filtersets.PowerPortTemplateFilterSet class PowerOutletTemplateViewSet(ModelViewSet): queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.PowerOutletTemplateSerializer - filterset_class = filters.PowerOutletTemplateFilterSet + filterset_class = filtersets.PowerOutletTemplateFilterSet class InterfaceTemplateViewSet(ModelViewSet): queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.InterfaceTemplateSerializer - filterset_class = filters.InterfaceTemplateFilterSet + filterset_class = filtersets.InterfaceTemplateFilterSet class FrontPortTemplateViewSet(ModelViewSet): queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.FrontPortTemplateSerializer - filterset_class = filters.FrontPortTemplateFilterSet + filterset_class = filtersets.FrontPortTemplateFilterSet class RearPortTemplateViewSet(ModelViewSet): queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.RearPortTemplateSerializer - filterset_class = filters.RearPortTemplateFilterSet + filterset_class = filtersets.RearPortTemplateFilterSet class DeviceBayTemplateViewSet(ModelViewSet): queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.DeviceBayTemplateSerializer - filterset_class = filters.DeviceBayTemplateFilterSet + filterset_class = filtersets.DeviceBayTemplateFilterSet # @@ -340,7 +340,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet): virtualmachine_count=count_related(VirtualMachine, 'role') ) serializer_class = serializers.DeviceRoleSerializer - filterset_class = filters.DeviceRoleFilterSet + filterset_class = filtersets.DeviceRoleFilterSet # @@ -353,7 +353,7 @@ class PlatformViewSet(CustomFieldModelViewSet): virtualmachine_count=count_related(VirtualMachine, 'platform') ) serializer_class = serializers.PlatformSerializer - filterset_class = filters.PlatformFilterSet + filterset_class = filtersets.PlatformFilterSet # @@ -365,7 +365,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', ) - filterset_class = filters.DeviceFilterSet + filterset_class = filtersets.DeviceFilterSet def get_serializer_class(self): """ @@ -510,7 +510,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.ConsolePortSerializer - filterset_class = filters.ConsolePortFilterSet + filterset_class = filtersets.ConsolePortFilterSet brief_prefetch_fields = ['device'] @@ -519,21 +519,21 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): 'device', '_path__destination', 'cable', '_cable_peer', 'tags' ) serializer_class = serializers.ConsoleServerPortSerializer - filterset_class = filters.ConsoleServerPortFilterSet + filterset_class = filtersets.ConsoleServerPortFilterSet brief_prefetch_fields = ['device'] class PowerPortViewSet(PathEndpointMixin, ModelViewSet): queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.PowerPortSerializer - filterset_class = filters.PowerPortFilterSet + filterset_class = filtersets.PowerPortFilterSet brief_prefetch_fields = ['device'] class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.PowerOutletSerializer - filterset_class = filters.PowerOutletFilterSet + filterset_class = filtersets.PowerOutletFilterSet brief_prefetch_fields = ['device'] @@ -542,35 +542,35 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet): 'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags' ) serializer_class = serializers.InterfaceSerializer - filterset_class = filters.InterfaceFilterSet + filterset_class = filtersets.InterfaceFilterSet brief_prefetch_fields = ['device'] class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') serializer_class = serializers.FrontPortSerializer - filterset_class = filters.FrontPortFilterSet + filterset_class = filtersets.FrontPortFilterSet brief_prefetch_fields = ['device'] class RearPortViewSet(PassThroughPortMixin, ModelViewSet): queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') serializer_class = serializers.RearPortSerializer - filterset_class = filters.RearPortFilterSet + filterset_class = filtersets.RearPortFilterSet brief_prefetch_fields = ['device'] class DeviceBayViewSet(ModelViewSet): queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') serializer_class = serializers.DeviceBaySerializer - filterset_class = filters.DeviceBayFilterSet + filterset_class = filtersets.DeviceBayFilterSet brief_prefetch_fields = ['device'] class InventoryItemViewSet(ModelViewSet): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') serializer_class = serializers.InventoryItemSerializer - filterset_class = filters.InventoryItemFilterSet + filterset_class = filtersets.InventoryItemFilterSet brief_prefetch_fields = ['device'] @@ -583,7 +583,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet): _path__destination_id__isnull=False ) serializer_class = serializers.ConsolePortSerializer - filterset_class = filters.ConsoleConnectionFilterSet + filterset_class = filtersets.ConsoleConnectionFilterSet class PowerConnectionViewSet(ListModelMixin, GenericViewSet): @@ -591,7 +591,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): _path__destination_id__isnull=False ) serializer_class = serializers.PowerPortSerializer - filterset_class = filters.PowerConnectionFilterSet + filterset_class = filtersets.PowerConnectionFilterSet class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): @@ -603,7 +603,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): pk__lt=F('_path__destination_id') ) serializer_class = serializers.InterfaceConnectionSerializer - filterset_class = filters.InterfaceConnectionFilterSet + filterset_class = filtersets.InterfaceConnectionFilterSet # @@ -616,7 +616,7 @@ class CableViewSet(ModelViewSet): 'termination_a', 'termination_b' ) serializer_class = serializers.CableSerializer - filterset_class = filters.CableFilterSet + filterset_class = filtersets.CableFilterSet # @@ -628,7 +628,7 @@ class VirtualChassisViewSet(ModelViewSet): member_count=count_related(Device, 'virtual_chassis') ) serializer_class = serializers.VirtualChassisSerializer - filterset_class = filters.VirtualChassisFilterSet + filterset_class = filtersets.VirtualChassisFilterSet brief_prefetch_fields = ['master'] @@ -643,7 +643,7 @@ class PowerPanelViewSet(ModelViewSet): powerfeed_count=count_related(PowerFeed, 'power_panel') ) serializer_class = serializers.PowerPanelSerializer - filterset_class = filters.PowerPanelFilterSet + filterset_class = filtersets.PowerPanelFilterSet # @@ -655,7 +655,7 @@ class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet): 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags' ) serializer_class = serializers.PowerFeedSerializer - filterset_class = filters.PowerFeedFilterSet + filterset_class = filtersets.PowerFeedFilterSet # diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2483c8d12..c5646cf2b 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1001,6 +1001,7 @@ class CableTypeChoices(ChoiceSet): TYPE_MMF_OM2 = 'mmf-om2' TYPE_MMF_OM3 = 'mmf-om3' TYPE_MMF_OM4 = 'mmf-om4' + TYPE_MMF_OM5 = 'mmf-om5' TYPE_SMF = 'smf' TYPE_SMF_OS1 = 'smf-os1' TYPE_SMF_OS2 = 'smf-os2' @@ -1031,6 +1032,7 @@ class CableTypeChoices(ChoiceSet): (TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), (TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), (TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), + (TYPE_MMF_OM5, 'Multimode Fiber (OM5)'), (TYPE_SMF, 'Singlemode Fiber'), (TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'), (TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'), diff --git a/netbox/dcim/filters.py b/netbox/dcim/filtersets.py similarity index 91% rename from netbox/dcim/filters.py rename to netbox/dcim/filtersets.py index 29c4281ba..b04c14ba9 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filtersets.py @@ -1,13 +1,16 @@ import django_filters from django.contrib.auth.models import User -from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet -from tenancy.filters import TenancyFilterSet +from extras.filters import TagFilter +from extras.filtersets import LocalConfigContextFilterSet +from netbox.filtersets import ( + BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet, +) +from tenancy.filtersets import TenancyFilterSet from tenancy.models import Tenant from utilities.choices import ColorChoices from utilities.filters import ( - BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, - NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter, + MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter, ) from virtualization.models import Cluster from .choices import * @@ -57,7 +60,7 @@ __all__ = ( ) -class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class RegionFilterSet(OrganizationalModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label='Parent region (ID)', @@ -74,7 +77,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilt fields = ['id', 'name', 'slug', 'description'] -class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class SiteGroupFilterSet(OrganizationalModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=SiteGroup.objects.all(), label='Parent site group (ID)', @@ -91,7 +94,7 @@ class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF fields = ['id', 'name', 'slug', 'description'] -class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -154,7 +157,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, return queryset.filter(qs_filter) -class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class LocationFilterSet(OrganizationalModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region', @@ -218,14 +221,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFi ) -class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class RackRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = RackRole fields = ['id', 'name', 'slug', 'color'] -class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -323,7 +326,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, ) -class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -383,14 +386,14 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel ) -class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class ManufacturerFilterSet(OrganizationalModelFilterSet): class Meta: model = Manufacturer fields = ['id', 'name', 'slug', 'description'] -class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class DeviceTypeFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -476,7 +479,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat return queryset.exclude(devicebaytemplates__isnull=value) -class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class DeviceTypeComponentFilterSet(django_filters.FilterSet): devicetype_id = django_filters.ModelMultipleChoiceFilter( queryset=DeviceType.objects.all(), field_name='device_type_id', @@ -484,28 +487,28 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet, CreatedUpdatedFilter ) -class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate fields = ['id', 'name', 'type'] -class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate fields = ['id', 'name', 'type'] -class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class Meta: model = PowerPortTemplate fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] -class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): feed_leg = django_filters.MultipleChoiceFilter( choices=PowerOutletFeedLegChoices, null_value=None @@ -516,7 +519,7 @@ class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): fields = ['id', 'name', 'type', 'feed_leg'] -class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=InterfaceTypeChoices, null_value=None @@ -527,7 +530,7 @@ class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): fields = ['id', 'name', 'type', 'mgmt_only'] -class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=PortTypeChoices, null_value=None @@ -538,7 +541,7 @@ class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): fields = ['id', 'name', 'type'] -class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): type = django_filters.MultipleChoiceFilter( choices=PortTypeChoices, null_value=None @@ -549,21 +552,21 @@ class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): fields = ['id', 'name', 'type', 'positions'] -class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): +class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class Meta: model = DeviceBayTemplate fields = ['id', 'name'] -class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class DeviceRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = DeviceRole fields = ['id', 'name', 'slug', 'color', 'vm_role'] -class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class PlatformFilterSet(OrganizationalModelFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='manufacturer', queryset=Manufacturer.objects.all(), @@ -581,13 +584,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFi fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] -class DeviceFilterSet( - BaseFilterSet, - TenancyFilterSet, - LocalConfigContextFilterSet, - CustomFieldModelFilterSet, - CreatedUpdatedFilterSet -): +class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -792,7 +789,7 @@ class DeviceFilterSet( return queryset.exclude(devicebays__isnull=value) -class DeviceComponentFilterSet(CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class DeviceComponentFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -876,7 +873,7 @@ class PathEndpointFilterSet(django_filters.FilterSet): return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)) -class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): +class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -887,12 +884,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina fields = ['id', 'name', 'label', 'description'] -class ConsoleServerPortFilterSet( - BaseFilterSet, - DeviceComponentFilterSet, - CableTerminationFilterSet, - PathEndpointFilterSet -): +class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=ConsolePortTypeChoices, null_value=None @@ -903,7 +895,7 @@ class ConsoleServerPortFilterSet( fields = ['id', 'name', 'label', 'description'] -class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): +class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerPortTypeChoices, null_value=None @@ -914,7 +906,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description'] -class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): +class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): type = django_filters.MultipleChoiceFilter( choices=PowerOutletTypeChoices, null_value=None @@ -929,7 +921,7 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina fields = ['id', 'name', 'label', 'feed_leg', 'description'] -class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): +class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1027,7 +1019,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati }.get(value, queryset.none()) -class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): +class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): type = django_filters.MultipleChoiceFilter( choices=PortTypeChoices, null_value=None @@ -1038,7 +1030,7 @@ class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati fields = ['id', 'name', 'label', 'type', 'description'] -class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): +class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): type = django_filters.MultipleChoiceFilter( choices=PortTypeChoices, null_value=None @@ -1049,14 +1041,14 @@ class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminatio fields = ['id', 'name', 'label', 'type', 'positions', 'description'] -class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): class Meta: model = DeviceBay fields = ['id', 'name', 'label', 'description'] -class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): +class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1129,7 +1121,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class VirtualChassisFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1209,7 +1201,7 @@ class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedU return queryset.filter(qs_filter).distinct() -class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class CableFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1273,7 +1265,7 @@ class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFil return queryset -class ConnectionFilterSet: +class ConnectionFilterSet(BaseFilterSet): def filter_site(self, queryset, name, value): if not value.strip(): @@ -1286,7 +1278,7 @@ class ConnectionFilterSet: return queryset.filter(**{f'{name}__in': value}) -class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): +class ConsoleConnectionFilterSet(ConnectionFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1304,7 +1296,7 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): fields = ['name'] -class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): +class PowerConnectionFilterSet(ConnectionFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1322,7 +1314,7 @@ class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): fields = ['name'] -class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): +class InterfaceConnectionFilterSet(ConnectionFilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1340,7 +1332,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): fields = [] -class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class PowerPanelFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1402,13 +1394,7 @@ class PowerPanelFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat return queryset.filter(qs_filter) -class PowerFeedFilterSet( - BaseFilterSet, - CableTerminationFilterSet, - PathEndpointFilterSet, - CustomFieldModelFilterSet, - CreatedUpdatedFilterSet -): +class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 4f7c05c71..5f275f1eb 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -520,6 +520,7 @@ class DeviceInterfaceTable(InterfaceTable): 'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', ) + order_by = ('name',) default_columns = ( 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'cable', 'connection', 'actions', diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filtersets.py similarity index 95% rename from netbox/dcim/tests/test_filters.py rename to netbox/dcim/tests/test_filtersets.py index 632a10c46..154ec0847 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2,14 +2,15 @@ from django.contrib.auth.models import User from django.test import TestCase from dcim.choices import * -from dcim.filters import * +from dcim.filtersets import * from dcim.models import * from ipam.models import IPAddress from tenancy.models import Tenant, TenantGroup +from utilities.testing import ChangeLoggedFilterSetTests from virtualization.models import Cluster, ClusterType -class RegionTestCase(TestCase): +class RegionTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Region.objects.all() filterset = RegionFilterSet @@ -35,10 +36,6 @@ class RegionTestCase(TestCase): for region in child_regions: region.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': ['Region 1', 'Region 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -59,7 +56,7 @@ class RegionTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class SiteGroupTestCase(TestCase): +class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = SiteGroup.objects.all() filterset = SiteGroupFilterSet @@ -85,10 +82,6 @@ class SiteGroupTestCase(TestCase): for sitegroup in child_sitegroups: sitegroup.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': ['Site Group 1', 'Site Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -109,7 +102,7 @@ class SiteGroupTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class SiteTestCase(TestCase): +class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Site.objects.all() filterset = SiteFilterSet @@ -154,10 +147,6 @@ class SiteTestCase(TestCase): ) Site.objects.bulk_create(sites) - 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': ['Site 1', 'Site 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -227,7 +216,7 @@ class SiteTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class LocationTestCase(TestCase): +class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Location.objects.all() filterset = LocationFilterSet @@ -273,10 +262,6 @@ class LocationTestCase(TestCase): for location in locations: location.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': ['Location 1', 'Location 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -318,7 +303,7 @@ class LocationTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class RackRoleTestCase(TestCase): +class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RackRole.objects.all() filterset = RackRoleFilterSet @@ -332,10 +317,6 @@ class RackRoleTestCase(TestCase): ) RackRole.objects.bulk_create(rack_roles) - 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': ['Rack Role 1', 'Rack Role 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -349,7 +330,7 @@ class RackRoleTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class RackTestCase(TestCase): +class RackTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Rack.objects.all() filterset = RackFilterSet @@ -416,10 +397,6 @@ class RackTestCase(TestCase): ) Rack.objects.bulk_create(racks) - 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': ['Rack 1', 'Rack 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -523,7 +500,7 @@ class RackTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class RackReservationTestCase(TestCase): +class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RackReservation.objects.all() filterset = RackReservationFilterSet @@ -581,10 +558,6 @@ class RackReservationTestCase(TestCase): ) RackReservation.objects.bulk_create(reservations) - 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_site(self): sites = Site.objects.all()[:2] params = {'site_id': [sites[0].pk, sites[1].pk]} @@ -621,7 +594,7 @@ class RackReservationTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ManufacturerTestCase(TestCase): +class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Manufacturer.objects.all() filterset = ManufacturerFilterSet @@ -635,10 +608,6 @@ class ManufacturerTestCase(TestCase): ) Manufacturer.objects.bulk_create(manufacturers) - 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': ['Manufacturer 1', 'Manufacturer 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -652,7 +621,7 @@ class ManufacturerTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class DeviceTypeTestCase(TestCase): +class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DeviceType.objects.all() filterset = DeviceTypeFilterSet @@ -708,10 +677,6 @@ class DeviceTypeTestCase(TestCase): DeviceBayTemplate(device_type=device_types[1], name='Device Bay 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_model(self): params = {'model': ['Model 1', 'Model 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -788,7 +753,7 @@ class DeviceTypeTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class ConsolePortTemplateTestCase(TestCase): +class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConsolePortTemplate.objects.all() filterset = ConsolePortTemplateFilterSet @@ -810,10 +775,6 @@ class ConsolePortTemplateTestCase(TestCase): ConsolePortTemplate(device_type=device_types[2], name='Console Port 3'), )) - 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': ['Console Port 1', 'Console Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -824,7 +785,7 @@ class ConsolePortTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ConsoleServerPortTemplateTestCase(TestCase): +class ConsoleServerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConsoleServerPortTemplate.objects.all() filterset = ConsoleServerPortTemplateFilterSet @@ -846,10 +807,6 @@ class ConsoleServerPortTemplateTestCase(TestCase): ConsoleServerPortTemplate(device_type=device_types[2], name='Console Server Port 3'), )) - 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': ['Console Server Port 1', 'Console Server Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -860,7 +817,7 @@ class ConsoleServerPortTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class PowerPortTemplateTestCase(TestCase): +class PowerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerPortTemplate.objects.all() filterset = PowerPortTemplateFilterSet @@ -882,10 +839,6 @@ class PowerPortTemplateTestCase(TestCase): PowerPortTemplate(device_type=device_types[2], name='Power Port 3', maximum_draw=300, allocated_draw=150), )) - 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': ['Power Port 1', 'Power Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -904,7 +857,7 @@ class PowerPortTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class PowerOutletTemplateTestCase(TestCase): +class PowerOutletTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerOutletTemplate.objects.all() filterset = PowerOutletTemplateFilterSet @@ -926,10 +879,6 @@ class PowerOutletTemplateTestCase(TestCase): PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C), )) - 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': ['Power Outlet 1', 'Power Outlet 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -944,7 +893,7 @@ class PowerOutletTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class InterfaceTemplateTestCase(TestCase): +class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = InterfaceTemplate.objects.all() filterset = InterfaceTemplateFilterSet @@ -966,10 +915,6 @@ class InterfaceTemplateTestCase(TestCase): InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False), )) - 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': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -990,7 +935,7 @@ class InterfaceTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class FrontPortTemplateTestCase(TestCase): +class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FrontPortTemplate.objects.all() filterset = FrontPortTemplateFilterSet @@ -1019,10 +964,6 @@ class FrontPortTemplateTestCase(TestCase): FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC), )) - 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': ['Front Port 1', 'Front Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1037,7 +978,7 @@ class FrontPortTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class RearPortTemplateTestCase(TestCase): +class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RearPortTemplate.objects.all() filterset = RearPortTemplateFilterSet @@ -1059,10 +1000,6 @@ class RearPortTemplateTestCase(TestCase): RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3), )) - 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': ['Rear Port 1', 'Rear Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1081,7 +1018,7 @@ class RearPortTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class DeviceBayTemplateTestCase(TestCase): +class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DeviceBayTemplate.objects.all() filterset = DeviceBayTemplateFilterSet @@ -1103,10 +1040,6 @@ class DeviceBayTemplateTestCase(TestCase): DeviceBayTemplate(device_type=device_types[2], name='Device Bay 3'), )) - 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': ['Device Bay 1', 'Device Bay 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1117,7 +1050,7 @@ class DeviceBayTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class DeviceRoleTestCase(TestCase): +class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DeviceRole.objects.all() filterset = DeviceRoleFilterSet @@ -1131,10 +1064,6 @@ class DeviceRoleTestCase(TestCase): ) DeviceRole.objects.bulk_create(device_roles) - 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': ['Device Role 1', 'Device Role 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1154,7 +1083,7 @@ class DeviceRoleTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class PlatformTestCase(TestCase): +class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Platform.objects.all() filterset = PlatformFilterSet @@ -1175,10 +1104,6 @@ class PlatformTestCase(TestCase): ) Platform.objects.bulk_create(platforms) - 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': ['Platform 1', 'Platform 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1203,7 +1128,7 @@ class PlatformTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class DeviceTestCase(TestCase): +class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Device.objects.all() filterset = DeviceFilterSet @@ -1356,10 +1281,6 @@ class DeviceTestCase(TestCase): Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1) Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=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_name(self): params = {'name': ['Device 1', 'Device 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1549,7 +1470,7 @@ class DeviceTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ConsolePortTestCase(TestCase): +class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConsolePort.objects.all() filterset = ConsolePortFilterSet @@ -1608,10 +1529,6 @@ class ConsolePortTestCase(TestCase): Cable(termination_a=console_ports[1], termination_b=console_server_ports[1]).save() # Third port is not connected - 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': ['Console Port 1', 'Console Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1665,7 +1582,7 @@ class ConsolePortTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class ConsoleServerPortTestCase(TestCase): +class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConsoleServerPort.objects.all() filterset = ConsoleServerPortFilterSet @@ -1724,10 +1641,6 @@ class ConsoleServerPortTestCase(TestCase): Cable(termination_a=console_server_ports[1], termination_b=console_ports[1]).save() # Third port is not connected - 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': ['Console Server Port 1', 'Console Server Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1781,7 +1694,7 @@ class ConsoleServerPortTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class PowerPortTestCase(TestCase): +class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerPort.objects.all() filterset = PowerPortFilterSet @@ -1840,10 +1753,6 @@ class PowerPortTestCase(TestCase): Cable(termination_a=power_ports[1], termination_b=power_outlets[1]).save() # Third port is not connected - 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': ['Power Port 1', 'Power Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1905,7 +1814,7 @@ class PowerPortTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class PowerOutletTestCase(TestCase): +class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerOutlet.objects.all() filterset = PowerOutletFilterSet @@ -1964,10 +1873,6 @@ class PowerOutletTestCase(TestCase): Cable(termination_a=power_outlets[1], termination_b=power_ports[1]).save() # Third port is not connected - 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': ['Power Outlet 1', 'Power Outlet 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2025,7 +1930,7 @@ class PowerOutletTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class InterfaceTestCase(TestCase): +class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Interface.objects.all() filterset = InterfaceFilterSet @@ -2081,10 +1986,6 @@ class InterfaceTestCase(TestCase): Cable(termination_a=interfaces[1], termination_b=interfaces[4]).save() # Third pair is not connected - 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': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2200,7 +2101,7 @@ class InterfaceTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class FrontPortTestCase(TestCase): +class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FrontPort.objects.all() filterset = FrontPortFilterSet @@ -2266,10 +2167,6 @@ class FrontPortTestCase(TestCase): Cable(termination_a=front_ports[1], termination_b=front_ports[4]).save() # Third port is not connected - 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': ['Front Port 1', 'Front Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2321,7 +2218,7 @@ class FrontPortTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class RearPortTestCase(TestCase): +class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RearPort.objects.all() filterset = RearPortFilterSet @@ -2377,10 +2274,6 @@ class RearPortTestCase(TestCase): Cable(termination_a=rear_ports[1], termination_b=rear_ports[4]).save() # Third port is not connected - 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': ['Rear Port 1', 'Rear Port 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2436,7 +2329,7 @@ class RearPortTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class DeviceBayTestCase(TestCase): +class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = DeviceBay.objects.all() filterset = DeviceBayFilterSet @@ -2483,10 +2376,6 @@ class DeviceBayTestCase(TestCase): ) DeviceBay.objects.bulk_create(device_bays) - 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': ['Device Bay 1', 'Device Bay 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2528,7 +2417,7 @@ class DeviceBayTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class InventoryItemTestCase(TestCase): +class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = InventoryItem.objects.all() filterset = InventoryItemFilterSet @@ -2591,10 +2480,6 @@ class InventoryItemTestCase(TestCase): for i in child_inventory_items: i.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': ['Inventory Item 1', 'Inventory Item 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2666,7 +2551,7 @@ class InventoryItemTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class VirtualChassisTestCase(TestCase): +class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VirtualChassis.objects.all() filterset = VirtualChassisFilterSet @@ -2721,10 +2606,6 @@ class VirtualChassisTestCase(TestCase): Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=virtual_chassis[1]) Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[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_domain(self): params = {'domain': ['Domain 1', 'Domain 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2762,7 +2643,7 @@ class VirtualChassisTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class CableTestCase(TestCase): +class CableTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Cable.objects.all() filterset = CableFilterSet @@ -2827,10 +2708,6 @@ class CableTestCase(TestCase): Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).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_label(self): params = {'label': ['Cable 1', 'Cable 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2886,7 +2763,7 @@ class CableTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class PowerPanelTestCase(TestCase): +class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerPanel.objects.all() filterset = PowerPanelFilterSet @@ -2931,10 +2808,6 @@ class PowerPanelTestCase(TestCase): ) PowerPanel.objects.bulk_create(power_panels) - 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': ['Power Panel 1', 'Power Panel 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -2966,7 +2839,7 @@ class PowerPanelTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class PowerFeedTestCase(TestCase): +class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerFeed.objects.all() filterset = PowerFeedFilterSet @@ -3029,10 +2902,6 @@ class PowerFeedTestCase(TestCase): Cable(termination_a=power_feeds[0], termination_b=power_ports[0]).save() Cable(termination_a=power_feeds[1], termination_b=power_ports[1]).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': ['Power Feed 1', 'Power Feed 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 69e043425..734f9bd1a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -24,7 +24,7 @@ from utilities.tables import paginate_table from utilities.utils import csv_format, count_related from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin from virtualization.models import VirtualMachine -from . import filters, forms, tables +from . import filtersets, forms, tables from .choices import DeviceFaceChoices from .constants import NONCONNECTABLE_IFACE_TYPES from .models import ( @@ -107,7 +107,7 @@ class RegionListView(generic.ObjectListView): 'site_count', cumulative=True ) - filterset = filters.RegionFilterSet + filterset = filtersets.RegionFilterSet filterset_form = forms.RegionFilterForm table = tables.RegionTable @@ -163,7 +163,7 @@ class RegionBulkEditView(generic.BulkEditView): 'site_count', cumulative=True ) - filterset = filters.RegionFilterSet + filterset = filtersets.RegionFilterSet table = tables.RegionTable form = forms.RegionBulkEditForm @@ -176,7 +176,7 @@ class RegionBulkDeleteView(generic.BulkDeleteView): 'site_count', cumulative=True ) - filterset = filters.RegionFilterSet + filterset = filtersets.RegionFilterSet table = tables.RegionTable @@ -192,7 +192,7 @@ class SiteGroupListView(generic.ObjectListView): 'site_count', cumulative=True ) - filterset = filters.SiteGroupFilterSet + filterset = filtersets.SiteGroupFilterSet filterset_form = forms.SiteGroupFilterForm table = tables.SiteGroupTable @@ -248,7 +248,7 @@ class SiteGroupBulkEditView(generic.BulkEditView): 'site_count', cumulative=True ) - filterset = filters.SiteGroupFilterSet + filterset = filtersets.SiteGroupFilterSet table = tables.SiteGroupTable form = forms.SiteGroupBulkEditForm @@ -261,7 +261,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView): 'site_count', cumulative=True ) - filterset = filters.SiteGroupFilterSet + filterset = filtersets.SiteGroupFilterSet table = tables.SiteGroupTable @@ -271,7 +271,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView): class SiteListView(generic.ObjectListView): queryset = Site.objects.all() - filterset = filters.SiteFilterSet + filterset = filtersets.SiteFilterSet filterset_form = forms.SiteFilterForm table = tables.SiteTable @@ -326,14 +326,14 @@ class SiteBulkImportView(generic.BulkImportView): class SiteBulkEditView(generic.BulkEditView): queryset = Site.objects.prefetch_related('region', 'tenant') - filterset = filters.SiteFilterSet + filterset = filtersets.SiteFilterSet table = tables.SiteTable form = forms.SiteBulkEditForm class SiteBulkDeleteView(generic.BulkDeleteView): queryset = Site.objects.prefetch_related('region', 'tenant') - filterset = filters.SiteFilterSet + filterset = filtersets.SiteFilterSet table = tables.SiteTable @@ -355,7 +355,7 @@ class LocationListView(generic.ObjectListView): 'rack_count', cumulative=True ) - filterset = filters.LocationFilterSet + filterset = filtersets.LocationFilterSet filterset_form = forms.LocationFilterForm table = tables.LocationTable @@ -414,7 +414,7 @@ class LocationBulkEditView(generic.BulkEditView): 'rack_count', cumulative=True ).prefetch_related('site') - filterset = filters.LocationFilterSet + filterset = filtersets.LocationFilterSet table = tables.LocationTable form = forms.LocationBulkEditForm @@ -427,7 +427,7 @@ class LocationBulkDeleteView(generic.BulkDeleteView): 'rack_count', cumulative=True ).prefetch_related('site') - filterset = filters.LocationFilterSet + filterset = filtersets.LocationFilterSet table = tables.LocationTable @@ -478,7 +478,7 @@ class RackRoleBulkEditView(generic.BulkEditView): queryset = RackRole.objects.annotate( rack_count=count_related(Rack, 'role') ) - filterset = filters.RackRoleFilterSet + filterset = filtersets.RackRoleFilterSet table = tables.RackRoleTable form = forms.RackRoleBulkEditForm @@ -500,7 +500,7 @@ class RackListView(generic.ObjectListView): ).annotate( device_count=count_related(Device, 'rack') ) - filterset = filters.RackFilterSet + filterset = filtersets.RackFilterSet filterset_form = forms.RackFilterForm table = tables.RackDetailTable @@ -513,7 +513,7 @@ class RackElevationListView(generic.ObjectListView): def get(self, request): - racks = filters.RackFilterSet(request.GET, self.queryset).qs + racks = filtersets.RackFilterSet(request.GET, self.queryset).qs total_count = racks.count() # Determine ordering @@ -602,14 +602,14 @@ class RackBulkImportView(generic.BulkImportView): class RackBulkEditView(generic.BulkEditView): queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role') - filterset = filters.RackFilterSet + filterset = filtersets.RackFilterSet table = tables.RackTable form = forms.RackBulkEditForm class RackBulkDeleteView(generic.BulkDeleteView): queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role') - filterset = filters.RackFilterSet + filterset = filtersets.RackFilterSet table = tables.RackTable @@ -619,7 +619,7 @@ class RackBulkDeleteView(generic.BulkDeleteView): class RackReservationListView(generic.ObjectListView): queryset = RackReservation.objects.all() - filterset = filters.RackReservationFilterSet + filterset = filtersets.RackReservationFilterSet filterset_form = forms.RackReservationFilterForm table = tables.RackReservationTable @@ -662,14 +662,14 @@ class RackReservationImportView(generic.BulkImportView): class RackReservationBulkEditView(generic.BulkEditView): queryset = RackReservation.objects.prefetch_related('rack', 'user') - filterset = filters.RackReservationFilterSet + filterset = filtersets.RackReservationFilterSet table = tables.RackReservationTable form = forms.RackReservationBulkEditForm class RackReservationBulkDeleteView(generic.BulkDeleteView): queryset = RackReservation.objects.prefetch_related('rack', 'user') - filterset = filters.RackReservationFilterSet + filterset = filtersets.RackReservationFilterSet table = tables.RackReservationTable @@ -692,6 +692,8 @@ class ManufacturerView(generic.ObjectView): def get_extra_context(self, request, instance): devicetypes = DeviceType.objects.restrict(request.user, 'view').filter( manufacturer=instance + ).annotate( + instance_count=count_related(Device, 'device_type') ) devicetypes_table = tables.DeviceTypeTable(devicetypes) @@ -722,7 +724,7 @@ class ManufacturerBulkEditView(generic.BulkEditView): queryset = Manufacturer.objects.annotate( devicetype_count=count_related(DeviceType, 'manufacturer') ) - filterset = filters.ManufacturerFilterSet + filterset = filtersets.ManufacturerFilterSet table = tables.ManufacturerTable form = forms.ManufacturerBulkEditForm @@ -742,7 +744,7 @@ class DeviceTypeListView(generic.ObjectListView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( instance_count=count_related(Device, 'device_type') ) - filterset = filters.DeviceTypeFilterSet + filterset = filtersets.DeviceTypeFilterSet filterset_form = forms.DeviceTypeFilterForm table = tables.DeviceTypeTable @@ -848,7 +850,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( instance_count=count_related(Device, 'device_type') ) - filterset = filters.DeviceTypeFilterSet + filterset = filtersets.DeviceTypeFilterSet table = tables.DeviceTypeTable form = forms.DeviceTypeBulkEditForm @@ -857,7 +859,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView): queryset = DeviceType.objects.prefetch_related('manufacturer').annotate( instance_count=count_related(Device, 'device_type') ) - filterset = filters.DeviceTypeFilterSet + filterset = filtersets.DeviceTypeFilterSet table = tables.DeviceTypeTable @@ -1190,7 +1192,7 @@ class DeviceRoleBulkEditView(generic.BulkEditView): device_count=count_related(Device, 'device_role'), vm_count=count_related(VirtualMachine, 'role') ) - filterset = filters.DeviceRoleFilterSet + filterset = filtersets.DeviceRoleFilterSet table = tables.DeviceRoleTable form = forms.DeviceRoleBulkEditForm @@ -1249,7 +1251,7 @@ class PlatformBulkImportView(generic.BulkImportView): class PlatformBulkEditView(generic.BulkEditView): queryset = Platform.objects.all() - filterset = filters.PlatformFilterSet + filterset = filtersets.PlatformFilterSet table = tables.PlatformTable form = forms.PlatformBulkEditForm @@ -1265,7 +1267,7 @@ class PlatformBulkDeleteView(generic.BulkDeleteView): class DeviceListView(generic.ObjectListView): queryset = Device.objects.all() - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet filterset_form = forms.DeviceFilterForm table = tables.DeviceTable template_name = 'dcim/device_list.html' @@ -1600,14 +1602,14 @@ class ChildDeviceBulkImportView(generic.BulkImportView): class DeviceBulkEditView(generic.BulkEditView): queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable form = forms.DeviceBulkEditForm class DeviceBulkDeleteView(generic.BulkDeleteView): queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable @@ -1617,7 +1619,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView): class ConsolePortListView(generic.ObjectListView): queryset = ConsolePort.objects.all() - filterset = filters.ConsolePortFilterSet + filterset = filtersets.ConsolePortFilterSet filterset_form = forms.ConsolePortFilterForm table = tables.ConsolePortTable action_buttons = ('import', 'export') @@ -1652,7 +1654,7 @@ class ConsolePortBulkImportView(generic.BulkImportView): class ConsolePortBulkEditView(generic.BulkEditView): queryset = ConsolePort.objects.all() - filterset = filters.ConsolePortFilterSet + filterset = filtersets.ConsolePortFilterSet table = tables.ConsolePortTable form = forms.ConsolePortBulkEditForm @@ -1667,7 +1669,7 @@ class ConsolePortBulkDisconnectView(BulkDisconnectView): class ConsolePortBulkDeleteView(generic.BulkDeleteView): queryset = ConsolePort.objects.all() - filterset = filters.ConsolePortFilterSet + filterset = filtersets.ConsolePortFilterSet table = tables.ConsolePortTable @@ -1677,7 +1679,7 @@ class ConsolePortBulkDeleteView(generic.BulkDeleteView): class ConsoleServerPortListView(generic.ObjectListView): queryset = ConsoleServerPort.objects.all() - filterset = filters.ConsoleServerPortFilterSet + filterset = filtersets.ConsoleServerPortFilterSet filterset_form = forms.ConsoleServerPortFilterForm table = tables.ConsoleServerPortTable action_buttons = ('import', 'export') @@ -1712,7 +1714,7 @@ class ConsoleServerPortBulkImportView(generic.BulkImportView): class ConsoleServerPortBulkEditView(generic.BulkEditView): queryset = ConsoleServerPort.objects.all() - filterset = filters.ConsoleServerPortFilterSet + filterset = filtersets.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable form = forms.ConsoleServerPortBulkEditForm @@ -1727,7 +1729,7 @@ class ConsoleServerPortBulkDisconnectView(BulkDisconnectView): class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView): queryset = ConsoleServerPort.objects.all() - filterset = filters.ConsoleServerPortFilterSet + filterset = filtersets.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable @@ -1737,7 +1739,7 @@ class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView): class PowerPortListView(generic.ObjectListView): queryset = PowerPort.objects.all() - filterset = filters.PowerPortFilterSet + filterset = filtersets.PowerPortFilterSet filterset_form = forms.PowerPortFilterForm table = tables.PowerPortTable action_buttons = ('import', 'export') @@ -1772,7 +1774,7 @@ class PowerPortBulkImportView(generic.BulkImportView): class PowerPortBulkEditView(generic.BulkEditView): queryset = PowerPort.objects.all() - filterset = filters.PowerPortFilterSet + filterset = filtersets.PowerPortFilterSet table = tables.PowerPortTable form = forms.PowerPortBulkEditForm @@ -1787,7 +1789,7 @@ class PowerPortBulkDisconnectView(BulkDisconnectView): class PowerPortBulkDeleteView(generic.BulkDeleteView): queryset = PowerPort.objects.all() - filterset = filters.PowerPortFilterSet + filterset = filtersets.PowerPortFilterSet table = tables.PowerPortTable @@ -1797,7 +1799,7 @@ class PowerPortBulkDeleteView(generic.BulkDeleteView): class PowerOutletListView(generic.ObjectListView): queryset = PowerOutlet.objects.all() - filterset = filters.PowerOutletFilterSet + filterset = filtersets.PowerOutletFilterSet filterset_form = forms.PowerOutletFilterForm table = tables.PowerOutletTable action_buttons = ('import', 'export') @@ -1832,7 +1834,7 @@ class PowerOutletBulkImportView(generic.BulkImportView): class PowerOutletBulkEditView(generic.BulkEditView): queryset = PowerOutlet.objects.all() - filterset = filters.PowerOutletFilterSet + filterset = filtersets.PowerOutletFilterSet table = tables.PowerOutletTable form = forms.PowerOutletBulkEditForm @@ -1847,7 +1849,7 @@ class PowerOutletBulkDisconnectView(BulkDisconnectView): class PowerOutletBulkDeleteView(generic.BulkDeleteView): queryset = PowerOutlet.objects.all() - filterset = filters.PowerOutletFilterSet + filterset = filtersets.PowerOutletFilterSet table = tables.PowerOutletTable @@ -1857,7 +1859,7 @@ class PowerOutletBulkDeleteView(generic.BulkDeleteView): class InterfaceListView(generic.ObjectListView): queryset = Interface.objects.all() - filterset = filters.InterfaceFilterSet + filterset = filtersets.InterfaceFilterSet filterset_form = forms.InterfaceFilterForm table = tables.InterfaceTable action_buttons = ('import', 'export') @@ -1927,7 +1929,7 @@ class InterfaceBulkImportView(generic.BulkImportView): class InterfaceBulkEditView(generic.BulkEditView): queryset = Interface.objects.all() - filterset = filters.InterfaceFilterSet + filterset = filtersets.InterfaceFilterSet table = tables.InterfaceTable form = forms.InterfaceBulkEditForm @@ -1942,7 +1944,7 @@ class InterfaceBulkDisconnectView(BulkDisconnectView): class InterfaceBulkDeleteView(generic.BulkDeleteView): queryset = Interface.objects.all() - filterset = filters.InterfaceFilterSet + filterset = filtersets.InterfaceFilterSet table = tables.InterfaceTable @@ -1952,7 +1954,7 @@ class InterfaceBulkDeleteView(generic.BulkDeleteView): class FrontPortListView(generic.ObjectListView): queryset = FrontPort.objects.all() - filterset = filters.FrontPortFilterSet + filterset = filtersets.FrontPortFilterSet filterset_form = forms.FrontPortFilterForm table = tables.FrontPortTable action_buttons = ('import', 'export') @@ -1987,7 +1989,7 @@ class FrontPortBulkImportView(generic.BulkImportView): class FrontPortBulkEditView(generic.BulkEditView): queryset = FrontPort.objects.all() - filterset = filters.FrontPortFilterSet + filterset = filtersets.FrontPortFilterSet table = tables.FrontPortTable form = forms.FrontPortBulkEditForm @@ -2002,7 +2004,7 @@ class FrontPortBulkDisconnectView(BulkDisconnectView): class FrontPortBulkDeleteView(generic.BulkDeleteView): queryset = FrontPort.objects.all() - filterset = filters.FrontPortFilterSet + filterset = filtersets.FrontPortFilterSet table = tables.FrontPortTable @@ -2012,7 +2014,7 @@ class FrontPortBulkDeleteView(generic.BulkDeleteView): class RearPortListView(generic.ObjectListView): queryset = RearPort.objects.all() - filterset = filters.RearPortFilterSet + filterset = filtersets.RearPortFilterSet filterset_form = forms.RearPortFilterForm table = tables.RearPortTable action_buttons = ('import', 'export') @@ -2047,7 +2049,7 @@ class RearPortBulkImportView(generic.BulkImportView): class RearPortBulkEditView(generic.BulkEditView): queryset = RearPort.objects.all() - filterset = filters.RearPortFilterSet + filterset = filtersets.RearPortFilterSet table = tables.RearPortTable form = forms.RearPortBulkEditForm @@ -2062,7 +2064,7 @@ class RearPortBulkDisconnectView(BulkDisconnectView): class RearPortBulkDeleteView(generic.BulkDeleteView): queryset = RearPort.objects.all() - filterset = filters.RearPortFilterSet + filterset = filtersets.RearPortFilterSet table = tables.RearPortTable @@ -2072,7 +2074,7 @@ class RearPortBulkDeleteView(generic.BulkDeleteView): class DeviceBayListView(generic.ObjectListView): queryset = DeviceBay.objects.all() - filterset = filters.DeviceBayFilterSet + filterset = filtersets.DeviceBayFilterSet filterset_form = forms.DeviceBayFilterForm table = tables.DeviceBayTable action_buttons = ('import', 'export') @@ -2172,7 +2174,7 @@ class DeviceBayBulkImportView(generic.BulkImportView): class DeviceBayBulkEditView(generic.BulkEditView): queryset = DeviceBay.objects.all() - filterset = filters.DeviceBayFilterSet + filterset = filtersets.DeviceBayFilterSet table = tables.DeviceBayTable form = forms.DeviceBayBulkEditForm @@ -2183,7 +2185,7 @@ class DeviceBayBulkRenameView(generic.BulkRenameView): class DeviceBayBulkDeleteView(generic.BulkDeleteView): queryset = DeviceBay.objects.all() - filterset = filters.DeviceBayFilterSet + filterset = filtersets.DeviceBayFilterSet table = tables.DeviceBayTable @@ -2193,7 +2195,7 @@ class DeviceBayBulkDeleteView(generic.BulkDeleteView): class InventoryItemListView(generic.ObjectListView): queryset = InventoryItem.objects.all() - filterset = filters.InventoryItemFilterSet + filterset = filtersets.InventoryItemFilterSet filterset_form = forms.InventoryItemFilterForm table = tables.InventoryItemTable action_buttons = ('import', 'export') @@ -2227,7 +2229,7 @@ class InventoryItemBulkImportView(generic.BulkImportView): class InventoryItemBulkEditView(generic.BulkEditView): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') - filterset = filters.InventoryItemFilterSet + filterset = filtersets.InventoryItemFilterSet table = tables.InventoryItemTable form = forms.InventoryItemBulkEditForm @@ -2252,7 +2254,7 @@ class DeviceBulkAddConsolePortView(generic.BulkComponentCreateView): form = forms.ConsolePortBulkCreateForm queryset = ConsolePort.objects.all() model_form = forms.ConsolePortForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2263,7 +2265,7 @@ class DeviceBulkAddConsoleServerPortView(generic.BulkComponentCreateView): form = forms.ConsoleServerPortBulkCreateForm queryset = ConsoleServerPort.objects.all() model_form = forms.ConsoleServerPortForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2274,7 +2276,7 @@ class DeviceBulkAddPowerPortView(generic.BulkComponentCreateView): form = forms.PowerPortBulkCreateForm queryset = PowerPort.objects.all() model_form = forms.PowerPortForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2285,7 +2287,7 @@ class DeviceBulkAddPowerOutletView(generic.BulkComponentCreateView): form = forms.PowerOutletBulkCreateForm queryset = PowerOutlet.objects.all() model_form = forms.PowerOutletForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2296,7 +2298,7 @@ class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView): form = forms.InterfaceBulkCreateForm queryset = Interface.objects.all() model_form = forms.InterfaceForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2307,7 +2309,7 @@ class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView): # form = forms.FrontPortBulkCreateForm # queryset = FrontPort.objects.all() # model_form = forms.FrontPortForm -# filterset = filters.DeviceFilterSet +# filterset = filtersets.DeviceFilterSet # table = tables.DeviceTable # default_return_url = 'dcim:device_list' @@ -2318,7 +2320,7 @@ class DeviceBulkAddRearPortView(generic.BulkComponentCreateView): form = forms.RearPortBulkCreateForm queryset = RearPort.objects.all() model_form = forms.RearPortForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2329,7 +2331,7 @@ class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): form = forms.DeviceBayBulkCreateForm queryset = DeviceBay.objects.all() model_form = forms.DeviceBayForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2340,7 +2342,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView): form = forms.InventoryItemBulkCreateForm queryset = InventoryItem.objects.all() model_form = forms.InventoryItemForm - filterset = filters.DeviceFilterSet + filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -2351,7 +2353,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView): class CableListView(generic.ObjectListView): queryset = Cable.objects.all() - filterset = filters.CableFilterSet + filterset = filtersets.CableFilterSet filterset_form = forms.CableFilterForm table = tables.CableTable action_buttons = ('import', 'export') @@ -2484,14 +2486,14 @@ class CableBulkImportView(generic.BulkImportView): class CableBulkEditView(generic.BulkEditView): queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') - filterset = filters.CableFilterSet + filterset = filtersets.CableFilterSet table = tables.CableTable form = forms.CableBulkEditForm class CableBulkDeleteView(generic.BulkDeleteView): queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') - filterset = filters.CableFilterSet + filterset = filtersets.CableFilterSet table = tables.CableTable @@ -2501,7 +2503,7 @@ class CableBulkDeleteView(generic.BulkDeleteView): class ConsoleConnectionsListView(generic.ObjectListView): queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device') - filterset = filters.ConsoleConnectionFilterSet + filterset = filtersets.ConsoleConnectionFilterSet filterset_form = forms.ConsoleConnectionFilterForm table = tables.ConsoleConnectionTable template_name = 'dcim/connections_list.html' @@ -2531,7 +2533,7 @@ class ConsoleConnectionsListView(generic.ObjectListView): class PowerConnectionsListView(generic.ObjectListView): queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device') - filterset = filters.PowerConnectionFilterSet + filterset = filtersets.PowerConnectionFilterSet filterset_form = forms.PowerConnectionFilterForm table = tables.PowerConnectionTable template_name = 'dcim/connections_list.html' @@ -2565,7 +2567,7 @@ class InterfaceConnectionsListView(generic.ObjectListView): _path__isnull=False, pk__lt=F('_path__destination_id') ).order_by('device') - filterset = filters.InterfaceConnectionFilterSet + filterset = filtersets.InterfaceConnectionFilterSet filterset_form = forms.InterfaceConnectionFilterForm table = tables.InterfaceConnectionTable template_name = 'dcim/connections_list.html' @@ -2604,7 +2606,7 @@ class VirtualChassisListView(generic.ObjectListView): member_count=count_related(Device, 'virtual_chassis') ) table = tables.VirtualChassisTable - filterset = filters.VirtualChassisFilterSet + filterset = filtersets.VirtualChassisFilterSet filterset_form = forms.VirtualChassisFilterForm @@ -2812,14 +2814,14 @@ class VirtualChassisBulkImportView(generic.BulkImportView): class VirtualChassisBulkEditView(generic.BulkEditView): queryset = VirtualChassis.objects.all() - filterset = filters.VirtualChassisFilterSet + filterset = filtersets.VirtualChassisFilterSet table = tables.VirtualChassisTable form = forms.VirtualChassisBulkEditForm class VirtualChassisBulkDeleteView(generic.BulkDeleteView): queryset = VirtualChassis.objects.all() - filterset = filters.VirtualChassisFilterSet + filterset = filtersets.VirtualChassisFilterSet table = tables.VirtualChassisTable @@ -2833,7 +2835,7 @@ class PowerPanelListView(generic.ObjectListView): ).annotate( powerfeed_count=count_related(PowerFeed, 'power_panel') ) - filterset = filters.PowerPanelFilterSet + filterset = filtersets.PowerPanelFilterSet filterset_form = forms.PowerPanelFilterForm table = tables.PowerPanelTable @@ -2873,7 +2875,7 @@ class PowerPanelBulkImportView(generic.BulkImportView): class PowerPanelBulkEditView(generic.BulkEditView): queryset = PowerPanel.objects.prefetch_related('site', 'location') - filterset = filters.PowerPanelFilterSet + filterset = filtersets.PowerPanelFilterSet table = tables.PowerPanelTable form = forms.PowerPanelBulkEditForm @@ -2884,7 +2886,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView): ).annotate( powerfeed_count=count_related(PowerFeed, 'power_panel') ) - filterset = filters.PowerPanelFilterSet + filterset = filtersets.PowerPanelFilterSet table = tables.PowerPanelTable @@ -2894,7 +2896,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView): class PowerFeedListView(generic.ObjectListView): queryset = PowerFeed.objects.all() - filterset = filters.PowerFeedFilterSet + filterset = filtersets.PowerFeedFilterSet filterset_form = forms.PowerFeedFilterForm table = tables.PowerFeedTable @@ -2920,7 +2922,7 @@ class PowerFeedBulkImportView(generic.BulkImportView): class PowerFeedBulkEditView(generic.BulkEditView): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') - filterset = filters.PowerFeedFilterSet + filterset = filtersets.PowerFeedFilterSet table = tables.PowerFeedTable form = forms.PowerFeedBulkEditForm @@ -2931,5 +2933,5 @@ class PowerFeedBulkDisconnectView(BulkDisconnectView): class PowerFeedBulkDeleteView(generic.BulkDeleteView): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') - filterset = filters.PowerFeedFilterSet + filterset = filtersets.PowerFeedFilterSet table = tables.PowerFeedTable diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index cee5146a6..7e6c97782 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -9,7 +9,7 @@ from rest_framework.routers import APIRootView from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from rq import Worker -from extras import filters +from extras import filtersets from extras.choices import JobResultStatusChoices from extras.models import * from extras.models import CustomField @@ -61,7 +61,7 @@ class WebhookViewSet(ModelViewSet): metadata_class = ContentTypeMetadata queryset = Webhook.objects.all() serializer_class = serializers.WebhookSerializer - filterset_class = filters.WebhookFilterSet + filterset_class = filtersets.WebhookFilterSet # @@ -72,7 +72,7 @@ class CustomFieldViewSet(ModelViewSet): metadata_class = ContentTypeMetadata queryset = CustomField.objects.all() serializer_class = serializers.CustomFieldSerializer - filterset_class = filters.CustomFieldFilterSet + filterset_class = filtersets.CustomFieldFilterSet class CustomFieldModelViewSet(ModelViewSet): @@ -101,7 +101,7 @@ class CustomLinkViewSet(ModelViewSet): metadata_class = ContentTypeMetadata queryset = CustomLink.objects.all() serializer_class = serializers.CustomLinkSerializer - filterset_class = filters.CustomLinkFilterSet + filterset_class = filtersets.CustomLinkFilterSet # @@ -112,7 +112,7 @@ class ExportTemplateViewSet(ModelViewSet): metadata_class = ContentTypeMetadata queryset = ExportTemplate.objects.all() serializer_class = serializers.ExportTemplateSerializer - filterset_class = filters.ExportTemplateFilterSet + filterset_class = filtersets.ExportTemplateFilterSet # @@ -124,7 +124,7 @@ class TagViewSet(ModelViewSet): tagged_items=count_related(TaggedItem, 'tag') ) serializer_class = serializers.TagSerializer - filterset_class = filters.TagFilterSet + filterset_class = filtersets.TagFilterSet # @@ -135,7 +135,7 @@ class ImageAttachmentViewSet(ModelViewSet): metadata_class = ContentTypeMetadata queryset = ImageAttachment.objects.all() serializer_class = serializers.ImageAttachmentSerializer - filterset_class = filters.ImageAttachmentFilterSet + filterset_class = filtersets.ImageAttachmentFilterSet # @@ -146,7 +146,7 @@ class JournalEntryViewSet(ModelViewSet): metadata_class = ContentTypeMetadata queryset = JournalEntry.objects.all() serializer_class = serializers.JournalEntrySerializer - filterset_class = filters.JournalEntryFilterSet + filterset_class = filtersets.JournalEntryFilterSet # @@ -158,7 +158,7 @@ class ConfigContextViewSet(ModelViewSet): 'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants', ) serializer_class = serializers.ConfigContextSerializer - filterset_class = filters.ConfigContextFilterSet + filterset_class = filtersets.ConfigContextFilterSet # @@ -358,7 +358,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet): metadata_class = ContentTypeMetadata queryset = ObjectChange.objects.prefetch_related('user') serializer_class = serializers.ObjectChangeSerializer - filterset_class = filters.ObjectChangeFilterSet + filterset_class = filtersets.ObjectChangeFilterSet # @@ -371,7 +371,7 @@ class JobResultViewSet(ReadOnlyModelViewSet): """ queryset = JobResult.objects.prefetch_related('user') serializer_class = serializers.JobResultSerializer - filterset_class = filters.JobResultFilterSet + filterset_class = filtersets.JobResultFilterSet # @@ -384,4 +384,4 @@ class ContentTypeViewSet(ReadOnlyModelViewSet): """ queryset = ContentType.objects.order_by('app_label', 'model') serializer_class = serializers.ContentTypeSerializer - filterset_class = filters.ContentTypeFilterSet + filterset_class = filtersets.ContentTypeFilterSet diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index aacdbda6b..aef2046fd 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -1,31 +1,12 @@ import django_filters -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.db.models import Q from django.forms import DateField, IntegerField, NullBooleanField -from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup -from tenancy.models import Tenant, TenantGroup -from utilities.filters import BaseFilterSet, ContentTypeFilter -from virtualization.models import Cluster, ClusterGroup +from .models import Tag from .choices import * -from .models import * - __all__ = ( - 'ConfigContextFilterSet', - 'ContentTypeFilterSet', - 'CreatedUpdatedFilterSet', 'CustomFieldFilter', - 'CustomLinkFilterSet', - 'CustomFieldModelFilterSet', - 'ExportTemplateFilterSet', - 'ImageAttachmentFilterSet', - 'JournalEntryFilterSet', - 'LocalConfigContextFilterSet', - 'ObjectChangeFilterSet', - 'TagFilterSet', - 'WebhookFilterSet', + 'TagFilter', ) EXACT_FILTER_TYPES = ( @@ -36,41 +17,6 @@ EXACT_FILTER_TYPES = ( ) -class CreatedUpdatedFilterSet(django_filters.FilterSet): - created = django_filters.DateFilter() - created__gte = django_filters.DateFilter( - field_name='created', - lookup_expr='gte' - ) - created__lte = django_filters.DateFilter( - field_name='created', - lookup_expr='lte' - ) - last_updated = django_filters.DateTimeFilter() - last_updated__gte = django_filters.DateTimeFilter( - field_name='last_updated', - lookup_expr='gte' - ) - last_updated__lte = django_filters.DateTimeFilter( - field_name='last_updated', - lookup_expr='lte' - ) - - -class WebhookFilterSet(BaseFilterSet): - content_types = ContentTypeFilter() - http_method = django_filters.MultipleChoiceFilter( - choices=WebhookHttpMethodChoices - ) - - class Meta: - model = Webhook - fields = [ - 'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', - 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', - ] - - class CustomFieldFilter(django_filters.Filter): """ Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name. @@ -94,310 +40,16 @@ class CustomFieldFilter(django_filters.Filter): self.lookup_expr = 'icontains' -class CustomFieldModelFilterSet(django_filters.FilterSet): +class TagFilter(django_filters.ModelMultipleChoiceFilter): """ - Dynamically add a Filter for each CustomField applicable to the parent model. + 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) - - custom_fields = CustomField.objects.filter( - content_types=ContentType.objects.get_for_model(self._meta.model) - ).exclude( - filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED - ) - for cf in custom_fields: - self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf) - - -class CustomFieldFilterSet(django_filters.FilterSet): - content_types = ContentTypeFilter() - - class Meta: - model = CustomField - fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight'] - - -class CustomLinkFilterSet(BaseFilterSet): - - class Meta: - model = CustomLink - fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window'] - - -class ExportTemplateFilterSet(BaseFilterSet): - - class Meta: - model = ExportTemplate - fields = ['id', 'content_type', 'name'] - - -class ImageAttachmentFilterSet(BaseFilterSet): - content_type = ContentTypeFilter() - - class Meta: - model = ImageAttachment - fields = ['id', 'content_type_id', 'object_id', 'name'] - - -class JournalEntryFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): - q = django_filters.CharFilter( - method='search', - label='Search', - ) - created = django_filters.DateTimeFromToRangeFilter() - assigned_object_type = ContentTypeFilter() - created_by_id = django_filters.ModelMultipleChoiceFilter( - queryset=User.objects.all(), - label='User (ID)', - ) - created_by = django_filters.ModelMultipleChoiceFilter( - field_name='created_by__username', - queryset=User.objects.all(), - to_field_name='username', - label='User (name)', - ) - kind = django_filters.MultipleChoiceFilter( - choices=JournalEntryKindChoices - ) - - class Meta: - model = JournalEntry - fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind'] - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter(comments__icontains=value) - - -class TagFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): - q = django_filters.CharFilter( - method='search', - label='Search', - ) - - class Meta: - model = Tag - fields = ['id', 'name', 'slug', 'color'] - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) - ) - - -class ConfigContextFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): - q = django_filters.CharFilter( - method='search', - label='Search', - ) - region_id = django_filters.ModelMultipleChoiceFilter( - field_name='regions', - queryset=Region.objects.all(), - label='Region', - ) - region = django_filters.ModelMultipleChoiceFilter( - field_name='regions__slug', - queryset=Region.objects.all(), - to_field_name='slug', - label='Region (slug)', - ) - site_group = django_filters.ModelMultipleChoiceFilter( - field_name='site_groups__slug', - queryset=SiteGroup.objects.all(), - to_field_name='slug', - label='Site group (slug)', - ) - site_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='site_groups', - queryset=SiteGroup.objects.all(), - label='Site group', - ) - site_id = django_filters.ModelMultipleChoiceFilter( - field_name='sites', - queryset=Site.objects.all(), - label='Site', - ) - site = django_filters.ModelMultipleChoiceFilter( - field_name='sites__slug', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site (slug)', - ) - device_type_id = django_filters.ModelMultipleChoiceFilter( - field_name='device_types', - queryset=DeviceType.objects.all(), - label='Device type', - ) - role_id = django_filters.ModelMultipleChoiceFilter( - field_name='roles', - queryset=DeviceRole.objects.all(), - label='Role', - ) - role = django_filters.ModelMultipleChoiceFilter( - field_name='roles__slug', - queryset=DeviceRole.objects.all(), - to_field_name='slug', - label='Role (slug)', - ) - platform_id = django_filters.ModelMultipleChoiceFilter( - field_name='platforms', - queryset=Platform.objects.all(), - label='Platform', - ) - platform = django_filters.ModelMultipleChoiceFilter( - field_name='platforms__slug', - queryset=Platform.objects.all(), - to_field_name='slug', - label='Platform (slug)', - ) - cluster_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='cluster_groups', - queryset=ClusterGroup.objects.all(), - label='Cluster group', - ) - cluster_group = django_filters.ModelMultipleChoiceFilter( - field_name='cluster_groups__slug', - queryset=ClusterGroup.objects.all(), - to_field_name='slug', - label='Cluster group (slug)', - ) - cluster_id = django_filters.ModelMultipleChoiceFilter( - field_name='clusters', - queryset=Cluster.objects.all(), - label='Cluster', - ) - tenant_group_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenant_groups', - queryset=TenantGroup.objects.all(), - label='Tenant group', - ) - tenant_group = django_filters.ModelMultipleChoiceFilter( - field_name='tenant_groups__slug', - queryset=TenantGroup.objects.all(), - to_field_name='slug', - label='Tenant group (slug)', - ) - tenant_id = django_filters.ModelMultipleChoiceFilter( - field_name='tenants', - queryset=Tenant.objects.all(), - label='Tenant', - ) - tenant = django_filters.ModelMultipleChoiceFilter( - field_name='tenants__slug', - queryset=Tenant.objects.all(), - to_field_name='slug', - label='Tenant (slug)', - ) - tag = django_filters.ModelMultipleChoiceFilter( - field_name='tags__slug', - queryset=Tag.objects.all(), - to_field_name='slug', - label='Tag (slug)', - ) - - class Meta: - model = ConfigContext - fields = ['id', 'name', 'is_active'] - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(description__icontains=value) | - Q(data__icontains=value) - ) - - -# -# Filter for Local Config Context Data -# - -class LocalConfigContextFilterSet(django_filters.FilterSet): - local_context_data = django_filters.BooleanFilter( - method='_local_context_data', - label='Has local config context data', - ) - - def _local_context_data(self, queryset, name, value): - return queryset.exclude(local_context_data__isnull=value) - - -class ObjectChangeFilterSet(BaseFilterSet): - q = django_filters.CharFilter( - method='search', - label='Search', - ) - time = django_filters.DateTimeFromToRangeFilter() - changed_object_type = ContentTypeFilter() - user_id = django_filters.ModelMultipleChoiceFilter( - queryset=User.objects.all(), - label='User (ID)', - ) - user = django_filters.ModelMultipleChoiceFilter( - field_name='user__username', - queryset=User.objects.all(), - to_field_name='username', - label='User name', - ) - - class Meta: - model = ObjectChange - fields = [ - 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id', - 'object_repr', - ] - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(user_name__icontains=value) | - Q(object_repr__icontains=value) - ) - - -# -# Job Results -# - -class JobResultFilterSet(BaseFilterSet): - q = django_filters.CharFilter( - method='search', - label='Search', - ) - created = django_filters.DateTimeFilter() - completed = django_filters.DateTimeFilter() - status = django_filters.MultipleChoiceFilter( - choices=JobResultStatusChoices, - null_value=None - ) - - class Meta: - model = JobResult - fields = [ - 'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name' - ] - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(user__username__icontains=value) - ) - - -# -# ContentTypes -# - -class ContentTypeFilterSet(django_filters.FilterSet): - - class Meta: - model = ContentType - fields = ['id', 'app_label', 'model'] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py new file mode 100644 index 000000000..92c0dc9a6 --- /dev/null +++ b/netbox/extras/filtersets.py @@ -0,0 +1,341 @@ +import django_filters +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +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 virtualization.models import Cluster, ClusterGroup +from .choices import * +from .models import * + + +__all__ = ( + 'ConfigContextFilterSet', + 'ContentTypeFilterSet', + 'CustomLinkFilterSet', + 'ExportTemplateFilterSet', + 'ImageAttachmentFilterSet', + 'JournalEntryFilterSet', + 'LocalConfigContextFilterSet', + 'ObjectChangeFilterSet', + 'TagFilterSet', + 'WebhookFilterSet', +) + +EXACT_FILTER_TYPES = ( + CustomFieldTypeChoices.TYPE_BOOLEAN, + CustomFieldTypeChoices.TYPE_DATE, + CustomFieldTypeChoices.TYPE_INTEGER, + CustomFieldTypeChoices.TYPE_SELECT, +) + + +class WebhookFilterSet(BaseFilterSet): + content_types = ContentTypeFilter() + http_method = django_filters.MultipleChoiceFilter( + choices=WebhookHttpMethodChoices + ) + + class Meta: + model = Webhook + fields = [ + 'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', + 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', + ] + + +class CustomFieldFilterSet(django_filters.FilterSet): + content_types = ContentTypeFilter() + + class Meta: + model = CustomField + fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight'] + + +class CustomLinkFilterSet(BaseFilterSet): + + class Meta: + model = CustomLink + fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window'] + + +class ExportTemplateFilterSet(BaseFilterSet): + + class Meta: + model = ExportTemplate + fields = ['id', 'content_type', 'name'] + + +class ImageAttachmentFilterSet(BaseFilterSet): + created = django_filters.DateTimeFilter() + content_type = ContentTypeFilter() + + class Meta: + model = ImageAttachment + fields = ['id', 'content_type_id', 'object_id', 'name'] + + +class JournalEntryFilterSet(ChangeLoggedModelFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + created = django_filters.DateTimeFromToRangeFilter() + assigned_object_type = ContentTypeFilter() + created_by_id = django_filters.ModelMultipleChoiceFilter( + queryset=User.objects.all(), + label='User (ID)', + ) + created_by = django_filters.ModelMultipleChoiceFilter( + field_name='created_by__username', + queryset=User.objects.all(), + to_field_name='username', + label='User (name)', + ) + kind = django_filters.MultipleChoiceFilter( + choices=JournalEntryKindChoices + ) + + class Meta: + model = JournalEntry + fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(comments__icontains=value) + + +class TagFilterSet(ChangeLoggedModelFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + + class Meta: + model = Tag + fields = ['id', 'name', 'slug', 'color'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(slug__icontains=value) + ) + + +class ConfigContextFilterSet(ChangeLoggedModelFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + region_id = django_filters.ModelMultipleChoiceFilter( + field_name='regions', + queryset=Region.objects.all(), + label='Region', + ) + region = django_filters.ModelMultipleChoiceFilter( + field_name='regions__slug', + queryset=Region.objects.all(), + to_field_name='slug', + label='Region (slug)', + ) + site_group = django_filters.ModelMultipleChoiceFilter( + field_name='site_groups__slug', + queryset=SiteGroup.objects.all(), + to_field_name='slug', + label='Site group (slug)', + ) + site_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='site_groups', + queryset=SiteGroup.objects.all(), + label='Site group', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + field_name='sites', + queryset=Site.objects.all(), + label='Site', + ) + site = django_filters.ModelMultipleChoiceFilter( + field_name='sites__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site (slug)', + ) + device_type_id = django_filters.ModelMultipleChoiceFilter( + field_name='device_types', + queryset=DeviceType.objects.all(), + label='Device type', + ) + role_id = django_filters.ModelMultipleChoiceFilter( + field_name='roles', + queryset=DeviceRole.objects.all(), + label='Role', + ) + role = django_filters.ModelMultipleChoiceFilter( + field_name='roles__slug', + queryset=DeviceRole.objects.all(), + to_field_name='slug', + label='Role (slug)', + ) + platform_id = django_filters.ModelMultipleChoiceFilter( + field_name='platforms', + queryset=Platform.objects.all(), + label='Platform', + ) + platform = django_filters.ModelMultipleChoiceFilter( + field_name='platforms__slug', + queryset=Platform.objects.all(), + to_field_name='slug', + label='Platform (slug)', + ) + cluster_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_groups', + queryset=ClusterGroup.objects.all(), + label='Cluster group', + ) + cluster_group = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_groups__slug', + queryset=ClusterGroup.objects.all(), + to_field_name='slug', + label='Cluster group (slug)', + ) + cluster_id = django_filters.ModelMultipleChoiceFilter( + field_name='clusters', + queryset=Cluster.objects.all(), + label='Cluster', + ) + tenant_group_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenant_groups', + queryset=TenantGroup.objects.all(), + label='Tenant group', + ) + tenant_group = django_filters.ModelMultipleChoiceFilter( + field_name='tenant_groups__slug', + queryset=TenantGroup.objects.all(), + to_field_name='slug', + label='Tenant group (slug)', + ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + field_name='tenants', + queryset=Tenant.objects.all(), + label='Tenant', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + field_name='tenants__slug', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) + tag = django_filters.ModelMultipleChoiceFilter( + field_name='tags__slug', + queryset=Tag.objects.all(), + to_field_name='slug', + label='Tag (slug)', + ) + + class Meta: + model = ConfigContext + fields = ['id', 'name', 'is_active'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(data__icontains=value) + ) + + +# +# Filter for Local Config Context Data +# + +class LocalConfigContextFilterSet(django_filters.FilterSet): + local_context_data = django_filters.BooleanFilter( + method='_local_context_data', + label='Has local config context data', + ) + + def _local_context_data(self, queryset, name, value): + return queryset.exclude(local_context_data__isnull=value) + + +class ObjectChangeFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + time = django_filters.DateTimeFromToRangeFilter() + changed_object_type = ContentTypeFilter() + user_id = django_filters.ModelMultipleChoiceFilter( + queryset=User.objects.all(), + label='User (ID)', + ) + user = django_filters.ModelMultipleChoiceFilter( + field_name='user__username', + queryset=User.objects.all(), + to_field_name='username', + label='User name', + ) + + class Meta: + model = ObjectChange + fields = [ + 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id', + 'object_repr', + ] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(user_name__icontains=value) | + Q(object_repr__icontains=value) + ) + + +# +# Job Results +# + +class JobResultFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + created = django_filters.DateTimeFilter() + completed = django_filters.DateTimeFilter() + status = django_filters.MultipleChoiceFilter( + choices=JobResultStatusChoices, + null_value=None + ) + + class Meta: + model = JobResult + fields = [ + 'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name' + ] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(user__username__icontains=value) + ) + + +# +# ContentTypes +# + +class ContentTypeFilterSet(django_filters.FilterSet): + + class Meta: + model = ContentType + fields = ['id', 'app_label', 'model'] diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index d1725ac9d..c14424ba6 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError from django.urls import reverse from rest_framework import status -from dcim.filters import SiteFilterSet +from dcim.filtersets import SiteFilterSet from dcim.forms import SiteCSVForm from dcim.models import Site, Rack from extras.choices import * diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filtersets.py similarity index 93% rename from netbox/extras/tests/test_filters.py rename to netbox/extras/tests/test_filtersets.py index bb78c4daf..eb08f5930 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filtersets.py @@ -1,4 +1,5 @@ import uuid +from datetime import datetime, timezone from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType @@ -6,14 +7,15 @@ from django.test import TestCase from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices -from extras.filters import * +from extras.filtersets import * from extras.models import * from ipam.models import IPAddress from tenancy.models import Tenant, TenantGroup +from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests from virtualization.models import Cluster, ClusterGroup, ClusterType -class WebhookTestCase(TestCase): +class WebhookTestCase(TestCase, BaseFilterSetTests): queryset = Webhook.objects.all() filterset = WebhookFilterSet @@ -52,10 +54,6 @@ class WebhookTestCase(TestCase): webhooks[1].content_types.add(content_types[1]) webhooks[2].content_types.add(content_types[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_name(self): params = {'name': ['Webhook 1', 'Webhook 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -89,7 +87,7 @@ class WebhookTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class CustomLinkTestCase(TestCase): +class CustomLinkTestCase(TestCase, BaseFilterSetTests): queryset = CustomLink.objects.all() filterset = CustomLinkFilterSet @@ -125,10 +123,6 @@ class CustomLinkTestCase(TestCase): ) CustomLink.objects.bulk_create(custom_links) - 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': ['Custom Link 1', 'Custom Link 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -148,7 +142,7 @@ class CustomLinkTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class ExportTemplateTestCase(TestCase): +class ExportTemplateTestCase(TestCase, BaseFilterSetTests): queryset = ExportTemplate.objects.all() filterset = ExportTemplateFilterSet @@ -164,10 +158,6 @@ class ExportTemplateTestCase(TestCase): ) ExportTemplate.objects.bulk_create(export_templates) - 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': ['Export Template 1', 'Export Template 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -177,7 +167,7 @@ class ExportTemplateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class ImageAttachmentTestCase(TestCase): +class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): queryset = ImageAttachment.objects.all() filterset = ImageAttachmentFilterSet @@ -235,10 +225,6 @@ class ImageAttachmentTestCase(TestCase): ) ImageAttachment.objects.bulk_create(image_attachments) - 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': ['Image Attachment 1', 'Image Attachment 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -254,8 +240,14 @@ class ImageAttachmentTestCase(TestCase): } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_created(self): + pk_list = self.queryset.values_list('pk', flat=True)[:2] + self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) + params = {'created': '2021-01-01T00:00:00'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class JournalEntryTestCase(TestCase): + +class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = JournalEntry.objects.all() filterset = JournalEntryFilterSet @@ -320,10 +312,6 @@ class JournalEntryTestCase(TestCase): ) JournalEntry.objects.bulk_create(journal_entries) - 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_created_by(self): users = User.objects.filter(username__in=['Alice', 'Bob']) params = {'created_by': [users[0].username, users[1].username]} @@ -348,8 +336,17 @@ class JournalEntryTestCase(TestCase): params = {'kind': [JournalEntryKindChoices.KIND_INFO, JournalEntryKindChoices.KIND_SUCCESS]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_created(self): + pk_list = self.queryset.values_list('pk', flat=True)[:2] + self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) + params = { + 'created_after': '2020-12-31T00:00:00', + 'created_before': '2021-01-02T00:00:00', + } + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ConfigContextTestCase(TestCase): + +class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ConfigContext.objects.all() filterset = ConfigContextFilterSet @@ -449,10 +446,6 @@ class ConfigContextTestCase(TestCase): c.tenant_groups.set([tenant_groups[i]]) c.tenants.set([tenants[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': ['Config Context 1', 'Config Context 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -530,7 +523,7 @@ class ConfigContextTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class TagTestCase(TestCase): +class TagTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Tag.objects.all() filterset = TagFilterSet @@ -544,10 +537,6 @@ class TagTestCase(TestCase): ) Tag.objects.bulk_create(tags) - 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': ['Tag 1', 'Tag 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -561,7 +550,7 @@ class TagTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ObjectChangeTestCase(TestCase): +class ObjectChangeTestCase(TestCase, BaseFilterSetTests): queryset = ObjectChange.objects.all() filterset = ObjectChangeFilterSet @@ -635,10 +624,6 @@ class ObjectChangeTestCase(TestCase): ) ObjectChange.objects.bulk_create(object_changes) - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:3]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - def test_user(self): params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 4cda84d99..3f86c98d2 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -13,7 +13,7 @@ from utilities.forms import ConfirmationForm from utilities.tables import paginate_table from utilities.utils import copy_safe_request, count_related, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin -from . import filters, forms, tables +from . import filtersets, forms, tables from .choices import JobResultStatusChoices from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem from .reports import get_report, get_reports, run_report @@ -28,7 +28,7 @@ class TagListView(generic.ObjectListView): queryset = Tag.objects.annotate( items=count_related(TaggedItem, 'tag') ) - filterset = filters.TagFilterSet + filterset = filtersets.TagFilterSet filterset_form = forms.TagFilterForm table = tables.TagTable @@ -94,7 +94,7 @@ class TagBulkDeleteView(generic.BulkDeleteView): class ConfigContextListView(generic.ObjectListView): queryset = ConfigContext.objects.all() - filterset = filters.ConfigContextFilterSet + filterset = filtersets.ConfigContextFilterSet filterset_form = forms.ConfigContextFilterForm table = tables.ConfigContextTable action_buttons = ('add',) @@ -127,7 +127,7 @@ class ConfigContextEditView(generic.ObjectEditView): class ConfigContextBulkEditView(generic.BulkEditView): queryset = ConfigContext.objects.all() - filterset = filters.ConfigContextFilterSet + filterset = filtersets.ConfigContextFilterSet table = tables.ConfigContextTable form = forms.ConfigContextBulkEditForm @@ -173,7 +173,7 @@ class ObjectConfigContextView(generic.ObjectView): class ObjectChangeListView(generic.ObjectListView): queryset = ObjectChange.objects.all() - filterset = filters.ObjectChangeFilterSet + filterset = filtersets.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm table = tables.ObjectChangeTable template_name = 'extras/objectchange_list.html' @@ -300,7 +300,7 @@ class ImageAttachmentDeleteView(generic.ObjectDeleteView): class JournalEntryListView(generic.ObjectListView): queryset = JournalEntry.objects.all() - filterset = filters.JournalEntryFilterSet + filterset = filtersets.JournalEntryFilterSet filterset_form = forms.JournalEntryFilterForm table = tables.JournalEntryTable action_buttons = ('export',) @@ -338,14 +338,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView): class JournalEntryBulkEditView(generic.BulkEditView): queryset = JournalEntry.objects.prefetch_related('created_by') - filterset = filters.JournalEntryFilterSet + filterset = filtersets.JournalEntryFilterSet table = tables.JournalEntryTable form = forms.JournalEntryBulkEditForm class JournalEntryBulkDeleteView(generic.BulkDeleteView): queryset = JournalEntry.objects.prefetch_related('created_by') - filterset = filters.JournalEntryFilterSet + filterset = filtersets.JournalEntryFilterSet table = tables.JournalEntryTable diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 1e1177772..f3f1335f7 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -10,7 +10,7 @@ from rest_framework.response import Response from rest_framework.routers import APIRootView from extras.api.views import CustomFieldModelViewSet -from ipam import filters +from ipam import filtersets from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF from netbox.api.views import ModelViewSet from utilities.constants import ADVISORY_LOCK_KEYS @@ -38,7 +38,7 @@ class VRFViewSet(CustomFieldModelViewSet): prefix_count=count_related(Prefix, 'vrf') ) serializer_class = serializers.VRFSerializer - filterset_class = filters.VRFFilterSet + filterset_class = filtersets.VRFFilterSet # @@ -48,7 +48,7 @@ class VRFViewSet(CustomFieldModelViewSet): class RouteTargetViewSet(CustomFieldModelViewSet): queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags') serializer_class = serializers.RouteTargetSerializer - filterset_class = filters.RouteTargetFilterSet + filterset_class = filtersets.RouteTargetFilterSet # @@ -60,7 +60,7 @@ class RIRViewSet(CustomFieldModelViewSet): aggregate_count=count_related(Aggregate, 'rir') ) serializer_class = serializers.RIRSerializer - filterset_class = filters.RIRFilterSet + filterset_class = filtersets.RIRFilterSet # @@ -70,7 +70,7 @@ class RIRViewSet(CustomFieldModelViewSet): class AggregateViewSet(CustomFieldModelViewSet): queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags') serializer_class = serializers.AggregateSerializer - filterset_class = filters.AggregateFilterSet + filterset_class = filtersets.AggregateFilterSet # @@ -83,7 +83,7 @@ class RoleViewSet(CustomFieldModelViewSet): vlan_count=count_related(VLAN, 'role') ) serializer_class = serializers.RoleSerializer - filterset_class = filters.RoleFilterSet + filterset_class = filtersets.RoleFilterSet # @@ -95,7 +95,7 @@ class PrefixViewSet(CustomFieldModelViewSet): 'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags' ) serializer_class = serializers.PrefixSerializer - filterset_class = filters.PrefixFilterSet + filterset_class = filtersets.PrefixFilterSet def get_serializer_class(self): if self.action == "available_prefixes" and self.request.method == "POST": @@ -275,7 +275,7 @@ class IPAddressViewSet(CustomFieldModelViewSet): 'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object' ) serializer_class = serializers.IPAddressSerializer - filterset_class = filters.IPAddressFilterSet + filterset_class = filtersets.IPAddressFilterSet # @@ -287,7 +287,7 @@ class VLANGroupViewSet(CustomFieldModelViewSet): vlan_count=count_related(VLAN, 'group') ) serializer_class = serializers.VLANGroupSerializer - filterset_class = filters.VLANGroupFilterSet + filterset_class = filtersets.VLANGroupFilterSet # @@ -301,7 +301,7 @@ class VLANViewSet(CustomFieldModelViewSet): prefix_count=count_related(Prefix, 'vlan') ) serializer_class = serializers.VLANSerializer - filterset_class = filters.VLANFilterSet + filterset_class = filtersets.VLANFilterSet # @@ -313,4 +313,4 @@ class ServiceViewSet(ModelViewSet): 'device', 'virtual_machine', 'tags', 'ipaddresses' ) serializer_class = serializers.ServiceSerializer - filterset_class = filters.ServiceFilterSet + filterset_class = filtersets.ServiceFilterSet diff --git a/netbox/ipam/filters.py b/netbox/ipam/filtersets.py similarity index 94% rename from netbox/ipam/filters.py rename to netbox/ipam/filtersets.py index 8f4030411..5ab4994ea 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filtersets.py @@ -6,11 +6,11 @@ from django.db.models import Q from netaddr.core import AddrFormatError from dcim.models import Device, Interface, Region, Site, SiteGroup -from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet -from tenancy.filters import TenancyFilterSet +from extras.filters import TagFilter +from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet +from tenancy.filtersets import TenancyFilterSet from utilities.filters import ( - BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, - NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter, + ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter, ) from virtualization.models import VirtualMachine, VMInterface from .choices import * @@ -31,7 +31,7 @@ __all__ = ( ) -class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -74,7 +74,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, C fields = ['id', 'name', 'rd', 'enforce_unique'] -class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -116,14 +116,14 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt fields = ['id', 'name'] -class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class RIRFilterSet(OrganizationalModelFilterSet): class Meta: model = RIR fields = ['id', 'name', 'slug', 'is_private', 'description'] -class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -173,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter return queryset.none() -class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class RoleFilterSet(OrganizationalModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -184,7 +184,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilter fields = ['id', 'name', 'slug'] -class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -369,7 +369,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet ) -class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -535,7 +535,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter return queryset.exclude(assigned_object_id__isnull=value) -class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class VLANGroupFilterSet(OrganizationalModelFilterSet): scope_type = ContentTypeFilter() region = django_filters.NumberFilter( method='filter_scope' @@ -570,7 +570,7 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF ) -class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -666,7 +666,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, return queryset.get_for_virtualmachine(value) -class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): +class ServiceFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filtersets.py similarity index 96% rename from netbox/ipam/tests/test_filters.py rename to netbox/ipam/tests/test_filtersets.py index 3ea54209c..f43a44c62 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -2,13 +2,14 @@ from django.test import TestCase from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup from ipam.choices import * -from ipam.filters import * +from ipam.filtersets import * from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF +from utilities.testing import ChangeLoggedFilterSetTests from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from tenancy.models import Tenant, TenantGroup -class VRFTestCase(TestCase): +class VRFTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VRF.objects.all() filterset = VRFFilterSet @@ -53,10 +54,6 @@ class VRFTestCase(TestCase): vrfs[2].import_targets.add(route_targets[2]) vrfs[2].export_targets.add(route_targets[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_name(self): params = {'name': ['VRF 1', 'VRF 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -100,7 +97,7 @@ class VRFTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class RouteTargetTestCase(TestCase): +class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RouteTarget.objects.all() filterset = RouteTargetFilterSet @@ -149,10 +146,6 @@ class RouteTargetTestCase(TestCase): vrfs[1].import_targets.add(route_targets[4], route_targets[5]) vrfs[1].export_targets.add(route_targets[6], route_targets[7]) - 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': ['65000:1001', '65000:1002', '65000:1003']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) @@ -186,7 +179,7 @@ class RouteTargetTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) -class RIRTestCase(TestCase): +class RIRTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = RIR.objects.all() filterset = RIRFilterSet @@ -203,10 +196,6 @@ class RIRTestCase(TestCase): ) RIR.objects.bulk_create(rirs) - 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': ['RIR 1', 'RIR 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -226,7 +215,7 @@ class RIRTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) -class AggregateTestCase(TestCase): +class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Aggregate.objects.all() filterset = AggregateFilterSet @@ -265,10 +254,6 @@ class AggregateTestCase(TestCase): ) Aggregate.objects.bulk_create(aggregates) - 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_family(self): params = {'family': '4'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) @@ -304,7 +289,7 @@ class AggregateTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class RoleTestCase(TestCase): +class RoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Role.objects.all() filterset = RoleFilterSet @@ -318,10 +303,6 @@ class RoleTestCase(TestCase): ) Role.objects.bulk_create(roles) - 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': ['Role 1', 'Role 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -331,7 +312,7 @@ class RoleTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class PrefixTestCase(TestCase): +class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Prefix.objects.all() filterset = PrefixFilterSet @@ -421,10 +402,6 @@ class PrefixTestCase(TestCase): ) Prefix.objects.bulk_create(prefixes) - 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_family(self): params = {'family': '6'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) @@ -528,7 +505,7 @@ class PrefixTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class IPAddressTestCase(TestCase): +class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = IPAddress.objects.all() filterset = IPAddressFilterSet @@ -607,10 +584,6 @@ class IPAddressTestCase(TestCase): ) IPAddress.objects.bulk_create(ipaddresses) - 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_family(self): params = {'family': '6'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) @@ -708,7 +681,7 @@ class IPAddressTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) -class VLANGroupTestCase(TestCase): +class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VLANGroup.objects.all() filterset = VLANGroupFilterSet @@ -751,10 +724,6 @@ class VLANGroupTestCase(TestCase): ) VLANGroup.objects.bulk_create(vlan_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': ['VLAN Group 1', 'VLAN Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -796,7 +765,7 @@ class VLANGroupTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) -class VLANTestCase(TestCase): +class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VLAN.objects.all() filterset = VLANFilterSet @@ -965,10 +934,6 @@ class VLANTestCase(TestCase): ) VLAN.objects.bulk_create(vlans) - 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': ['VLAN 101', 'VLAN 102']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1041,7 +1006,7 @@ class VLANTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global -class ServiceTestCase(TestCase): +class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Service.objects.all() filterset = ServiceFilterSet @@ -1080,10 +1045,6 @@ class ServiceTestCase(TestCase): ) Service.objects.bulk_create(services) - def test_id(self): - params = {'id': self.queryset.values_list('pk', flat=True)[:3]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) - def test_name(self): params = {'name': ['Service 1', 'Service 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 0339aff07..168933af7 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -7,7 +7,7 @@ from netbox.views import generic from utilities.tables import paginate_table from utilities.utils import count_related from virtualization.models import VirtualMachine, VMInterface -from . import filters, forms, tables +from . import filtersets, forms, tables from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans @@ -19,7 +19,7 @@ from .utils import add_available_ipaddresses, add_available_prefixes, add_availa class VRFListView(generic.ObjectListView): queryset = VRF.objects.all() - filterset = filters.VRFFilterSet + filterset = filtersets.VRFFilterSet filterset_form = forms.VRFFilterForm table = tables.VRFTable @@ -65,14 +65,14 @@ class VRFBulkImportView(generic.BulkImportView): class VRFBulkEditView(generic.BulkEditView): queryset = VRF.objects.prefetch_related('tenant') - filterset = filters.VRFFilterSet + filterset = filtersets.VRFFilterSet table = tables.VRFTable form = forms.VRFBulkEditForm class VRFBulkDeleteView(generic.BulkDeleteView): queryset = VRF.objects.prefetch_related('tenant') - filterset = filters.VRFFilterSet + filterset = filtersets.VRFFilterSet table = tables.VRFTable @@ -82,7 +82,7 @@ class VRFBulkDeleteView(generic.BulkDeleteView): class RouteTargetListView(generic.ObjectListView): queryset = RouteTarget.objects.all() - filterset = filters.RouteTargetFilterSet + filterset = filtersets.RouteTargetFilterSet filterset_form = forms.RouteTargetFilterForm table = tables.RouteTargetTable @@ -123,14 +123,14 @@ class RouteTargetBulkImportView(generic.BulkImportView): class RouteTargetBulkEditView(generic.BulkEditView): queryset = RouteTarget.objects.prefetch_related('tenant') - filterset = filters.RouteTargetFilterSet + filterset = filtersets.RouteTargetFilterSet table = tables.RouteTargetTable form = forms.RouteTargetBulkEditForm class RouteTargetBulkDeleteView(generic.BulkDeleteView): queryset = RouteTarget.objects.prefetch_related('tenant') - filterset = filters.RouteTargetFilterSet + filterset = filtersets.RouteTargetFilterSet table = tables.RouteTargetTable @@ -142,7 +142,7 @@ class RIRListView(generic.ObjectListView): queryset = RIR.objects.annotate( aggregate_count=count_related(Aggregate, 'rir') ) - filterset = filters.RIRFilterSet + filterset = filtersets.RIRFilterSet filterset_form = forms.RIRFilterForm table = tables.RIRTable template_name = 'ipam/rir_list.html' @@ -184,7 +184,7 @@ class RIRBulkEditView(generic.BulkEditView): queryset = RIR.objects.annotate( aggregate_count=count_related(Aggregate, 'rir') ) - filterset = filters.RIRFilterSet + filterset = filtersets.RIRFilterSet table = tables.RIRTable form = forms.RIRBulkEditForm @@ -193,7 +193,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView): queryset = RIR.objects.annotate( aggregate_count=count_related(Aggregate, 'rir') ) - filterset = filters.RIRFilterSet + filterset = filtersets.RIRFilterSet table = tables.RIRTable @@ -205,7 +205,7 @@ class AggregateListView(generic.ObjectListView): queryset = Aggregate.objects.annotate( child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) ) - filterset = filters.AggregateFilterSet + filterset = filtersets.AggregateFilterSet filterset_form = forms.AggregateFilterForm table = tables.AggregateDetailTable template_name = 'ipam/aggregate_list.html' @@ -280,14 +280,14 @@ class AggregateBulkImportView(generic.BulkImportView): class AggregateBulkEditView(generic.BulkEditView): queryset = Aggregate.objects.prefetch_related('rir') - filterset = filters.AggregateFilterSet + filterset = filtersets.AggregateFilterSet table = tables.AggregateTable form = forms.AggregateBulkEditForm class AggregateBulkDeleteView(generic.BulkDeleteView): queryset = Aggregate.objects.prefetch_related('rir') - filterset = filters.AggregateFilterSet + filterset = filtersets.AggregateFilterSet table = tables.AggregateTable @@ -337,7 +337,7 @@ class RoleBulkImportView(generic.BulkImportView): class RoleBulkEditView(generic.BulkEditView): queryset = Role.objects.all() - filterset = filters.RoleFilterSet + filterset = filtersets.RoleFilterSet table = tables.RoleTable form = forms.RoleBulkEditForm @@ -353,7 +353,7 @@ class RoleBulkDeleteView(generic.BulkDeleteView): class PrefixListView(generic.ObjectListView): queryset = Prefix.objects.annotate_tree() - filterset = filters.PrefixFilterSet + filterset = filtersets.PrefixFilterSet filterset_form = forms.PrefixFilterForm table = tables.PrefixDetailTable template_name = 'ipam/prefix_list.html' @@ -493,14 +493,14 @@ class PrefixBulkImportView(generic.BulkImportView): class PrefixBulkEditView(generic.BulkEditView): queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') - filterset = filters.PrefixFilterSet + filterset = filtersets.PrefixFilterSet table = tables.PrefixTable form = forms.PrefixBulkEditForm class PrefixBulkDeleteView(generic.BulkDeleteView): queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') - filterset = filters.PrefixFilterSet + filterset = filtersets.PrefixFilterSet table = tables.PrefixTable @@ -510,7 +510,7 @@ class PrefixBulkDeleteView(generic.BulkDeleteView): class IPAddressListView(generic.ObjectListView): queryset = IPAddress.objects.all() - filterset = filters.IPAddressFilterSet + filterset = filtersets.IPAddressFilterSet filterset_form = forms.IPAddressFilterForm table = tables.IPAddressDetailTable @@ -613,7 +613,7 @@ class IPAddressAssignView(generic.ObjectView): addresses = self.queryset.prefetch_related('vrf', 'tenant') # Limit to 100 results - addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100] + addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100] table = tables.IPAddressAssignTable(addresses) return render(request, 'ipam/ipaddress_assign.html', { @@ -643,14 +643,14 @@ class IPAddressBulkImportView(generic.BulkImportView): class IPAddressBulkEditView(generic.BulkEditView): queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') - filterset = filters.IPAddressFilterSet + filterset = filtersets.IPAddressFilterSet table = tables.IPAddressTable form = forms.IPAddressBulkEditForm class IPAddressBulkDeleteView(generic.BulkDeleteView): queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') - filterset = filters.IPAddressFilterSet + filterset = filtersets.IPAddressFilterSet table = tables.IPAddressTable @@ -662,7 +662,7 @@ class VLANGroupListView(generic.ObjectListView): queryset = VLANGroup.objects.annotate( vlan_count=count_related(VLAN, 'group') ) - filterset = filters.VLANGroupFilterSet + filterset = filtersets.VLANGroupFilterSet filterset_form = forms.VLANGroupFilterForm table = tables.VLANGroupTable @@ -673,7 +673,7 @@ class VLANGroupView(generic.ObjectView): def get_extra_context(self, request, instance): vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related( Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)) - ) + ).order_by('vid') vlans_count = vlans.count() vlans = add_available_vlans(instance, vlans) @@ -684,9 +684,17 @@ class VLANGroupView(generic.ObjectView): vlans_table.columns.hide('group') paginate_table(vlans_table, request) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_vlan'), + 'change': request.user.has_perm('ipam.change_vlan'), + 'delete': request.user.has_perm('ipam.delete_vlan'), + } + return { 'vlans_count': vlans_count, 'vlans_table': vlans_table, + 'permissions': permissions, } @@ -710,7 +718,7 @@ class VLANGroupBulkEditView(generic.BulkEditView): queryset = VLANGroup.objects.annotate( vlan_count=count_related(VLAN, 'group') ) - filterset = filters.VLANGroupFilterSet + filterset = filtersets.VLANGroupFilterSet table = tables.VLANGroupTable form = forms.VLANGroupBulkEditForm @@ -719,7 +727,7 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView): queryset = VLANGroup.objects.annotate( vlan_count=count_related(VLAN, 'group') ) - filterset = filters.VLANGroupFilterSet + filterset = filtersets.VLANGroupFilterSet table = tables.VLANGroupTable @@ -729,7 +737,7 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView): class VLANListView(generic.ObjectListView): queryset = VLAN.objects.all() - filterset = filters.VLANFilterSet + filterset = filtersets.VLANFilterSet filterset_form = forms.VLANFilterForm table = tables.VLANDetailTable @@ -797,14 +805,14 @@ class VLANBulkImportView(generic.BulkImportView): class VLANBulkEditView(generic.BulkEditView): queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') - filterset = filters.VLANFilterSet + filterset = filtersets.VLANFilterSet table = tables.VLANTable form = forms.VLANBulkEditForm class VLANBulkDeleteView(generic.BulkDeleteView): queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role') - filterset = filters.VLANFilterSet + filterset = filtersets.VLANFilterSet table = tables.VLANTable @@ -814,7 +822,7 @@ class VLANBulkDeleteView(generic.BulkDeleteView): class ServiceListView(generic.ObjectListView): queryset = Service.objects.all() - filterset = filters.ServiceFilterSet + filterset = filtersets.ServiceFilterSet filterset_form = forms.ServiceFilterForm table = tables.ServiceTable action_buttons = ('import', 'export') @@ -855,12 +863,12 @@ class ServiceDeleteView(generic.ObjectDeleteView): class ServiceBulkEditView(generic.BulkEditView): queryset = Service.objects.prefetch_related('device', 'virtual_machine') - filterset = filters.ServiceFilterSet + filterset = filtersets.ServiceFilterSet table = tables.ServiceTable form = forms.ServiceBulkEditForm class ServiceBulkDeleteView(generic.BulkDeleteView): queryset = Service.objects.prefetch_related('device', 'virtual_machine') - filterset = filters.ServiceFilterSet + filterset = filtersets.ServiceFilterSet table = tables.ServiceTable diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index c40e280dd..461d7f4cd 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -246,6 +246,9 @@ RQ_DEFAULT_TIMEOUT = 300 # this setting is derived from the installed location. # SCRIPTS_ROOT = '/opt/netbox/netbox/scripts' +# The name to use for the session cookie. +SESSION_COOKIE_NAME = 'sessionid' + # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only # database access.) Note that the user as which NetBox runs must have read and write permissions to this path. diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 5568f4e70..b6da0b2de 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -1,9 +1,9 @@ from collections import OrderedDict -from circuits.filters import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet +from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet from circuits.models import Circuit, ProviderNetwork, Provider from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable -from dcim.filters import ( +from dcim.filtersets import ( CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet, SiteFilterSet, VirtualChassisFilterSet, ) @@ -12,17 +12,17 @@ from dcim.tables import ( CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable, VirtualChassisTable, ) -from ipam.filters import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet +from ipam.filtersets import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable -from secrets.filters import SecretFilterSet +from secrets.filtersets import SecretFilterSet from secrets.models import Secret from secrets.tables import SecretTable -from tenancy.filters import TenantFilterSet +from tenancy.filtersets import TenantFilterSet from tenancy.models import Tenant from tenancy.tables import TenantTable from utilities.utils import count_related -from virtualization.filters import ClusterFilterSet, VirtualMachineFilterSet +from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet from virtualization.models import Cluster, VirtualMachine from virtualization.tables import ClusterTable, VirtualMachineDetailTable diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py new file mode 100644 index 000000000..aa9e15385 --- /dev/null +++ b/netbox/netbox/filtersets.py @@ -0,0 +1,238 @@ +import django_filters +from copy import deepcopy +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django_filters.utils import get_model_field, resolve_field + +from dcim.forms import MACAddressField +from extras.choices import CustomFieldFilterLogicChoices +from extras.filters import CustomFieldFilter, TagFilter +from extras.models import CustomField +from utilities.constants import ( + FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, + FILTER_NUMERIC_BASED_LOOKUP_MAP +) +from utilities import filters + + +__all__ = ( + 'BaseFilterSet', + 'ChangeLoggedModelFilterSet', + 'OrganizationalModelFilterSet', + 'PrimaryModelFilterSet', +) + + +# +# FilterSets +# + +class BaseFilterSet(django_filters.FilterSet): + """ + A base FilterSet which provides common functionality to all NetBox FilterSets + """ + FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS) + FILTER_DEFAULTS.update({ + models.AutoField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.CharField: { + 'filter_class': filters.MultiValueCharFilter + }, + models.DateField: { + 'filter_class': filters.MultiValueDateFilter + }, + models.DateTimeField: { + 'filter_class': filters.MultiValueDateTimeFilter + }, + models.DecimalField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.EmailField: { + 'filter_class': filters.MultiValueCharFilter + }, + models.FloatField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.IntegerField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.PositiveIntegerField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.PositiveSmallIntegerField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.SlugField: { + 'filter_class': filters.MultiValueCharFilter + }, + models.SmallIntegerField: { + 'filter_class': filters.MultiValueNumberFilter + }, + models.TimeField: { + 'filter_class': filters.MultiValueTimeFilter + }, + models.URLField: { + 'filter_class': filters.MultiValueCharFilter + }, + MACAddressField: { + 'filter_class': filters.MultiValueMACAddressFilter + }, + }) + + @staticmethod + def _get_filter_lookup_dict(existing_filter): + # Choose the lookup expression map based on the filter type + if isinstance(existing_filter, ( + filters.MultiValueDateFilter, + filters.MultiValueDateTimeFilter, + filters.MultiValueNumberFilter, + filters.MultiValueTimeFilter + )): + lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP + + elif isinstance(existing_filter, ( + filters.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, + filters.MultiValueCharFilter, + filters.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 `__` + """ + filters = super().get_filters() + + new_filters = {} + for existing_filter_name, existing_filter in filters.items(): + # Loop over existing filters to extract metadata by which to create new filters + + # If the filter makes use of a custom filter method or lookup expression skip it + # as we cannot sanely handle these cases in a generic mannor + if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']: + continue + + # Choose the lookup expression map based on the filter type + lookup_map = cls._get_filter_lookup_dict(existing_filter) + if lookup_map is None: + # Do not augment this filter type with more lookup expressions + continue + + # Get properties of the existing filter for later use + field_name = existing_filter.field_name + field = get_model_field(cls._meta.model, field_name) + + # Create new filters for each lookup expression in the map + for lookup_name, lookup_expr in lookup_map.items(): + new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name) + + try: + if existing_filter_name in cls.declared_filters: + # The filter field has been explicity defined on the filterset class so we must manually + # create the new filter with the same type because there is no guarantee the defined type + # is the same as the default type for the field + resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid + new_filter = type(existing_filter)( + field_name=field_name, + lookup_expr=lookup_expr, + label=existing_filter.label, + exclude=existing_filter.exclude, + distinct=existing_filter.distinct, + **existing_filter.extra + ) + else: + # The filter field is listed in Meta.fields so we can safely rely on default behaviour + # Will raise FieldLookupError if the lookup is invalid + new_filter = cls.filter_for_field(field, field_name, lookup_expr) + except django_filters.exceptions.FieldLookupError: + # The filter could not be created because the lookup expression is not supported on the field + continue + + if lookup_name.startswith('n'): + # This is a negation filter which requires a queryset.exclude() clause + # Of course setting the negation of the existing filter's exclude attribute handles both cases + new_filter.exclude = not existing_filter.exclude + + new_filters[new_filter_name] = new_filter + + filters.update(new_filters) + return filters + + +class ChangeLoggedModelFilterSet(BaseFilterSet): + created = django_filters.DateFilter() + created__gte = django_filters.DateFilter( + field_name='created', + lookup_expr='gte' + ) + created__lte = django_filters.DateFilter( + field_name='created', + lookup_expr='lte' + ) + last_updated = django_filters.DateTimeFilter() + last_updated__gte = django_filters.DateTimeFilter( + field_name='last_updated', + lookup_expr='gte' + ) + last_updated__lte = django_filters.DateTimeFilter( + field_name='last_updated', + lookup_expr='lte' + ) + + +class PrimaryModelFilterSet(ChangeLoggedModelFilterSet): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Dynamically add a Filter for each CustomField applicable to the parent model + custom_fields = CustomField.objects.filter( + content_types=ContentType.objects.get_for_model(self._meta.model) + ).exclude( + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED + ) + for cf in custom_fields: + self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf) + + +class OrganizationalModelFilterSet(PrimaryModelFilterSet): + """ + A base class for adding the search method to models which only expose the `name` and `slug` fields + """ + q = django_filters.CharFilter( + method='search', + label='Search', + ) + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + models.Q(name__icontains=value) | + models.Q(slug__icontains=value) + ) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7d4fb91da..71edae573 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -114,6 +114,7 @@ REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 're RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300) SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) +SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid') SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 3650abd30..7982d29f1 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -10,7 +10,7 @@ from rest_framework.viewsets import ViewSet from extras.api.views import CustomFieldModelViewSet from netbox.api.views import ModelViewSet -from secrets import filters +from secrets import filtersets from secrets.exceptions import InvalidKey from secrets.models import Secret, SecretRole, SessionKey, UserKey from utilities.utils import count_related @@ -39,7 +39,7 @@ class SecretRoleViewSet(CustomFieldModelViewSet): secret_count=count_related(Secret, 'role') ) serializer_class = serializers.SecretRoleSerializer - filterset_class = filters.SecretRoleFilterSet + filterset_class = filtersets.SecretRoleFilterSet # @@ -49,7 +49,7 @@ class SecretRoleViewSet(CustomFieldModelViewSet): class SecretViewSet(ModelViewSet): queryset = Secret.objects.prefetch_related('role', 'tags') serializer_class = serializers.SecretSerializer - filterset_class = filters.SecretFilterSet + filterset_class = filtersets.SecretFilterSet master_key = None diff --git a/netbox/secrets/filters.py b/netbox/secrets/filtersets.py similarity index 84% rename from netbox/secrets/filters.py rename to netbox/secrets/filtersets.py index fb36c827a..644864ecb 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filtersets.py @@ -2,8 +2,8 @@ import django_filters from django.db.models import Q from dcim.models import Device -from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet -from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter +from extras.filters import TagFilter +from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet from virtualization.models import VirtualMachine from .models import Secret, SecretRole @@ -14,14 +14,14 @@ __all__ = ( ) -class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class SecretRoleFilterSet(OrganizationalModelFilterSet): class Meta: model = SecretRole fields = ['id', 'name', 'slug'] -class SecretFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class SecretFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/secrets/tests/test_filters.py b/netbox/secrets/tests/test_filtersets.py similarity index 91% rename from netbox/secrets/tests/test_filters.py rename to netbox/secrets/tests/test_filtersets.py index 0be1ef594..d06c56598 100644 --- a/netbox/secrets/tests/test_filters.py +++ b/netbox/secrets/tests/test_filtersets.py @@ -1,12 +1,13 @@ from django.test import TestCase from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site -from secrets.filters import * +from secrets.filtersets import * from secrets.models import Secret, SecretRole +from utilities.testing import ChangeLoggedFilterSetTests from virtualization.models import Cluster, ClusterType, VirtualMachine -class SecretRoleTestCase(TestCase): +class SecretRoleTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = SecretRole.objects.all() filterset = SecretRoleFilterSet @@ -20,10 +21,6 @@ class SecretRoleTestCase(TestCase): ) SecretRole.objects.bulk_create(roles) - 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': ['Secret Role 1', 'Secret Role 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -33,7 +30,7 @@ class SecretRoleTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class SecretTestCase(TestCase): +class SecretTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Secret.objects.all() filterset = SecretFilterSet @@ -80,10 +77,6 @@ class SecretTestCase(TestCase): for s in secrets: s.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': ['Secret 1', 'Secret 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index cc0504329..7ae0b66eb 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -2,14 +2,14 @@ import base64 import logging from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import redirect, render from django.utils.html import escape from django.utils.safestring import mark_safe from netbox.views import generic from utilities.tables import paginate_table from utilities.utils import count_related -from . import filters, forms, tables +from . import filtersets, forms, tables from .models import SecretRole, Secret, SessionKey, UserKey @@ -70,7 +70,7 @@ class SecretRoleBulkEditView(generic.BulkEditView): queryset = SecretRole.objects.annotate( secret_count=count_related(Secret, 'role') ) - filterset = filters.SecretRoleFilterSet + filterset = filtersets.SecretRoleFilterSet table = tables.SecretRoleTable form = forms.SecretRoleBulkEditForm @@ -88,7 +88,7 @@ class SecretRoleBulkDeleteView(generic.BulkDeleteView): class SecretListView(generic.ObjectListView): queryset = Secret.objects.all() - filterset = filters.SecretFilterSet + filterset = filtersets.SecretFilterSet filterset_form = forms.SecretFilterForm table = tables.SecretTable action_buttons = ('add', 'import', 'export') @@ -220,12 +220,12 @@ class SecretBulkImportView(generic.BulkImportView): class SecretBulkEditView(generic.BulkEditView): queryset = Secret.objects.prefetch_related('role') - filterset = filters.SecretFilterSet + filterset = filtersets.SecretFilterSet table = tables.SecretTable form = forms.SecretBulkEditForm class SecretBulkDeleteView(generic.BulkDeleteView): queryset = Secret.objects.prefetch_related('role') - filterset = filters.SecretFilterSet + filterset = filtersets.SecretFilterSet table = tables.SecretTable diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index 7d0f9e668..285851e7b 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -10,6 +10,15 @@ {% endblock %} +{% block buttons %} + {% if perms.ipam.add_vlan %} + + Add VLAN + + {% endif %} + {{ block.super }} +{% endblock %} + {% block content %}
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/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..45dffb3c0 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -4,7 +4,7 @@ from ipam.models import 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 @@ -130,12 +130,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 `__` - """ - filters = super().get_filters() - - new_filters = {} - for existing_filter_name, existing_filter in filters.items(): - # Loop over existing filters to extract metadata by which to create new filters - - # If the filter makes use of a custom filter method or lookup expression skip it - # as we cannot sanely handle these cases in a generic mannor - if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']: - continue - - # Choose the lookup expression map based on the filter type - lookup_map = cls._get_filter_lookup_dict(existing_filter) - if lookup_map is None: - # Do not augment this filter type with more lookup expressions - continue - - # Get properties of the existing filter for later use - field_name = existing_filter.field_name - field = get_model_field(cls._meta.model, field_name) - - # Create new filters for each lookup expression in the map - for lookup_name, lookup_expr in lookup_map.items(): - new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name) - - try: - if existing_filter_name in cls.declared_filters: - # The filter field has been explicity defined on the filterset class so we must manually - # create the new filter with the same type because there is no guarantee the defined type - # is the same as the default type for the field - resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid - new_filter = type(existing_filter)( - field_name=field_name, - lookup_expr=lookup_expr, - label=existing_filter.label, - exclude=existing_filter.exclude, - distinct=existing_filter.distinct, - **existing_filter.extra - ) - else: - # The filter field is listed in Meta.fields so we can safely rely on default behaviour - # Will raise FieldLookupError if the lookup is invalid - new_filter = cls.filter_for_field(field, field_name, lookup_expr) - except django_filters.exceptions.FieldLookupError: - # The filter could not be created because the lookup expression is not supported on the field - continue - - if lookup_name.startswith('n'): - # This is a negation filter which requires a queryset.exclude() clause - # Of course setting the negation of the existing filter's exclude attribute handles both cases - new_filter.exclude = not existing_filter.exclude - - new_filters[new_filter_name] = new_filter - - filters.update(new_filters) - return filters - - -class NameSlugSearchFilterSet(django_filters.FilterSet): - """ - A base class for adding the search method to models which only expose the `name` and `slug` fields - """ - q = django_filters.CharFilter( - method='search', - label='Search', - ) - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - models.Q(name__icontains=value) | - models.Q(slug__icontains=value) - ) diff --git a/netbox/utilities/testing/__init__.py b/netbox/utilities/testing/__init__.py index c6634f22e..6a52d93f3 100644 --- a/netbox/utilities/testing/__init__.py +++ b/netbox/utilities/testing/__init__.py @@ -1,4 +1,5 @@ from .api import * from .base import * +from .filtersets import * from .utils import * from .views import * diff --git a/netbox/utilities/testing/filtersets.py b/netbox/utilities/testing/filtersets.py new file mode 100644 index 000000000..98bb0d1ac --- /dev/null +++ b/netbox/utilities/testing/filtersets.py @@ -0,0 +1,35 @@ +from datetime import date, datetime, timezone + + +__all__ = ( + 'BaseFilterSetTests', + 'ChangeLoggedFilterSetTests', +) + + +class BaseFilterSetTests: + queryset = None + filterset = None + + def test_id(self): + """ + Test filtering for two PKs from a set of >2 objects. + """ + params = {'id': self.queryset.values_list('pk', flat=True)[:2]} + self.assertGreater(self.queryset.count(), 2) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + +class ChangeLoggedFilterSetTests(BaseFilterSetTests): + + def test_created(self): + pk_list = self.queryset.values_list('pk', flat=True)[:2] + self.queryset.filter(pk__in=pk_list).update(created=date(2021, 1, 1)) + params = {'created': '2021-01-01'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_last_updated(self): + pk_list = self.queryset.values_list('pk', flat=True)[:2] + self.queryset.filter(pk__in=pk_list).update(last_updated=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) + params = {'last_updated': '2021-01-01T00:00:00'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/utilities/tests/test_filters.py b/netbox/utilities/tests/test_filters.py index 56eaabd4c..374167f1c 100644 --- a/netbox/utilities/tests/test_filters.py +++ b/netbox/utilities/tests/test_filters.py @@ -7,14 +7,16 @@ from taggit.managers import TaggableManager from dcim.choices import * from dcim.fields import MACAddressField -from dcim.filters import DeviceFilterSet, SiteFilterSet +from dcim.filtersets import DeviceFilterSet, SiteFilterSet from dcim.models import ( Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site ) +from extras.filters import TagFilter from extras.models import TaggedItem +from netbox.filtersets import BaseFilterSet from utilities.filters import ( - BaseFilterSet, MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, - MultiValueNumberFilter, MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter, + MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, MultiValueNumberFilter, + MultiValueTimeFilter, TreeNodeMultipleChoiceFilter, ) diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 1c4371ed0..8eebd2120 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -3,7 +3,7 @@ from rest_framework.routers import APIRootView from dcim.models import Device from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet, ModelViewSet from utilities.utils import count_related -from virtualization import filters +from virtualization import filtersets from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from . import serializers @@ -25,7 +25,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet): cluster_count=count_related(Cluster, 'type') ) serializer_class = serializers.ClusterTypeSerializer - filterset_class = filters.ClusterTypeFilterSet + filterset_class = filtersets.ClusterTypeFilterSet class ClusterGroupViewSet(CustomFieldModelViewSet): @@ -33,7 +33,7 @@ class ClusterGroupViewSet(CustomFieldModelViewSet): cluster_count=count_related(Cluster, 'group') ) serializer_class = serializers.ClusterGroupSerializer - filterset_class = filters.ClusterGroupFilterSet + filterset_class = filtersets.ClusterGroupFilterSet class ClusterViewSet(CustomFieldModelViewSet): @@ -44,7 +44,7 @@ class ClusterViewSet(CustomFieldModelViewSet): virtualmachine_count=count_related(VirtualMachine, 'cluster') ) serializer_class = serializers.ClusterSerializer - filterset_class = filters.ClusterFilterSet + filterset_class = filtersets.ClusterFilterSet # @@ -55,7 +55,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet) queryset = VirtualMachine.objects.prefetch_related( 'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags' ) - filterset_class = filters.VirtualMachineFilterSet + filterset_class = filtersets.VirtualMachineFilterSet def get_serializer_class(self): """ @@ -83,5 +83,5 @@ class VMInterfaceViewSet(ModelViewSet): 'virtual_machine', 'parent', 'tags', 'tagged_vlans', 'ip_addresses' ) serializer_class = serializers.VMInterfaceSerializer - filterset_class = filters.VMInterfaceFilterSet + filterset_class = filtersets.VMInterfaceFilterSet brief_prefetch_fields = ['virtual_machine'] diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filtersets.py similarity index 90% rename from netbox/virtualization/filters.py rename to netbox/virtualization/filtersets.py index 6d706b6cf..6d930b69e 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filtersets.py @@ -2,12 +2,11 @@ import django_filters from django.db.models import Q from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup -from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet -from tenancy.filters import TenancyFilterSet -from utilities.filters import ( - BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, TagFilter, - TreeNodeMultipleChoiceFilter, -) +from extras.filters import TagFilter +from extras.filtersets import LocalConfigContextFilterSet +from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet +from tenancy.filtersets import TenancyFilterSet +from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter from .choices import * from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -20,21 +19,21 @@ __all__ = ( ) -class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class ClusterTypeFilterSet(OrganizationalModelFilterSet): class Meta: model = ClusterType fields = ['id', 'name', 'slug', 'description'] -class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet): +class ClusterGroupFilterSet(OrganizationalModelFilterSet): class Meta: model = ClusterGroup fields = ['id', 'name', 'slug', 'description'] -class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -110,13 +109,7 @@ class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSe ) -class VirtualMachineFilterSet( - BaseFilterSet, - LocalConfigContextFilterSet, - TenancyFilterSet, - CustomFieldModelFilterSet, - CreatedUpdatedFilterSet -): +class VirtualMachineFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -237,7 +230,7 @@ class VirtualMachineFilterSet( return queryset.exclude(params) -class VMInterfaceFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): +class VMInterfaceFilterSet(PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 6fff34d95..96295e84f 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -646,7 +646,7 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm) vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id) + self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) # Limit VLAN choices by virtual machine self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) @@ -668,7 +668,7 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): queryset=VMInterface.objects.all(), required=False, query_params={ - 'virtualmachine_id': 'virtual_machine', + 'virtual_machine_id': '$virtual_machine', } ) mtu = forms.IntegerField( @@ -711,9 +711,6 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): super().__init__(*args, **kwargs) vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') - # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id) - # Limit VLAN choices by virtual machine self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) @@ -796,7 +793,7 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): vm_id = self.initial.get('virtual_machine') # Restrict parent interface assignment by VM - self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id) + self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id) # Limit VLAN choices by virtual machine self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id) diff --git a/netbox/virtualization/tests/test_filters.py b/netbox/virtualization/tests/test_filtersets.py similarity index 94% rename from netbox/virtualization/tests/test_filters.py rename to netbox/virtualization/tests/test_filtersets.py index c11423663..0ca6364a5 100644 --- a/netbox/virtualization/tests/test_filters.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -3,12 +3,13 @@ from django.test import TestCase from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup from ipam.models import IPAddress from tenancy.models import Tenant, TenantGroup +from utilities.testing import ChangeLoggedFilterSetTests from virtualization.choices import * -from virtualization.filters import * +from virtualization.filtersets import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface -class ClusterTypeTestCase(TestCase): +class ClusterTypeTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ClusterType.objects.all() filterset = ClusterTypeFilterSet @@ -22,10 +23,6 @@ class ClusterTypeTestCase(TestCase): ) ClusterType.objects.bulk_create(cluster_types) - 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': ['Cluster Type 1', 'Cluster Type 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -39,7 +36,7 @@ class ClusterTypeTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ClusterGroupTestCase(TestCase): +class ClusterGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = ClusterGroup.objects.all() filterset = ClusterGroupFilterSet @@ -53,10 +50,6 @@ class ClusterGroupTestCase(TestCase): ) ClusterGroup.objects.bulk_create(cluster_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': ['Cluster Group 1', 'Cluster Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -70,7 +63,7 @@ class ClusterGroupTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class ClusterTestCase(TestCase): +class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = Cluster.objects.all() filterset = ClusterFilterSet @@ -136,10 +129,6 @@ class ClusterTestCase(TestCase): ) Cluster.objects.bulk_create(clusters) - 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': ['Cluster 1', 'Cluster 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -194,7 +183,7 @@ class ClusterTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class VirtualMachineTestCase(TestCase): +class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VirtualMachine.objects.all() filterset = VirtualMachineFilterSet @@ -297,10 +286,6 @@ class VirtualMachineTestCase(TestCase): VirtualMachine.objects.filter(pk=vms[0].pk).update(primary_ip4=ipaddresses[0]) VirtualMachine.objects.filter(pk=vms[1].pk).update(primary_ip4=ipaddresses[1]) - 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': ['Virtual Machine 1', 'Virtual Machine 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -409,7 +394,7 @@ class VirtualMachineTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) -class VMInterfaceTestCase(TestCase): +class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VMInterface.objects.all() filterset = VMInterfaceFilterSet @@ -444,11 +429,6 @@ class VMInterfaceTestCase(TestCase): ) VMInterface.objects.bulk_create(interfaces) - def test_id(self): - id_list = self.queryset.values_list('id', flat=True)[:2] - params = {'id': [str(id) for id in id_list]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_name(self): params = {'name': ['Interface 1', 'Interface 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 6b316de0e..421278d6e 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -13,7 +13,7 @@ from netbox.views import generic from secrets.models import Secret from utilities.tables import paginate_table from utilities.utils import count_related -from . import filters, forms, tables +from . import filtersets, forms, tables from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -64,7 +64,7 @@ class ClusterTypeBulkEditView(generic.BulkEditView): queryset = ClusterType.objects.annotate( cluster_count=count_related(Cluster, 'type') ) - filterset = filters.ClusterTypeFilterSet + filterset = filtersets.ClusterTypeFilterSet table = tables.ClusterTypeTable form = forms.ClusterTypeBulkEditForm @@ -125,7 +125,7 @@ class ClusterGroupBulkEditView(generic.BulkEditView): queryset = ClusterGroup.objects.annotate( cluster_count=count_related(Cluster, 'group') ) - filterset = filters.ClusterGroupFilterSet + filterset = filtersets.ClusterGroupFilterSet table = tables.ClusterGroupTable form = forms.ClusterGroupBulkEditForm @@ -148,7 +148,7 @@ class ClusterListView(generic.ObjectListView): vm_count=count_related(VirtualMachine, 'cluster') ) table = tables.ClusterTable - filterset = filters.ClusterFilterSet + filterset = filtersets.ClusterFilterSet filterset_form = forms.ClusterFilterForm @@ -205,14 +205,14 @@ class ClusterBulkImportView(generic.BulkImportView): class ClusterBulkEditView(generic.BulkEditView): queryset = Cluster.objects.prefetch_related('type', 'group', 'site') - filterset = filters.ClusterFilterSet + filterset = filtersets.ClusterFilterSet table = tables.ClusterTable form = forms.ClusterBulkEditForm class ClusterBulkDeleteView(generic.BulkDeleteView): queryset = Cluster.objects.prefetch_related('type', 'group', 'site') - filterset = filters.ClusterFilterSet + filterset = filtersets.ClusterFilterSet table = tables.ClusterTable @@ -304,7 +304,7 @@ class ClusterRemoveDevicesView(generic.ObjectEditView): class VirtualMachineListView(generic.ObjectListView): queryset = VirtualMachine.objects.all() - filterset = filters.VirtualMachineFilterSet + filterset = filtersets.VirtualMachineFilterSet filterset_form = forms.VirtualMachineFilterForm table = tables.VirtualMachineDetailTable template_name = 'virtualization/virtualmachine_list.html' @@ -388,14 +388,14 @@ class VirtualMachineBulkImportView(generic.BulkImportView): class VirtualMachineBulkEditView(generic.BulkEditView): queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role') - filterset = filters.VirtualMachineFilterSet + filterset = filtersets.VirtualMachineFilterSet table = tables.VirtualMachineTable form = forms.VirtualMachineBulkEditForm class VirtualMachineBulkDeleteView(generic.BulkDeleteView): queryset = VirtualMachine.objects.prefetch_related('cluster', 'tenant', 'role') - filterset = filters.VirtualMachineFilterSet + filterset = filtersets.VirtualMachineFilterSet table = tables.VirtualMachineTable @@ -405,7 +405,7 @@ class VirtualMachineBulkDeleteView(generic.BulkDeleteView): class VMInterfaceListView(generic.ObjectListView): queryset = VMInterface.objects.all() - filterset = filters.VMInterfaceFilterSet + filterset = filtersets.VMInterfaceFilterSet filterset_form = forms.VMInterfaceFilterForm table = tables.VMInterfaceTable action_buttons = ('export',) @@ -500,7 +500,7 @@ class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView): form = forms.VMInterfaceBulkCreateForm queryset = VMInterface.objects.all() model_form = forms.VMInterfaceForm - filterset = filters.VirtualMachineFilterSet + filterset = filtersets.VirtualMachineFilterSet table = tables.VirtualMachineTable def get_required_permission(self):