9627 fix merge conflict

This commit is contained in:
Arthur Hanson 2024-06-25 07:46:20 -07:00
commit f96ca902cc
152 changed files with 5069 additions and 170100 deletions

View File

@ -26,7 +26,7 @@ body:
attributes: attributes:
label: NetBox Version label: NetBox Version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.0.5 placeholder: v4.0.6
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.0.5 placeholder: v4.0.6
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -5,10 +5,12 @@ on:
paths-ignore: paths-ignore:
- 'contrib/**' - 'contrib/**'
- 'docs/**' - 'docs/**'
- 'netbox/translations/**'
pull_request: pull_request:
paths-ignore: paths-ignore:
- 'contrib/**' - 'contrib/**'
- 'docs/**' - 'docs/**'
- 'netbox/translations/**'
permissions: permissions:
contents: read contents: read

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ netbox.pid
.idea .idea
.coverage .coverage
.vscode .vscode
.python-version

View File

@ -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="#why-netbox">Why NetBox?</a> |
<a href="#getting-started">Getting Started</a> | <a href="#getting-started">Getting Started</a> |
<a href="#get-involved">Get Involved</a> | <a href="#get-involved">Get Involved</a> |
<a href="#project-stats">Project Stats</a> |
<a href="#screenshots">Screenshots</a> <a href="#screenshots">Screenshots</a>
</p> </p>

View File

@ -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. 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 ### Bug Bounties

View File

@ -8,7 +8,9 @@ django-cors-headers
# Runtime UI tool for debugging Django # Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst # 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 # Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst # https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
@ -108,7 +110,7 @@ Pillow
# PostgreSQL database adapter for Python # PostgreSQL database adapter for Python
# https://github.com/psycopg/psycopg/blob/master/docs/news.rst # https://github.com/psycopg/psycopg/blob/master/docs/news.rst
psycopg[binary,pool] psycopg[c,pool]
# YAML rendering library # YAML rendering library
# https://github.com/yaml/pyyaml/blob/master/CHANGES # https://github.com/yaml/pyyaml/blob/master/CHANGES

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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: The Script object provides a set of convenient functions for recording messages at different severity levels:
* `log_debug(message, object=None)` * `log_debug(message=None, obj=None)`
* `log_success(message, object=None)` * `log_success(message=None, obj=None)`
* `log_info(message, object=None)` * `log_info(message=None, obj=None)`
* `log_warning(message, object=None)` * `log_warning(message=None, obj=None)`
* `log_failure(message, object=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. 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.) 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 !!! info
This functionality was ported from [legacy reports](./reports.md) in NetBox v4.0. This functionality was ported from [legacy reports](./reports.md) in NetBox v4.0.

View File

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

View File

@ -107,3 +107,7 @@ For numeric custom fields only. The maximum valid value (optional).
### Validation Regex ### Validation Regex
For string-based custom fields only. A regular expression used to validate the field's value (optional). 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).

View File

@ -27,7 +27,7 @@ Serializers are responsible for converting Python objects to JSON data suitable
#### Example #### 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 ```python
# api/serializers.py # api/serializers.py
@ -36,9 +36,7 @@ from netbox.api.serializers import NetBoxModelSerializer
from my_plugin.models import MyModel from my_plugin.models import MyModel
class MyModelSerializer(NetBoxModelSerializer): class MyModelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField( foo = SiteSerializer(nested=True, allow_null=True)
view_name='plugins-api:myplugin-api:mymodel-detail'
)
class Meta: class Meta:
model = MyModel model = MyModel
@ -63,9 +61,7 @@ from netbox.api.serializers import WritableNestedSerializer
from my_plugin.models import MyModel from my_plugin.models import MyModel
class NestedMyModelSerializer(WritableNestedSerializer): class NestedMyModelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField( foo = SiteSerializer(nested=True, allow_null=True)
view_name='plugins-api:myplugin-api:mymodel-detail'
)
class Meta: class Meta:
model = MyModel model = MyModel

View File

@ -1,6 +1,36 @@
# NetBox v4.0 # 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
--- ---

View File

@ -106,10 +106,16 @@ class LoginView(View):
# Ensure the user has a UserConfig defined. (This should normally be handled by # Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.) # create_userconfig() on user creation.)
if not hasattr(request.user, 'config'): if not hasattr(request.user, 'config'):
config = get_config() request.user.config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save() 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: else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}") logger.debug(f"Login form validation failed for username: {form['username'].value()}")
@ -147,9 +153,10 @@ class LogoutView(View):
logger.info(f"User {username} has logged out") logger.info(f"User {username} has logged out")
messages.info(request, "You have 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 = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key') response.delete_cookie('session_key')
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response return response

View File

@ -20,11 +20,10 @@ __all__ = [
# #
class NestedProviderNetworkSerializer(WritableNestedSerializer): class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta: class Meta:
model = ProviderNetwork 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',), exclude_fields=('circuit_count',),
) )
class NestedProviderSerializer(WritableNestedSerializer): class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = RelatedObjectCountField('circuits') circuit_count = RelatedObjectCountField('circuits')
class Meta: class Meta:
model = Provider 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): class NestedProviderAccountSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
class Meta: class Meta:
model = ProviderAccount 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',), exclude_fields=('circuit_count',),
) )
class NestedCircuitTypeSerializer(WritableNestedSerializer): class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = RelatedObjectCountField('circuits') circuit_count = RelatedObjectCountField('circuits')
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
class NestedCircuitSerializer(WritableNestedSerializer): class NestedCircuitSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'url', 'display', 'cid'] fields = ['id', 'url', 'display_url', 'display', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer): class NestedCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer() circuit = NestedCircuitSerializer()
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']

View File

