mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Update to latest feature
This commit is contained in:
commit
b12ce97474
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -26,7 +26,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.5
|
||||
placeholder: v4.0.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.5
|
||||
placeholder: v4.0.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -5,10 +5,12 @@ on:
|
||||
paths-ignore:
|
||||
- 'contrib/**'
|
||||
- 'docs/**'
|
||||
- 'netbox/translations/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'contrib/**'
|
||||
- 'docs/**'
|
||||
- 'netbox/translations/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,3 +28,4 @@ netbox.pid
|
||||
.idea
|
||||
.coverage
|
||||
.vscode
|
||||
.python-version
|
||||
|
@ -17,7 +17,6 @@ NetBox exists to empower network engineers. Since its release in 2016, it has be
|
||||
<a href="#why-netbox">Why NetBox?</a> |
|
||||
<a href="#getting-started">Getting Started</a> |
|
||||
<a href="#get-involved">Get Involved</a> |
|
||||
<a href="#project-stats">Project Stats</a> |
|
||||
<a href="#screenshots">Screenshots</a>
|
||||
</p>
|
||||
|
||||
|
@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
|
||||
|
||||
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
|
||||
|
||||
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub, or email a brief description of the suspected bug and instructions for reproduction to **security@netbox.dev**. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||
If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
|
||||
|
||||
### Bug Bounties
|
||||
|
||||
|
@ -8,7 +8,9 @@ django-cors-headers
|
||||
|
||||
# Runtime UI tool for debugging Django
|
||||
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
|
||||
django-debug-toolbar
|
||||
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
|
||||
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
|
||||
django-debug-toolbar==4.3.0
|
||||
|
||||
# Library for writing reusable URL query filters
|
||||
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
|
||||
@ -108,7 +110,7 @@ Pillow
|
||||
|
||||
# PostgreSQL database adapter for Python
|
||||
# https://github.com/psycopg/psycopg/blob/master/docs/news.rst
|
||||
psycopg[binary,pool]
|
||||
psycopg[c,pool]
|
||||
|
||||
# YAML rendering library
|
||||
# https://github.com/yaml/pyyaml/blob/master/CHANGES
|
||||
|
95605
contrib/openapi2.json
95605
contrib/openapi2.json
File diff suppressed because it is too large
Load Diff
69695
contrib/openapi2.yaml
69695
contrib/openapi2.yaml
File diff suppressed because it is too large
Load Diff
@ -138,11 +138,11 @@ These two methods will load data in YAML or JSON format, respectively, from file
|
||||
|
||||
The Script object provides a set of convenient functions for recording messages at different severity levels:
|
||||
|
||||
* `log_debug(message, object=None)`
|
||||
* `log_success(message, object=None)`
|
||||
* `log_info(message, object=None)`
|
||||
* `log_warning(message, object=None)`
|
||||
* `log_failure(message, object=None)`
|
||||
* `log_debug(message=None, obj=None)`
|
||||
* `log_success(message=None, obj=None)`
|
||||
* `log_info(message=None, obj=None)`
|
||||
* `log_warning(message=None, obj=None)`
|
||||
* `log_failure(message=None, obj=None)`
|
||||
|
||||
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages. A message may optionally be associated with a particular object by passing it as the second argument to the logging method.
|
||||
|
||||
@ -152,6 +152,8 @@ A script can define one or more test methods to report on certain conditions. Al
|
||||
|
||||
These methods are detected and run automatically when the script is executed, unless its `run()` method has been overridden. (When overriding `run()`, `run_tests()` can be called to run all test methods present in the script.)
|
||||
|
||||
Calling any of these logging methods without a message will increment the relevant counter, but will not generate an output line in the script's log.
|
||||
|
||||
!!! info
|
||||
This functionality was ported from [legacy reports](./reports.md) in NetBox v4.0.
|
||||
|
||||
|
@ -126,3 +126,13 @@ VERSION = 'v3.3.2-dev'
|
||||
```
|
||||
|
||||
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
|
||||
|
||||
### Update the Public Documentation
|
||||
|
||||
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
|
||||
|
||||
First, run the `build-site` action, by navigating to Actions > build-site > Run workflow. This process compiles the documentation along with an overlay for integration with the documentation portal at <https://netboxlabs.com/docs>. The job should take about two minutes.
|
||||
|
||||
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
|
||||
|
||||
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.
|
||||
|
@ -107,3 +107,7 @@ For numeric custom fields only. The maximum valid value (optional).
|
||||
### Validation Regex
|
||||
|
||||
For string-based custom fields only. A regular expression used to validate the field's value (optional).
|
||||
|
||||
### Uniqueness Validation
|
||||
|
||||
If enabled, each object must have a unique value set for this custom field (per object type).
|
||||
|
@ -40,3 +40,7 @@ The security cipher used to apply wireless authentication. Options include:
|
||||
### Pre-Shared Key
|
||||
|
||||
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
|
||||
|
||||
### Distance
|
||||
|
||||
The numeric distance of the link, including a unit designation (e.g. 100 meters or 25 feet).
|
||||
|
@ -27,7 +27,7 @@ Serializers are responsible for converting Python objects to JSON data suitable
|
||||
|
||||
#### Example
|
||||
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class. It is generally advisable to include a `url` attribute on each serializer. This will render the direct link to access the object being rendered.
|
||||
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
|
||||
|
||||
```python
|
||||
# api/serializers.py
|
||||
@ -36,9 +36,7 @@ from netbox.api.serializers import NetBoxModelSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class MyModelSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='plugins-api:myplugin-api:mymodel-detail'
|
||||
)
|
||||
foo = SiteSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
@ -63,9 +61,7 @@ from netbox.api.serializers import WritableNestedSerializer
|
||||
from my_plugin.models import MyModel
|
||||
|
||||
class NestedMyModelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='plugins-api:myplugin-api:mymodel-detail'
|
||||
)
|
||||
foo = SiteSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
|
@ -191,7 +191,7 @@ class MyView(generic.ObjectView):
|
||||
|
||||
### Extra Template Content
|
||||
|
||||
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired method(s) to render custom content. Five methods are available:
|
||||
Plugins can inject custom content into certain areas of core NetBox views. This is accomplished by subclassing `PluginTemplateExtension`, optionally designating one or more particular NetBox models, and defining the desired method(s) to render custom content. Five methods are available:
|
||||
|
||||
| Method | View | Description |
|
||||
|---------------------|-------------|-----------------------------------------------------|
|
||||
@ -206,7 +206,9 @@ Plugins can inject custom content into certain areas of core NetBox views. This
|
||||
|
||||
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
|
||||
|
||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include:
|
||||
To control where the custom content is injected, plugin authors can specify an iterable of models by overriding the `models` attribute on the subclass. Extensions which do not specify a set of models will be invoked on every view, where supported.
|
||||
|
||||
When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data includes:
|
||||
|
||||
* `object` - The object being viewed (object views only)
|
||||
* `model` - The model of the list view (list views only)
|
||||
@ -223,7 +225,7 @@ from netbox.plugins import PluginTemplateExtension
|
||||
from .models import Animal
|
||||
|
||||
class SiteAnimalCount(PluginTemplateExtension):
|
||||
model = 'dcim.site'
|
||||
models = ['dcim.site']
|
||||
|
||||
def right_page(self):
|
||||
return self.render('netbox_animal_sounds/inc/animal_count.html', extra_context={
|
||||
|
@ -1,6 +1,36 @@
|
||||
# NetBox v4.0
|
||||
|
||||
## v4.0.6 (FUTURE)
|
||||
## v4.0.7 (FUTURE)
|
||||
|
||||
---
|
||||
|
||||
## v4.0.6 (2024-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#15348](https://github.com/netbox-community/netbox/issues/15348) - Show saved filters alongside quick search on object list views
|
||||
* [#15794](https://github.com/netbox-community/netbox/issues/15794) - Dynamically populate related objects in UI views
|
||||
* [#16256](https://github.com/netbox-community/netbox/issues/16256) - Enable alphabetical ordering of bookmarks on dashboard
|
||||
* [#16307](https://github.com/netbox-community/netbox/issues/16307) - Enable calling `log_*()` methods on Script without passing a message
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13925](https://github.com/netbox-community/netbox/issues/13925) - Fix support for "zulu" (UTC) timestamps for custom fields
|
||||
* [#14829](https://github.com/netbox-community/netbox/issues/14829) - Fix support for simple conditions (without AND/OR) in event rules
|
||||
* [#15717](https://github.com/netbox-community/netbox/issues/15717) - Allow assigning a device/VM in a site to a cluster with no site assigned
|
||||
* [#16143](https://github.com/netbox-community/netbox/issues/16143) - Display timestamps in tables in the configured timezone
|
||||
* [#16149](https://github.com/netbox-community/netbox/issues/16149) - Fix object linking in custom script logs
|
||||
* [#16252](https://github.com/netbox-community/netbox/issues/16252) - Fix total count in tab at top of rack elevations view
|
||||
* [#16273](https://github.com/netbox-community/netbox/issues/16273) - Restore global search bar on mobile
|
||||
* [#16416](https://github.com/netbox-community/netbox/issues/16416) - Retain dark/light mode toggle on mobile view
|
||||
* [#16444](https://github.com/netbox-community/netbox/issues/16444) - Disable ordering circuits list by A/Z termination
|
||||
* [#16450](https://github.com/netbox-community/netbox/issues/16450) - Searching for rack unit in form dropdown should be case-insensitive
|
||||
* [#16452](https://github.com/netbox-community/netbox/issues/16452) - Fix sizing of buttons within object attribute panels
|
||||
* [#16454](https://github.com/netbox-community/netbox/issues/16454) - Address DNS lookup bug in `django-debug-toolbar
|
||||
* [#16460](https://github.com/netbox-community/netbox/issues/16460) - Omit spaces from telephone number URLs
|
||||
* [#16512](https://github.com/netbox-community/netbox/issues/16512) - Restore a user's preferred language (if any) on login
|
||||
* [#16542](https://github.com/netbox-community/netbox/issues/16542) - Fix bulk form operations when HTMX is enabled
|
||||
* [#16702](https://github.com/netbox-community/netbox/issues/16702) - Fix validation of `return_url` query parameter
|
||||
|
||||
---
|
||||
|
||||
|
30
docs/release-notes/version-4.1.md
Normal file
30
docs/release-notes/version-4.1.md
Normal file
@ -0,0 +1,30 @@
|
||||
# NetBox v4.1
|
||||
|
||||
## v4.1.0 (FUTURE)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)).
|
||||
* The unit size for virtual disk size has been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly.
|
||||
|
||||
### New Features
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines
|
||||
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
|
||||
|
||||
### Other Changes
|
||||
|
||||
* [#14692](https://github.com/netbox-community/netbox/issues/14692) - Change atomic unit for virtual disks from 1GB to 1MB
|
||||
* [#15410](https://github.com/netbox-community/netbox/issues/15410) - Removed various deprecated filters
|
||||
* [#15908](https://github.com/netbox-community/netbox/issues/15908) - Indicate product edition in release data
|
||||
* [#16388](https://github.com/netbox-community/netbox/issues/16388) - Move all change logging resources from `extras` to `core`
|
||||
|
||||
### REST API Changes
|
||||
|
||||
* The `/api/extras/object-changes/` endpoint has moved to `/api/core/object-changes/`
|
||||
* virtualization.VirtualMachine
|
||||
* Added the optional `serial` field
|
||||
* wireless.WirelessLink
|
||||
* Added the optional `distance` and `distance_unit` fields
|
@ -296,6 +296,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- Summary: 'release-notes/index.md'
|
||||
- Version 4.1: 'release-notes/version-4.1.md'
|
||||
- Version 4.0: 'release-notes/version-4.0.md'
|
||||
- Version 3.7: 'release-notes/version-3.7.md'
|
||||
- Version 3.6: 'release-notes/version-3.6.md'
|
||||
|
@ -19,8 +19,10 @@ from django.views.generic import View
|
||||
from social_core.backends.utils import load_backends
|
||||
|
||||
from account.models import UserToken
|
||||
from extras.models import Bookmark, ObjectChange
|
||||
from extras.tables import BookmarkTable, ObjectChangeTable
|
||||
from core.models import ObjectChange
|
||||
from core.tables import ObjectChangeTable
|
||||
from extras.models import Bookmark
|
||||
from extras.tables import BookmarkTable
|
||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||
from netbox.config import get_config
|
||||
from netbox.views import generic
|
||||
@ -104,10 +106,16 @@ class LoginView(View):
|
||||
# Ensure the user has a UserConfig defined. (This should normally be handled by
|
||||
# create_userconfig() on user creation.)
|
||||
if not hasattr(request.user, 'config'):
|
||||
config = get_config()
|
||||
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
|
||||
request.user.config = get_config()
|
||||
UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save()
|
||||
|
||||
return self.redirect_to_next(request, logger)
|
||||
response = self.redirect_to_next(request, logger)
|
||||
|
||||
# Set the user's preferred language (if any)
|
||||
if language := request.user.config.get('locale.language'):
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
|
||||
@ -145,9 +153,10 @@ class LogoutView(View):
|
||||
logger.info(f"User {username} has logged out")
|
||||
messages.info(request, "You have logged out.")
|
||||
|
||||
# Delete session key cookie (if set) upon logout
|
||||
# Delete session key & language cookies (if set) upon logout
|
||||
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
|
||||
response.delete_cookie('session_key')
|
||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
return response
|
||||
|
||||
|
@ -20,11 +20,10 @@ __all__ = [
|
||||
#
|
||||
|
||||
class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
@ -35,12 +34,11 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
@ -48,11 +46,10 @@ class NestedProviderSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display', 'name', 'account']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
|
||||
|
||||
|
||||
#
|
||||
@ -63,26 +60,23 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'display', 'cid']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
|
@ -18,7 +18,6 @@ __all__ = (
|
||||
|
||||
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
# Related object counts
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
@ -26,27 +25,25 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'description',
|
||||
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'description',
|
||||
]
|
||||
|
||||
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
@ -58,15 +55,14 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
|
||||
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'cid', 'description')
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -74,8 +70,8 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end',
|
||||
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
@ -15,7 +15,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
@ -36,34 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'comments',
|
||||
'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
|
||||
|
||||
|
||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
@ -63,10 +63,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side A')
|
||||
)
|
||||
termination_z = tables.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side Z')
|
||||
)
|
||||
commit_rate = CommitRateColumn(
|
||||
|
@ -7,7 +7,7 @@ from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.query import count_related
|
||||
from utilities.views import register_model_view
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
||||
@ -26,17 +26,12 @@ class ProviderListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Provider)
|
||||
class ProviderView(generic.ObjectView):
|
||||
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -92,16 +87,12 @@ class ProviderAccountListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount)
|
||||
class ProviderAccountView(generic.ObjectView):
|
||||
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -156,19 +147,21 @@ class ProviderNetworkListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork)
|
||||
class ProviderNetworkView(generic.ObjectView):
|
||||
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
'provider_network_id',
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
extra=(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
'provider_network_id',
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -215,16 +208,12 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(CircuitType)
|
||||
class CircuitTypeView(generic.ObjectView):
|
||||
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
|
@ -14,23 +14,20 @@ __all__ = (
|
||||
|
||||
|
||||
class NestedDataSourceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDataFileSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = ['id', 'url', 'display', 'path']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'path']
|
||||
|
||||
|
||||
class NestedJobSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
status = ChoiceField(choices=JobStatusChoices)
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
@ -39,4 +36,4 @@ class NestedJobSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ['url', 'created', 'completed', 'user', 'status']
|
||||
fields = ['url', 'display_url', 'created', 'completed', 'user', 'status']
|
||||
|
@ -1,3 +1,4 @@
|
||||
from .serializers_.change_logging import *
|
||||
from .serializers_.data import *
|
||||
from .serializers_.jobs import *
|
||||
from .nested_serializers import *
|
||||
|
@ -1,8 +1,8 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import ObjectChange
|
||||
from core.choices import *
|
||||
from core.models import ObjectChange
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
@ -15,7 +15,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ObjectChangeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@ -44,8 +43,8 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
|
||||
'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
@ -13,9 +13,6 @@ __all__ = (
|
||||
|
||||
|
||||
class DataSourceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=get_data_backend_choices()
|
||||
)
|
||||
@ -30,16 +27,13 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class DataFileSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datafile-detail'
|
||||
)
|
||||
source = DataSourceSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@ -48,6 +42,6 @@ class DataFileSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = [
|
||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'path')
|
||||
|
@ -12,7 +12,6 @@ __all__ = (
|
||||
|
||||
|
||||
class JobSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@ -25,7 +24,7 @@ class JobSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
]
|
||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||
|
@ -5,12 +5,10 @@ from . import views
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CoreRootView
|
||||
|
||||
# Data sources
|
||||
router.register('data-sources', views.DataSourceViewSet)
|
||||
router.register('data-files', views.DataFileViewSet)
|
||||
|
||||
# Jobs
|
||||
router.register('jobs', views.JobViewSet)
|
||||
router.register('object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
app_name = 'core-api'
|
||||
urlpatterns = router.urls
|
||||
|
@ -8,6 +8,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from core import filtersets
|
||||
from core.models import *
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from . import serializers
|
||||
|
||||
@ -54,3 +55,13 @@ class JobViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Job.objects.all()
|
||||
serializer_class = serializers.JobSerializer
|
||||
filterset_class = filtersets.JobFilterSet
|
||||
|
||||
|
||||
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
@ -64,3 +64,20 @@ class JobStatusChoices(ChoiceSet):
|
||||
STATUS_ERRORED,
|
||||
STATUS_FAILED,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ObjectChanges
|
||||
#
|
||||
|
||||
class ObjectChangeActionChoices(ChoiceSet):
|
||||
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
|
||||
CHOICES = (
|
||||
(ACTION_CREATE, _('Created'), 'green'),
|
||||
(ACTION_UPDATE, _('Updated'), 'blue'),
|
||||
(ACTION_DELETE, _('Deleted'), 'red'),
|
||||
)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@ -5,6 +7,7 @@ import django_filters
|
||||
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -13,6 +16,7 @@ __all__ = (
|
||||
'DataFileFilterSet',
|
||||
'DataSourceFilterSet',
|
||||
'JobFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@ -126,6 +130,43 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=get_user_model().objects.all(),
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=get_user_model().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',
|
||||
'related_object_type', 'related_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)
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
@ -7,8 +7,10 @@ from core.models import *
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from netbox.forms.mixins import SavedFiltersMixin
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
||||
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
|
||||
@ -17,6 +19,7 @@ __all__ = (
|
||||
'DataFileFilterForm',
|
||||
'DataSourceFilterForm',
|
||||
'JobFilterForm',
|
||||
'ObjectChangeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@ -124,6 +127,40 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ObjectChange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('time_before', 'time_after', name=_('Time')),
|
||||
FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
|
||||
)
|
||||
time_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
time_before = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
label=_('Action'),
|
||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||
required=False
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=get_user_model().objects.all(),
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
changed_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('change_logging'),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
|
@ -6,6 +6,7 @@ from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
__all__ = (
|
||||
'DataFileFilter',
|
||||
'DataSourceFilter',
|
||||
'ObjectChangeFilter',
|
||||
)
|
||||
|
||||
|
||||
@ -19,3 +20,9 @@ class DataFileFilter(BaseFilterMixin):
|
||||
@autotype_decorator(filtersets.DataSourceFilterSet)
|
||||
class DataSourceFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@autotype_decorator(filtersets.ObjectChangeFilterSet)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
24
netbox/core/graphql/mixins.py
Normal file
24
netbox/core/graphql/mixins.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from core.models import ObjectChange
|
||||
|
||||
__all__ = (
|
||||
'ChangelogMixin',
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ChangelogMixin:
|
||||
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=self.pk
|
||||
)
|
||||
return object_changes.restrict(info.context.request.user, 'view')
|
@ -10,6 +10,7 @@ from .filters import *
|
||||
__all__ = (
|
||||
'DataFileType',
|
||||
'DataSourceType',
|
||||
'ObjectChangeType',
|
||||
)
|
||||
|
||||
|
||||
@ -30,3 +31,12 @@ class DataFileType(BaseObjectType):
|
||||
class DataSourceType(NetBoxObjectType):
|
||||
|
||||
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ObjectChange,
|
||||
fields='__all__',
|
||||
filters=ObjectChangeFilter
|
||||
)
|
||||
class ObjectChangeType(BaseObjectType):
|
||||
pass
|
||||
|
45
netbox/core/migrations/0011_move_objectchange.py
Normal file
45
netbox/core/migrations/0011_move_objectchange.py
Normal file
@ -0,0 +1,45 @@
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0010_gfk_indexes'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='ObjectChange',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('time', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('user_name', models.CharField(editable=False, max_length=150)),
|
||||
('request_id', models.UUIDField(db_index=True, editable=False)),
|
||||
('action', models.CharField(max_length=50)),
|
||||
('changed_object_id', models.PositiveBigIntegerField()),
|
||||
('related_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('object_repr', models.CharField(editable=False, max_length=200)),
|
||||
('prechange_data', models.JSONField(blank=True, editable=False, null=True)),
|
||||
('postchange_data', models.JSONField(blank=True, editable=False, null=True)),
|
||||
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'object change',
|
||||
'verbose_name_plural': 'object changes',
|
||||
'ordering': ['-time'],
|
||||
'indexes': [models.Index(fields=['changed_object_type', 'changed_object_id'], name='core_object_changed_c227ce_idx'), models.Index(fields=['related_object_type', 'related_object_id'], name='core_object_related_3375d6_idx')],
|
||||
},
|
||||
),
|
||||
],
|
||||
# Table has been renamed from 'extras' app
|
||||
database_operations=[],
|
||||
),
|
||||
]
|
@ -1,5 +1,6 @@
|
||||
from .config import *
|
||||
from .contenttypes import *
|
||||
from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .files import *
|
||||
from .jobs import *
|
||||
|
@ -8,11 +8,11 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.querysets import ObjectChangeQuerySet
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from ..querysets import ObjectChangeQuerySet
|
||||
from .contenttypes import ObjectType
|
||||
|
||||
__all__ = (
|
||||
'ObjectChange',
|
||||
@ -136,7 +136,7 @@ class ObjectChange(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:objectchange', args=[self.pk])
|
||||
return reverse('core:objectchange', args=[self.pk])
|
||||
|
||||
def get_action_color(self):
|
||||
return ObjectChangeActionChoices.colors.get(self.action)
|
26
netbox/core/querysets.py
Normal file
26
netbox/core/querysets.py
Normal file
@ -0,0 +1,26 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.utils import ProgrammingError
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'ObjectChangeQuerySet',
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeQuerySet(RestrictedQuerySet):
|
||||
|
||||
def valid_models(self):
|
||||
# Exclude any change records which refer to an instance of a model that's no longer installed. This
|
||||
# can happen when a plugin is removed but its data remains in the database, for example.
|
||||
try:
|
||||
content_types = ContentType.objects.get_for_models(*apps.get_models()).values()
|
||||
except ProgrammingError:
|
||||
# Handle the case where the database schema has not yet been initialized
|
||||
content_types = ContentType.objects.none()
|
||||
|
||||
content_type_ids = set(
|
||||
ct.pk for ct in content_types
|
||||
)
|
||||
return self.filter(changed_object_type_id__in=content_type_ids)
|
@ -1,3 +1,4 @@
|
||||
from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .jobs import *
|
||||
|
53
netbox/core/tables/change_logging.py
Normal file
53
netbox/core/tables/change_logging.py
Normal file
@ -0,0 +1,53 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectChange
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
'ObjectChangeTable',
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeTable(NetBoxTable):
|
||||
time = columns.DateTimeColumn(
|
||||
verbose_name=_('Time'),
|
||||
timespec='minutes',
|
||||
linkify=True
|
||||
)
|
||||
user_name = tables.Column(
|
||||
verbose_name=_('Username')
|
||||
)
|
||||
full_name = tables.TemplateColumn(
|
||||
accessor=tables.A('user'),
|
||||
template_code=OBJECTCHANGE_FULL_NAME,
|
||||
verbose_name=_('Full Name'),
|
||||
orderable=False
|
||||
)
|
||||
action = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Action'),
|
||||
)
|
||||
changed_object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
object_repr = tables.TemplateColumn(
|
||||
accessor=tables.A('changed_object'),
|
||||
template_code=OBJECTCHANGE_OBJECT,
|
||||
verbose_name=_('Object'),
|
||||
orderable=False
|
||||
)
|
||||
request_id = tables.TemplateColumn(
|
||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||
verbose_name=_('Request ID')
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=()
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ObjectChange
|
||||
fields = (
|
||||
'pk', 'id', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
|
||||
'actions',
|
||||
)
|
16
netbox/core/tables/template_code.py
Normal file
16
netbox/core/tables/template_code.py
Normal file
@ -0,0 +1,16 @@
|
||||
OBJECTCHANGE_FULL_NAME = """
|
||||
{% load helpers %}
|
||||
{{ value.get_full_name|placeholder }}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_OBJECT = """
|
||||
{% if value and value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% else %}
|
||||
{{ record.object_repr }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_REQUEST_ID = """
|
||||
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
|
||||
"""
|
@ -3,11 +3,12 @@ from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from core.models import ObjectType
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.models import ObjectChange, ObjectType
|
||||
from dcim.choices import SiteStatusChoices
|
||||
from dcim.models import Site
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, CustomFieldChoiceSet, ObjectChange, Tag
|
||||
from extras.models import CustomField, CustomFieldChoiceSet, Tag
|
||||
from utilities.testing import APITestCase
|
||||
from utilities.testing.utils import create_tags, post_data
|
||||
from utilities.testing.views import ModelViewTestCase
|
@ -1,7 +1,13 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
from dcim.models import Site
|
||||
from ipam.models import IPAddress
|
||||
from users.models import User
|
||||
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
|
||||
from ..choices import *
|
||||
from ..filtersets import *
|
||||
from ..models import *
|
||||
@ -132,3 +138,99 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2',
|
||||
]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ObjectChange.objects.all()
|
||||
filterset = ObjectChangeFilterSet
|
||||
ignore_fields = ('prechange_data', 'postchange_data')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
users = (
|
||||
User(username='user1'),
|
||||
User(username='user2'),
|
||||
User(username='user3'),
|
||||
)
|
||||
User.objects.bulk_create(users)
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
ipaddress = IPAddress.objects.create(address='192.0.2.1/24')
|
||||
|
||||
object_changes = (
|
||||
ObjectChange(
|
||||
user=users[0],
|
||||
user_name=users[0].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[0],
|
||||
user_name=users[0].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[1],
|
||||
user_name=users[1].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[1],
|
||||
user_name=users[1].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[2],
|
||||
user_name=users[2].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[2],
|
||||
user_name=users[2].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
)
|
||||
ObjectChange.objects.bulk_create(object_changes)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'Site 1'}
|
||||
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)
|
||||
params = {'user': ['user1', 'user2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_user_name(self):
|
||||
params = {'user_name': ['user1', 'user2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_changed_object_type(self):
|
||||
params = {'changed_object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import DataSource
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
@ -10,8 +10,11 @@ from django_rq.workers import get_worker
|
||||
from rq.job import Job as RQ_Job, JobStatus
|
||||
from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.models import *
|
||||
from dcim.models import Site
|
||||
from users.models import User
|
||||
from utilities.testing import TestCase, ViewTestCases, create_tags
|
||||
from ..models import *
|
||||
|
||||
|
||||
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@ -99,6 +102,43 @@ class DataFileTestCase(
|
||||
DataFile.objects.bulk_create(data_files)
|
||||
|
||||
|
||||
# TODO: Convert to StandardTestCases.Views
|
||||
class ObjectChangeTestCase(TestCase):
|
||||
user_permissions = (
|
||||
'core.view_objectchange',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
# Create three ObjectChanges
|
||||
user = User.objects.create_user(username='testuser2')
|
||||
for i in range(1, 4):
|
||||
oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
oc.user = user
|
||||
oc.request_id = uuid.uuid4()
|
||||
oc.save()
|
||||
|
||||
def test_objectchange_list(self):
|
||||
|
||||
url = reverse('core:objectchange_list')
|
||||
params = {
|
||||
"user": User.objects.first().pk,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
def test_objectchange(self):
|
||||
|
||||
objectchange = ObjectChange.objects.first()
|
||||
response = self.client.get(objectchange.get_absolute_url())
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class BackgroundTaskTestCase(TestCase):
|
||||
user_permissions = ()
|
||||
|
||||
|
@ -25,6 +25,10 @@ urlpatterns = (
|
||||
path('jobs/<int:pk>/', views.JobView.as_view(), name='job'),
|
||||
path('jobs/<int:pk>/delete/', views.JobDeleteView.as_view(), name='job_delete'),
|
||||
|
||||
# Change logging
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path('changelog/<int:pk>/', include(get_model_urls('core', 'objectchange'))),
|
||||
|
||||
# Background Tasks
|
||||
path('background-queues/', views.BackgroundQueueListView.as_view(), name='background_queue_list'),
|
||||
path('background-queues/<int:queue_index>/<str:status>/', views.BackgroundTaskListView.as_view(), name='background_task_list'),
|
||||
|
@ -29,10 +29,11 @@ from netbox.config import get_config, PARAMS
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
||||
@ -51,16 +52,12 @@ class DataSourceListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(DataSource)
|
||||
class DataSourceView(generic.ObjectView):
|
||||
class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = DataSource.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -176,6 +173,75 @@ class JobBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.JobTable
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeListView(generic.ObjectListView):
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
filterset = filtersets.ObjectChangeFilterSet
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = tables.ObjectChangeTable
|
||||
template_name = 'core/objectchange_list.html'
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ObjectChange)
|
||||
class ObjectChangeView(generic.ObjectView):
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_changes = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
request_id=instance.request_id
|
||||
).exclude(
|
||||
pk=instance.pk
|
||||
)
|
||||
related_changes_table = tables.ObjectChangeTable(
|
||||
data=related_changes[:50],
|
||||
orderable=False
|
||||
)
|
||||
|
||||
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
changed_object_type=instance.changed_object_type,
|
||||
changed_object_id=instance.changed_object_id,
|
||||
)
|
||||
|
||||
next_change = objectchanges.filter(time__gt=instance.time).order_by('time').first()
|
||||
prev_change = objectchanges.filter(time__lt=instance.time).order_by('-time').first()
|
||||
|
||||
if not instance.prechange_data and instance.action in ['update', 'delete'] and prev_change:
|
||||
non_atomic_change = True
|
||||
prechange_data = prev_change.postchange_data_clean
|
||||
else:
|
||||
non_atomic_change = False
|
||||
prechange_data = instance.prechange_data_clean
|
||||
|
||||
if prechange_data and instance.postchange_data:
|
||||
diff_added = shallow_compare_dict(
|
||||
prechange_data or dict(),
|
||||
instance.postchange_data_clean or dict(),
|
||||
exclude=['last_updated'],
|
||||
)
|
||||
diff_removed = {
|
||||
x: prechange_data.get(x) for x in diff_added
|
||||
} if prechange_data else {}
|
||||
else:
|
||||
diff_added = None
|
||||
diff_removed = None
|
||||
|
||||
return {
|
||||
'diff_added': diff_added,
|
||||
'diff_removed': diff_removed,
|
||||
'next_change': next_change,
|
||||
'prev_change': prev_change,
|
||||
'related_changes_table': related_changes_table,
|
||||
'related_changes_count': related_changes.count(),
|
||||
'non_atomic_change': non_atomic_change
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Config Revisions
|
||||
#
|
||||
|
@ -57,34 +57,31 @@ __all__ = [
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Site
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
@ -95,46 +92,42 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
|
||||
class Meta:
|
||||
model = models.RackRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count',),
|
||||
)
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
fields = ['id', 'url', 'display', 'name', 'device_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'device_count']
|
||||
|
||||
|
||||
class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||
user = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RackReservation
|
||||
fields = ['id', 'url', 'display', 'user', 'units']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'user', 'units']
|
||||
|
||||
def get_user(self, obj):
|
||||
return obj.user.username
|
||||
@ -148,34 +141,31 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('devicetype_count',),
|
||||
)
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = RelatedObjectCountField('device_types')
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'devicetype_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count',),
|
||||
)
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
device_count = RelatedObjectCountField('instances')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
|
||||
|
||||
|
||||
class NestedModuleTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleType
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'manufacturer', 'model']
|
||||
|
||||
|
||||
#
|
||||
@ -183,84 +173,74 @@ class NestedModuleTypeSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutletTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.RearPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedModuleBayTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBayTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBayTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemTemplate
|
||||
fields = ['id', 'url', 'display', 'name', '_depth']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', '_depth']
|
||||
|
||||
|
||||
#
|
||||
@ -271,171 +251,154 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||
)
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||
)
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = models.Platform
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Module
|
||||
fields = ['id', 'url', 'display', 'serial']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'serial']
|
||||
|
||||
|
||||
class NestedModuleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
|
||||
module_type = NestedModuleTypeSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Module
|
||||
fields = ['id', 'url', 'display', 'device', 'module_bay', 'module_type']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RearPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display', 'installed_module', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'installed_module', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = ['id', 'url', 'display', 'device', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('inventoryitem_count',),
|
||||
)
|
||||
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'inventoryitem_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'inventoryitem_count']
|
||||
|
||||
|
||||
#
|
||||
@ -443,11 +406,10 @@ class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||
#
|
||||
|
||||
class NestedCableSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
fields = ['id', 'url', 'display', 'label']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'label']
|
||||
|
||||
|
||||
#
|
||||
@ -458,13 +420,12 @@ class NestedCableSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('member_count',),
|
||||
)
|
||||
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'url', 'display', 'name', 'master', 'member_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
@ -475,27 +436,24 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
exclude_fields=('powerfeed_count',),
|
||||
)
|
||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
fields = ['id', 'url', 'display', 'name', 'powerfeed_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'powerfeed_count']
|
||||
|
||||
|
||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedVirtualDeviceContextSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualDeviceContext
|
||||
fields = ['id', 'url', 'display', 'name', 'identifier', 'device']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'identifier', 'device']
|
||||
|
@ -7,7 +7,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import Cable, CablePath, CableTermination
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import GenericObjectSerializer, NetBoxModelSerializer
|
||||
from netbox.api.serializers import BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
@ -21,7 +21,6 @@ __all__ = (
|
||||
|
||||
|
||||
class CableSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
a_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||
@ -31,27 +30,26 @@ class CableSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant',
|
||||
'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'label', 'description')
|
||||
|
||||
|
||||
class TracedCableSerializer(serializers.ModelSerializer):
|
||||
class TracedCableSerializer(BaseModelSerializer):
|
||||
"""
|
||||
Used only while tracing a cable path.
|
||||
"""
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = [
|
||||
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description',
|
||||
'id', 'url', 'display_url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description',
|
||||
]
|
||||
|
||||
|
||||
class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail')
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||
)
|
||||
@ -60,8 +58,8 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CableTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
|
||||
'termination', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
|
@ -41,7 +41,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -63,7 +62,7 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer,
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
@ -72,7 +71,6 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer,
|
||||
|
||||
|
||||
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -94,7 +92,7 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
@ -103,7 +101,6 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
|
||||
|
||||
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -121,8 +118,8 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
|
||||
'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
@ -130,7 +127,6 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
|
||||
|
||||
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -159,8 +155,8 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port',
|
||||
'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', '_occupied',
|
||||
]
|
||||
@ -168,7 +164,6 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
|
||||
|
||||
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
vdcs = SerializedPKRelatedField(
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
@ -224,11 +219,11 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
|
||||
'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
|
||||
'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
|
||||
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
||||
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description',
|
||||
'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width',
|
||||
'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link',
|
||||
'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||
]
|
||||
@ -250,7 +245,6 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
|
||||
|
||||
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -263,9 +257,9 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
|
||||
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||
|
||||
@ -274,15 +268,13 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
"""
|
||||
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
|
||||
"""
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
fields = ['id', 'url', 'display', 'name', 'label', 'description']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -296,7 +288,7 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
@ -304,7 +296,6 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
|
||||
|
||||
class ModuleBaySerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
installed_module = ModuleSerializer(
|
||||
nested=True,
|
||||
@ -316,28 +307,26 @@ class ModuleBaySerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = ModuleBay
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'name', 'installed_module', 'label', 'position',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
|
||||
|
||||
|
||||
class DeviceBaySerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device',
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
|
||||
|
||||
|
||||
class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -353,9 +342,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
|
||||
'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer',
|
||||
'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
|
||||
'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
|
||||
|
||||
|
@ -29,7 +29,6 @@ __all__ = (
|
||||
|
||||
|
||||
class DeviceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device_type = DeviceTypeSerializer(nested=True)
|
||||
role = DeviceRoleSerializer(nested=True)
|
||||
tenant = TenantSerializer(
|
||||
@ -78,13 +77,13 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
|
||||
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
|
||||
'module_bay_count', 'inventory_item_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
@ -105,13 +104,13 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||
'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
|
||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
|
||||
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
|
||||
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
@ -120,7 +119,6 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
|
||||
|
||||
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
@ -135,15 +133,14 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'interface_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip',
|
||||
'primary_ip4', 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'interface_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
|
||||
|
||||
|
||||
class ModuleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
device = DeviceSerializer(nested=True)
|
||||
module_bay = NestedModuleBaySerializer()
|
||||
module_type = ModuleTypeSerializer(nested=True)
|
||||
@ -152,7 +149,7 @@ class ModuleSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial',
|
||||
'asset_tag', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
|
||||
|
@ -32,7 +32,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -54,14 +53,13 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -83,14 +81,13 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
|
||||
'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -113,14 +110,13 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw',
|
||||
'allocated_draw', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
|
||||
'maximum_draw', 'allocated_draw', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -154,14 +150,13 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
|
||||
'power_port', 'feed_leg', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -201,14 +196,13 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only',
|
||||
'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled',
|
||||
'mgmt_only', 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
required=False,
|
||||
nested=True,
|
||||
@ -226,14 +220,13 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||
'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||
'positions', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -252,14 +245,13 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port',
|
||||
'rear_port_position', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True
|
||||
)
|
||||
@ -267,26 +259,27 @@ class ModuleBayTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'name', 'label', 'description',
|
||||
'created', 'last_updated'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
|
||||
device_type = DeviceTypeSerializer(
|
||||
nested=True
|
||||
)
|
||||
@ -313,8 +306,9 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id',
|
||||
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
||||
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer',
|
||||
'part_id', 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated',
|
||||
'_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth')
|
||||
|
||||
|
@ -17,7 +17,6 @@ __all__ = (
|
||||
|
||||
|
||||
class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = ManufacturerSerializer(nested=True)
|
||||
default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
|
||||
u_height = serializers.DecimalField(
|
||||
@ -51,26 +50,25 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
|
||||
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||
'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'device_count', 'console_port_template_count', 'console_server_port_template_count',
|
||||
'power_port_template_count', 'power_outlet_template_count', 'interface_template_count',
|
||||
'front_port_template_count', 'rear_port_template_count', 'device_bay_template_count',
|
||||
'module_bay_template_count', 'inventory_item_template_count',
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number',
|
||||
'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
|
||||
'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'device_count', 'console_port_template_count',
|
||||
'console_server_port_template_count', 'power_port_template_count', 'power_outlet_template_count',
|
||||
'interface_template_count', 'front_port_template_count', 'rear_port_template_count',
|
||||
'device_bay_template_count', 'module_bay_template_count', 'inventory_item_template_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
|
||||
|
||||
|
||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||
manufacturer = ManufacturerSerializer(nested=True)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')
|
||||
|
@ -10,7 +10,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ManufacturerSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
|
||||
# Related object counts
|
||||
devicetype_count = RelatedObjectCountField('device_types')
|
||||
@ -20,7 +19,7 @@ class ManufacturerSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
|
||||
|
@ -12,7 +12,6 @@ __all__ = (
|
||||
|
||||
|
||||
class PlatformSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True)
|
||||
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
|
||||
@ -23,7 +22,7 @@ class PlatformSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
|
||||
|
@ -17,7 +17,6 @@ __all__ = (
|
||||
|
||||
|
||||
class PowerPanelSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
site = SiteSerializer(nested=True)
|
||||
location = LocationSerializer(
|
||||
nested=True,
|
||||
@ -32,14 +31,13 @@ class PowerPanelSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'powerfeed_count', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'powerfeed_count', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count')
|
||||
|
||||
|
||||
class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
power_panel = PowerPanelSerializer(nested=True)
|
||||
rack = RackSerializer(
|
||||
nested=True,
|
||||
@ -72,9 +70,9 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
|
||||
'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply',
|
||||
'phase', 'voltage', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
||||
'description', 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied')
|
||||
|
@ -20,7 +20,6 @@ __all__ = (
|
||||
|
||||
|
||||
class RackRoleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
|
||||
# Related object counts
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
@ -28,14 +27,13 @@ class RackRoleSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'rack_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'rack_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||
|
||||
|
||||
class RackSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
site = SiteSerializer(nested=True)
|
||||
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -55,16 +53,15 @@ class RackSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
||||
'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit',
|
||||
'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight',
|
||||
'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||
|
||||
|
||||
class RackReservationSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||
rack = RackSerializer(nested=True)
|
||||
user = UserSerializer(nested=True)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -72,8 +69,8 @@ class RackReservationSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = [
|
||||
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
|
||||
'comments', 'tags', 'custom_fields',
|
||||
'id', 'url', 'display_url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant',
|
||||
'description', 'comments', 'tags', 'custom_fields',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')
|
||||
|
||||
|
@ -12,7 +12,6 @@ __all__ = (
|
||||
|
||||
|
||||
class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
|
||||
# Related object counts
|
||||
@ -22,14 +21,13 @@ class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
|
||||
|
||||
|
||||
class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||
|
||||
# Related object counts
|
||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||
@ -37,7 +35,7 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'inventoryitem_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'inventoryitem_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
|
||||
|
@ -19,35 +19,32 @@ __all__ = (
|
||||
|
||||
|
||||
class RegionSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'site_count', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'site_count', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||
|
||||
|
||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||
|
||||
class Meta:
|
||||
model = SiteGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'site_count', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'site_count', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||
|
||||
|
||||
class SiteSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||
region = RegionSerializer(nested=True, required=False, allow_null=True)
|
||||
group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -72,16 +69,15 @@ class SiteSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
|
||||
'virtualmachine_count', 'vlan_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility',
|
||||
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude',
|
||||
'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count',
|
||||
'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
|
||||
|
||||
|
||||
class LocationSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
site = SiteSerializer(nested=True)
|
||||
parent = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=LocationStatusChoices, required=False)
|
||||
@ -92,7 +88,7 @@ class LocationSerializer(NestedGroupModelSerializer):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'description',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
|
||||
|
@ -10,7 +10,6 @@ __all__ = (
|
||||
|
||||
|
||||
class VirtualChassisSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
|
||||
members = NestedDeviceSerializer(many=True, read_only=True)
|
||||
|
||||
@ -20,7 +19,7 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'member_count', 'members',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'member_count', 'members',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')
|
||||
|
@ -219,9 +219,9 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
)
|
||||
|
||||
# Enable filtering rack units by ID
|
||||
q = data['q']
|
||||
if q:
|
||||
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])]
|
||||
if q := data['q']:
|
||||
q = q.lower()
|
||||
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
|
@ -465,7 +465,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
label=_('Cluster'),
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
selector=True
|
||||
selector=True,
|
||||
query_params={
|
||||
'site_id': ['$site', 'null']
|
||||
},
|
||||
)
|
||||
comments = CommentField()
|
||||
local_context_data = JSONField(
|
||||
@ -656,11 +659,6 @@ class CableForm(TenancyForm, NetBoxModelForm):
|
||||
'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
||||
'length', 'length_unit', 'description', 'comments', 'tags',
|
||||
]
|
||||
error_messages = {
|
||||
'length': {
|
||||
'max_value': _('Maximum length is 32767 (any unit)')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PowerPanelForm(NetBoxModelForm):
|
||||
|
@ -3,14 +3,10 @@ from typing import Annotated, List, Union
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core.graphql.mixins import ChangelogMixin
|
||||
from dcim import models
|
||||
from extras.graphql.mixins import (
|
||||
ChangelogMixin,
|
||||
ConfigContextMixin,
|
||||
ContactsMixin,
|
||||
CustomFieldsMixin,
|
||||
ImageAttachmentsMixin,
|
||||
TagsMixin,
|
||||
ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
)
|
||||
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
||||
from netbox.graphql.scalars import BigInt
|
||||
|
@ -393,6 +393,8 @@ class CableTraceSVG:
|
||||
labels = [f"{cable}"] if len(links) > 2 else [f"Wireless {cable}", cable.get_status_display()]
|
||||
if cable.ssid:
|
||||
description.append(f"{cable.ssid}")
|
||||
if cable.distance and cable.distance_unit:
|
||||
description.append(f"{cable.distance} {cable.get_distance_unit_display()}")
|
||||
near = [term for term in near_terminations if term.object == cable.interface_a]
|
||||
far = [term for term in far_terminations if term.object == cable.interface_b]
|
||||
if not (near and far):
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from dcim.models import Cable
|
||||
@ -35,7 +36,7 @@ class CableTerminationsColumn(tables.Column):
|
||||
|
||||
def render(self, value):
|
||||
links = [
|
||||
f'<a href="{term.get_absolute_url()}">{term}</a>' for term in self._get_terminations(value)
|
||||
f'<a href="{term.get_absolute_url()}">{escape(term)}</a>' for term in self._get_terminations(value)
|
||||
]
|
||||
return mark_safe('<br />'.join(links) or '—')
|
||||
|
||||
@ -109,7 +110,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
|
||||
status = columns.ChoiceFieldColumn()
|
||||
length = columns.TemplateColumn(
|
||||
template_code=CABLE_LENGTH,
|
||||
order_by=('_abs_length', 'length_unit')
|
||||
order_by=('_abs_length')
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
comments = columns.MarkdownColumn()
|
||||
|
@ -8,6 +8,7 @@ from dcim.models import *
|
||||
from extras.models import CustomField
|
||||
from tenancy.models import Tenant
|
||||
from utilities.data import drange
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
|
||||
class LocationTestCase(TestCase):
|
||||
@ -533,6 +534,36 @@ class DeviceTestCase(TestCase):
|
||||
device2.full_clean()
|
||||
device2.save()
|
||||
|
||||
def test_device_mismatched_site_cluster(self):
|
||||
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||
Cluster.objects.create(name='Cluster 1', type=cluster_type)
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
clusters = (
|
||||
Cluster(name='Cluster 1', type=cluster_type, site=sites[0]),
|
||||
Cluster(name='Cluster 2', type=cluster_type, site=sites[1]),
|
||||
Cluster(name='Cluster 3', type=cluster_type, site=None),
|
||||
)
|
||||
Cluster.objects.bulk_create(clusters)
|
||||
|
||||
device_type = DeviceType.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
# Device with site only should pass
|
||||
Device(name='device1', site=sites[0], device_type=device_type, role=device_role).full_clean()
|
||||
|
||||
# Device with site, cluster non-site should pass
|
||||
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[2]).full_clean()
|
||||
|
||||
# Device with mismatched site & cluster should fail
|
||||
with self.assertRaises(ValidationError):
|
||||
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean()
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
|
@ -17,7 +17,7 @@ from jinja2.exceptions import TemplateError
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
||||
from ipam.models import ASN, IPAddress, VLANGroup
|
||||
from ipam.tables import InterfaceVLANTable
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.views import generic
|
||||
@ -27,7 +27,9 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.query import count_related
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||
from utilities.views import (
|
||||
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
|
||||
)
|
||||
from virtualization.filtersets import VirtualMachineFilterSet
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.tables import VirtualMachineTable
|
||||
@ -226,19 +228,21 @@ class RegionListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Region)
|
||||
class RegionView(generic.ObjectView):
|
||||
class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Region.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
regions = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Site.objects.restrict(request.user, 'view').filter(region__in=regions), 'region_id'),
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
regions,
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -306,19 +310,21 @@ class SiteGroupListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(SiteGroup)
|
||||
class SiteGroupView(generic.ObjectView):
|
||||
class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Site.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
groups,
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -380,31 +386,25 @@ class SiteListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Site)
|
||||
class SiteView(generic.ObjectView):
|
||||
class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Site.objects.prefetch_related('tenant__group')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
# DCIM
|
||||
(Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
(Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
# Virtualization
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), 'site_id'),
|
||||
# IPAM
|
||||
(Prefix.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
|
||||
(VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=instance.pk
|
||||
), 'site'),
|
||||
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
|
||||
# Circuits
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
[CableTermination, CircuitTermination],
|
||||
(
|
||||
(VLANGroup.objects.restrict(request.user, 'view').filter(
|
||||
scope_type=ContentType.objects.get_for_model(Site),
|
||||
scope_id=instance.pk
|
||||
), 'site'),
|
||||
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
|
||||
'site_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -466,18 +466,13 @@ class LocationListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Location)
|
||||
class LocationView(generic.ObjectView):
|
||||
class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Location.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
locations = instance.get_descendants(include_self=True)
|
||||
related_models = (
|
||||
(Rack.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
|
||||
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, locations, [CableTermination]),
|
||||
}
|
||||
|
||||
|
||||
@ -541,16 +536,12 @@ class RackRoleListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(RackRole)
|
||||
class RackRoleView(generic.ObjectView):
|
||||
class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = RackRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -655,15 +646,10 @@ class RackElevationListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Rack)
|
||||
class RackView(generic.ObjectView):
|
||||
class RackView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user, 'view').filter(rack=instance), 'rack_id'),
|
||||
(PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'),
|
||||
)
|
||||
|
||||
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||
|
||||
if instance.location:
|
||||
@ -679,7 +665,7 @@ class RackView(generic.ObjectView):
|
||||
])
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance, [CableTermination]),
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
'svg_extra': svg_extra,
|
||||
@ -838,19 +824,12 @@ class ManufacturerListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Manufacturer)
|
||||
class ManufacturerView(generic.ObjectView):
|
||||
class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(DeviceType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
(ModuleType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
(InventoryItem.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
(Platform.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance, [InventoryItemTemplate]),
|
||||
}
|
||||
|
||||
|
||||
@ -912,16 +891,16 @@ class DeviceTypeListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(DeviceType)
|
||||
class DeviceTypeView(generic.ObjectView):
|
||||
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = DeviceType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance, omit=[
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
|
||||
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
|
||||
RearPortTemplate,
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
@ -1151,16 +1130,16 @@ class ModuleTypeListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(ModuleType)
|
||||
class ModuleTypeView(generic.ObjectView):
|
||||
class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ModuleType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance, omit=[
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
|
||||
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
|
||||
RearPortTemplate,
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
@ -1711,17 +1690,12 @@ class DeviceRoleListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(DeviceRole)
|
||||
class DeviceRoleView(generic.ObjectView):
|
||||
class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -1775,17 +1749,12 @@ class PlatformListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Platform)
|
||||
class PlatformView(generic.ObjectView):
|
||||
class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Platform.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Device.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
|
||||
(VirtualMachine.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -2157,22 +2126,12 @@ class ModuleListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Module)
|
||||
class ModuleView(generic.ObjectView):
|
||||
class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Module.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Interface.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(ConsolePort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(ConsoleServerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(PowerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(PowerOutlet.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(FrontPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
(RearPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -3451,8 +3410,9 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
|
||||
if membership_form.is_valid():
|
||||
|
||||
membership_form.save()
|
||||
msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
|
||||
messages.success(request, mark_safe(msg))
|
||||
messages.success(request, mark_safe(
|
||||
f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
|
||||
))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
@ -3552,16 +3512,12 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(PowerPanel)
|
||||
class PowerPanelView(generic.ObjectView):
|
||||
class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = PowerPanel.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@ -3665,16 +3621,18 @@ class VirtualDeviceContextListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(VirtualDeviceContext)
|
||||
class VirtualDeviceContextView(generic.ObjectView):
|
||||
class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
extra=(
|
||||
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ from rest_framework.serializers import ValidationError
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
|
||||
from extras.models import CustomField
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
@ -75,7 +76,7 @@ class CustomFieldsDataField(Field):
|
||||
|
||||
# Serialize object and multi-object values
|
||||
for cf in self._get_custom_fields():
|
||||
if cf.name in data and data[cf.name] not in (None, []) and cf.type in (
|
||||
if cf.name in data and data[cf.name] not in CUSTOMFIELD_EMPTY_VALUES and cf.type in (
|
||||
CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
||||
):
|
||||
|
@ -22,79 +22,69 @@ __all__ = [
|
||||
|
||||
|
||||
class NestedEventRuleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.EventRule
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedWebhookSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedCustomFieldSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedCustomFieldChoiceSetSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomFieldChoiceSet
|
||||
fields = ['id', 'url', 'display', 'name', 'choices_count']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'choices_count']
|
||||
|
||||
|
||||
class NestedCustomLinkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.CustomLink
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConfigContextSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigContext
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConfigTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ConfigTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedExportTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ExportTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedSavedFilterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.SavedFilter
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
class NestedBookmarkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Bookmark
|
||||
@ -102,7 +92,6 @@ class NestedBookmarkSerializer(WritableNestedSerializer):
|
||||
|
||||
|
||||
class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ImageAttachment
|
||||
@ -110,11 +99,10 @@ class NestedImageAttachmentSerializer(WritableNestedSerializer):
|
||||
|
||||
|
||||
class NestedJournalEntrySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.JournalEntry
|
||||
fields = ['id', 'url', 'display', 'created']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'created']
|
||||
|
||||
|
||||
class NestedScriptSerializer(WritableNestedSerializer):
|
||||
@ -123,12 +111,17 @@ class NestedScriptSerializer(WritableNestedSerializer):
|
||||
lookup_field='full_name',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
display_url = serializers.HyperlinkedIdentityField(
|
||||
view_name='extras:script',
|
||||
lookup_field='full_name',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
name = serializers.CharField(read_only=True)
|
||||
display = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Script
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name']
|
||||
|
||||
def get_display(self, obj):
|
||||
return f'{obj.name} ({obj.module})'
|
||||
|
@ -1,7 +1,6 @@
|
||||
from .serializers_.objecttypes import *
|
||||
from .serializers_.attachments import *
|
||||
from .serializers_.bookmarks import *
|
||||
from .serializers_.change_logging import *
|
||||
from .serializers_.customfields import *
|
||||
from .serializers_.customlinks import *
|
||||
from .serializers_.dashboard import *
|
||||
|
@ -14,7 +14,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
|
||||
object_type = ContentTypeField(
|
||||
queryset=ObjectType.objects.all()
|
||||
)
|
||||
@ -23,8 +22,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'image_height',
|
||||
'image_width', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image',
|
||||
'image_height', 'image_width', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'image')
|
||||
|
||||
|
@ -14,7 +14,6 @@ __all__ = (
|
||||
|
||||
|
||||
class BookmarkSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
|
||||
object_type = ContentTypeField(
|
||||
queryset=ObjectType.objects.with_feature('bookmarks'),
|
||||
)
|
||||
|
@ -20,7 +20,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
|
||||
regions = SerializedPKRelatedField(
|
||||
queryset=Region.objects.all(),
|
||||
serializer=RegionSerializer,
|
||||
@ -123,9 +122,9 @@ class ConfigContextSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
|
||||
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
|
||||
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'weight', 'description', 'is_active', 'regions',
|
||||
'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
|
||||
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path',
|
||||
'data_file', 'data_synced', 'data', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
@ -11,7 +11,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
|
||||
data_source = DataSourceSerializer(
|
||||
nested=True,
|
||||
required=False
|
||||
@ -24,7 +23,7 @@ class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer
|
||||
class Meta:
|
||||
model = ConfigTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
|
||||
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'description', 'environment_params', 'template_code',
|
||||
'data_source', 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
@ -16,7 +16,6 @@ __all__ = (
|
||||
|
||||
|
||||
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||
base_choices = ChoiceField(
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
required=False
|
||||
@ -31,14 +30,13 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
||||
'choices_count', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'description', 'base_choices', 'extra_choices',
|
||||
'order_alphabetically', 'choices_count', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
|
||||
|
||||
|
||||
class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||
many=True
|
||||
@ -62,10 +60,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label',
|
||||
'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
|
||||
'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||
'choice_set', 'comments', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
|
||||
'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
||||
'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum',
|
||||
'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
@ -11,7 +11,6 @@ __all__ = (
|
||||
|
||||
|
||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||
many=True
|
||||
@ -20,7 +19,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
||||
'button_class', 'new_window', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url',
|
||||
'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name')
|
||||
|
@ -21,7 +21,6 @@ __all__ = (
|
||||
#
|
||||
|
||||
class EventRuleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||
many=True
|
||||
@ -35,7 +34,7 @@ class EventRuleSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = EventRule
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete',
|
||||
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete',
|
||||
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
|
||||
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
@ -58,13 +57,12 @@ class EventRuleSerializer(NetBoxModelSerializer):
|
||||
#
|
||||
|
||||
class WebhookSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
|
||||
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
|
||||
'tags', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'description', 'payload_url', 'http_method',
|
||||
'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
'custom_fields', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
@ -12,7 +12,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||
many=True
|
||||
@ -29,7 +28,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
|
||||
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
|
||||
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
|
||||
'last_updated',
|
||||
]
|
||||
|
@ -16,7 +16,6 @@ __all__ = (
|
||||
|
||||
|
||||
class JournalEntrySerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ObjectType.objects.all()
|
||||
)
|
||||
@ -35,8 +34,8 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = [
|
||||
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
|
||||
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object',
|
||||
'created', 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'created')
|
||||
|
||||
|
@ -11,7 +11,6 @@ __all__ = (
|
||||
|
||||
|
||||
class SavedFilterSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ObjectType.objects.all(),
|
||||
many=True
|
||||
@ -20,7 +19,7 @@ class SavedFilterSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = SavedFilter
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
|
||||
'shared', 'parameters', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight',
|
||||
'enabled', 'shared', 'parameters', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
||||
|
@ -14,7 +14,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ScriptSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
|
||||
description = serializers.SerializerMethodField(read_only=True)
|
||||
vars = serializers.SerializerMethodField(read_only=True)
|
||||
result = JobSerializer(nested=True, read_only=True)
|
||||
@ -22,7 +21,7 @@ class ScriptSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = Script
|
||||
fields = [
|
||||
'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
|
||||
'id', 'url', 'display_url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
@ -11,7 +11,6 @@ __all__ = (
|
||||
|
||||
|
||||
class TagSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ObjectType.objects.with_feature('tags'),
|
||||
many=True,
|
||||
@ -24,7 +23,7 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
|
||||
'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'object_types',
|
||||
'tagged_items', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
|
||||
|
@ -21,7 +21,6 @@ router.register('journal-entries', views.JournalEntryViewSet)
|
||||
router.register('config-contexts', views.ConfigContextViewSet)
|
||||
router.register('config-templates', views.ConfigTemplateViewSet)
|
||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||
router.register('object-changes', views.ObjectChangeViewSet)
|
||||
router.register('object-types', views.ObjectTypeViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
|
@ -271,20 +271,6 @@ class ScriptViewSet(ModelViewSet):
|
||||
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Object types
|
||||
#
|
||||
|
@ -117,27 +117,14 @@ class BookmarkOrderingChoices(ChoiceSet):
|
||||
|
||||
ORDERING_NEWEST = '-created'
|
||||
ORDERING_OLDEST = 'created'
|
||||
ORDERING_ALPHABETICAL_AZ = 'name'
|
||||
ORDERING_ALPHABETICAL_ZA = '-name'
|
||||
|
||||
CHOICES = (
|
||||
(ORDERING_NEWEST, _('Newest')),
|
||||
(ORDERING_OLDEST, _('Oldest')),
|
||||
)
|
||||
|
||||
#
|
||||
# ObjectChanges
|
||||
#
|
||||
|
||||
|
||||
class ObjectChangeActionChoices(ChoiceSet):
|
||||
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
|
||||
CHOICES = (
|
||||
(ACTION_CREATE, _('Created'), 'green'),
|
||||
(ACTION_UPDATE, _('Updated'), 'blue'),
|
||||
(ACTION_DELETE, _('Deleted'), 'red'),
|
||||
(ORDERING_ALPHABETICAL_AZ, _('Alphabetical (A-Z)')),
|
||||
(ORDERING_ALPHABETICAL_ZA, _('Alphabetical (Z-A)')),
|
||||
)
|
||||
|
||||
|
||||
|
@ -135,23 +135,23 @@ class ConditionSet:
|
||||
def __init__(self, ruleset):
|
||||
if type(ruleset) is not dict:
|
||||
raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset)))
|
||||
if len(ruleset) != 1:
|
||||
raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format(
|
||||
ruleset=len(ruleset)))
|
||||
|
||||
# Determine the logic type
|
||||
logic = list(ruleset.keys())[0]
|
||||
if type(logic) is not str or logic.lower() not in (AND, OR):
|
||||
raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format(
|
||||
logic=logic, op_and=AND, op_or=OR
|
||||
))
|
||||
self.logic = logic.lower()
|
||||
if len(ruleset) == 1:
|
||||
self.logic = (list(ruleset.keys())[0]).lower()
|
||||
if self.logic not in (AND, OR):
|
||||
raise ValueError(_("Invalid logic type: must be 'AND' or 'OR'. Please check documentation."))
|
||||
|
||||
# Compile the set of Conditions
|
||||
self.conditions = [
|
||||
ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
|
||||
for rule in ruleset[self.logic]
|
||||
]
|
||||
# Compile the set of Conditions
|
||||
self.conditions = [
|
||||
ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
|
||||
for rule in ruleset[self.logic]
|
||||
]
|
||||
else:
|
||||
try:
|
||||
self.logic = None
|
||||
self.conditions = [Condition(**ruleset)]
|
||||
except TypeError:
|
||||
raise ValueError(_("Incorrect key(s) informed. Please check documentation."))
|
||||
|
||||
def eval(self, data):
|
||||
"""
|
||||
|
@ -1,3 +1,5 @@
|
||||
from extras.choices import LogLevelChoices
|
||||
|
||||
# Events
|
||||
EVENT_CREATE = 'create'
|
||||
EVENT_UPDATE = 'update'
|
||||
@ -5,6 +7,8 @@ EVENT_DELETE = 'delete'
|
||||
EVENT_JOB_START = 'job_start'
|
||||
EVENT_JOB_END = 'job_end'
|
||||
|
||||
# Custom fields
|
||||
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])
|
||||
|
||||
# Webhooks
|
||||
HTTP_CONTENT_TYPE_JSON = 'application/json'
|
||||
@ -128,8 +132,17 @@ DEFAULT_DASHBOARD = [
|
||||
'title': 'Change Log',
|
||||
'color': 'blue',
|
||||
'config': {
|
||||
'model': 'extras.objectchange',
|
||||
'model': 'core.objectchange',
|
||||
'page_size': 25,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
LOG_LEVEL_RANK = {
|
||||
LogLevelChoices.LOG_DEFAULT: 0,
|
||||
LogLevelChoices.LOG_DEBUG: 1,
|
||||
LogLevelChoices.LOG_SUCCESS: 2,
|
||||
LogLevelChoices.LOG_INFO: 3,
|
||||
LogLevelChoices.LOG_WARNING: 4,
|
||||
LogLevelChoices.LOG_FAILURE: 5,
|
||||
}
|
||||
|
@ -381,11 +381,17 @@ class BookmarksWidget(DashboardWidget):
|
||||
if request.user.is_anonymous:
|
||||
bookmarks = list()
|
||||
else:
|
||||
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||
user_bookmarks = Bookmark.objects.filter(user=request.user)
|
||||
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
|
||||
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower())
|
||||
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
|
||||
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
|
||||
else:
|
||||
bookmarks = user_bookmarks.order_by(self.config['order_by'])
|
||||
if object_types := self.config.get('object_types'):
|
||||
models = get_models_from_content_types(object_types)
|
||||
conent_types = ObjectType.objects.get_for_models(*models).values()
|
||||
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||
content_types = ObjectType.objects.get_for_models(*models).values()
|
||||
bookmarks = bookmarks.filter(object_type__in=content_types)
|
||||
if max_items := self.config.get('max_items'):
|
||||
bookmarks = bookmarks[:max_items]
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -6,6 +8,7 @@ from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
from django_rq import get_queue
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.models import Job
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
@ -13,7 +16,7 @@ from netbox.registry import registry
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.rqworker import get_rq_retry
|
||||
from utilities.serialization import serialize_object
|
||||
from .choices import *
|
||||
from .choices import EventRuleActionChoices
|
||||
from .models import EventRule
|
||||
|
||||
logger = logging.getLogger('netbox.events_processor')
|
||||
|
@ -26,7 +26,6 @@ __all__ = (
|
||||
'ImageAttachmentFilterSet',
|
||||
'JournalEntryFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'ObjectTypeFilterSet',
|
||||
'SavedFilterFilterSet',
|
||||
'ScriptFilterSet',
|
||||
@ -155,7 +154,7 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
|
||||
fields = (
|
||||
'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
||||
'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
|
||||
'validation_regex',
|
||||
'validation_regex', 'validation_unique',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@ -645,43 +644,6 @@ class LocalConfigContextFilterSet(django_filters.FilterSet):
|
||||
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()
|
||||
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=get_user_model().objects.all(),
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=get_user_model().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',
|
||||
'related_object_type', 'related_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)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ContentTypes
|
||||
#
|
||||
|
@ -6,6 +6,7 @@ from extras.models import *
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from utilities.forms import BulkEditForm, add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
||||
|
||||
__all__ = (
|
||||
@ -64,8 +65,32 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
validation_minimum = forms.IntegerField(
|
||||
label=_('Minimum value'),
|
||||
required=False,
|
||||
)
|
||||
validation_maximum = forms.IntegerField(
|
||||
label=_('Maximum value'),
|
||||
required=False,
|
||||
)
|
||||
validation_regex = forms.CharField(
|
||||
label=_('Validation regex'),
|
||||
required=False
|
||||
)
|
||||
validation_unique = forms.NullBooleanField(
|
||||
label=_('Must be unique'),
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('group_name', 'description', 'weight', 'choice_set', name=_('Attributes')),
|
||||
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
|
||||
FieldSet(
|
||||
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
||||
),
|
||||
)
|
||||
nullable_fields = ('group_name', 'description', 'choice_set')
|
||||
|
||||
|
||||
|
@ -71,7 +71,8 @@ class CustomFieldImportForm(CSVModelForm):
|
||||
fields = (
|
||||
'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
|
||||
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
|
||||
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments',
|
||||
'validation_maximum', 'validation_regex', 'validation_unique', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||
'comments',
|
||||
)
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
__all__ = (
|
||||
@ -28,7 +28,6 @@ __all__ = (
|
||||
'ImageAttachmentFilterForm',
|
||||
'JournalEntryFilterForm',
|
||||
'LocalConfigContextFilterForm',
|
||||
'ObjectChangeFilterForm',
|
||||
'SavedFilterFilterForm',
|
||||
'TagFilterForm',
|
||||
'WebhookFilterForm',
|
||||
@ -42,6 +41,9 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
|
||||
'ui_editable', 'is_cloneable', name=_('Attributes')
|
||||
),
|
||||
FieldSet(
|
||||
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
||||
),
|
||||
)
|
||||
related_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||
@ -90,6 +92,25 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
validation_minimum = forms.IntegerField(
|
||||
label=_('Minimum value'),
|
||||
required=False
|
||||
)
|
||||
validation_maximum = forms.IntegerField(
|
||||
label=_('Maximum value'),
|
||||
required=False
|
||||
)
|
||||
validation_regex = forms.CharField(
|
||||
label=_('Validation regex'),
|
||||
required=False
|
||||
)
|
||||
validation_unique = forms.NullBooleanField(
|
||||
label=_('Must be unique'),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||
@ -475,37 +496,3 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ObjectChange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('time_before', 'time_after', name=_('Time')),
|
||||
FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
|
||||
)
|
||||
time_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
time_before = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
label=_('Action'),
|
||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||
required=False
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=get_user_model().objects.all(),
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
changed_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('change_logging'),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
)
|
||||
|
@ -64,7 +64,9 @@ class CustomFieldForm(forms.ModelForm):
|
||||
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
|
||||
),
|
||||
FieldSet('default', 'choice_set', name=_('Values')),
|
||||
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
|
||||
FieldSet(
|
||||
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -13,7 +13,6 @@ __all__ = (
|
||||
'ExportTemplateFilter',
|
||||
'ImageAttachmentFilter',
|
||||
'JournalEntryFilter',
|
||||
'ObjectChangeFilter',
|
||||
'SavedFilterFilter',
|
||||
'TagFilter',
|
||||
'WebhookFilter',
|
||||
@ -68,12 +67,6 @@ class JournalEntryFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@autotype_decorator(filtersets.ObjectChangeFilterSet)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SavedFilter, lookups=True)
|
||||
@autotype_decorator(filtersets.SavedFilterFilterSet)
|
||||
class SavedFilterFilter(BaseFilterMixin):
|
||||
|
@ -2,12 +2,8 @@ from typing import TYPE_CHECKING, Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from extras.models import ObjectChange
|
||||
|
||||
__all__ = (
|
||||
'ChangelogMixin',
|
||||
'ConfigContextMixin',
|
||||
'ContactsMixin',
|
||||
'CustomFieldsMixin',
|
||||
@ -17,23 +13,10 @@ __all__ = (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import ImageAttachmentType, JournalEntryType, ObjectChangeType, TagType
|
||||
from .types import ImageAttachmentType, JournalEntryType, TagType
|
||||
from tenancy.graphql.types import ContactAssignmentType
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ChangelogMixin:
|
||||
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=self.pk
|
||||
)
|
||||
return object_changes.restrict(info.context.request.user, 'view')
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ConfigContextMixin:
|
||||
|
||||
|
@ -18,7 +18,6 @@ __all__ = (
|
||||
'ExportTemplateType',
|
||||
'ImageAttachmentType',
|
||||
'JournalEntryType',
|
||||
'ObjectChangeType',
|
||||
'SavedFilterType',
|
||||
'TagType',
|
||||
'WebhookType',
|
||||
@ -123,15 +122,6 @@ class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
created_by: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ObjectChange,
|
||||
fields='__all__',
|
||||
filters=ObjectChangeFilter
|
||||
)
|
||||
class ObjectChangeType(BaseObjectType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.SavedFilter,
|
||||
exclude=['content_types',],
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user