mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-28 03:16:25 -06:00
Merge branch 'feature' into 5278-remove-secrets
# Conflicts: # netbox/netbox/constants.py # netbox/project-static/dist/netbox.js # netbox/project-static/dist/netbox.js.map # netbox/secrets/api/views.py # netbox/secrets/filtersets.py # netbox/secrets/models.py # netbox/secrets/tests/test_filtersets.py # netbox/secrets/views.py
This commit is contained in:
commit
d86856e546
39
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
39
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -5,21 +5,25 @@ labels: ["type: bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a
|
||||
current NetBox installation. If you're having trouble with installation or just
|
||||
looking for assistance with using NetBox, please visit our
|
||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead."
|
||||
value: >
|
||||
**NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox
|
||||
installation. If you're having trouble with installation or just looking for
|
||||
assistance with using NetBox, please visit our
|
||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: "What version of NetBox are you currently running?"
|
||||
placeholder: v2.10.4
|
||||
description: >
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v2.11.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Python version
|
||||
description: "What version of Python are you currently running?"
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- 3.6
|
||||
- 3.7
|
||||
@ -30,12 +34,13 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: "Describe in detail the exact steps that someone else can take to
|
||||
reproduce this bug using the current stable release of NetBox. Begin with the
|
||||
creation of any necessary database objects and call out every operation being
|
||||
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
|
||||
the raw HTTP request(s) being made: Don't rely on a client library such as
|
||||
pynetbox."
|
||||
description: >
|
||||
Describe in detail the exact steps that someone else can take to
|
||||
reproduce this bug using the current stable release of NetBox. Begin with the
|
||||
creation of any necessary database objects and call out every operation being
|
||||
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
|
||||
the raw HTTP request(s) being made: Don't rely on a client library such as
|
||||
pynetbox."
|
||||
placeholder: |
|
||||
1. Click on "create widget"
|
||||
2. Set foo to 12 and bar to G
|
||||
@ -45,14 +50,14 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: "What did you expect to happen?"
|
||||
placeholder: "A new widget should have been created with the specified attributes"
|
||||
description: What did you expect to happen?
|
||||
placeholder: A new widget should have been created with the specified attributes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Observed Behavior
|
||||
description: "What happened instead?"
|
||||
placeholder: "A TypeError exception was raised"
|
||||
description: What happened instead?
|
||||
placeholder: A TypeError exception was raised
|
||||
validations:
|
||||
required: true
|
||||
|
@ -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
|
||||
|
36
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
36
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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.)
|
||||
|
12
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
12
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
@ -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
|
||||
|
5
.github/workflows/stale.yml
vendored
5
.github/workflows/stale.yml
vendored
@ -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: >
|
||||
|
59
README.md
59
README.md
@ -1,4 +1,6 @@
|
||||