@ -18,7 +18,6 @@ __all__ = (
class CircuitTypeSerializer(NetBoxModelSerializer): class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
# Related object counts # Related object counts
circuit_count = RelatedObjectCountField('circuits') circuit_count = RelatedObjectCountField('circuits')
@ -26,27 +25,25 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'last_updated', 'circuit_count', 'created', 'last_updated', 'circuit_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
class CircuitCircuitTerminationSerializer(WritableNestedSerializer): class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = SiteSerializer(nested=True, allow_null=True) site = SiteSerializer(nested=True, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True) provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'description', 'xconnect_id', 'description',
] ]
class CircuitSerializer(NetBoxModelSerializer): class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = ProviderSerializer(nested=True) provider = ProviderSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None) provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
status = ChoiceField(choices=CircuitStatusChoices, required=False) status = ChoiceField(choices=CircuitStatusChoices, required=False)
@ -58,15 +55,14 @@ class CircuitSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Circuit model = Circuit
fields = [ fields = [
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
'custom_fields', 'created', 'last_updated', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'cid', 'description') brief_fields = ('id', 'url', 'display', 'cid', 'description')
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer): class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = CircuitSerializer(nested=True) circuit = CircuitSerializer(nested=True)
site = SiteSerializer(nested=True, required=False, allow_null=True) site = SiteSerializer(nested=True, required=False, allow_null=True)
provider_network = ProviderNetworkSerializer(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: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end',
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')

View File

@ -15,7 +15,6 @@ __all__ = (
class ProviderSerializer(NetBoxModelSerializer): class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
accounts = SerializedPKRelatedField( accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer, serializer=NestedProviderAccountSerializer,
@ -36,34 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'comments',
'custom_fields', 'created', 'last_updated', 'circuit_count', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
class ProviderAccountSerializer(NetBoxModelSerializer): class ProviderAccountSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
provider = ProviderSerializer(nested=True) provider = ProviderSerializer(nested=True)
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='') name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
class Meta: class Meta:
model = ProviderAccount model = ProviderAccount
fields = [ fields = [
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags',
'created', 'last_updated', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description') brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
class ProviderNetworkSerializer(NetBoxModelSerializer): class ProviderNetworkSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = ProviderSerializer(nested=True) provider = ProviderSerializer(nested=True)
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = [ 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', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -63,10 +63,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn( termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side A') verbose_name=_('Side A')
) )
termination_z = tables.TemplateColumn( termination_z = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side Z') verbose_name=_('Side Z')
) )
commit_rate = CommitRateColumn( commit_rate = CommitRateColumn(

View File

@ -7,7 +7,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.query import count_related 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 . import filtersets, forms, tables
from .models import * from .models import *
@ -26,17 +26,12 @@ class ProviderListView(generic.ObjectListView):
@register_model_view(Provider) @register_model_view(Provider)
class ProviderView(generic.ObjectView): class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
def get_extra_context(self, request, instance): 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 { 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) @register_model_view(ProviderAccount)
class ProviderAccountView(generic.ObjectView): class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderAccount.objects.all() queryset = ProviderAccount.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
)
return { 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) @register_model_view(ProviderNetwork)
class ProviderNetworkView(generic.ObjectView): class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderNetwork.objects.all() queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'provider_network_id',
),
)
return { 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) @register_model_view(CircuitType)
class CircuitTypeView(generic.ObjectView): class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -14,23 +14,20 @@ __all__ = (
class NestedDataSourceSerializer(WritableNestedSerializer): class NestedDataSourceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
class Meta: class Meta:
model = DataSource model = DataSource
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedDataFileSerializer(WritableNestedSerializer): class NestedDataFileSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
class Meta: class Meta:
model = DataFile model = DataFile
fields = ['id', 'url', 'display', 'path'] fields = ['id', 'url', 'display_url', 'display', 'path']
class NestedJobSerializer(serializers.ModelSerializer): class NestedJobSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
status = ChoiceField(choices=JobStatusChoices) status = ChoiceField(choices=JobStatusChoices)
user = UserSerializer( user = UserSerializer(
nested=True, nested=True,
@ -39,4 +36,4 @@ class NestedJobSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Job model = Job
fields = ['url', 'created', 'completed', 'user', 'status'] fields = ['url', 'display_url', 'created', 'completed', 'user', 'status']

View File

@ -15,7 +15,6 @@ __all__ = (
class ObjectChangeSerializer(BaseModelSerializer): class ObjectChangeSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:objectchange-detail')
user = UserSerializer( user = UserSerializer(
nested=True, nested=True,
read_only=True read_only=True
@ -44,8 +43,8 @@ class ObjectChangeSerializer(BaseModelSerializer):
class Meta: class Meta:
model = ObjectChange model = ObjectChange
fields = [ fields = [
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data', 'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
] ]
@extend_schema_field(serializers.JSONField(allow_null=True)) @extend_schema_field(serializers.JSONField(allow_null=True))

View File

@ -13,9 +13,6 @@ __all__ = (
class DataSourceSerializer(NetBoxModelSerializer): class DataSourceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datasource-detail'
)
type = ChoiceField( type = ChoiceField(
choices=get_data_backend_choices() choices=get_data_backend_choices()
) )
@ -30,16 +27,13 @@ class DataSourceSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = DataSource model = DataSource
fields = [ 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', 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class DataFileSerializer(NetBoxModelSerializer): class DataFileSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datafile-detail'
)
source = DataSourceSerializer( source = DataSourceSerializer(
nested=True, nested=True,
read_only=True read_only=True
@ -48,6 +42,6 @@ class DataFileSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = DataFile model = DataFile
fields = [ 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') brief_fields = ('id', 'url', 'display', 'path')

View File

@ -12,7 +12,6 @@ __all__ = (
class JobSerializer(BaseModelSerializer): class JobSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
user = UserSerializer( user = UserSerializer(
nested=True, nested=True,
read_only=True read_only=True
@ -25,7 +24,7 @@ class JobSerializer(BaseModelSerializer):
class Meta: class Meta:
model = Job model = Job
fields = [ 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', 'started', 'completed', 'user', 'data', 'error', 'job_id',
] ]
brief_fields = ('url', 'created', 'completed', 'user', 'status') brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

@ -33,7 +33,7 @@ from utilities.data import shallow_compare_dict
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial from utilities.htmx import htmx_partial
from utilities.query import count_related 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 . import filtersets, forms, tables
from .models import * from .models import *
@ -52,16 +52,12 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource) @register_model_view(DataSource)
class DataSourceView(generic.ObjectView): class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all() queryset = DataSource.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -57,34 +57,31 @@ __all__ = [
exclude_fields=('site_count',), exclude_fields=('site_count',),
) )
class NestedRegionSerializer(WritableNestedSerializer): class NestedRegionSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
site_count = serializers.IntegerField(read_only=True) site_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.Region 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( @extend_schema_serializer(
exclude_fields=('site_count',), exclude_fields=('site_count',),
) )
class NestedSiteGroupSerializer(WritableNestedSerializer): class NestedSiteGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
site_count = serializers.IntegerField(read_only=True) site_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.SiteGroup 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): class NestedSiteSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
class Meta: class Meta:
model = models.Site 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',), exclude_fields=('rack_count',),
) )
class NestedLocationSerializer(WritableNestedSerializer): class NestedLocationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.Location 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( @extend_schema_serializer(
exclude_fields=('rack_count',), exclude_fields=('rack_count',),
) )
class NestedRackRoleSerializer(WritableNestedSerializer): class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = RelatedObjectCountField('racks') rack_count = RelatedObjectCountField('racks')
class Meta: class Meta:
model = models.RackRole model = models.RackRole
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count']
@extend_schema_serializer( @extend_schema_serializer(
exclude_fields=('device_count',), exclude_fields=('device_count',),
) )
class NestedRackSerializer(WritableNestedSerializer): class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
device_count = RelatedObjectCountField('devices') device_count = RelatedObjectCountField('devices')
class Meta: class Meta:
model = models.Rack model = models.Rack
fields = ['id', 'url', 'display', 'name', 'device_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'device_count']
class NestedRackReservationSerializer(WritableNestedSerializer): class NestedRackReservationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
user = serializers.SerializerMethodField(read_only=True) user = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
model = models.RackReservation model = models.RackReservation
fields = ['id', 'url', 'display', 'user', 'units'] fields = ['id', 'url', 'display_url', 'display', 'user', 'units']
def get_user(self, obj): def get_user(self, obj):
return obj.user.username return obj.user.username
@ -148,34 +141,31 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
exclude_fields=('devicetype_count',), exclude_fields=('devicetype_count',),
) )
class NestedManufacturerSerializer(WritableNestedSerializer): class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = RelatedObjectCountField('device_types') devicetype_count = RelatedObjectCountField('device_types')
class Meta: class Meta:
model = models.Manufacturer model = models.Manufacturer
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'devicetype_count']
@extend_schema_serializer( @extend_schema_serializer(
exclude_fields=('device_count',), exclude_fields=('device_count',),
) )
class NestedDeviceTypeSerializer(WritableNestedSerializer): class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True) manufacturer = NestedManufacturerSerializer(read_only=True)
device_count = RelatedObjectCountField('instances') device_count = RelatedObjectCountField('instances')
class Meta: class Meta:
model = models.DeviceType 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): class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True) manufacturer = NestedManufacturerSerializer(read_only=True)
class Meta: class Meta:
model = models.ModuleType 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): class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
class Meta: class Meta:
model = models.ConsolePortTemplate model = models.ConsolePortTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer): class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
class Meta: class Meta:
model = models.ConsoleServerPortTemplate model = models.ConsoleServerPortTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedPowerPortTemplateSerializer(WritableNestedSerializer): class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
class Meta: class Meta:
model = models.PowerPortTemplate model = models.PowerPortTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer): class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
class Meta: class Meta:
model = models.PowerOutletTemplate model = models.PowerOutletTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedInterfaceTemplateSerializer(WritableNestedSerializer): class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
class Meta: class Meta:
model = models.InterfaceTemplate model = models.InterfaceTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedRearPortTemplateSerializer(WritableNestedSerializer): class NestedRearPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
class Meta: class Meta:
model = models.RearPortTemplate model = models.RearPortTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedFrontPortTemplateSerializer(WritableNestedSerializer): class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
class Meta: class Meta:
model = models.FrontPortTemplate model = models.FrontPortTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedModuleBayTemplateSerializer(WritableNestedSerializer): class NestedModuleBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
class Meta: class Meta:
model = models.ModuleBayTemplate model = models.ModuleBayTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer): class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
class Meta: class Meta:
model = models.DeviceBayTemplate model = models.DeviceBayTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedInventoryItemTemplateSerializer(WritableNestedSerializer): class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.InventoryItemTemplate 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'), exclude_fields=('device_count', 'virtualmachine_count'),
) )
class NestedDeviceRoleSerializer(WritableNestedSerializer): class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = RelatedObjectCountField('devices') device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines') virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = models.DeviceRole 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( @extend_schema_serializer(
exclude_fields=('device_count', 'virtualmachine_count'), exclude_fields=('device_count', 'virtualmachine_count'),
) )
class NestedPlatformSerializer(WritableNestedSerializer): class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
device_count = RelatedObjectCountField('devices') device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines') virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = models.Platform 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): class NestedDeviceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
class Meta: class Meta:
model = models.Device model = models.Device
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class ModuleNestedModuleBaySerializer(WritableNestedSerializer): class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
class Meta: class Meta:
model = models.ModuleBay model = models.ModuleBay
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class ModuleBayNestedModuleSerializer(WritableNestedSerializer): class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
class Meta: class Meta:
model = models.Module model = models.Module
fields = ['id', 'url', 'display', 'serial'] fields = ['id', 'url', 'display_url', 'display', 'serial']
class NestedModuleSerializer(WritableNestedSerializer): class NestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
module_bay = ModuleNestedModuleBaySerializer(read_only=True) module_bay = ModuleNestedModuleBaySerializer(read_only=True)
module_type = NestedModuleTypeSerializer(read_only=True) module_type = NestedModuleTypeSerializer(read_only=True)
class Meta: class Meta:
model = models.Module 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): class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.ConsoleServerPort model = models.ConsoleServerPort
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedConsolePortSerializer(WritableNestedSerializer): class NestedConsolePortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.ConsolePort model = models.ConsolePort
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedPowerOutletSerializer(WritableNestedSerializer): class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.PowerOutlet model = models.PowerOutlet
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedPowerPortSerializer(WritableNestedSerializer): class NestedPowerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.PowerPort model = models.PowerPort
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedInterfaceSerializer(WritableNestedSerializer): class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.Interface model = models.Interface
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedRearPortSerializer(WritableNestedSerializer): class NestedRearPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.RearPort model = models.RearPort
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedFrontPortSerializer(WritableNestedSerializer): class NestedFrontPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.FrontPort model = models.FrontPort
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedModuleBaySerializer(WritableNestedSerializer): class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True) installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta: class Meta:
model = models.ModuleBay model = models.ModuleBay
fields = ['id', 'url', 'display', 'installed_module', 'name'] fields = ['id', 'url', 'display_url', 'display', 'installed_module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
class Meta: class Meta:
model = models.DeviceBay model = models.DeviceBay
fields = ['id', 'url', 'display', 'device', 'name'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
class NestedInventoryItemSerializer(WritableNestedSerializer): class NestedInventoryItemSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.InventoryItem model = models.InventoryItem
fields = ['id', 'url', 'display', 'device', 'name', '_depth'] fields = ['id', 'url', 'display_url', 'display', 'device', 'name', '_depth']
@extend_schema_serializer( @extend_schema_serializer(
exclude_fields=('inventoryitem_count',), exclude_fields=('inventoryitem_count',),
) )
class NestedInventoryItemRoleSerializer(WritableNestedSerializer): class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = RelatedObjectCountField('inventory_items') inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta: class Meta:
model = models.InventoryItemRole 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): class NestedCableSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta: class Meta:
model = models.Cable 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',), exclude_fields=('member_count',),
) )
class NestedVirtualChassisSerializer(WritableNestedSerializer): class NestedVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer() master = NestedDeviceSerializer()
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = models.VirtualChassis 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',), exclude_fields=('powerfeed_count',),
) )
class NestedPowerPanelSerializer(WritableNestedSerializer): class NestedPowerPanelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
powerfeed_count = RelatedObjectCountField('powerfeeds') powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel
fields = ['id', 'url', 'display', 'name', 'powerfeed_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'powerfeed_count']
class NestedPowerFeedSerializer(WritableNestedSerializer): class NestedPowerFeedSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
_occupied = serializers.BooleanField(required=False, read_only=True) _occupied = serializers.BooleanField(required=False, read_only=True)
class Meta: class Meta:
model = models.PowerFeed model = models.PowerFeed
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied'] fields = ['id', 'url', 'display_url', 'display', 'name', 'cable', '_occupied']
class NestedVirtualDeviceContextSerializer(WritableNestedSerializer): class NestedVirtualDeviceContextSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
class Meta: class Meta:
model = models.VirtualDeviceContext model = models.VirtualDeviceContext
fields = ['id', 'url', 'display', 'name', 'identifier', 'device'] fields = ['id', 'url', 'display_url', 'display', 'name', 'identifier', 'device']

View File

@ -7,7 +7,7 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import Cable, CablePath, CableTermination from dcim.models import Cable, CablePath, CableTermination
from netbox.api.fields import ChoiceField, ContentTypeField 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 tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
@ -21,7 +21,6 @@ __all__ = (
class CableSerializer(NetBoxModelSerializer): class CableSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
a_terminations = GenericObjectSerializer(many=True, required=False) a_terminations = GenericObjectSerializer(many=True, required=False)
b_terminations = GenericObjectSerializer(many=True, required=False) b_terminations = GenericObjectSerializer(many=True, required=False)
status = ChoiceField(choices=LinkStatusChoices, required=False) status = ChoiceField(choices=LinkStatusChoices, required=False)
@ -31,27 +30,26 @@ class CableSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color', 'id', 'url', 'display_url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant',
'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'label', 'description') brief_fields = ('id', 'url', 'display', 'label', 'description')
class TracedCableSerializer(serializers.ModelSerializer): class TracedCableSerializer(BaseModelSerializer):
""" """
Used only while tracing a cable path. Used only while tracing a cable path.
""" """
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta: class Meta:
model = Cable model = Cable
fields = [ 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): class CableTerminationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail')
termination_type = ContentTypeField( termination_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
) )
@ -60,8 +58,8 @@ class CableTerminationSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = CableTermination model = CableTermination
fields = [ fields = [
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination', 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
'created', 'last_updated', 'termination', 'created', 'last_updated',
] ]
@extend_schema_field(serializers.JSONField(allow_null=True)) @extend_schema_field(serializers.JSONField(allow_null=True))

View File

@ -41,7 +41,6 @@ __all__ = (
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@ -63,7 +62,7 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer,
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ 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', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
@ -72,7 +71,6 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer,
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@ -94,7 +92,7 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ 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', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied', 'last_updated', '_occupied',
@ -103,7 +101,6 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@ -121,8 +118,8 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'allocated_draw', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied', 'created', 'last_updated', '_occupied',
] ]
@ -130,7 +127,6 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@ -159,8 +155,8 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied', 'created', 'last_updated', '_occupied',
] ]
@ -168,7 +164,6 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
vdcs = SerializedPKRelatedField( vdcs = SerializedPKRelatedField(
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
@ -224,11 +219,11 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description',
'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width',
'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link',
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
] ]
@ -250,7 +245,6 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@ -263,9 +257,9 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class Meta: class Meta:
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions',
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags',
'last_updated', '_occupied', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_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) 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: class Meta:
model = RearPort model = RearPort
fields = ['id', 'url', 'display', 'name', 'label', 'description'] fields = ['id', 'url', 'display_url', 'display', 'name', 'label', 'description']
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module = ModuleSerializer( module = ModuleSerializer(
nested=True, nested=True,
@ -296,7 +288,7 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = [ 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', 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -304,7 +296,6 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class ModuleBaySerializer(NetBoxModelSerializer): class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
installed_module = ModuleSerializer( installed_module = ModuleSerializer(
nested=True, nested=True,
@ -316,28 +307,26 @@ class ModuleBaySerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = ModuleBay model = ModuleBay
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', 'id', 'url', 'display_url', 'display', 'device', 'name', 'installed_module', 'label', 'position',
'custom_fields', 'created', 'last_updated', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
class DeviceBaySerializer(NetBoxModelSerializer): class DeviceBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True) installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', 'id', 'url', 'display_url', 'display', 'device', 'name', 'label', 'description', 'installed_device',
'custom_fields', 'created', 'last_updated', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
class InventoryItemSerializer(NetBoxModelSerializer): class InventoryItemSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
@ -353,9 +342,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ fields = [
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer',
'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
'custom_fields', 'created', 'last_updated', '_depth', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')

View File

@ -29,7 +29,6 @@ __all__ = (
class DeviceSerializer(NetBoxModelSerializer): class DeviceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = DeviceTypeSerializer(nested=True) device_type = DeviceTypeSerializer(nested=True)
role = DeviceRoleSerializer(nested=True) role = DeviceRoleSerializer(nested=True)
tenant = TenantSerializer( tenant = TenantSerializer(
@ -78,13 +77,13 @@ class DeviceSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Device model = Device
fields = [ fields = [
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count', 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
'module_bay_count', 'inventory_item_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
@ -105,13 +104,13 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta): class Meta(DeviceSerializer.Meta):
fields = [ fields = [
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags', 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
'device_bay_count', 'module_bay_count', 'inventory_item_count', 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
] ]
@extend_schema_field(serializers.JSONField(allow_null=True)) @extend_schema_field(serializers.JSONField(allow_null=True))
@ -120,7 +119,6 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class VirtualDeviceContextSerializer(NetBoxModelSerializer): class VirtualDeviceContextSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None) 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) tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
@ -135,15 +133,14 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VirtualDeviceContext model = VirtualDeviceContext
fields = [ fields = [
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', 'id', 'url', 'display_url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip',
'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'primary_ip4', 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields',
'interface_count', 'created', 'last_updated', 'interface_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
class ModuleSerializer(NetBoxModelSerializer): class ModuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = DeviceSerializer(nested=True) device = DeviceSerializer(nested=True)
module_bay = NestedModuleBaySerializer() module_bay = NestedModuleBaySerializer()
module_type = ModuleTypeSerializer(nested=True) module_type = ModuleTypeSerializer(nested=True)
@ -152,7 +149,7 @@ class ModuleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Module model = Module
fields = [ fields = [
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'asset_tag', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')

View File

@ -32,7 +32,6 @@ __all__ = (
class ConsolePortTemplateSerializer(ValidatedModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True, nested=True,
required=False, required=False,
@ -54,14 +53,13 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
'last_updated', 'description', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True, nested=True,
required=False, required=False,
@ -83,14 +81,13 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
'last_updated', 'description', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True, nested=True,
required=False, required=False,
@ -113,14 +110,13 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
'allocated_draw', 'description', 'created', 'last_updated', 'maximum_draw', 'allocated_draw', 'description', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerOutletTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True, nested=True,
required=False, required=False,
@ -154,14 +150,13 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
'description', 'created', 'last_updated', 'power_port', 'feed_leg', 'description', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class InterfaceTemplateSerializer(ValidatedModelSerializer): class InterfaceTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True, nested=True,
required=False, required=False,
@ -201,14 +196,13 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled',
'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', 'mgmt_only', 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class RearPortTemplateSerializer(ValidatedModelSerializer): class RearPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
required=False, required=False,
nested=True, nested=True,
@ -226,14 +220,13 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
'description', 'created', 'last_updated', 'positions', 'description', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class FrontPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True, nested=True,
required=False, required=False,
@ -252,14 +245,13 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
'rear_port_position', 'description', 'created', 'last_updated', 'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class ModuleBayTemplateSerializer(ValidatedModelSerializer): class ModuleBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True nested=True
) )
@ -267,26 +259,27 @@ class ModuleBayTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ModuleBayTemplate model = ModuleBayTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description',
'last_updated', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class DeviceBayTemplateSerializer(ValidatedModelSerializer): class DeviceBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True nested=True
) )
class Meta: class Meta:
model = DeviceBayTemplate 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') brief_fields = ('id', 'url', 'display', 'name', 'description')
class InventoryItemTemplateSerializer(ValidatedModelSerializer): class InventoryItemTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
device_type = DeviceTypeSerializer( device_type = DeviceTypeSerializer(
nested=True nested=True
) )
@ -313,8 +306,9 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InventoryItemTemplate model = InventoryItemTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer',
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', 'part_id', 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated',
'_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth')

View File

@ -17,7 +17,6 @@ __all__ = (
class DeviceTypeSerializer(NetBoxModelSerializer): class DeviceTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = ManufacturerSerializer(nested=True) manufacturer = ManufacturerSerializer(nested=True)
default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
u_height = serializers.DecimalField( u_height = serializers.DecimalField(
@ -51,26 +50,25 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'id', 'url', 'display_url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number',
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'weight_unit', 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields',
'device_count', 'console_port_template_count', 'console_server_port_template_count', 'created', 'last_updated', 'device_count', 'console_port_template_count',
'power_port_template_count', 'power_outlet_template_count', 'interface_template_count', 'console_server_port_template_count', 'power_port_template_count', 'power_outlet_template_count',
'front_port_template_count', 'rear_port_template_count', 'device_bay_template_count', 'interface_template_count', 'front_port_template_count', 'rear_port_template_count',
'module_bay_template_count', 'inventory_item_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') brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
class ModuleTypeSerializer(NetBoxModelSerializer): class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = ManufacturerSerializer(nested=True) manufacturer = ManufacturerSerializer(nested=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = [ fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')

View File

@ -10,7 +10,6 @@ __all__ = (
class ManufacturerSerializer(NetBoxModelSerializer): class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
# Related object counts # Related object counts
devicetype_count = RelatedObjectCountField('device_types') devicetype_count = RelatedObjectCountField('device_types')
@ -20,7 +19,7 @@ class ManufacturerSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields',
'devicetype_count', 'inventoryitem_count', 'platform_count', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')

View File

@ -12,7 +12,6 @@ __all__ = (
class PlatformSerializer(NetBoxModelSerializer): class PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True) manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True)
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
@ -23,7 +22,7 @@ class PlatformSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description',
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')

View File

@ -17,7 +17,6 @@ __all__ = (
class PowerPanelSerializer(NetBoxModelSerializer): class PowerPanelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = SiteSerializer(nested=True) site = SiteSerializer(nested=True)
location = LocationSerializer( location = LocationSerializer(
nested=True, nested=True,
@ -32,14 +31,13 @@ class PowerPanelSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = [ fields = [
'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags',
'powerfeed_count', 'created', 'last_updated', 'custom_fields', 'powerfeed_count', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count') brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count')
class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = PowerPanelSerializer(nested=True) power_panel = PowerPanelSerializer(nested=True)
rack = RackSerializer( rack = RackSerializer(
nested=True, nested=True,
@ -72,9 +70,9 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'id', 'url', 'display_url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply',
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'phase', 'voltage', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'description', 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied')

View File

@ -20,7 +20,6 @@ __all__ = (
class RackRoleSerializer(NetBoxModelSerializer): class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
# Related object counts # Related object counts
rack_count = RelatedObjectCountField('racks') rack_count = RelatedObjectCountField('racks')
@ -28,14 +27,13 @@ class RackRoleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'last_updated', 'rack_count', 'created', 'last_updated', 'rack_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
class RackSerializer(NetBoxModelSerializer): class RackSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = SiteSerializer(nested=True) site = SiteSerializer(nested=True)
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@ -55,16 +53,15 @@ class RackSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', 'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight',
'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description',
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
class RackReservationSerializer(NetBoxModelSerializer): class RackReservationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = RackSerializer(nested=True) rack = RackSerializer(nested=True)
user = UserSerializer(nested=True) user = UserSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@ -72,8 +69,8 @@ class RackReservationSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = [ fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', 'id', 'url', 'display_url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant',
'comments', 'tags', 'custom_fields', 'description', 'comments', 'tags', 'custom_fields',
] ]
brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')

View File

@ -12,7 +12,6 @@ __all__ = (
class DeviceRoleSerializer(NetBoxModelSerializer): class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
# Related object counts # Related object counts
@ -22,14 +21,13 @@ class DeviceRoleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template',
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
class InventoryItemRoleSerializer(NetBoxModelSerializer): class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
# Related object counts # Related object counts
inventoryitem_count = RelatedObjectCountField('inventory_items') inventoryitem_count = RelatedObjectCountField('inventory_items')
@ -37,7 +35,7 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = InventoryItemRole model = InventoryItemRole
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'last_updated', 'inventoryitem_count', 'created', 'last_updated', 'inventoryitem_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')

View File

@ -19,35 +19,32 @@ __all__ = (
class RegionSerializer(NestedGroupModelSerializer): class RegionSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
parent = NestedRegionSerializer(required=False, allow_null=True, default=None) parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True, default=0) site_count = serializers.IntegerField(read_only=True, default=0)
class Meta: class Meta:
model = Region model = Region
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
'last_updated', 'site_count', '_depth', 'created', 'last_updated', 'site_count', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
class SiteGroupSerializer(NestedGroupModelSerializer): class SiteGroupSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True, default=0) site_count = serializers.IntegerField(read_only=True, default=0)
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
'last_updated', 'site_count', '_depth', 'created', 'last_updated', 'site_count', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
class SiteSerializer(NetBoxModelSerializer): class SiteSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False) status = ChoiceField(choices=SiteStatusChoices, required=False)
region = RegionSerializer(nested=True, required=False, allow_null=True) region = RegionSerializer(nested=True, required=False, allow_null=True)
group = SiteGroupSerializer(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: class Meta:
model = Site model = Site
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude',
'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count',
'virtualmachine_count', 'vlan_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
class LocationSerializer(NestedGroupModelSerializer): class LocationSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
site = SiteSerializer(nested=True) site = SiteSerializer(nested=True)
parent = NestedLocationSerializer(required=False, allow_null=True, default=None) parent = NestedLocationSerializer(required=False, allow_null=True, default=None)
status = ChoiceField(choices=LocationStatusChoices, required=False) status = ChoiceField(choices=LocationStatusChoices, required=False)
@ -92,7 +88,7 @@ class LocationSerializer(NestedGroupModelSerializer):
class Meta: class Meta:
model = Location model = Location
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', 'description', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')

View File

@ -10,7 +10,6 @@ __all__ = (
class VirtualChassisSerializer(NetBoxModelSerializer): class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False, allow_null=True, default=None) master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
members = NestedDeviceSerializer(many=True, read_only=True) members = NestedDeviceSerializer(many=True, read_only=True)
@ -20,7 +19,7 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = [ fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags',
'created', 'last_updated', 'member_count', 'members', 'custom_fields', 'created', 'last_updated', 'member_count', 'members',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')

View File

@ -219,9 +219,9 @@ class RackViewSet(NetBoxModelViewSet):
) )
# Enable filtering rack units by ID # Enable filtering rack units by ID
q = data['q'] if q := data['q']:
if q: q = q.lower()
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])] elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
page = self.paginate_queryset(elevation) page = self.paginate_queryset(elevation)
if page is not None: if page is not None:

View File

@ -465,7 +465,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Cluster'), label=_('Cluster'),
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
selector=True selector=True,
query_params={
'site_id': ['$site', 'null']
},
) )
comments = CommentField() comments = CommentField()
local_context_data = JSONField( local_context_data = JSONField(

View File

@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from dcim.models import Cable from dcim.models import Cable
@ -35,7 +36,7 @@ class CableTerminationsColumn(tables.Column):
def render(self, value): def render(self, value):
links = [ 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 '&mdash;') return mark_safe('<br />'.join(links) or '&mdash;')

View File

@ -8,6 +8,7 @@ from dcim.models import *
from extras.models import CustomField from extras.models import CustomField
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.data import drange from utilities.data import drange
from virtualization.models import Cluster, ClusterType
class LocationTestCase(TestCase): class LocationTestCase(TestCase):
@ -533,6 +534,36 @@ class DeviceTestCase(TestCase):
device2.full_clean() device2.full_clean()
device2.save() 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): class CableTestCase(TestCase):

View File

@ -17,7 +17,7 @@ from jinja2.exceptions import TemplateError
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView 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 ipam.tables import InterfaceVLANTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic 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.permissions import get_permission_for_model
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar 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.filtersets import VirtualMachineFilterSet
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable from virtualization.tables import VirtualMachineTable
@ -226,19 +228,21 @@ class RegionListView(generic.ObjectListView):
@register_model_view(Region) @register_model_view(Region)
class RegionView(generic.ObjectView): class RegionView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Region.objects.all() queryset = Region.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
regions = instance.get_descendants(include_self=True) 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 { 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) @register_model_view(SiteGroup)
class SiteGroupView(generic.ObjectView): class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = SiteGroup.objects.all() queryset = SiteGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) 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 { 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) @register_model_view(Site)
class SiteView(generic.ObjectView): class SiteView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Site.objects.prefetch_related('tenant__group') queryset = Site.objects.prefetch_related('tenant__group')
def get_extra_context(self, request, instance): 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 { 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) @register_model_view(Location)
class LocationView(generic.ObjectView): class LocationView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Location.objects.all() queryset = Location.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
locations = instance.get_descendants(include_self=True) 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 { 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) @register_model_view(RackRole)
class RackRoleView(generic.ObjectView): class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { 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) @register_model_view(Rack)
class RackView(generic.ObjectView): class RackView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role') queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
def get_extra_context(self, request, instance): 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) peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
if instance.location: if instance.location:
@ -679,7 +665,7 @@ class RackView(generic.ObjectView):
]) ])
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, [CableTermination]),
'next_rack': next_rack, 'next_rack': next_rack,
'prev_rack': prev_rack, 'prev_rack': prev_rack,
'svg_extra': svg_extra, 'svg_extra': svg_extra,
@ -838,19 +824,12 @@ class ManufacturerListView(generic.ObjectListView):
@register_model_view(Manufacturer) @register_model_view(Manufacturer)
class ManufacturerView(generic.ObjectView): class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Manufacturer.objects.all() queryset = Manufacturer.objects.all()
def get_extra_context(self, request, instance): 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 { 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) @register_model_view(DeviceType)
class DeviceTypeView(generic.ObjectView): class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'),
)
return { 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) @register_model_view(ModuleType)
class ModuleTypeView(generic.ObjectView): class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'),
)
return { 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) @register_model_view(DeviceRole)
class DeviceRoleView(generic.ObjectView): class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
def get_extra_context(self, request, instance): 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 { 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) @register_model_view(Platform)
class PlatformView(generic.ObjectView): class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
def get_extra_context(self, request, instance): 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 { 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) @register_model_view(Module)
class ModuleView(generic.ObjectView): class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Module.objects.all() queryset = Module.objects.all()
def get_extra_context(self, request, instance): 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 { 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(): if membership_form.is_valid():
membership_form.save() membership_form.save()
msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>' messages.success(request, mark_safe(
messages.success(request, mark_safe(msg)) f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
))
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect(request.get_full_path()) return redirect(request.get_full_path())
@ -3552,16 +3512,12 @@ class PowerPanelListView(generic.ObjectListView):
@register_model_view(PowerPanel) @register_model_view(PowerPanel)
class PowerPanelView(generic.ObjectView): class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView):
queryset = PowerPanel.objects.all() queryset = PowerPanel.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'),
)
return { 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) @register_model_view(VirtualDeviceContext)
class VirtualDeviceContextView(generic.ObjectView): class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
)
return { 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'),
),
),
} }

