From f489ffa043c8859cce39df69a8397b11825a720b Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 7 Sep 2022 22:33:24 +0200 Subject: [PATCH 01/13] Allow running scripts nested in modules/packages --- netbox/extras/api/views.py | 2 +- netbox/extras/scripts.py | 8 +++++++- netbox/extras/urls.py | 4 ++-- netbox/templates/extras/script_list.html | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 82c68c86d..c7c6cc2aa 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -257,7 +257,7 @@ class ScriptViewSet(ViewSet): lookup_value_regex = '[^/]+' # Allow dots def _get_script(self, pk): - module_name, script_name = pk.split('.') + module_name, script_name = pk.split('.', maxsplit=1) script = get_script(module_name, script_name) if script is None: raise Http404 diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 6e4478304..23a778789 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -299,6 +299,10 @@ class BaseScript: def module(cls): return cls.__module__ + @classmethod + def root_module(cls): + return cls.__module__.split(".")[0] + @classproperty def job_timeout(self): return getattr(self.Meta, 'job_timeout', None) @@ -514,7 +518,9 @@ def get_scripts(use_names=False): ordered_scripts = [cls for cls in script_order if is_script(cls)] unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order] for cls in [*ordered_scripts, *unordered_scripts]: - module_scripts[cls.__name__] = cls + # For scripts in submodules use the full import path w/o the root module as the name + script_name = cls.full_name.split(".", maxsplit=1)[1] + module_scripts[script_name] = cls if module_scripts: scripts[module_name] = module_scripts diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 4c23adb0f..6c6156f4a 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import path, re_path from extras import models, views from netbox.views.generic import ObjectChangeLogView @@ -105,7 +105,7 @@ urlpatterns = [ # Scripts path('scripts/', views.ScriptListView.as_view(), name='script_list'), - path('scripts/./', views.ScriptView.as_view(), name='script'), path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), + re_path(r'^scripts/(?P.([^.]+)).(?P.(.+))/', views.ScriptView.as_view(), name='script'), ] diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index 8884ff77c..1f34f4d5e 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -34,7 +34,7 @@ {% for class_name, script in module_scripts.items %} - {{ script.name }} + {{ script.name }} {% include 'extras/inc/job_label.html' with result=script.result %} From ace66eab61f53294a78a8ede48cf73d17518e8f0 Mon Sep 17 00:00:00 2001 From: Zachary Clark Date: Mon, 12 Sep 2022 00:21:20 -0400 Subject: [PATCH 02/13] Fixes #10305: Allows null master in VirtualChassis APIs --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/tests/test_api.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 79f5339ad..897ee4ca3 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1076,7 +1076,7 @@ class CablePathSerializer(serializers.ModelSerializer): class VirtualChassisSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') - master = NestedDeviceSerializer(required=False) + master = NestedDeviceSerializer(required=False, allow_null=True, default=None) member_count = serializers.IntegerField(read_only=True) class Meta: diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index acd52178d..2697c29b2 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2057,6 +2057,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase): cls.bulk_update_data = { 'domain': 'newdomain', + 'master': None } From 356ff457be08d5527920c617eb598f24a6edbc3d Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 14 Sep 2022 19:57:37 +0200 Subject: [PATCH 03/13] Allow reports to be nested in submodules --- netbox/extras/api/views.py | 6 ++--- .../extras/management/commands/runreport.py | 4 +-- netbox/extras/reports.py | 27 ++++++++++++------- netbox/extras/urls.py | 2 +- netbox/extras/views.py | 7 ++--- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index c7c6cc2aa..63003bdf2 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -159,7 +159,7 @@ class ReportViewSet(ViewSet): # Read the PK as "." if '.' not in pk: raise Http404 - module_name, report_name = pk.split('.', 1) + module_name, report_name = pk.split('.', maxsplit=1) # Raise a 404 on an invalid Report module/name report = get_report(module_name, report_name) @@ -183,8 +183,8 @@ class ReportViewSet(ViewSet): } # Iterate through all available Reports. - for module_name, reports in get_reports(): - for report in reports: + for module_name, reports in get_reports().items(): + for report in reports.values(): # Attach the relevant JobResult (if any) to each Report. report.result = results.get(report.full_name, None) diff --git a/netbox/extras/management/commands/runreport.py b/netbox/extras/management/commands/runreport.py index ee166ae6a..38d435613 100644 --- a/netbox/extras/management/commands/runreport.py +++ b/netbox/extras/management/commands/runreport.py @@ -21,8 +21,8 @@ class Command(BaseCommand): reports = get_reports() # Run reports - for module_name, report_list in reports: - for report in report_list: + for module_name, report_list in reports.items(): + for report in report_list.values(): if module_name in options['reports'] or report.full_name in options['reports']: # Run the report and create a new JobResult diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 43d916aff..702ea0338 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -26,20 +26,18 @@ def get_report(module_name, report_name): """ Return a specific report from within a module. """ - file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name) + reports = get_reports() + module = reports.get(module_name) - spec = importlib.util.spec_from_file_location(module_name, file_path) - module = importlib.util.module_from_spec(spec) - try: - spec.loader.exec_module(module) - except FileNotFoundError: + if module is None: return None - report = getattr(module, report_name, None) + report = module.get(report_name) + if report is None: return None - return report() + return report def get_reports(): @@ -52,7 +50,7 @@ def get_reports(): ... ] """ - module_list = [] + module_list = {} # Iterate through all modules within the reports path. These are the user-created files in which reports are # defined. @@ -61,7 +59,16 @@ def get_reports(): report_order = getattr(module, "report_order", ()) ordered_reports = [cls() for cls in report_order if is_report(cls)] unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order] - module_list.append((module_name, [*ordered_reports, *unordered_reports])) + + module_reports = {} + + for cls in [*ordered_reports, *unordered_reports]: + # For reports in submodules use the full import path w/o the root module as the name + report_name = cls.full_name.split(".", maxsplit=1)[1] + module_reports[report_name] = cls + + if module_reports: + module_list[module_name] = module_reports return module_list diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 6c6156f4a..ced3bd4b9 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -100,8 +100,8 @@ urlpatterns = [ # Reports path('reports/', views.ReportListView.as_view(), name='report_list'), - path('reports/./', views.ReportView.as_view(), name='report'), path('reports/results//', views.ReportResultView.as_view(), name='report_result'), + re_path(r'^reports/(?P.([^.]+)).(?P.(.+))/', views.ReportView.as_view(), name='report'), # Scripts path('scripts/', views.ScriptListView.as_view(), name='script_list'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 30f48f817..d8a015bb0 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -534,9 +534,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View): } ret = [] - for module, report_list in reports: + + for module, report_list in reports.items(): module_reports = [] - for report in report_list: + for report in report_list.values(): report.result = results.get(report.full_name, None) module_reports.append(report) ret.append((module, module_reports)) @@ -613,7 +614,7 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View): result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type) # Retrieve the Report and attach the JobResult to it - module, report_name = result.name.split('.') + module, report_name = result.name.split('.', maxsplit=1) report = get_report(module, report_name) report.result = result From c335b76ec69515ea2055a93a5b8cd0f735139dd6 Mon Sep 17 00:00:00 2001 From: kkthxbye-code Date: Wed, 14 Sep 2022 20:00:12 +0200 Subject: [PATCH 04/13] PEP8: Fix whitespace on blank line --- netbox/extras/reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 702ea0338..32e4efc2d 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -59,14 +59,14 @@ def get_reports(): report_order = getattr(module, "report_order", ()) ordered_reports = [cls() for cls in report_order if is_report(cls)] unordered_reports = [cls() for _, cls in inspect.getmembers(module, is_report) if cls not in report_order] - + module_reports = {} for cls in [*ordered_reports, *unordered_reports]: # For reports in submodules use the full import path w/o the root module as the name report_name = cls.full_name.split(".", maxsplit=1)[1] module_reports[report_name] = cls - + if module_reports: module_list[module_name] = module_reports From 4d97043e268b29ef7ddfea848b8e768949896a9f Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 14 Sep 2022 15:50:45 -0700 Subject: [PATCH 05/13] #10359 add region column to site table --- netbox/dcim/tables/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index c42731b90..a7cdf4b9f 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -203,7 +203,7 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable): model = Device fields = ( 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', - 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', + 'platform', 'serial', 'asset_tag', 'site', 'region', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) From 4e03419e85d30a3f9b4d3503aec57955208a584a Mon Sep 17 00:00:00 2001 From: kvedder Date: Wed, 14 Sep 2022 22:15:12 -0400 Subject: [PATCH 06/13] add custom fields to l2vpntermination edit template --- netbox/templates/ipam/l2vpntermination_edit.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/templates/ipam/l2vpntermination_edit.html b/netbox/templates/ipam/l2vpntermination_edit.html index c66b8a3d1..4379a0899 100644 --- a/netbox/templates/ipam/l2vpntermination_edit.html +++ b/netbox/templates/ipam/l2vpntermination_edit.html @@ -46,4 +46,12 @@ + {% if form.custom_fields %} +
+
+
Custom Fields
+
+ {% render_custom_fields form %} +
+{% endif %} {% endblock %} From c4b7ab067a914349abd88398dd9bfef9f6c2f806 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 15 Sep 2022 10:10:32 -0400 Subject: [PATCH 07/13] Fixes #10247: Allow changing selected device/VM when creating a new component (#10312) * Initial work on #10247 * Continued work on #10247 * Clean up component creation tests * Move valdiation of replicated field to form * Clean up ordering of fields in component creation forms * Omit fieldset header if none * Clean up ordering of fields in component template creation forms * View tests should not move component templates to new device type * Define replication_fields on VMInterfaceCreateForm * Clean up expandable field help texts * Update comments * Update component bulk update forms & views to support new replication fields * Fix ModularDeviceComponentForm parent class * Fix bulk creation of VM interfaces (thanks @kkthxbye-code!) --- netbox/dcim/forms/bulk_create.py | 25 +- netbox/dcim/forms/models.py | 247 ++++++++++------- netbox/dcim/forms/object_create.py | 260 ++++++++++++------ netbox/dcim/models/device_components.py | 24 +- netbox/dcim/tables/template_code.py | 2 +- netbox/dcim/tests/test_forms.py | 18 +- netbox/dcim/tests/test_views.py | 201 +++++++------- netbox/dcim/views.py | 97 +------ netbox/netbox/views/generic/bulk_views.py | 17 +- netbox/netbox/views/generic/object_views.py | 32 +-- .../dcim/component_template_create.html | 38 --- .../templates/dcim/device_component_edit.html | 16 -- .../dcim/frontporttemplate_create.html | 7 - .../templates/dcim/inventoryitem_create.html | 17 -- .../dcim/inventoryitemtemplate_create.html | 17 -- .../dcim/modulebaytemplate_create.html | 7 - netbox/templates/generic/object_edit.html | 8 +- .../virtualization/vminterface_edit.html | 69 ----- netbox/utilities/forms/fields/expandable.py | 2 +- netbox/utilities/testing/views.py | 5 +- netbox/virtualization/forms/bulk_create.py | 4 +- netbox/virtualization/forms/models.py | 12 +- netbox/virtualization/forms/object_create.py | 19 +- netbox/virtualization/tests/test_views.py | 7 +- netbox/virtualization/views.py | 2 - 25 files changed, 523 insertions(+), 630 deletions(-) delete mode 100644 netbox/templates/dcim/component_template_create.html delete mode 100644 netbox/templates/dcim/device_component_edit.html delete mode 100644 netbox/templates/dcim/frontporttemplate_create.html delete mode 100644 netbox/templates/dcim/inventoryitem_create.html delete mode 100644 netbox/templates/dcim/inventoryitemtemplate_create.html delete mode 100644 netbox/templates/dcim/modulebaytemplate_create.html delete mode 100644 netbox/templates/virtualization/vminterface_edit.html diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 43b852928..f6bc27079 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -3,7 +3,7 @@ from django import forms from dcim.models import * from extras.forms import CustomFieldsMixin from extras.models import Tag -from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model +from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model from .object_create import ComponentCreateForm __all__ = ( @@ -24,7 +24,7 @@ __all__ = ( # Device components # -class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm): +class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() @@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm): queryset=Tag.objects.all(), required=False ) + replication_fields = ('name', 'label') class ConsolePortBulkCreateForm( @@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm( DeviceBulkAddComponentForm ): model = ConsolePort - field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags') + field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags') class ConsoleServerPortBulkCreateForm( @@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm( DeviceBulkAddComponentForm ): model = ConsoleServerPort - field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags') + field_order = ('name', 'label', 'type', 'speed', 'description', 'tags') class PowerPortBulkCreateForm( @@ -60,7 +61,7 @@ class PowerPortBulkCreateForm( DeviceBulkAddComponentForm ): model = PowerPort - field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags') + field_order = ('name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags') class PowerOutletBulkCreateForm( @@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm( DeviceBulkAddComponentForm ): model = PowerOutlet - field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags') + field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags') class InterfaceBulkCreateForm( @@ -79,7 +80,7 @@ class InterfaceBulkCreateForm( ): model = Interface field_order = ( - 'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode', + 'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mark_connected', 'description', 'tags', ) @@ -96,13 +97,13 @@ class RearPortBulkCreateForm( DeviceBulkAddComponentForm ): model = RearPort - field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags') + field_order = ('name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags') class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm): model = ModuleBay - field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags') - + field_order = ('name', 'label', 'position_pattern', 'description', 'tags') + replication_fields = ('name', 'label', 'position') position_pattern = ExpandableNameField( label='Position', required=False, @@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm): class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm): model = DeviceBay - field_order = ('name_pattern', 'label_pattern', 'description', 'tags') + field_order = ('name', 'label', 'description', 'tags') class InventoryItemBulkCreateForm( @@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm( ): model = InventoryItem field_order = ( - 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', + 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'tags', ) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index a21265db4..4fa27ae69 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -986,47 +986,74 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): # Device component templates # +class ComponentTemplateForm(BootstrapMixin, forms.ModelForm): + device_type = DynamicModelChoiceField( + queryset=DeviceType.objects.all() + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Disable reassignment of DeviceType when editing an existing instance + if self.instance.pk: + self.fields['device_type'].disabled = True + + +class ModularComponentTemplateForm(ComponentTemplateForm): + module_type = DynamicModelChoiceField( + queryset=ModuleType.objects.all(), + required=False + ) + + +class ConsolePortTemplateForm(ModularComponentTemplateForm): + fieldsets = ( + (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')), + ) -class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'description', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect, } -class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): +class ConsoleServerPortTemplateForm(ModularComponentTemplateForm): + fieldsets = ( + (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')), + ) + class Meta: model = ConsoleServerPortTemplate fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'description', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect, } -class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): +class PowerPortTemplateForm(ModularComponentTemplateForm): + fieldsets = ( + (None, ( + 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', + )), + ) + class Meta: model = PowerPortTemplate fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect(), } -class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): +class PowerOutletTemplateForm(ModularComponentTemplateForm): power_port = DynamicModelChoiceField( queryset=PowerPortTemplate.objects.all(), required=False, @@ -1035,35 +1062,40 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): } ) + fieldsets = ( + (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')), + ) + class Meta: model = PowerOutletTemplate fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect(), 'feed_leg': StaticSelect(), } -class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm): +class InterfaceTemplateForm(ModularComponentTemplateForm): + fieldsets = ( + (None, ('device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description')), + ('PoE', ('poe_mode', 'poe_type')) + ) + class Meta: model = InterfaceTemplate fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect(), 'poe_mode': StaticSelect(), 'poe_type': StaticSelect(), } -class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): +class FrontPortTemplateForm(ModularComponentTemplateForm): rear_port = DynamicModelChoiceField( queryset=RearPortTemplate.objects.all(), required=False, @@ -1073,6 +1105,13 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): } ) + fieldsets = ( + (None, ( + 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', + 'description', + )), + ) + class Meta: model = FrontPortTemplate fields = [ @@ -1080,48 +1119,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): 'description', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect(), } -class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): +class RearPortTemplateForm(ModularComponentTemplateForm): + fieldsets = ( + (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')), + ) + class Meta: model = RearPortTemplate fields = [ 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description', ] widgets = { - 'device_type': forms.HiddenInput(), - 'module_type': forms.HiddenInput(), 'type': StaticSelect(), } -class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm): +class ModuleBayTemplateForm(ComponentTemplateForm): + fieldsets = ( + (None, ('device_type', 'name', 'label', 'position', 'description')), + ) + class Meta: model = ModuleBayTemplate fields = [ 'device_type', 'name', 'label', 'position', 'description', ] - widgets = { - 'device_type': forms.HiddenInput(), - } -class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm): +class DeviceBayTemplateForm(ComponentTemplateForm): + fieldsets = ( + (None, ('device_type', 'name', 'label', 'description')), + ) + class Meta: model = DeviceBayTemplate fields = [ 'device_type', 'name', 'label', 'description', ] - widgets = { - 'device_type': forms.HiddenInput(), - } -class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm): +class InventoryItemTemplateForm(ComponentTemplateForm): parent = DynamicModelChoiceField( queryset=InventoryItemTemplate.objects.all(), required=False, @@ -1148,22 +1189,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm): widget=forms.HiddenInput ) + fieldsets = ( + (None, ( + 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', + 'component_type', 'component_id', + )), + ) + class Meta: model = InventoryItemTemplate fields = [ 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 'component_type', 'component_id', ] - widgets = { - 'device_type': forms.HiddenInput(), - } # # Device components # -class ConsolePortForm(NetBoxModelForm): +class DeviceComponentForm(NetBoxModelForm): + device = DynamicModelChoiceField( + queryset=Device.objects.all() + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Disable reassignment of Device when editing an existing instance + if self.instance.pk: + self.fields['device'].disabled = True + + +class ModularDeviceComponentForm(DeviceComponentForm): module = DynamicModelChoiceField( queryset=Module.objects.all(), required=False, @@ -1172,25 +1230,31 @@ class ConsolePortForm(NetBoxModelForm): } ) + +class ConsolePortForm(ModularDeviceComponentForm): + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', + )), + ) + class Meta: model = ConsolePort fields = [ 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), 'speed': StaticSelect(), } -class ConsoleServerPortForm(NetBoxModelForm): - module = DynamicModelChoiceField( - queryset=Module.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } +class ConsoleServerPortForm(ModularDeviceComponentForm): + + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', + )), ) class Meta: @@ -1199,42 +1263,32 @@ class ConsoleServerPortForm(NetBoxModelForm): 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), 'speed': StaticSelect(), } -class PowerPortForm(NetBoxModelForm): - module = DynamicModelChoiceField( - queryset=Module.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } +class PowerPortForm(ModularDeviceComponentForm): + + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', + 'description', 'tags', + )), ) class Meta: model = PowerPort fields = [ 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', - 'description', - 'tags', + 'description', 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), } -class PowerOutletForm(NetBoxModelForm): - module = DynamicModelChoiceField( - queryset=Module.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } - ) +class PowerOutletForm(ModularDeviceComponentForm): power_port = DynamicModelChoiceField( queryset=PowerPort.objects.all(), required=False, @@ -1243,6 +1297,13 @@ class PowerOutletForm(NetBoxModelForm): } ) + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', + 'tags', + )), + ) + class Meta: model = PowerOutlet fields = [ @@ -1250,20 +1311,12 @@ class PowerOutletForm(NetBoxModelForm): 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), 'feed_leg': StaticSelect(), } -class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): - module = DynamicModelChoiceField( - queryset=Module.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } - ) +class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): parent = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, @@ -1338,7 +1391,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): ) fieldsets = ( - ('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')), + ('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')), ('Addressing', ('vrf', 'mac_address', 'wwn')), ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), ('Related Interfaces', ('parent', 'bridge', 'lag')), @@ -1358,7 +1411,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), 'speed': SelectSpeedWidget(), 'poe_mode': StaticSelect(), @@ -1388,14 +1440,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk) -class FrontPortForm(NetBoxModelForm): - module = DynamicModelChoiceField( - queryset=Module.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } - ) +class FrontPortForm(ModularDeviceComponentForm): rear_port = DynamicModelChoiceField( queryset=RearPort.objects.all(), query_params={ @@ -1403,6 +1448,13 @@ class FrontPortForm(NetBoxModelForm): } ) + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', + 'description', 'tags', + )), + ) + class Meta: model = FrontPort fields = [ @@ -1410,18 +1462,15 @@ class FrontPortForm(NetBoxModelForm): 'description', 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), } -class RearPortForm(NetBoxModelForm): - module = DynamicModelChoiceField( - queryset=Module.objects.all(), - required=False, - query_params={ - 'device_id': '$device', - } +class RearPortForm(ModularDeviceComponentForm): + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', + )), ) class Meta: @@ -1430,33 +1479,32 @@ class RearPortForm(NetBoxModelForm): 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', ] widgets = { - 'device': forms.HiddenInput(), 'type': StaticSelect(), } -class ModuleBayForm(NetBoxModelForm): +class ModuleBayForm(DeviceComponentForm): + fieldsets = ( + (None, ('device', 'name', 'label', 'position', 'description', 'tags',)), + ) class Meta: model = ModuleBay fields = [ 'device', 'name', 'label', 'position', 'description', 'tags', ] - widgets = { - 'device': forms.HiddenInput(), - } -class DeviceBayForm(NetBoxModelForm): +class DeviceBayForm(DeviceComponentForm): + fieldsets = ( + (None, ('device', 'name', 'label', 'description', 'tags',)), + ) class Meta: model = DeviceBay fields = [ 'device', 'name', 'label', 'description', 'tags', ] - widgets = { - 'device': forms.HiddenInput(), - } class PopulateDeviceBayForm(BootstrapMixin, forms.Form): @@ -1479,10 +1527,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): ).exclude(pk=device_bay.device.pk) -class InventoryItemForm(NetBoxModelForm): - device = DynamicModelChoiceField( - queryset=Device.objects.all() - ) +class InventoryItemForm(DeviceComponentForm): parent = DynamicModelChoiceField( queryset=InventoryItem.objects.all(), required=False, diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index d2c941b34..a03597db1 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -2,46 +2,56 @@ from django import forms from dcim.models import * from netbox.forms import NetBoxModelForm -from utilities.forms import ( - BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, -) +from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField +from . import models as model_forms __all__ = ( - 'ComponentTemplateCreateForm', - 'DeviceComponentCreateForm', + 'ComponentCreateForm', + 'ConsolePortCreateForm', + 'ConsolePortTemplateCreateForm', + 'ConsoleServerPortCreateForm', + 'ConsoleServerPortTemplateCreateForm', + 'DeviceBayCreateForm', + 'DeviceBayTemplateCreateForm', 'FrontPortCreateForm', 'FrontPortTemplateCreateForm', + 'InterfaceCreateForm', + 'InterfaceTemplateCreateForm', 'InventoryItemCreateForm', - 'ModularComponentTemplateCreateForm', + 'InventoryItemTemplateCreateForm', 'ModuleBayCreateForm', 'ModuleBayTemplateCreateForm', + 'PowerOutletCreateForm', + 'PowerOutletTemplateCreateForm', + 'PowerPortCreateForm', + 'PowerPortTemplateCreateForm', + 'RearPortCreateForm', + 'RearPortTemplateCreateForm', 'VirtualChassisCreateForm', ) -class ComponentCreateForm(BootstrapMixin, forms.Form): +class ComponentCreateForm(forms.Form): """ - Subclass this form when facilitating the creation of one or more device component or component templates based on + Subclass this form when facilitating the creation of one or more component or component template objects based on a name pattern. """ - name_pattern = ExpandableNameField( - label='Name' - ) - label_pattern = ExpandableNameField( - label='Label', + name = ExpandableNameField() + label = ExpandableNameField( required=False, - help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)' + help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)' ) + # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by + # ComponentCreateView when creating objects. + replication_fields = ('name', 'label') + def clean(self): super().clean() - # Validate that all patterned fields generate an equal number of values - patterned_fields = [ - field_name for field_name in self.fields if field_name.endswith('_pattern') - ] - pattern_count = len(self.cleaned_data['name_pattern']) - for field_name in patterned_fields: + # Validate that all replication fields generate an equal number of values + pattern_count = len(self.cleaned_data[self.replication_fields[0]]) + for field_name in self.replication_fields: value_count = len(self.cleaned_data[field_name]) if self.cleaned_data[field_name] and value_count != pattern_count: raise forms.ValidationError({ @@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form): }, code='label_pattern_mismatch') -class ComponentTemplateCreateForm(ComponentCreateForm): - """ - Creation form for component templates that can be assigned only to a DeviceType. - """ - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all(), - ) - field_order = ('device_type', 'name_pattern', 'label_pattern') +# +# Device component templates +# + +class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm): + + class Meta(model_forms.ConsolePortTemplateForm.Meta): + exclude = ('name', 'label') -class ModularComponentTemplateCreateForm(ComponentCreateForm): - """ - Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType. - """ - name_pattern = ExpandableNameField( - label='Name', - help_text=""" - Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range - are not supported. Example: [ge,xe]-0/0/[0-9]. {module} is accepted as a substitution for - the module bay position. - """ - ) - device_type = DynamicModelChoiceField( - queryset=DeviceType.objects.all(), - required=False - ) - module_type = DynamicModelChoiceField( - queryset=ModuleType.objects.all(), - required=False - ) - field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern') +class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm): + + class Meta(model_forms.ConsoleServerPortTemplateForm.Meta): + exclude = ('name', 'label') -class DeviceComponentCreateForm(ComponentCreateForm): - device = DynamicModelChoiceField( - queryset=Device.objects.all() - ) - field_order = ('device', 'name_pattern', 'label_pattern') +class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm): + + class Meta(model_forms.PowerPortTemplateForm.Meta): + exclude = ('name', 'label') -class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm): - rear_port_set = forms.MultipleChoiceField( +class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm): + + class Meta(model_forms.PowerOutletTemplateForm.Meta): + exclude = ('name', 'label') + + +class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm): + + class Meta(model_forms.InterfaceTemplateForm.Meta): + exclude = ('name', 'label') + + +class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm): + rear_port = forms.MultipleChoiceField( choices=[], label='Rear ports', help_text='Select one rear port assignment for each front port being created.', ) - field_order = ( - 'device_type', 'name_pattern', 'label_pattern', 'rear_port_set', + + # Override fieldsets from FrontPortTemplateForm to omit rear_port_position + fieldsets = ( + (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')), ) + class Meta(model_forms.FrontPortTemplateForm.Meta): + exclude = ('name', 'label', 'rear_port', 'rear_port_position') + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm): choices.append( ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) ) - self.fields['rear_port_set'].choices = choices + self.fields['rear_port'].choices = choices def get_iterative_data(self, iteration): # Assign rear port and position from selected set - rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':') + rear_port, position = self.cleaned_data['rear_port'][iteration].split(':') return { 'rear_port': int(rear_port), @@ -143,16 +152,94 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm): } -class FrontPortCreateForm(DeviceComponentCreateForm): - rear_port_set = forms.MultipleChoiceField( +class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm): + + class Meta(model_forms.RearPortTemplateForm.Meta): + exclude = ('name', 'label') + + +class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm): + + class Meta(model_forms.DeviceBayTemplateForm.Meta): + exclude = ('name', 'label') + + +class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm): + position = ExpandableNameField( + label='Position', + required=False, + help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)' + ) + replication_fields = ('name', 'label', 'position') + + class Meta(model_forms.ModuleBayTemplateForm.Meta): + exclude = ('name', 'label', 'position') + + +class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm): + + class Meta(model_forms.InventoryItemTemplateForm.Meta): + exclude = ('name', 'label') + + +# +# Device components +# + +class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm): + + class Meta(model_forms.ConsolePortForm.Meta): + exclude = ('name', 'label') + + +class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm): + + class Meta(model_forms.ConsoleServerPortForm.Meta): + exclude = ('name', 'label') + + +class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm): + + class Meta(model_forms.PowerPortForm.Meta): + exclude = ('name', 'label') + + +class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm): + + class Meta(model_forms.PowerOutletForm.Meta): + exclude = ('name', 'label') + + +class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm): + + class Meta(model_forms.InterfaceForm.Meta): + exclude = ('name', 'label') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if 'module' in self.fields: + self.fields['name'].help_text += ' The string {module} will be replaced with the position ' \ + 'of the assigned module, if any' + + +class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm): + rear_port = forms.MultipleChoiceField( choices=[], label='Rear ports', help_text='Select one rear port assignment for each front port being created.', ) - field_order = ( - 'device', 'name_pattern', 'label_pattern', 'rear_port_set', + + # Override fieldsets from FrontPortForm to omit rear_port_position + fieldsets = ( + (None, ( + 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags', + )), ) + class Meta(model_forms.FrontPortForm.Meta): + exclude = ('name', 'label', 'rear_port', 'rear_port_position') + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm): choices.append( ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) ) - self.fields['rear_port_set'].choices = choices + self.fields['rear_port'].choices = choices def get_iterative_data(self, iteration): # Assign rear port and position from selected set - rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':') + rear_port, position = self.cleaned_data['rear_port'][iteration].split(':') return { 'rear_port': int(rear_port), @@ -189,28 +276,39 @@ class FrontPortCreateForm(DeviceComponentCreateForm): } -class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm): - position_pattern = ExpandableNameField( +class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm): + + class Meta(model_forms.RearPortForm.Meta): + exclude = ('name', 'label') + + +class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm): + + class Meta(model_forms.DeviceBayForm.Meta): + exclude = ('name', 'label') + + +class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm): + position = ExpandableNameField( label='Position', required=False, - help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)' + help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)' ) - field_order = ('device_type', 'name_pattern', 'label_pattern', 'position_pattern') + replication_fields = ('name', 'label', 'position') + + class Meta(model_forms.ModuleBayForm.Meta): + exclude = ('name', 'label', 'position') -class ModuleBayCreateForm(DeviceComponentCreateForm): - position_pattern = ExpandableNameField( - label='Position', - required=False, - help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)' - ) - field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern') +class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm): + + class Meta(model_forms.InventoryItemForm.Meta): + exclude = ('name', 'label') -class InventoryItemCreateForm(ComponentCreateForm): - # Device is assigned by the model form - field_order = ('name_pattern', 'label_pattern') - +# +# Virtual chassis +# class VirtualChassisCreateForm(NetBoxModelForm): region = DynamicModelChoiceField( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 838336e21..8f1285901 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel): def clean(self): super().clean() - # Validate rear port assignment - if self.rear_port.device != self.device: - raise ValidationError({ - "rear_port": f"Rear port ({self.rear_port}) must belong to the same device" - }) + if hasattr(self, 'rear_port'): - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError({ - "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port " - f"{self.rear_port.name} has only {self.rear_port.positions} positions" - }) + # Validate rear port assignment + if self.rear_port.device != self.device: + raise ValidationError({ + "rear_port": f"Rear port ({self.rear_port}) must belong to the same device" + }) + + # Validate rear port position assignment + if self.rear_port_position > self.rear_port.positions: + raise ValidationError({ + "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port " + f"{self.rear_port.name} has only {self.rear_port.positions} positions" + }) class RearPort(ModularComponentModel, CabledObjectModel): diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index d34003ee5..dfc77b854 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
  • Inventory Item
  • {% endif %} {% if perms.dcim.add_interface %} -
  • Child Interface
  • +
  • Child Interface
  • {% endif %} {% if perms.ipam.add_l2vpntermination %}
  • L2VPN Termination
  • diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 53474314f..1cd75765a 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -1,6 +1,6 @@ from django.test import TestCase -from dcim.choices import DeviceFaceChoices, DeviceStatusChoices +from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices from dcim.forms import * from dcim.models import * from utilities.testing import create_test_device @@ -129,10 +129,11 @@ class LabelTestCase(TestCase): """ interface_data = { 'device': self.device.pk, - 'name_pattern': 'eth[0-9]', - 'label_pattern': 'Interface[0-9]', + 'name': 'eth[0-9]', + 'label': 'Interface[0-9]', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, } - form = DeviceComponentCreateForm(interface_data) + form = InterfaceCreateForm(interface_data) self.assertTrue(form.is_valid()) @@ -142,10 +143,11 @@ class LabelTestCase(TestCase): """ bad_interface_data = { 'device': self.device.pk, - 'name_pattern': 'eth[0-9]', - 'label_pattern': 'Interface[0-1]', + 'name': 'eth[0-9]', + 'label': 'Interface[0-1]', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, } - form = DeviceComponentCreateForm(bad_interface_data) + form = InterfaceCreateForm(bad_interface_data) self.assertFalse(form.is_valid()) - self.assertIn('label_pattern', form.errors) + self.assertIn('label', form.errors) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index a25267166..50b36e36d 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1082,31 +1082,28 @@ front-ports: class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = ConsolePortTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') ConsolePortTemplate.objects.bulk_create(( - ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'), - ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'), - ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'), + ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'), + ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'), + ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Console Port Template X', 'type': ConsolePortTypeChoices.TYPE_RJ45, } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Console Port Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Console Port Template [4-6]', 'type': ConsolePortTypeChoices.TYPE_RJ45, } @@ -1117,31 +1114,28 @@ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = ConsoleServerPortTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') ConsoleServerPortTemplate.objects.bulk_create(( - ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'), - ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'), - ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'), + ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'), + ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 2'), + ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Console Server Port Template X', 'type': ConsolePortTypeChoices.TYPE_RJ45, } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Console Server Port Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Console Server Port Template [4-6]', 'type': ConsolePortTypeChoices.TYPE_RJ45, } @@ -1152,24 +1146,21 @@ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateVie class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = PowerPortTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') PowerPortTemplate.objects.bulk_create(( - PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'), - PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'), - PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'), + PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'), + PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'), + PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Power Port Template X', 'type': PowerPortTypeChoices.TYPE_IEC_C14, 'maximum_draw': 100, @@ -1177,8 +1168,8 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Power Port Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Power Port Template [4-6]', 'type': PowerPortTypeChoices.TYPE_IEC_C14, 'maximum_draw': 100, 'allocated_draw': 50, @@ -1193,6 +1184,7 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = PowerOutletTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -1220,7 +1212,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC cls.bulk_create_data = { 'device_type': devicetype.pk, - 'name_pattern': 'Power Outlet Template [4-6]', + 'name': 'Power Outlet Template [4-6]', 'type': PowerOutletTypeChoices.TYPE_IEC_C13, 'power_port': powerports[0].pk, 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, @@ -1234,34 +1226,31 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = InterfaceTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') InterfaceTemplate.objects.bulk_create(( - InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'), - InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'), - InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'), + InterfaceTemplate(device_type=devicetype, name='Interface Template 1'), + InterfaceTemplate(device_type=devicetype, name='Interface Template 2'), + InterfaceTemplate(device_type=devicetype, name='Interface Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Interface Template X', 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'mgmt_only': True, } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Interface Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Interface Template [4-6]', # Test that a label can be applied to each generated interface templates - 'label_pattern': 'Interface Template Label [3-5]', + 'label': 'Interface Template Label [3-5]', 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'mgmt_only': True, } @@ -1274,6 +1263,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = FrontPortTemplate + validation_excluded_fields = ('name', 'label', 'rear_port') @classmethod def setUpTestData(cls): @@ -1306,11 +1296,9 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas cls.bulk_create_data = { 'device_type': devicetype.pk, - 'name_pattern': 'Front Port [4-6]', + 'name': 'Front Port [4-6]', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port_set': [ - '{}:1'.format(rp.pk) for rp in rearports[3:6] - ], + 'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]], } cls.bulk_edit_data = { @@ -1320,32 +1308,29 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = RearPortTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') RearPortTemplate.objects.bulk_create(( - RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'), - RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'), - RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'), + RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Rear Port Template X', 'type': PortTypeChoices.TYPE_8P8C, 'positions': 2, } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Rear Port Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Rear Port Template [4-6]', 'type': PortTypeChoices.TYPE_8P8C, 'positions': 2, } @@ -1357,30 +1342,27 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = ModuleBayTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') ModuleBayTemplate.objects.bulk_create(( - ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'), - ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'), - ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'), + ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'), + ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'), + ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Module Bay Template X', } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Module Bay Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Module Bay Template [4-6]', } cls.bulk_edit_data = { @@ -1390,30 +1372,27 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = DeviceBayTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') - devicetypes = ( - DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), - DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT) DeviceBayTemplate.objects.bulk_create(( - DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'), - DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'), - DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'), + DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'), + DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'), + DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'), )) cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Device Bay Template X', } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Device Bay Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Device Bay Template [4-6]', } cls.bulk_edit_data = { @@ -1423,6 +1402,7 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): model = InventoryItemTemplate + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -1431,30 +1411,25 @@ class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), ) Manufacturer.objects.bulk_create(manufacturers) - - devicetypes = ( - DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'), - DeviceType(manufacturer=manufacturers[0], model='Device Type 2', slug='device-type-2'), - ) - DeviceType.objects.bulk_create(devicetypes) + devicetype = DeviceType.objects.create(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1') inventory_item_templates = ( - InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 1', manufacturer=manufacturers[0]), - InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 2', manufacturer=manufacturers[0]), - InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 3', manufacturer=manufacturers[0]), + InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturers[0]), + InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturers[0]), + InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturers[0]), ) for item in inventory_item_templates: item.save() cls.form_data = { - 'device_type': devicetypes[1].pk, + 'device_type': devicetype.pk, 'name': 'Inventory Item Template X', 'manufacturer': manufacturers[1].pk, } cls.bulk_create_data = { - 'device_type': devicetypes[1].pk, - 'name_pattern': 'Inventory Item Template [4-6]', + 'device_type': devicetype.pk, + 'name': 'Inventory Item Template [4-6]', 'manufacturer': manufacturers[1].pk, } @@ -1912,6 +1887,7 @@ class ModuleTestCase( class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = ConsolePort + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -1935,9 +1911,9 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Console Port [4-6]', + 'name': 'Console Port [4-6]', # Test that a label can be applied to each generated console ports - 'label_pattern': 'Serial[3-5]', + 'label': 'Serial[3-5]', 'type': ConsolePortTypeChoices.TYPE_RJ45, 'description': 'A console port', 'tags': sorted([t.pk for t in tags]), @@ -1970,6 +1946,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = ConsoleServerPort + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -1993,7 +1970,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Console Server Port [4-6]', + 'name': 'Console Server Port [4-6]', 'type': ConsolePortTypeChoices.TYPE_RJ45, 'description': 'A console server port', 'tags': [t.pk for t in tags], @@ -2026,6 +2003,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = PowerPort + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2051,7 +2029,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Power Port [4-6]]', + 'name': 'Power Port [4-6]]', 'type': PowerPortTypeChoices.TYPE_IEC_C14, 'maximum_draw': 100, 'allocated_draw': 50, @@ -2088,6 +2066,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): model = PowerOutlet + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2119,7 +2098,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Power Outlet [4-6]', + 'name': 'Power Outlet [4-6]', 'type': PowerOutletTypeChoices.TYPE_IEC_C13, 'power_port': powerports[1].pk, 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, @@ -2153,6 +2132,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): model = Interface + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2217,7 +2197,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Interface [4-6]', + 'name': 'Interface [4-6]', 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'enabled': False, 'bridge': interfaces[4].pk, @@ -2277,6 +2257,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = FrontPort + validation_excluded_fields = ('name', 'label', 'rear_port') @classmethod def setUpTestData(cls): @@ -2312,11 +2293,9 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Front Port [4-6]', + 'name': 'Front Port [4-6]', 'type': PortTypeChoices.TYPE_8P8C, - 'rear_port_set': [ - '{}:1'.format(rp.pk) for rp in rearports[3:6] - ], + 'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]], 'description': 'New description', 'tags': [t.pk for t in tags], } @@ -2348,6 +2327,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): model = RearPort + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2372,7 +2352,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Rear Port [4-6]', + 'name': 'Rear Port [4-6]', 'type': PortTypeChoices.TYPE_8P8C, 'positions': 3, 'description': 'A rear port', @@ -2406,6 +2386,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase): model = ModuleBay + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2428,7 +2409,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Module Bay [4-6]', + 'name': 'Module Bay [4-6]', 'description': 'A module bay', 'tags': [t.pk for t in tags], } @@ -2447,6 +2428,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase): class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): model = DeviceBay + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2472,7 +2454,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Device Bay [4-6]', + 'name': 'Device Bay [4-6]', 'description': 'A device bay', 'tags': [t.pk for t in tags], } @@ -2491,6 +2473,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): model = InventoryItem + validation_excluded_fields = ('name', 'label') @classmethod def setUpTestData(cls): @@ -2525,7 +2508,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'device': device.pk, - 'name_pattern': 'Inventory Item [4-6]', + 'name': 'Inventory Item [4-6]', 'role': roles[1].pk, 'manufacturer': manufacturer.pk, 'parent': None, diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6ee74377a..aee0cb384 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1120,9 +1120,8 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView): class ConsolePortTemplateCreateView(generic.ComponentCreateView): queryset = ConsolePortTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.ConsolePortTemplateCreateForm model_form = forms.ConsolePortTemplateForm - template_name = 'dcim/component_template_create.html' class ConsolePortTemplateEditView(generic.ObjectEditView): @@ -1155,9 +1154,8 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView): class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView): queryset = ConsoleServerPortTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.ConsoleServerPortTemplateCreateForm model_form = forms.ConsoleServerPortTemplateForm - template_name = 'dcim/component_template_create.html' class ConsoleServerPortTemplateEditView(generic.ObjectEditView): @@ -1190,9 +1188,8 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView): class PowerPortTemplateCreateView(generic.ComponentCreateView): queryset = PowerPortTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.PowerPortTemplateCreateForm model_form = forms.PowerPortTemplateForm - template_name = 'dcim/component_template_create.html' class PowerPortTemplateEditView(generic.ObjectEditView): @@ -1225,9 +1222,8 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView): class PowerOutletTemplateCreateView(generic.ComponentCreateView): queryset = PowerOutletTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.PowerOutletTemplateCreateForm model_form = forms.PowerOutletTemplateForm - template_name = 'dcim/component_template_create.html' class PowerOutletTemplateEditView(generic.ObjectEditView): @@ -1260,9 +1256,8 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView): class InterfaceTemplateCreateView(generic.ComponentCreateView): queryset = InterfaceTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.InterfaceTemplateCreateForm model_form = forms.InterfaceTemplateForm - template_name = 'dcim/component_template_create.html' class InterfaceTemplateEditView(generic.ObjectEditView): @@ -1297,15 +1292,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView): queryset = FrontPortTemplate.objects.all() form = forms.FrontPortTemplateCreateForm model_form = forms.FrontPortTemplateForm - template_name = 'dcim/frontporttemplate_create.html' - - def initialize_forms(self, request): - form, model_form = super().initialize_forms(request) - - model_form.fields.pop('rear_port') - model_form.fields.pop('rear_port_position') - - return form, model_form class FrontPortTemplateEditView(generic.ObjectEditView): @@ -1338,9 +1324,8 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView): class RearPortTemplateCreateView(generic.ComponentCreateView): queryset = RearPortTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.RearPortTemplateCreateForm model_form = forms.RearPortTemplateForm - template_name = 'dcim/component_template_create.html' class RearPortTemplateEditView(generic.ObjectEditView): @@ -1375,8 +1360,6 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView): queryset = ModuleBayTemplate.objects.all() form = forms.ModuleBayTemplateCreateForm model_form = forms.ModuleBayTemplateForm - template_name = 'dcim/modulebaytemplate_create.html' - patterned_fields = ('name', 'label', 'position') class ModuleBayTemplateEditView(generic.ObjectEditView): @@ -1409,9 +1392,8 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView): class DeviceBayTemplateCreateView(generic.ComponentCreateView): queryset = DeviceBayTemplate.objects.all() - form = forms.ComponentTemplateCreateForm + form = forms.DeviceBayTemplateCreateForm model_form = forms.DeviceBayTemplateForm - template_name = 'dcim/component_template_create.html' class DeviceBayTemplateEditView(generic.ObjectEditView): @@ -1444,9 +1426,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView): class InventoryItemTemplateCreateView(generic.ComponentCreateView): queryset = InventoryItemTemplate.objects.all() - form = forms.ModularComponentTemplateCreateForm + form = forms.InventoryItemTemplateCreateForm model_form = forms.InventoryItemTemplateForm - template_name = 'dcim/inventoryitemtemplate_create.html' def alter_object(self, instance, request): # Set component (if any) @@ -1874,14 +1855,13 @@ class ConsolePortView(generic.ObjectView): class ConsolePortCreateView(generic.ComponentCreateView): queryset = ConsolePort.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.ConsolePortCreateForm model_form = forms.ConsolePortForm class ConsolePortEditView(generic.ObjectEditView): queryset = ConsolePort.objects.all() form = forms.ConsolePortForm - template_name = 'dcim/device_component_edit.html' class ConsolePortDeleteView(generic.ObjectDeleteView): @@ -1933,14 +1913,13 @@ class ConsoleServerPortView(generic.ObjectView): class ConsoleServerPortCreateView(generic.ComponentCreateView): queryset = ConsoleServerPort.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.ConsoleServerPortCreateForm model_form = forms.ConsoleServerPortForm class ConsoleServerPortEditView(generic.ObjectEditView): queryset = ConsoleServerPort.objects.all() form = forms.ConsoleServerPortForm - template_name = 'dcim/device_component_edit.html' class ConsoleServerPortDeleteView(generic.ObjectDeleteView): @@ -1992,14 +1971,13 @@ class PowerPortView(generic.ObjectView): class PowerPortCreateView(generic.ComponentCreateView): queryset = PowerPort.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.PowerPortCreateForm model_form = forms.PowerPortForm class PowerPortEditView(generic.ObjectEditView): queryset = PowerPort.objects.all() form = forms.PowerPortForm - template_name = 'dcim/device_component_edit.html' class PowerPortDeleteView(generic.ObjectDeleteView): @@ -2051,14 +2029,13 @@ class PowerOutletView(generic.ObjectView): class PowerOutletCreateView(generic.ComponentCreateView): queryset = PowerOutlet.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.PowerOutletCreateForm model_form = forms.PowerOutletForm class PowerOutletEditView(generic.ObjectEditView): queryset = PowerOutlet.objects.all() form = forms.PowerOutletForm - template_name = 'dcim/device_component_edit.html' class PowerOutletDeleteView(generic.ObjectDeleteView): @@ -2154,42 +2131,13 @@ class InterfaceView(generic.ObjectView): class InterfaceCreateView(generic.ComponentCreateView): queryset = Interface.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.InterfaceCreateForm model_form = forms.InterfaceForm - # template_name = 'dcim/interface_create.html' - - # TODO: Figure out what to do with this - # def post(self, request): - # """ - # Override inherited post() method to handle request to assign newly created - # interface objects (first object) to an IP Address object. - # """ - # form = self.form(request.POST, initial=request.GET) - # new_objs = self.validate_form(request, form) - # - # if form.is_valid() and not form.errors: - # if '_addanother' in request.POST: - # return redirect(request.get_full_path()) - # elif new_objs is not None and '_assignip' in request.POST and len(new_objs) >= 1 and \ - # request.user.has_perm('ipam.add_ipaddress'): - # first_obj = new_objs[0].pk - # return redirect( - # f'/ipam/ip-addresses/add/?interface={first_obj}&return_url={self.get_return_url(request)}' - # ) - # else: - # return redirect(self.get_return_url(request)) - # - # return render(request, self.template_name, { - # 'obj_type': self.queryset.model._meta.verbose_name, - # 'form': form, - # 'return_url': self.get_return_url(request), - # }) class InterfaceEditView(generic.ObjectEditView): queryset = Interface.objects.all() form = forms.InterfaceForm - template_name = 'dcim/interface_edit.html' class InterfaceDeleteView(generic.ObjectDeleteView): @@ -2244,19 +2192,10 @@ class FrontPortCreateView(generic.ComponentCreateView): form = forms.FrontPortCreateForm model_form = forms.FrontPortForm - def initialize_forms(self, request): - form, model_form = super().initialize_forms(request) - - model_form.fields.pop('rear_port') - model_form.fields.pop('rear_port_position') - - return form, model_form - class FrontPortEditView(generic.ObjectEditView): queryset = FrontPort.objects.all() form = forms.FrontPortForm - template_name = 'dcim/device_component_edit.html' class FrontPortDeleteView(generic.ObjectDeleteView): @@ -2308,14 +2247,13 @@ class RearPortView(generic.ObjectView): class RearPortCreateView(generic.ComponentCreateView): queryset = RearPort.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.RearPortCreateForm model_form = forms.RearPortForm class RearPortEditView(generic.ObjectEditView): queryset = RearPort.objects.all() form = forms.RearPortForm - template_name = 'dcim/device_component_edit.html' class RearPortDeleteView(generic.ObjectDeleteView): @@ -2369,13 +2307,11 @@ class ModuleBayCreateView(generic.ComponentCreateView): queryset = ModuleBay.objects.all() form = forms.ModuleBayCreateForm model_form = forms.ModuleBayForm - patterned_fields = ('name', 'label', 'position') class ModuleBayEditView(generic.ObjectEditView): queryset = ModuleBay.objects.all() form = forms.ModuleBayForm - template_name = 'dcim/device_component_edit.html' class ModuleBayDeleteView(generic.ObjectDeleteView): @@ -2423,14 +2359,13 @@ class DeviceBayView(generic.ObjectView): class DeviceBayCreateView(generic.ComponentCreateView): queryset = DeviceBay.objects.all() - form = forms.DeviceComponentCreateForm + form = forms.DeviceBayCreateForm model_form = forms.DeviceBayForm class DeviceBayEditView(generic.ObjectEditView): queryset = DeviceBay.objects.all() form = forms.DeviceBayForm - template_name = 'dcim/device_component_edit.html' class DeviceBayDeleteView(generic.ObjectDeleteView): @@ -2552,7 +2487,6 @@ class InventoryItemCreateView(generic.ComponentCreateView): queryset = InventoryItem.objects.all() form = forms.InventoryItemCreateForm model_form = forms.InventoryItemForm - template_name = 'dcim/inventoryitem_create.html' def alter_object(self, instance, request): # Set component (if any) @@ -2736,7 +2670,6 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView): filterset = filtersets.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' - patterned_fields = ('name', 'label', 'position') class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 7340ea2a0..f0741af2c 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -774,7 +774,6 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): model_form = None filterset = None table = None - patterned_fields = ('name', 'label') def get_required_permission(self): return f'dcim.add_{self.queryset.model._meta.model_name}' @@ -804,23 +803,25 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): new_components = [] data = deepcopy(form.cleaned_data) + replication_data = { + field: data.pop(field) for field in form.replication_fields + } try: with transaction.atomic(): for obj in data['pk']: - pattern_count = len(data[f'{self.patterned_fields[0]}_pattern']) + pattern_count = len(replication_data[form.replication_fields[0]]) for i in range(pattern_count): component_data = { self.parent_field: obj.pk } - - for field_name in self.patterned_fields: - if data.get(f'{field_name}_pattern'): - component_data[field_name] = data[f'{field_name}_pattern'][i] - component_data.update(data) + for field, values in replication_data.items(): + if values: + component_data[field] = values[i] + component_form = self.model_form(component_data) if component_form.is_valid(): instance = component_form.save() @@ -829,7 +830,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView): else: for field, errors in component_form.errors.as_data().items(): for e in errors: - form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e))) + form.add_error(field, '{}: {}'.format(obj, ', '.join(e))) # Enforce object-level permissions if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components): diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 7617e0402..a56a832b6 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -538,10 +538,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): """ Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine. """ - template_name = 'dcim/component_create.html' + template_name = 'generic/object_edit.html' form = None model_form = None - patterned_fields = ('name', 'label') def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'add') @@ -549,44 +548,38 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): def alter_object(self, instance, request): return instance - def initialize_forms(self, request): + def initialize_form(self, request): data = request.POST if request.method == 'POST' else None initial_data = normalize_querydict(request.GET) - form = self.form(data=data, initial=request.GET) - model_form = self.model_form(data=data, initial=initial_data) + form = self.form(data=data, initial=initial_data) - # These fields will be set from the pattern values - for field_name in self.patterned_fields: - model_form.fields[field_name].widget = HiddenInput() - - return form, model_form + return form def get(self, request): - form, model_form = self.initialize_forms(request) + form = self.initialize_form(request) instance = self.alter_object(self.queryset.model(), request) return render(request, self.template_name, { 'object': instance, - 'replication_form': form, - 'form': model_form, + 'form': form, 'return_url': self.get_return_url(request), }) def post(self, request): logger = logging.getLogger('netbox.views.ComponentCreateView') - form, model_form = self.initialize_forms(request) + form = self.initialize_form(request) instance = self.alter_object(self.queryset.model(), request) if form.is_valid(): new_components = [] data = deepcopy(request.POST) - pattern_count = len(form.cleaned_data[f'{self.patterned_fields[0]}_pattern']) + pattern_count = len(form.cleaned_data[self.form.replication_fields[0]]) for i in range(pattern_count): - for field_name in self.patterned_fields: - if form.cleaned_data.get(f'{field_name}_pattern'): - data[field_name] = form.cleaned_data[f'{field_name}_pattern'][i] + for field_name in self.form.replication_fields: + if form.cleaned_data.get(field_name): + data[field_name] = form.cleaned_data[field_name][i] if hasattr(form, 'get_iterative_data'): data.update(form.get_iterative_data(i)) @@ -626,7 +619,6 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): return render(request, self.template_name, { 'object': instance, - 'replication_form': form, - 'form': model_form, + 'form': form, 'return_url': self.get_return_url(request), }) diff --git a/netbox/templates/dcim/component_template_create.html b/netbox/templates/dcim/component_template_create.html deleted file mode 100644 index d164db872..000000000 --- a/netbox/templates/dcim/component_template_create.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} - -{% block form %} - {% if form.module_type %} -
    -
    - -
    -
    -
    -
    - {% render_field replication_form.device_type %} -
    -
    - {% render_field replication_form.module_type %} -
    -
    - {% else %} - {% render_field replication_form.device_type %} - {% endif %} - {% block replication_fields %} - {% render_field replication_form.name_pattern %} - {% render_field replication_form.label_pattern %} - {% endblock replication_fields %} - {{ block.super }} -{% endblock form %} diff --git a/netbox/templates/dcim/device_component_edit.html b/netbox/templates/dcim/device_component_edit.html deleted file mode 100644 index 44b93d870..000000000 --- a/netbox/templates/dcim/device_component_edit.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} - -{% block form %} -
    - {% if form.instance.device %} -
    - -
    - -
    -
    - {% endif %} - {% render_form form %} -
    -{% endblock form %} diff --git a/netbox/templates/dcim/frontporttemplate_create.html b/netbox/templates/dcim/frontporttemplate_create.html deleted file mode 100644 index 50e9d355c..000000000 --- a/netbox/templates/dcim/frontporttemplate_create.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'dcim/component_template_create.html' %} -{% load form_helpers %} - -{% block replication_fields %} - {{ block.super }} - {% render_field replication_form.rear_port_set %} -{% endblock replication_fields %} diff --git a/netbox/templates/dcim/inventoryitem_create.html b/netbox/templates/dcim/inventoryitem_create.html deleted file mode 100644 index be910f143..000000000 --- a/netbox/templates/dcim/inventoryitem_create.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'dcim/component_create.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block replication_fields %} - {{ block.super }} - {% if object.component %} -
    - -
    - -
    -
    - {% endif %} -{% endblock replication_fields %} diff --git a/netbox/templates/dcim/inventoryitemtemplate_create.html b/netbox/templates/dcim/inventoryitemtemplate_create.html deleted file mode 100644 index 9180cf6ab..000000000 --- a/netbox/templates/dcim/inventoryitemtemplate_create.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'dcim/component_template_create.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block replication_fields %} - {{ block.super }} - {% if object.component %} -
    - -
    - -
    -
    - {% endif %} -{% endblock replication_fields %} diff --git a/netbox/templates/dcim/modulebaytemplate_create.html b/netbox/templates/dcim/modulebaytemplate_create.html deleted file mode 100644 index 74323ac4b..000000000 --- a/netbox/templates/dcim/modulebaytemplate_create.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'dcim/component_template_create.html' %} -{% load form_helpers %} - -{% block replication_fields %} - {{ block.super }} - {% render_field replication_form.position_pattern %} -{% endblock replication_fields %} diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 4ce270b30..56e4f5a32 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -59,9 +59,11 @@ Context: {# Render grouped fields according to Form #} {% for group, fields in form.fieldsets %}
    -
    -
    {{ group }}
    -
    + {% if group %} +
    +
    {{ group }}
    +
    + {% endif %} {% for name in fields %} {% with field=form|getfield:name %} {% if not field.field.widget.is_hidden %} diff --git a/netbox/templates/virtualization/vminterface_edit.html b/netbox/templates/virtualization/vminterface_edit.html deleted file mode 100644 index efb138954..000000000 --- a/netbox/templates/virtualization/vminterface_edit.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} - -{% block form %} - {# Render hidden fields #} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} - -
    -
    -
    Interface
    -
    - {% if form.instance.virtual_machine %} -
    - -
    - -
    -
    - {% endif %} - {% render_field form.name %} - {% render_field form.description %} - {% render_field form.tags %} -
    - -
    -
    -
    Addressing
    -
    - {% render_field form.vrf %} - {% render_field form.mac_address %} -
    - -
    -
    -
    Operation
    -
    - {% render_field form.mtu %} - {% render_field form.enabled %} -
    - -
    -
    -
    Related Interfaces
    -
    - {% render_field form.parent %} - {% render_field form.bridge %} -
    - -
    -
    -
    802.1Q Switching
    -
    - {% render_field form.mode %} - {% render_field form.vlan_group %} - {% render_field form.untagged_vlan %} - {% render_field form.tagged_vlans %} -
    - - {% if form.custom_fields %} -
    -
    -
    Custom Fields
    -
    - {% render_custom_fields form %} -
    - {% endif %} -{% endblock %} diff --git a/netbox/utilities/forms/fields/expandable.py b/netbox/utilities/forms/fields/expandable.py index 214775f03..fca370c26 100644 --- a/netbox/utilities/forms/fields/expandable.py +++ b/netbox/utilities/forms/fields/expandable.py @@ -22,7 +22,7 @@ class ExpandableNameField(forms.CharField): if not self.help_text: self.help_text = """ Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range - are not supported. Example: [ge,xe]-0/0/[0-9] + are not supported (example: [ge,xe]-0/0/[0-9]). """ def to_python(self, value): diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 7fa9f66bc..93cb88088 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -466,6 +466,7 @@ class ViewTestCases: """ bulk_create_count = 3 bulk_create_data = {} + validation_excluded_fields = [] @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_create_multiple_objects_without_permission(self): @@ -500,7 +501,7 @@ class ViewTestCases: self.assertHttpStatus(response, 302) self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count()) for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]: - self.assertInstanceEqual(instance, self.bulk_create_data) + self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields) @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_create_multiple_objects_with_constrained_permission(self): @@ -532,7 +533,7 @@ class ViewTestCases: self.assertHttpStatus(response, 302) self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count()) for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]: - self.assertInstanceEqual(instance, self.bulk_create_data) + self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields) class BulkImportObjectsViewTestCase(ModelViewTestCase): """ diff --git a/netbox/virtualization/forms/bulk_create.py b/netbox/virtualization/forms/bulk_create.py index 6cf7c0d7c..03997f88d 100644 --- a/netbox/virtualization/forms/bulk_create.py +++ b/netbox/virtualization/forms/bulk_create.py @@ -13,7 +13,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput() ) - name_pattern = ExpandableNameField( + name = ExpandableNameField( label='Name' ) @@ -27,4 +27,4 @@ class VMInterfaceBulkCreateForm( form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']), VirtualMachineBulkAddComponentForm ): - pass + replication_fields = ('name',) diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py index fca9c6b56..268afb9bb 100644 --- a/netbox/virtualization/forms/models.py +++ b/netbox/virtualization/forms/models.py @@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError from dcim.forms.common import InterfaceCommonForm from dcim.forms.models import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup -from extras.models import Tag from ipam.models import IPAddress, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm @@ -278,6 +277,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): + virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all() + ) parent = DynamicModelChoiceField( queryset=VMInterface.objects.all(), required=False, @@ -338,7 +340,6 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', ] widgets = { - 'virtual_machine': forms.HiddenInput(), 'mode': StaticSelect() } labels = { @@ -347,3 +348,10 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): help_texts = { 'mode': INTERFACE_MODE_HELP_TEXT, } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Disable reassignment of VirtualMachine when editing an existing instance + if self.instance.pk: + self.fields['virtual_machine'].disabled = True diff --git a/netbox/virtualization/forms/object_create.py b/netbox/virtualization/forms/object_create.py index feab3bb3a..79457a56e 100644 --- a/netbox/virtualization/forms/object_create.py +++ b/netbox/virtualization/forms/object_create.py @@ -1,17 +1,14 @@ -from django import forms - -from utilities.forms import BootstrapMixin, DynamicModelChoiceField, ExpandableNameField -from .models import VirtualMachine +from utilities.forms import ExpandableNameField +from .models import VMInterfaceForm __all__ = ( 'VMInterfaceCreateForm', ) -class VMInterfaceCreateForm(BootstrapMixin, forms.Form): - virtual_machine = DynamicModelChoiceField( - queryset=VirtualMachine.objects.all() - ) - name_pattern = ExpandableNameField( - label='Name' - ) +class VMInterfaceCreateForm(VMInterfaceForm): + name = ExpandableNameField() + replication_fields = ('name',) + + class Meta(VMInterfaceForm.Meta): + exclude = ('name',) diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 01d4394f3..d00ceb5a2 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -251,6 +251,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase): class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): model = VMInterface + validation_excluded_fields = ('name',) @classmethod def setUpTestData(cls): @@ -290,10 +291,10 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { - 'virtual_machine': virtualmachines[1].pk, + 'virtual_machine': virtualmachines[0].pk, 'name': 'Interface X', 'enabled': False, - 'bridge': interfaces[3].pk, + 'bridge': interfaces[1].pk, 'mac_address': EUI('01-02-03-04-05-06'), 'mtu': 65000, 'description': 'New description', @@ -306,7 +307,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): cls.bulk_create_data = { 'virtual_machine': virtualmachines[1].pk, - 'name_pattern': 'Interface [4-6]', + 'name': 'Interface [4-6]', 'enabled': False, 'bridge': interfaces[3].pk, 'mac_address': EUI('01-02-03-04-05-06'), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 5b26f8503..611725d62 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -451,13 +451,11 @@ class VMInterfaceCreateView(generic.ComponentCreateView): queryset = VMInterface.objects.all() form = forms.VMInterfaceCreateForm model_form = forms.VMInterfaceForm - patterned_fields = ('name',) class VMInterfaceEditView(generic.ObjectEditView): queryset = VMInterface.objects.all() form = forms.VMInterfaceForm - template_name = 'virtualization/vminterface_edit.html' class VMInterfaceDeleteView(generic.ObjectDeleteView): From f97eb99950819789d10866981a8aead250905126 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 15 Sep 2022 10:14:16 -0400 Subject: [PATCH 08/13] Changelog for #10247, #10258, #10305, #10362 --- docs/release-notes/version-3.3.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index c5ca3d5be..c249d9874 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -11,14 +11,18 @@ ### Bug Fixes * [#9231](https://github.com/netbox-community/netbox/issues/9231) - Fix `empty` lookup expression for string filters +* [#10247](https://github.com/netbox-community/netbox/issues/10247) - Allow changing the pre-populated device/VM when creating new components * [#10250](https://github.com/netbox-community/netbox/issues/10250) - Fix exception when CableTermination validation fails during bulk import of cables +* [#10258](https://github.com/netbox-community/netbox/issues/10258) - Enable the use of reports & scripts packaged in submodules * [#10259](https://github.com/netbox-community/netbox/issues/10259) - Fix `NoReverseMatch` exception when listing available prefixes with "flat" column displayed * [#10270](https://github.com/netbox-community/netbox/issues/10270) - Fix custom field validation when creating new services * [#10278](https://github.com/netbox-community/netbox/issues/10278) - Fix "create & add another" for image attachments * [#10294](https://github.com/netbox-community/netbox/issues/10294) - Fix spurious changelog diff for interface WWN field * [#10304](https://github.com/netbox-community/netbox/issues/10304) - Enable cloning for custom fields & custom links +* [#10305](https://github.com/netbox-community/netbox/issues/10305) - Fix Virtual Chassis master field cannot be null according to the API * [#10307](https://github.com/netbox-community/netbox/issues/10307) - Correct value for "Passive 48V (4-pair)" PoE type selection * [#10333](https://github.com/netbox-community/netbox/issues/10333) - Show available values for `ui_visibility` field of CustomField for CSV import +* [#10362](https://github.com/netbox-community/netbox/issues/10362) - Correct display of custom fields when editing an L2VPN termination --- From e9a91455e8827fffaab7f10e9938bce3022b9ba3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 15 Sep 2022 12:55:21 -0400 Subject: [PATCH 09/13] #10359: Add region and site group columns to the devices table --- docs/release-notes/version-3.3.md | 1 + netbox/dcim/tables/devices.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index c5ca3d5be..6c7d5d291 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -7,6 +7,7 @@ * [#8580](https://github.com/netbox-community/netbox/issues/8580) - Add `occupied` filter for cabled objects to filter by cable or `mark_connected` * [#9577](https://github.com/netbox-community/netbox/issues/9577) - Add `has_front_image` and `has_rear_image` filters for device types * [#10268](https://github.com/netbox-community/netbox/issues/10268) - Omit trailing ".0" in device positions within UI +* [#10359](https://github.com/netbox-community/netbox/issues/10359) - Add region and site group columns to the devices table ### Bug Fixes diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index a7cdf4b9f..142c7ef67 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -143,6 +143,15 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable): template_code=DEVICE_LINK ) status = columns.ChoiceFieldColumn() + region = tables.Column( + accessor=Accessor('site__region'), + linkify=True + ) + site_group = tables.Column( + accessor=Accessor('site__group'), + linkify=True, + verbose_name='Site Group' + ) site = tables.Column( linkify=True ) @@ -203,9 +212,9 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable): model = Device fields = ( 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', - 'platform', 'serial', 'asset_tag', 'site', 'region', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', - 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', - 'contacts', 'tags', 'created', 'last_updated', + 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face', + 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', From e05696dfcc8f3c711f1435795ea9676638fc12b8 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 15 Sep 2022 13:17:04 -0400 Subject: [PATCH 10/13] Fixes #10337: Display SSO links when local authentication fails --- docs/release-notes/version-3.3.md | 1 + netbox/templates/login.html | 20 ++++++++++---------- netbox/users/views.py | 27 +++++++++++++++------------ 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index ba6e4a06e..3f4272f95 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -23,6 +23,7 @@ * [#10305](https://github.com/netbox-community/netbox/issues/10305) - Fix Virtual Chassis master field cannot be null according to the API * [#10307](https://github.com/netbox-community/netbox/issues/10307) - Correct value for "Passive 48V (4-pair)" PoE type selection * [#10333](https://github.com/netbox-community/netbox/issues/10333) - Show available values for `ui_visibility` field of CustomField for CSV import +* [#10337](https://github.com/netbox-community/netbox/issues/10337) - Display SSO links when local authentication fails * [#10362](https://github.com/netbox-community/netbox/issues/10362) - Correct display of custom fields when editing an L2VPN termination --- diff --git a/netbox/templates/login.html b/netbox/templates/login.html index ea5cfc3e5..66b519671 100644 --- a/netbox/templates/login.html +++ b/netbox/templates/login.html @@ -13,6 +13,16 @@
    {% endif %} + {# Login form errors #} + {% if form.non_field_errors %} + + {% endif %} + {# Login form #}