|
||||
<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** |  |
|
||||
| **develop** |  |
|
||||
|
||||
<div align="center">
|
||||
<h4>Thank you to our sponsors!</h4>
|
||||
|
||||
[](https://ns1.com/)
|
||||
|
||||
[](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** |  |
|
||||
| **develop** |  |
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Related projects
|
||||
|
||||
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
|
||||
for a list of relevant community projects.
|
||||
|
@ -33,6 +33,16 @@ The `as_attachment` attribute of an export template controls its behavior when r
|
||||
|
||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||
|
||||
## REST API Integration
|
||||
|
||||
When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/optional-settings.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example:
|
||||
|
||||
```
|
||||
GET /api/dcim/sites/?export=MyTemplateName
|
||||
```
|
||||
|
||||
Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list.
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -2,7 +2,38 @@
|
||||
|
||||
## v2.12-beta1 (FUTURE)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#3665](https://github.com/netbox-community/netbox/issues/3665) - Enable rendering export templates via REST API
|
||||
* [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized
|
||||
* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit
|
||||
* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6
|
||||
* [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar
|
||||
* [#6338](https://github.com/netbox-community/netbox/issues/6338) - Decimal fields are no longer coerced to strings in REST API
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* dcim.Cable
|
||||
* `length` is now a decimal value
|
||||
* dcim.Device
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* dcim.DeviceType
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* dcim.Rack
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* dcim.Site
|
||||
* `latitude` and `longitude` are now decimal fields rather than strings
|
||||
* extras.ContentType
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* ipam.Prefix
|
||||
* Added the `mark_utilized` boolean field
|
||||
* ipam.VLAN
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* ipam.VRF
|
||||
* Removed the `display_name` attribute (use `display` instead)
|
||||
* virtualization.VirtualMachine
|
||||
* `vcpus` is now a decimal field rather than a string
|
||||
|
@ -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']
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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():
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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', {
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
#
|
||||
|
@ -1001,6 +1001,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
TYPE_MMF_OM2 = 'mmf-om2'
|
||||
TYPE_MMF_OM3 = 'mmf-om3'
|
||||
TYPE_MMF_OM4 = 'mmf-om4'
|
||||
TYPE_MMF_OM5 = 'mmf-om5'
|
||||
TYPE_SMF = 'smf'
|
||||
TYPE_SMF_OS1 = 'smf-os1'
|
||||
TYPE_SMF_OS2 = 'smf-os2'
|
||||
@ -1031,6 +1032,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
(TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
|
||||
(TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
|
||||
(TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
|
||||
(TYPE_MMF_OM5, 'Multimode Fiber (OM5)'),
|
||||
(TYPE_SMF, 'Singlemode Fiber'),
|
||||
(TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
|
||||
(TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
|
||||
@ -1062,14 +1064,21 @@ class CableStatusChoices(ChoiceSet):
|
||||
|
||||
class CableLengthUnitChoices(ChoiceSet):
|
||||
|
||||
# Metric
|
||||
UNIT_KILOMETER = 'km'
|
||||
UNIT_METER = 'm'
|
||||
UNIT_CENTIMETER = 'cm'
|
||||
|
||||
# Imperial
|
||||
UNIT_MILE = 'mi'
|
||||
UNIT_FOOT = 'ft'
|
||||
UNIT_INCH = 'in'
|
||||
|
||||
CHOICES = (
|
||||
(UNIT_KILOMETER, 'Kilometers'),
|
||||
(UNIT_METER, 'Meters'),
|
||||
(UNIT_CENTIMETER, 'Centimeters'),
|
||||
(UNIT_MILE, 'Miles'),
|
||||
(UNIT_FOOT, 'Feet'),
|
||||
(UNIT_INCH, 'Inches'),
|
||||
)
|
||||
|
@ -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 ''
|
||||
|
@ -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',
|
@ -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(
|
||||
|
16
netbox/dcim/migrations/0132_cable_length.py
Normal file
16
netbox/dcim/migrations/0132_cable_length.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0131_consoleport_speed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cable',
|
||||
name='length',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||
),
|
||||
]
|
@ -74,7 +74,9 @@ class Cable(PrimaryModel):
|
||||
color = ColorField(
|
||||
blank=True
|
||||
)
|
||||
length = models.PositiveSmallIntegerField(
|
||||
length = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ __all__ = (
|
||||
# Device Types
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Manufacturer(OrganizationalModel):
|
||||
"""
|
||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||
@ -320,10 +320,6 @@ class DeviceType(PrimaryModel):
|
||||
if self.rear_image:
|
||||
self.rear_image.delete(save=False)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return f'{self.manufacturer.name} {self.model}'
|
||||
|
||||
@property
|
||||
def is_parent_device(self):
|
||||
return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT
|
||||
@ -337,7 +333,7 @@ class DeviceType(PrimaryModel):
|
||||
# Devices
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceRole(OrganizationalModel):
|
||||
"""
|
||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||
@ -388,7 +384,7 @@ class DeviceRole(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Platform(OrganizationalModel):
|
||||
"""
|
||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
||||
@ -616,7 +612,13 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super().__str__()
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||
elif self.device_type:
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
return super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
@ -710,7 +712,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
pass
|
||||
|
||||
# Validate primary IP addresses
|
||||
vc_interfaces = self.vc_interfaces()
|
||||
vc_interfaces = self.vc_interfaces(if_master=False)
|
||||
if self.primary_ip4:
|
||||
if self.primary_ip4.family != 4:
|
||||
raise ValidationError({
|
||||
@ -817,17 +819,6 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
self.comments,
|
||||
)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.virtual_chassis:
|
||||
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
|
||||
elif self.device_type:
|
||||
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
|
||||
else:
|
||||
return '' # Device has not yet been created
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
@ -850,9 +841,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
|
||||
@property
|
||||
def interfaces_count(self):
|
||||
if self.virtual_chassis and self.virtual_chassis.master == self:
|
||||
return self.vc_interfaces().count()
|
||||
return self.interfaces.count()
|
||||
return self.vc_interfaces().count()
|
||||
|
||||
def get_vc_master(self):
|
||||
"""
|
||||
@ -860,7 +849,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
"""
|
||||
return self.virtual_chassis.master if self.virtual_chassis else None
|
||||
|
||||
def vc_interfaces(self, if_master=False):
|
||||
def vc_interfaces(self, if_master=True):
|
||||
"""
|
||||
Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
|
||||
Device belonging to the same VirtualChassis.
|
||||
@ -868,7 +857,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
:param if_master: If True, return VC member interfaces only if this Device is the VC master.
|
||||
"""
|
||||
filter = Q(device=self)
|
||||
if self.virtual_chassis and (not if_master or self.virtual_chassis.master == self):
|
||||
if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master):
|
||||
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
|
||||
return Interface.objects.filter(filter)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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)
|
@ -23,7 +23,7 @@ from utilities.tables import paginate_table
|
||||
from utilities.utils import csv_format, count_related
|
||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
from .models import (
|
||||
@ -106,7 +106,7 @@ class RegionListView(generic.ObjectListView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset = filtersets.RegionFilterSet
|
||||
filterset_form = forms.RegionFilterForm
|
||||
table = tables.RegionTable
|
||||
|
||||
@ -162,7 +162,7 @@ class RegionBulkEditView(generic.BulkEditView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset = filtersets.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
form = forms.RegionBulkEditForm
|
||||
|
||||
@ -175,7 +175,7 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset = filtersets.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
@ -191,7 +191,7 @@ class SiteGroupListView(generic.ObjectListView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
filterset_form = forms.SiteGroupFilterForm
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
@ -247,7 +247,7 @@ class SiteGroupBulkEditView(generic.BulkEditView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
form = forms.SiteGroupBulkEditForm
|
||||
|
||||
@ -260,7 +260,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
@ -270,7 +270,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class SiteListView(generic.ObjectListView):
|
||||
queryset = Site.objects.all()
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset = filtersets.SiteFilterSet
|
||||
filterset_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
|
||||
@ -325,14 +325,14 @@ class SiteBulkImportView(generic.BulkImportView):
|
||||
|
||||
class SiteBulkEditView(generic.BulkEditView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset = filtersets.SiteFilterSet
|
||||
table = tables.SiteTable
|
||||
form = forms.SiteBulkEditForm
|
||||
|
||||
|
||||
class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset = filtersets.SiteFilterSet
|
||||
table = tables.SiteTable
|
||||
|
||||
|
||||
@ -354,7 +354,7 @@ class LocationListView(generic.ObjectListView):
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset = filtersets.LocationFilterSet
|
||||
filterset_form = forms.LocationFilterForm
|
||||
table = tables.LocationTable
|
||||
|
||||
@ -413,7 +413,7 @@ class LocationBulkEditView(generic.BulkEditView):
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset = filtersets.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
form = forms.LocationBulkEditForm
|
||||
|
||||
@ -426,7 +426,7 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset = filtersets.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
@ -477,7 +477,7 @@ class RackRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
filterset = filters.RackRoleFilterSet
|
||||
filterset = filtersets.RackRoleFilterSet
|
||||
table = tables.RackRoleTable
|
||||
form = forms.RackRoleBulkEditForm
|
||||
|
||||
@ -499,7 +499,7 @@ class RackListView(generic.ObjectListView):
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
)
|
||||
filterset = filters.RackFilterSet
|
||||
filterset = filtersets.RackFilterSet
|
||||
filterset_form = forms.RackFilterForm
|
||||
table = tables.RackDetailTable
|
||||
|
||||
@ -512,7 +512,7 @@ class RackElevationListView(generic.ObjectListView):
|
||||
|
||||
def get(self, request):
|
||||
|
||||
racks = filters.RackFilterSet(request.GET, self.queryset).qs
|
||||
racks = filtersets.RackFilterSet(request.GET, self.queryset).qs
|
||||
total_count = racks.count()
|
||||
|
||||
# Determine ordering
|
||||
@ -601,14 +601,14 @@ class RackBulkImportView(generic.BulkImportView):
|
||||
|
||||
class RackBulkEditView(generic.BulkEditView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
filterset = filtersets.RackFilterSet
|
||||
table = tables.RackTable
|
||||
form = forms.RackBulkEditForm
|
||||
|
||||
|
||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
filterset = filtersets.RackFilterSet
|
||||
table = tables.RackTable
|
||||
|
||||
|
||||
@ -618,7 +618,7 @@ class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RackReservationListView(generic.ObjectListView):
|
||||
queryset = RackReservation.objects.all()
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
filterset_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
|
||||
@ -661,14 +661,14 @@ class RackReservationImportView(generic.BulkImportView):
|
||||
|
||||
class RackReservationBulkEditView(generic.BulkEditView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
form = forms.RackReservationBulkEditForm
|
||||
|
||||
|
||||
class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
|
||||
|
||||
@ -691,6 +691,8 @@ class ManufacturerView(generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
devicetypes = DeviceType.objects.restrict(request.user, 'view').filter(
|
||||
manufacturer=instance
|
||||
).annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
|
||||
devicetypes_table = tables.DeviceTypeTable(devicetypes)
|
||||
@ -721,7 +723,7 @@ class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
)
|
||||
filterset = filters.ManufacturerFilterSet
|
||||
filterset = filtersets.ManufacturerFilterSet
|
||||
table = tables.ManufacturerTable
|
||||
form = forms.ManufacturerBulkEditForm
|
||||
|
||||
@ -741,7 +743,7 @@ class DeviceTypeListView(generic.ObjectListView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
filterset_form = forms.DeviceTypeFilterForm
|
||||
table = tables.DeviceTypeTable
|
||||
|
||||
@ -847,7 +849,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
table = tables.DeviceTypeTable
|
||||
form = forms.DeviceTypeBulkEditForm
|
||||
|
||||
@ -856,7 +858,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
table = tables.DeviceTypeTable
|
||||
|
||||
|
||||
@ -1189,7 +1191,7 @@ class DeviceRoleBulkEditView(generic.BulkEditView):
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
vm_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
filterset = filters.DeviceRoleFilterSet
|
||||
filterset = filtersets.DeviceRoleFilterSet
|
||||
table = tables.DeviceRoleTable
|
||||
form = forms.DeviceRoleBulkEditForm
|
||||
|
||||
@ -1248,7 +1250,7 @@ class PlatformBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PlatformBulkEditView(generic.BulkEditView):
|
||||
queryset = Platform.objects.all()
|
||||
filterset = filters.PlatformFilterSet
|
||||
filterset = filtersets.PlatformFilterSet
|
||||
table = tables.PlatformTable
|
||||
form = forms.PlatformBulkEditForm
|
||||
|
||||
@ -1264,7 +1266,7 @@ class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class DeviceListView(generic.ObjectListView):
|
||||
queryset = Device.objects.all()
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
filterset_form = forms.DeviceFilterForm
|
||||
table = tables.DeviceTable
|
||||
template_name = 'dcim/device_list.html'
|
||||
@ -1400,7 +1402,7 @@ class DeviceInterfacesView(generic.ObjectView):
|
||||
template_name = 'dcim/device/interfaces.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related(
|
||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||
'lag', 'cable', '_path__destination', 'tags',
|
||||
@ -1522,7 +1524,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
|
||||
template_name = 'dcim/device/lldp_neighbors.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related(
|
||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||
'_path__destination'
|
||||
).exclude(
|
||||
type__in=NONCONNECTABLE_IFACE_TYPES
|
||||
@ -1595,14 +1597,14 @@ class ChildDeviceBulkImportView(generic.BulkImportView):
|
||||
|
||||
class DeviceBulkEditView(generic.BulkEditView):
|
||||
queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
form = forms.DeviceBulkEditForm
|
||||
|
||||
|
||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
|
||||
|
||||
@ -1612,7 +1614,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsolePortListView(generic.ObjectListView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
filterset_form = forms.ConsolePortFilterForm
|
||||
table = tables.ConsolePortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -1647,7 +1649,7 @@ class ConsolePortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class ConsolePortBulkEditView(generic.BulkEditView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
table = tables.ConsolePortTable
|
||||
form = forms.ConsolePortBulkEditForm
|
||||
|
||||
@ -1662,7 +1664,7 @@ class ConsolePortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class ConsolePortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
table = tables.ConsolePortTable
|
||||
|
||||
|
||||
@ -1672,7 +1674,7 @@ class ConsolePortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsoleServerPortListView(generic.ObjectListView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
filterset_form = forms.ConsoleServerPortFilterForm
|
||||
table = tables.ConsoleServerPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -1707,7 +1709,7 @@ class ConsoleServerPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class ConsoleServerPortBulkEditView(generic.BulkEditView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
table = tables.ConsoleServerPortTable
|
||||
form = forms.ConsoleServerPortBulkEditForm
|
||||
|
||||
@ -1722,7 +1724,7 @@ class ConsoleServerPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
table = tables.ConsoleServerPortTable
|
||||
|
||||
|
||||
@ -1732,7 +1734,7 @@ class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerPortListView(generic.ObjectListView):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
filterset_form = forms.PowerPortFilterForm
|
||||
table = tables.PowerPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -1767,7 +1769,7 @@ class PowerPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerPortBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
table = tables.PowerPortTable
|
||||
form = forms.PowerPortBulkEditForm
|
||||
|
||||
@ -1782,7 +1784,7 @@ class PowerPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class PowerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
table = tables.PowerPortTable
|
||||
|
||||
|
||||
@ -1792,7 +1794,7 @@ class PowerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerOutletListView(generic.ObjectListView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
filterset_form = forms.PowerOutletFilterForm
|
||||
table = tables.PowerOutletTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -1827,7 +1829,7 @@ class PowerOutletBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerOutletBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
table = tables.PowerOutletTable
|
||||
form = forms.PowerOutletBulkEditForm
|
||||
|
||||
@ -1842,7 +1844,7 @@ class PowerOutletBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class PowerOutletBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
table = tables.PowerOutletTable
|
||||
|
||||
|
||||
@ -1852,7 +1854,7 @@ class PowerOutletBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InterfaceListView(generic.ObjectListView):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
filterset_form = forms.InterfaceFilterForm
|
||||
table = tables.InterfaceTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -1922,7 +1924,7 @@ class InterfaceBulkImportView(generic.BulkImportView):
|
||||
|
||||
class InterfaceBulkEditView(generic.BulkEditView):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
|
||||
@ -1937,7 +1939,7 @@ class InterfaceBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class InterfaceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
table = tables.InterfaceTable
|
||||
|
||||
|
||||
@ -1947,7 +1949,7 @@ class InterfaceBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class FrontPortListView(generic.ObjectListView):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
filterset_form = forms.FrontPortFilterForm
|
||||
table = tables.FrontPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -1982,7 +1984,7 @@ class FrontPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class FrontPortBulkEditView(generic.BulkEditView):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
table = tables.FrontPortTable
|
||||
form = forms.FrontPortBulkEditForm
|
||||
|
||||
@ -1997,7 +1999,7 @@ class FrontPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class FrontPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
table = tables.FrontPortTable
|
||||
|
||||
|
||||
@ -2007,7 +2009,7 @@ class FrontPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RearPortListView(generic.ObjectListView):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
filterset_form = forms.RearPortFilterForm
|
||||
table = tables.RearPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -2042,7 +2044,7 @@ class RearPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class RearPortBulkEditView(generic.BulkEditView):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
table = tables.RearPortTable
|
||||
form = forms.RearPortBulkEditForm
|
||||
|
||||
@ -2057,7 +2059,7 @@ class RearPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class RearPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
table = tables.RearPortTable
|
||||
|
||||
|
||||
@ -2067,7 +2069,7 @@ class RearPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class DeviceBayListView(generic.ObjectListView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
filterset_form = forms.DeviceBayFilterForm
|
||||
table = tables.DeviceBayTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -2167,7 +2169,7 @@ class DeviceBayBulkImportView(generic.BulkImportView):
|
||||
|
||||
class DeviceBayBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
table = tables.DeviceBayTable
|
||||
form = forms.DeviceBayBulkEditForm
|
||||
|
||||
@ -2178,7 +2180,7 @@ class DeviceBayBulkRenameView(generic.BulkRenameView):
|
||||
|
||||
class DeviceBayBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
table = tables.DeviceBayTable
|
||||
|
||||
|
||||
@ -2188,7 +2190,7 @@ class DeviceBayBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InventoryItemListView(generic.ObjectListView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
filterset = filters.InventoryItemFilterSet
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
filterset_form = forms.InventoryItemFilterForm
|
||||
table = tables.InventoryItemTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -2222,7 +2224,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
|
||||
|
||||
class InventoryItemBulkEditView(generic.BulkEditView):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
|
||||
filterset = filters.InventoryItemFilterSet
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
table = tables.InventoryItemTable
|
||||
form = forms.InventoryItemBulkEditForm
|
||||
|
||||
@ -2247,7 +2249,7 @@ class DeviceBulkAddConsolePortView(generic.BulkComponentCreateView):
|
||||
form = forms.ConsolePortBulkCreateForm
|
||||
queryset = ConsolePort.objects.all()
|
||||
model_form = forms.ConsolePortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2258,7 +2260,7 @@ class DeviceBulkAddConsoleServerPortView(generic.BulkComponentCreateView):
|
||||
form = forms.ConsoleServerPortBulkCreateForm
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2269,7 +2271,7 @@ class DeviceBulkAddPowerPortView(generic.BulkComponentCreateView):
|
||||
form = forms.PowerPortBulkCreateForm
|
||||
queryset = PowerPort.objects.all()
|
||||
model_form = forms.PowerPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2280,7 +2282,7 @@ class DeviceBulkAddPowerOutletView(generic.BulkComponentCreateView):
|
||||
form = forms.PowerOutletBulkCreateForm
|
||||
queryset = PowerOutlet.objects.all()
|
||||
model_form = forms.PowerOutletForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2291,7 +2293,7 @@ class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView):
|
||||
form = forms.InterfaceBulkCreateForm
|
||||
queryset = Interface.objects.all()
|
||||
model_form = forms.InterfaceForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2302,7 +2304,7 @@ class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView):
|
||||
# form = forms.FrontPortBulkCreateForm
|
||||
# queryset = FrontPort.objects.all()
|
||||
# model_form = forms.FrontPortForm
|
||||
# filterset = filters.DeviceFilterSet
|
||||
# filterset = filtersets.DeviceFilterSet
|
||||
# table = tables.DeviceTable
|
||||
# default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2313,7 +2315,7 @@ class DeviceBulkAddRearPortView(generic.BulkComponentCreateView):
|
||||
form = forms.RearPortBulkCreateForm
|
||||
queryset = RearPort.objects.all()
|
||||
model_form = forms.RearPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2324,7 +2326,7 @@ class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
||||
form = forms.DeviceBayBulkCreateForm
|
||||
queryset = DeviceBay.objects.all()
|
||||
model_form = forms.DeviceBayForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2335,7 +2337,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
|
||||
form = forms.InventoryItemBulkCreateForm
|
||||
queryset = InventoryItem.objects.all()
|
||||
model_form = forms.InventoryItemForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -2346,7 +2348,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
|
||||
|
||||
class CableListView(generic.ObjectListView):
|
||||
queryset = Cable.objects.all()
|
||||
filterset = filters.CableFilterSet
|
||||
filterset = filtersets.CableFilterSet
|
||||
filterset_form = forms.CableFilterForm
|
||||
table = tables.CableTable
|
||||
action_buttons = ('import', 'export')
|
||||
@ -2479,14 +2481,14 @@ class CableBulkImportView(generic.BulkImportView):
|
||||
|
||||
class CableBulkEditView(generic.BulkEditView):
|
||||
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
|
||||
filterset = filters.CableFilterSet
|
||||
filterset = filtersets.CableFilterSet
|
||||
table = tables.CableTable
|
||||
form = forms.CableBulkEditForm
|
||||
|
||||
|
||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
|
||||
filterset = filters.CableFilterSet
|
||||
filterset = filtersets.CableFilterSet
|
||||
table = tables.CableTable
|
||||
|
||||
|
||||
@ -2496,7 +2498,7 @@ class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device')
|
||||
filterset = filters.ConsoleConnectionFilterSet
|
||||
filterset = filtersets.ConsoleConnectionFilterSet
|
||||
filterset_form = forms.ConsoleConnectionFilterForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
@ -2526,7 +2528,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
|
||||
class PowerConnectionsListView(generic.ObjectListView):
|
||||
queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device')
|
||||
filterset = filters.PowerConnectionFilterSet
|
||||
filterset = filtersets.PowerConnectionFilterSet
|
||||
filterset_form = forms.PowerConnectionFilterForm
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
@ -2560,7 +2562,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
_path__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
).order_by('device')
|
||||
filterset = filters.InterfaceConnectionFilterSet
|
||||
filterset = filtersets.InterfaceConnectionFilterSet
|
||||
filterset_form = forms.InterfaceConnectionFilterForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
@ -2599,7 +2601,7 @@ class VirtualChassisListView(generic.ObjectListView):
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
)
|
||||
table = tables.VirtualChassisTable
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
filterset_form = forms.VirtualChassisFilterForm
|
||||
|
||||
|
||||
@ -2807,14 +2809,14 @@ class VirtualChassisBulkImportView(generic.BulkImportView):
|
||||
|
||||
class VirtualChassisBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
table = tables.VirtualChassisTable
|
||||
form = forms.VirtualChassisBulkEditForm
|
||||
|
||||
|
||||
class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
table = tables.VirtualChassisTable
|
||||
|
||||
|
||||
@ -2828,7 +2830,7 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
filterset_form = forms.PowerPanelFilterForm
|
||||
table = tables.PowerPanelTable
|
||||
|
||||
@ -2868,7 +2870,7 @@ class PowerPanelBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
form = forms.PowerPanelBulkEditForm
|
||||
|
||||
@ -2879,7 +2881,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
|
||||
|
||||
@ -2889,7 +2891,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerFeedListView(generic.ObjectListView):
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
filterset_form = forms.PowerFeedFilterForm
|
||||
table = tables.PowerFeedTable
|
||||
|
||||
@ -2915,7 +2917,7 @@ class PowerFeedBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerFeedBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
table = tables.PowerFeedTable
|
||||
form = forms.PowerFeedBulkEditForm
|
||||
|
||||
@ -2926,5 +2928,5 @@ class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
table = tables.PowerFeedTable
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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
341
netbox/extras/filtersets.py
Normal 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']
|
@ -300,13 +300,12 @@ class ExportTemplate(BigIDModel):
|
||||
|
||||
# Build the response
|
||||
response = HttpResponse(output, content_type=mime_type)
|
||||
filename = 'netbox_{}{}'.format(
|
||||
queryset.model._meta.verbose_name_plural,
|
||||
'.{}'.format(self.file_extension) if self.file_extension else ''
|
||||
)
|
||||
|
||||
if self.as_attachment:
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
basename = queryset.model._meta.verbose_name_plural.replace(' ', '_')
|
||||
extension = f'.{self.file_extension}' if self.file_extension else ''
|
||||
filename = f'netbox_{basename}{extension}'
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
|
||||
return response
|
||||
|
||||
|
@ -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 *
|
||||
|
@ -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)
|
@ -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
|
||||
|
||||
|
||||
|
@ -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']
|
||||
|
||||
|
||||
#
|
||||
|
@ -44,8 +44,7 @@ class VRFSerializer(PrimaryModelSerializer):
|
||||
model = VRF
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets',
|
||||
'export_targets', 'tags', 'display_name', 'custom_fields', 'created', 'last_updated', 'ipaddress_count',
|
||||
'prefix_count',
|
||||
'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'prefix_count',
|
||||
]
|
||||
|
||||
|
||||
@ -167,7 +166,7 @@ class VLANSerializer(PrimaryModelSerializer):
|
||||
model = VLAN
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags',
|
||||
'display_name', 'custom_fields', 'created', 'last_updated', 'prefix_count',
|
||||
'custom_fields', 'created', 'last_updated', 'prefix_count',
|
||||
]
|
||||
validators = []
|
||||
|
||||
@ -203,7 +202,7 @@ class PrefixSerializer(PrimaryModelSerializer):
|
||||
model = Prefix
|
||||
fields = [
|
||||
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = ['family']
|
||||
|
||||
|
@ -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
|
||||
|
@ -6,11 +6,11 @@ from django.db.models import Q
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
|
||||
NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
@ -31,7 +31,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -74,7 +74,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, C
|
||||
fields = ['id', 'name', 'rd', 'enforce_unique']
|
||||
|
||||
|
||||
class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -116,14 +116,14 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
|
||||
class RIRFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['id', 'name', 'slug', 'is_private', 'description']
|
||||
|
||||
|
||||
class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -173,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
|
||||
class RoleFilterSet(OrganizationalModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -184,7 +184,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilter
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -304,7 +304,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'is_pool']
|
||||
fields = ['id', 'is_pool', 'mark_utilized']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -369,7 +369,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
)
|
||||
|
||||
|
||||
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -535,7 +535,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
return queryset.exclude(assigned_object_id__isnull=value)
|
||||
|
||||
|
||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedFilterSet):
|
||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
@ -570,7 +570,7 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CreatedUpdatedF
|
||||
)
|
||||
|
||||
|
||||
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -666,7 +666,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
return queryset.get_for_virtualmachine(value)
|
||||
|
||||
|
||||
class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
|
||||
class ServiceFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
@ -454,11 +454,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = [
|
||||
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
|
||||
'tags',
|
||||
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||
'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')),
|
||||
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
|
||||
('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
@ -582,6 +582,11 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label='Is a pool'
|
||||
)
|
||||
mark_utilized = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label='Treat as 100% utilized'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@ -597,7 +602,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
model = Prefix
|
||||
field_order = [
|
||||
'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id',
|
||||
'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool',
|
||||
'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized',
|
||||
]
|
||||
mask_length__lte = forms.IntegerField(
|
||||
widget=forms.HiddenInput()
|
||||
@ -675,6 +680,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
mark_utilized = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Marked as 100% utilized'),
|
||||
widget=StaticSelect2(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
16
netbox/ipam/migrations/0047_prefix_mark_utilized.py
Normal file
16
netbox/ipam/migrations/0047_prefix_mark_utilized.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0046_set_vlangroup_scope_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='mark_utilized',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
@ -29,7 +29,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class RIR(OrganizationalModel):
|
||||
"""
|
||||
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
||||
@ -184,7 +184,7 @@ class Aggregate(PrimaryModel):
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Role(OrganizationalModel):
|
||||
"""
|
||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||
@ -288,6 +288,10 @@ class Prefix(PrimaryModel):
|
||||
default=False,
|
||||
help_text='All IP addresses within this prefix are considered usable'
|
||||
)
|
||||
mark_utilized = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Treat as 100% utilized"
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
@ -296,10 +300,11 @@ class Prefix(PrimaryModel):
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description',
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized',
|
||||
'description',
|
||||
]
|
||||
clone_fields = [
|
||||
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
||||
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -364,6 +369,7 @@ class Prefix(PrimaryModel):
|
||||
self.get_status_display(),
|
||||
self.role.name if self.role else None,
|
||||
self.is_pool,
|
||||
self.mark_utilized,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@ -422,23 +428,18 @@ class Prefix(PrimaryModel):
|
||||
"""
|
||||
Return all available IPs within this prefix as an IPSet.
|
||||
"""
|
||||
if self.mark_utilized:
|
||||
return list()
|
||||
|
||||
prefix = netaddr.IPSet(self.prefix)
|
||||
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
|
||||
available_ips = prefix - child_ips
|
||||
|
||||
# All IP addresses within a pool are considered usable
|
||||
if self.is_pool:
|
||||
# IPv6, pool, or IPv4 /31 sets are fully usable
|
||||
if self.family == 6 or self.is_pool or self.prefix.prefixlen == 31:
|
||||
return available_ips
|
||||
|
||||
# All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable
|
||||
if (
|
||||
self.prefix.version == 4 and self.prefix.prefixlen == 31 # RFC 3021
|
||||
) or (
|
||||
self.prefix.version == 6 and self.prefix.prefixlen == 127 # RFC 6164
|
||||
):
|
||||
return available_ips
|
||||
|
||||
# Omit first and last IP address from the available set
|
||||
# For "normal" IPv4 prefixes, omit first and last addresses
|
||||
available_ips -= netaddr.IPSet([
|
||||
netaddr.IPAddress(self.prefix.first),
|
||||
netaddr.IPAddress(self.prefix.last),
|
||||
@ -469,6 +470,9 @@ class Prefix(PrimaryModel):
|
||||
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
|
||||
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
|
||||
"""
|
||||
if self.mark_utilized:
|
||||
return 100
|
||||
|
||||
if self.status == PrefixStatusChoices.STATUS_CONTAINER:
|
||||
queryset = Prefix.objects.filter(
|
||||
prefix__net_contained=str(self.prefix),
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -256,6 +256,21 @@ class RoleTable(BaseTable):
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixUtilizationColumn(UtilizationColumn):
|
||||
"""
|
||||
Extend UtilizationColumn to allow disabling the warning & danger thresholds for prefixes
|
||||
marked as fully utilized.
|
||||
"""
|
||||
template_code = """
|
||||
{% load helpers %}
|
||||
{% if record.pk and record.mark_utilized %}
|
||||
{% utilization_graph value warning_threshold=0 danger_threshold=0 %}
|
||||
{% elif record.pk %}
|
||||
{% utilization_graph value %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
class PrefixTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
prefix = tables.TemplateColumn(
|
||||
@ -283,11 +298,15 @@ class PrefixTable(BaseTable):
|
||||
is_pool = BooleanColumn(
|
||||
verbose_name='Pool'
|
||||
)
|
||||
mark_utilized = BooleanColumn(
|
||||
verbose_name='Marked Utilized'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description',
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'mark_utilized',
|
||||
'description',
|
||||
)
|
||||
default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
row_attrs = {
|
||||
@ -296,7 +315,7 @@ class PrefixTable(BaseTable):
|
||||
|
||||
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
utilization = UtilizationColumn(
|
||||
utilization = PrefixUtilizationColumn(
|
||||
accessor='get_utilization',
|
||||
orderable=False
|
||||
)
|
||||
@ -308,7 +327,7 @@ class PrefixDetailTable(PrefixTable):
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool',
|
||||
'description', 'tags',
|
||||
'mark_utilized', 'description', 'tags',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
|
||||
from ipam.choices import *
|
||||
from ipam.filters import *
|
||||
from ipam.filtersets import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class VRFTestCase(TestCase):
|
||||
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VRF.objects.all()
|
||||
filterset = VRFFilterSet
|
||||
|
||||
@ -53,10 +54,6 @@ class VRFTestCase(TestCase):
|
||||
vrfs[2].import_targets.add(route_targets[2])
|
||||
vrfs[2].export_targets.add(route_targets[2])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VRF 1', 'VRF 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -100,7 +97,7 @@ class VRFTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class RouteTargetTestCase(TestCase):
|
||||
class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RouteTarget.objects.all()
|
||||
filterset = RouteTargetFilterSet
|
||||
|
||||
@ -149,10 +146,6 @@ class RouteTargetTestCase(TestCase):
|
||||
vrfs[1].import_targets.add(route_targets[4], route_targets[5])
|
||||
vrfs[1].export_targets.add(route_targets[6], route_targets[7])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['65000:1001', '65000:1002', '65000:1003']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@ -186,7 +179,7 @@ class RouteTargetTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
|
||||
class RIRTestCase(TestCase):
|
||||
class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RIR.objects.all()
|
||||
filterset = RIRFilterSet
|
||||
|
||||
@ -203,10 +196,6 @@ class RIRTestCase(TestCase):
|
||||
)
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['RIR 1', 'RIR 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -226,7 +215,7 @@ class RIRTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class AggregateTestCase(TestCase):
|
||||
class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Aggregate.objects.all()
|
||||
filterset = AggregateFilterSet
|
||||
|
||||
@ -265,10 +254,6 @@ class AggregateTestCase(TestCase):
|
||||
)
|
||||
Aggregate.objects.bulk_create(aggregates)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '4'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@ -304,7 +289,7 @@ class AggregateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class RoleTestCase(TestCase):
|
||||
class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Role.objects.all()
|
||||
filterset = RoleFilterSet
|
||||
|
||||
@ -318,10 +303,6 @@ class RoleTestCase(TestCase):
|
||||
)
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Role 1', 'Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -331,7 +312,7 @@ class RoleTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class PrefixTestCase(TestCase):
|
||||
class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Prefix.objects.all()
|
||||
filterset = PrefixFilterSet
|
||||
|
||||
@ -408,11 +389,11 @@ class PrefixTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
prefixes = (
|
||||
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True),
|
||||
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
||||
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
||||
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True),
|
||||
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
||||
Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
||||
Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||
Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||
@ -421,10 +402,6 @@ class PrefixTestCase(TestCase):
|
||||
)
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@ -440,6 +417,12 @@ class PrefixTestCase(TestCase):
|
||||
params = {'is_pool': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
def test_mark_utilized(self):
|
||||
params = {'mark_utilized': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'mark_utilized': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
def test_within(self):
|
||||
params = {'within': '10.0.0.0/16'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
@ -528,7 +511,7 @@ class PrefixTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class IPAddressTestCase(TestCase):
|
||||
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = IPAddress.objects.all()
|
||||
filterset = IPAddressFilterSet
|
||||
|
||||
@ -607,10 +590,6 @@ class IPAddressTestCase(TestCase):
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@ -708,7 +687,7 @@ class IPAddressTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class VLANGroupTestCase(TestCase):
|
||||
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLANGroup.objects.all()
|
||||
filterset = VLANGroupFilterSet
|
||||
|
||||
@ -751,10 +730,6 @@ class VLANGroupTestCase(TestCase):
|
||||
)
|
||||
VLANGroup.objects.bulk_create(vlan_groups)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VLAN Group 1', 'VLAN Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -796,7 +771,7 @@ class VLANGroupTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class VLANTestCase(TestCase):
|
||||
class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLAN.objects.all()
|
||||
filterset = VLANFilterSet
|
||||
|
||||
@ -965,10 +940,6 @@ class VLANTestCase(TestCase):
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VLAN 101', 'VLAN 102']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@ -1041,7 +1012,7 @@ class VLANTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
|
||||
|
||||
|
||||
class ServiceTestCase(TestCase):
|
||||
class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Service.objects.all()
|
||||
filterset = ServiceFilterSet
|
||||
|
||||
@ -1080,10 +1051,6 @@ class ServiceTestCase(TestCase):
|
||||
)
|
||||
Service.objects.bulk_create(services)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:3]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Service 1', 'Service 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
@ -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
|
||||
|
@ -5,9 +5,11 @@ from collections import OrderedDict
|
||||
from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import ProtectedError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
@ -16,6 +18,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet as ModelViewSet_
|
||||
from rq.worker import Worker
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from netbox.api import BulkOperationSerializer
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
@ -222,6 +225,18 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_):
|
||||
# Check that the instance is matched by the view's queryset
|
||||
self.queryset.get(pk=instance.pk)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Overrides ListModelMixin to allow processing ExportTemplates.
|
||||
"""
|
||||
if 'export' in request.GET:
|
||||
content_type = ContentType.objects.get_for_model(self.serializer_class.Meta.model)
|
||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
return et.render_to_response(queryset)
|
||||
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
model = self.queryset.model
|
||||
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||
|
@ -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.
|
||||
|
@ -1,9 +1,9 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from circuits.filters import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
|
||||
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
|
||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
||||
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
|
||||
from dcim.filters import (
|
||||
from dcim.filtersets import (
|
||||
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
|
||||
SiteFilterSet, VirtualChassisFilterSet,
|
||||
)
|
||||
@ -12,14 +12,14 @@ from dcim.tables import (
|
||||
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable,
|
||||
VirtualChassisTable,
|
||||
)
|
||||
from ipam.filters import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
||||
from ipam.filtersets import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||
from tenancy.filters import TenantFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from tenancy.tables import TenantTable
|
||||
from utilities.utils import count_related
|
||||
from virtualization.filters import ClusterFilterSet, VirtualMachineFilterSet
|
||||
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
|
||||
|
||||
|
238
netbox/netbox/filtersets.py
Normal file
238
netbox/netbox/filtersets.py
Normal 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)
|
||||
)
|
@ -114,6 +114,7 @@ REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 're
|
||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
@ -461,6 +462,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null'
|
||||
REST_FRAMEWORK_VERSION = VERSION.rsplit('.', 1)[0] # Use major.minor as API version
|
||||
REST_FRAMEWORK = {
|
||||
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
|
||||
'COERCE_DECIMAL_TO_STRING': False,
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'netbox.api.authentication.TokenAuthentication',
|
||||
|
@ -104,12 +104,13 @@ class HomeView(View):
|
||||
for section_label, section_items in sections:
|
||||
stat = {"label": section_label, "items": []}
|
||||
for perm, item_label, description, get_count in section_items:
|
||||
app, scope = perm.split(".")
|
||||
url = ":".join((app, scope.replace("view_", "") + "_list"))
|
||||
item = {"label": item_label, "description": description, "count": None, "url": url, "disabled": True}
|
||||
if perm in perms:
|
||||
app, scope = perm.split(".")
|
||||
url = ":".join((app, scope.replace("view_", "") + "_list"))
|
||||
stat["items"].append(
|
||||
{"label": item_label, "description": description, "count": get_count(), "url": url}
|
||||
)
|
||||
item["count"] = get_count()
|
||||
item["disabled"] = False
|
||||
stat["items"].append(item)
|
||||
stats.append(stat)
|
||||
return stats
|
||||
|
||||
|
10
netbox/project-static/_dark.scss
Normal file
10
netbox/project-static/_dark.scss
Normal 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';
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
// Stylesheet for rendering SVG rack elevations
|
||||
// Entry for rack_elevation.css stylesheet.
|
||||
|
||||
@import './theme-light.scss';
|
||||
|
||||
* {
|
4
netbox/project-static/_external.scss
Normal file
4
netbox/project-static/_external.scss
Normal 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';
|
6
netbox/project-static/_light.scss
Normal file
6
netbox/project-static/_light.scss
Normal file
@ -0,0 +1,6 @@
|
||||
// Entry for netbox-light.css stylesheet.
|
||||
|
||||
@import './theme-light.scss';
|
||||
@import './bootstrap.scss';
|
||||
@import './select.scss';
|
||||
@import './netbox.scss';
|
@ -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
|
||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Normal file
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Normal file
Binary file not shown.
1
netbox/project-static/dist/netbox-dark.css.map
vendored
Normal file
1
netbox/project-static/dist/netbox-dark.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
netbox/project-static/dist/netbox-external.css
vendored
Normal file
BIN
netbox/project-static/dist/netbox-external.css
vendored
Normal file
Binary file not shown.
1
netbox/project-static/dist/netbox-external.css.map
vendored
Normal file
1
netbox/project-static/dist/netbox-external.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
netbox/project-static/dist/netbox-light.css
vendored
Normal file
BIN
netbox/project-static/dist/netbox-light.css
vendored
Normal file
Binary file not shown.
1
netbox/project-static/dist/netbox-light.css.map
vendored
Normal file
1
netbox/project-static/dist/netbox-light.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
1
netbox/project-static/dist/netbox.css.map
vendored
1
netbox/project-static/dist/netbox.css.map
vendored
File diff suppressed because one or more lines are too long
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -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';
|
||||
}
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +62,21 @@
|
||||
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);
|
||||
@ -68,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'] {
|
||||
@ -102,7 +130,14 @@ body {
|
||||
stroke: $gray-200;
|
||||
}
|
||||
}
|
||||
table {
|
||||
& 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;
|
||||
@ -142,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 {
|
||||
@ -190,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);
|
||||
@ -248,11 +302,14 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -278,8 +335,17 @@ nav.nav.nav-pills {
|
||||
top: 8.125rem;
|
||||
}
|
||||
|
||||
.sidebar-nav-link {
|
||||
color: var(--nbx-sidebar-link-color);
|
||||
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 {
|
||||
@ -287,12 +353,15 @@ nav.nav.nav-pills {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
function isSearchButton(el: any): el is SearchFilterButton {
|
||||
return el?.dataset?.searchValue ?? null !== null;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (selectedValue !== null && selectedType !== null) {
|
||||
if (isTruthy(searchValue) && selected !== searchValue) {
|
||||
selected = searchValue;
|
||||
selectedValue.innerHTML = button.textContent ?? 'Error';
|
||||
selectedType.value = searchValue;
|
||||
} else {
|
||||
selected = '';
|
||||
selectedValue.innerHTML = 'All Objects';
|
||||
selectedType.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Search Bar Elements.
|
||||
*/
|
||||
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;
|
||||
selectedValue.innerHTML = button.textContent ?? 'Error';
|
||||
selectedType.value = objectType;
|
||||
} else {
|
||||
selected = '';
|
||||
selectedType.innerHTML = 'All Objects';
|
||||
selectedType.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
button.addEventListener('click', handleClick);
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,7 +132,7 @@ $font-weight-lighter: 200;
|
||||
$font-weight-medium: 600;
|
||||
$font-weight-bolder: 800;
|
||||
|
||||
$font-size-xs: 0.8rem;
|
||||
$font-size-xs: 0.9rem;
|
||||
|
||||
$line-height-base: 1.5;
|
||||
$line-height-xs: 1;
|
||||
@ -180,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,
|
||||
@ -217,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;
|
||||
|
@ -47,7 +47,7 @@ $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);
|
||||
$text-muted: $gray-500;
|
||||
$text-muted: $gray-400;
|
||||
$blockquote-footer-color: $gray-600;
|
||||
$mark-bg: #fcf8e3;
|
||||
$link-color: $primary;
|
||||
@ -174,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;
|
||||
@ -263,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;
|
||||
|
@ -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;
|
||||
|
@ -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' %}" />
|
||||
|
@ -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>
|
||||
|
@ -17,12 +17,6 @@
|
||||
</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>
|
||||
@ -82,8 +82,8 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col 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' %}
|
||||
{% 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>
|
||||
|
@ -60,7 +60,7 @@
|
||||
<th scope="row">Length</th>
|
||||
<td>
|
||||
{% if object.length %}
|
||||
{{ object.length }} {{ object.get_length_unit_display }}
|
||||
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
|
@ -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>
|
||||
@ -339,7 +339,7 @@
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ rd.device_type.display_name }}</td>
|
||||
<td>{{ rd.device_type }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
@ -46,8 +46,8 @@
|
||||
{% block buttons %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% if obj.pk %}
|
||||
<button type="submit" formaction="?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-outline-primary">Update & Continue Editing</button>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
<button type="submit" formaction="?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
|
@ -344,7 +344,7 @@
|
||||
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
|
||||
</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.device_type.display_name }}</td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
{% if device.parent_bay %}
|
||||
<td><a href="{{ device.parent_bay.device.get_absolute_url }}">{{ device.parent_bay.device }}</a></td>
|
||||
<td>{{ device.parent_bay }}</td>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<span class="badge bg-secondary">{{ cable.get_type_display|default:"" }}</span>
|
||||
{% endif %}
|
||||
{% if cable.length %}
|
||||
({{ cable.length }} {{ cable.get_length_unit_display }})<br />
|
||||
({{ cable.length|floatformat }} {{ cable.get_length_unit_display }})<br />
|
||||
{% endif %}
|
||||
<span class="badge bg-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span><br />
|
||||
{% for tag in cable.tags.all %}
|
||||
|
@ -94,7 +94,7 @@
|
||||
<div class="col col-md-8 col col-md-offset-2 text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||
{% if vc_form.instance.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
{% endif %}
|
||||
|
@ -56,7 +56,7 @@
|
||||
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
|
||||
{% if obj.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">
|
||||
Update
|
||||
Save
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load helpers %}
|
||||
|
||||
{% block title_container %}{% endblock %}
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="stats-container">
|
||||
@ -13,7 +14,7 @@
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for item in section.items %}
|
||||
<a href="{% url item.url %}" class="list-group-item list-group-item-action">
|
||||
<a href="{% url item.url %}" class="list-group-item list-group-item-action{% if item.disabled %} disabled{% endif %}">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div class="d-flex flex-column align-items-start">
|
||||
<h6 class="mb-1">{{ item.label }}</h6>
|
||||
@ -21,7 +22,13 @@
|
||||
<small class="mb-1 text-muted">{{ item.description }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="badge bg-secondary rounded-pill">{{ item.count }}</span>
|
||||
<span class="badge stat-badge rounded-pill">
|
||||
{% if item.count == None %}
|
||||
<i class="mdi mdi-lock"></i>
|
||||
{% else %}
|
||||
{{ item.count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@ -35,7 +35,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="per_page" class="form-select" id="per_page">
|
||||
<select name="per_page" class="form-select per-page">
|
||||
{% for n in settings.PER_PAGE_DEFAULTS %}
|
||||
<option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
|
||||
{% endfor %}
|
||||
|
@ -7,10 +7,10 @@
|
||||
<div class="col col-md-5">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Prefix
|
||||
Prefix
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
||||
@ -20,7 +20,6 @@
|
||||
<span class="badge bg-info">Not a Pool</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Family</th>
|
||||
@ -79,7 +78,7 @@
|
||||
{% if object.vlan.group %}
|
||||
<a href="{{ object.vlan.group.get_absolute_url }}">{{ object.vlan.group }}</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'ipam:vlan' pk=object.vlan.pk %}">{{ object.vlan.display_name }}</a>
|
||||
<a href="{% url 'ipam:vlan' pk=object.vlan.pk %}">{{ object.vlan }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
@ -101,9 +100,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Utilization</th>
|
||||
<td>{% utilization_graph object.get_utilization %}</td>
|
||||
<td>
|
||||
{% if object.mark_utilized %}
|
||||
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
|
||||
<small>(Marked fully utilized)</small>
|
||||
{% else %}
|
||||
{% utilization_graph object.get_utilization %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user