View File

@ -6,6 +6,7 @@ from rest_framework.serializers import ValidationError
from core.models import ObjectType from core.models import ObjectType
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
from extras.models import CustomField from extras.models import CustomField
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
@ -75,7 +76,7 @@ class CustomFieldsDataField(Field):
# Serialize object and multi-object values # Serialize object and multi-object values
for cf in self._get_custom_fields(): 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_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT CustomFieldTypeChoices.TYPE_MULTIOBJECT
): ):

View File

@ -22,79 +22,69 @@ __all__ = [
class NestedEventRuleSerializer(WritableNestedSerializer): class NestedEventRuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
class Meta: class Meta:
model = models.EventRule model = models.EventRule
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedWebhookSerializer(WritableNestedSerializer): class NestedWebhookSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
class Meta: class Meta:
model = models.Webhook model = models.Webhook
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedCustomFieldSerializer(WritableNestedSerializer): class NestedCustomFieldSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
class Meta: class Meta:
model = models.CustomField model = models.CustomField
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedCustomFieldChoiceSetSerializer(WritableNestedSerializer): class NestedCustomFieldChoiceSetSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
class Meta: class Meta:
model = models.CustomFieldChoiceSet model = models.CustomFieldChoiceSet
fields = ['id', 'url', 'display', 'name', 'choices_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'choices_count']
class NestedCustomLinkSerializer(WritableNestedSerializer): class NestedCustomLinkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
class Meta: class Meta:
model = models.CustomLink model = models.CustomLink
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedConfigContextSerializer(WritableNestedSerializer): class NestedConfigContextSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
class Meta: class Meta:
model = models.ConfigContext model = models.ConfigContext
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedConfigTemplateSerializer(WritableNestedSerializer): class NestedConfigTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
class Meta: class Meta:
model = models.ConfigTemplate model = models.ConfigTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedExportTemplateSerializer(WritableNestedSerializer): class NestedExportTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
class Meta: class Meta:
model = models.ExportTemplate model = models.ExportTemplate
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedSavedFilterSerializer(WritableNestedSerializer): class NestedSavedFilterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
class Meta: class Meta:
model = models.SavedFilter model = models.SavedFilter
fields = ['id', 'url', 'display', 'name', 'slug'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug']
class NestedBookmarkSerializer(WritableNestedSerializer): class NestedBookmarkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
class Meta: class Meta:
model = models.Bookmark model = models.Bookmark
@ -102,7 +92,6 @@ class NestedBookmarkSerializer(WritableNestedSerializer):
class NestedImageAttachmentSerializer(WritableNestedSerializer): class NestedImageAttachmentSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
class Meta: class Meta:
model = models.ImageAttachment model = models.ImageAttachment
@ -110,11 +99,10 @@ class NestedImageAttachmentSerializer(WritableNestedSerializer):
class NestedJournalEntrySerializer(WritableNestedSerializer): class NestedJournalEntrySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
class Meta: class Meta:
model = models.JournalEntry model = models.JournalEntry
fields = ['id', 'url', 'display', 'created'] fields = ['id', 'url', 'display_url', 'display', 'created']
class NestedScriptSerializer(WritableNestedSerializer): class NestedScriptSerializer(WritableNestedSerializer):
@ -123,12 +111,17 @@ class NestedScriptSerializer(WritableNestedSerializer):
lookup_field='full_name', lookup_field='full_name',
lookup_url_kwarg='pk' 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) name = serializers.CharField(read_only=True)
display = serializers.SerializerMethodField(read_only=True) display = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
model = models.Script model = models.Script
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
def get_display(self, obj): def get_display(self, obj):
return f'{obj.name} ({obj.module})' return f'{obj.name} ({obj.module})'

View File

@ -14,7 +14,6 @@ __all__ = (
class ImageAttachmentSerializer(ValidatedModelSerializer): class ImageAttachmentSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
object_type = ContentTypeField( object_type = ContentTypeField(
queryset=ObjectType.objects.all() queryset=ObjectType.objects.all()
) )
@ -23,8 +22,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment
fields = [ fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image',
'image_width', 'created', 'last_updated', 'image_height', 'image_width', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'image') brief_fields = ('id', 'url', 'display', 'name', 'image')

View File

@ -14,7 +14,6 @@ __all__ = (
class BookmarkSerializer(ValidatedModelSerializer): class BookmarkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
object_type = ContentTypeField( object_type = ContentTypeField(
queryset=ObjectType.objects.with_feature('bookmarks'), queryset=ObjectType.objects.with_feature('bookmarks'),
) )

View File

@ -20,7 +20,6 @@ __all__ = (
class ConfigContextSerializer(ValidatedModelSerializer): class ConfigContextSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
regions = SerializedPKRelatedField( regions = SerializedPKRelatedField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
serializer=RegionSerializer, serializer=RegionSerializer,
@ -123,9 +122,9 @@ class ConfigContextSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConfigContext model = ConfigContext
fields = [ fields = [
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'id', 'url', 'display_url', 'display', 'name', 'weight', 'description', 'is_active', 'regions',
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path',
'created', 'last_updated', 'data_file', 'data_synced', 'data', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -11,7 +11,6 @@ __all__ = (
class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
data_source = DataSourceSerializer( data_source = DataSourceSerializer(
nested=True, nested=True,
required=False required=False
@ -24,7 +23,7 @@ class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer
class Meta: class Meta:
model = ConfigTemplate model = ConfigTemplate
fields = [ fields = [
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', 'id', 'url', 'display_url', 'display', 'name', 'description', 'environment_params', 'template_code',
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', 'data_source', 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -16,7 +16,6 @@ __all__ = (
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
base_choices = ChoiceField( base_choices = ChoiceField(
choices=CustomFieldChoiceSetBaseChoices, choices=CustomFieldChoiceSetBaseChoices,
required=False required=False
@ -31,14 +30,13 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = [ fields = [
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'id', 'url', 'display_url', 'display', 'name', 'description', 'base_choices', 'extra_choices',
'choices_count', 'created', 'last_updated', 'order_alphabetically', 'choices_count', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
class CustomFieldSerializer(ValidatedModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
object_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
many=True many=True
@ -62,10 +60,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ fields = [
'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label', 'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible',
'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum',
'choice_set', 'comments', 'created', 'last_updated', 'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -11,7 +11,6 @@ __all__ = (
class CustomLinkSerializer(ValidatedModelSerializer): class CustomLinkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
object_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('custom_links'), queryset=ObjectType.objects.with_feature('custom_links'),
many=True many=True
@ -20,7 +19,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CustomLink model = CustomLink
fields = [ fields = [
'id', 'url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url',
'button_class', 'new_window', 'created', 'last_updated', 'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name') brief_fields = ('id', 'url', 'display', 'name')

View File

@ -21,7 +21,6 @@ __all__ = (
# #
class EventRuleSerializer(NetBoxModelSerializer): class EventRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
object_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
many=True many=True
@ -35,7 +34,7 @@ class EventRuleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = EventRule model = EventRule
fields = [ 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', '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', 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
] ]
@ -58,13 +57,12 @@ class EventRuleSerializer(NetBoxModelSerializer):
# #
class WebhookSerializer(NetBoxModelSerializer): class WebhookSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
class Meta: class Meta:
model = Webhook model = Webhook
fields = [ fields = [
'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type', 'id', 'url', 'display_url', 'display', 'name', 'description', 'payload_url', 'http_method',
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path',
'tags', 'created', 'last_updated', 'custom_fields', 'tags', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -12,7 +12,6 @@ __all__ = (
class ExportTemplateSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
object_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('export_templates'), queryset=ObjectType.objects.with_feature('export_templates'),
many=True many=True
@ -29,7 +28,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = [ 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', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
'last_updated', 'last_updated',
] ]

View File

@ -16,7 +16,6 @@ __all__ = (
class JournalEntrySerializer(NetBoxModelSerializer): class JournalEntrySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
assigned_object_type = ContentTypeField( assigned_object_type = ContentTypeField(
queryset=ObjectType.objects.all() queryset=ObjectType.objects.all()
) )
@ -35,8 +34,8 @@ class JournalEntrySerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = JournalEntry model = JournalEntry
fields = [ fields = [
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created', 'id', 'url', 'display_url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object',
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated', 'created', 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'created') brief_fields = ('id', 'url', 'display', 'created')

View File

@ -11,7 +11,6 @@ __all__ = (
class SavedFilterSerializer(ValidatedModelSerializer): class SavedFilterSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
object_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.all(), queryset=ObjectType.objects.all(),
many=True many=True
@ -20,7 +19,7 @@ class SavedFilterSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = SavedFilter model = SavedFilter
fields = [ fields = [
'id', 'url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight',
'shared', 'parameters', 'created', 'last_updated', 'enabled', 'shared', 'parameters', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

View File

@ -14,7 +14,6 @@ __all__ = (
class ScriptSerializer(ValidatedModelSerializer): class ScriptSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
description = serializers.SerializerMethodField(read_only=True) description = serializers.SerializerMethodField(read_only=True)
vars = serializers.SerializerMethodField(read_only=True) vars = serializers.SerializerMethodField(read_only=True)
result = JobSerializer(nested=True, read_only=True) result = JobSerializer(nested=True, read_only=True)
@ -22,7 +21,7 @@ class ScriptSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Script model = Script
fields = [ 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') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -11,7 +11,6 @@ __all__ = (
class TagSerializer(ValidatedModelSerializer): class TagSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
object_types = ContentTypeField( object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('tags'), queryset=ObjectType.objects.with_feature('tags'),
many=True, many=True,
@ -24,7 +23,7 @@ class TagSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Tag model = Tag
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'object_types',
'last_updated', 'tagged_items', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')

View File

@ -117,10 +117,14 @@ class BookmarkOrderingChoices(ChoiceSet):
ORDERING_NEWEST = '-created' ORDERING_NEWEST = '-created'
ORDERING_OLDEST = 'created' ORDERING_OLDEST = 'created'
ORDERING_ALPHABETICAL_AZ = 'name'
ORDERING_ALPHABETICAL_ZA = '-name'
CHOICES = ( CHOICES = (
(ORDERING_NEWEST, _('Newest')), (ORDERING_NEWEST, _('Newest')),
(ORDERING_OLDEST, _('Oldest')), (ORDERING_OLDEST, _('Oldest')),
(ORDERING_ALPHABETICAL_AZ, _('Alphabetical (A-Z)')),
(ORDERING_ALPHABETICAL_ZA, _('Alphabetical (Z-A)')),
) )

View File

@ -135,23 +135,23 @@ class ConditionSet:
def __init__(self, ruleset): def __init__(self, ruleset):
if type(ruleset) is not dict: if type(ruleset) is not dict:
raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset))) 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 if len(ruleset) == 1:
logic = list(ruleset.keys())[0] self.logic = (list(ruleset.keys())[0]).lower()
if type(logic) is not str or logic.lower() not in (AND, OR): if self.logic not in (AND, OR):
raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format( raise ValueError(_("Invalid logic type: must be 'AND' or 'OR'. Please check documentation."))
logic=logic, op_and=AND, op_or=OR
))
self.logic = logic.lower()
# Compile the set of Conditions # Compile the set of Conditions
self.conditions = [ self.conditions = [
ConditionSet(rule) if is_ruleset(rule) else Condition(**rule) ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
for rule in ruleset[self.logic] 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): def eval(self, data):
""" """

View File

@ -5,6 +5,8 @@ EVENT_DELETE = 'delete'
EVENT_JOB_START = 'job_start' EVENT_JOB_START = 'job_start'
EVENT_JOB_END = 'job_end' EVENT_JOB_END = 'job_end'
# Custom fields
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])
# Webhooks # Webhooks
HTTP_CONTENT_TYPE_JSON = 'application/json' HTTP_CONTENT_TYPE_JSON = 'application/json'

View File

@ -381,11 +381,17 @@ class BookmarksWidget(DashboardWidget):
if request.user.is_anonymous: if request.user.is_anonymous:
bookmarks = list() bookmarks = list()
else: 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'): if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types) models = get_models_from_content_types(object_types)
conent_types = ObjectType.objects.get_for_models(*models).values() content_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=conent_types) bookmarks = bookmarks.filter(object_type__in=content_types)
if max_items := self.config.get('max_items'): if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items] bookmarks = bookmarks[:max_items]

View File

@ -154,7 +154,7 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
fields = ( fields = (
'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum', 'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
'validation_regex', 'validation_regex', 'validation_unique',
) )
def search(self, queryset, name, value): def search(self, queryset, name, value):

View File

@ -6,6 +6,7 @@ from extras.models import *
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from utilities.forms import BulkEditForm, add_blank_choice from utilities.forms import BulkEditForm, add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect from utilities.forms.widgets import BulkEditNullBooleanSelect
__all__ = ( __all__ = (
@ -64,8 +65,32 @@ class CustomFieldBulkEditForm(BulkEditForm):
required=False, required=False,
widget=BulkEditNullBooleanSelect() 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() 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') nullable_fields = ('group_name', 'description', 'choice_set')

View File

@ -71,7 +71,8 @@ class CustomFieldImportForm(CSVModelForm):
fields = ( fields = (
'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description', 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', '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',
) )

View File

@ -41,6 +41,9 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
'ui_editable', 'is_cloneable', name=_('Attributes') 'ui_editable', 'is_cloneable', name=_('Attributes')
), ),
FieldSet(
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
),
) )
related_object_type_id = ContentTypeMultipleChoiceField( related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
@ -89,6 +92,25 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES 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): class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):

View File

@ -64,7 +64,9 @@ class CustomFieldForm(forms.ModelForm):
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior') 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
), ),
FieldSet('default', 'choice_set', name=_('Values')), 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: class Meta:

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0116_move_objectchange'),
]
operations = [
migrations.AddField(
model_name='customfield',
name='validation_unique',
field=models.BooleanField(default=False),
),
]

View File

@ -10,6 +10,7 @@ from django.contrib.postgres.fields import ArrayField
from django.core.validators import RegexValidator, ValidationError from django.core.validators import RegexValidator, ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -179,6 +180,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.' 'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
) )
) )
validation_unique = models.BooleanField(
verbose_name=_('must be unique'),
default=False,
help_text=_('The value of this field must be unique for the assigned object')
)
choice_set = models.ForeignKey( choice_set = models.ForeignKey(
to='CustomFieldChoiceSet', to='CustomFieldChoiceSet',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -216,7 +222,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
clone_fields = ( clone_fields = (
'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight', 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', 'validation_unique', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
) )
class Meta: class Meta:
@ -333,6 +339,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'validation_regex': _("Regular expression validation is supported only for text and URL fields") 'validation_regex': _("Regular expression validation is supported only for text and URL fields")
}) })
# Uniqueness can not be enforced for boolean fields
if self.validation_unique and self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
raise ValidationError({
'validation_unique': _("Uniqueness cannot be enforced for boolean fields")
})
# Choice set must be set on selection fields, and *only* on selection fields # Choice set must be set on selection fields, and *only* on selection fields
if self.type in ( if self.type in (
CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_SELECT,
@ -520,7 +532,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
RegexValidator( RegexValidator(
regex=self.validation_regex, regex=self.validation_regex,
message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format( message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(
regex=self.validation_regex regex=escape(self.validation_regex)
)) ))
) )
] ]
@ -660,6 +672,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate date & time # Validate date & time
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
if type(value) is not datetime: if type(value) is not datetime:
# Work around UTC issue for Python < 3.11; see
# https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
if type(value) is str and value.endswith('Z'):
value = f'{value[:-1]}+00:00'
try: try:
datetime.fromisoformat(value) datetime.fromisoformat(value)
except ValueError: except ValueError:

View File

@ -480,19 +480,21 @@ class BaseScript:
# A test method is currently active, so log the message using legacy Report logging # A test method is currently active, so log the message using legacy Report logging
if self._current_test: if self._current_test:
# TODO: Use a dataclass for test method logs
self.tests[self._current_test]['log'].append((
timezone.now().isoformat(),
level,
str(obj) if obj else None,
obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
str(message),
))
# Increment the event counter for this level # Increment the event counter for this level
if level in self.tests[self._current_test]: if level in self.tests[self._current_test]:
self.tests[self._current_test][level] += 1 self.tests[self._current_test][level] += 1
# Record message (if any) to the report log
if message:
# TODO: Use a dataclass for test method logs
self.tests[self._current_test]['log'].append((
timezone.now().isoformat(),
level,
str(obj) if obj else None,
obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
str(message),
))
elif message: elif message:
# Record to the script's log # Record to the script's log
@ -500,6 +502,8 @@ class BaseScript:
'time': timezone.now().isoformat(), 'time': timezone.now().isoformat(),
'status': level, 'status': level,
'message': str(message), 'message': str(message),
'obj': str(obj) if obj else None,
'url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
}) })
# Record to the system log # Record to the system log
@ -507,19 +511,19 @@ class BaseScript:
message = f"{obj}: {message}" message = f"{obj}: {message}"
self.logger.log(LogLevelChoices.SYSTEM_LEVELS[level], message) self.logger.log(LogLevelChoices.SYSTEM_LEVELS[level], message)
def log_debug(self, message, obj=None): def log_debug(self, message=None, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_DEBUG) self._log(message, obj, level=LogLevelChoices.LOG_DEBUG)
def log_success(self, message, obj=None): def log_success(self, message=None, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_SUCCESS) self._log(message, obj, level=LogLevelChoices.LOG_SUCCESS)
def log_info(self, message, obj=None): def log_info(self, message=None, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_INFO) self._log(message, obj, level=LogLevelChoices.LOG_INFO)
def log_warning(self, message, obj=None): def log_warning(self, message=None, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_WARNING) self._log(message, obj, level=LogLevelChoices.LOG_WARNING)
def log_failure(self, message, obj=None): def log_failure(self, message=None, obj=None):
self._log(message, obj, level=LogLevelChoices.LOG_FAILURE) self._log(message, obj, level=LogLevelChoices.LOG_FAILURE)
self.failed = True self.failed = True

View File

@ -1,12 +1,12 @@
import json import json
import django_tables2 as tables import django_tables2 as tables
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.models import * from extras.models import *
from netbox.constants import EMPTY_TABLE_TEXT from netbox.constants import EMPTY_TABLE_TEXT
from netbox.tables import BaseTable, NetBoxTable, columns from netbox.tables import BaseTable, NetBoxTable, columns
from .template_code import *
__all__ = ( __all__ = (
'BookmarkTable', 'BookmarkTable',
@ -71,13 +71,26 @@ class CustomFieldTable(NetBoxTable):
is_cloneable = columns.BooleanColumn( is_cloneable = columns.BooleanColumn(
verbose_name=_('Is Cloneable'), verbose_name=_('Is Cloneable'),
) )
validation_minimum = tables.Column(
verbose_name=_('Minimum Value'),
)
validation_maximum = tables.Column(
verbose_name=_('Maximum Value'),
)
validation_regex = tables.Column(
verbose_name=_('Validation Regex'),
)
validation_unique = columns.BooleanColumn(
verbose_name=_('Validate Uniqueness'),
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required', 'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'weight', 'choice_set', 'choices', 'comments', 'created', 'last_updated', 'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum', 'validation_regex',
'validation_unique', 'comments', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')
@ -501,6 +514,9 @@ class ScriptResultsTable(BaseTable):
template_code="""{% load log_levels %}{% log_level record.status %}""", template_code="""{% load log_levels %}{% log_level record.status %}""",
verbose_name=_('Level') verbose_name=_('Level')
) )
object = tables.Column(
verbose_name=_('Object')
)
message = columns.MarkdownColumn( message = columns.MarkdownColumn(
verbose_name=_('Message') verbose_name=_('Message')
) )
@ -508,8 +524,17 @@ class ScriptResultsTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
empty_text = _(EMPTY_TABLE_TEXT) empty_text = _(EMPTY_TABLE_TEXT)
fields = ( fields = (
'index', 'time', 'status', 'message', 'index', 'time', 'status', 'object', 'message',
) )
default_columns = (
'index', 'time', 'status', 'object', 'message',
)
def render_object(self, value, record):
return format_html("<a href='{}'>{}</a>", record['url'], value)
def render_url(self, value):
return format_html("<a href='{}'>{}</a>", value, value)
class ReportResultsTable(BaseTable): class ReportResultsTable(BaseTable):
@ -541,3 +566,9 @@ class ReportResultsTable(BaseTable):
fields = ( fields = (
'index', 'method', 'time', 'status', 'object', 'url', 'message', 'index', 'method', 'time', 'status', 'object', 'url', 'message',
) )
def render_object(self, value, record):
return format_html("<a href='{}'>{}</a>", record['url'], value)
def render_url(self, value):
return format_html("<a href='{}'>{}</a>", value, value)

View File

@ -1,8 +0,0 @@
CONFIGCONTEXT_ACTIONS = """
{% if perms.extras.change_configcontext %}
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-sm btn-warning"><i class="mdi mdi-pencil" aria-hidden="true"></i></a>
{% endif %}
{% if perms.extras.delete_configcontext %}
<a href="{% url 'extras:configcontext_delete' pk=record.pk %}" class="btn btn-sm btn-danger"><i class="mdi mdi-trash-can-outline" aria-hidden="true"></i></a>
{% endif %}
"""

View File

@ -1,4 +1,5 @@
from django import template from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from core.models import ObjectType from core.models import ObjectType
@ -59,8 +60,7 @@ def custom_links(context, obj):
# Add non-grouped links # Add non-grouped links
else: else:
try: try:
rendered = cl.render(link_context) if rendered := cl.render(link_context):
if rendered:
template_code += LINK_BUTTON.format( template_code += LINK_BUTTON.format(
rendered['link'], rendered['link_target'], cl.button_class, rendered['text'] rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
) )
@ -75,8 +75,7 @@ def custom_links(context, obj):
for cl in links: for cl in links:
try: try:
rendered = cl.render(link_context) if rendered := cl.render(link_context):
if rendered:
links_rendered.append( links_rendered.append(
GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text']) GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text'])
) )
@ -88,7 +87,7 @@ def custom_links(context, obj):
if links_rendered: if links_rendered:
template_code += GROUP_BUTTON.format( template_code += GROUP_BUTTON.format(
links[0].button_class, group, ''.join(links_rendered) links[0].button_class, escape(group), ''.join(links_rendered)
) )
return mark_safe(template_code) return mark_safe(template_code)

View File

@ -1,6 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.conditions import Condition, ConditionSet from extras.conditions import Condition, ConditionSet
from extras.events import serialize_for_event
from extras.forms import EventRuleForm
from extras.models import EventRule, Webhook
class ConditionTestCase(TestCase): class ConditionTestCase(TestCase):
@ -217,3 +223,93 @@ class ConditionSetTest(TestCase):
self.assertTrue(cs.eval({'a': 1, 'b': 2, 'c': 9})) self.assertTrue(cs.eval({'a': 1, 'b': 2, 'c': 9}))
self.assertFalse(cs.eval({'a': 9, 'b': 2, 'c': 9})) self.assertFalse(cs.eval({'a': 9, 'b': 2, 'c': 9}))
self.assertFalse(cs.eval({'a': 9, 'b': 9, 'c': 3})) self.assertFalse(cs.eval({'a': 9, 'b': 9, 'c': 3}))
def test_event_rule_conditions_without_logic_operator(self):
"""
Test evaluation of EventRule conditions without logic operator.
"""
event_rule = EventRule(
name='Event Rule 1',
type_create=True,
type_update=True,
conditions={
'attr': 'status.value',
'value': 'active',
}
)
# Create a Site to evaluate - Status = active
site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
data = serialize_for_event(site)
# Evaluate the conditions (status='active')
self.assertTrue(event_rule.eval_conditions(data))
def test_event_rule_conditions_with_logical_operation(self):
"""
Test evaluation of EventRule conditions without logic operator, but with logical operation (in).
"""
event_rule = EventRule(
name='Event Rule 1',
type_create=True,
type_update=True,
conditions={
"attr": "status.value",
"value": ["planned", "staging"],
"op": "in",
}
)
# Create a Site to evaluate - Status = active
site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
data = serialize_for_event(site)
# Evaluate the conditions (status in ['planned, 'staging'])
self.assertFalse(event_rule.eval_conditions(data))
def test_event_rule_conditions_with_logical_operation_and_negate(self):
"""
Test evaluation of EventRule with logical operation (in) and negate.
"""
event_rule = EventRule(
name='Event Rule 1',
type_create=True,
type_update=True,
conditions={
"attr": "status.value",
"value": ["planned", "staging"],
"op": "in",
"negate": True,
}
)
# Create a Site to evaluate - Status = active
site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
data = serialize_for_event(site)
# Evaluate the conditions (status NOT in ['planned, 'staging'])
self.assertTrue(event_rule.eval_conditions(data))
def test_event_rule_conditions_with_incorrect_key_must_return_false(self):
"""
Test Event Rule with incorrect condition (key "foo" is wrong). Must return false.
"""
ct = ContentType.objects.get(app_label='extras', model='webhook')
site_ct = ContentType.objects.get_for_model(Site)
webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST')
form = EventRuleForm({
"name": "Event Rule 1",
"type_create": True,
"type_update": True,
"action_object_type": ct.pk,
"action_type": "webhook",
"action_choice": webhook.pk,
"content_types": [site_ct.pk],
"conditions": {
"foo": "status.value",
"value": "active"
}
})
self.assertFalse(form.is_valid())

View File

@ -1140,6 +1140,29 @@ class CustomFieldAPITest(APITestCase):
response = self.client.patch(url, data, format='json', **self.header) response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK) self.assertHttpStatus(response, status.HTTP_200_OK)
def test_uniqueness_validation(self):
# Create a unique custom field
cf_text = CustomField.objects.get(name='text_field')
cf_text.validation_unique = True
cf_text.save()
# Set a value on site 1
site1 = Site.objects.get(name='Site 1')
site1.custom_field_data['text_field'] = 'ABC123'
site1.save()
site2 = Site.objects.get(name='Site 2')
url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
self.add_permissions('dcim.change_site')
data = {'custom_fields': {'text_field': 'ABC123'}}
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
data = {'custom_fields': {'text_field': 'DEF456'}}
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
class CustomFieldImportTest(TestCase): class CustomFieldImportTest(TestCase):
user_permissions = ( user_permissions = (

View File

@ -1131,6 +1131,8 @@ class ScriptResultView(TableMixin, generic.ObjectView):
'time': log.get('time'), 'time': log.get('time'),
'status': log.get('status'), 'status': log.get('status'),
'message': log.get('message'), 'message': log.get('message'),
'object': log.get('obj'),
'url': log.get('url'),
} }
data.append(result) data.append(result)

View File

@ -31,11 +31,10 @@ __all__ = [
# #
class NestedASNRangeSerializer(WritableNestedSerializer): class NestedASNRangeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
class Meta: class Meta:
model = models.ASNRange model = models.ASNRange
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
# #
@ -43,11 +42,10 @@ class NestedASNRangeSerializer(WritableNestedSerializer):
# #
class NestedASNSerializer(WritableNestedSerializer): class NestedASNSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
class Meta: class Meta:
model = models.ASN model = models.ASN
fields = ['id', 'url', 'display', 'asn'] fields = ['id', 'url', 'display_url', 'display', 'asn']
# #
@ -58,12 +56,11 @@ class NestedASNSerializer(WritableNestedSerializer):
exclude_fields=('prefix_count',), exclude_fields=('prefix_count',),
) )
class NestedVRFSerializer(WritableNestedSerializer): class NestedVRFSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
prefix_count = RelatedObjectCountField('prefixes') prefix_count = RelatedObjectCountField('prefixes')
class Meta: class Meta:
model = models.VRF model = models.VRF
fields = ['id', 'url', 'display', 'name', 'rd', 'prefix_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'rd', 'prefix_count']
# #
@ -71,11 +68,10 @@ class NestedVRFSerializer(WritableNestedSerializer):
# #
class NestedRouteTargetSerializer(WritableNestedSerializer): class NestedRouteTargetSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
class Meta: class Meta:
model = models.RouteTarget model = models.RouteTarget
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display_url', 'display', 'name']
# #
@ -86,21 +82,19 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
exclude_fields=('aggregate_count',), exclude_fields=('aggregate_count',),
) )
class NestedRIRSerializer(WritableNestedSerializer): class NestedRIRSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = RelatedObjectCountField('aggregates') aggregate_count = RelatedObjectCountField('aggregates')
class Meta: class Meta:
model = models.RIR model = models.RIR
fields = ['id', 'url', 'display', 'name', 'slug', 'aggregate_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'aggregate_count']
class NestedAggregateSerializer(WritableNestedSerializer): class NestedAggregateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
family = serializers.IntegerField(read_only=True) family = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = models.Aggregate model = models.Aggregate
fields = ['id', 'url', 'display', 'family', 'prefix'] fields = ['id', 'url', 'display_url', 'display', 'family', 'prefix']
# #
@ -108,20 +102,18 @@ class NestedAggregateSerializer(WritableNestedSerializer):
# #
class NestedFHRPGroupSerializer(WritableNestedSerializer): class NestedFHRPGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
class Meta: class Meta:
model = models.FHRPGroup model = models.FHRPGroup
fields = ['id', 'url', 'display', 'protocol', 'group_id'] fields = ['id', 'url', 'display_url', 'display', 'protocol', 'group_id']
class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer): class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer() group = NestedFHRPGroupSerializer()
class Meta: class Meta:
model = models.FHRPGroupAssignment model = models.FHRPGroupAssignment
fields = ['id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority'] fields = ['id', 'url', 'display_url', 'display', 'group', 'interface_type', 'interface_id', 'priority']
# #
@ -132,33 +124,30 @@ class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
exclude_fields=('prefix_count', 'vlan_count'), exclude_fields=('prefix_count', 'vlan_count'),
) )
class NestedRoleSerializer(WritableNestedSerializer): class NestedRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = RelatedObjectCountField('prefixes') prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans') vlan_count = RelatedObjectCountField('vlans')
class Meta: class Meta:
model = models.Role model = models.Role
fields = ['id', 'url', 'display', 'name', 'slug', 'prefix_count', 'vlan_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'prefix_count', 'vlan_count']
@extend_schema_serializer( @extend_schema_serializer(
exclude_fields=('vlan_count',), exclude_fields=('vlan_count',),
) )
class NestedVLANGroupSerializer(WritableNestedSerializer): class NestedVLANGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
vlan_count = RelatedObjectCountField('vlans') vlan_count = RelatedObjectCountField('vlans')
class Meta: class Meta:
model = models.VLANGroup model = models.VLANGroup
fields = ['id', 'url', 'display', 'name', 'slug', 'vlan_count'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'vlan_count']
class NestedVLANSerializer(WritableNestedSerializer): class NestedVLANSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta: class Meta:
model = models.VLAN model = models.VLAN
fields = ['id', 'url', 'display', 'vid', 'name'] fields = ['id', 'url', 'display_url', 'display', 'vid', 'name']
# #
@ -166,13 +155,12 @@ class NestedVLANSerializer(WritableNestedSerializer):
# #
class NestedPrefixSerializer(WritableNestedSerializer): class NestedPrefixSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
family = serializers.IntegerField(read_only=True) family = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(read_only=True) _depth = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = models.Prefix model = models.Prefix
fields = ['id', 'url', 'display', 'family', 'prefix', '_depth'] fields = ['id', 'url', 'display_url', 'display', 'family', 'prefix', '_depth']
# #
@ -180,14 +168,13 @@ class NestedPrefixSerializer(WritableNestedSerializer):
# #
class NestedIPRangeSerializer(WritableNestedSerializer): class NestedIPRangeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = serializers.IntegerField(read_only=True) family = serializers.IntegerField(read_only=True)
start_address = IPAddressField() start_address = IPAddressField()
end_address = IPAddressField() end_address = IPAddressField()
class Meta: class Meta:
model = models.IPRange model = models.IPRange
fields = ['id', 'url', 'display', 'family', 'start_address', 'end_address'] fields = ['id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address']
# #
@ -195,13 +182,12 @@ class NestedIPRangeSerializer(WritableNestedSerializer):
# #
class NestedIPAddressSerializer(WritableNestedSerializer): class NestedIPAddressSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = serializers.IntegerField(read_only=True) family = serializers.IntegerField(read_only=True)
address = IPAddressField() address = IPAddressField()
class Meta: class Meta:
model = models.IPAddress model = models.IPAddress
fields = ['id', 'url', 'display', 'family', 'address'] fields = ['id', 'url', 'display_url', 'display', 'family', 'address']
# #
@ -209,16 +195,14 @@ class NestedIPAddressSerializer(WritableNestedSerializer):
# #
class NestedServiceTemplateSerializer(WritableNestedSerializer): class NestedServiceTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
class Meta: class Meta:
model = models.ServiceTemplate model = models.ServiceTemplate
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports'] fields = ['id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports']
class NestedServiceSerializer(WritableNestedSerializer): class NestedServiceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
class Meta: class Meta:
model = models.Service model = models.Service
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports'] fields = ['id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports']

View File

@ -14,7 +14,6 @@ __all__ = (
class RIRSerializer(NetBoxModelSerializer): class RIRSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
# Related object counts # Related object counts
aggregate_count = RelatedObjectCountField('aggregates') aggregate_count = RelatedObjectCountField('aggregates')
@ -22,14 +21,13 @@ class RIRSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = RIR model = RIR
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'is_private', 'description', 'tags',
'last_updated', 'aggregate_count', 'custom_fields', 'created', 'last_updated', 'aggregate_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
class ASNRangeSerializer(NetBoxModelSerializer): class ASNRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
rir = RIRSerializer(nested=True) rir = RIRSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
asn_count = serializers.IntegerField(read_only=True) asn_count = serializers.IntegerField(read_only=True)
@ -37,14 +35,13 @@ class ASNRangeSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = ASNRange model = ASNRange
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description',
'custom_fields', 'created', 'last_updated', 'asn_count', 'tags', 'custom_fields', 'created', 'last_updated', 'asn_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class ASNSerializer(NetBoxModelSerializer): class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
rir = RIRSerializer(nested=True, required=False, allow_null=True) rir = RIRSerializer(nested=True, required=False, allow_null=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@ -55,8 +52,8 @@ class ASNSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = ASN model = ASN
fields = [ fields = [
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags',
'created', 'last_updated', 'site_count', 'provider_count', 'custom_fields', 'created', 'last_updated', 'site_count', 'provider_count',
] ]
brief_fields = ('id', 'url', 'display', 'asn', 'description') brief_fields = ('id', 'url', 'display', 'asn', 'description')

View File

@ -15,20 +15,18 @@ __all__ = (
class FHRPGroupSerializer(NetBoxModelSerializer): class FHRPGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True) ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True)
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = [ fields = [
'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments', 'id', 'name', 'url', 'display_url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key',
'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses',
] ]
brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description') brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description')
class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = FHRPGroupSerializer(nested=True) group = FHRPGroupSerializer(nested=True)
interface_type = ContentTypeField( interface_type = ContentTypeField(
queryset=ContentType.objects.all() queryset=ContentType.objects.all()
@ -38,8 +36,8 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = FHRPGroupAssignment model = FHRPGroupAssignment
fields = [ fields = [
'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface', 'priority', 'created', 'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface',
'last_updated', 'priority', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority') brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority')

View File

@ -29,7 +29,6 @@ __all__ = (
class AggregateSerializer(NetBoxModelSerializer): class AggregateSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
rir = RIRSerializer(nested=True) rir = RIRSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@ -38,14 +37,13 @@ class AggregateSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Aggregate model = Aggregate
fields = [ fields = [
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'id', 'url', 'display_url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description',
'tags', 'custom_fields', 'created', 'last_updated', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description') brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description')
class PrefixSerializer(NetBoxModelSerializer): class PrefixSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
site = SiteSerializer(nested=True, required=False, allow_null=True) site = SiteSerializer(nested=True, required=False, allow_null=True)
vrf = VRFSerializer(nested=True, required=False, allow_null=True) vrf = VRFSerializer(nested=True, required=False, allow_null=True)
@ -60,9 +58,9 @@ class PrefixSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Prefix model = Prefix
fields = [ fields = [
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'id', 'url', 'display_url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status',
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields',
'_depth', 'created', 'last_updated', 'children', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
@ -119,7 +117,6 @@ class AvailablePrefixSerializer(serializers.Serializer):
# #
class IPRangeSerializer(NetBoxModelSerializer): class IPRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
start_address = IPAddressField() start_address = IPAddressField()
end_address = IPAddressField() end_address = IPAddressField()
@ -131,8 +128,8 @@ class IPRangeSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = IPRange model = IPRange
fields = [ fields = [
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role', 'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
@ -143,7 +140,6 @@ class IPRangeSerializer(NetBoxModelSerializer):
# #
class IPAddressSerializer(NetBoxModelSerializer): class IPAddressSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
address = IPAddressField() address = IPAddressField()
vrf = VRFSerializer(nested=True, required=False, allow_null=True) vrf = VRFSerializer(nested=True, required=False, allow_null=True)
@ -162,9 +158,9 @@ class IPAddressSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside',
'tags', 'custom_fields', 'created', 'last_updated', 'dns_name', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'family', 'address', 'description') brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')

View File

@ -10,7 +10,6 @@ __all__ = (
class RoleSerializer(NetBoxModelSerializer): class RoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
# Related object counts # Related object counts
prefix_count = RelatedObjectCountField('prefixes') prefix_count = RelatedObjectCountField('prefixes')
@ -19,7 +18,7 @@ class RoleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Role model = Role
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields',
'last_updated', 'prefix_count', 'vlan_count', 'created', 'last_updated', 'prefix_count', 'vlan_count',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count')

View File

@ -15,20 +15,18 @@ __all__ = (
class ServiceTemplateSerializer(NetBoxModelSerializer): class ServiceTemplateSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = [ fields = [
'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags',
'created', 'last_updated', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')
class ServiceSerializer(NetBoxModelSerializer): class ServiceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
device = DeviceSerializer(nested=True, required=False, allow_null=True) device = DeviceSerializer(nested=True, required=False, allow_null=True)
virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True) virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True)
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
@ -43,7 +41,7 @@ class ServiceSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Service model = Service
fields = [ fields = [
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'id', 'url', 'display_url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddresses', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')

View File

@ -31,7 +31,6 @@ class NumericRangeArraySerializer(serializers.CharField):
class VLANGroupSerializer(NetBoxModelSerializer): class VLANGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField( scope_type = ContentTypeField(
queryset=ContentType.objects.filter( queryset=ContentType.objects.filter(
model__in=VLANGROUP_SCOPE_TYPES model__in=VLANGROUP_SCOPE_TYPES
@ -51,7 +50,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vlan_id_ranges', 'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vlan_id_ranges',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
] ]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
@ -67,7 +66,6 @@ class VLANGroupSerializer(NetBoxModelSerializer):
class VLANSerializer(NetBoxModelSerializer): class VLANSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
site = SiteSerializer(nested=True, required=False, allow_null=True) site = SiteSerializer(nested=True, required=False, allow_null=True)
group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None) group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@ -81,8 +79,9 @@ class VLANSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VLAN model = VLAN
fields = [ fields = [
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'id', 'url', 'display_url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role',
'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', 'description', 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated',
'prefix_count',
] ]
brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description') brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description')

View File

@ -12,20 +12,18 @@ __all__ = (
class RouteTargetSerializer(NetBoxModelSerializer): class RouteTargetSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
class Meta: class Meta:
model = RouteTarget model = RouteTarget
fields = [ fields = [
'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'id', 'url', 'display_url', 'display', 'name', 'tenant', 'description', 'comments', 'tags',
'last_updated', 'custom_fields', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')
class VRFSerializer(NetBoxModelSerializer): class VRFSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
import_targets = SerializedPKRelatedField( import_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
@ -47,7 +45,7 @@ class VRFSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VRF model = VRF
fields = [ fields = [
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'id', 'url', 'display_url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments',
'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count',
'prefix_count', 'prefix_count',
] ]

View File

@ -12,7 +12,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.tables import get_table_ordering from utilities.tables import get_table_ordering
from utilities.views import ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface from virtualization.models import VMInterface
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -34,15 +34,10 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF) @register_model_view(VRF)
class VRFView(generic.ObjectView): class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Prefix.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'),
(IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'),
)
import_targets_table = tables.RouteTargetTable( import_targets_table = tables.RouteTargetTable(
instance.import_targets.all(), instance.import_targets.all(),
orderable=False orderable=False
@ -53,7 +48,7 @@ class VRFView(generic.ObjectView):
) )
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
'import_targets_table': import_targets_table, 'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table, 'export_targets_table': export_targets_table,
} }
@ -147,16 +142,12 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR) @register_model_view(RIR)
class RIRView(generic.ObjectView): class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Aggregate.objects.restrict(request.user, 'view').filter(rir=instance), 'rir_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -273,17 +264,19 @@ class ASNListView(generic.ObjectListView):
@register_model_view(ASN) @register_model_view(ASN)
class ASNView(generic.ObjectView): class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all() queryset = ASN.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
(Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(
request,
instance,
extra=(
(Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
(Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
),
),
} }
@ -427,18 +420,12 @@ class RoleListView(generic.ObjectListView):
@register_model_view(Role) @register_model_view(Role)
class RoleView(generic.ObjectView): class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all() queryset = Role.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Prefix.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
(IPRange.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
(VLAN.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -926,16 +913,12 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup) @register_model_view(VLANGroup)
class VLANGroupView(generic.ObjectView): class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -6,6 +6,7 @@ from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from utilities.api import get_related_object_by_attrs from utilities.api import get_related_object_by_attrs
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
__all__ = ( __all__ = (
'BaseModelSerializer', 'BaseModelSerializer',
@ -14,6 +15,8 @@ __all__ = (
class BaseModelSerializer(serializers.ModelSerializer): class BaseModelSerializer(serializers.ModelSerializer):
url = NetBoxAPIHyperlinkedIdentityField()
display_url = NetBoxURLHyperlinkedIdentityField()
display = serializers.SerializerMethodField(read_only=True) display = serializers.SerializerMethodField(read_only=True)
def __init__(self, *args, nested=False, fields=None, **kwargs): def __init__(self, *args, nested=False, fields=None, **kwargs):

View File

@ -0,0 +1,53 @@
from rest_framework import serializers
__all__ = (
'NetBoxAPIHyperlinkedIdentityField',
'NetBoxURLHyperlinkedIdentityField',
)
class BaseNetBoxHyperlinkedIdentityField(serializers.HyperlinkedIdentityField):
"""
Overrides HyperlinkedIdentityField to use standard NetBox view naming
instead of passing in the view_name. Initialize with a blank view_name
and it will get replaced in the get_url call. Derived classes must
define a get_view_name.
"""
def __init__(self, *args, **kwargs):
super().__init__(view_name="", *args, **kwargs)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
# Unsaved objects will not yet have a valid URL.
if hasattr(obj, 'pk') and obj.pk in (None, ''):
return None
lookup_value = getattr(obj, self.lookup_field)
kwargs = {self.lookup_url_kwarg: lookup_value}
model_name = self.parent.Meta.model._meta.model_name
app_name = self.parent.Meta.model._meta.app_label
view_name = self.get_view_name(app_name, model_name)
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
def get_view_name(self, app_name, model_name):
raise NotImplementedError(_('{class_name} must implement get_view_name()').format(
class_name=self.__class__.__name__
))
class NetBoxAPIHyperlinkedIdentityField(BaseNetBoxHyperlinkedIdentityField):
def get_view_name(self, app_name, model_name):
return f'{app_name}-api:{model_name}-detail'
class NetBoxURLHyperlinkedIdentityField(BaseNetBoxHyperlinkedIdentityField):
def get_view_name(self, app_name, model_name):
return f'{app_name}:{model_name}'

View File

@ -23,8 +23,7 @@ class WritableNestedSerializer(BaseModelSerializer):
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers # Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
class NestedTagSerializer(WritableNestedSerializer): class NestedTagSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'url', 'display', 'name', 'slug', 'color'] fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'color']

View File

@ -12,6 +12,7 @@ from taggit.managers import TaggableManager
from core.choices import JobStatusChoices, ObjectChangeActionChoices from core.choices import JobStatusChoices, ObjectChangeActionChoices
from core.models import ObjectType from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
from extras.utils import is_taggable from extras.utils import is_taggable
from netbox.config import get_config from netbox.config import get_config
from netbox.registry import registry from netbox.registry import registry
@ -249,7 +250,7 @@ class CustomFieldsMixin(models.Model):
for cf in visible_custom_fields: for cf in visible_custom_fields:
value = self.custom_field_data.get(cf.name) value = self.custom_field_data.get(cf.name)
if value in (None, '', []) and cf.ui_visible == CustomFieldUIVisibleChoices.IF_SET: if value in CUSTOMFIELD_EMPTY_VALUES and cf.ui_visible == CustomFieldUIVisibleChoices.IF_SET:
continue continue
value = cf.deserialize(value) value = cf.deserialize(value)
groups[cf.group_name][cf] = value groups[cf.group_name][cf] = value
@ -285,6 +286,15 @@ class CustomFieldsMixin(models.Model):
name=field_name, error=e.message name=field_name, error=e.message
)) ))
# Validate uniqueness if enforced
if custom_fields[field_name].validation_unique and value not in CUSTOMFIELD_EMPTY_VALUES:
if self._meta.model.objects.filter(**{
f'custom_field_data__{field_name}': value
}).exists():
raise ValidationError(_("Custom field '{name}' must have a unique value.").format(
name=field_name
))
# Check for missing required values # Check for missing required values
for cf in custom_fields.values(): for cf in custom_fields.values():
if cf.required and cf.name not in self.custom_field_data: if cf.required and cf.name not in self.custom_field_data:

View File

@ -553,7 +553,7 @@ if SENTRY_ENABLED:
# Calculate a unique deployment ID from the secret key # Calculate a unique deployment ID from the secret key
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16] DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
CENSUS_URL = 'https://census.netbox.dev/api/v1/' CENSUS_URL = 'https://census.netbox.oss.netboxlabs.com/api/v1/'
CENSUS_PARAMS = { CENSUS_PARAMS = {
'version': RELEASE.full_version, 'version': RELEASE.full_version,
'python_version': sys.version.split()[0], 'python_version': sys.version.split()[0],

View File

@ -1,3 +1,4 @@
import zoneinfo
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from urllib.parse import quote from urllib.parse import quote
@ -83,6 +84,8 @@ class DateTimeColumn(tables.Column):
def render(self, value): def render(self, value):
if value: if value:
current_tz = zoneinfo.ZoneInfo(settings.TIME_ZONE)
value = value.astimezone(current_tz)
return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}" return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}"
def value(self, value): def value(self, value):
@ -430,7 +433,7 @@ class LinkedCountColumn(tables.Column):
f'{k}={getattr(record, v) or settings.FILTERS_NULL_CHOICE_VALUE}' f'{k}={getattr(record, v) or settings.FILTERS_NULL_CHOICE_VALUE}'
for k, v in self.url_params.items() for k, v in self.url_params.items()
]) ])
return mark_safe(f'<a href="{url}">{value}</a>') return mark_safe(f'<a href="{url}">{escape(value)}</a>')
return value return value
def value(self, value): def value(self, value):

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -27,10 +27,10 @@
"bootstrap": "5.3.3", "bootstrap": "5.3.3",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"gridstack": "10.2.0", "gridstack": "10.2.1",
"htmx.org": "1.9.12", "htmx.org": "1.9.12",
"query-string": "9.0.0", "query-string": "9.0.0",
"sass": "1.77.4", "sass": "1.77.6",
"tom-select": "2.3.1", "tom-select": "2.3.1",
"typeface-inter": "3.18.1", "typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13" "typeface-roboto-mono": "1.1.13"

View File

@ -0,0 +1,30 @@
import { isTruthy } from '../util';
/**
* Handle saved filter change event.
*
* @param event "change" event for the saved filter select
*/
function handleSavedFilterChange(event: Event): void {
const savedFilter = event.currentTarget as HTMLSelectElement;
let baseUrl = savedFilter.baseURI.split('?')[0];
const preFilter = '?';
const selectedOptions = Array.from(savedFilter.options)
.filter(option => option.selected)
.map(option => `filter_id=${option.value}`)
.join('&');
baseUrl += `${preFilter}${selectedOptions}`;
document.location.href = baseUrl;
}
export function initSavedFilterSelect(): void {
const divResults = document.getElementById('results');
if (isTruthy(divResults)) {
const savedFilterSelect = document.getElementById('id_filter_id');
if (isTruthy(savedFilterSelect)) {
savedFilterSelect.addEventListener('change', handleSavedFilterChange);
}
}
}

View File

@ -13,6 +13,7 @@ import { initSideNav } from './sidenav';
import { initDashboard } from './dashboard'; import { initDashboard } from './dashboard';
import { initRackElevation } from './racks'; import { initRackElevation } from './racks';
import { initHtmx } from './htmx'; import { initHtmx } from './htmx';
import { initSavedFilterSelect } from './forms/savedFiltersSelect';
function initDocument(): void { function initDocument(): void {
for (const init of [ for (const init of [
@ -31,6 +32,7 @@ function initDocument(): void {
initDashboard, initDashboard,
initRackElevation, initRackElevation,
initHtmx, initHtmx,
initSavedFilterSelect,
]) { ]) {
init(); init();
} }

View File

@ -7,6 +7,7 @@
// Overrides of external libraries // Overrides of external libraries
@import 'overrides/bootstrap'; @import 'overrides/bootstrap';
@import 'overrides/tabler'; @import 'overrides/tabler';
@import 'overrides/tomselect';
// Transitional styling to ease migration of templates from NetBox v3.x // Transitional styling to ease migration of templates from NetBox v3.x
@import 'transitional/badges'; @import 'transitional/badges';

View File

@ -0,0 +1,8 @@
.ts-wrapper.multi {
.ts-control {
padding: 7px 7px 3px 7px;
div {
margin: 0 4px 4px 0;
}
}
}

View File

@ -1754,10 +1754,10 @@ graphql@16.8.1:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
gridstack@10.2.0: gridstack@10.2.1:
version "10.2.0" version "10.2.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f" resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.1.tgz#3ce6119ae86cfb0a533c5f0d15b03777a55384ca"
integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA== integrity sha512-UAPKnIvd9sIqPDFMtKMqj0G5GDj8MUFPcelRJq7FzQFSxSYBblKts/Gd52iEJg0EvTFP51t6ZuMWGx0pSSFBdw==
has-bigints@^1.0.1, has-bigints@^1.0.2: has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2" version "1.0.2"
@ -2482,10 +2482,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0" es-errors "^1.3.0"
is-regex "^1.1.4" is-regex "^1.1.4"
sass@1.77.4: sass@1.77.6:
version "1.77.4" version "1.77.6"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd" resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4"
integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw== integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==
dependencies: dependencies:
chokidar ">=3.0.0 <4.0.0" chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0" immutable "^4.0.0"

View File

@ -36,6 +36,7 @@ Blocks:
{# User menu (mobile view) #} {# User menu (mobile view) #}
<div class="navbar-nav flex-row d-lg-none"> <div class="navbar-nav flex-row d-lg-none">
{% include 'inc/light_toggle.html' %}
{% include 'inc/user_menu.html' %} {% include 'inc/user_menu.html' %}
</div> </div>
@ -57,14 +58,7 @@ Blocks:
{% plugin_navbar %} {% plugin_navbar %}
{# Dark/light mode toggle #} {# Dark/light mode toggle #}
<div class="d-none d-md-flex ms-2"> {% include 'inc/light_toggle.html' %}
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>
{# User menu #} {# User menu #}
{% include 'inc/user_menu.html' %} {% include 'inc/user_menu.html' %}

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