diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 6919ff16f..3c52c973c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -5,21 +5,25 @@ labels: ["type: bug"] body: - type: markdown attributes: - value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a - current NetBox installation. If you're having trouble with installation or just - looking for assistance with using NetBox, please visit our - [discussion forum](https://github.com/netbox-community/netbox/discussions) instead." + value: > + **NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox + installation. If you're having trouble with installation or just looking for + assistance with using NetBox, please visit our + [discussion forum](https://github.com/netbox-community/netbox/discussions) instead. - type: input attributes: label: NetBox version - description: "What version of NetBox are you currently running?" - placeholder: v2.10.4 + description: > + What version of NetBox are you currently running? (If you don't have access to the most + recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) + before opening a bug report to see if your issue has already been addressed.) + placeholder: v2.11.3 validations: required: true - type: dropdown attributes: label: Python version - description: "What version of Python are you currently running?" + description: What version of Python are you currently running? options: - 3.6 - 3.7 @@ -30,12 +34,13 @@ body: - type: textarea attributes: label: Steps to Reproduce - description: "Describe in detail the exact steps that someone else can take to - reproduce this bug using the current stable release of NetBox. Begin with the - creation of any necessary database objects and call out every operation being - performed explicitly. If reporting a bug in the REST API, be sure to reconstruct - the raw HTTP request(s) being made: Don't rely on a client library such as - pynetbox." + description: > + Describe in detail the exact steps that someone else can take to + reproduce this bug using the current stable release of NetBox. Begin with the + creation of any necessary database objects and call out every operation being + performed explicitly. If reporting a bug in the REST API, be sure to reconstruct + the raw HTTP request(s) being made: Don't rely on a client library such as + pynetbox." placeholder: | 1. Click on "create widget" 2. Set foo to 12 and bar to G @@ -45,14 +50,14 @@ body: - type: textarea attributes: label: Expected Behavior - description: "What did you expect to happen?" - placeholder: "A new widget should have been created with the specified attributes" + description: What did you expect to happen? + placeholder: A new widget should have been created with the specified attributes validations: required: true - type: textarea attributes: label: Observed Behavior - description: "What happened instead?" - placeholder: "A TypeError exception was raised" + description: What happened instead? + placeholder: A TypeError exception was raised validations: required: true diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index 19d9696ad..0f87115fc 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -30,6 +30,6 @@ body: - type: textarea attributes: label: Proposed Changes - description: "Describe the proposed changes and why they are necessary" + description: Describe the proposed changes and why they are necessary. validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 7d7bde225..9181f7ce4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -5,15 +5,16 @@ labels: ["type: feature"] body: - type: markdown attributes: - value: "**NOTE:** This form is only for submitting well-formed proposals to extend or - modify NetBox in some way. If you're trying to solve a problem but can't figure out how, - or if you still need time to work on the details of a proposed new feature, please start - a [discussion](https://github.com/netbox-community/netbox/discussions) instead." + value: > + **NOTE:** This form is only for submitting well-formed proposals to extend or modify + NetBox in some way. If you're trying to solve a problem but can't figure out how, or if + you still need time to work on the details of a proposed new feature, please start a + [discussion](https://github.com/netbox-community/netbox/discussions) instead. - type: input attributes: label: NetBox version - description: "What version of NetBox are you currently running?" - placeholder: v2.10.4 + description: What version of NetBox are you currently running? + placeholder: v2.11.3 validations: required: true - type: dropdown @@ -28,26 +29,29 @@ body: - type: textarea attributes: label: Proposed functionality - description: "Describe in detail the new feature or behavior you'd like to propose. - Include any specific changes to work flows, data models, or the user interface." + description: > + Describe in detail the new feature or behavior you'd like to propose. Include any specific + changes to work flows, data models, or the user interface. validations: required: true - type: textarea attributes: label: Use case - description: "Explain how adding this functionality would benefit NetBox users. What - need does it address?" + description: > + Explain how adding this functionality would benefit NetBox users. What need does it address? validations: required: true - type: textarea attributes: label: Database changes - description: "Note any changes to the database schema necessary to support the new - feature. For example, does the proposal require adding a new model or field? (Not - all new features require database changes.)" + description: > + Note any changes to the database schema necessary to support the new feature. For example, + does the proposal require adding a new model or field? (Not all new features require database + changes.) - type: textarea attributes: label: External dependencies - description: "List any new dependencies on external libraries or services that this - new feature would introduce. For example, does the proposal require the installation - of a new Python package? (Not all new features introduce new dependencies.)" + description: > + List any new dependencies on external libraries or services that this new feature would + introduce. For example, does the proposal require the installation of a new Python package? + (Not all new features introduce new dependencies.) diff --git a/.github/ISSUE_TEMPLATE/housekeeping.yaml b/.github/ISSUE_TEMPLATE/housekeeping.yaml index 5e675583e..777871395 100644 --- a/.github/ISSUE_TEMPLATE/housekeeping.yaml +++ b/.github/ISSUE_TEMPLATE/housekeeping.yaml @@ -5,18 +5,20 @@ labels: ["type: housekeeping"] body: - type: markdown attributes: - value: "**NOTE:** This template is for use by maintainers only. Please do not submit - an issue using this template unless you have been specifically asked to do so." + value: > + **NOTE:** This template is for use by maintainers only. Please do not submit + an issue using this template unless you have been specifically asked to do so. - type: textarea attributes: label: Proposed Changes - description: "Describe in detail the new feature or behavior you'd like to propose. - Include any specific changes to work flows, data models, or the user interface." + description: > + Describe in detail the new feature or behavior you'd like to propose. + Include any specific changes to work flows, data models, or the user interface. validations: required: true - type: textarea attributes: label: Justification - description: "Please provide justification for the proposed change(s)." + description: Please provide justification for the proposed change(s). validations: required: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8fc85ead6..0f617e8aa 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,9 +17,10 @@ jobs: necessary. close-pr-message: > This PR has been automatically closed due to lack of activity. - days-before-stale: 45 - days-before-close: 15 + days-before-stale: 60 + days-before-close: 30 exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone' + operations-per-run: 100 remove-stale-when-updated: false stale-issue-label: 'pending closure' stale-issue-message: > diff --git a/README.md b/README.md index 237673e6b..877d8b515 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -![NetBox](docs/netbox_logo.svg "NetBox logo") +
+ NetBox logo +
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at @@ -10,7 +12,21 @@ 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. + +| | status | +|-------------|------------| +| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) | +| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) | + +
+

Thank you to our sponsors!

