Merge branch 'feature' into feature-2434

This commit is contained in:
checktheroads 2021-05-08 11:08:06 -07:00
commit 515aed7022
217 changed files with 2064 additions and 1992 deletions

View File

@ -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,7 +34,8 @@ body:
- type: textarea
attributes:
label: Steps to Reproduce
description: "Describe in detail the exact steps that someone else can take to
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
@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
![NetBox](docs/netbox_logo.svg "NetBox logo")
<div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
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) |
<div align="center">
<h4>Thank you to our sponsors!</h4>
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
</div>
### 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.

View File

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

View File

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

View File

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

View File

@ -6,3 +6,18 @@
* [#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
### REST API Changes
* 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)
* extras.ContentType
* Removed the `display_name` attribute (use `display` instead)
* ipam.VLAN
* Removed the `display_name` attribute (use `display` instead)
* ipam.VRF
* Removed the `display_name` attribute (use `display` instead)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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".
@ -622,7 +618,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])
@ -716,7 +718,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({
@ -823,17 +825,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):
"""
@ -856,9 +847,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()
def get_vc_master(self):
"""
@ -866,7 +855,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.
@ -874,7 +863,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)

View File

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

View File

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

View File

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

View File

@ -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',
}

View File

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

View File

@ -27,7 +27,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 (
@ -110,7 +110,7 @@ class RegionListView(generic.ObjectListView):
'site_count',
cumulative=True
)
filterset = filters.RegionFilterSet
filterset = filtersets.RegionFilterSet
filterset_form = forms.RegionFilterForm
table = tables.RegionTable
@ -166,7 +166,7 @@ class RegionBulkEditView(generic.BulkEditView):
'site_count',
cumulative=True
)
filterset = filters.RegionFilterSet
filterset = filtersets.RegionFilterSet
table = tables.RegionTable
form = forms.RegionBulkEditForm
@ -179,7 +179,7 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
'site_count',
cumulative=True
)
filterset = filters.RegionFilterSet
filterset = filtersets.RegionFilterSet
table = tables.RegionTable
@ -195,7 +195,7 @@ class SiteGroupListView(generic.ObjectListView):
'site_count',
cumulative=True
)
filterset = filters.SiteGroupFilterSet
filterset = filtersets.SiteGroupFilterSet
filterset_form = forms.SiteGroupFilterForm
table = tables.SiteGroupTable
@ -251,7 +251,7 @@ class SiteGroupBulkEditView(generic.BulkEditView):
'site_count',
cumulative=True
)
filterset = filters.SiteGroupFilterSet
filterset = filtersets.SiteGroupFilterSet
table = tables.SiteGroupTable
form = forms.SiteGroupBulkEditForm
@ -264,7 +264,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
'site_count',
cumulative=True
)
filterset = filters.SiteGroupFilterSet
filterset = filtersets.SiteGroupFilterSet
table = tables.SiteGroupTable
@ -274,7 +274,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
@ -329,14 +329,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
@ -358,7 +358,7 @@ class LocationListView(generic.ObjectListView):
'rack_count',
cumulative=True
)
filterset = filters.LocationFilterSet
filterset = filtersets.LocationFilterSet
filterset_form = forms.LocationFilterForm
table = tables.LocationTable
@ -417,7 +417,7 @@ class LocationBulkEditView(generic.BulkEditView):
'rack_count',
cumulative=True
).prefetch_related('site')
filterset = filters.LocationFilterSet
filterset = filtersets.LocationFilterSet
table = tables.LocationTable
form = forms.LocationBulkEditForm
@ -430,7 +430,7 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
'rack_count',
cumulative=True
).prefetch_related('site')
filterset = filters.LocationFilterSet
filterset = filtersets.LocationFilterSet
table = tables.LocationTable
@ -481,7 +481,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
@ -503,7 +503,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
@ -516,7 +516,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
@ -605,14 +605,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
@ -622,7 +622,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
@ -665,14 +665,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
@ -695,6 +695,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)
@ -725,7 +727,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
@ -745,7 +747,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
@ -851,7 +853,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
@ -860,7 +862,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
@ -1193,7 +1195,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
@ -1252,7 +1254,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
@ -1268,7 +1270,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'
@ -1408,7 +1410,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',
@ -1530,7 +1532,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
@ -1603,14 +1605,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
@ -1620,7 +1622,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')
@ -1655,7 +1657,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
@ -1670,7 +1672,7 @@ class ConsolePortBulkDisconnectView(BulkDisconnectView):
class ConsolePortBulkDeleteView(generic.BulkDeleteView):
queryset = ConsolePort.objects.all()
filterset = filters.ConsolePortFilterSet
filterset = filtersets.ConsolePortFilterSet
table = tables.ConsolePortTable
@ -1680,7 +1682,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')
@ -1715,7 +1717,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
@ -1730,7 +1732,7 @@ class ConsoleServerPortBulkDisconnectView(BulkDisconnectView):
class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView):
queryset = ConsoleServerPort.objects.all()
filterset = filters.ConsoleServerPortFilterSet
filterset = filtersets.ConsoleServerPortFilterSet
table = tables.ConsoleServerPortTable
@ -1740,7 +1742,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')
@ -1775,7 +1777,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
@ -1790,7 +1792,7 @@ class PowerPortBulkDisconnectView(BulkDisconnectView):
class PowerPortBulkDeleteView(generic.BulkDeleteView):
queryset = PowerPort.objects.all()
filterset = filters.PowerPortFilterSet
filterset = filtersets.PowerPortFilterSet
table = tables.PowerPortTable
@ -1800,7 +1802,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')
@ -1835,7 +1837,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
@ -1850,7 +1852,7 @@ class PowerOutletBulkDisconnectView(BulkDisconnectView):
class PowerOutletBulkDeleteView(generic.BulkDeleteView):
queryset = PowerOutlet.objects.all()
filterset = filters.PowerOutletFilterSet
filterset = filtersets.PowerOutletFilterSet
table = tables.PowerOutletTable
@ -1860,7 +1862,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')
@ -1997,7 +1999,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
@ -2012,7 +2014,7 @@ class InterfaceBulkDisconnectView(BulkDisconnectView):
class InterfaceBulkDeleteView(generic.BulkDeleteView):
queryset = Interface.objects.all()
filterset = filters.InterfaceFilterSet
filterset = filtersets.InterfaceFilterSet
table = tables.InterfaceTable
@ -2022,7 +2024,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')
@ -2057,7 +2059,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
@ -2072,7 +2074,7 @@ class FrontPortBulkDisconnectView(BulkDisconnectView):
class FrontPortBulkDeleteView(generic.BulkDeleteView):
queryset = FrontPort.objects.all()
filterset = filters.FrontPortFilterSet
filterset = filtersets.FrontPortFilterSet
table = tables.FrontPortTable
@ -2082,7 +2084,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')
@ -2117,7 +2119,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
@ -2132,7 +2134,7 @@ class RearPortBulkDisconnectView(BulkDisconnectView):
class RearPortBulkDeleteView(generic.BulkDeleteView):
queryset = RearPort.objects.all()
filterset = filters.RearPortFilterSet
filterset = filtersets.RearPortFilterSet
table = tables.RearPortTable
@ -2142,7 +2144,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')
@ -2242,7 +2244,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
@ -2253,7 +2255,7 @@ class DeviceBayBulkRenameView(generic.BulkRenameView):
class DeviceBayBulkDeleteView(generic.BulkDeleteView):
queryset = DeviceBay.objects.all()
filterset = filters.DeviceBayFilterSet
filterset = filtersets.DeviceBayFilterSet
table = tables.DeviceBayTable
@ -2263,7 +2265,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')
@ -2297,7 +2299,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
@ -2322,7 +2324,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'
@ -2333,7 +2335,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'
@ -2344,7 +2346,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'
@ -2355,7 +2357,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'
@ -2366,7 +2368,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'
@ -2377,7 +2379,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'
@ -2388,7 +2390,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'
@ -2399,7 +2401,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'
@ -2410,7 +2412,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'
@ -2421,7 +2423,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')
@ -2554,14 +2556,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
@ -2571,7 +2573,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'
@ -2601,7 +2603,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'
@ -2635,7 +2637,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'
@ -2674,7 +2676,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
@ -2882,14 +2884,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
@ -2903,7 +2905,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
@ -2943,7 +2945,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
@ -2954,7 +2956,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
).annotate(
powerfeed_count=count_related(PowerFeed, 'power_panel')
)
filterset = filters.PowerPanelFilterSet
filterset = filtersets.PowerPanelFilterSet
table = tables.PowerPanelTable
@ -2964,7 +2966,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
@ -2990,7 +2992,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
@ -3001,5 +3003,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

