From 5a4feb70998d986d2ce121c64942a9e6d585b498 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Fri, 7 Apr 2023 11:13:58 -0700 Subject: [PATCH 01/17] 10615 filter cable termination_id with cable_end (#12182) * 10615 filter cable termination_id with cable_end * 10615 filter distinct * 10615 filter distinct --- netbox/dcim/filtersets.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 7b6c855d9..d9f378c6b 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1667,12 +1667,14 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): field_name='terminations__termination_type' ) termination_a_id = MultiValueNumberFilter( + method='filter_by_cable_end_a', field_name='terminations__termination_id' ) termination_b_type = ContentTypeFilter( field_name='terminations__termination_type' ) termination_b_id = MultiValueNumberFilter( + method='filter_by_cable_end_b', field_name='terminations__termination_id' ) type = django_filters.MultipleChoiceFilter( @@ -1730,6 +1732,18 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): # Supported objects: device, rack, location, site return queryset.filter(**{f'terminations___{name}__in': value}).distinct() + def filter_by_cable_end(self, queryset, name, value, side): + # Filter by termination id and cable_end type + return queryset.filter(**{f'{name}__in': value, 'terminations__cable_end': side}).distinct() + + def filter_by_cable_end_a(self, queryset, name, value): + # Filter by termination id and cable_end type + return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_A) + + def filter_by_cable_end_b(self, queryset, name, value): + # Filter by termination id and cable_end type + return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_B) + class CableTerminationFilterSet(BaseFilterSet): termination_type = ContentTypeFilter() From 1146aaff89a0c6bf607c88ba24737c04a06686bc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Apr 2023 09:12:04 -0400 Subject: [PATCH 02/17] Closes #11453: Display a warning banner when DEBUG is enabled --- docs/configuration/development.md | 2 +- docs/release-notes/version-3.4.md | 2 ++ netbox/templates/base/layout.html | 11 +++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/configuration/development.md b/docs/configuration/development.md index 3af56b0e3..1579f2cdb 100644 --- a/docs/configuration/development.md +++ b/docs/configuration/development.md @@ -18,4 +18,4 @@ interface. Default: False -This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base. +This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Additionally, enabling this setting disables the debug warning banner in the UI. Set this to `True` **only** if you are actively developing the NetBox code base. diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 5f086bca4..0df40a74d 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,11 +4,13 @@ ### Enhancements +* [#11453](https://github.com/netbox-community/netbox/issues/11453) - Display a warning banner when `DEBUG` is enabled * [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN * [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type ### Bug Fixes +* [#10615](https://github.com/netbox-community/netbox/issues/10615) - Fix filtering of cable terminations by A/B end * [#11746](https://github.com/netbox-community/netbox/issues/11746) - Fix cleanup of object data when deleting a custom field * [#12011](https://github.com/netbox-community/netbox/issues/12011) - Fix KeyError exception when attempting to add module bays in bulk * [#12074](https://github.com/netbox-community/netbox/issues/12074) - Fix the automatic assignment of racks to devices via the REST API diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index 2fc6d0d98..6b247d81a 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -70,10 +70,17 @@ Blocks: {% endif %} + {% if settings.DEBUG and not settings.DEVELOPER %} + + {% endif %} + {% if config.MAINTENANCE_MODE %} {% endif %} From 768d6f624ebde75d287d00ec0e37f655bd280676 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Apr 2023 09:17:13 -0400 Subject: [PATCH 03/17] Fixes #12191: Change absolute image path to relative --- docs/customization/reports.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customization/reports.md b/docs/customization/reports.md index b83c4a177..f7d9109ec 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -132,7 +132,7 @@ Once you have created a report, it will appear in the reports list. Initially, r !!! note To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below. - ![Adding the run action to a permission](/media/admin_ui_run_permission.png) + ![Adding the run action to a permission](../media/admin_ui_run_permission.png) ### Via the Web UI From 278f2b173af3612e1bd5ccb7de9e78a7e9a40186 Mon Sep 17 00:00:00 2001 From: kkthxbye <400797+kkthxbye-code@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:13:08 +0200 Subject: [PATCH 04/17] Fixes #11431 - Disallow changing customfield type after creation (#11449) * Disallow changing customfield type after creation * Fix test_api.CustomFieldTest --------- Co-authored-by: kkthxbye-code <> --- netbox/extras/api/serializers.py | 6 ++++++ netbox/extras/forms/model_forms.py | 7 +++++++ netbox/extras/tests/test_api.py | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 8b9c6dcb1..01b841a0f 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -97,6 +97,12 @@ class CustomFieldSerializer(ValidatedModelSerializer): 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated', ] + 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.') + + return value + def get_data_type(self, obj): types = CustomFieldTypeChoices if obj.type == types.TYPE_INTEGER: diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 040882d27..365d55c72 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -64,6 +64,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): 'ui_visibility': StaticSelect(), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Disable changing the type of a CustomField as it almost universally causes errors if custom field data is already present. + if self.instance.pk: + self.fields['type'].disabled = True + class CustomLinkForm(BootstrapMixin, forms.ModelForm): content_types = ContentTypeMultipleChoiceField( diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 4b5efda3c..c915d596a 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -102,6 +102,11 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + update_data = { + 'content_types': ['dcim.device'], + 'name': 'New_Name', + 'description': 'New description', + } @classmethod def setUpTestData(cls): From 2c07762b7a4f992eac17007d38f45a0dd04b4c1c Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 10 Apr 2023 22:10:13 +0530 Subject: [PATCH 05/17] Added optional user and group on custom field (#12206) * added group and user model to object_type * Update netbox/utilities/utils.py Co-authored-by: Jeremy Stretch --------- Co-authored-by: Jeremy Stretch --- netbox/extras/forms/model_forms.py | 3 ++- netbox/utilities/utils.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 365d55c72..b7e606f7d 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -1,6 +1,7 @@ import json from django import forms +from django.db.models import Q from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ @@ -37,7 +38,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): object_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), # TODO: Come up with a canonical way to register suitable models - limit_choices_to=FeatureQuery('webhooks'), + limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") ) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index aec0d896c..57092bb7d 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -48,6 +48,10 @@ def get_viewname(model, action=None, rest_api=False): if is_plugin: viewname = f'plugins-api:{app_label}-api:{model_name}' else: + # Alter the app_label for group and user model_name to point to users app + if app_label == 'auth' and model_name in ['group', 'user']: + app_label = 'users' + viewname = f'{app_label}-api:{model_name}' # Append the action, if any if action: From b41f8755df4ec32041177a4b53f822bebcc8bdc7 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 10 Apr 2023 23:32:32 +0530 Subject: [PATCH 06/17] Fixes GenericForeignKey validation (#11550) * added model validation for GenericForeignKey * added ct_field and fk_field null validation * applied suggestion --- netbox/netbox/models/__init__.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index a4c8e0ec2..96567bc55 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey from django.core.validators import ValidationError from django.db import models from mptt.models import MPTTModel, TreeForeignKey @@ -58,6 +59,33 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model): class Meta: abstract = True + def clean(self): + """ + Validate the model for GenericForeignKey fields to ensure that the content type and object ID exist. + """ + super().clean() + + for field in self._meta.get_fields(): + if isinstance(field, GenericForeignKey): + ct_field = getattr(self, field.ct_field) + fk_field = getattr(self, field.fk_field) + + if ct_field is None and fk_field is not None: + raise ValidationError({ + field.ct_field: "This field cannot be null.", + }) + if fk_field is None and ct_field is not None: + raise ValidationError({ + field.fk_field: "This field cannot be null.", + }) + + if ct_field and fk_field: + klass = getattr(self, field.ct_field).model_class() + if not klass.objects.filter(pk=fk_field).exists(): + raise ValidationError({ + field.fk_field: f"Invalid {fk_field}: object does not exist on {ct_field}." + }) + class PrimaryModel(NetBoxModel): """ From ada01b39cc459219b8675827d7c2626f5a9e7acd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Apr 2023 14:03:59 -0400 Subject: [PATCH 07/17] #10221: Tweak variable names & error message --- netbox/netbox/models/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index 96567bc55..f686828b6 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -67,23 +67,23 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model): for field in self._meta.get_fields(): if isinstance(field, GenericForeignKey): - ct_field = getattr(self, field.ct_field) - fk_field = getattr(self, field.fk_field) + ct_value = getattr(self, field.ct_field) + fk_value = getattr(self, field.fk_field) - if ct_field is None and fk_field is not None: + if ct_value is None and fk_value is not None: raise ValidationError({ field.ct_field: "This field cannot be null.", }) - if fk_field is None and ct_field is not None: + if fk_value is None and ct_value is not None: raise ValidationError({ field.fk_field: "This field cannot be null.", }) - if ct_field and fk_field: + if ct_value and fk_value: klass = getattr(self, field.ct_field).model_class() - if not klass.objects.filter(pk=fk_field).exists(): + if not klass.objects.filter(pk=fk_value).exists(): raise ValidationError({ - field.fk_field: f"Invalid {fk_field}: object does not exist on {ct_field}." + field.fk_field: f"Related object not found using the provided value: {fk_value}." }) From 4c9cf9032c5f840961a84ca0d620b464e4975714 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Apr 2023 14:06:36 -0400 Subject: [PATCH 08/17] Changelog for #10221, #10600, #11431, #11454 --- docs/release-notes/version-3.4.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 0df40a74d..c9603538f 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,13 +4,16 @@ ### Enhancements +* [#10600](https://github.com/netbox-community/netbox/issues/10600) - Allow custom object fields to reference a user or group +* [#11431](https://github.com/netbox-community/netbox/issues/11431) - Disallow changing custom field type after creation * [#11453](https://github.com/netbox-community/netbox/issues/11453) - Display a warning banner when `DEBUG` is enabled * [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN * [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type ### Bug Fixes -* [#10615](https://github.com/netbox-community/netbox/issues/10615) - Fix filtering of cable terminations by A/B end +* [#10221](https://github.com/netbox-community/netbox/issues/10221) - Validate generic foreign key relations assigned via REST API requests +* [#11454](https://github.com/netbox-community/netbox/issues/11454) - Raise validation error if generic foreign key assignment does not specify both object type and ID * [#11746](https://github.com/netbox-community/netbox/issues/11746) - Fix cleanup of object data when deleting a custom field * [#12011](https://github.com/netbox-community/netbox/issues/12011) - Fix KeyError exception when attempting to add module bays in bulk * [#12074](https://github.com/netbox-community/netbox/issues/12074) - Fix the automatic assignment of racks to devices via the REST API From 4a331b560fda50316382bce340d81291e552ec04 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Apr 2023 16:35:21 -0400 Subject: [PATCH 09/17] Closes #11015: Remove unit from commit rate column header in circuits table --- docs/release-notes/version-3.4.md | 1 + netbox/circuits/tables/circuits.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index c9603538f..c1e41a321 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -5,6 +5,7 @@ ### Enhancements * [#10600](https://github.com/netbox-community/netbox/issues/10600) - Allow custom object fields to reference a user or group +* [#11015](https://github.com/netbox-community/netbox/issues/11015) - Remove unit from commit rate column header in circuits table * [#11431](https://github.com/netbox-community/netbox/issues/11431) - Disallow changing custom field type after creation * [#11453](https://github.com/netbox-community/netbox/issues/11453) - Display a warning banner when `DEBUG` is enabled * [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 477f9c1ab..e2cd2b9a9 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -57,7 +57,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): template_code=CIRCUITTERMINATION_LINK, verbose_name='Side Z' ) - commit_rate = CommitRateColumn() + commit_rate = CommitRateColumn( + verbose_name='Commit Rate' + ) comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='circuits:circuit_list' From 6820796c103b785b71569799019a4bca29301434 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 Apr 2023 16:43:40 -0400 Subject: [PATCH 10/17] Closes #10414: Enable general purpose image attachments for device types --- docs/release-notes/version-3.4.md | 1 + netbox/dcim/models/devices.py | 4 ++++ netbox/templates/dcim/devicetype.html | 1 + 3 files changed, 6 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index c1e41a321..596bd0180 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,6 +4,7 @@ ### Enhancements +* [#10414](https://github.com/netbox-community/netbox/issues/10414) - Enable general purpose image attachments for device types * [#10600](https://github.com/netbox-community/netbox/issues/10600) - Allow custom object fields to reference a user or group * [#11015](https://github.com/netbox-community/netbox/issues/11015) - Remove unit from commit rate column header in circuits table * [#11431](https://github.com/netbox-community/netbox/issues/11431) - Disallow changing custom field type after creation diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 0526c49cb..2061a951e 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -120,6 +120,10 @@ class DeviceType(PrimaryModel, WeightMixin): blank=True ) + images = GenericRelation( + to='extras.ImageAttachment' + ) + clone_fields = ( 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit' ) diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 930390a56..ab2fa3382 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -98,6 +98,7 @@ {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %} From 97ed6439cee5a2ab72d72a639eee0f1da2a1da1b Mon Sep 17 00:00:00 2001 From: pobradovic08 Date: Wed, 12 Apr 2023 14:27:24 +0200 Subject: [PATCH 11/17] Fixes #12227: L2VPN Bulk import not setting Tenant field --- netbox/ipam/forms/bulk_import.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 972b98db2..6546722ca 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -443,7 +443,8 @@ class L2VPNImportForm(NetBoxModelImportForm): class Meta: model = L2VPN - fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments', 'tags') + fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description', + 'comments', 'tags') class L2VPNTerminationImportForm(NetBoxModelImportForm): From 9e305c618152045ec4259da28d1b5fb0684e3781 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 12 Apr 2023 07:25:06 -0700 Subject: [PATCH 12/17] Closes #12207: Establish a permission for creating API tokens on behalf of other users (#12192) * 11091 add permission to allow user to create api tokens for other users * 11091 update docs * 11091 fix for test * 11091 fix for test * 11091 test case for invalid token creation * 11091 add test for permission grant * Cleanup & fix serializer validation --------- Co-authored-by: jeremystretch --- docs/integrations/rest-api.md | 11 ++++++++--- docs/media/admin_ui_grant_permission.png | Bin 0 -> 33454 bytes netbox/users/api/serializers.py | 11 +++++++++++ netbox/users/tests/test_api.py | 20 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 docs/media/admin_ui_grant_permission.png diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 6f54a8cb0..28d9b55b6 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -584,11 +584,16 @@ Additionally, a token can be set to expire at a specific time. This can be usefu #### Client IP Restriction -!!! note - This feature was introduced in NetBox v3.3. - Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) +#### Creating Tokens for Other Users + +It is possible to provision authentication tokens for other users via the REST API. To do, so the requesting user must have the `users.grant_token` permission assigned. While all users have inherent permission to create their own tokens, this permission is required to enable the creation of tokens for other users. + +![Adding the grant action to a permission](../media/admin_ui_grant_permission.png) + +!!! warning "Exercise Caution" + The ability to create tokens on behalf of other users enables the requestor to access the created token. This ability is intended e.g. for the provisioning of tokens by automated services, and should be used with extreme caution to avoid a security compromise. ### Authenticating to the API diff --git a/docs/media/admin_ui_grant_permission.png b/docs/media/admin_ui_grant_permission.png new file mode 100644 index 0000000000000000000000000000000000000000..2b82dcca200f77e34570e002a8e42ffd2d485bce GIT binary patch literal 33454 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 Date: Wed, 12 Apr 2023 07:25:54 -0700 Subject: [PATCH 13/17] 11432 device field (#11567) * 11432 make device field on interface read-only on api edit call * 11432 make device field on interface read-only on api edit call * 11432 extend serializer change to mixin * 11432 add readonlydevicemixin to template serializers * 11432 change subclass ordering * 11432 fix device_type for template serializers * 11432 DRY * 11432 DRY * 11432 make internal var * 11432 change to model-level validation * 11432 fix fk accessor * Clean up validation error messages --------- Co-authored-by: jeremystretch --- netbox/dcim/models/device_component_templates.py | 11 +++++++++++ netbox/dcim/models/device_components.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 3b136987d..6ff58b0f0 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -120,6 +120,12 @@ class ModularComponentTemplateModel(ComponentTemplateModel): ), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Cache the original DeviceType ID for reference under clean() + self._original_device_type = self.device_type_id + def to_objectchange(self, action): objectchange = super().to_objectchange(action) if self.device_type is not None: @@ -131,6 +137,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel): def clean(self): super().clean() + if self.pk is not None and self._original_device_type != self.device_type_id: + raise ValidationError({ + "device_type": "Component templates cannot be moved to a different device type." + }) + # A component template must belong to a DeviceType *or* to a ModuleType if self.device_type and self.module_type: raise ValidationError( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index b879b77d3..c30ce3a97 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -78,6 +78,12 @@ class ComponentModel(NetBoxModel): ), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Cache the original Device ID for reference under clean() + self._original_device = self.device_id + def __str__(self): if self.label: return f"{self.name} ({self.label})" @@ -88,6 +94,14 @@ class ComponentModel(NetBoxModel): objectchange.related_object = self.device return objectchange + def clean(self): + super().clean() + + if self.pk is not None and self._original_device != self.device_id: + raise ValidationError({ + "device": "Components cannot be moved to a different device." + }) + @property def parent_object(self): return self.device From bb9a125934cebb97dcb793c24d522ca5198c5465 Mon Sep 17 00:00:00 2001 From: Austin de Coup-Crank Date: Wed, 12 Apr 2023 11:16:17 -0500 Subject: [PATCH 14/17] Closes #12040: fix bulk import tab selection --- netbox/templates/generic/bulk_import.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/generic/bulk_import.html b/netbox/templates/generic/bulk_import.html index 4ddfb884c..92223b04f 100644 --- a/netbox/templates/generic/bulk_import.html +++ b/netbox/templates/generic/bulk_import.html @@ -15,12 +15,12 @@ Context: {% block tabs %}