diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 08bccc551..9735da5b5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.3.2
+ placeholder: v3.3.3
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 4dbf51f2c..691e99cc6 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.3.2
+ placeholder: v3.3.3
validations:
required: true
- type: dropdown
diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md
index c5ca3d5be..14cbc74a3 100644
--- a/docs/release-notes/version-3.3.md
+++ b/docs/release-notes/version-3.3.md
@@ -1,24 +1,35 @@
# NetBox v3.3
-## v3.3.3 (FUTURE)
+## v3.3.4 (FUTURE)
+
+---
+
+## v3.3.3 (2022-09-15)
### Enhancements
* [#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
* [#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
+* [#10337](https://github.com/netbox-community/netbox/issues/10337) - Display SSO links when local authentication fails
+* [#10353](https://github.com/netbox-community/netbox/issues/10353) - Table action buttons should reserve return URL parameters
+* [#10362](https://github.com/netbox-community/netbox/issues/10362) - Correct display of custom fields when editing an L2VPN termination
---
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/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 e22913a8b..c521ee095 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/devices.py b/netbox/dcim/tables/devices.py
index c42731b90..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', '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',
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_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
}
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/extras/api/views.py b/netbox/extras/api/views.py
index 82c68c86d..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)
@@ -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/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..32e4efc2d 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/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..ced3bd4b9 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
@@ -100,12 +100,12 @@ 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'),
- 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/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
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index f96b6085b..41a6fd6db 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup
#
-VERSION = '3.3.3-dev'
+VERSION = '3.3.4-dev'
# Hostname
HOSTNAME = platform.node()
diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py
index 6ab50d4c2..c7545192a 100644
--- a/netbox/netbox/tables/columns.py
+++ b/netbox/netbox/tables/columns.py
@@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
from django.db.models import DateField, DateTimeField
from django.template import Context, Template
from django.urls import reverse
+from django.utils.encoding import escape_uri_path
from django.utils.html import escape
from django.utils.formats import date_format
from django.utils.safestring import mark_safe
@@ -210,7 +211,7 @@ class ActionsColumn(tables.Column):
model = table.Meta.model
request = getattr(table, 'context', {}).get('request')
- url_appendix = f'?return_url={request.path}' if request else ''
+ url_appendix = f'?return_url={escape_uri_path(request.get_full_path())}' if request else ''
html = ''
# Compile actions menu
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 %}
-
-
-
-
-
- Device Type
-
-
-
-
- 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 %}
-
-
- {{ object.component|meta:"verbose_name"|bettertitle }}
-
-
-
-
-
- {% 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 %}
-
-
- {{ object.component|meta:"verbose_name"|bettertitle }}
-
-
-
-
-
- {% 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/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 %}
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/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 %}
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 %}
+
+
Errors
+
+ {{ form.non_field_errors }}
+
+
+ {% endif %}
+
{# Login form #}