View File

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

View File

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

View File

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

341
netbox/extras/filtersets.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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']
#

View File

@ -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 = []

View File

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

View File

@ -6,11 +6,11 @@ from django.db.models import Q
from netaddr.core import AddrFormatError
from dcim.models import Device, Interface, Region, Site, SiteGroup
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet
from extras.filters import TagFilter
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import (
BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import VirtualMachine, VMInterface
from .choices import *
@ -31,7 +31,7 @@ __all__ = (
)
class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -74,7 +74,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, C
fields = ['id', 'name', 'rd', 'enforce_unique']
class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -116,14 +116,14 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt
fields = ['id', 'name']
class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
class RIRFilterSet(OrganizationalModelFilterSet):
class Meta:
model = RIR
fields = ['id', 'name', 'slug', 'is_private', 'description']
class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -173,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
return queryset.none()
class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
class RoleFilterSet(OrganizationalModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -184,7 +184,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilter
fields = ['id', 'name', 'slug']
class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -369,7 +369,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
)
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -535,7 +535,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
return queryset.exclude(assigned_object_id__isnull=value)
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
class VLANGroupFilterSet(OrganizationalModelFilterSet):
scope_type = ContentTypeFilter()
region = django_filters.NumberFilter(
method='filter_scope'
@ -570,7 +570,7 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF
)
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@ -666,7 +666,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
return queryset.get_for_virtualmachine(value)
class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
class ServiceFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@ -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
@ -426,19 +426,11 @@ class Prefix(PrimaryModel):
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),

View File

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

View File

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

View File

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

View File

@ -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',
}

View File

