diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 43ab47c9d..b0b8c02ad 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -13,7 +13,9 @@ body:
- type: dropdown
attributes:
label: Deployment Type
- description: How are you running NetBox?
+ description: >
+ How are you running NetBox? (For issues with the Docker image, please go to the
+ [netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
options:
- Self-hosted
- NetBox Cloud
@@ -23,7 +25,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
- placeholder: v3.7.2
+ placeholder: v3.7.3
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index a198fd731..bc99999c0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.7.2
+ placeholder: v3.7.3
validations:
required: true
- type: dropdown
diff --git a/README.md b/README.md
index f166919c4..a81e41854 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/base_requirements.txt b/base_requirements.txt
index 4ac2b044a..49d259349 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -105,7 +105,7 @@ mkdocs-material
mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses
-# https://github.com/netaddr/netaddr/blob/master/CHANGELOG
+# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
netaddr
# Python bindings to the ammonia HTML sanitization library.
diff --git a/docs/configuration/remote-authentication.md b/docs/configuration/remote-authentication.md
index fb789bd98..e7fe56a09 100644
--- a/docs/configuration/remote-authentication.md
+++ b/docs/configuration/remote-authentication.md
@@ -67,7 +67,7 @@ When remote user authentication is in use, this is the name of the HTTP header w
Default: `|` (Pipe)
-The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
+The Separator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` )
---
diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md
index 96423a94b..bdc3f9104 100644
--- a/docs/customization/custom-scripts.md
+++ b/docs/customization/custom-scripts.md
@@ -304,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
* `model` - The model class
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
+* `context` - A custom dictionary mapping template context variables to fields, used when rendering `` elements within the dropdown menu (optional; see below)
* `null_option` - A label representing a "null" or empty choice (optional)
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
@@ -331,6 +332,22 @@ site = ObjectVar(
)
```
+#### Context Variables
+
+Custom context variables can be passed to override the default attribute names or to display additional information, such as a parent object.
+
+| Name | Default | Description |
+|---------------|-----------------|------------------------------------------------------------------------------|
+| `value` | `"id"` | The attribute which contains the option's value |
+| `label` | `"display"` | The attribute used as the option's human-friendly label |
+| `description` | `"description"` | The attribute to use as a description |
+| `depth`[^1] | `"_depth"` | The attribute which indicates an object's depth within a recursive hierarchy |
+| `disabled` | -- | The attribute which, if true, signifies that the option should be disabled |
+| `parent` | -- | The attribute which represents the object's parent object |
+| `count`[^1] | -- | The attribute which contains a numeric count of related objects |
+
+[^1]: The value of this attribute must be a positive integer
+
### MultiObjectVar
Similar to `ObjectVar`, but allows for the selection of multiple objects.
diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md
index c9f05cd93..8b38d7c89 100644
--- a/docs/models/dcim/device.md
+++ b/docs/models/dcim/device.md
@@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
-### Device Role
+### Role
-The functional [role](./devicerole.md) assigned to this device.
+The functional [device role](./devicerole.md) assigned to this device.
### Device Type
diff --git a/docs/release-notes/version-3.7.md b/docs/release-notes/version-3.7.md
index 103b0664c..21e7489c3 100644
--- a/docs/release-notes/version-3.7.md
+++ b/docs/release-notes/version-3.7.md
@@ -1,6 +1,41 @@
# NetBox v3.7
-## v3.7.3 (FUTURE)
+## v3.7.4 (FUTURE)
+
+---
+
+## v3.7.3 (2024-02-21)
+
+### Enhancements
+
+* [#14587](https://github.com/netbox-community/netbox/issues/14587) - Display a human-friendly name for the OpenID Connect remote auth backend
+* [#14946](https://github.com/netbox-community/netbox/issues/14946) - Remove `associate_by_email()` from default social auth pipeline
+* [#14966](https://github.com/netbox-community/netbox/issues/14966) - Add PostgreSQL index for object type & ID on CachedValue table to improve performance
+* [#15177](https://github.com/netbox-community/netbox/issues/15177) - Add "last login" time to user display & REST API serializer
+
+### Bug Fixes
+
+* [#14058](https://github.com/netbox-community/netbox/issues/14058) - Limit platform options by manufacturer when editing a device or device type
+* [#14064](https://github.com/netbox-community/netbox/issues/14064) - Resolving parent location should consider assigned site when bulk importing locations
+* [#14079](https://github.com/netbox-community/netbox/issues/14079) - Ensure changes are logged on related objects when deleting an object referenced via a many-to-many relationship (e.g. tags)
+* [#14405](https://github.com/netbox-community/netbox/issues/14405) - Clean up formatting of link peers in bulk CSV export of cable termination objects
+* [#14689](https://github.com/netbox-community/netbox/issues/14689) - Preserve "empty" default values for JSON custom fields
+* [#14952](https://github.com/netbox-community/netbox/issues/14952) - Update existing AutoSyncRecord when changing the data file of an auto-synced object
+* [#15059](https://github.com/netbox-community/netbox/issues/15059) - Correct IP address count link in VM interfaces table
+* [#15067](https://github.com/netbox-community/netbox/issues/15067) - Fix uncaught exception when attempting invalid device bay import
+* [#15070](https://github.com/netbox-community/netbox/issues/15070) - Fix inclusion of `config_template` field on REST API serializer for virtual machines
+* [#15084](https://github.com/netbox-community/netbox/issues/15084) - Fix "add export template" link under "export" button on object list views
+* [#15090](https://github.com/netbox-community/netbox/issues/15090) - Ensure protection rules are evaluated prior to enqueueing events when deleting an object
+* [#15091](https://github.com/netbox-community/netbox/issues/15091) - Fix designation of the active tab for assigned object when modifying an L2VPN termination
+* [#15101](https://github.com/netbox-community/netbox/issues/15101) - Correct OpenAPI schema for rack elevation REST API endpoint
+* [#15115](https://github.com/netbox-community/netbox/issues/15115) - Fix unhandled exception with invalid permission constraints
+* [#15126](https://github.com/netbox-community/netbox/issues/15126) - `group` field should be optional when creating VPN tunnel via REST API
+* [#15127](https://github.com/netbox-community/netbox/issues/15127) - Add missing group column to VPN tunnels table
+* [#15133](https://github.com/netbox-community/netbox/issues/15133) - Fix FHRP group representation on assignments REST API endpoint using brief mode
+* [#15174](https://github.com/netbox-community/netbox/issues/15174) - Warn that permission constraints are not supported for reports or scripts
+* [#15184](https://github.com/netbox-community/netbox/issues/15184) - Correct REST API schema definition for `front_image` & `rear_image` on DeviceType
+* [#15185](https://github.com/netbox-community/netbox/issues/15185) - Ensure error messages pertaining to related objects are displayed on the bulk import form
+* [#15192](https://github.com/netbox-community/netbox/issues/15192) - Fix exception when viewing current config when no history is present
---
diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md
index e2502df8c..4bae93fa8 100644
--- a/docs/release-notes/version-4.0.md
+++ b/docs/release-notes/version-4.0.md
@@ -2,23 +2,38 @@
## v4.0.0 (FUTURE)
+### Breaking Changes
+
+* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
+* The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade.
+
### New Features
#### Complete UI Refresh ([#12128](https://github.com/netbox-community/netbox/issues/12128))
The NetBox user interface has been completely refreshed and updated.
+#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
+
+The REST API now supports specifying which fields to include in the response data.
+
### Enhancements
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
+* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields
+* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
+* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
+* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
+* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
### Other Changes
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
+* [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django
* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)
@@ -26,3 +41,6 @@ The NetBox user interface has been completely refreshed and updated.
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
+* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features
+* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
+* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
diff --git a/mkdocs.yml b/mkdocs.yml
index c5816f5f5..354c10608 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -292,6 +292,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
+ - Version 4.0: 'release-notes/version-4.0.md'
- Version 3.7: 'release-notes/version-3.7.md'
- Version 3.6: 'release-notes/version-3.6.md'
- Version 3.5: 'release-notes/version-3.5.md'
diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py
index c7aa583fa..36254dc8b 100644
--- a/netbox/circuits/api/nested_serializers.py
+++ b/netbox/circuits/api/nested_serializers.py
@@ -1,8 +1,8 @@
-from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
-from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from circuits.models import *
+from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
__all__ = [
@@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
)
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
- circuit_count = serializers.IntegerField(read_only=True)
+ circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
@@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
- circuit_count = serializers.IntegerField(read_only=True)
+ circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index 5223de339..574924c4a 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -4,9 +4,9 @@ from circuits.choices import CircuitStatusChoices
from circuits.models import *
from dcim.api.nested_serializers import NestedSiteSerializer
from dcim.api.serializers import CabledObjectSerializer
-from ipam.models import ASN
from ipam.api.nested_serializers import NestedASNSerializer
-from netbox.api.fields import ChoiceField, SerializedPKRelatedField
+from ipam.models import ASN
+from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import *
@@ -32,7 +32,7 @@ class ProviderSerializer(NetBoxModelSerializer):
)
# Related object counts
- circuit_count = serializers.IntegerField(read_only=True)
+ circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
@@ -40,6 +40,7 @@ class ProviderSerializer(NetBoxModelSerializer):
'id', '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')
#
@@ -56,6 +57,7 @@ class ProviderAccountSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
#
@@ -72,6 +74,7 @@ class ProviderNetworkSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
#
@@ -80,14 +83,17 @@ class ProviderNetworkSerializer(NetBoxModelSerializer):
class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
- circuit_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = [
- 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
- 'circuit_count',
+ 'id', '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):
@@ -120,6 +126,7 @@ class CircuitSerializer(NetBoxModelSerializer):
'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):
@@ -135,3 +142,4 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
'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')
diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py
index bd9431887..fffb59a57 100644
--- a/netbox/circuits/api/views.py
+++ b/netbox/circuits/api/views.py
@@ -4,7 +4,6 @@ from circuits import filtersets
from circuits.models import *
from dcim.api.views import PassThroughPortMixin
from netbox.api.viewsets import NetBoxModelViewSet
-from utilities.utils import count_related
from . import serializers
@@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
#
class ProviderViewSet(NetBoxModelViewSet):
- queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
- circuit_count=count_related(Circuit, 'provider')
- )
+ queryset = Provider.objects.all()
serializer_class = serializers.ProviderSerializer
filterset_class = filtersets.ProviderFilterSet
@@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
#
class CircuitTypeViewSet(NetBoxModelViewSet):
- queryset = CircuitType.objects.prefetch_related('tags').annotate(
- circuit_count=count_related(Circuit, 'type')
- )
+ queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filterset_class = filtersets.CircuitTypeFilterSet
@@ -45,9 +40,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
#
class CircuitViewSet(NetBoxModelViewSet):
- queryset = Circuit.objects.prefetch_related(
- 'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
- ).prefetch_related('tags')
+ queryset = Circuit.objects.all()
serializer_class = serializers.CircuitSerializer
filterset_class = filtersets.CircuitFilterSet
@@ -57,12 +50,9 @@ class CircuitViewSet(NetBoxModelViewSet):
#
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
- queryset = CircuitTermination.objects.prefetch_related(
- 'circuit', 'site', 'provider_network', 'cable__terminations'
- )
+ queryset = CircuitTermination.objects.all()
serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filtersets.CircuitTerminationFilterSet
- brief_prefetch_fields = ['circuit']
#
@@ -70,7 +60,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
#
class ProviderAccountViewSet(NetBoxModelViewSet):
- queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
+ queryset = ProviderAccount.objects.all()
serializer_class = serializers.ProviderAccountSerializer
filterset_class = filtersets.ProviderAccountFilterSet
@@ -80,6 +70,6 @@ class ProviderAccountViewSet(NetBoxModelViewSet):
#
class ProviderNetworkViewSet(NetBoxModelViewSet):
- queryset = ProviderNetwork.objects.prefetch_related('tags')
+ queryset = ProviderNetwork.objects.all()
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet
diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py
index 3acf3b98c..df6804303 100644
--- a/netbox/circuits/apps.py
+++ b/netbox/circuits/apps.py
@@ -6,4 +6,8 @@ class CircuitsConfig(AppConfig):
verbose_name = "Circuits"
def ready(self):
+ from netbox.models.features import register_models
from . import signals, search
+
+ # Register models
+ register_models(*self.get_models())
diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py
index 4dc775364..7b65d52ad 100644
--- a/netbox/circuits/models/circuits.py
+++ b/netbox/circuits/models/circuits.py
@@ -234,9 +234,9 @@ class CircuitTermination(
# Must define either site *or* provider network
if self.site is None and self.provider_network is None:
- raise ValidationError("A circuit termination must attach to either a site or a provider network.")
+ raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
if self.site and self.provider_network:
- raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
+ raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py
index 1969441eb..c8ec08943 100644
--- a/netbox/circuits/tests/test_api.py
+++ b/netbox/circuits/tests/test_api.py
@@ -18,7 +18,7 @@ class AppTest(APITestCase):
class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider
- brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
bulk_update_data = {
'comments': 'New comments',
}
@@ -60,7 +60,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = CircuitType
- brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url']
create_data = (
{
'name': 'Circuit Type 4',
@@ -92,7 +92,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
class CircuitTest(APIViewTestCases.APIViewTestCase):
model = Circuit
- brief_fields = ['cid', 'display', 'id', 'url']
+ brief_fields = ['cid', 'description', 'display', 'id', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -149,7 +149,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = CircuitTermination
- brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
+ brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
@classmethod
def setUpTestData(cls):
@@ -208,7 +208,7 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
model = ProviderAccount
- brief_fields = ['account', 'display', 'id', 'name', 'url']
+ brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
@@ -251,7 +251,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py
index b7e537c23..8eecfa8b9 100644
--- a/netbox/core/api/schema.py
+++ b/netbox/core/api/schema.py
@@ -8,6 +8,7 @@ from drf_spectacular.plumbing import (
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
)
from drf_spectacular.types import OpenApiTypes
+from rest_framework import serializers
from rest_framework.relations import ManyRelatedField
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py
index a16a06d62..be3e9ff4a 100644
--- a/netbox/core/api/serializers.py
+++ b/netbox/core/api/serializers.py
@@ -2,7 +2,7 @@ from rest_framework import serializers
from core.choices import *
from core.models import *
-from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
from netbox.utils import get_data_backend_choices
from users.api.nested_serializers import NestedUserSerializer
@@ -28,9 +28,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
)
# Related object counts
- file_count = serializers.IntegerField(
- read_only=True
- )
+ file_count = RelatedObjectCountField('datafiles')
class Meta:
model = DataSource
@@ -38,6 +36,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
'id', '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):
@@ -53,6 +52,7 @@ class DataFileSerializer(NetBoxModelSerializer):
fields = [
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
+ brief_fields = ('id', 'url', 'display', 'path')
class JobSerializer(BaseModelSerializer):
@@ -71,3 +71,4 @@ class JobSerializer(BaseModelSerializer):
'id', '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')
diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py
index 7bf2f87a6..3fddfd691 100644
--- a/netbox/core/api/views.py
+++ b/netbox/core/api/views.py
@@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from core import filtersets
from core.models import *
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
-from utilities.utils import count_related
from . import serializers
@@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
class DataSourceViewSet(NetBoxModelViewSet):
- queryset = DataSource.objects.annotate(
- file_count=count_related(DataFile, 'source')
- )
+ queryset = DataSource.objects.all()
serializer_class = serializers.DataSourceSerializer
filterset_class = filtersets.DataSourceFilterSet
@@ -44,7 +41,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
- queryset = DataFile.objects.defer('data').prefetch_related('source')
+ queryset = DataFile.objects.defer('data')
serializer_class = serializers.DataFileSerializer
filterset_class = filtersets.DataFileFilterSet
@@ -53,6 +50,6 @@ class JobViewSet(ReadOnlyModelViewSet):
"""
Retrieve a list of job results
"""
- queryset = Job.objects.prefetch_related('user')
+ queryset = Job.objects.all()
serializer_class = serializers.JobSerializer
filterset_class = filtersets.JobFilterSet
diff --git a/netbox/core/apps.py b/netbox/core/apps.py
index 2d999c57e..b1103469c 100644
--- a/netbox/core/apps.py
+++ b/netbox/core/apps.py
@@ -16,5 +16,9 @@ class CoreConfig(AppConfig):
name = "core"
def ready(self):
+ from core.api import schema # noqa
+ from netbox.models.features import register_models
from . import data_backends, search
- from core.api import schema # noqa: E402
+
+ # Register models
+ register_models(*self.get_models())
diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py
index 9ff0b4d63..15891a6f5 100644
--- a/netbox/core/data_backends.py
+++ b/netbox/core/data_backends.py
@@ -102,7 +102,7 @@ class GitBackend(DataBackend):
try:
porcelain.clone(self.url, local_path.name, **clone_args)
except BaseException as e:
- raise SyncError(f"Fetching remote data failed ({type(e).__name__}): {e}")
+ raise SyncError(_("Fetching remote data failed ({name}): {error}").format(name=type(e).__name__, error=e))
yield local_path.name
diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py
index 52fa3a608..e0c71fe48 100644
--- a/netbox/core/forms/model_forms.py
+++ b/netbox/core/forms/model_forms.py
@@ -103,9 +103,9 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
super().clean()
if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'):
- raise forms.ValidationError("Cannot upload a file and sync from an existing file")
+ raise forms.ValidationError(_("Cannot upload a file and sync from an existing file"))
if not self.cleaned_data.get('upload_file') and not self.cleaned_data.get('data_file'):
- raise forms.ValidationError("Must upload a file or select a data file to sync")
+ raise forms.ValidationError(_("Must upload a file or select a data file to sync"))
return self.cleaned_data
diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py
index 6c8e41477..b2381ae40 100644
--- a/netbox/core/models/config.py
+++ b/netbox/core/models/config.py
@@ -44,7 +44,7 @@ class ConfigRevision(models.Model):
return gettext('Config revision #{id}').format(id=self.pk)
def __getattr__(self, item):
- if item in self.data:
+ if self.data and item in self.data:
return self.data[item]
return super().__getattribute__(item)
diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py
index 6597a4b4d..4ceb22ba9 100644
--- a/netbox/core/models/data.py
+++ b/netbox/core/models/data.py
@@ -177,7 +177,7 @@ class DataSource(JobsMixin, PrimaryModel):
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
"""
if self.status == DataSourceStatusChoices.SYNCING:
- raise SyncError("Cannot initiate sync; syncing already in progress.")
+ raise SyncError(_("Cannot initiate sync; syncing already in progress."))
# Emit the pre_sync signal
pre_sync.send(sender=self.__class__, instance=self)
@@ -190,7 +190,7 @@ class DataSource(JobsMixin, PrimaryModel):
backend = self.get_backend()
except ModuleNotFoundError as e:
raise SyncError(
- f"There was an error initializing the backend. A dependency needs to be installed: {e}"
+ _("There was an error initializing the backend. A dependency needs to be installed: ") + str(e)
)
with backend.fetch() as local_path:
diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py
index 7cc62a15a..2e3425129 100644
--- a/netbox/core/models/jobs.py
+++ b/netbox/core/models/jobs.py
@@ -181,7 +181,11 @@ class Job(models.Model):
"""
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
if status not in valid_statuses:
- raise ValueError(f"Invalid status for job termination. Choices are: {', '.join(valid_statuses)}")
+ raise ValueError(
+ _("Invalid status for job termination. Choices are: {choices}").format(
+ choices=', '.join(valid_statuses)
+ )
+ )
# Mark the job as completed
self.status = status
diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py
index cd25761f0..44db21bff 100644
--- a/netbox/core/tests/test_api.py
+++ b/netbox/core/tests/test_api.py
@@ -16,7 +16,7 @@ class AppTest(APITestCase):
class DataSourceTest(APIViewTestCases.APIViewTestCase):
model = DataSource
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'enabled': False,
'description': 'foo bar baz',
diff --git a/netbox/core/views.py b/netbox/core/views.py
index 5662b126e..0379900e1 100644
--- a/netbox/core/views.py
+++ b/netbox/core/views.py
@@ -184,7 +184,7 @@ class ConfigView(generic.ObjectView):
except ConfigRevision.DoesNotExist:
# Fall back to using the active config data if no record is found
return ConfigRevision(
- data=get_config()
+ data=get_config().defaults
)
diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py
index c8440612d..9a59af8e2 100644
--- a/netbox/dcim/api/nested_serializers.py
+++ b/netbox/dcim/api/nested_serializers.py
@@ -2,7 +2,8 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from dcim import models
-from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
+from netbox.api.fields import RelatedObjectCountField
+from netbox.api.serializers import WritableNestedSerializer
__all__ = [
'ComponentNestedModuleSerializer',
@@ -110,7 +111,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
)
class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
- rack_count = serializers.IntegerField(read_only=True)
+ rack_count = RelatedObjectCountField('racks')
class Meta:
model = models.RackRole
@@ -122,7 +123,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
)
class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
- device_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('devices')
class Meta:
model = models.Rack
@@ -150,7 +151,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
)
class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
- devicetype_count = serializers.IntegerField(read_only=True)
+ devicetype_count = RelatedObjectCountField('device_types')
class Meta:
model = models.Manufacturer
@@ -163,7 +164,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
- device_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('instances')
class Meta:
model = models.DeviceType
@@ -173,7 +174,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
- # module_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.ModuleType
@@ -274,8 +274,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
)
class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('devices')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = models.DeviceRole
@@ -287,8 +287,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
)
class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
+ device_count = RelatedObjectCountField('devices')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = models.Platform
@@ -445,7 +445,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
)
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
- inventoryitem_count = serializers.IntegerField(read_only=True)
+ inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta:
model = models.InventoryItemRole
@@ -490,7 +490,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
)
class NestedPowerPanelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
- powerfeed_count = serializers.IntegerField(read_only=True)
+ powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = models.PowerPanel
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 09933f2de..8fbe9fd04 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -15,7 +15,7 @@ from ipam.api.nested_serializers import (
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.models import ASN, VLAN
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import (
GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
WritableNestedSerializer,
@@ -114,6 +114,7 @@ class RegionSerializer(NestedGroupModelSerializer):
'id', '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):
@@ -127,6 +128,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
'id', '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):
@@ -144,12 +146,12 @@ class SiteSerializer(NetBoxModelSerializer):
)
# Related object counts
- circuit_count = serializers.IntegerField(read_only=True)
- device_count = serializers.IntegerField(read_only=True)
- prefix_count = serializers.IntegerField(read_only=True)
- rack_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
- vlan_count = serializers.IntegerField(read_only=True)
+ circuit_count = RelatedObjectCountField('circuit_terminations')
+ device_count = RelatedObjectCountField('devices')
+ prefix_count = RelatedObjectCountField('prefixes')
+ rack_count = RelatedObjectCountField('racks')
+ vlan_count = RelatedObjectCountField('vlans')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Site
@@ -159,6 +161,7 @@ class SiteSerializer(NetBoxModelSerializer):
'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')
#
@@ -180,11 +183,14 @@ class LocationSerializer(NestedGroupModelSerializer):
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
- rack_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ rack_count = RelatedObjectCountField('racks')
class Meta:
model = RackRole
@@ -192,6 +198,7 @@ class RackRoleSerializer(NetBoxModelSerializer):
'id', '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):
@@ -207,8 +214,10 @@ class RackSerializer(NetBoxModelSerializer):
width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
- device_count = serializers.IntegerField(read_only=True)
- powerfeed_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ device_count = RelatedObjectCountField('devices')
+ powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = Rack
@@ -218,6 +227,7 @@ class RackSerializer(NetBoxModelSerializer):
'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 RackUnitSerializer(serializers.Serializer):
@@ -252,6 +262,7 @@ class RackReservationSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
'comments', 'tags', 'custom_fields',
]
+ brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')
class RackElevationDetailFilterSerializer(serializers.Serializer):
@@ -299,9 +310,11 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
- devicetype_count = serializers.IntegerField(read_only=True)
- inventoryitem_count = serializers.IntegerField(read_only=True)
- platform_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ devicetype_count = RelatedObjectCountField('device_types')
+ inventoryitem_count = RelatedObjectCountField('inventory_items')
+ platform_count = RelatedObjectCountField('platforms')
class Meta:
model = Manufacturer
@@ -309,6 +322,7 @@ class ManufacturerSerializer(NetBoxModelSerializer):
'id', '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')
class DeviceTypeSerializer(NetBoxModelSerializer):
@@ -325,7 +339,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
- device_count = serializers.IntegerField(read_only=True)
+ front_image = serializers.URLField(allow_null=True, required=False)
+ rear_image = serializers.URLField(allow_null=True, required=False)
# Counter fields
console_port_template_count = serializers.IntegerField(read_only=True)
@@ -339,6 +354,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
module_bay_template_count = serializers.IntegerField(read_only=True)
inventory_item_template_count = serializers.IntegerField(read_only=True)
+ # Related object counts
+ device_count = RelatedObjectCountField('instances')
+
class Meta:
model = DeviceType
fields = [
@@ -350,6 +368,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
'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):
@@ -363,6 +382,7 @@ class ModuleTypeSerializer(NetBoxModelSerializer):
'id', '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')
#
@@ -393,6 +413,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
@@ -419,6 +440,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerPortTemplateSerializer(ValidatedModelSerializer):
@@ -446,6 +468,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
'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):
@@ -483,6 +506,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
'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):
@@ -527,6 +551,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
'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):
@@ -549,6 +574,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
'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):
@@ -572,6 +598,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
'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):
@@ -584,6 +611,7 @@ class ModuleBayTemplateSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
@@ -593,6 +621,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = DeviceBayTemplate
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
@@ -619,6 +648,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
'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')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_component(self, obj):
@@ -636,8 +666,10 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ device_count = RelatedObjectCountField('devices')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = DeviceRole
@@ -645,14 +677,17 @@ class DeviceRoleSerializer(NetBoxModelSerializer):
'id', '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 PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
- device_count = serializers.IntegerField(read_only=True)
- virtualmachine_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ device_count = RelatedObjectCountField('devices')
+ virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Platform
@@ -660,13 +695,13 @@ class PlatformSerializer(NetBoxModelSerializer):
'id', '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')
class DeviceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = NestedDeviceTypeSerializer()
role = NestedDeviceRoleSerializer()
- device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.')
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer()
@@ -708,14 +743,15 @@ class DeviceSerializer(NetBoxModelSerializer):
class Meta:
model = Device
fields = [
- 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_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', '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')
@extend_schema_field(NestedDeviceSerializer)
def get_parent_device(self, obj):
@@ -728,22 +764,19 @@ class DeviceSerializer(NetBoxModelSerializer):
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
return data
- def get_device_role(self, obj):
- return obj.role
-
class DeviceWithConfigContextSerializer(DeviceSerializer):
config_context = serializers.SerializerMethodField(read_only=True)
class Meta(DeviceSerializer.Meta):
fields = [
- 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_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', '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))
@@ -761,7 +794,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts
- interface_count = serializers.IntegerField(read_only=True)
+ interface_count = RelatedObjectCountField('interfaces')
class Meta:
model = VirtualDeviceContext
@@ -770,6 +803,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
'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):
@@ -785,6 +819,7 @@ class ModuleSerializer(NetBoxModelSerializer):
'id', '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')
#
@@ -817,6 +852,7 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer,
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
]
+ brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
@@ -845,6 +881,7 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
]
+ brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
@@ -879,6 +916,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied',
]
+ brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
@@ -903,6 +941,7 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied',
]
+ brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
@@ -965,6 +1004,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
]
+ brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
def validate(self, data):
@@ -996,6 +1036,7 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
'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')
class FrontPortRearPortSerializer(WritableNestedSerializer):
@@ -1026,6 +1067,7 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
'rear_port_position', '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')
class ModuleBaySerializer(NetBoxModelSerializer):
@@ -1037,9 +1079,9 @@ class ModuleBaySerializer(NetBoxModelSerializer):
model = ModuleBay
fields = [
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
- 'custom_fields',
- 'created', 'last_updated',
+ 'custom_fields', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
class DeviceBaySerializer(NetBoxModelSerializer):
@@ -1053,6 +1095,7 @@ class DeviceBaySerializer(NetBoxModelSerializer):
'id', '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):
@@ -1076,6 +1119,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
'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')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_component(self, obj):
@@ -1092,7 +1136,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
- inventoryitem_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta:
model = InventoryItemRole
@@ -1100,6 +1146,7 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'inventoryitem_count',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
#
@@ -1120,6 +1167,7 @@ class CableSerializer(NetBoxModelSerializer):
'id', '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):
@@ -1190,6 +1238,7 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'member_count',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')
#
@@ -1204,7 +1253,9 @@ class PowerPanelSerializer(NetBoxModelSerializer):
allow_null=True,
default=None
)
- powerfeed_count = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = PowerPanel
@@ -1212,6 +1263,7 @@ class PowerPanelSerializer(NetBoxModelSerializer):
'id', '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):
@@ -1251,3 +1303,4 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'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')
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index cd5a297c9..8a674656d 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -13,7 +13,6 @@ from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import *
from dcim.svg import CableTraceSVG
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
-from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
@@ -23,7 +22,6 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model
from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
-from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@@ -103,7 +101,7 @@ class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'region',
'site_count',
cumulative=True
- ).prefetch_related('tags')
+ )
serializer_class = serializers.RegionSerializer
filterset_class = filtersets.RegionFilterSet
@@ -119,7 +117,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'group',
'site_count',
cumulative=True
- ).prefetch_related('tags')
+ )
serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet
@@ -129,16 +127,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class SiteViewSet(NetBoxModelViewSet):
- queryset = Site.objects.prefetch_related(
- 'region', 'tenant', 'asns', 'tags'
- ).annotate(
- device_count=count_related(Device, 'site'),
- rack_count=count_related(Rack, 'site'),
- prefix_count=count_related(Prefix, 'site'),
- vlan_count=count_related(VLAN, 'site'),
- circuit_count=count_related(Circuit, 'terminations__site'),
- virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
- )
+ queryset = Site.objects.all()
serializer_class = serializers.SiteSerializer
filterset_class = filtersets.SiteFilterSet
@@ -160,7 +149,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'location',
'rack_count',
cumulative=True
- ).prefetch_related('site', 'tags')
+ )
serializer_class = serializers.LocationSerializer
filterset_class = filtersets.LocationFilterSet
@@ -170,9 +159,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class RackRoleViewSet(NetBoxModelViewSet):
- queryset = RackRole.objects.prefetch_related('tags').annotate(
- rack_count=count_related(Rack, 'role')
- )
+ queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filterset_class = filtersets.RackRoleFilterSet
@@ -182,15 +169,16 @@ class RackRoleViewSet(NetBoxModelViewSet):
#
class RackViewSet(NetBoxModelViewSet):
- queryset = Rack.objects.prefetch_related(
- 'site', 'location', 'role', 'tenant', 'tags'
- ).annotate(
- device_count=count_related(Device, 'rack'),
- powerfeed_count=count_related(PowerFeed, 'rack')
- )
+ queryset = Rack.objects.all()
serializer_class = serializers.RackSerializer
filterset_class = filtersets.RackFilterSet
+ @extend_schema(
+ operation_id='dcim_racks_elevation_retrieve',
+ filters=False,
+ parameters=[serializers.RackElevationDetailFilterSerializer],
+ responses={200: serializers.RackUnitSerializer(many=True)}
+ )
@action(detail=True)
def elevation(self, request, pk=None):
"""
@@ -249,7 +237,7 @@ class RackViewSet(NetBoxModelViewSet):
#
class RackReservationViewSet(NetBoxModelViewSet):
- queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
+ queryset = RackReservation.objects.all()
serializer_class = serializers.RackReservationSerializer
filterset_class = filtersets.RackReservationFilterSet
@@ -259,11 +247,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
#
class ManufacturerViewSet(NetBoxModelViewSet):
- queryset = Manufacturer.objects.prefetch_related('tags').annotate(
- devicetype_count=count_related(DeviceType, 'manufacturer'),
- inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
- platform_count=count_related(Platform, 'manufacturer')
- )
+ queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filterset_class = filtersets.ManufacturerFilterSet
@@ -273,21 +257,15 @@ class ManufacturerViewSet(NetBoxModelViewSet):
#
class DeviceTypeViewSet(NetBoxModelViewSet):
- queryset = DeviceType.objects.prefetch_related('manufacturer', 'default_platform', 'tags').annotate(
- device_count=count_related(Device, 'device_type')
- )
+ queryset = DeviceType.objects.all()
serializer_class = serializers.DeviceTypeSerializer
filterset_class = filtersets.DeviceTypeFilterSet
- brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(NetBoxModelViewSet):
- queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
- # module_count=count_related(Module, 'module_type')
- )
+ queryset = ModuleType.objects.all()
serializer_class = serializers.ModuleTypeSerializer
filterset_class = filtersets.ModuleTypeFilterSet
- brief_prefetch_fields = ['manufacturer']
#
@@ -295,61 +273,61 @@ class ModuleTypeViewSet(NetBoxModelViewSet):
#
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
- queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = ConsolePortTemplate.objects.all()
serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
- queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = ConsoleServerPortTemplate.objects.all()
serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(NetBoxModelViewSet):
- queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = PowerPortTemplate.objects.all()
serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
- queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = PowerOutletTemplate.objects.all()
serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(NetBoxModelViewSet):
- queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = InterfaceTemplate.objects.all()
serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(NetBoxModelViewSet):
- queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = FrontPortTemplate.objects.all()
serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(NetBoxModelViewSet):
- queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = RearPortTemplate.objects.all()
serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filtersets.RearPortTemplateFilterSet
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
- queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = ModuleBayTemplate.objects.all()
serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
- queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
+ queryset = DeviceBayTemplate.objects.all()
serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
- queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
+ queryset = InventoryItemTemplate.objects.all()
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
@@ -359,10 +337,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class DeviceRoleViewSet(NetBoxModelViewSet):
- queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate(
- device_count=count_related(Device, 'role'),
- virtualmachine_count=count_related(VirtualMachine, 'role')
- )
+ queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filterset_class = filtersets.DeviceRoleFilterSet
@@ -372,10 +347,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
#
class PlatformViewSet(NetBoxModelViewSet):
- queryset = Platform.objects.prefetch_related('config_template', 'tags').annotate(
- device_count=count_related(Device, 'platform'),
- virtualmachine_count=count_related(VirtualMachine, 'platform')
- )
+ queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_class = filtersets.PlatformFilterSet
@@ -391,8 +363,7 @@ class DeviceViewSet(
NetBoxModelViewSet
):
queryset = Device.objects.prefetch_related(
- 'device_type__manufacturer', 'role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
- 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
+ 'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
)
filterset_class = filtersets.DeviceFilterSet
pagination_class = StripCountAnnotationsPaginator
@@ -407,31 +378,21 @@ class DeviceViewSet(
Else, return the DeviceWithConfigContextSerializer
"""
-
request = self.get_serializer_context()['request']
- if request.query_params.get('brief', False):
- return serializers.NestedDeviceSerializer
-
- elif 'config_context' in request.query_params.get('exclude', []):
+ if self.brief or 'config_context' in request.query_params.get('exclude', []):
return serializers.DeviceSerializer
return serializers.DeviceWithConfigContextSerializer
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
- queryset = VirtualDeviceContext.objects.prefetch_related(
- 'device__device_type', 'device', 'tenant', 'tags',
- ).annotate(
- interface_count=count_related(Interface, 'vdcs'),
- )
+ queryset = VirtualDeviceContext.objects.all()
serializer_class = serializers.VirtualDeviceContextSerializer
filterset_class = filtersets.VirtualDeviceContextFilterSet
class ModuleViewSet(NetBoxModelViewSet):
- queryset = Module.objects.prefetch_related(
- 'device', 'module_bay', 'module_type__manufacturer', 'tags',
- )
+ queryset = Module.objects.all()
serializer_class = serializers.ModuleSerializer
filterset_class = filtersets.ModuleFilterSet
@@ -442,49 +403,45 @@ class ModuleViewSet(NetBoxModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsolePort.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.ConsolePortSerializer
filterset_class = filtersets.ConsolePortFilterSet
- brief_prefetch_fields = ['device']
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet
- brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerPort.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.PowerPortSerializer
filterset_class = filtersets.PowerPortFilterSet
- brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerOutlet.objects.prefetch_related(
- 'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet
- brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related(
- 'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
- 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations',
- 'vdcs',
+ '_path', 'cable__terminations',
+ 'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
+ 'ip_addresses', # Referenced by Interface.count_ipaddresses()
+ 'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
)
serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet
- brief_prefetch_fields = ['device']
def get_bulk_destroy_queryset(self):
# Ensure child interfaces are deleted prior to their parents
@@ -493,41 +450,36 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related(
- 'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags'
+ 'cable__terminations',
)
serializer_class = serializers.FrontPortSerializer
filterset_class = filtersets.FrontPortFilterSet
- brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = RearPort.objects.prefetch_related(
- 'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags'
+ 'cable__terminations',
)
serializer_class = serializers.RearPortSerializer
filterset_class = filtersets.RearPortFilterSet
- brief_prefetch_fields = ['device']
class ModuleBayViewSet(NetBoxModelViewSet):
- queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
+ queryset = ModuleBay.objects.all()
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
- brief_prefetch_fields = ['device']
class DeviceBayViewSet(NetBoxModelViewSet):
- queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
+ queryset = DeviceBay.objects.all()
serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet
- brief_prefetch_fields = ['device']
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
- queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
+ queryset = InventoryItem.objects.all()
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
- brief_prefetch_fields = ['device']
#
@@ -535,9 +487,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class InventoryItemRoleViewSet(NetBoxModelViewSet):
- queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
- inventoryitem_count=count_related(InventoryItem, 'role')
- )
+ queryset = InventoryItemRole.objects.all()
serializer_class = serializers.InventoryItemRoleSerializer
filterset_class = filtersets.InventoryItemRoleFilterSet
@@ -554,7 +504,7 @@ class CableViewSet(NetBoxModelViewSet):
class CableTerminationViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
- queryset = CableTermination.objects.prefetch_related('cable', 'termination')
+ queryset = CableTermination.objects.all()
serializer_class = serializers.CableTerminationSerializer
filterset_class = filtersets.CableTerminationFilterSet
@@ -564,10 +514,9 @@ class CableTerminationViewSet(NetBoxModelViewSet):
#
class VirtualChassisViewSet(NetBoxModelViewSet):
- queryset = VirtualChassis.objects.prefetch_related('tags')
+ queryset = VirtualChassis.objects.all()
serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet
- brief_prefetch_fields = ['master']
#
@@ -575,11 +524,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
#
class PowerPanelViewSet(NetBoxModelViewSet):
- queryset = PowerPanel.objects.prefetch_related(
- 'site', 'location'
- ).annotate(
- powerfeed_count=count_related(PowerFeed, 'power_panel')
- )
+ queryset = PowerPanel.objects.all()
serializer_class = serializers.PowerPanelSerializer
filterset_class = filtersets.PowerPanelFilterSet
@@ -590,7 +535,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerFeed.objects.prefetch_related(
- 'power_panel', 'rack', '_path', 'cable__terminations', 'tags'
+ '_path', 'cable__terminations',
)
serializer_class = serializers.PowerFeedSerializer
filterset_class = filtersets.PowerFeedFilterSet
diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py
index 78ff0d4c1..4df66e367 100644
--- a/netbox/dcim/apps.py
+++ b/netbox/dcim/apps.py
@@ -8,9 +8,13 @@ class DCIMConfig(AppConfig):
verbose_name = "DCIM"
def ready(self):
+ from netbox.models.features import register_models
+ from utilities.counters import connect_counters
from . import signals, search
from .models import CableTermination, Device, DeviceType, VirtualChassis
- from utilities.counters import connect_counters
+
+ # Register models
+ register_models(*self.get_models())
# Register denormalized fields
denormalized.register(CableTermination, '_device', {
diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py
index db1a28d39..535756bac 100644
--- a/netbox/dcim/fields.py
+++ b/netbox/dcim/fields.py
@@ -1,6 +1,7 @@
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import models
+from django.utils.translation import gettext as _
from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
from .lookups import PathContains
@@ -41,7 +42,7 @@ class MACAddressField(models.Field):
try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError:
- raise ValidationError(f"Invalid MAC address format: {value}")
+ raise ValidationError(_("Invalid MAC address format: {value}").format(value=value))
def db_type(self, connection):
return 'macaddr'
@@ -67,7 +68,7 @@ class WWNField(models.Field):
try:
return EUI(value, version=64, dialect=eui64_unix_expanded_uppercase)
except AddrFormatError:
- raise ValidationError(f"Invalid WWN format: {value}")
+ raise ValidationError(_("Invalid WWN format: {value}").format(value=value))
def db_type(self, connection):
return 'macaddr8'
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 68edc93f6..6b1611694 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -2,6 +2,8 @@ import django_filters
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from circuits.models import CircuitTermination
from extras.filtersets import LocalConfigContextFilterSet
@@ -818,6 +820,10 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
to_field_name='slug',
label=_('Manufacturer (slug)'),
)
+ available_for_device_type = django_filters.ModelChoiceFilter(
+ queryset=DeviceType.objects.all(),
+ method='get_for_device_type'
+ )
config_template_id = django_filters.ModelMultipleChoiceFilter(
queryset=ConfigTemplate.objects.all(),
label=_('Config template (ID)'),
@@ -827,6 +833,14 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
model = Platform
fields = ['id', 'name', 'slug', 'description']
+ @extend_schema_field(OpenApiTypes.STR)
+ def get_for_device_type(self, queryset, name, value):
+ """
+ Return all Platforms available for a specific manufacturer based on device type and Platforms not assigned any
+ manufacturer
+ """
+ return queryset.filter(Q(manufacturer=None) | Q(manufacturer__device_types=value))
+
class DeviceFilterSet(
NetBoxModelFilterSet,
@@ -1288,18 +1302,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='name',
label=_('Virtual Chassis'),
)
- # TODO: Remove in v4.0
- device_role_id = django_filters.ModelMultipleChoiceFilter(
- field_name='device__role',
- queryset=DeviceRole.objects.all(),
- label=_('Device role (ID)'),
- )
- device_role = django_filters.ModelMultipleChoiceFilter(
- field_name='device__role__slug',
- queryset=DeviceRole.objects.all(),
- to_field_name='slug',
- label=_('Device role (slug)'),
- )
def search(self, queryset, name, value):
if not value.strip():
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index 68d8d4f89..79ecc8383 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -557,6 +557,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
label=_('Device type'),
queryset=DeviceType.objects.all(),
required=False,
+ context={
+ 'parent': 'manufacturer',
+ },
query_params={
'manufacturer_id': '$manufacturer'
}
@@ -640,6 +643,9 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False,
query_params={
'manufacturer_id': '$manufacturer'
+ },
+ context={
+ 'parent': 'manufacturer',
}
)
status = forms.ChoiceField(
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index f30ff91fa..47974096f 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -159,6 +159,14 @@ class LocationImportForm(NetBoxModelImportForm):
model = Location
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
+ def __init__(self, data=None, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
+
+ if data:
+ # Limit location queryset by assigned site
+ params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
+ self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
+
class RackRoleImportForm(NetBoxModelImportForm):
slug = SlugField()
@@ -870,7 +878,11 @@ class InterfaceImportForm(NetBoxModelImportForm):
def clean_vdcs(self):
for vdc in self.cleaned_data['vdcs']:
if vdc.device != self.cleaned_data['device']:
- raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
+ raise forms.ValidationError(
+ _("VDC {vdc} is not assigned to device {device}").format(
+ vdc=vdc, device=self.cleaned_data['device']
+ )
+ )
return self.cleaned_data['vdcs']
@@ -996,7 +1008,7 @@ class DeviceBayImportForm(NetBoxModelImportForm):
device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
).exclude(pk=device.pk)
else:
- self.fields['installed_device'].queryset = Interface.objects.none()
+ self.fields['installed_device'].queryset = Device.objects.none()
class InventoryItemImportForm(NetBoxModelImportForm):
@@ -1075,7 +1087,11 @@ class InventoryItemImportForm(NetBoxModelImportForm):
component = model.objects.get(device=device, name=component_name)
self.instance.component = component
except ObjectDoesNotExist:
- raise forms.ValidationError(f"Component not found: {device} - {component_name}")
+ raise forms.ValidationError(
+ _("Component not found: {device} - {component_name}").format(
+ device=device, component_name=component_name
+ )
+ )
#
@@ -1193,10 +1209,17 @@ class CableImportForm(NetBoxModelImportForm):
else:
termination_object = model.objects.get(device=device, name=name)
if termination_object.cable is not None and termination_object.cable != self.instance:
- raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
+ raise forms.ValidationError(
+ _("Side {side_upper}: {device} {termination_object} is already connected").format(
+ side_upper=side.upper(), device=device, termination_object=termination_object
+ )
+ )
except ObjectDoesNotExist:
- raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
-
+ raise forms.ValidationError(
+ _("{side_upper} side termination not found: {device} {name}").format(
+ side_upper=side.upper(), device=device, name=name
+ )
+ )
setattr(self.instance, f'{side}_terminations', [termination_object])
return termination_object
diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py
index 854c5ebed..67ef360fe 100644
--- a/netbox/dcim/forms/connections.py
+++ b/netbox/dcim/forms/connections.py
@@ -30,7 +30,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(),
label=term_cls._meta.verbose_name.title(),
- disabled_indicator='_occupied',
+ context={
+ 'disabled': '_occupied',
+ },
query_params={
'device_id': f'$termination_{cable_end}_device',
'kind': 'physical', # Exclude virtual interfaces
@@ -52,7 +54,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(),
label=_('Power Feed'),
- disabled_indicator='_occupied',
+ context={
+ 'disabled': '_occupied',
+ },
query_params={
'power_panel_id': f'$termination_{cable_end}_powerpanel',
}
@@ -72,7 +76,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(),
label=_('Side'),
- disabled_indicator='_occupied',
+ context={
+ 'disabled': '_occupied',
+ },
query_params={
'circuit_id': f'$termination_{cable_end}_circuit',
}
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index fdb5f1d5a..6c33ea8d6 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -291,7 +291,11 @@ class DeviceTypeForm(NetBoxModelForm):
default_platform = DynamicModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
- required=False
+ required=False,
+ selector=True,
+ query_params={
+ 'manufacturer_id': ['$manufacturer', 'null'],
+ }
)
slug = SlugField(
label=_('Slug'),
@@ -426,7 +430,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/elevation/',
attrs={
- 'disabled-indicator': 'device',
+ 'ts-disabled-field': 'device',
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
},
)
@@ -434,6 +438,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
+ context={
+ 'parent': 'manufacturer',
+ },
selector=True
)
role = DynamicModelChoiceField(
@@ -444,7 +451,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Platform'),
queryset=Platform.objects.all(),
required=False,
- selector=True
+ selector=True,
+ query_params={
+ 'available_for_device_type': '$device_type',
+ }
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
@@ -461,6 +471,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(),
required=False,
+ context={
+ 'parent': 'master',
+ },
selector=True
)
vc_position = forms.IntegerField(
@@ -568,6 +581,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
+ context={
+ 'parent': 'manufacturer',
+ },
selector=True
)
comments = CommentField()
@@ -774,7 +790,10 @@ class VCMemberSelectForm(forms.Form):
class ComponentTemplateForm(forms.ModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
- queryset=DeviceType.objects.all()
+ queryset=DeviceType.objects.all(),
+ context={
+ 'parent': 'manufacturer',
+ }
)
def __init__(self, *args, **kwargs):
@@ -789,12 +808,18 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all().all(),
- required=False
+ required=False,
+ context={
+ 'parent': 'manufacturer',
+ }
)
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
- required=False
+ required=False,
+ context={
+ 'parent': 'manufacturer',
+ }
)
def __init__(self, *args, **kwargs):
diff --git a/netbox/dcim/migrations/0002_squashed.py b/netbox/dcim/migrations/0002_squashed.py
index c7325210e..786167680 100644
--- a/netbox/dcim/migrations/0002_squashed.py
+++ b/netbox/dcim/migrations/0002_squashed.py
@@ -233,7 +233,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='powerfeed',
name='rack',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.rack'),
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.rack'),
),
migrations.AddField(
model_name='powerfeed',
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index 7ae755602..cba345941 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -160,25 +160,26 @@ class Cable(PrimaryModel):
# Validate length and length_unit
if self.length is not None and not self.length_unit:
- raise ValidationError("Must specify a unit when setting a cable length")
+ raise ValidationError(_("Must specify a unit when setting a cable length"))
if self.pk is None and (not self.a_terminations or not self.b_terminations):
- raise ValidationError("Must define A and B terminations when creating a new cable.")
+ raise ValidationError(_("Must define A and B terminations when creating a new cable."))
if self._terminations_modified:
# Check that all termination objects for either end are of the same type
for terms in (self.a_terminations, self.b_terminations):
if len(terms) > 1 and not all(isinstance(t, type(terms[0])) for t in terms[1:]):
- raise ValidationError("Cannot connect different termination types to same end of cable.")
+ raise ValidationError(_("Cannot connect different termination types to same end of cable."))
# Check that termination types are compatible
if self.a_terminations and self.b_terminations:
a_type = self.a_terminations[0]._meta.model_name
b_type = self.b_terminations[0]._meta.model_name
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
- raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
-
+ raise ValidationError(
+ _("Incompatible termination types: {type_a} and {type_b}").format(type_a=a_type, type_b=b_type)
+ )
if a_type == b_type:
# can't directly use self.a_terminations here as possible they
# don't have pk yet
@@ -327,17 +328,24 @@ class CableTermination(ChangeLoggedModel):
existing_termination = qs.first()
if existing_termination is not None:
raise ValidationError(
- f"Duplicate termination found for {self.termination_type.app_label}.{self.termination_type.model} "
- f"{self.termination_id}: cable {existing_termination.cable.pk}"
+ _("Duplicate termination found for {app_label}.{model} {termination_id}: cable {cable_pk}".format(
+ app_label=self.termination_type.app_label,
+ model=self.termination_type.model,
+ termination_id=self.termination_id,
+ cable_pk=existing_termination.cable.pk
+ ))
)
-
# Validate interface type (if applicable)
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
- raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces")
+ raise ValidationError(
+ _("Cables cannot be terminated to {type_display} interfaces").format(
+ type_display=self.termination.get_type_display()
+ )
+ )
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
- raise ValidationError("Circuit terminations attached to a provider network may not be cabled.")
+ raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled."))
def save(self, *args, **kwargs):
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 88dddb312..5b2564b32 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1133,13 +1133,13 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
super().clean()
# Validate that the parent Device can have DeviceBays
- if not self.device.device_type.is_parent_device:
+ if hasattr(self, 'device') and not self.device.device_type.is_parent_device:
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
device_type=self.device.device_type
))
# Cannot install a device into itself, obviously
- if self.device == self.installed_device:
+ if self.installed_device and getattr(self, 'device', None) == self.installed_device:
raise ValidationError(_("Cannot install a device into itself."))
# Check that the installed device is not already installed elsewhere
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index b0edf49fe..c75757fa7 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -815,20 +815,6 @@ class Device(
def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk])
- @property
- def device_role(self):
- """
- For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
- """
- return self.role
-
- @device_role.setter
- def device_role(self, value):
- """
- For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
- """
- self.role = value
-
def clean(self):
super().clean()
@@ -875,7 +861,7 @@ class Device(
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'position': _(
- "A U0 device type ({device_type}) cannot be assigned to a rack position."
+ "A 0U device type ({device_type}) cannot be assigned to a rack position."
).format(device_type=self.device_type)
})
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 62578d6c4..826eaae9c 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
rack = models.ForeignKey(
to='Rack',
on_delete=models.PROTECT,
+ related_name='powerfeeds',
blank=True,
null=True
)
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 4c863e12a..d4c9641b6 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -359,6 +359,11 @@ class CableTerminationTable(NetBoxTable):
verbose_name=_('Mark Connected'),
)
+ def value_link_peer(self, value):
+ return ', '.join([
+ f"{termination.parent_object} > {termination}" for termination in value
+ ])
+
class PathEndpointTable(CableTerminationTable):
connection = columns.TemplateColumn(
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index 835a72bc6..d3134656c 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -36,7 +36,7 @@ DEVICEBAY_STATUS = """
INTERFACE_IPADDRESSES = """
{% if value.count > 3 %}
- {{ value.count }}
+ {{ value.count }}
{% else %}
{% for ip in value.all %}
{% if ip.status != 'active' %}
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index d012a840f..0a3931696 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -1,6 +1,7 @@
from django.contrib.auth import get_user_model
from django.test import override_settings
from django.urls import reverse
+from django.utils.translation import gettext as _
from rest_framework import status
from dcim.choices import *
@@ -45,7 +46,7 @@ class Mixins:
name='Peer Device'
)
if self.peer_termination_type is None:
- raise NotImplementedError("Test case must set peer_termination_type")
+ raise NotImplementedError(_("Test case must set peer_termination_type"))
peer_obj = self.peer_termination_type.objects.create(
device=peer_device,
name='Peer Termination'
@@ -67,7 +68,7 @@ class Mixins:
class RegionTest(APIViewTestCases.APIViewTestCase):
model = Region
- brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
+ brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url']
create_data = [
{
'name': 'Region 4',
@@ -96,7 +97,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
class SiteGroupTest(APIViewTestCases.APIViewTestCase):
model = SiteGroup
- brief_fields = ['_depth', 'display', 'id', 'name', 'site_count', 'slug', 'url']
+ brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url']
create_data = [
{
'name': 'Site Group 4',
@@ -125,7 +126,7 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase):
class SiteTest(APIViewTestCases.APIViewTestCase):
model = Site
- brief_fields = ['display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -187,7 +188,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
class LocationTest(APIViewTestCases.APIViewTestCase):
model = Location
- brief_fields = ['_depth', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
+ brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -237,7 +238,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
class RackRoleTest(APIViewTestCases.APIViewTestCase):
model = RackRole
- brief_fields = ['display', 'id', 'name', 'rack_count', 'slug', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url']
create_data = [
{
'name': 'Rack Role 4',
@@ -272,7 +273,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
class RackTest(APIViewTestCases.APIViewTestCase):
model = Rack
- brief_fields = ['device_count', 'display', 'id', 'name', 'url']
+ brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -360,7 +361,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
class RackReservationTest(APIViewTestCases.APIViewTestCase):
model = RackReservation
- brief_fields = ['display', 'id', 'units', 'url', 'user']
+ brief_fields = ['description', 'display', 'id', 'units', 'url', 'user']
bulk_update_data = {
'description': 'New description',
}
@@ -407,7 +408,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
model = Manufacturer
- brief_fields = ['devicetype_count', 'display', 'id', 'name', 'slug', 'url']
+ brief_fields = ['description', 'devicetype_count', 'display', 'id', 'name', 'slug', 'url']
create_data = [
{
'name': 'Manufacturer 4',
@@ -439,7 +440,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase):
class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
model = DeviceType
- brief_fields = ['device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
+ brief_fields = ['description', 'device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
bulk_update_data = {
'part_number': 'ABC123',
}
@@ -484,7 +485,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
model = ModuleType
- brief_fields = ['display', 'id', 'manufacturer', 'model', 'url']
+ brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'url']
bulk_update_data = {
'part_number': 'ABC123',
}
@@ -523,7 +524,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsolePortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -567,7 +568,7 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase):
class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = ConsoleServerPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -611,7 +612,7 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase):
class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = PowerPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -655,7 +656,7 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase):
class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
model = PowerOutletTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -712,7 +713,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
model = InterfaceTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -760,7 +761,7 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase):
class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = FrontPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -849,7 +850,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
model = RearPortTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -897,7 +898,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = ModuleBayTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -937,7 +938,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
model = DeviceBayTemplate
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -977,7 +978,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
model = InventoryItemTemplate
- brief_fields = ['_depth', 'display', 'id', 'name', 'url']
+ brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1028,7 +1029,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
model = DeviceRole
- brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
+ brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
create_data = [
{
'name': 'Device Role 4',
@@ -1063,7 +1064,7 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase):
class PlatformTest(APIViewTestCases.APIViewTestCase):
model = Platform
- brief_fields = ['device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
+ brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
create_data = [
{
'name': 'Platform 4',
@@ -1095,7 +1096,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
class DeviceTest(APIViewTestCases.APIViewTestCase):
model = Device
- brief_fields = ['display', 'id', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'failed',
}
@@ -1285,7 +1286,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
class ModuleTest(APIViewTestCases.APIViewTestCase):
model = Module
- brief_fields = ['device', 'display', 'id', 'module_bay', 'module_type', 'url']
+ brief_fields = ['description', 'device', 'display', 'id', 'module_bay', 'module_type', 'url']
bulk_update_data = {
'serial': '1234ABCD',
}
@@ -1349,7 +1350,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsolePort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1391,7 +1392,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsoleServerPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1433,7 +1434,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = PowerPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1472,7 +1473,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = PowerOutlet
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1520,7 +1521,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = Interface
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1654,7 +1655,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
class FrontPortTest(APIViewTestCases.APIViewTestCase):
model = FrontPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1712,7 +1713,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
class RearPortTest(APIViewTestCases.APIViewTestCase):
model = RearPort
- brief_fields = ['_occupied', 'cable', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1754,7 +1755,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay
- brief_fields = ['display', 'id', 'module', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'installed_module', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1793,7 +1794,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
class DeviceBayTest(APIViewTestCases.APIViewTestCase):
model = DeviceBay
- brief_fields = ['device', 'display', 'id', 'name', 'url']
+ brief_fields = ['description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1856,7 +1857,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
class InventoryItemTest(APIViewTestCases.APIViewTestCase):
model = InventoryItem
- brief_fields = ['_depth', 'device', 'display', 'id', 'name', 'url']
+ brief_fields = ['_depth', 'description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@@ -1916,7 +1917,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
model = InventoryItemRole
- brief_fields = ['display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
+ brief_fields = ['description', 'display', 'id', 'inventoryitem_count', 'name', 'slug', 'url']
create_data = [
{
'name': 'Inventory Item Role 4',
@@ -1951,7 +1952,7 @@ class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase):
class CableTest(APIViewTestCases.APIViewTestCase):
model = Cable
- brief_fields = ['display', 'id', 'label', 'url']
+ brief_fields = ['description', 'display', 'id', 'label', 'url']
bulk_update_data = {
'length': 100,
'length_unit': 'm',
@@ -2074,7 +2075,7 @@ class ConnectedDeviceTest(APITestCase):
class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
model = VirtualChassis
- brief_fields = ['display', 'id', 'master', 'member_count', 'name', 'url']
+ brief_fields = ['description', 'display', 'id', 'master', 'member_count', 'name', 'url']
@classmethod
def setUpTestData(cls):
@@ -2155,7 +2156,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
model = PowerPanel
- brief_fields = ['display', 'id', 'name', 'powerfeed_count', 'url']
+ brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
@classmethod
def setUpTestData(cls):
@@ -2204,7 +2205,7 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
class PowerFeedTest(APIViewTestCases.APIViewTestCase):
model = PowerFeed
- brief_fields = ['_occupied', 'cable', 'display', 'id', 'name', 'url']
+ brief_fields = ['_occupied', 'cable', 'description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
@@ -2259,7 +2260,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase):
model = VirtualDeviceContext
- brief_fields = ['device', 'display', 'id', 'identifier', 'name', 'url']
+ brief_fields = ['description', 'device', 'display', 'id', 'identifier', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py
index a827939f7..49a71022e 100644
--- a/netbox/dcim/tests/test_cablepaths.py
+++ b/netbox/dcim/tests/test_cablepaths.py
@@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
device = Device.objects.create(
site=self.site,
device_type=self.device.device_type,
- device_role=self.device.device_role,
+ role=self.device.role,
name='Test mid-span Device'
)
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 89d15a0ef..b255c283e 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -1787,6 +1787,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='foobar1'),
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='foobar2'),
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='foobar3'),
+ Platform(name='Platform 4', slug='platform-4'),
)
Platform.objects.bulk_create(platforms)
@@ -1813,6 +1814,17 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_available_for_device_type(self):
+ manufacturers = Manufacturer.objects.all()[:2]
+ device_type = DeviceType.objects.create(
+ manufacturer=manufacturers[0],
+ model='Device Type 1',
+ slug='device-type-1',
+ u_height=1
+ )
+ params = {'available_for_device_type': device_type.pk}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Device.objects.all()
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index d56bf0741..8eb057020 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
device2.full_clean()
device2.save()
- def test_old_device_role_field(self):
- """
- Ensure that the old device role field sets the value in the new role field.
- """
-
- # Test getter method
- device = Device(
- site=Site.objects.first(),
- device_type=DeviceType.objects.first(),
- role=DeviceRole.objects.first(),
- name='Test Device 1',
- device_role=DeviceRole.objects.first()
- )
- device.full_clean()
- device.save()
-
- self.assertEqual(device.role, device.device_role)
-
- # Test setter method
- device.device_role = DeviceRole.objects.last()
- device.full_clean()
- device.save()
- self.assertEqual(device.role, device.device_role)
-
class CableTestCase(TestCase):
diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py
index ce3b388f4..6cd3a245e 100644
--- a/netbox/extras/api/customfields.py
+++ b/netbox/extras/api/customfields.py
@@ -1,4 +1,5 @@
from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
from rest_framework.fields import Field
@@ -88,7 +89,7 @@ class CustomFieldsDataField(Field):
if serializer.is_valid():
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
else:
- raise ValidationError(f"Unknown related object(s): {data[cf.name]}")
+ raise ValidationError(_("Unknown related object(s): {name}").format(name=data[cf.name]))
# If updating an existing instance, start with existing custom_field_data
if self.parent.instance:
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index e2f30e8b0..7dad95263 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -1,9 +1,9 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
+from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from rest_framework.fields import ListField
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
from core.api.serializers import JobSerializer
@@ -16,7 +16,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
from extras.choices import *
from extras.models import *
from netbox.api.exceptions import SerializerNotFound
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
from netbox.api.serializers.features import TaggableModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
@@ -44,9 +44,6 @@ __all__ = (
'ImageAttachmentSerializer',
'JournalEntrySerializer',
'ObjectChangeSerializer',
- 'ReportDetailSerializer',
- 'ReportSerializer',
- 'ReportInputSerializer',
'SavedFilterSerializer',
'ScriptDetailSerializer',
'ScriptInputSerializer',
@@ -79,15 +76,16 @@ class EventRuleSerializer(NetBoxModelSerializer):
'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',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(OpenApiTypes.OBJECT)
def get_action_object(self, instance):
context = {'request': self.context['request']}
# We need to manually instantiate the serializer for scripts
if instance.action_type == EventRuleActionChoices.SCRIPT:
- script_name = instance.action_parameters['script_name']
- script = instance.action_object.scripts[script_name]()
- return NestedScriptSerializer(script, context=context).data
+ script = instance.action_object
+ instance = script.python_class() if script.python_class else None
+ return NestedScriptSerializer(instance, context=context).data
else:
serializer = get_serializer_for_model(
model=instance.action_object_type.model_class(),
@@ -110,6 +108,7 @@ class WebhookSerializer(NetBoxModelSerializer):
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
'tags', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
#
@@ -145,10 +144,11 @@ class CustomFieldSerializer(ValidatedModelSerializer):
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
def validate_type(self, value):
if self.instance and self.instance.type != value:
- raise serializers.ValidationError('Changing the type of custom fields is not supported.')
+ raise serializers.ValidationError(_('Changing the type of custom fields is not supported.'))
return value
@@ -187,6 +187,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
'choices_count', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
#
@@ -206,6 +207,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name')
#
@@ -232,6 +234,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
#
@@ -251,6 +254,7 @@ class SavedFilterSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
'shared', 'parameters', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
#
@@ -270,6 +274,7 @@ class BookmarkSerializer(ValidatedModelSerializer):
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
]
+ brief_fields = ('id', 'url', 'display', 'object_id', 'object_type')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, instance):
@@ -288,7 +293,9 @@ class TagSerializer(ValidatedModelSerializer):
many=True,
required=False
)
- tagged_items = serializers.IntegerField(read_only=True)
+
+ # Related object counts
+ tagged_items = RelatedObjectCountField('extras_taggeditem_items')
class Meta:
model = Tag
@@ -296,6 +303,7 @@ class TagSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
#
@@ -315,6 +323,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
'image_width', 'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'image')
def validate(self, data):
@@ -364,6 +373,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
'id', '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')
def validate(self, data):
@@ -487,6 +497,7 @@ class ConfigContextSerializer(ValidatedModelSerializer):
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
'created', 'last_updated',
]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
#
@@ -508,81 +519,58 @@ class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
]
-
-
-#
-# Reports
-#
-
-class ReportSerializer(serializers.Serializer):
- url = serializers.HyperlinkedIdentityField(
- view_name='extras-api:report-detail',
- lookup_field='full_name',
- lookup_url_kwarg='pk'
- )
- id = serializers.CharField(read_only=True, source="full_name")
- module = serializers.CharField(max_length=255)
- name = serializers.CharField(max_length=255)
- description = serializers.CharField(max_length=255, required=False)
- test_methods = serializers.ListField(child=serializers.CharField(max_length=255), read_only=True)
- result = NestedJobSerializer()
- display = serializers.SerializerMethodField(read_only=True)
-
- @extend_schema_field(serializers.CharField())
- def get_display(self, obj):
- return f'{obj.name} ({obj.module})'
-
-
-class ReportDetailSerializer(ReportSerializer):
- result = JobSerializer()
-
-
-class ReportInputSerializer(serializers.Serializer):
- schedule_at = serializers.DateTimeField(required=False, allow_null=True)
- interval = serializers.IntegerField(required=False, allow_null=True)
-
- def validate_schedule_at(self, value):
- if value and not self.context['report'].scheduling_enabled:
- raise serializers.ValidationError("Scheduling is not enabled for this report.")
- return value
-
- def validate_interval(self, value):
- if value and not self.context['report'].scheduling_enabled:
- raise serializers.ValidationError("Scheduling is not enabled for this report.")
- return value
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# Scripts
#
-class ScriptSerializer(serializers.Serializer):
- url = serializers.HyperlinkedIdentityField(
- view_name='extras-api:script-detail',
- lookup_field='full_name',
- lookup_url_kwarg='pk'
- )
- id = serializers.CharField(read_only=True, source="full_name")
- module = serializers.CharField(max_length=255)
- name = serializers.CharField(read_only=True)
- description = serializers.CharField(read_only=True)
+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 = NestedJobSerializer()
- display = serializers.SerializerMethodField(read_only=True)
+ result = NestedJobSerializer(read_only=True)
+
+ class Meta:
+ model = Script
+ fields = [
+ 'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
+ ]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(serializers.JSONField(allow_null=True))
- def get_vars(self, instance):
- return {
- k: v.__class__.__name__ for k, v in instance._get_vars().items()
- }
+ def get_vars(self, obj):
+ if obj.python_class:
+ return {
+ k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items()
+ }
+ else:
+ return {}
@extend_schema_field(serializers.CharField())
def get_display(self, obj):
return f'{obj.name} ({obj.module})'
+ @extend_schema_field(serializers.CharField())
+ def get_description(self, obj):
+ if obj.python_class:
+ return obj.python_class().description
+ else:
+ return None
+
class ScriptDetailSerializer(ScriptSerializer):
- result = JobSerializer()
+ result = serializers.SerializerMethodField(read_only=True)
+
+ @extend_schema_field(JobSerializer())
+ def get_result(self, obj):
+ job = obj.jobs.all().order_by('-created').first()
+ context = {
+ 'request': self.context['request']
+ }
+ data = JobSerializer(job, context=context).data
+ return data
class ScriptInputSerializer(serializers.Serializer):
@@ -593,12 +581,12 @@ class ScriptInputSerializer(serializers.Serializer):
def validate_schedule_at(self, value):
if value and not self.context['script'].scheduling_enabled:
- raise serializers.ValidationError("Scheduling is not enabled for this script.")
+ raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
return value
def validate_interval(self, value):
if value and not self.context['script'].scheduling_enabled:
- raise serializers.ValidationError("Scheduling is not enabled for this script.")
+ raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
return value
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
index 8f5841da1..72450f9c9 100644
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -1,5 +1,4 @@
from django.contrib.contenttypes.models import ContentType
-from django.http import Http404
from django.shortcuts import get_object_or_404
from django_rq.queues import get_connection
from rest_framework import status
@@ -9,21 +8,20 @@ from rest_framework.generics import RetrieveUpdateDestroyAPIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.routers import APIRootView
-from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
+from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rq import Worker
-from core.choices import JobStatusChoices
from core.models import Job
from extras import filtersets
from extras.models import *
-from extras.scripts import get_module_and_script, run_script
+from extras.scripts import run_script
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.features import SyncedDataMixin
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException
-from utilities.utils import copy_safe_request, count_related
+from utilities.utils import copy_safe_request
from . import serializers
from .mixins import ConfigTemplateRenderMixin
@@ -115,7 +113,7 @@ class CustomLinkViewSet(NetBoxModelViewSet):
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
- queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
+ queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer
filterset_class = filtersets.ExportTemplateFilterSet
@@ -147,9 +145,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
#
class TagViewSet(NetBoxModelViewSet):
- queryset = Tag.objects.annotate(
- tagged_items=count_related(TaggedItem, 'tag')
- )
+ queryset = Tag.objects.all()
serializer_class = serializers.TagSerializer
filterset_class = filtersets.TagFilterSet
@@ -181,10 +177,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
#
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
- queryset = ConfigContext.objects.prefetch_related(
- 'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
- 'data_file',
- )
+ queryset = ConfigContext.objects.all()
serializer_class = serializers.ConfigContextSerializer
filterset_class = filtersets.ConfigContextFilterSet
@@ -194,7 +187,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
#
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
- queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
+ queryset = ConfigTemplate.objects.all()
serializer_class = serializers.ConfigTemplateSerializer
filterset_class = filtersets.ConfigTemplateFilterSet
@@ -214,66 +207,30 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
# Scripts
#
-class ScriptViewSet(ViewSet):
+class ScriptViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrLoginNotRequired]
+ queryset = Script.objects.prefetch_related('jobs')
+ serializer_class = serializers.ScriptSerializer
+ filterset_class = filtersets.ScriptFilterSet
+
_ignore_model_permissions = True
- schema = None
lookup_value_regex = '[^/]+' # Allow dots
- def _get_script(self, pk):
- try:
- module_name, script_name = pk.split('.', maxsplit=1)
- except ValueError:
- raise Http404
-
- module, script = get_module_and_script(module_name, script_name)
- if script is None:
- raise Http404
-
- return module, script
-
- def list(self, request):
- results = {
- job.name: job
- for job in Job.objects.filter(
- object_type=ContentType.objects.get(app_label='extras', model='scriptmodule'),
- status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
- ).order_by('name', '-created').distinct('name').defer('data')
- }
-
- script_list = []
- for script_module in ScriptModule.objects.restrict(request.user):
- script_list.extend(script_module.scripts.values())
-
- # Attach Job objects to each script (if any)
- for script in script_list:
- script.result = results.get(script.class_name, None)
-
- serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
-
- return Response({'count': len(script_list), 'results': serializer.data})
-
def retrieve(self, request, pk):
- module, script = self._get_script(pk)
- object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
- script.result = Job.objects.filter(
- object_type=object_type,
- name=script.class_name,
- status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
- ).first()
+ script = get_object_or_404(self.queryset, pk=pk)
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
return Response(serializer.data)
def post(self, request, pk):
"""
- Run a Script identified as ".