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:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.0.5
placeholder: v4.0.6
validations:
required: true
- type: dropdown

View File

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

View File

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

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ netbox.pid
.idea
.coverage
.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="#getting-started">Getting Started</a> |
<a href="#get-involved">Get Involved</a> |
<a href="#project-stats">Project Stats</a> |
<a href="#screenshots">Screenshots</a>
</p>

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

View File

@ -8,7 +8,9 @@ django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
django-debug-toolbar
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
django-debug-toolbar==4.3.0
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
@ -108,7 +110,7 @@ Pillow
# PostgreSQL database adapter for Python
# https://github.com/psycopg/psycopg/blob/master/docs/news.rst
psycopg[binary,pool]
psycopg[c,pool]
# YAML rendering library
# https://github.com/yaml/pyyaml/blob/master/CHANGES

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:
* `log_debug(message, object=None)`
* `log_success(message, object=None)`
* `log_info(message, object=None)`
* `log_warning(message, object=None)`
* `log_failure(message, object=None)`
* `log_debug(message=None, obj=None)`
* `log_success(message=None, obj=None)`
* `log_info(message=None, obj=None)`
* `log_warning(message=None, obj=None)`
* `log_failure(message=None, obj=None)`
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages. A message may optionally be associated with a particular object by passing it as the second argument to the logging method.
@ -152,6 +152,8 @@ A script can define one or more test methods to report on certain conditions. Al
These methods are detected and run automatically when the script is executed, unless its `run()` method has been overridden. (When overriding `run()`, `run_tests()` can be called to run all test methods present in the script.)
Calling any of these logging methods without a message will increment the relevant counter, but will not generate an output line in the script's log.
!!! info
This functionality was ported from [legacy reports](./reports.md) in NetBox v4.0.

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.
### 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
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
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class. It is generally advisable to include a `url` attribute on each serializer. This will render the direct link to access the object being rendered.
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
```python
# api/serializers.py
@ -36,9 +36,7 @@ from netbox.api.serializers import NetBoxModelSerializer
from my_plugin.models import MyModel
class MyModelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:myplugin-api:mymodel-detail'
)
foo = SiteSerializer(nested=True, allow_null=True)
class Meta:
model = MyModel
@ -63,9 +61,7 @@ from netbox.api.serializers import WritableNestedSerializer
from my_plugin.models import MyModel
class NestedMyModelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:myplugin-api:mymodel-detail'
)
foo = SiteSerializer(nested=True, allow_null=True)
class Meta:
model = MyModel

View File

@ -1,6 +1,36 @@
# NetBox v4.0
## v4.0.6 (FUTURE)
## v4.0.7 (FUTURE)
---
## v4.0.6 (2024-06-24)
### Enhancements
* [#15348](https://github.com/netbox-community/netbox/issues/15348) - Show saved filters alongside quick search on object list views
* [#15794](https://github.com/netbox-community/netbox/issues/15794) - Dynamically populate related objects in UI views
* [#16256](https://github.com/netbox-community/netbox/issues/16256) - Enable alphabetical ordering of bookmarks on dashboard
* [#16307](https://github.com/netbox-community/netbox/issues/16307) - Enable calling `log_*()` methods on Script without passing a message
### Bug Fixes
* [#13925](https://github.com/netbox-community/netbox/issues/13925) - Fix support for "zulu" (UTC) timestamps for custom fields
* [#14829](https://github.com/netbox-community/netbox/issues/14829) - Fix support for simple conditions (without AND/OR) in event rules
* [#15717](https://github.com/netbox-community/netbox/issues/15717) - Allow assigning a device/VM in a site to a cluster with no site assigned
* [#16143](https://github.com/netbox-community/netbox/issues/16143) - Display timestamps in tables in the configured timezone
* [#16149](https://github.com/netbox-community/netbox/issues/16149) - Fix object linking in custom script logs
* [#16252](https://github.com/netbox-community/netbox/issues/16252) - Fix total count in tab at top of rack elevations view
* [#16273](https://github.com/netbox-community/netbox/issues/16273) - Restore global search bar on mobile
* [#16416](https://github.com/netbox-community/netbox/issues/16416) - Retain dark/light mode toggle on mobile view
* [#16444](https://github.com/netbox-community/netbox/issues/16444) - Disable ordering circuits list by A/Z termination
* [#16450](https://github.com/netbox-community/netbox/issues/16450) - Searching for rack unit in form dropdown should be case-insensitive
* [#16452](https://github.com/netbox-community/netbox/issues/16452) - Fix sizing of buttons within object attribute panels
* [#16454](https://github.com/netbox-community/netbox/issues/16454) - Address DNS lookup bug in `django-debug-toolbar
* [#16460](https://github.com/netbox-community/netbox/issues/16460) - Omit spaces from telephone number URLs
* [#16512](https://github.com/netbox-community/netbox/issues/16512) - Restore a user's preferred language (if any) on login
* [#16542](https://github.com/netbox-community/netbox/issues/16542) - Fix bulk form operations when HTMX is enabled
* [#16702](https://github.com/netbox-community/netbox/issues/16702) - Fix validation of `return_url` query parameter
---

View File

@ -106,10 +106,16 @@ class LoginView(View):
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
if not hasattr(request.user, 'config'):
config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
request.user.config = get_config()
UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger)
response = self.redirect_to_next(request, logger)
# Set the user's preferred language (if any)
if language := request.user.config.get('locale.language'):
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
return response
else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
@ -147,9 +153,10 @@ class LogoutView(View):
logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout
# Delete session key & language cookies (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key')
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response

View File

@ -20,11 +20,10 @@ __all__ = [
#
class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta:
model = ProviderNetwork
fields = ['id', 'url', 'display', 'name']
fields = ['id', 'url', 'display_url', 'display', 'name']
#
@ -35,12 +34,11 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
exclude_fields=('circuit_count',),
)
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
#
@ -48,11 +46,10 @@ class NestedProviderSerializer(WritableNestedSerializer):
#
class NestedProviderAccountSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
class Meta:
model = ProviderAccount
fields = ['id', 'url', 'display', 'name', 'account']
fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
#
@ -63,26 +60,23 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
exclude_fields=('circuit_count',),
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
class NestedCircuitSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'display', 'cid']
fields = ['id', 'url', 'display_url', 'display', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm
from utilities.query import count_related
from utilities.views import register_model_view
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
@ -26,17 +26,12 @@ class ProviderListView(generic.ObjectListView):
@register_model_view(Provider)
class ProviderView(generic.ObjectView):
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Provider.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -92,16 +87,12 @@ class ProviderAccountListView(generic.ObjectListView):
@register_model_view(ProviderAccount)
class ProviderAccountView(generic.ObjectView):
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderAccount.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -156,19 +147,21 @@ class ProviderNetworkListView(generic.ObjectListView):
@register_model_view(ProviderNetwork)
class ProviderNetworkView(generic.ObjectView):
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'provider_network_id',
),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(
request,
instance,
extra=(
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'provider_network_id',
),
),
),
}
@ -215,16 +208,12 @@ class CircuitTypeListView(generic.ObjectListView):
@register_model_view(CircuitType)
class CircuitTypeView(generic.ObjectView):
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ __all__ = (
class JobSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
user = UserSerializer(
nested=True,
read_only=True
@ -25,7 +24,7 @@ class JobSerializer(BaseModelSerializer):
class Meta:
model = Job
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
'started', 'completed', 'user', 'data', 'error', 'job_id',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django_tables2.utils import Accessor
from django.utils.html import escape
from django.utils.safestring import mark_safe
from dcim.models import Cable
@ -35,7 +36,7 @@ class CableTerminationsColumn(tables.Column):
def render(self, value):
links = [
f'<a href="{term.get_absolute_url()}">{term}</a>' for term in self._get_terminations(value)
f'<a href="{term.get_absolute_url()}">{escape(term)}</a>' for term in self._get_terminations(value)
]
return mark_safe('<br />'.join(links) or '&mdash;')

View File

@ -8,6 +8,7 @@ from dcim.models import *
from extras.models import CustomField
from tenancy.models import Tenant
from utilities.data import drange
from virtualization.models import Cluster, ClusterType
class LocationTestCase(TestCase):
@ -533,6 +534,36 @@ class DeviceTestCase(TestCase):
device2.full_clean()
device2.save()
def test_device_mismatched_site_cluster(self):
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
Cluster.objects.create(name='Cluster 1', type=cluster_type)
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
clusters = (
Cluster(name='Cluster 1', type=cluster_type, site=sites[0]),
Cluster(name='Cluster 2', type=cluster_type, site=sites[1]),
Cluster(name='Cluster 3', type=cluster_type, site=None),
)
Cluster.objects.bulk_create(clusters)
device_type = DeviceType.objects.first()
device_role = DeviceRole.objects.first()
# Device with site only should pass
Device(name='device1', site=sites[0], device_type=device_type, role=device_role).full_clean()
# Device with site, cluster non-site should pass
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[2]).full_clean()
# Device with mismatched site & cluster should fail
with self.assertRaises(ValidationError):
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean()
class CableTestCase(TestCase):

View File

@ -17,7 +17,7 @@ from jinja2.exceptions import TemplateError
from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
from ipam.models import ASN, IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
@ -27,7 +27,9 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model
from utilities.query import count_related
from utilities.query_functions import CollateAsChar
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
from utilities.views import (
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
)
from virtualization.filtersets import VirtualMachineFilterSet
from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable
@ -226,19 +228,21 @@ class RegionListView(generic.ObjectListView):
@register_model_view(Region)
class RegionView(generic.ObjectView):
class RegionView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Region.objects.all()
def get_extra_context(self, request, instance):
regions = instance.get_descendants(include_self=True)
related_models = (
(Site.objects.restrict(request.user, 'view').filter(region__in=regions), 'region_id'),
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(
request,
regions,
extra=(
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
),
),
}
@ -306,19 +310,21 @@ class SiteGroupListView(generic.ObjectListView):
@register_model_view(SiteGroup)
class SiteGroupView(generic.ObjectView):
class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = SiteGroup.objects.all()
def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True)
related_models = (
(Site.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(
request,
groups,
extra=(
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
),
),
}
@ -380,31 +386,25 @@ class SiteListView(generic.ObjectListView):
@register_model_view(Site)
class SiteView(generic.ObjectView):
class SiteView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Site.objects.prefetch_related('tenant__group')
def get_extra_context(self, request, instance):
related_models = (
# DCIM
(Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
(Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
(Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
# Virtualization
(VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), 'site_id'),
# IPAM
(Prefix.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
(VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type=ContentType.objects.get_for_model(Site),
scope_id=instance.pk
), 'site'),
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
# Circuits
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(
request,
instance,
[CableTermination, CircuitTermination],
(
(VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type=ContentType.objects.get_for_model(Site),
scope_id=instance.pk
), 'site'),
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
'site_id'),
),
),
}
@ -466,18 +466,13 @@ class LocationListView(generic.ObjectListView):
@register_model_view(Location)
class LocationView(generic.ObjectView):
class LocationView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Location.objects.all()
def get_extra_context(self, request, instance):
locations = instance.get_descendants(include_self=True)
related_models = (
(Rack.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, locations, [CableTermination]),
}
@ -541,16 +536,12 @@ class RackRoleListView(generic.ObjectListView):
@register_model_view(RackRole)
class RackRoleView(generic.ObjectView):
class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RackRole.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -655,15 +646,10 @@ class RackElevationListView(generic.ObjectListView):
@register_model_view(Rack)
class RackView(generic.ObjectView):
class RackView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user, 'view').filter(rack=instance), 'rack_id'),
(PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'),
)
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
if instance.location:
@ -679,7 +665,7 @@ class RackView(generic.ObjectView):
])
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance, [CableTermination]),
'next_rack': next_rack,
'prev_rack': prev_rack,
'svg_extra': svg_extra,
@ -838,19 +824,12 @@ class ManufacturerListView(generic.ObjectListView):
@register_model_view(Manufacturer)
class ManufacturerView(generic.ObjectView):
class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Manufacturer.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(DeviceType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
(ModuleType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
(InventoryItem.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
(Platform.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance, [InventoryItemTemplate]),
}
@ -912,16 +891,16 @@ class DeviceTypeListView(generic.ObjectListView):
@register_model_view(DeviceType)
class DeviceTypeView(generic.ObjectView):
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceType.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
]),
}
@ -1151,16 +1130,16 @@ class ModuleTypeListView(generic.ObjectListView):
@register_model_view(ModuleType)
class ModuleTypeView(generic.ObjectView):
class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleType.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
]),
}
@ -1711,17 +1690,12 @@ class DeviceRoleListView(generic.ObjectListView):
@register_model_view(DeviceRole)
class DeviceRoleView(generic.ObjectView):
class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceRole.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
(VirtualMachine.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -1775,17 +1749,12 @@ class PlatformListView(generic.ObjectListView):
@register_model_view(Platform)
class PlatformView(generic.ObjectView):
class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Platform.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
(VirtualMachine.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -2157,22 +2126,12 @@ class ModuleListView(generic.ObjectListView):
@register_model_view(Module)
class ModuleView(generic.ObjectView):
class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Module.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Interface.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(ConsolePort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(ConsoleServerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(PowerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(PowerOutlet.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(FrontPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(RearPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -3451,8 +3410,9 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
if membership_form.is_valid():
membership_form.save()
msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
messages.success(request, mark_safe(msg))
messages.success(request, mark_safe(
f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
))
if '_addanother' in request.POST:
return redirect(request.get_full_path())
@ -3552,16 +3512,12 @@ class PowerPanelListView(generic.ObjectListView):
@register_model_view(PowerPanel)
class PowerPanelView(generic.ObjectView):
class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView):
queryset = PowerPanel.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -3665,16 +3621,18 @@ class VirtualDeviceContextListView(generic.ObjectListView):
@register_model_view(VirtualDeviceContext)
class VirtualDeviceContextView(generic.ObjectView):
class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualDeviceContext.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
)
return {
'related_models': related_models,
'related_models': self.get_related_models(
request,
instance,
extra=(
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
),
),
}

View File

@ -6,6 +6,7 @@ from rest_framework.serializers import ValidationError
from core.models import ObjectType
from extras.choices import CustomFieldTypeChoices
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
from extras.models import CustomField
from utilities.api import get_serializer_for_model
@ -75,7 +76,7 @@ class CustomFieldsDataField(Field):
# Serialize object and multi-object values
for cf in self._get_custom_fields():
if cf.name in data and data[cf.name] not in (None, []) and cf.type in (
if cf.name in data and data[cf.name] not in CUSTOMFIELD_EMPTY_VALUES and cf.type in (
CustomFieldTypeChoices.TYPE_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT
):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ __all__ = (
class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('export_templates'),
many=True
@ -29,7 +28,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ExportTemplate
fields = [
'id', 'url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
'last_updated',
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -135,23 +135,23 @@ class ConditionSet:
def __init__(self, ruleset):
if type(ruleset) is not dict:
raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset)))
if len(ruleset) != 1:
raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format(
ruleset=len(ruleset)))
# Determine the logic type
logic = list(ruleset.keys())[0]
if type(logic) is not str or logic.lower() not in (AND, OR):
raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format(
logic=logic, op_and=AND, op_or=OR
))
self.logic = logic.lower()
if len(ruleset) == 1:
self.logic = (list(ruleset.keys())[0]).lower()
if self.logic not in (AND, OR):
raise ValueError(_("Invalid logic type: must be 'AND' or 'OR'. Please check documentation."))
# Compile the set of Conditions
self.conditions = [
ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
for rule in ruleset[self.logic]
]
# Compile the set of Conditions
self.conditions = [
ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
for rule in ruleset[self.logic]
]
else:
try:
self.logic = None
self.conditions = [Condition(**ruleset)]
except TypeError:
raise ValueError(_("Incorrect key(s) informed. Please check documentation."))
def eval(self, data):
"""

View File

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

View File

@ -381,11 +381,17 @@ class BookmarksWidget(DashboardWidget):
if request.user.is_anonymous:
bookmarks = list()
else:
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
user_bookmarks = Bookmark.objects.filter(user=request.user)
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower())
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
else:
bookmarks = user_bookmarks.order_by(self.config['order_by'])
if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types)
conent_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=conent_types)
content_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=content_types)
if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items]

View File

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

View File

@ -6,6 +6,7 @@ from extras.models import *
from netbox.forms import NetBoxModelBulkEditForm
from utilities.forms import BulkEditForm, add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect
__all__ = (
@ -64,8 +65,32 @@ class CustomFieldBulkEditForm(BulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect()
)
validation_minimum = forms.IntegerField(
label=_('Minimum value'),
required=False,
)
validation_maximum = forms.IntegerField(
label=_('Maximum value'),
required=False,
)
validation_regex = forms.CharField(
label=_('Validation regex'),
required=False
)
validation_unique = forms.NullBooleanField(
label=_('Must be unique'),
required=False,
widget=BulkEditNullBooleanSelect()
)
comments = CommentField()
fieldsets = (
FieldSet('group_name', 'description', 'weight', 'choice_set', name=_('Attributes')),
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
FieldSet(
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
),
)
nullable_fields = ('group_name', 'description', 'choice_set')

View File

@ -71,7 +71,8 @@ class CustomFieldImportForm(CSVModelForm):
fields = (
'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments',
'validation_maximum', 'validation_regex', 'validation_unique', 'ui_visible', 'ui_editable', 'is_cloneable',
'comments',
)

View File

@ -41,6 +41,9 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
'ui_editable', 'is_cloneable', name=_('Attributes')
),
FieldSet(
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
),
)
related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'),
@ -89,6 +92,25 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
validation_minimum = forms.IntegerField(
label=_('Minimum value'),
required=False
)
validation_maximum = forms.IntegerField(
label=_('Maximum value'),
required=False
)
validation_regex = forms.CharField(
label=_('Validation regex'),
required=False
)
validation_unique = forms.NullBooleanField(
label=_('Must be unique'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):

View File

@ -64,7 +64,9 @@ class CustomFieldForm(forms.ModelForm):
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
),
FieldSet('default', 'choice_set', name=_('Values')),
FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
FieldSet(
'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
),
)
class Meta:

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.db import models
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
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.'
)
)
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(
to='CustomFieldChoiceSet',
on_delete=models.PROTECT,
@ -216,7 +222,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
clone_fields = (
'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
'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:
@ -333,6 +339,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'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
if self.type in (
CustomFieldTypeChoices.TYPE_SELECT,
@ -520,7 +532,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
RegexValidator(
regex=self.validation_regex,
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
elif self.type == CustomFieldTypeChoices.TYPE_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:
datetime.fromisoformat(value)
except ValueError:

View File

@ -480,19 +480,21 @@ class BaseScript:
# A test method is currently active, so log the message using legacy Report logging
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
if level in self.tests[self._current_test]:
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:
# Record to the script's log
@ -500,6 +502,8 @@ class BaseScript:
'time': timezone.now().isoformat(),
'status': level,
'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
@ -507,19 +511,19 @@ class BaseScript:
message = f"{obj}: {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)
def log_success(self, message, obj=None):
def log_success(self, message=None, obj=None):
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)
def log_warning(self, message, obj=None):
def log_warning(self, message=None, obj=None):
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.failed = True

View File

@ -1,12 +1,12 @@
import json
import django_tables2 as tables
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from extras.models import *
from netbox.constants import EMPTY_TABLE_TEXT
from netbox.tables import BaseTable, NetBoxTable, columns
from .template_code import *
__all__ = (
'BookmarkTable',
@ -71,13 +71,26 @@ class CustomFieldTable(NetBoxTable):
is_cloneable = columns.BooleanColumn(
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):
model = CustomField
fields = (
'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',
'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')
@ -501,6 +514,9 @@ class ScriptResultsTable(BaseTable):
template_code="""{% load log_levels %}{% log_level record.status %}""",
verbose_name=_('Level')
)
object = tables.Column(
verbose_name=_('Object')
)
message = columns.MarkdownColumn(
verbose_name=_('Message')
)
@ -508,8 +524,17 @@ class ScriptResultsTable(BaseTable):
class Meta(BaseTable.Meta):
empty_text = _(EMPTY_TABLE_TEXT)
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):
@ -541,3 +566,9 @@ class ReportResultsTable(BaseTable):
fields = (
'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.utils.html import escape
from django.utils.safestring import mark_safe
from core.models import ObjectType
@ -59,8 +60,7 @@ def custom_links(context, obj):
# Add non-grouped links
else:
try:
rendered = cl.render(link_context)
if rendered:
if rendered := cl.render(link_context):
template_code += LINK_BUTTON.format(
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
)
@ -75,8 +75,7 @@ def custom_links(context, obj):
for cl in links:
try:
rendered = cl.render(link_context)
if rendered:
if rendered := cl.render(link_context):
links_rendered.append(
GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text'])
)
@ -88,7 +87,7 @@ def custom_links(context, obj):
if links_rendered:
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)

View File

@ -1,6 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from dcim.choices import SiteStatusChoices
from dcim.models import Site
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):
@ -217,3 +223,93 @@ class ConditionSetTest(TestCase):
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': 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)
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):
user_permissions = (

View File

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

View File

@ -31,11 +31,10 @@ __all__ = [
#
class NestedASNRangeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
class Meta:
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',),
)
class NestedVRFSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
prefix_count = RelatedObjectCountField('prefixes')
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
class Meta:
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',),
)
class NestedRIRSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = RelatedObjectCountField('aggregates')
class Meta:
model = models.RIR
fields = ['id', 'url', 'display', 'name', 'slug', 'aggregate_count']
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'aggregate_count']
class NestedAggregateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
family = serializers.IntegerField(read_only=True)
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
class Meta:
model = models.FHRPGroup
fields = ['id', 'url', 'display', 'protocol', 'group_id']
fields = ['id', 'url', 'display_url', 'display', 'protocol', 'group_id']
class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer()
class Meta:
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'),
)
class NestedRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans')
class Meta:
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(
exclude_fields=('vlan_count',),
)
class NestedVLANGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = models.VLANGroup
fields = ['id', 'url', 'display', 'name', 'slug', 'vlan_count']
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'vlan_count']
class NestedVLANSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
family = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(read_only=True)
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = serializers.IntegerField(read_only=True)
start_address = IPAddressField()
end_address = IPAddressField()
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = serializers.IntegerField(read_only=True)
address = IPAddressField()
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
class Meta:
model = models.ServiceTemplate
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
fields = ['id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports']
class NestedServiceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
class Meta:
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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
# Related object counts
aggregate_count = RelatedObjectCountField('aggregates')
@ -22,14 +21,13 @@ class RIRSerializer(NetBoxModelSerializer):
class Meta:
model = RIR
fields = [
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'aggregate_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'is_private', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'aggregate_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
class ASNRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
rir = RIRSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
asn_count = serializers.IntegerField(read_only=True)
@ -37,14 +35,13 @@ class ASNRangeSerializer(NetBoxModelSerializer):
class Meta:
model = ASNRange
fields = [
'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'asn_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description',
'tags', 'custom_fields', 'created', 'last_updated', 'asn_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
rir = RIRSerializer(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:
model = ASN
fields = [
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'site_count', 'provider_count',
'id', 'url', 'display_url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'site_count', 'provider_count',
]
brief_fields = ('id', 'url', 'display', 'asn', 'description')

View File

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

View File

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

View File

@ -10,7 +10,6 @@ __all__ = (
class RoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
# Related object counts
prefix_count = RelatedObjectCountField('prefixes')
@ -19,7 +18,7 @@ class RoleSerializer(NetBoxModelSerializer):
class Meta:
model = Role
fields = [
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'prefix_count', 'vlan_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields',
'created', 'last_updated', '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):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
class Meta:
model = ServiceTemplate
fields = [
'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')
class ServiceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
device = DeviceSerializer(nested=True, required=False, allow_null=True)
virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True)
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
@ -43,7 +41,7 @@ class ServiceSerializer(NetBoxModelSerializer):
class Meta:
model = Service
fields = [
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports',
'ipaddresses', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')

View File

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

View File

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

View File

@ -12,7 +12,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.query import count_related
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.models import VMInterface
from . import filtersets, forms, tables
@ -34,15 +34,10 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF)
class VRFView(generic.ObjectView):
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all()
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(
instance.import_targets.all(),
orderable=False
@ -53,7 +48,7 @@ class VRFView(generic.ObjectView):
)
return {
'related_models': related_models,
'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table,
}
@ -147,16 +142,12 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR)
class RIRView(generic.ObjectView):
class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Aggregate.objects.restrict(request.user, 'view').filter(rir=instance), 'rir_id'),
)
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)
class ASNView(generic.ObjectView):
class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all()
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 {
'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)
class RoleView(generic.ObjectView):
class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all()
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 {
'related_models': related_models,
'related_models': self.get_related_models(request, instance),
}
@ -926,16 +913,12 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup)
class VLANGroupView(generic.ObjectView):
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
def get_extra_context(self, request, instance):
related_models = (
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
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 utilities.api import get_related_object_by_attrs
from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField
__all__ = (
'BaseModelSerializer',
@ -14,6 +15,8 @@ __all__ = (
class BaseModelSerializer(serializers.ModelSerializer):
url = NetBoxAPIHyperlinkedIdentityField()
display_url = NetBoxURLHyperlinkedIdentityField()
display = serializers.SerializerMethodField(read_only=True)
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
class NestedTagSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
class Meta:
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.models import ObjectType
from extras.choices import *
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
from extras.utils import is_taggable
from netbox.config import get_config
from netbox.registry import registry
@ -249,7 +250,7 @@ class CustomFieldsMixin(models.Model):
for cf in visible_custom_fields:
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
value = cf.deserialize(value)
groups[cf.group_name][cf] = value
@ -285,6 +286,15 @@ class CustomFieldsMixin(models.Model):
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
for cf in custom_fields.values():
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
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 = {
'version': RELEASE.full_version,
'python_version': sys.version.split()[0],

View File

@ -1,3 +1,4 @@
import zoneinfo
from dataclasses import dataclass
from typing import Optional
from urllib.parse import quote
@ -83,6 +84,8 @@ class DateTimeColumn(tables.Column):
def render(self, 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)}"
def value(self, value):
@ -430,7 +433,7 @@ class LinkedCountColumn(tables.Column):
f'{k}={getattr(record, v) or settings.FILTERS_NULL_CHOICE_VALUE}'
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
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",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "10.2.0",
"gridstack": "10.2.1",
"htmx.org": "1.9.12",
"query-string": "9.0.0",
"sass": "1.77.4",
"sass": "1.77.6",
"tom-select": "2.3.1",
"typeface-inter": "3.18.1",
"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 { initRackElevation } from './racks';
import { initHtmx } from './htmx';
import { initSavedFilterSelect } from './forms/savedFiltersSelect';
function initDocument(): void {
for (const init of [
@ -31,6 +32,7 @@ function initDocument(): void {
initDashboard,
initRackElevation,
initHtmx,
initSavedFilterSelect,
]) {
init();
}

View File

@ -7,6 +7,7 @@
// Overrides of external libraries
@import 'overrides/bootstrap';
@import 'overrides/tabler';
@import 'overrides/tomselect';
// Transitional styling to ease migration of templates from NetBox v3.x
@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"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
gridstack@10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f"
integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA==
gridstack@10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.1.tgz#3ce6119ae86cfb0a533c5f0d15b03777a55384ca"
integrity sha512-UAPKnIvd9sIqPDFMtKMqj0G5GDj8MUFPcelRJq7FzQFSxSYBblKts/Gd52iEJg0EvTFP51t6ZuMWGx0pSSFBdw==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@ -2482,10 +2482,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
sass@1.77.4:
version "1.77.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd"
integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==
sass@1.77.6:
version "1.77.6"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4"
integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"

View File

@ -36,6 +36,7 @@ Blocks:
{# User menu (mobile view) #}
<div class="navbar-nav flex-row d-lg-none">
{% include 'inc/light_toggle.html' %}
{% include 'inc/user_menu.html' %}
</div>
@ -57,14 +58,7 @@ Blocks:
{% plugin_navbar %}
{# Dark/light mode toggle #}
<div class="d-none d-md-flex ms-2">
<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>
{% include 'inc/light_toggle.html' %}
{# User menu #}
{% include 'inc/user_menu.html' %}

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