@ -2,13 +2,14 @@ from django.test import TestCase
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
from ipam.choices import *
from ipam.filters import *
from ipam.filtersets import *
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
from utilities.testing import ChangeLoggedFilterSetTests
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from tenancy.models import Tenant, TenantGroup
class VRFTestCase(TestCase):
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VRF.objects.all()
filterset = VRFFilterSet
@ -53,10 +54,6 @@ class VRFTestCase(TestCase):
vrfs[2].import_targets.add(route_targets[2])
vrfs[2].export_targets.add(route_targets[2])
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['VRF 1', 'VRF 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -100,7 +97,7 @@ class VRFTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class RouteTargetTestCase(TestCase):
class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RouteTarget.objects.all()
filterset = RouteTargetFilterSet
@ -149,10 +146,6 @@ class RouteTargetTestCase(TestCase):
vrfs[1].import_targets.add(route_targets[4], route_targets[5])
vrfs[1].export_targets.add(route_targets[6], route_targets[7])
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['65000:1001', '65000:1002', '65000:1003']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@ -186,7 +179,7 @@ class RouteTargetTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
class RIRTestCase(TestCase):
class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RIR.objects.all()
filterset = RIRFilterSet
@ -203,10 +196,6 @@ class RIRTestCase(TestCase):
)
RIR.objects.bulk_create(rirs)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['RIR 1', 'RIR 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -226,7 +215,7 @@ class RIRTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
class AggregateTestCase(TestCase):
class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Aggregate.objects.all()
filterset = AggregateFilterSet
@ -265,10 +254,6 @@ class AggregateTestCase(TestCase):
)
Aggregate.objects.bulk_create(aggregates)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_family(self):
params = {'family': '4'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@ -304,7 +289,7 @@ class AggregateTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class RoleTestCase(TestCase):
class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Role.objects.all()
filterset = RoleFilterSet
@ -318,10 +303,6 @@ class RoleTestCase(TestCase):
)
Role.objects.bulk_create(roles)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['Role 1', 'Role 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -331,7 +312,7 @@ class RoleTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class PrefixTestCase(TestCase):
class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Prefix.objects.all()
filterset = PrefixFilterSet
@ -421,10 +402,6 @@ class PrefixTestCase(TestCase):
)
Prefix.objects.bulk_create(prefixes)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_family(self):
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
@ -528,7 +505,7 @@ class PrefixTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class IPAddressTestCase(TestCase):
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPAddress.objects.all()
filterset = IPAddressFilterSet
@ -607,10 +584,6 @@ class IPAddressTestCase(TestCase):
)
IPAddress.objects.bulk_create(ipaddresses)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_family(self):
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
@ -708,7 +681,7 @@ class IPAddressTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class VLANGroupTestCase(TestCase):
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VLANGroup.objects.all()
filterset = VLANGroupFilterSet
@ -751,10 +724,6 @@ class VLANGroupTestCase(TestCase):
)
VLANGroup.objects.bulk_create(vlan_groups)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['VLAN Group 1', 'VLAN Group 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -796,7 +765,7 @@ class VLANGroupTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class VLANTestCase(TestCase):
class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VLAN.objects.all()
filterset = VLANFilterSet
@ -965,10 +934,6 @@ class VLANTestCase(TestCase):
)
VLAN.objects.bulk_create(vlans)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['VLAN 101', 'VLAN 102']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -1041,7 +1006,7 @@ class VLANTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
class ServiceTestCase(TestCase):
class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Service.objects.all()
filterset = ServiceFilterSet
@ -1080,10 +1045,6 @@ class ServiceTestCase(TestCase):
)
Service.objects.bulk_create(services)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:3]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_name(self):
params = {'name': ['Service 1', 'Service 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

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

View File

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

View File

@ -1,9 +1,9 @@
from collections import OrderedDict
from circuits.filters import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
from circuits.models import Circuit, ProviderNetwork, Provider
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
from dcim.filters import (
from dcim.filtersets import (
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
SiteFilterSet, VirtualChassisFilterSet,
)
@ -12,17 +12,17 @@ from dcim.tables import (
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable,
VirtualChassisTable,
)
from ipam.filters import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
from ipam.filtersets import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
from secrets.filters import SecretFilterSet
from secrets.filtersets import SecretFilterSet
from secrets.models import Secret
from secrets.tables import SecretTable
from tenancy.filters import TenantFilterSet
from tenancy.filtersets import TenantFilterSet
from tenancy.models import Tenant
from tenancy.tables import TenantTable
from utilities.utils import count_related
from virtualization.filters import ClusterFilterSet, VirtualMachineFilterSet
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineDetailTable

238
netbox/netbox/filtersets.py Normal file
View File

@ -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 `<field_name>__<lookup_expr>`
"""
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)
)

View File

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

View File

@ -109,12 +109,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:
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 = {"label": item_label, "description": description, "count": None, "url": url, "disabled": True}
if perm in perms:
item["count"] = get_count()
item["disabled"] = False
stat["items"].append(item)
stats.append(stat)
return stats

View File

@ -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';
}

View File

@ -1,4 +1,5 @@
// Stylesheet for rendering SVG rack elevations
// Entry for rack_elevation.css stylesheet.
@import './theme-light.scss';
* {

View File

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

View File

@ -0,0 +1,6 @@
// Entry for netbox-light.css stylesheet.
@import './theme-light.scss';
@import './bootstrap.scss';
@import './select.scss';
@import './netbox.scss';

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,27 +0,0 @@
// Main Entry Point for all Netbox Styles.
// Note: The order of these imports is critical for proper inheritance.
// Light Mode Styles.
@import './theme-light.scss';
@import './bootstrap.scss';
@import '@mdi/font/css/materialdesignicons.min.css';
@import './select.scss';
@import 'flatpickr/dist/flatpickr.css';
@import './netbox.scss';
// Dark Mode Styles.
body[data-netbox-color-mode='dark'] {
@import './theme-dark.scss';
@import './bootstrap.scss';
@import '@mdi/font/css/materialdesignicons.min.css';
@import './select.scss';
@import 'flatpickr/dist/flatpickr.css';
@import './flatpickr-dark.scss';
@import './netbox.scss';
}

View File

@ -11,7 +11,7 @@
--nbx-sidebar-title-color: #{$text-muted};
--nbx-breadcrumb-bg: #{$light};
--nbx-body-bg: #{$white};
--nbx-body-color: #{$black};
--nbx-body-color: #{$gray-800};
--nbx-pre-bg: #{$gray-100};
--nbx-pre-border-color: #{$gray-600};
--nbx-change-added: #{rgba($green, 0.4)};
@ -20,6 +20,9 @@
--nbx-cable-node-border-color: #{$gray-200};
--nbx-cable-termination-bg: #{$gray-200};
--nbx-cable-termination-border-color: #{$gray-300};
--nbx-search-filter-border-left-color: #{$gray-300};
--nbx-color-mode-toggle-color: #{$primary};
--nbx-stat-badge-bg: #{$gray-600};
body[data-netbox-color-mode='dark'] {
--nbx-logo-color-1: #{$white};
@ -30,7 +33,7 @@
--nbx-sidebar-title-color: #{$gray-300};
--nbx-breadcrumb-bg: #{$gray-800};
--nbx-body-bg: #{$gray-900};
--nbx-body-color: #{$white};
--nbx-body-color: #{$gray-50};
--nbx-pre-bg: #{$gray-700};
--nbx-pre-border-color: #{$gray-600};
--nbx-change-added: #{rgba($green-300, 0.4)};
@ -39,6 +42,9 @@
--nbx-cable-node-border-color: #{$gray-600};
--nbx-cable-termination-bg: #{$gray-800};
--nbx-cable-termination-border-color: #{$gray-700};
--nbx-search-filter-border-left-color: #{$gray-600};
--nbx-color-mode-toggle-color: #{$yellow-300};
--nbx-stat-badge-bg: #{$gray-600};
}
}
@ -46,6 +52,31 @@
transition: background-color, color 0.15s ease-in-out;
}
.text-xs {
font-size: $font-size-xs;
line-height: $line-height-sm;
}
// Automatically space out adjacent columns.
.col:not(:last-child):not(:only-child) {
margin-bottom: $spacer;
}
// Use proper contrasting color for badge & progress-bar foreground color.
@each $color, $value in $theme-colors {
.badge,
.progress-bar {
&.bg-#{$color} {
color: color-contrast($value);
}
}
}
// Ensure progress bars (utilization graph) in tables aren't too narrow to display the percentage.
table td > .progress {
min-width: 6rem;
}
body {
background-color: var(--nbx-body-bg);
color: var(--nbx-body-color);
@ -58,9 +89,16 @@ body {
fill: #1685fc;
stroke: #1685fc;
}
span.badge.stat-badge {
margin-left: map.get($spacers, 2);
background-color: var(--nbx-stat-badge-bg);
}
&[data-netbox-color-mode='light'] {
.btn.btn-primary {
color: $white;
.btn.btn-primary,
.progress-bar.bg-primary,
.badge.bg-primary {
color: $gray-50;
}
}
&[data-netbox-color-mode='dark'] {
@ -92,6 +130,32 @@ body {
stroke: $gray-200;
}
}
& table,
&[data-netbox-color-mode] table {
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
&.table > :not(caption) > * > * {
padding-left: $table-cell-padding-x-sm !important;
padding-right: $table-cell-padding-x-sm !important;
}
td,
th {
font-size: $font-size-xs;
line-height: $line-height-sm;
vertical-align: middle;
}
&.attr-table {
td,
th {
font-size: $font-size-sm;
line-height: $line-height-sm;
}
}
}
}
div.title-container {
@ -113,17 +177,35 @@ div.title-container {
nav.search {
background-color: var(--nbx-body-bg);
form button.dropdown-toggle {
border-color: $input-border-color;
font-weight: $input-group-addon-font-weight;
line-height: $input-line-height;
color: $input-group-addon-color;
background-color: $input-group-addon-bg;
border: $input-border-width solid $input-group-addon-border-color;
@include border-radius($input-border-radius);
border-left: 1px solid var(--nbx-search-filter-border-left-color);
&:focus {
box-shadow: unset !important;
}
}
}
main.login-container {
display: flex;
height: calc(100vh - 4rem);
width: 100vw;
width: 100%;
max-width: 100vw;
align-items: center;
justify-content: center;
flex-direction: column;
padding-top: 40px;
padding-bottom: 40px;
& + footer.footer button.color-mode-toggle {
color: var(--nbx-color-mode-toggle-color);
}
}
footer.login-footer {
@ -161,7 +243,8 @@ h3.accordion-item-title,
h4.accordion-item-title,
h5.accordion-item-title,
h6.accordion-item-title {
padding: 0 0.5rem;
// padding: 0 0.5rem;
padding: 0.25rem 0.5rem;
font-weight: $font-weight-bold;
text-transform: uppercase;
color: var(--nbx-sidebar-title-color);
@ -201,12 +284,6 @@ li.dropdown-item.dropdown-item-btns {
align-items: center;
}
@media (max-width: 767.98px) {
.sidebar {
top: 5rem;
}
}
.sidebar-sticky {
position: relative;
top: 0;
@ -225,15 +302,26 @@ li.dropdown-item.dropdown-item-btns {
nav.nav.nav-pills {
.nav-item.nav-link {
padding: 0.25rem 0.5rem;
font-size: $font-size-base;
// font-size: $font-size-base;
font-size: $font-size-sm;
border-radius: $border-radius;
&:hover {
color: $body-color;
background-color: var(--nbx-sidebar-link-hover-bg);
// color: $body-color;
// background-color: var(--nbx-sidebar-link-hover-bg);
background-color: $accordion-button-active-bg;
color: $accordion-button-active-color;
}
}
}
// Prevent scrolling of body content when nav menu is open on mobile.
.sidebar.collapse.show ~ .content-container {
@media (max-width: map.get($grid-breakpoints, 'md')) {
position: fixed;
overflow-y: hidden;
}
}
.sidebar {
position: fixed;
top: 0;
@ -242,26 +330,51 @@ nav.nav.nav-pills {
z-index: 100; /* Behind the navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
background-color: var(--nbx-sidebar-bg);
.sidebar-nav-link {
color: var(--nbx-sidebar-link-color);
@media (max-width: map.get($grid-breakpoints, 'md')) {
top: 8.125rem;
}
div.accordion-item > a.accordion-button.nav-link {
&:hover {
// color: $body-color;
// background-color: var(--nbx-sidebar-link-hover-bg);
color: $accordion-button-active-color;
background-color: $accordion-button-active-bg;
}
&:focus {
border-color: unset;
box-shadow: unset;
}
}
.accordion-body {
max-height: calc(100vh - 24rem);
overflow-y: auto;
.nav-item {
.nav-link {
padding: 0.25rem 0.5rem;
font-size: $font-size-base;
padding: 0.25rem 0.6rem;
// font-size: $font-size-base;
font-size: $font-size-sm;
border-radius: $border-radius;
&:hover {
color: $body-color;
background-color: var(--nbx-sidebar-link-hover-bg);
// color: $body-color;
// background-color: var(--nbx-sidebar-link-hover-bg);
color: $accordion-button-active-color;
background-color: $accordion-button-active-bg;
}
}
}
}
// Ensure navigation accounts for the height of the header on mobile when nav is expanded.
&.collapse.show div.position-sticky {
@media (max-width: map.get($grid-breakpoints, 'md')) {
height: calc(100vh - 16.125rem);
overflow-y: auto;
}
}
div.position-sticky {
height: calc(100% - 8rem);
height: calc(100vh - 8rem);
}
div.sidebar-bottom {
padding-left: 0.5rem;
@ -499,10 +612,6 @@ table tbody {
}
}
}
table td,
table th {
font-size: $font-size-sm;
}
// Cable Tracing
.cable-trace {

View File

@ -54,7 +54,6 @@
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
},

View File

@ -60,6 +60,7 @@ div.form-floating div.ss-main div.ss-multi-selected {
div.ss-multi-selected .ss-values .ss-disabled,
div.ss-single-selected span.placeholder .ss-disabled {
color: var(--nbx-select-placeholder-color);
font-size: $font-size-xs;
}
.ss-single-selected {
@ -109,12 +110,19 @@ div.form-floating div.ss-main div.ss-multi-selected {
border-bottom-left-radius: $form-select-border-radius;
border-bottom-right-radius: $form-select-border-radius;
}
.ss-option.ss-disabled {
background-color: transparent;
}
.ss-option.ss-disabled:hover {
color: unset;
}
}
border-bottom-left-radius: $form-select-border-radius;
border-bottom-right-radius: $form-select-border-radius;
.ss-search {
input[type='search'] {
background-color: $form-select-bg;
color: $input-color;
border: $form-select-border-width solid $form-select-border-color;
&:focus {
border-color: $form-select-focus-border-color;

View File

@ -265,6 +265,19 @@ function initSelectAll() {
}
}
function handlePerPageSelect(event: Event) {
const select = event.currentTarget as HTMLSelectElement;
if (select.form !== null) {
select.form.submit();
}
}
function initPerPage() {
for (const element of getElements<HTMLSelectElement>('select.per-page')) {
element.addEventListener('change', handlePerPageSelect);
}
}
export function initButtons() {
for (const func of [
initRackElevation,
@ -272,6 +285,7 @@ export function initButtons() {
initReslug,
initSelectAll,
initPreferenceUpdate,
initPerPage,
]) {
func();
}

View File

@ -1,40 +1,44 @@
import debounce from 'just-debounce-it';
import { getElements, getRowValues, findFirstAdjacent } from './util';
import { getElements, getRowValues, findFirstAdjacent, isTruthy } from './util';
interface SearchFilterButton extends EventTarget {
dataset: { searchValue: string };
}
/**
* Change the display value and hidden input values of the search filter based on dropdown
* selection.
*
* @param event "click" event for each dropdown item.
* @param button Each dropdown item element.
*/
function handleSearchDropdownClick(event: Event, button: HTMLButtonElement) {
const dropdown = event.currentTarget as HTMLButtonElement;
const selectedValue = findFirstAdjacent<HTMLSpanElement>(dropdown, 'span.search-obj-selected');
const selectedType = findFirstAdjacent<HTMLInputElement>(dropdown, 'input.search-obj-type');
const searchValue = dropdown.getAttribute('data-search-value');
let selected = '' as string;
function isSearchButton(el: any): el is SearchFilterButton {
return el?.dataset?.searchValue ?? null !== null;
}
function initSearchBar() {
const dropdown = document.getElementById('object-type-selector');
const selectedValue = document.getElementById('selected-value') as HTMLSpanElement;
const selectedType = document.getElementById('search-obj-type') as HTMLInputElement;
let selected = '';
if (dropdown !== null) {
const buttons = dropdown.querySelectorAll('li > button.dropdown-item');
for (const button of buttons) {
if (button !== null) {
function handleClick(event: Event) {
if (isSearchButton(event.target)) {
const objectType = event.target.dataset.searchValue;
if (objectType !== '' && selected !== objectType) {
selected = objectType;
if (selectedValue !== null && selectedType !== null) {
if (isTruthy(searchValue) && selected !== searchValue) {
selected = searchValue;
selectedValue.innerHTML = button.textContent ?? 'Error';
selectedType.value = objectType;
selectedType.value = searchValue;
} else {
selected = '';
selectedType.innerHTML = 'All Objects';
selectedValue.innerHTML = 'All Objects';
selectedType.value = '';
}
}
}
button.addEventListener('click', handleClick);
}
}
/**
* Initialize Search Bar Elements.
*/
function initSearchBar() {
for (const dropdown of getElements<HTMLUListElement>(
'div.search-container ul.search-obj-selector',
)) {
for (const button of dropdown.querySelectorAll<HTMLButtonElement>(
'li > button.dropdown-item',
)) {
button.addEventListener('click', event => handleSearchDropdownClick(event, button));
}
}
}

View File

@ -184,7 +184,7 @@ export function initApiSelect() {
// element's value.
const event = new Event(`netbox.select.onload.${select.name}`);
// Query Parameters - will have attributes added below.
const query = {} as Record<string, string>;
const query = { limit: 0 } as Record<string, string | number>;
if (hasUrl(select)) {
// Store the original URL, so it can be referred back to as filter-by elements change.

View File

@ -1,4 +1,5 @@
// Base Netbox Theme Overrides and Settings - color mode agnostic.
// Base NetBox Theme Overrides and Settings - color mode agnostic.
@import 'bootstrap/scss/functions';
$alt: #13293d;
@ -11,6 +12,7 @@ $green: #10b981;
$blue: #3b82f6;
$purple: #8b5cf6;
$pink: #ec4899;
$cyan: #06b6d4;
$gray-50: #f9fafb;
$gray-100: #f3f4f6;
@ -67,6 +69,17 @@ $blue-700: #1d4ed8;
$blue-800: #1e40af;
$blue-900: #1e3a8a;
$cyan-50: #ecfeff;
$cyan-100: #cffafe;
$cyan-200: #a5f3fc;
$cyan-300: #67e8f9;
$cyan-400: #22d3ee;
$cyan-500: #06b6d4;
$cyan-600: #0891b2;
$cyan-700: #0e7490;
$cyan-800: #155e75;
$cyan-900: #164e63;
$indigo-50: #eef2ff;
$indigo-100: #e0e7ff;
$indigo-200: #c7d2fe;
@ -119,6 +132,13 @@ $font-weight-lighter: 200;
$font-weight-medium: 600;
$font-weight-bolder: 800;
$font-size-xs: 0.9rem;
$line-height-base: 1.5;
$line-height-xs: 1;
$line-height-sm: 1.25;
$line-height-lg: 1.75;
$theme-color-addons: (
'alt': $alt,
'gray': $gray-400,
@ -173,6 +193,16 @@ $theme-color-addons: (
'blue-700': $blue-700,
'blue-800': $blue-800,
'blue-900': $blue-900,
'cyan-50': $cyan-50,
'cyan-100': $cyan-100,
'cyan-200': $cyan-200,
'cyan-300': $cyan-300,
'cyan-400': $cyan-400,
'cyan-500': $cyan-500,
'cyan-600': $cyan-600,
'cyan-700': $cyan-700,
'cyan-800': $cyan-800,
'cyan-900': $cyan-900,
'indigo-50': $indigo-50,
'indigo-100': $indigo-100,
'indigo-200': $indigo-200,
@ -210,3 +240,6 @@ $font-family-sans-serif: ui-sans-serif, system-ui, -apple-system, BlinkMacSystem
'Segoe UI Symbol', 'Noto Color Emoji';
$font-family-monospace: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
$accordion-padding-y: 0.8125rem;
$accordion-padding-x: 0.8125rem;

View File

@ -47,11 +47,11 @@ $box-shadow: 0 0.5rem 1rem rgba($black, 0.15);
$box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075);
$box-shadow-lg: 0 1rem 3rem rgba($black, 0.175);
$box-shadow-inset: inset 0 1px 2px rgba($black, 0.075);
// $component-active-color: $white;
// $component-active-bg: $primary;
$text-muted: $gray-500;
$text-muted: $gray-400;
$blockquote-footer-color: $gray-600;
$mark-bg: #fcf8e3;
$link-color: $primary;
$link-hover-color: $blue-200;
// Tables
$table-color: $gray-100;
@ -129,6 +129,10 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg
$nav-pills-link-active-color: $component-active-color;
$nav-pills-link-active-bg: $component-active-bg;
$navbar-light-color: $gray-500;
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>");
$navbar-light-toggler-border-color: $gray-700;
// Dropdowns
$dropdown-color: $body-color;
$dropdown-bg: $gray-900;
@ -170,8 +174,8 @@ $accordion-bg: transparent;
$accordion-border-color: rgba($white, 0.125);
$accordion-button-color: $accordion-color;
$accordion-button-bg: $accordion-bg;
$accordion-button-active-bg: tint-color($component-active-bg, 5%);
$accordion-button-active-color: shade-color($primary, 10%);
$accordion-button-active-bg: rgba($blue-300, 0.15);
$accordion-button-active-color: $gray-300;
$accordion-button-focus-border-color: $input-focus-border-color;
$accordion-icon-color: $accordion-color;
$accordion-icon-active-color: $accordion-button-active-color;
@ -259,7 +263,7 @@ $btn-close-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' v
$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
// Code
$code-color: $pink-300;
$code-color: $gray-200;
$kbd-color: $white;
$kbd-bg: $gray-300;
$pre-color: null;

View File

@ -17,5 +17,12 @@ $card-cap-color: $gray-800;
$accordion-bg: transparent;
$accordion-button-bg: $accordion-bg;
$accordion-button-active-bg: $blue-100;
$accordion-button-active-color: $gray-800;
$breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='currentColor'/%3E%3C/svg%3E");
$code-color: $gray-900;
$list-group-color: $gray-700;
$list-group-disabled-color: $gray-500;

View File

@ -10,7 +10,7 @@ from rest_framework.viewsets import ViewSet
from extras.api.views import CustomFieldModelViewSet
from netbox.api.views import ModelViewSet
from secrets import filters
from secrets import filtersets
from secrets.exceptions import InvalidKey
from secrets.models import Secret, SecretRole, SessionKey, UserKey
from utilities.utils import count_related
@ -39,7 +39,7 @@ class SecretRoleViewSet(CustomFieldModelViewSet):
secret_count=count_related(Secret, 'role')
)
serializer_class = serializers.SecretRoleSerializer
filterset_class = filters.SecretRoleFilterSet
filterset_class = filtersets.SecretRoleFilterSet
#
@ -49,7 +49,7 @@ class SecretRoleViewSet(CustomFieldModelViewSet):
class SecretViewSet(ModelViewSet):
queryset = Secret.objects.prefetch_related('role', 'tags')
serializer_class = serializers.SecretSerializer
filterset_class = filters.SecretFilterSet
filterset_class = filtersets.SecretFilterSet
master_key = None

View File

@ -2,8 +2,8 @@ import django_filters
from django.db.models import Q
from dcim.models import Device
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
from extras.filters import TagFilter
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
from virtualization.models import VirtualMachine
from .models import Secret, SecretRole
@ -14,14 +14,14 @@ __all__ = (
)
class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
class SecretRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = SecretRole
fields = ['id', 'name', 'slug']
class SecretFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
class SecretFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@ -233,7 +233,7 @@ class SessionKey(BigIDModel):
return session_key
@extras_features('custom_fields', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class SecretRole(OrganizationalModel):
"""
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles

View File

@ -1,12 +1,13 @@
from django.test import TestCase
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.filters import *
from secrets.filtersets import *
from secrets.models import Secret, SecretRole
from utilities.testing import ChangeLoggedFilterSetTests
from virtualization.models import Cluster, ClusterType, VirtualMachine
class SecretRoleTestCase(TestCase):
class SecretRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = SecretRole.objects.all()
filterset = SecretRoleFilterSet
@ -20,10 +21,6 @@ class SecretRoleTestCase(TestCase):
)
SecretRole.objects.bulk_create(roles)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['Secret Role 1', 'Secret Role 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -33,7 +30,7 @@ class SecretRoleTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class SecretTestCase(TestCase):
class SecretTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Secret.objects.all()
filterset = SecretFilterSet
@ -80,10 +77,6 @@ class SecretTestCase(TestCase):
for s in secrets:
s.save()
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self):
params = {'name': ['Secret 1', 'Secret 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -2,14 +2,14 @@ import base64
import logging
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import redirect, render
from django.utils.html import escape
from django.utils.safestring import mark_safe
from netbox.views import generic
from utilities.tables import paginate_table
from utilities.utils import count_related
from . import filters, forms, tables
from . import filtersets, forms, tables
from .models import SecretRole, Secret, SessionKey, UserKey
@ -70,7 +70,7 @@ class SecretRoleBulkEditView(generic.BulkEditView):
queryset = SecretRole.objects.annotate(
secret_count=count_related(Secret, 'role')
)
filterset = filters.SecretRoleFilterSet
filterset = filtersets.SecretRoleFilterSet
table = tables.SecretRoleTable
form = forms.SecretRoleBulkEditForm
@ -88,7 +88,7 @@ class SecretRoleBulkDeleteView(generic.BulkDeleteView):
class SecretListView(generic.ObjectListView):
queryset = Secret.objects.all()
filterset = filters.SecretFilterSet
filterset = filtersets.SecretFilterSet
filterset_form = forms.SecretFilterForm
table = tables.SecretTable
action_buttons = ('add', 'import', 'export')
@ -220,12 +220,12 @@ class SecretBulkImportView(generic.BulkImportView):
class SecretBulkEditView(generic.BulkEditView):
queryset = Secret.objects.prefetch_related('role')
filterset = filters.SecretFilterSet
filterset = filtersets.SecretFilterSet
table = tables.SecretTable
form = forms.SecretBulkEditForm
class SecretBulkDeleteView(generic.BulkDeleteView):
queryset = Secret.objects.prefetch_related('role')
filterset = filters.SecretFilterSet
filterset = filtersets.SecretFilterSet
table = tables.SecretTable

View File

@ -11,7 +11,7 @@
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="col col-md-6 offset-md-3">
<div class="card bg-danger mt-5">
<h5 class="card-header">
<i class="mdi mdi-alert"></i> Server Error

View File

@ -2,11 +2,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}Home{% endblock %} - NetBox</title>
<title>{% block title %}Home{% endblock %} | NetBox</title>
<link
rel="stylesheet"
href="{% static 'netbox.css'%}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox.css'"
href="{% static 'netbox-external.css'%}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
/>
<link
rel="stylesheet"
href="{% static 'netbox-light.css'%}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-light.css'"
/>
<link
rel="stylesheet"
href="{% static 'netbox-dark.css'%}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-dark.css'"
/>
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />

View File

@ -1,12 +1,4 @@
<div class="d-flex flex-column container-fluid mt-auto justify-content-end sidebar-bottom">
<div class="d-flex flex-column align-items-center px-2 fw-light">
<small class="text-muted">{% now 'Y-m-d H:i:s T' %}</small>
</div>
<div class="d-flex flex-column align-items-center px-2">
<small class="text-muted">
{{ settings.HOSTNAME }} (v{{ settings.VERSION }})
</small>
</div>
<nav class="nav justify-content-between mb-2 mt-4 px-2">
<a type="button" target="_blank" class="nav-link" href="https://netbox.readthedocs.io/">
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>

View File

@ -10,19 +10,13 @@
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Circuit
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td colspan="2">
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
</td>
</tr>
<tr>
<th scope="row">Provider</th>
<td>
@ -50,6 +44,12 @@
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
</td>
</tr>
<tr>
<th scope="row">Install Date</th>
<td>{{ object.install_date|placeholder }}</td>
@ -81,14 +81,14 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %}
<div class="col col-md-6">
{% 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 %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -9,7 +9,7 @@
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Circuit Type
@ -35,13 +35,13 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">
Circuits

View File

@ -4,13 +4,13 @@
{% load plugins %}
{% block breadcrumbs %}
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
<li>{{ object }}</li>
<li class="breadcrumb-item"><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
<li class="breadcrumb-item">{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-4">
<div class="col col-md-4">
<div class="card">
<h5 class="card-header">
Provider
@ -54,7 +54,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
{% include 'inc/custom_fields_panel.html' %}
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
<div class="card">
@ -71,7 +71,7 @@
</div>
{% plugin_right_page object %}
</div>
<div class="col-md-8">
<div class="col col-md-8">
<div class="card">
<h5 class="card-header">
Circuits
@ -91,7 +91,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -11,7 +11,7 @@
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Provider Network
@ -37,7 +37,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Comments
@ -56,7 +56,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">
Circuits
@ -69,7 +69,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -25,7 +25,7 @@
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Cable
@ -73,7 +73,7 @@
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:cable_list' %}
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Termination A
@ -94,7 +94,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -14,7 +14,7 @@
{{ field }}
{% endfor %}
<div class="row my-3">
<div class="col-md-5">
<div class="col col-md-5">
<div class="card h-100">
<h5 class="card-header">
A Side
@ -68,10 +68,10 @@
</div>
</div>
</div>
<div class="col-md-2 d-flex flex-column justify-content-center align-items-center">
<div class="col col-md-2 d-flex flex-column justify-content-center align-items-center">
<i class="mdi mdi-swap-horizontal-bold mdi-48px"></i>
</div>
<div class="col-md-5">
<div class="col col-md-5">
<div class="card h-100">
<h5 class="card-header">
B Side
@ -123,12 +123,12 @@
</div>
</div>
<div class="row my-3 justify-content-center">
<div class="col-md-8">
<div class="col col-md-8">
{% include 'dcim/inc/cable_form.html' %}
</div>
</div>
<div class="row my-3">
<div class="col-md-12 text-center">
<div class="col col-md-12 text-center">
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
</div>

View File

@ -7,7 +7,7 @@
{% block content %}
<div class="row">
<div class="col-md-5 col-sm-12">
<div class="col col-md-5">
<div class="cable-trace">
{% with traced_path=path.origin.trace %}
{% for near_end, cable, far_end in traced_path %}
@ -87,7 +87,7 @@
{% endwith %}
</div>
</div>
<div class="col-md-7 col-sm-12">
<div class="col col-md-7">
<div class="card">
<h5 class="card-header">

View File

@ -7,7 +7,7 @@
{% block content %}
<div class="row mb-3">
<div class="col-md-9">
<div class="col col-md-9">
<div class="card">
<div class="card-body">
{% include 'responsive_table.html' %}
@ -15,7 +15,7 @@
</div>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
<div class="col-md-3 float-end right-side-panel noprint">
<div class="col col-md-3 float-end right-side-panel noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>

View File

@ -9,7 +9,7 @@
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Console Port
@ -49,7 +49,7 @@
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all %}
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
@ -145,7 +145,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -9,7 +9,7 @@
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Console Server Port
@ -49,7 +49,7 @@
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all %}
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
@ -145,7 +145,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -7,11 +7,11 @@
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="details">
<div class="row">
<div class="col-md-6">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Device
@ -94,7 +94,7 @@
<tr>
<th scope="row">Device Type</th>
<td>
<span><a href="{{ object.device_type.get_absolute_url }}">{{ object.device_type.display_name }}</a> ({{ object.device_type.u_height }}U)</span>
<span><a href="{{ object.device_type.get_absolute_url }}">{{ object.device_type }}</a> ({{ object.device_type.u_height }}U)</span>
</td>
</tr>
<tr>
@ -228,7 +228,7 @@
</div>
{% plugin_left_page object %}
</div>
<div class="col-md-6">
<div class="col col-md-6">
{% if object.powerports.exists and object.poweroutlets.exists %}
<div class="card">
<h5 class="card-header">
@ -356,7 +356,7 @@
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
<td>{{ rd.device_type.display_name }}</td>
<td>{{ rd.device_type }}</td>
</tr>
{% endfor %}
</table>
@ -369,7 +369,7 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -9,7 +9,7 @@
{% block content %}
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="col col-md-10">
<div class="card">
<div class="card-overlay">
<div class="spinner-border" role="status">

Some files were not shown because too many files have changed in this diff Show More