+ + [![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/) +            + [![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/) + +
### Discussion @@ -18,37 +34,14 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne * [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out * [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions -### Build Status - -| | status | -| ----------- | ------------------------------------------------------------------------------------------------- | -| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) | -| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) | - -### Screenshots - -![Screenshot of Main Page](docs/media/home-light.png "Main Page") - ---- - -![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation") - ---- - -![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy") - ---- - -![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing") - -## Installation +### Installation Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases) and run `upgrade.sh`. -## Providing Feedback +### Providing Feedback The best platform for general feedback, assistance, and other discussion is our [GitHub discussions](https://github.com/netbox-community/netbox/discussions). @@ -58,7 +51,17 @@ the [appropriate template](https://github.com/netbox-community/netbox/issues/new If you are interested in contributing to the development of NetBox, please read our [contributing guide](CONTRIBUTING.md) prior to beginning any work. -## Related projects +### Screenshots + +![Screenshot of Main Page](docs/media/home-light.png "Main Page") + +![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation") + +![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy") + +![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing") + +### Related projects Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for a list of relevant community projects. diff --git a/docs/additional-features/export-templates.md b/docs/additional-features/export-templates.md index b3f585bee..c80d5b8a1 100644 --- a/docs/additional-features/export-templates.md +++ b/docs/additional-features/export-templates.md @@ -33,6 +33,16 @@ The `as_attachment` attribute of an export template controls its behavior when r A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. +## REST API Integration + +When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/optional-settings.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: + +``` +GET /api/dcim/sites/?export=MyTemplateName +``` + +Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. + ## Example Here's an example device export template that will generate a simple Nagios configuration from a list of devices. diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index c3b5b0ce2..058a55695 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/development/release-checklist.md b/docs/development/release-checklist.md index d8cb671f6..91d5ab2ab 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -70,7 +70,11 @@ Ensure that continuous integration testing on the `develop` branch is completing ### Update Version and Changelog -Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. Commit these changes to the `develop` branch. +* Update the `VERSION` constant in `settings.py` to the new release version. +* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`. +* Replace the "FUTURE" placeholder in the release notes with the current date. + +Commit these changes to the `develop` branch. ### Submit a Pull Request diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index a43e354de..0827d5434 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,5 +1,32 @@ # NetBox v2.11 +## v2.11.3 (2021-05-07) + +### 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 +* [#6351](https://github.com/netbox-community/netbox/issues/6351) - Add aggregates count to tenant view +* [#6359](https://github.com/netbox-community/netbox/issues/6359) - Enable custom links for organizational and nested group models + +### 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 +* [#6312](https://github.com/netbox-community/netbox/issues/6312) - Interface device filter should return all virtual chassis interfaces only if device is master +* [#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 +* [#6350](https://github.com/netbox-community/netbox/issues/6350) - Include first & last IP addresses when allocating available IPv6 addresses via the REST API +* [#6355](https://github.com/netbox-community/netbox/issues/6355) - Fix caching error when swapping A/Z circuit terminations +* [#6357](https://github.com/netbox-community/netbox/issues/6357) - Fix ProviderNetwork nested API serializer +* [#6363](https://github.com/netbox-community/netbox/issues/6363) - Correct pre-population of cluster group when creating a cluster +* [#6369](https://github.com/netbox-community/netbox/issues/6369) - Fix interface assignment for VLANs in non-scoped groups + +--- + ## v2.11.2 (2021-04-27) ### Enhancements diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 548868a3d..573cd68fe 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -2,7 +2,38 @@ ## v2.12-beta1 (FUTURE) +### Enhancements + +* [#3665](https://github.com/netbox-community/netbox/issues/3665) - Enable rendering export templates via REST API +* [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized +* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit +* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths + ### Other Changes * [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6 * [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar +* [#6338](https://github.com/netbox-community/netbox/issues/6338) - Decimal fields are no longer coerced to strings in REST API + +### REST API Changes + +* dcim.Cable + * `length` is now a decimal value +* dcim.Device + * Removed the `display_name` attribute (use `display` instead) +* dcim.DeviceType + * Removed the `display_name` attribute (use `display` instead) +* dcim.Rack + * Removed the `display_name` attribute (use `display` instead) +* dcim.Site + * `latitude` and `longitude` are now decimal fields rather than strings +* extras.ContentType + * Removed the `display_name` attribute (use `display` instead) +* ipam.Prefix + * Added the `mark_utilized` boolean field +* ipam.VLAN + * Removed the `display_name` attribute (use `display` instead) +* ipam.VRF + * Removed the `display_name` attribute (use `display` instead) +* virtualization.VirtualMachine + * `vcpus` is now a decimal field rather than a string diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py index fccf4a8b6..6f7cb4f21 100644 --- a/netbox/circuits/api/nested_serializers.py +++ b/netbox/circuits/api/nested_serializers.py @@ -20,7 +20,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') class Meta: - model = Provider + model = ProviderNetwork fields = ['id', 'url', 'display', 'name'] 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/models.py b/netbox/circuits/models.py index b2ffb3c09..31d08537e 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -149,7 +149,7 @@ class ProviderNetwork(PrimaryModel): ) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class CircuitType(OrganizationalModel): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named diff --git a/netbox/circuits/signals.py b/netbox/circuits/signals.py index 0a000fb2e..a12cef671 100644 --- a/netbox/circuits/signals.py +++ b/netbox/circuits/signals.py @@ -1,9 +1,8 @@ from django.db.models.signals import post_delete, post_save from django.dispatch import receiver -from django.utils import timezone from dcim.signals import rebuild_paths -from .models import Circuit, CircuitTermination +from .models import CircuitTermination @receiver(post_save, sender=CircuitTermination) @@ -11,11 +10,9 @@ def update_circuit(instance, **kwargs): """ When a CircuitTermination has been modified, update its parent Circuit. """ - fields = { - 'last_updated': timezone.now(), - f'termination_{instance.term_side.lower()}': instance.pk, - } - Circuit.objects.filter(pk=instance.circuit_id).update(**fields) + termination_name = f'termination_{instance.term_side.lower()}' + setattr(instance.circuit, termination_name, instance) + instance.circuit.save() @receiver((post_save, post_delete), sender=CircuitTermination) 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..b4bb0155e 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 @@ -211,27 +211,6 @@ class CircuitListView(generic.ObjectListView): class CircuitView(generic.ObjectView): queryset = Circuit.objects.all() - def get_extra_context(self, request, instance): - - # A-side termination - termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( - 'site__region' - ).filter( - circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A - ).first() - - # Z-side termination - termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( - 'site__region' - ).filter( - circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z - ).first() - - return { - 'termination_a': termination_a, - 'termination_z': termination_z, - } - class CircuitEditView(generic.ObjectEditView): queryset = Circuit.objects.all() @@ -252,7 +231,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 +240,7 @@ class CircuitBulkDeleteView(generic.BulkDeleteView): queryset = Circuit.objects.prefetch_related( 'provider', 'type', 'tenant', 'terminations' ) - filterset = filters.CircuitFilterSet + filterset = filtersets.CircuitFilterSet table = tables.CircuitTable @@ -296,16 +275,11 @@ class CircuitSwapTerminations(generic.ObjectEditView): if form.is_valid(): - termination_a = CircuitTermination.objects.filter( - circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A - ).first() - termination_z = CircuitTermination.objects.filter( - circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z - ).first() + termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first() + termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first() if termination_a and termination_z: # Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint - print('swapping') with transaction.atomic(): termination_a.term_side = '_' termination_a.save() @@ -316,11 +290,20 @@ class CircuitSwapTerminations(generic.ObjectEditView): elif termination_a: termination_a.term_side = 'Z' termination_a.save() + circuit.refresh_from_db() + circuit.termination_a = None + circuit.save() else: termination_z.term_side = 'A' termination_z.save() + circuit.refresh_from_db() + circuit.termination_z = None + circuit.save() - messages.success(request, "Swapped terminations for circuit {}.".format(circuit)) + print(f'term A: {circuit.termination_a}') + print(f'term Z: {circuit.termination_z}') + + messages.success(request, f"Swapped terminations for circuit {circuit}.") return redirect('circuits:circuit', pk=circuit.pk) return render(request, 'circuits/circuit_terminations_swap.html', { diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 80e003efc..67ae9b046 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -101,7 +101,7 @@ class NestedRackSerializer(WritableNestedSerializer): class Meta: model = models.Rack - fields = ['id', 'url', 'display', 'name', 'display_name', 'device_count'] + fields = ['id', 'url', 'display', 'name', 'device_count'] class NestedRackReservationSerializer(WritableNestedSerializer): @@ -136,7 +136,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer): class Meta: model = models.DeviceType - fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'device_count'] + fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count'] class NestedConsolePortTemplateSerializer(WritableNestedSerializer): @@ -232,7 +232,7 @@ class NestedDeviceSerializer(WritableNestedSerializer): class Meta: model = models.Device - fields = ['id', 'url', 'display', 'name', 'display_name'] + fields = ['id', 'url', 'display', 'name'] class NestedConsoleServerPortSerializer(WritableNestedSerializer): diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 377449140..d2ecad608 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -172,10 +172,9 @@ class RackSerializer(PrimaryModelSerializer): class Meta: model = Rack fields = [ - 'id', 'url', 'display', 'name', 'facility_id', 'display_name', 'site', 'location', 'tenant', 'status', - 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', - 'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', - 'powerfeed_count', + 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', + 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', ] # Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This # prevents facility_id from being interpreted as a required field. @@ -284,9 +283,9 @@ class DeviceTypeSerializer(PrimaryModelSerializer): class Meta: model = DeviceType fields = [ - 'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', - 'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'device_count', + 'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', + 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', 'device_count', ] @@ -465,10 +464,10 @@ class DeviceSerializer(PrimaryModelSerializer): class Meta: model = Device fields = [ - 'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', - 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', - 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', - 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', + 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', + 'tags', 'custom_fields', 'created', 'last_updated', ] validators = [] @@ -501,10 +500,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): class Meta(DeviceSerializer.Meta): fields = [ - 'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', - 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', - 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', - 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', + 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', + 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', ] @swagger_serializer_method(serializer_or_field=serializers.DictField) 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..be35a24c5 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)'), @@ -1062,14 +1064,21 @@ class CableStatusChoices(ChoiceSet): class CableLengthUnitChoices(ChoiceSet): + # Metric + UNIT_KILOMETER = 'km' UNIT_METER = 'm' UNIT_CENTIMETER = 'cm' + + # Imperial + UNIT_MILE = 'mi' UNIT_FOOT = 'ft' UNIT_INCH = 'in' CHOICES = ( + (UNIT_KILOMETER, 'Kilometers'), (UNIT_METER, 'Meters'), (UNIT_CENTIMETER, 'Centimeters'), + (UNIT_MILE, 'Miles'), (UNIT_FOOT, 'Feet'), (UNIT_INCH, 'Inches'), ) diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py index 8571c6684..473b9608f 100644 --- a/netbox/dcim/elevations.py +++ b/netbox/dcim/elevations.py @@ -34,10 +34,11 @@ class RackElevationSVG: @staticmethod def _get_device_description(device): - return '{} ({}) — {} ({}U) {} {}'.format( + return '{} ({}) — {} {} ({}U) {} {}'.format( device.name, device.device_role, - device.device_type.display_name, + device.device_type.manufacturer.name, + device.device_type.model, device.device_type.u_height, device.asset_tag or '', device.serial or '' 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/forms.py b/netbox/dcim/forms.py index 16e909895..ab8475431 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2153,7 +2153,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ip_choices = [(None, '---------')] # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member - interface_ids = self.instance.vc_interfaces().values_list('pk', flat=True) + interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True) # Collect interface IPs interface_ips = IPAddress.objects.filter( diff --git a/netbox/dcim/migrations/0132_cable_length.py b/netbox/dcim/migrations/0132_cable_length.py new file mode 100644 index 000000000..e20a8b8aa --- /dev/null +++ b/netbox/dcim/migrations/0132_cable_length.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0131_consoleport_speed'), + ] + + operations = [ + migrations.AlterField( + model_name='cable', + name='length', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 28d21ff68..2b8f05206 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -74,7 +74,9 @@ class Cable(PrimaryModel): color = ColorField( blank=True ) - length = models.PositiveSmallIntegerField( + length = models.DecimalField( + max_digits=8, + decimal_places=2, blank=True, null=True ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index a473d83b1..3102eacab 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -36,7 +36,7 @@ __all__ = ( # Device Types # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Manufacturer(OrganizationalModel): """ A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. @@ -320,10 +320,6 @@ class DeviceType(PrimaryModel): if self.rear_image: self.rear_image.delete(save=False) - @property - def display_name(self): - return f'{self.manufacturer.name} {self.model}' - @property def is_parent_device(self): return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT @@ -337,7 +333,7 @@ class DeviceType(PrimaryModel): # Devices # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class DeviceRole(OrganizationalModel): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a @@ -388,7 +384,7 @@ class DeviceRole(OrganizationalModel): ) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Platform(OrganizationalModel): """ Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". @@ -616,7 +612,13 @@ class Device(PrimaryModel, ConfigContextModel): ) def __str__(self): - return self.display_name or super().__str__() + if self.name: + return self.name + elif self.virtual_chassis: + return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})' + elif self.device_type: + return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})' + return super().__str__() def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) @@ -710,7 +712,7 @@ class Device(PrimaryModel, ConfigContextModel): pass # Validate primary IP addresses - vc_interfaces = self.vc_interfaces() + vc_interfaces = self.vc_interfaces(if_master=False) if self.primary_ip4: if self.primary_ip4.family != 4: raise ValidationError({ @@ -817,17 +819,6 @@ class Device(PrimaryModel, ConfigContextModel): self.comments, ) - @property - def display_name(self): - if self.name: - return self.name - elif self.virtual_chassis: - return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})' - elif self.device_type: - return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})' - else: - return '' # Device has not yet been created - @property def identifier(self): """ @@ -850,9 +841,7 @@ class Device(PrimaryModel, ConfigContextModel): @property def interfaces_count(self): - if self.virtual_chassis and self.virtual_chassis.master == self: - return self.vc_interfaces().count() - return self.interfaces.count() + return self.vc_interfaces().count() def get_vc_master(self): """ @@ -860,7 +849,7 @@ class Device(PrimaryModel, ConfigContextModel): """ return self.virtual_chassis.master if self.virtual_chassis else None - def vc_interfaces(self, if_master=False): + def vc_interfaces(self, if_master=True): """ Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another Device belonging to the same VirtualChassis. @@ -868,7 +857,7 @@ class Device(PrimaryModel, ConfigContextModel): :param if_master: If True, return VC member interfaces only if this Device is the VC master. """ filter = Q(device=self) - if self.virtual_chassis and (not if_master or self.virtual_chassis.master == self): + if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master): filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) return Interface.objects.filter(filter) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 2869c4265..3c63c1a3c 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -35,7 +35,7 @@ __all__ = ( # Racks # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class RackRole(OrganizationalModel): """ Racks can be organized by functional role, similar to Devices. @@ -209,7 +209,9 @@ class Rack(PrimaryModel): ) def __str__(self): - return self.display_name or super().__str__() + if self.facility_id: + return f'{self.name} ({self.facility_id})' + return self.name def get_absolute_url(self): return reverse('dcim:rack', args=[self.pk]) @@ -277,12 +279,6 @@ class Rack(PrimaryModel): else: return reversed(range(1, self.u_height + 1)) - @property - def display_name(self): - if self.facility_id: - return f'{self.name} ({self.facility_id})' - return self.name - def get_status_class(self): return RackStatusChoices.CSS_CLASSES.get(self.status) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 225a8e749..1e5165088 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -26,7 +26,7 @@ __all__ = ( # Regions # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Region(NestedGroupModel): """ A region represents a geographic collection of sites. For example, you might create regions representing countries, @@ -78,7 +78,7 @@ class Region(NestedGroupModel): # Site groups # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class SiteGroup(NestedGroupModel): """ A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and @@ -285,7 +285,7 @@ class Site(PrimaryModel): # Locations # -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Location(NestedGroupModel): """ A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a 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_api.py b/netbox/dcim/tests/test_api.py index 24130c649..92ffd1207 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -251,7 +251,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase): class RackTest(APIViewTestCases.APIViewTestCase): model = Rack - brief_fields = ['device_count', 'display', 'display_name', 'id', 'name', 'url'] + brief_fields = ['device_count', 'display', 'id', 'name', 'url'] bulk_update_data = { 'status': 'planned', } @@ -422,7 +422,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase): class DeviceTypeTest(APIViewTestCases.APIViewTestCase): model = DeviceType - brief_fields = ['device_count', 'display', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url'] + brief_fields = ['device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url'] bulk_update_data = { 'part_number': 'ABC123', } @@ -870,7 +870,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase): class DeviceTest(APIViewTestCases.APIViewTestCase): model = Device - brief_fields = ['display', 'display_name', 'id', 'name', 'url'] + brief_fields = ['display', 'id', 'name', 'url'] bulk_update_data = { 'status': 'failed', } 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 4a7c5a941..26b25b8ea 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -23,7 +23,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 ( @@ -106,7 +106,7 @@ class RegionListView(generic.ObjectListView): 'site_count', cumulative=True ) - filterset = filters.RegionFilterSet + filterset = filtersets.RegionFilterSet filterset_form = forms.RegionFilterForm table = tables.RegionTable @@ -162,7 +162,7 @@ class RegionBulkEditView(generic.BulkEditView): 'site_count', cumulative=True ) - filterset = filters.RegionFilterSet + filterset = filtersets.RegionFilterSet table = tables.RegionTable form = forms.RegionBulkEditForm @@ -175,7 +175,7 @@ class RegionBulkDeleteView(generic.BulkDeleteView): 'site_count', cumulative=True ) - filterset = filters.RegionFilterSet + filterset = filtersets.RegionFilterSet table = tables.RegionTable @@ -191,7 +191,7 @@ class SiteGroupListView(generic.ObjectListView): 'site_count', cumulative=True ) - filterset = filters.SiteGroupFilterSet + filterset = filtersets.SiteGroupFilterSet filterset_form = forms.SiteGroupFilterForm table = tables.SiteGroupTable @@ -247,7 +247,7 @@ class SiteGroupBulkEditView(generic.BulkEditView): 'site_count', cumulative=True ) - filterset = filters.SiteGroupFilterSet + filterset = filtersets.SiteGroupFilterSet table = tables.SiteGroupTable form = forms.SiteGroupBulkEditForm @@ -260,7 +260,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView): 'site_count', cumulative=True ) - filterset = filters.SiteGroupFilterSet + filterset = filtersets.SiteGroupFilterSet table = tables.SiteGroupTable @@ -270,7 +270,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 @@ -325,14 +325,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 @@ -354,7 +354,7 @@ class LocationListView(generic.ObjectListView): 'rack_count', cumulative=True ) - filterset = filters.LocationFilterSet + filterset = filtersets.LocationFilterSet filterset_form = forms.LocationFilterForm table = tables.LocationTable @@ -413,7 +413,7 @@ class LocationBulkEditView(generic.BulkEditView): 'rack_count', cumulative=True ).prefetch_related('site') - filterset = filters.LocationFilterSet + filterset = filtersets.LocationFilterSet table = tables.LocationTable form = forms.LocationBulkEditForm @@ -426,7 +426,7 @@ class LocationBulkDeleteView(generic.BulkDeleteView): 'rack_count', cumulative=True ).prefetch_related('site') - filterset = filters.LocationFilterSet + filterset = filtersets.LocationFilterSet table = tables.LocationTable @@ -477,7 +477,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 @@ -499,7 +499,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 @@ -512,7 +512,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 @@ -601,14 +601,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 @@ -618,7 +618,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 @@ -661,14 +661,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 @@ -691,6 +691,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) @@ -721,7 +723,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 @@ -741,7 +743,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 @@ -847,7 +849,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 @@ -856,7 +858,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 @@ -1189,7 +1191,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 @@ -1248,7 +1250,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 @@ -1264,7 +1266,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' @@ -1400,7 +1402,7 @@ class DeviceInterfacesView(generic.ObjectView): template_name = 'dcim/device/interfaces.html' def get_extra_context(self, request, instance): - interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related( + interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), 'lag', 'cable', '_path__destination', 'tags', @@ -1522,7 +1524,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView): template_name = 'dcim/device/lldp_neighbors.html' def get_extra_context(self, request, instance): - interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related( + interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( '_path__destination' ).exclude( type__in=NONCONNECTABLE_IFACE_TYPES @@ -1595,14 +1597,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 @@ -1612,7 +1614,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') @@ -1647,7 +1649,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 @@ -1662,7 +1664,7 @@ class ConsolePortBulkDisconnectView(BulkDisconnectView): class ConsolePortBulkDeleteView(generic.BulkDeleteView): queryset = ConsolePort.objects.all() - filterset = filters.ConsolePortFilterSet + filterset = filtersets.ConsolePortFilterSet table = tables.ConsolePortTable @@ -1672,7 +1674,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') @@ -1707,7 +1709,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 @@ -1722,7 +1724,7 @@ class ConsoleServerPortBulkDisconnectView(BulkDisconnectView): class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView): queryset = ConsoleServerPort.objects.all() - filterset = filters.ConsoleServerPortFilterSet + filterset = filtersets.ConsoleServerPortFilterSet table = tables.ConsoleServerPortTable @@ -1732,7 +1734,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') @@ -1767,7 +1769,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 @@ -1782,7 +1784,7 @@ class PowerPortBulkDisconnectView(BulkDisconnectView): class PowerPortBulkDeleteView(generic.BulkDeleteView): queryset = PowerPort.objects.all() - filterset = filters.PowerPortFilterSet + filterset = filtersets.PowerPortFilterSet table = tables.PowerPortTable @@ -1792,7 +1794,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') @@ -1827,7 +1829,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 @@ -1842,7 +1844,7 @@ class PowerOutletBulkDisconnectView(BulkDisconnectView): class PowerOutletBulkDeleteView(generic.BulkDeleteView): queryset = PowerOutlet.objects.all() - filterset = filters.PowerOutletFilterSet + filterset = filtersets.PowerOutletFilterSet table = tables.PowerOutletTable @@ -1852,7 +1854,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') @@ -1922,7 +1924,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 @@ -1937,7 +1939,7 @@ class InterfaceBulkDisconnectView(BulkDisconnectView): class InterfaceBulkDeleteView(generic.BulkDeleteView): queryset = Interface.objects.all() - filterset = filters.InterfaceFilterSet + filterset = filtersets.InterfaceFilterSet table = tables.InterfaceTable @@ -1947,7 +1949,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') @@ -1982,7 +1984,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 @@ -1997,7 +1999,7 @@ class FrontPortBulkDisconnectView(BulkDisconnectView): class FrontPortBulkDeleteView(generic.BulkDeleteView): queryset = FrontPort.objects.all() - filterset = filters.FrontPortFilterSet + filterset = filtersets.FrontPortFilterSet table = tables.FrontPortTable @@ -2007,7 +2009,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') @@ -2042,7 +2044,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 @@ -2057,7 +2059,7 @@ class RearPortBulkDisconnectView(BulkDisconnectView): class RearPortBulkDeleteView(generic.BulkDeleteView): queryset = RearPort.objects.all() - filterset = filters.RearPortFilterSet + filterset = filtersets.RearPortFilterSet table = tables.RearPortTable @@ -2067,7 +2069,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') @@ -2167,7 +2169,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 @@ -2178,7 +2180,7 @@ class DeviceBayBulkRenameView(generic.BulkRenameView): class DeviceBayBulkDeleteView(generic.BulkDeleteView): queryset = DeviceBay.objects.all() - filterset = filters.DeviceBayFilterSet + filterset = filtersets.DeviceBayFilterSet table = tables.DeviceBayTable @@ -2188,7 +2190,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') @@ -2222,7 +2224,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 @@ -2247,7 +2249,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' @@ -2258,7 +2260,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' @@ -2269,7 +2271,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' @@ -2280,7 +2282,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' @@ -2291,7 +2293,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' @@ -2302,7 +2304,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' @@ -2313,7 +2315,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' @@ -2324,7 +2326,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' @@ -2335,7 +2337,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' @@ -2346,7 +2348,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') @@ -2479,14 +2481,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 @@ -2496,7 +2498,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' @@ -2526,7 +2528,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' @@ -2560,7 +2562,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' @@ -2599,7 +2601,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 @@ -2807,14 +2809,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 @@ -2828,7 +2830,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 @@ -2868,7 +2870,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 @@ -2879,7 +2881,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView): ).annotate( powerfeed_count=count_related(PowerFeed, 'power_panel') ) - filterset = filters.PowerPanelFilterSet + filterset = filtersets.PowerPanelFilterSet table = tables.PowerPanelTable @@ -2889,7 +2891,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 @@ -2915,7 +2917,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 @@ -2926,5 +2928,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/serializers.py b/netbox/extras/api/serializers.py index 66627bfbc..536df1c75 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -453,12 +453,7 @@ class ObjectChangeSerializer(BaseModelSerializer): class ContentTypeSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') - display_name = serializers.SerializerMethodField() class Meta: model = ContentType - fields = ['id', 'url', 'display', 'app_label', 'model', 'display_name'] - - @swagger_serializer_method(serializer_or_field=serializers.CharField) - def get_display_name(self, obj): - return obj.app_labeled_name + fields = ['id', 'url', 'display', 'app_label', 'model'] 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/models/models.py b/netbox/extras/models/models.py index c2cebe163..ab9cbe9f3 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -300,13 +300,12 @@ class ExportTemplate(BigIDModel): # Build the response response = HttpResponse(output, content_type=mime_type) - filename = 'netbox_{}{}'.format( - queryset.model._meta.verbose_name_plural, - '.{}'.format(self.file_extension) if self.file_extension else '' - ) if self.as_attachment: - response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) + basename = queryset.model._meta.verbose_name_plural.replace(' ', '_') + extension = f'.{self.file_extension}' if self.file_extension else '' + filename = f'netbox_{basename}{extension}' + response['Content-Disposition'] = f'attachment; filename="{filename}"' return response 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/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index a5eb63911..f2aa5f19e 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/netbox/ipam/api/nested_serializers.py @@ -27,7 +27,7 @@ class NestedVRFSerializer(WritableNestedSerializer): class Meta: model = models.VRF - fields = ['id', 'url', 'display', 'name', 'rd', 'display_name', 'prefix_count'] + fields = ['id', 'url', 'display', 'name', 'rd', 'prefix_count'] # @@ -92,7 +92,7 @@ class NestedVLANSerializer(WritableNestedSerializer): class Meta: model = models.VLAN - fields = ['id', 'url', 'display', 'vid', 'name', 'display_name'] + fields = ['id', 'url', 'display', 'vid', 'name'] # diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 931e2cc47..203cdc3fb 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -44,8 +44,7 @@ class VRFSerializer(PrimaryModelSerializer): model = VRF fields = [ 'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', - 'export_targets', 'tags', 'display_name', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', - 'prefix_count', + 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count', ] @@ -167,7 +166,7 @@ class VLANSerializer(PrimaryModelSerializer): model = VLAN fields = [ 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', - 'display_name', 'custom_fields', 'created', 'last_updated', 'prefix_count', + 'custom_fields', 'created', 'last_updated', 'prefix_count', ] validators = [] @@ -203,7 +202,7 @@ class PrefixSerializer(PrimaryModelSerializer): model = Prefix fields = [ 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] read_only_fields = ['family'] 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..63165d8d2 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', @@ -304,7 +304,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet class Meta: model = Prefix - fields = ['id', 'is_pool'] + fields = ['id', 'is_pool', 'mark_utilized'] def search(self, queryset, name, value): if not value.strip(): @@ -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/forms.py b/netbox/ipam/forms.py index 6a3753859..eed92dffd 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -454,11 +454,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class Meta: model = Prefix fields = [ - 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant', - 'tags', + 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', + 'tenant_group', 'tenant', 'tags', ] fieldsets = ( - ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')), + ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), ('Tenancy', ('tenant_group', 'tenant')), ) @@ -582,6 +582,11 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF widget=BulkEditNullBooleanSelect(), label='Is a pool' ) + mark_utilized = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label='Treat as 100% utilized' + ) description = forms.CharField( max_length=100, required=False @@ -597,7 +602,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) model = Prefix field_order = [ 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id', - 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', + 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized', ] mask_length__lte = forms.IntegerField( widget=forms.HiddenInput() @@ -675,6 +680,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + mark_utilized = forms.NullBooleanField( + required=False, + label=_('Marked as 100% utilized'), + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) tag = TagFilterField(model) diff --git a/netbox/ipam/migrations/0047_prefix_mark_utilized.py b/netbox/ipam/migrations/0047_prefix_mark_utilized.py new file mode 100644 index 000000000..332066b04 --- /dev/null +++ b/netbox/ipam/migrations/0047_prefix_mark_utilized.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0046_set_vlangroup_scope_types'), + ] + + operations = [ + migrations.AddField( + model_name='prefix', + name='mark_utilized', + field=models.BooleanField(default=False), + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index b11a88d54..cf469c930 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -29,7 +29,7 @@ __all__ = ( ) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class RIR(OrganizationalModel): """ A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address @@ -184,7 +184,7 @@ class Aggregate(PrimaryModel): return int(float(child_prefixes.size) / self.prefix.size * 100) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Role(OrganizationalModel): """ A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or @@ -288,6 +288,10 @@ class Prefix(PrimaryModel): default=False, help_text='All IP addresses within this prefix are considered usable' ) + mark_utilized = models.BooleanField( + default=False, + help_text="Treat as 100% utilized" + ) description = models.CharField( max_length=200, blank=True @@ -296,10 +300,11 @@ class Prefix(PrimaryModel): objects = PrefixQuerySet.as_manager() csv_headers = [ - 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description', + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', + 'description', ] clone_fields = [ - 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', ] class Meta: @@ -364,6 +369,7 @@ class Prefix(PrimaryModel): self.get_status_display(), self.role.name if self.role else None, self.is_pool, + self.mark_utilized, self.description, ) @@ -422,23 +428,18 @@ class Prefix(PrimaryModel): """ Return all available IPs within this prefix as an IPSet. """ + if self.mark_utilized: + return list() + prefix = netaddr.IPSet(self.prefix) child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]) available_ips = prefix - child_ips - # All IP addresses within a pool are considered usable - if self.is_pool: + # IPv6, pool, or IPv4 /31 sets are fully usable + if self.family == 6 or self.is_pool or self.prefix.prefixlen == 31: return available_ips - # All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable - if ( - self.prefix.version == 4 and self.prefix.prefixlen == 31 # RFC 3021 - ) or ( - self.prefix.version == 6 and self.prefix.prefixlen == 127 # RFC 6164 - ): - return available_ips - - # Omit first and last IP address from the available set + # For "normal" IPv4 prefixes, omit first and last addresses available_ips -= netaddr.IPSet([ netaddr.IPAddress(self.prefix.first), netaddr.IPAddress(self.prefix.last), @@ -469,6 +470,9 @@ class Prefix(PrimaryModel): Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of "container", calculate utilization based on child prefixes. For all others, count child IP addresses. """ + if self.mark_utilized: + return 100 + if self.status == PrefixStatusChoices.STATUS_CONTAINER: queryset = Prefix.objects.filter( prefix__net_contained=str(self.prefix), diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index d0f5375e2..b4964e761 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -21,7 +21,7 @@ __all__ = ( ) -@extras_features('custom_fields', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class VLANGroup(OrganizationalModel): """ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. @@ -172,7 +172,7 @@ class VLAN(PrimaryModel): verbose_name_plural = 'VLANs' def __str__(self): - return self.display_name or super().__str__() + return f'{self.name} ({self.vid})' def get_absolute_url(self): return reverse('ipam:vlan', args=[self.pk]) @@ -199,10 +199,6 @@ class VLAN(PrimaryModel): self.description, ) - @property - def display_name(self): - return f'{self.name} ({self.vid})' - def get_status_class(self): return VLANStatusChoices.CSS_CLASSES.get(self.status) diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py index 9eb2c6ab6..f674a7f65 100644 --- a/netbox/ipam/models/vrfs.py +++ b/netbox/ipam/models/vrfs.py @@ -71,7 +71,9 @@ class VRF(PrimaryModel): verbose_name_plural = 'VRFs' def __str__(self): - return self.display_name or super().__str__() + if self.rd: + return f'{self.name} ({self.rd})' + return self.name def get_absolute_url(self): return reverse('ipam:vrf', args=[self.pk]) @@ -85,12 +87,6 @@ class VRF(PrimaryModel): self.description, ) - @property - def display_name(self): - if self.rd: - return f'{self.name} ({self.rd})' - return self.name - @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class RouteTarget(PrimaryModel): diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 1a723421d..784d58342 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -64,6 +64,7 @@ class VLANQuerySet(RestrictedQuerySet): return self.filter( Q(group__in=VLANGroup.objects.filter(q)) | Q(site=device.site) | + Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs ) @@ -104,6 +105,7 @@ class VLANQuerySet(RestrictedQuerySet): # Return all applicable VLANs q = ( Q(group__in=vlan_groups) | + Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs ) if vm.cluster.site: diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 0bbaddb52..e0f63a5ca 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -256,6 +256,21 @@ class RoleTable(BaseTable): # Prefixes # +class PrefixUtilizationColumn(UtilizationColumn): + """ + Extend UtilizationColumn to allow disabling the warning & danger thresholds for prefixes + marked as fully utilized. + """ + template_code = """ + {% load helpers %} + {% if record.pk and record.mark_utilized %} + {% utilization_graph value warning_threshold=0 danger_threshold=0 %} + {% elif record.pk %} + {% utilization_graph value %} + {% endif %} + """ + + class PrefixTable(BaseTable): pk = ToggleColumn() prefix = tables.TemplateColumn( @@ -283,11 +298,15 @@ class PrefixTable(BaseTable): is_pool = BooleanColumn( verbose_name='Pool' ) + mark_utilized = BooleanColumn( + verbose_name='Marked Utilized' + ) class Meta(BaseTable.Meta): model = Prefix fields = ( - 'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description', + 'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'mark_utilized', + 'description', ) default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') row_attrs = { @@ -296,7 +315,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): - utilization = UtilizationColumn( + utilization = PrefixUtilizationColumn( accessor='get_utilization', orderable=False ) @@ -308,7 +327,7 @@ class PrefixDetailTable(PrefixTable): class Meta(PrefixTable.Meta): fields = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', - 'description', 'tags', + 'mark_utilized', 'description', 'tags', ) default_columns = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index b2b4b9e8f..0de07b06b 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -22,7 +22,7 @@ class AppTest(APITestCase): class VRFTest(APIViewTestCases.APIViewTestCase): model = VRF - brief_fields = ['display', 'display_name', 'id', 'name', 'prefix_count', 'rd', 'url'] + brief_fields = ['display', 'id', 'name', 'prefix_count', 'rd', 'url'] create_data = [ { 'name': 'VRF 4', @@ -421,7 +421,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase): class VLANTest(APIViewTestCases.APIViewTestCase): model = VLAN - brief_fields = ['display', 'display_name', 'id', 'name', 'url', 'vid'] + brief_fields = ['display', 'id', 'name', 'url', 'vid'] bulk_update_data = { 'description': 'New description', } 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..e668215ad 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 @@ -408,11 +389,11 @@ class PrefixTestCase(TestCase): Tenant.objects.bulk_create(tenants) prefixes = ( - Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), - Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), @@ -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) @@ -440,6 +417,12 @@ class PrefixTestCase(TestCase): params = {'is_pool': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + def test_mark_utilized(self): + params = {'mark_utilized': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'mark_utilized': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + def test_within(self): params = {'within': '10.0.0.0/16'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) @@ -528,7 +511,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 +590,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 +687,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 +730,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 +771,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 +940,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 +1012,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 +1051,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/api/views.py b/netbox/netbox/api/views.py index 92c73840f..46aa429bd 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -5,9 +5,11 @@ from collections import OrderedDict from django import __version__ as DJANGO_VERSION from django.apps import apps from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import ProtectedError +from django.shortcuts import get_object_or_404 from django_rq.queues import get_connection from rest_framework import status from rest_framework.response import Response @@ -16,6 +18,7 @@ from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet as ModelViewSet_ from rq.worker import Worker +from extras.models import ExportTemplate from netbox.api import BulkOperationSerializer from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.exceptions import SerializerNotFound @@ -222,6 +225,18 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): # Check that the instance is matched by the view's queryset self.queryset.get(pk=instance.pk) + def list(self, request, *args, **kwargs): + """ + Overrides ListModelMixin to allow processing ExportTemplates. + """ + if 'export' in request.GET: + content_type = ContentType.objects.get_for_model(self.serializer_class.Meta.model) + et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export']) + queryset = self.filter_queryset(self.get_queryset()) + return et.render_to_response(queryset) + + return super().list(request, *args, **kwargs) + def perform_create(self, serializer): model = self.queryset.model logger = logging.getLogger('netbox.api.views.ModelViewSet') diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 46c32415f..9b80270d2 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 63f50b7ff..01d73737e 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,14 +12,14 @@ 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 tenancy.filters 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 de1213e1c..72a4dc92e 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') @@ -461,6 +462,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null' REST_FRAMEWORK_VERSION = VERSION.rsplit('.', 1)[0] # Use major.minor as API version REST_FRAMEWORK = { 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], + 'COERCE_DECIMAL_TO_STRING': False, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'netbox.api.authentication.TokenAuthentication', diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index 64bd30754..d54c79e10 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -104,12 +104,13 @@ class HomeView(View): for section_label, section_items in sections: stat = {"label": section_label, "items": []} for perm, item_label, description, get_count in section_items: + app, scope = perm.split(".") + url = ":".join((app, scope.replace("view_", "") + "_list")) + item = {"label": item_label, "description": description, "count": None, "url": url, "disabled": True} if perm in perms: - app, scope = perm.split(".") - url = ":".join((app, scope.replace("view_", "") + "_list")) - stat["items"].append( - {"label": item_label, "description": description, "count": get_count(), "url": url} - ) + item["count"] = get_count() + item["disabled"] = False + stat["items"].append(item) stats.append(stat) return stats diff --git a/netbox/project-static/_dark.scss b/netbox/project-static/_dark.scss new file mode 100644 index 000000000..9de45c1e7 --- /dev/null +++ b/netbox/project-static/_dark.scss @@ -0,0 +1,10 @@ +// Entry for netbox-dark.css stylesheet. + +body[data-netbox-color-mode='dark'] { + // Imports are scoped under the body when its data-netbox-color-mode attribute is set to 'dark'. + @import './theme-dark.scss'; + @import './bootstrap.scss'; + @import './select.scss'; + @import './flatpickr-dark.scss'; + @import './netbox.scss'; +} diff --git a/netbox/project-static/rack_elevation.scss b/netbox/project-static/_elevations.scss similarity index 97% rename from netbox/project-static/rack_elevation.scss rename to netbox/project-static/_elevations.scss index 6962e7763..859142a4c 100644 --- a/netbox/project-static/rack_elevation.scss +++ b/netbox/project-static/_elevations.scss @@ -1,4 +1,5 @@ -// Stylesheet for rendering SVG rack elevations +// Entry for rack_elevation.css stylesheet. + @import './theme-light.scss'; * { diff --git a/netbox/project-static/_external.scss b/netbox/project-static/_external.scss new file mode 100644 index 000000000..ae37789c0 --- /dev/null +++ b/netbox/project-static/_external.scss @@ -0,0 +1,4 @@ +// Entry for all 3rd party library imports that do not rely on Bootstrap or NetBox styles. + +@import '@mdi/font/css/materialdesignicons.min.css'; +@import 'flatpickr/dist/flatpickr.css'; diff --git a/netbox/project-static/_light.scss b/netbox/project-static/_light.scss new file mode 100644 index 000000000..af0bbaab8 --- /dev/null +++ b/netbox/project-static/_light.scss @@ -0,0 +1,6 @@ +// Entry for netbox-light.css stylesheet. + +@import './theme-light.scss'; +@import './bootstrap.scss'; +@import './select.scss'; +@import './netbox.scss'; diff --git a/netbox/project-static/bundle.js b/netbox/project-static/bundle.js index e60a8fdfd..b12cb4261 100644 --- a/netbox/project-static/bundle.js +++ b/netbox/project-static/bundle.js @@ -27,8 +27,10 @@ if (args.includes('--no-cache')) { // Style (SCSS) bundle jobs. Generally, everything should be bundled into netbox.css from main.scss // unless there is a specific reason to do otherwise. const styles = [ - ['main.scss', 'netbox.css'], - ['rack_elevation.scss', 'rack_elevation.css'], + ['_external.scss', 'netbox-external.css'], + ['_light.scss', 'netbox-light.css'], + ['_dark.scss', 'netbox-dark.css'], + ['_elevations.scss', 'rack_elevation.css'], ]; // Script (JavaScript) bundle jobs. Generally, everything should be bundled into netbox.js from diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css new file mode 100644 index 000000000..b5e7e841b Binary files /dev/null and b/netbox/project-static/dist/netbox-dark.css differ diff --git a/netbox/project-static/dist/netbox-dark.css.map b/netbox/project-static/dist/netbox-dark.css.map new file mode 100644 index 000000000..e9e7d7459 --- /dev/null +++ b/netbox/project-static/dist/netbox-dark.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["node_modules/bootstrap/scss/_type.scss","node_modules/bootstrap/scss/_root.scss","node_modules/bootstrap/scss/_reboot.scss","node_modules/bootstrap/scss/_variables.scss","node_modules/bootstrap/scss/vendor/_rfs.scss","theme-base.scss","node_modules/bootstrap/scss/mixins/_border-radius.scss","node_modules/bootstrap/scss/mixins/_lists.scss","node_modules/bootstrap/scss/_images.scss","node_modules/bootstrap/scss/mixins/_image.scss","node_modules/bootstrap/scss/_containers.scss","node_modules/bootstrap/scss/mixins/_container.scss","node_modules/bootstrap/scss/mixins/_breakpoints.scss","node_modules/bootstrap/scss/_grid.scss","node_modules/bootstrap/scss/mixins/_grid.scss","node_modules/bootstrap/scss/_tables.scss","node_modules/bootstrap/scss/mixins/_table-variants.scss","node_modules/bootstrap/scss/forms/_labels.scss","node_modules/bootstrap/scss/forms/_form-text.scss","node_modules/bootstrap/scss/forms/_form-control.scss","node_modules/bootstrap/scss/mixins/_transition.scss","theme-dark.scss","node_modules/bootstrap/scss/mixins/_gradients.scss","node_modules/bootstrap/scss/forms/_form-select.scss","node_modules/bootstrap/scss/forms/_form-check.scss","node_modules/bootstrap/scss/forms/_form-range.scss","node_modules/bootstrap/scss/forms/_floating-labels.scss","node_modules/bootstrap/scss/forms/_input-group.scss","node_modules/bootstrap/scss/mixins/_forms.scss","node_modules/bootstrap/scss/_buttons.scss","node_modules/bootstrap/scss/mixins/_buttons.scss","node_modules/bootstrap/scss/_transitions.scss","node_modules/bootstrap/scss/_dropdown.scss","node_modules/bootstrap/scss/mixins/_caret.scss","node_modules/bootstrap/scss/_button-group.scss","node_modules/bootstrap/scss/_nav.scss","node_modules/bootstrap/scss/_navbar.scss","node_modules/bootstrap/scss/_card.scss","node_modules/bootstrap/scss/_accordion.scss","node_modules/bootstrap/scss/_breadcrumb.scss","node_modules/bootstrap/scss/_pagination.scss","node_modules/bootstrap/scss/mixins/_pagination.scss","node_modules/bootstrap/scss/_badge.scss","node_modules/bootstrap/scss/_alert.scss","node_modules/bootstrap/scss/mixins/_alert.scss","node_modules/bootstrap/scss/_progress.scss","node_modules/bootstrap/scss/_list-group.scss","node_modules/bootstrap/scss/mixins/_list-group.scss","node_modules/bootstrap/scss/_close.scss","node_modules/bootstrap/scss/_toasts.scss","node_modules/bootstrap/scss/_modal.scss","node_modules/bootstrap/scss/_tooltip.scss","node_modules/bootstrap/scss/mixins/_reset-text.scss","node_modules/bootstrap/scss/_popover.scss","node_modules/bootstrap/scss/_carousel.scss","node_modules/bootstrap/scss/mixins/_clearfix.scss","node_modules/bootstrap/scss/_spinners.scss","node_modules/bootstrap/scss/helpers/_colored-links.scss","node_modules/bootstrap/scss/helpers/_ratio.scss","node_modules/bootstrap/scss/helpers/_position.scss","node_modules/bootstrap/scss/helpers/_visually-hidden.scss","node_modules/bootstrap/scss/mixins/_visually-hidden.scss","node_modules/bootstrap/scss/helpers/_stretched-link.scss","node_modules/bootstrap/scss/helpers/_text-truncation.scss","node_modules/bootstrap/scss/mixins/_text-truncate.scss","node_modules/bootstrap/scss/mixins/_utilities.scss","node_modules/bootstrap/scss/utilities/_api.scss","select.scss","node_modules/slim-select/src/slim-select/slimselect.scss","flatpickr-dark.scss","netbox.scss"],"names":[],"mappings":"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A,uCoBfN,6CpBgBQ,iBoBPN,mDACE,SAAA,CACA,anC8CO,CmC5CP,wBnCEO,CmCDP,qBAGF,mDACE,SAAA,CACA,anCsCO,CmCrCP,wBnCLO,CmCMP,SrCqgCgC,CqCpgChC,8CAKF,0EACE,iBAGF,+DACE,SAAA,CACA,UrC9BO,CmBJT,wBjB2DS,CmCvBP,qBAGF,iEACE,anCtBO,CmCuBP,mBAAA,CACA,wBnCtBO,CmCuBP,qBC3CF,6CACE,uBAOI,oEnCqCJ,4BAAA,CACA,gCmChCI,mEnCiBJ,6BAAA,CACA,iCmChCF,4DACE,qBAAA,CrCsPE,kBqC/OE,mFnCqCJ,6BAAA,CACA,iCmChCI,kFnCiBJ,8BAAA,CACA,kCmChCF,4DACE,oBAAA,CrCsPE,kBqC/OE,mFnCqCJ,4BAAA,CACA,gCmChCI,kFnCiBJ,6BAAA,CACA,iCoC/BJ,yCACE,oBAAA,CACA,mBAAA,CtCoPI,eALI,CsC7OR,evCya4B,CuCxa5B,aAAA,CACA,UvCHS,CuCIT,iBAAA,CACA,kBAAA,CACA,uBAAA,CpCKE,sBoCAF,+CACE,aAKJ,8CACE,iBAAA,CACA,SCvBF,yCACE,iBAAA,CACA,YAAA,CACA,kBxCmvC8B,CwClvC9B,4BAAA,CrCWE,oBqCNJ,iDAEE,cAIF,8CACE,gBAQF,qDACE,mBAGA,gEACE,iBAAA,CACA,KAAA,CACA,OAAA,CACA,SAAA,CACA,qBAeF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,mDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,8CClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,0DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,gDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,4DACE,cD6CF,+CClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,2DACE,cD6CF,8CClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,0DACE,cD6CF,6CClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,yDACE,cD6CF,8CClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,0DACE,cD6CF,gDClDA,aD8Cc,CrB5Cd,qBqB0CmB,CC1CnB,qBAEA,4DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,gDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,4DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,iDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,mDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,mDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,+DACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,oDClDA,aD8Cc,CrB5Cd,wBqB0CmB,CC1CnB,qBAEA,gEACE,cD6CF,iDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,6DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8DACE,cD6CF,kDClDA,aDgDgB,CrB9ChB,wBqB0CmB,CC1CnB,qBAEA,8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e,CC5Jf,yBAGF,wFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,yDACE,aDmKmB,CClKnB,yBAGE,4KAEE,aD6Je,CC5Jf,yBAGF,uFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,wDACE,aDiKiB,CChKjB,yBAGE,0KAEE,aD2Ja,CC1Jb,yBAGF,sFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,uDACE,aDiKiB,CChKjB,yBAGE,wKAEE,aD2Ja,CC1Jb,yBAGF,qFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,wDACE,aDiKiB,CChKjB,yBAGE,0KAEE,aD2Ja,CC1Jb,yBAGF,sFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,0DACE,aDiKiB,CChKjB,sBAGE,8KAEE,aD2Ja,CC1Jb,yBAGF,wFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,0DACE,aDmKmB,CClKnB,yBAGE,8KAEE,aD6Je,CC5Jf,yBAGF,wFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,2DACE,aDiKiB,CChKjB,yBAGE,gLAEE,aD2Ja,CC1Jb,yBAGF,yFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDiKiB,CChKjB,yBAGE,gLAEE,aD2Ja,CC1Jb,yBAGF,yFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDiKiB,CChKjB,yBAGE,gLAEE,aD2Ja,CC1Jb,yBAGF,yFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDiKiB,CChKjB,yBAGE,gLAEE,aD2Ja,CC1Jb,yBAGF,yFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDiKiB,CChKjB,yBAGE,gLAEE,aD2Ja,CC1Jb,yBAGF,yFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDiKiB,CChKjB,yBAGE,gLAEE,aD2Ja,CC1Jb,yBAGF,yFACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,6DACE,aDiKiB,CChKjB,yBAGE,oLAEE,aD2Ja,CC1Jb,yBAGF,2FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDiKiB,CChKjB,yBAGE,oLAEE,aD2Ja,CC1Jb,yBAGF,2FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDiKiB,CChKjB,yBAGE,oLAEE,aD2Ja,CC1Jb,yBAGF,2FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDiKiB,CChKjB,yBAGE,oLAEE,aD2Ja,CC1Jb,yBAGF,2FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDiKiB,CChKjB,yBAGE,oLAEE,aD2Ja,CC1Jb,yBAGF,2FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,6DACE,aDmKmB,CClKnB,yBAGE,oLAEE,aD6Je,CC5Jf,yBAGF,2FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDmKmB,CClKnB,yBAGE,sLAEE,aD6Je,CC5Jf,yBAGF,4FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,8DACE,aDiKiB,CChKjB,yBAGE,sLAEE,aD2Ja,CC1Jb,yBAGF,4FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,2DACE,aDmKmB,CClKnB,yBAGE,gLAEE,aD6Je,CC5Jf,yBAGF,yFACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDmKmB,CClKnB,yBAGE,kLAEE,aD6Je,CC5Jf,yBAGF,0FACE,U5CRG,C4CSH,wBDuJe,CCtJf,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBAdN,4DACE,aDiKiB,CChKjB,yBAGE,kLAEE,aD2Ja,CC1Jb,yBAGF,0FACE,U5CRG,C4CSH,wBDqJa,CCpJb,qBCbR,6CACE,sBAAA,CACA,S7Cq4C2B,C6Cp4C3B,U7Co4C2B,C6Cn4C3B,aAAA,CACA,U7CFS,C6CGT,qXAAA,CAC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,UlEdK,CkEeL,UAEF,iIACE,mBAGJ,qEACE,WACA,2EACE,qBAEF,khDAkBE,kBhEWK,CgEVL,oBhEUK,CgETL,WAEF,2EACE,oBhEvCK,CgEwCL,kBhExCK,CgEyCL,WAEF,omBAOE,cChEN,wCACE,0BAAA,CACA,0BAAA,CACA,wBAAA,CACA,gCAAA,CACA,mCAAA,CACA,iCAAA,CACA,2BAAA,CACA,kBAAA,CACA,wBAAA,CACA,oBAAA,CACA,8BAAA,CACA,uCAAA,CACA,wCAAA,CACA,2BAAA,CACA,qCAAA,CACA,kCAAA,CACA,4CAAA,CACA,6CAAA,CACA,qCAAA,CACA,4BAEA,0EACE,uBAAA,CACA,0BAAA,CACA,wBAAA,CACA,gCAAA,CACA,kDAAA,CACA,iCAAA,CACA,2BAAA,CACA,qBAAA,CACA,wBAAA,CACA,oBAAA,CACA,8BAAA,CACA,wCAAA,CACA,0CAAA,CACA,2BAAA,CACA,qCAAA,CACA,kCAAA,CACA,4CAAA,CACA,6CAAA,CACA,qCAAA,CACA,4BAIJ,oCACE,mDAGF,2CACE,ejE+Ea,CiE9Eb,iBAIF,yEACE,mBAOE,02BACE,WADF,uGACE,WADF,yGACE,WADF,6GACE,WADF,mjBACE,WADF,qjBACE,WADF,wpBACE,WADF,4bACE,WADF,izBACE,WADF,+VACE,WADF,myBACE,WADF,yVACE,WADF,oqBACE,WADF,ocACE,WADF,qxBACE,WADF,mVACE,WADF,4rBACE,WADF,odACE,WADF,4rBACE,WADF,odACE,WADF,oqBACE,WADF,ocACE,WAMN,qDACE,eAGF,uCACE,mCAAA,CACA,4BACA,uDACE,YAAA,CACA,eAGF,uDACE,YAAA,CACA,eAEF,6DACE,iBAAA,CACA,0CAIA,2QAGE,cAOF,mLACE,cAEF,2EACE,WAIA,+KACE,wBAGJ,sFACE,sCAEF,oFACE,SnEnHK,CmEoHL,YAGF,oFACE,YjE9GK,CiE+GL,eAKF,sHACE,qBACA,kIACE,0BAGJ,kKACE,6BAAA,CACA,+BAEF,gPAEE,ejEZS,CiEaT,gBjETW,CiEUX,sBAGA,4RAEE,iBnEmRsB,CmElRtB,iBAMR,sDACE,YAAA,CACA,6BAAA,CACA,cAAA,CACA,kBAAA,CACA,gBAAA,CACA,kBAAA,CACA,gCAEA,wEACE,YAAA,CACA,qBAAA,CACA,aAAA,CACA,qBAIJ,6CACE,oCACA,yEAEE,enE4P0B,CmE3P1B,ejE9Ce,CiE+Cf,ajEtKO,CiEuKP,wBjEjKO,CiEkKP,wBAAA,ChEtKA,mBAAA,CgEwKA,iEACA,+EACE,2BAKN,uDACE,YAAA,CACA,yBAAA,CACA,UAAA,CACA,eAAA,CACA,kBAAA,CACA,sBAAA,CACA,qBAAA,CACA,gBAAA,CACA,oBAEA,8FACE,yCAIJ,sDACE,WAAA,CACA,gBAEA,4ZACE,YAAA,CACA,wBAAA,CACA,sBAIJ,2EACE,gBAGF,2EACE,gBAQF,4SAEE,gBAGF,8rBAOE,oBAAA,CACA,enE2L4B,CmE1L5B,wBAAA,CACA,oCAAA,CACA,kBAGF,8CACE,UAAA,CACA,eAAA,CACA,aAIF,+DACE,kBAAA,CACA,4BAAA,CACA,4BAGF,mEACE,kBAAA,CACA,wBAAA,CACA,0BAGF,4DACE,iBAAA,CACA,qBAAA,CACA,WAAA,CACA,YAAA,CACA,eAGF,sEACE,YAAA,CACA,6BAAA,CACA,mBAGF,kDACE,iBAAA,CACA,KAAA,CACA,yBAAA,CACA,iBAAA,CACA,iBAAA,CACA,gBAGF,gDACE,kBAAA,CACA,qBAAA,CACA,eAIA,uEACE,oBAAA,CAEA,iBnE2H0B,CmE1H1B,oBACA,6EAGE,sCjDtIuB,CiDuIvB,cAOJ,yBADF,4EAEI,cAAA,CACA,mBAIJ,2CACE,cAAA,CACA,KAAA,CACA,QAAA,CACA,MAAA,CACA,WAAA,CACA,wCAAA,CACA,uCAEA,yBATF,2CAUI,cAIA,gGAGE,ajElUK,CiEmUL,uCAEF,gGACE,kBAAA,CACA,iBAIJ,2DACE,8BAAA,CACA,gBAEE,+EACE,oBAAA,CAEA,iBnEuEsB,CmEtEtB,oBACA,qFAGE,ajEvVC,CiEwVD,uCAON,yBADF,6EAEI,8BAAA,CACA,iBAGJ,+DACE,0BAEF,8DACE,kBAAA,CACA,mBAAA,CACA,eAAA,CACA,WAAA,CACA,sCAAA,CACA,yCACA,wEACE,qBAGJ,0DACE,YAAA,CACA,aAAA,CACA,UAAA,CACA,YAIJ,2CACE,oBAGF,6CACE,6BAIA,oOAEE,kBAIJ,mDACE,gBAGF,mDACE,UAAA,CACA,WAAA,CACA,aAAA,CACA,4CjDtXc,CiDuXd,mBjE/SiB,CiEgTjB,oBAGF,sCACE,mBjEpTiB,CiEqTjB,4CAAA,CACA,kCAAA,CACA,YnEpLO,CmEqLP,gBAGF,uCACE,mBAGF,wCACE,6CACA,qDACE,UnEhbO,CmEibP,kBAAA,CACA,aAEF,gEACE,cAEF,mDACE,gBAIJ,iDACE,kBAEA,uJAEE,yBnEgZ8B,CmE/Y9B,oBAGF,oEACE,iBAAA,CACA,KAAA,CACA,MAAA,CACA,WAAA,CACA,mBAAA,CACA,mBAAA,CACA,4BAAA,CACA,oBAAA,ClDrcE,6DAIA,uCkDybJ,oElDxbM,iBkDqcJ,yFACE,kBAGF,sLAEE,oBnE4X4B,CmE3X5B,uBAGF,6FACE,oBnEuX4B,CmEtX5B,uBAIJ,wSAKE,oBnE6W8B,CmE5W9B,uBAQA,mZACE,WnEqW4B,CmEpW5B,0DnEqW4B,CmEpW5B,UAKF,mGACE,WnE6V4B,CmE5V5B,0DnE6V4B,CmE5V5B,UAWN,gPACE,oGAGF,0DACE,sBAGF,2DACE,mBAGF,4CACE,eAGF,2DACE,mBAAA,CACA,mBjE1aiB,CiE2ajB,iBnErH4B,CmEsH5B,iBAAA,CACA,yCAAA,CACA,enE3SO,CmE4SP,mBAEA,yEACE,gBACA,8FACE,qBAEF,oGACE,0BAKN,qEACE,kBAGF,sEACE,gBAEA,k3BAME,mBAIJ,iDACE,gBACA,uDACE,iCAAA,CACA,YAAA,CACA,eAAA,CACA,aAAA,CACA,iBAAA,CACA,gBAAA,CACA,oBAAA,CACA,oBAAA,CACA,kBAIJ,mDACE,aACA,qDACE,cAIJ,yGAEE,0BAKE,yDACE,sCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,sDACE,sCAAA,CACA,qBAFF,yDACE,qCAAA,CACA,qBAFF,wDACE,sCAAA,CACA,qBAFF,uDACE,sCAAA,CACA,qBAFF,sDACE,sCAAA,CACA,qBAFF,qDACE,mCAAA,CACA,qBAFF,sDACE,sCAAA,CACA,qBAFF,wDACE,gCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,mCAAA,CACA,qBAFF,0DACE,mCAAA,CACA,qBAFF,0DACE,mCAAA,CACA,qBAFF,0DACE,mCAAA,CACA,qBAFF,wDACE,sCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,yDACE,oCAAA,CACA,qBAFF,yDACE,oCAAA,CACA,qBAFF,yDACE,oCAAA,CACA,qBAFF,yDACE,oCAAA,CACA,qBAFF,yDACE,oCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,4DACE,mCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,2DACE,qCAAA,CACA,qBAFF,2DACE,qCAAA,CACA,qBAFF,2DACE,oCAAA,CACA,qBAFF,2DACE,mCAAA,CACA,qBAFF,2DACE,kCAAA,CACA,qBAFF,2DACE,kCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,qCAAA,CACA,qBAFF,0DACE,qCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,qCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,qCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,mCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,2DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,sCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,qCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,4DACE,oCAAA,CACA,qBAFF,yDACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,sCAAA,CACA,qBAFF,0DACE,qCAAA,CACA,qBAFF,0DACE,qCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAFF,0DACE,oCAAA,CACA,qBAMN,+CACE,eAAA,CACA,gBAAA,CACA,kBAEF,qDACE,yCAAA,CACA,mDAAA,CACA,mBjE1fiB,CiE2fjB,mBAAA,CACA,iBAAA,CACA,UAEF,4DACE,gDAAA,CACA,0DAAA,CACA,uCjD3kBW,CiD4kBX,mBjEngBiB,CiEogBjB,iBAAA,CACA,aAAA,CACA,iBAAA,CACA,SAAA,CACA,UAEF,uDACE,4BAEF,sDACE,uBAAA,CACA,wBAAA,CACA,sBAAA,CACA,cAAA,CACA,eAAA,CACA,UAEF,0DACE,eAAA,CACA,kBAGF,kDACE,cAAA,CACA,gBACA,uDACE,aAAA,CACA,iBnE5ZK,CmE6ZL,mBACA,6DACE,yCAEF,+DACE,2CAKN,kDACE,yBACA,iEACE,2CAEF,+DACE,yCAIJ,mDACE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,oCAAA,CACA,mBjEzjBiB,CiE0jBjB,YAAA,CACA,sBAAA,CACA,mBACA,sEACE,UAAA,CACA,WAAA,CACA,cAIJ,8EACE,aAAA,CACA","file":"netbox-dark.css","sourceRoot":"..","sourcesContent":["//\n// Headings\n//\n.h1 {\n @extend h1;\n}\n\n.h2 {\n @extend h2;\n}\n\n.h3 {\n @extend h3;\n}\n\n.h4 {\n @extend h4;\n}\n\n.h5 {\n @extend h5;\n}\n\n.h6 {\n @extend h6;\n}\n\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n@each $display, $font-size in $display-font-sizes {\n .display-#{$display} {\n @include font-size($font-size);\n font-weight: $display-font-weight;\n line-height: $display-line-height;\n }\n}\n\n//\n// Emphasis\n//\n.small {\n @extend small;\n}\n\n.mark {\n @extend mark;\n}\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size($initialism-font-size);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $blockquote-margin-y;\n @include font-size($blockquote-font-size);\n\n > :last-child {\n margin-bottom: 0;\n }\n}\n\n.blockquote-footer {\n margin-top: -$blockquote-margin-y;\n margin-bottom: $blockquote-margin-y;\n @include font-size($blockquote-footer-font-size);\n color: $blockquote-footer-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n",":root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n font-size: $font-size-root;\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: $body-text-align;\n background-color: $body-bg; // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`

- - - - + + + + @@ -82,8 +82,8 @@ {% plugin_left_page object %}
- {% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %} - {% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %} + {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %} + {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index 61524d345..af55cd70b 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -60,7 +60,7 @@ @@ -339,7 +339,7 @@ {% endif %} - + {% endfor %}
- {{ object.get_status_display }} -
Provider @@ -50,6 +44,12 @@ {% endif %}
Status + {{ object.get_status_display }} +
Install Date {{ object.install_date|placeholder }} Length {% if object.length %} - {{ object.length }} {{ object.get_length_unit_display }} + {{ object.length|floatformat }} {{ object.get_length_unit_display }} {% else %} {% endif %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 50442632c..b61d885f9 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -94,7 +94,7 @@
Device Type - {{ object.device_type.display_name }} ({{ object.device_type.u_height }}U) + {{ object.device_type }} ({{ object.device_type.u_height }}U)
{{ rd.device_type.display_name }}{{ rd.device_type }}
diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index 52bb23f06..0317e0323 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -46,8 +46,8 @@ {% block buttons %}
Cancel {% if obj.pk %} - - + + {% else %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 208539d27..0cc115ddd 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -344,7 +344,7 @@ {{ device }} {{ device.device_role }} - {{ device.device_type.display_name }} + {{ device.device_type }} {% if device.parent_bay %} {{ device.parent_bay.device }} {{ device.parent_bay }} diff --git a/netbox/templates/dcim/trace/cable.html b/netbox/templates/dcim/trace/cable.html index 43b4910f4..5f8fb01eb 100644 --- a/netbox/templates/dcim/trace/cable.html +++ b/netbox/templates/dcim/trace/cable.html @@ -10,7 +10,7 @@ {{ cable.get_type_display|default:"" }} {% endif %} {% if cable.length %} - ({{ cable.length }} {{ cable.get_length_unit_display }})
+ ({{ cable.length|floatformat }} {{ cable.get_length_unit_display }})
{% endif %} {{ cable.get_status_display }}
{% for tag in cable.tags.all %} diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index 31958b787..195855620 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -94,7 +94,7 @@
Cancel {% if vc_form.instance.pk %} - + {% else %} {% endif %} diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index c3ef2cad5..f10cceb15 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -56,7 +56,7 @@ Cancel {% if obj.pk %} {% else %}
-
+
{% search_options %} {% include './profile_button.html' %}
@@ -64,6 +63,15 @@ {% block tabs %}{% endblock %} {% block content %}{% endblock %}
+
+
+
+
+ {% now 'Y-m-d H:i:s T' %} + {{ settings.HOSTNAME }} (v{{ settings.VERSION }}) +
+
+
diff --git a/netbox/templates/login.html b/netbox/templates/login.html index 63ea15733..164cb1176 100644 --- a/netbox/templates/login.html +++ b/netbox/templates/login.html @@ -7,7 +7,9 @@ {% endif %} {% endif %} -