Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2022-09-15 14:30:24 -04:00
commit 1eb0e5d307
44 changed files with 627 additions and 687 deletions

View File

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

View File

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

View File

@ -1,24 +1,35 @@
# NetBox v3.3 # NetBox v3.3
## v3.3.3 (FUTURE) ## v3.3.4 (FUTURE)
---
## v3.3.3 (2022-09-15)
### Enhancements ### Enhancements
* [#8580](https://github.com/netbox-community/netbox/issues/8580) - Add `occupied` filter for cabled objects to filter by cable or `mark_connected` * [#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 * [#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 * [#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 ### Bug Fixes
* [#9231](https://github.com/netbox-community/netbox/issues/9231) - Fix `empty` lookup expression for string filters * [#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 * [#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 * [#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 * [#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 * [#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 * [#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 * [#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 * [#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 * [#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
--- ---

View File

@ -1076,7 +1076,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class VirtualChassisSerializer(NetBoxModelSerializer): class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') 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) member_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:

View File

@ -3,7 +3,7 @@ from django import forms
from dcim.models import * from dcim.models import *
from extras.forms import CustomFieldsMixin from extras.forms import CustomFieldsMixin
from extras.models import Tag 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 from .object_create import ComponentCreateForm
__all__ = ( __all__ = (
@ -24,7 +24,7 @@ __all__ = (
# Device components # Device components
# #
class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm): class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
required=False required=False
) )
replication_fields = ('name', 'label')
class ConsolePortBulkCreateForm( class ConsolePortBulkCreateForm(
@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm(
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = ConsolePort model = ConsolePort
field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags') field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
class ConsoleServerPortBulkCreateForm( class ConsoleServerPortBulkCreateForm(
@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm(
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = ConsoleServerPort model = ConsoleServerPort
field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags') field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
class PowerPortBulkCreateForm( class PowerPortBulkCreateForm(
@ -60,7 +61,7 @@ class PowerPortBulkCreateForm(
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = PowerPort 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( class PowerOutletBulkCreateForm(
@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm(
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = PowerOutlet model = PowerOutlet
field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags') field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
class InterfaceBulkCreateForm( class InterfaceBulkCreateForm(
@ -79,7 +80,7 @@ class InterfaceBulkCreateForm(
): ):
model = Interface model = Interface
field_order = ( 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', 'poe_type', 'mark_connected', 'description', 'tags',
) )
@ -96,13 +97,13 @@ class RearPortBulkCreateForm(
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = RearPort 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): class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay 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( position_pattern = ExpandableNameField(
label='Position', label='Position',
required=False, required=False,
@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm): class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
model = DeviceBay model = DeviceBay
field_order = ('name_pattern', 'label_pattern', 'description', 'tags') field_order = ('name', 'label', 'description', 'tags')
class InventoryItemBulkCreateForm( class InventoryItemBulkCreateForm(
@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm(
): ):
model = InventoryItem model = InventoryItem
field_order = ( 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', 'description', 'tags',
) )

View File

@ -986,47 +986,74 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
# Device component templates # 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: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect, 'type': StaticSelect,
} }
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
)
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect, '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: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): class PowerOutletTemplateForm(ModularComponentTemplateForm):
power_port = DynamicModelChoiceField( power_port = DynamicModelChoiceField(
queryset=PowerPortTemplate.objects.all(), queryset=PowerPortTemplate.objects.all(),
required=False, 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: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
'feed_leg': 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: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
'poe_mode': StaticSelect(), 'poe_mode': StaticSelect(),
'poe_type': StaticSelect(), 'poe_type': StaticSelect(),
} }
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm): class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField( rear_port = DynamicModelChoiceField(
queryset=RearPortTemplate.objects.all(), queryset=RearPortTemplate.objects.all(),
required=False, 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: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
@ -1080,48 +1119,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
'description', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm): class RearPortTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
)
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
] ]
widgets = { widgets = {
'device_type': forms.HiddenInput(),
'module_type': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm): class ModuleBayTemplateForm(ComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'name', 'label', 'position', 'description')),
)
class Meta: class Meta:
model = ModuleBayTemplate model = ModuleBayTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'position', 'description', '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: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = [ fields = [
'device_type', 'name', 'label', 'description', 'device_type', 'name', 'label', 'description',
] ]
widgets = {
'device_type': forms.HiddenInput(),
}
class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm): class InventoryItemTemplateForm(ComponentTemplateForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=InventoryItemTemplate.objects.all(), queryset=InventoryItemTemplate.objects.all(),
required=False, required=False,
@ -1148,22 +1189,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
widget=forms.HiddenInput widget=forms.HiddenInput
) )
fieldsets = (
(None, (
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id',
)),
)
class Meta: class Meta:
model = InventoryItemTemplate model = InventoryItemTemplate
fields = [ fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id', 'component_type', 'component_id',
] ]
widgets = {
'device_type': forms.HiddenInput(),
}
# #
# Device components # 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( module = DynamicModelChoiceField(
queryset=Module.objects.all(), queryset=Module.objects.all(),
required=False, 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: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
'speed': StaticSelect(), 'speed': StaticSelect(),
} }
class ConsoleServerPortForm(NetBoxModelForm): class ConsoleServerPortForm(ModularDeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(), fieldsets = (
required=False, (None, (
query_params={ 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
'device_id': '$device', )),
}
) )
class Meta: class Meta:
@ -1199,42 +1263,32 @@ class ConsoleServerPortForm(NetBoxModelForm):
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
'speed': StaticSelect(), 'speed': StaticSelect(),
} }
class PowerPortForm(NetBoxModelForm): class PowerPortForm(ModularDeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(), fieldsets = (
required=False, (None, (
query_params={ 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'device_id': '$device', 'description', 'tags',
} )),
) )
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
'description', 'description', 'tags',
'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class PowerOutletForm(NetBoxModelForm): class PowerOutletForm(ModularDeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
power_port = DynamicModelChoiceField( power_port = DynamicModelChoiceField(
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
required=False, 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: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
@ -1250,20 +1311,12 @@ class PowerOutletForm(NetBoxModelForm):
'tags', 'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
'feed_leg': StaticSelect(), 'feed_leg': StaticSelect(),
} }
class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
@ -1338,7 +1391,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
) )
fieldsets = ( 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')), ('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')), ('Related Interfaces', ('parent', 'bridge', 'lag')),
@ -1358,7 +1411,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
'speed': SelectSpeedWidget(), 'speed': SelectSpeedWidget(),
'poe_mode': StaticSelect(), '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) self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
class FrontPortForm(NetBoxModelForm): class FrontPortForm(ModularDeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
rear_port = DynamicModelChoiceField( rear_port = DynamicModelChoiceField(
queryset=RearPort.objects.all(), queryset=RearPort.objects.all(),
query_params={ 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: class Meta:
model = FrontPort model = FrontPort
fields = [ fields = [
@ -1410,18 +1462,15 @@ class FrontPortForm(NetBoxModelForm):
'description', 'tags', 'description', 'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class RearPortForm(NetBoxModelForm): class RearPortForm(ModularDeviceComponentForm):
module = DynamicModelChoiceField( fieldsets = (
queryset=Module.objects.all(), (None, (
required=False, 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
query_params={ )),
'device_id': '$device',
}
) )
class Meta: class Meta:
@ -1430,33 +1479,32 @@ class RearPortForm(NetBoxModelForm):
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
] ]
widgets = { widgets = {
'device': forms.HiddenInput(),
'type': StaticSelect(), 'type': StaticSelect(),
} }
class ModuleBayForm(NetBoxModelForm): class ModuleBayForm(DeviceComponentForm):
fieldsets = (
(None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
)
class Meta: class Meta:
model = ModuleBay model = ModuleBay
fields = [ fields = [
'device', 'name', 'label', 'position', 'description', 'tags', '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: class Meta:
model = DeviceBay model = DeviceBay
fields = [ fields = [
'device', 'name', 'label', 'description', 'tags', 'device', 'name', 'label', 'description', 'tags',
] ]
widgets = {
'device': forms.HiddenInput(),
}
class PopulateDeviceBayForm(BootstrapMixin, forms.Form): class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
@ -1479,10 +1527,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
).exclude(pk=device_bay.device.pk) ).exclude(pk=device_bay.device.pk)
class InventoryItemForm(NetBoxModelForm): class InventoryItemForm(DeviceComponentForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all()
)
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
required=False, required=False,

View File

@ -2,46 +2,56 @@ from django import forms
from dcim.models import * from dcim.models import *
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from utilities.forms import ( from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, from . import models as model_forms
)
__all__ = ( __all__ = (
'ComponentTemplateCreateForm', 'ComponentCreateForm',
'DeviceComponentCreateForm', 'ConsolePortCreateForm',
'ConsolePortTemplateCreateForm',
'ConsoleServerPortCreateForm',
'ConsoleServerPortTemplateCreateForm',
'DeviceBayCreateForm',
'DeviceBayTemplateCreateForm',
'FrontPortCreateForm', 'FrontPortCreateForm',
'FrontPortTemplateCreateForm', 'FrontPortTemplateCreateForm',
'InterfaceCreateForm',
'InterfaceTemplateCreateForm',
'InventoryItemCreateForm', 'InventoryItemCreateForm',
'ModularComponentTemplateCreateForm', 'InventoryItemTemplateCreateForm',
'ModuleBayCreateForm', 'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm', 'ModuleBayTemplateCreateForm',
'PowerOutletCreateForm',
'PowerOutletTemplateCreateForm',
'PowerPortCreateForm',
'PowerPortTemplateCreateForm',
'RearPortCreateForm',
'RearPortTemplateCreateForm',
'VirtualChassisCreateForm', '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. a name pattern.
""" """
name_pattern = ExpandableNameField( name = ExpandableNameField()
label='Name' label = ExpandableNameField(
)
label_pattern = ExpandableNameField(
label='Label',
required=False, 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): def clean(self):
super().clean() super().clean()
# Validate that all patterned fields generate an equal number of values # Validate that all replication fields generate an equal number of values
patterned_fields = [ pattern_count = len(self.cleaned_data[self.replication_fields[0]])
field_name for field_name in self.fields if field_name.endswith('_pattern') for field_name in self.replication_fields:
]
pattern_count = len(self.cleaned_data['name_pattern'])
for field_name in patterned_fields:
value_count = len(self.cleaned_data[field_name]) value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count: if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({ raise forms.ValidationError({
@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form):
}, code='label_pattern_mismatch') }, code='label_pattern_mismatch')
class ComponentTemplateCreateForm(ComponentCreateForm): #
""" # Device component templates
Creation form for component templates that can be assigned only to a DeviceType. #
"""
device_type = DynamicModelChoiceField( class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
queryset=DeviceType.objects.all(),
) class Meta(model_forms.ConsolePortTemplateForm.Meta):
field_order = ('device_type', 'name_pattern', 'label_pattern') exclude = ('name', 'label')
class ModularComponentTemplateCreateForm(ComponentCreateForm): class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
"""
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType. class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
""" exclude = ('name', 'label')
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: <code>[ge,xe]-0/0/[0-9]</code>. {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 DeviceComponentCreateForm(ComponentCreateForm): class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all() class Meta(model_forms.PowerPortTemplateForm.Meta):
) exclude = ('name', 'label')
field_order = ('device', 'name_pattern', 'label_pattern')
class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm): class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
rear_port_set = forms.MultipleChoiceField(
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=[], choices=[],
label='Rear ports', label='Rear ports',
help_text='Select one rear port assignment for each front port being created.', 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
choices.append( choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) ('{}:{}'.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): def get_iterative_data(self, iteration):
# Assign rear port and position from selected set # 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 { return {
'rear_port': int(rear_port), 'rear_port': int(rear_port),
@ -143,16 +152,94 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
} }
class FrontPortCreateForm(DeviceComponentCreateForm): class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
rear_port_set = forms.MultipleChoiceField(
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 <code>{module}</code> will be replaced with the position ' \
'of the assigned module, if any'
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
rear_port = forms.MultipleChoiceField(
choices=[], choices=[],
label='Rear ports', label='Rear ports',
help_text='Select one rear port assignment for each front port being created.', 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
choices.append( choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i)) ('{}:{}'.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): def get_iterative_data(self, iteration):
# Assign rear port and position from selected set # 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 { return {
'rear_port': int(rear_port), 'rear_port': int(rear_port),
@ -189,28 +276,39 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
} }
class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm): class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
position_pattern = ExpandableNameField(
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', label='Position',
required=False, 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): class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
position_pattern = ExpandableNameField(
label='Position', class Meta(model_forms.InventoryItemForm.Meta):
required=False, exclude = ('name', 'label')
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): #
# Device is assigned by the model form # Virtual chassis
field_order = ('name_pattern', 'label_pattern') #
class VirtualChassisCreateForm(NetBoxModelForm): class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(

View File

@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
def clean(self): def clean(self):
super().clean() super().clean()
# Validate rear port assignment if hasattr(self, 'rear_port'):
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 # Validate rear port assignment
if self.rear_port_position > self.rear_port.positions: if self.rear_port.device != self.device:
raise ValidationError({ raise ValidationError({
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port " "rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
f"{self.rear_port.name} has only {self.rear_port.positions} positions" })
})
# 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): class RearPort(ModularComponentModel, CabledObjectModel):

View File

@ -143,6 +143,15 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = columns.ChoiceFieldColumn() 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( site = tables.Column(
linkify=True linkify=True
) )
@ -203,9 +212,9 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
model = Device model = Device
fields = ( fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', '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', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
'contacts', 'tags', 'created', 'last_updated', 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',

View File

@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li> <li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ record.device_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Inventory Item</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name_pattern={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li> <li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ record.device_id }}&parent={{ record.pk }}&name={{ record.name }}.&type=virtual&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Child Interface</a></li>
{% endif %} {% endif %}
{% if perms.ipam.add_l2vpntermination %} {% if perms.ipam.add_l2vpntermination %}
<li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li> <li><a class="dropdown-item" href="{% url 'ipam:l2vpntermination_add' %}?device={{ object.pk }}&interface={{ record.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">L2VPN Termination</a></li>

View File

@ -2057,6 +2057,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
cls.bulk_update_data = { cls.bulk_update_data = {
'domain': 'newdomain', 'domain': 'newdomain',
'master': None
} }

View File

@ -1,6 +1,6 @@
from django.test import TestCase from django.test import TestCase
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
from dcim.forms import * from dcim.forms import *
from dcim.models import * from dcim.models import *
from utilities.testing import create_test_device from utilities.testing import create_test_device
@ -129,10 +129,11 @@ class LabelTestCase(TestCase):
""" """
interface_data = { interface_data = {
'device': self.device.pk, 'device': self.device.pk,
'name_pattern': 'eth[0-9]', 'name': 'eth[0-9]',
'label_pattern': 'Interface[0-9]', 'label': 'Interface[0-9]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
} }
form = DeviceComponentCreateForm(interface_data) form = InterfaceCreateForm(interface_data)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
@ -142,10 +143,11 @@ class LabelTestCase(TestCase):
""" """
bad_interface_data = { bad_interface_data = {
'device': self.device.pk, 'device': self.device.pk,
'name_pattern': 'eth[0-9]', 'name': 'eth[0-9]',
'label_pattern': 'Interface[0-1]', '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.assertFalse(form.is_valid())
self.assertIn('label_pattern', form.errors) self.assertIn('label', form.errors)

View File

@ -1082,31 +1082,28 @@ front-ports:
class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ConsolePortTemplate model = ConsolePortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
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)
ConsolePortTemplate.objects.bulk_create(( ConsolePortTemplate.objects.bulk_create((
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'), ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'), ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'),
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'), ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Console Port Template X', 'name': 'Console Port Template X',
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Console Port Template [4-6]', 'name': 'Console Port Template [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
} }
@ -1117,31 +1114,28 @@ class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
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)
ConsoleServerPortTemplate.objects.bulk_create(( ConsoleServerPortTemplate.objects.bulk_create((
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'), ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'), ConsoleServerPortTemplate(device_type=devicetype, 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 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Console Server Port Template X', 'name': 'Console Server Port Template X',
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Console Server Port Template [4-6]', 'name': 'Console Server Port Template [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
} }
@ -1152,24 +1146,21 @@ class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateVie
class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = PowerPortTemplate model = PowerPortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
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)
PowerPortTemplate.objects.bulk_create(( PowerPortTemplate.objects.bulk_create((
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'), PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'), PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'), PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Power Port Template X', 'name': 'Power Port Template X',
'type': PowerPortTypeChoices.TYPE_IEC_C14, 'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100, 'maximum_draw': 100,
@ -1177,8 +1168,8 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Power Port Template [4-6]', 'name': 'Power Port Template [4-6]',
'type': PowerPortTypeChoices.TYPE_IEC_C14, 'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100, 'maximum_draw': 100,
'allocated_draw': 50, 'allocated_draw': 50,
@ -1193,6 +1184,7 @@ class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = PowerOutletTemplate model = PowerOutletTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1220,7 +1212,7 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetype.pk, 'device_type': devicetype.pk,
'name_pattern': 'Power Outlet Template [4-6]', 'name': 'Power Outlet Template [4-6]',
'type': PowerOutletTypeChoices.TYPE_IEC_C13, 'type': PowerOutletTypeChoices.TYPE_IEC_C13,
'power_port': powerports[0].pk, 'power_port': powerports[0].pk,
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@ -1234,34 +1226,31 @@ class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = InterfaceTemplate model = InterfaceTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
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)
InterfaceTemplate.objects.bulk_create(( InterfaceTemplate.objects.bulk_create((
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'), InterfaceTemplate(device_type=devicetype, name='Interface Template 1'),
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'), InterfaceTemplate(device_type=devicetype, name='Interface Template 2'),
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'), InterfaceTemplate(device_type=devicetype, name='Interface Template 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Interface Template X', 'name': 'Interface Template X',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mgmt_only': True, 'mgmt_only': True,
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Interface Template [4-6]', 'name': 'Interface Template [4-6]',
# Test that a label can be applied to each generated interface templates # 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, 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mgmt_only': True, 'mgmt_only': True,
} }
@ -1274,6 +1263,7 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = FrontPortTemplate model = FrontPortTemplate
validation_excluded_fields = ('name', 'label', 'rear_port')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1306,11 +1296,9 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetype.pk, 'device_type': devicetype.pk,
'name_pattern': 'Front Port [4-6]', 'name': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'rear_port_set': [ 'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
'{}:1'.format(rp.pk) for rp in rearports[3:6]
],
} }
cls.bulk_edit_data = { cls.bulk_edit_data = {
@ -1320,32 +1308,29 @@ class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = RearPortTemplate model = RearPortTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
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)
RearPortTemplate.objects.bulk_create(( RearPortTemplate.objects.bulk_create((
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'), RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'), RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'), RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Rear Port Template X', 'name': 'Rear Port Template X',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'positions': 2, 'positions': 2,
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Rear Port Template [4-6]', 'name': 'Rear Port Template [4-6]',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'positions': 2, 'positions': 2,
} }
@ -1357,30 +1342,27 @@ class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase
class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = ModuleBayTemplate model = ModuleBayTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
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)
ModuleBayTemplate.objects.bulk_create(( ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 1'), ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 2'), ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
ModuleBayTemplate(device_type=devicetypes[0], name='Module Bay Template 3'), ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Module Bay Template X', 'name': 'Module Bay Template X',
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Module Bay Template [4-6]', 'name': 'Module Bay Template [4-6]',
} }
cls.bulk_edit_data = { cls.bulk_edit_data = {
@ -1390,30 +1372,27 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = DeviceBayTemplate model = DeviceBayTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetypes = ( devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)
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)
DeviceBayTemplate.objects.bulk_create(( DeviceBayTemplate.objects.bulk_create((
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'), DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'),
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'), DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'),
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'), DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'),
)) ))
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Device Bay Template X', 'name': 'Device Bay Template X',
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Device Bay Template [4-6]', 'name': 'Device Bay Template [4-6]',
} }
cls.bulk_edit_data = { cls.bulk_edit_data = {
@ -1423,6 +1402,7 @@ class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase): class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = InventoryItemTemplate model = InventoryItemTemplate
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1431,30 +1411,25 @@ class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
) )
Manufacturer.objects.bulk_create(manufacturers) Manufacturer.objects.bulk_create(manufacturers)
devicetype = DeviceType.objects.create(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1')
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)
inventory_item_templates = ( inventory_item_templates = (
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 1', manufacturer=manufacturers[0]), InventoryItemTemplate(device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturers[0]),
InventoryItemTemplate(device_type=devicetypes[0], name='Inventory Item Template 2', manufacturer=manufacturers[0]), InventoryItemTemplate(device_type=devicetype, 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 3', manufacturer=manufacturers[0]),
) )
for item in inventory_item_templates: for item in inventory_item_templates:
item.save() item.save()
cls.form_data = { cls.form_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name': 'Inventory Item Template X', 'name': 'Inventory Item Template X',
'manufacturer': manufacturers[1].pk, 'manufacturer': manufacturers[1].pk,
} }
cls.bulk_create_data = { cls.bulk_create_data = {
'device_type': devicetypes[1].pk, 'device_type': devicetype.pk,
'name_pattern': 'Inventory Item Template [4-6]', 'name': 'Inventory Item Template [4-6]',
'manufacturer': manufacturers[1].pk, 'manufacturer': manufacturers[1].pk,
} }
@ -1912,6 +1887,7 @@ class ModuleTestCase(
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ConsolePort model = ConsolePort
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1935,9 +1911,9 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, '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 # 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, 'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port', 'description': 'A console port',
'tags': sorted([t.pk for t in tags]), 'tags': sorted([t.pk for t in tags]),
@ -1970,6 +1946,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ConsoleServerPort model = ConsoleServerPort
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1993,7 +1970,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Console Server Port [4-6]', 'name': 'Console Server Port [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console server port', 'description': 'A console server port',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
@ -2026,6 +2003,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = PowerPort model = PowerPort
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2051,7 +2029,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Power Port [4-6]]', 'name': 'Power Port [4-6]]',
'type': PowerPortTypeChoices.TYPE_IEC_C14, 'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100, 'maximum_draw': 100,
'allocated_draw': 50, 'allocated_draw': 50,
@ -2088,6 +2066,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = PowerOutlet model = PowerOutlet
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2119,7 +2098,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Power Outlet [4-6]', 'name': 'Power Outlet [4-6]',
'type': PowerOutletTypeChoices.TYPE_IEC_C13, 'type': PowerOutletTypeChoices.TYPE_IEC_C13,
'power_port': powerports[1].pk, 'power_port': powerports[1].pk,
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
@ -2153,6 +2132,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = Interface model = Interface
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2217,7 +2197,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Interface [4-6]', 'name': 'Interface [4-6]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False, 'enabled': False,
'bridge': interfaces[4].pk, 'bridge': interfaces[4].pk,
@ -2277,6 +2257,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = FrontPort model = FrontPort
validation_excluded_fields = ('name', 'label', 'rear_port')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2312,11 +2293,9 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Front Port [4-6]', 'name': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'rear_port_set': [ 'rear_port': [f'{rp.pk}:1' for rp in rearports[3:6]],
'{}:1'.format(rp.pk) for rp in rearports[3:6]
],
'description': 'New description', 'description': 'New description',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2348,6 +2327,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = RearPort model = RearPort
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2372,7 +2352,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Rear Port [4-6]', 'name': 'Rear Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'positions': 3, 'positions': 3,
'description': 'A rear port', 'description': 'A rear port',
@ -2406,6 +2386,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase): class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = ModuleBay model = ModuleBay
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2428,7 +2409,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Module Bay [4-6]', 'name': 'Module Bay [4-6]',
'description': 'A module bay', 'description': 'A module bay',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2447,6 +2428,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = DeviceBay model = DeviceBay
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2472,7 +2454,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Device Bay [4-6]', 'name': 'Device Bay [4-6]',
'description': 'A device bay', 'description': 'A device bay',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2491,6 +2473,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = InventoryItem model = InventoryItem
validation_excluded_fields = ('name', 'label')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2525,7 +2508,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'device': device.pk, 'device': device.pk,
'name_pattern': 'Inventory Item [4-6]', 'name': 'Inventory Item [4-6]',
'role': roles[1].pk, 'role': roles[1].pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
'parent': None, 'parent': None,

View File

@ -1120,9 +1120,8 @@ class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
class ConsolePortTemplateCreateView(generic.ComponentCreateView): class ConsolePortTemplateCreateView(generic.ComponentCreateView):
queryset = ConsolePortTemplate.objects.all() queryset = ConsolePortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm model_form = forms.ConsolePortTemplateForm
template_name = 'dcim/component_template_create.html'
class ConsolePortTemplateEditView(generic.ObjectEditView): class ConsolePortTemplateEditView(generic.ObjectEditView):
@ -1155,9 +1154,8 @@ class ConsolePortTemplateBulkDeleteView(generic.BulkDeleteView):
class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView): class ConsoleServerPortTemplateCreateView(generic.ComponentCreateView):
queryset = ConsoleServerPortTemplate.objects.all() queryset = ConsoleServerPortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm model_form = forms.ConsoleServerPortTemplateForm
template_name = 'dcim/component_template_create.html'
class ConsoleServerPortTemplateEditView(generic.ObjectEditView): class ConsoleServerPortTemplateEditView(generic.ObjectEditView):
@ -1190,9 +1188,8 @@ class ConsoleServerPortTemplateBulkDeleteView(generic.BulkDeleteView):
class PowerPortTemplateCreateView(generic.ComponentCreateView): class PowerPortTemplateCreateView(generic.ComponentCreateView):
queryset = PowerPortTemplate.objects.all() queryset = PowerPortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm model_form = forms.PowerPortTemplateForm
template_name = 'dcim/component_template_create.html'
class PowerPortTemplateEditView(generic.ObjectEditView): class PowerPortTemplateEditView(generic.ObjectEditView):
@ -1225,9 +1222,8 @@ class PowerPortTemplateBulkDeleteView(generic.BulkDeleteView):
class PowerOutletTemplateCreateView(generic.ComponentCreateView): class PowerOutletTemplateCreateView(generic.ComponentCreateView):
queryset = PowerOutletTemplate.objects.all() queryset = PowerOutletTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm model_form = forms.PowerOutletTemplateForm
template_name = 'dcim/component_template_create.html'
class PowerOutletTemplateEditView(generic.ObjectEditView): class PowerOutletTemplateEditView(generic.ObjectEditView):
@ -1260,9 +1256,8 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
class InterfaceTemplateCreateView(generic.ComponentCreateView): class InterfaceTemplateCreateView(generic.ComponentCreateView):
queryset = InterfaceTemplate.objects.all() queryset = InterfaceTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm model_form = forms.InterfaceTemplateForm
template_name = 'dcim/component_template_create.html'
class InterfaceTemplateEditView(generic.ObjectEditView): class InterfaceTemplateEditView(generic.ObjectEditView):
@ -1297,15 +1292,6 @@ class FrontPortTemplateCreateView(generic.ComponentCreateView):
queryset = FrontPortTemplate.objects.all() queryset = FrontPortTemplate.objects.all()
form = forms.FrontPortTemplateCreateForm form = forms.FrontPortTemplateCreateForm
model_form = forms.FrontPortTemplateForm 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): class FrontPortTemplateEditView(generic.ObjectEditView):
@ -1338,9 +1324,8 @@ class FrontPortTemplateBulkDeleteView(generic.BulkDeleteView):
class RearPortTemplateCreateView(generic.ComponentCreateView): class RearPortTemplateCreateView(generic.ComponentCreateView):
queryset = RearPortTemplate.objects.all() queryset = RearPortTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.RearPortTemplateCreateForm
model_form = forms.RearPortTemplateForm model_form = forms.RearPortTemplateForm
template_name = 'dcim/component_template_create.html'
class RearPortTemplateEditView(generic.ObjectEditView): class RearPortTemplateEditView(generic.ObjectEditView):
@ -1375,8 +1360,6 @@ class ModuleBayTemplateCreateView(generic.ComponentCreateView):
queryset = ModuleBayTemplate.objects.all() queryset = ModuleBayTemplate.objects.all()
form = forms.ModuleBayTemplateCreateForm form = forms.ModuleBayTemplateCreateForm
model_form = forms.ModuleBayTemplateForm model_form = forms.ModuleBayTemplateForm
template_name = 'dcim/modulebaytemplate_create.html'
patterned_fields = ('name', 'label', 'position')
class ModuleBayTemplateEditView(generic.ObjectEditView): class ModuleBayTemplateEditView(generic.ObjectEditView):
@ -1409,9 +1392,8 @@ class ModuleBayTemplateBulkDeleteView(generic.BulkDeleteView):
class DeviceBayTemplateCreateView(generic.ComponentCreateView): class DeviceBayTemplateCreateView(generic.ComponentCreateView):
queryset = DeviceBayTemplate.objects.all() queryset = DeviceBayTemplate.objects.all()
form = forms.ComponentTemplateCreateForm form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm model_form = forms.DeviceBayTemplateForm
template_name = 'dcim/component_template_create.html'
class DeviceBayTemplateEditView(generic.ObjectEditView): class DeviceBayTemplateEditView(generic.ObjectEditView):
@ -1444,9 +1426,8 @@ class DeviceBayTemplateBulkDeleteView(generic.BulkDeleteView):
class InventoryItemTemplateCreateView(generic.ComponentCreateView): class InventoryItemTemplateCreateView(generic.ComponentCreateView):
queryset = InventoryItemTemplate.objects.all() queryset = InventoryItemTemplate.objects.all()
form = forms.ModularComponentTemplateCreateForm form = forms.InventoryItemTemplateCreateForm
model_form = forms.InventoryItemTemplateForm model_form = forms.InventoryItemTemplateForm
template_name = 'dcim/inventoryitemtemplate_create.html'
def alter_object(self, instance, request): def alter_object(self, instance, request):
# Set component (if any) # Set component (if any)
@ -1874,14 +1855,13 @@ class ConsolePortView(generic.ObjectView):
class ConsolePortCreateView(generic.ComponentCreateView): class ConsolePortCreateView(generic.ComponentCreateView):
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
form = forms.DeviceComponentCreateForm form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm model_form = forms.ConsolePortForm
class ConsolePortEditView(generic.ObjectEditView): class ConsolePortEditView(generic.ObjectEditView):
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
form = forms.ConsolePortForm form = forms.ConsolePortForm
template_name = 'dcim/device_component_edit.html'
class ConsolePortDeleteView(generic.ObjectDeleteView): class ConsolePortDeleteView(generic.ObjectDeleteView):
@ -1933,14 +1913,13 @@ class ConsoleServerPortView(generic.ObjectView):
class ConsoleServerPortCreateView(generic.ComponentCreateView): class ConsoleServerPortCreateView(generic.ComponentCreateView):
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
form = forms.DeviceComponentCreateForm form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm model_form = forms.ConsoleServerPortForm
class ConsoleServerPortEditView(generic.ObjectEditView): class ConsoleServerPortEditView(generic.ObjectEditView):
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
form = forms.ConsoleServerPortForm form = forms.ConsoleServerPortForm
template_name = 'dcim/device_component_edit.html'
class ConsoleServerPortDeleteView(generic.ObjectDeleteView): class ConsoleServerPortDeleteView(generic.ObjectDeleteView):
@ -1992,14 +1971,13 @@ class PowerPortView(generic.ObjectView):
class PowerPortCreateView(generic.ComponentCreateView): class PowerPortCreateView(generic.ComponentCreateView):
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
form = forms.DeviceComponentCreateForm form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm model_form = forms.PowerPortForm
class PowerPortEditView(generic.ObjectEditView): class PowerPortEditView(generic.ObjectEditView):
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
form = forms.PowerPortForm form = forms.PowerPortForm
template_name = 'dcim/device_component_edit.html'
class PowerPortDeleteView(generic.ObjectDeleteView): class PowerPortDeleteView(generic.ObjectDeleteView):
@ -2051,14 +2029,13 @@ class PowerOutletView(generic.ObjectView):
class PowerOutletCreateView(generic.ComponentCreateView): class PowerOutletCreateView(generic.ComponentCreateView):
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
form = forms.DeviceComponentCreateForm form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm model_form = forms.PowerOutletForm
class PowerOutletEditView(generic.ObjectEditView): class PowerOutletEditView(generic.ObjectEditView):
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
form = forms.PowerOutletForm form = forms.PowerOutletForm
template_name = 'dcim/device_component_edit.html'
class PowerOutletDeleteView(generic.ObjectDeleteView): class PowerOutletDeleteView(generic.ObjectDeleteView):
@ -2154,42 +2131,13 @@ class InterfaceView(generic.ObjectView):
class InterfaceCreateView(generic.ComponentCreateView): class InterfaceCreateView(generic.ComponentCreateView):
queryset = Interface.objects.all() queryset = Interface.objects.all()
form = forms.DeviceComponentCreateForm form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm 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): class InterfaceEditView(generic.ObjectEditView):
queryset = Interface.objects.all() queryset = Interface.objects.all()
form = forms.InterfaceForm form = forms.InterfaceForm
template_name = 'dcim/interface_edit.html'
class InterfaceDeleteView(generic.ObjectDeleteView): class InterfaceDeleteView(generic.ObjectDeleteView):
@ -2244,19 +2192,10 @@ class FrontPortCreateView(generic.ComponentCreateView):
form = forms.FrontPortCreateForm form = forms.FrontPortCreateForm
model_form = forms.FrontPortForm 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): class FrontPortEditView(generic.ObjectEditView):
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
form = forms.FrontPortForm form = forms.FrontPortForm
template_name = 'dcim/device_component_edit.html'
class FrontPortDeleteView(generic.ObjectDeleteView): class FrontPortDeleteView(generic.ObjectDeleteView):
@ -2308,14 +2247,13 @@ class RearPortView(generic.ObjectView):
class RearPortCreateView(generic.ComponentCreateView): class RearPortCreateView(generic.ComponentCreateView):
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
form = forms.DeviceComponentCreateForm form = forms.RearPortCreateForm
model_form = forms.RearPortForm model_form = forms.RearPortForm
class RearPortEditView(generic.ObjectEditView): class RearPortEditView(generic.ObjectEditView):
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
form = forms.RearPortForm form = forms.RearPortForm
template_name = 'dcim/device_component_edit.html'
class RearPortDeleteView(generic.ObjectDeleteView): class RearPortDeleteView(generic.ObjectDeleteView):
@ -2369,13 +2307,11 @@ class ModuleBayCreateView(generic.ComponentCreateView):
queryset = ModuleBay.objects.all() queryset = ModuleBay.objects.all()
form = forms.ModuleBayCreateForm form = forms.ModuleBayCreateForm
model_form = forms.ModuleBayForm model_form = forms.ModuleBayForm
patterned_fields = ('name', 'label', 'position')
class ModuleBayEditView(generic.ObjectEditView): class ModuleBayEditView(generic.ObjectEditView):
queryset = ModuleBay.objects.all() queryset = ModuleBay.objects.all()
form = forms.ModuleBayForm form = forms.ModuleBayForm
template_name = 'dcim/device_component_edit.html'
class ModuleBayDeleteView(generic.ObjectDeleteView): class ModuleBayDeleteView(generic.ObjectDeleteView):
@ -2423,14 +2359,13 @@ class DeviceBayView(generic.ObjectView):
class DeviceBayCreateView(generic.ComponentCreateView): class DeviceBayCreateView(generic.ComponentCreateView):
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()
form = forms.DeviceComponentCreateForm form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm model_form = forms.DeviceBayForm
class DeviceBayEditView(generic.ObjectEditView): class DeviceBayEditView(generic.ObjectEditView):
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()
form = forms.DeviceBayForm form = forms.DeviceBayForm
template_name = 'dcim/device_component_edit.html'
class DeviceBayDeleteView(generic.ObjectDeleteView): class DeviceBayDeleteView(generic.ObjectDeleteView):
@ -2552,7 +2487,6 @@ class InventoryItemCreateView(generic.ComponentCreateView):
queryset = InventoryItem.objects.all() queryset = InventoryItem.objects.all()
form = forms.InventoryItemCreateForm form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm model_form = forms.InventoryItemForm
template_name = 'dcim/inventoryitem_create.html'
def alter_object(self, instance, request): def alter_object(self, instance, request):
# Set component (if any) # Set component (if any)
@ -2736,7 +2670,6 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
filterset = filtersets.DeviceFilterSet filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable table = tables.DeviceTable
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
patterned_fields = ('name', 'label', 'position')
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):

View File

@ -159,7 +159,7 @@ class ReportViewSet(ViewSet):
# Read the PK as "<module>.<report>" # Read the PK as "<module>.<report>"
if '.' not in pk: if '.' not in pk:
raise Http404 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 # Raise a 404 on an invalid Report module/name
report = get_report(module_name, report_name) report = get_report(module_name, report_name)
@ -183,8 +183,8 @@ class ReportViewSet(ViewSet):
} }
# Iterate through all available Reports. # Iterate through all available Reports.
for module_name, reports in get_reports(): for module_name, reports in get_reports().items():
for report in reports: for report in reports.values():
# Attach the relevant JobResult (if any) to each Report. # Attach the relevant JobResult (if any) to each Report.
report.result = results.get(report.full_name, None) report.result = results.get(report.full_name, None)
@ -257,7 +257,7 @@ class ScriptViewSet(ViewSet):
lookup_value_regex = '[^/]+' # Allow dots lookup_value_regex = '[^/]+' # Allow dots
def _get_script(self, pk): 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) script = get_script(module_name, script_name)
if script is None: if script is None:
raise Http404 raise Http404

View File

@ -21,8 +21,8 @@ class Command(BaseCommand):
reports = get_reports() reports = get_reports()
# Run reports # Run reports
for module_name, report_list in reports: for module_name, report_list in reports.items():
for report in report_list: for report in report_list.values():
if module_name in options['reports'] or report.full_name in options['reports']: if module_name in options['reports'] or report.full_name in options['reports']:
# Run the report and create a new JobResult # Run the report and create a new JobResult

View File

@ -26,20 +26,18 @@ def get_report(module_name, report_name):
""" """
Return a specific report from within a module. 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) if module is None:
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
except FileNotFoundError:
return None return None
report = getattr(module, report_name, None) report = module.get(report_name)
if report is None: if report is None:
return None return None
return report() return report
def get_reports(): 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 # Iterate through all modules within the reports path. These are the user-created files in which reports are
# defined. # defined.
@ -61,7 +59,16 @@ def get_reports():
report_order = getattr(module, "report_order", ()) report_order = getattr(module, "report_order", ())
ordered_reports = [cls() for cls in report_order if is_report(cls)] 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] 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 return module_list

View File

@ -299,6 +299,10 @@ class BaseScript:
def module(cls): def module(cls):
return cls.__module__ return cls.__module__
@classmethod
def root_module(cls):
return cls.__module__.split(".")[0]
@classproperty @classproperty
def job_timeout(self): def job_timeout(self):
return getattr(self.Meta, 'job_timeout', None) 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)] 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] unordered_scripts = [cls for _, cls in inspect.getmembers(module, is_script) if cls not in script_order]
for cls in [*ordered_scripts, *unordered_scripts]: 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: if module_scripts:
scripts[module_name] = module_scripts scripts[module_name] = module_scripts

View File

@ -1,4 +1,4 @@
from django.urls import path from django.urls import path, re_path
from extras import models, views from extras import models, views
from netbox.views.generic import ObjectChangeLogView from netbox.views.generic import ObjectChangeLogView
@ -100,12 +100,12 @@ urlpatterns = [
# Reports # Reports
path('reports/', views.ReportListView.as_view(), name='report_list'), path('reports/', views.ReportListView.as_view(), name='report_list'),
path('reports/<str:module>.<str:name>/', views.ReportView.as_view(), name='report'),
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'), path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
re_path(r'^reports/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ReportView.as_view(), name='report'),
# Scripts # Scripts
path('scripts/', views.ScriptListView.as_view(), name='script_list'), path('scripts/', views.ScriptListView.as_view(), name='script_list'),
path('scripts/<str:module>.<str:name>/', views.ScriptView.as_view(), name='script'),
path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'), path('scripts/results/<int:job_result_pk>/', views.ScriptResultView.as_view(), name='script_result'),
re_path(r'^scripts/(?P<module>.([^.]+)).(?P<name>.(.+))/', views.ScriptView.as_view(), name='script'),
] ]

View File

@ -534,9 +534,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
} }
ret = [] ret = []
for module, report_list in reports:
for module, report_list in reports.items():
module_reports = [] module_reports = []
for report in report_list: for report in report_list.values():
report.result = results.get(report.full_name, None) report.result = results.get(report.full_name, None)
module_reports.append(report) module_reports.append(report)
ret.append((module, module_reports)) 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) 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 # 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 = get_report(module, report_name)
report.result = result report.result = result

View File

@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup # Environment setup
# #
VERSION = '3.3.3-dev' VERSION = '3.3.4-dev'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

View File

@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
from django.db.models import DateField, DateTimeField from django.db.models import DateField, DateTimeField
from django.template import Context, Template from django.template import Context, Template
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import escape_uri_path
from django.utils.html import escape from django.utils.html import escape
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -210,7 +211,7 @@ class ActionsColumn(tables.Column):
model = table.Meta.model model = table.Meta.model
request = getattr(table, 'context', {}).get('request') 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 = '' html = ''
# Compile actions menu # Compile actions menu

View File

@ -774,7 +774,6 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
model_form = None model_form = None
filterset = None filterset = None
table = None table = None
patterned_fields = ('name', 'label')
def get_required_permission(self): def get_required_permission(self):
return f'dcim.add_{self.queryset.model._meta.model_name}' return f'dcim.add_{self.queryset.model._meta.model_name}'
@ -804,23 +803,25 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
new_components = [] new_components = []
data = deepcopy(form.cleaned_data) data = deepcopy(form.cleaned_data)
replication_data = {
field: data.pop(field) for field in form.replication_fields
}
try: try:
with transaction.atomic(): with transaction.atomic():
for obj in data['pk']: 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): for i in range(pattern_count):
component_data = { component_data = {
self.parent_field: obj.pk 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) 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) component_form = self.model_form(component_data)
if component_form.is_valid(): if component_form.is_valid():
instance = component_form.save() instance = component_form.save()
@ -829,7 +830,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
else: else:
for field, errors in component_form.errors.as_data().items(): for field, errors in component_form.errors.as_data().items():
for e in errors: 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 # Enforce object-level permissions
if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components): if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components):

View File

@ -538,10 +538,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
""" """
Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine. 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 form = None
model_form = None model_form = None
patterned_fields = ('name', 'label')
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'add') return get_permission_for_model(self.queryset.model, 'add')
@ -549,44 +548,38 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
def alter_object(self, instance, request): def alter_object(self, instance, request):
return instance return instance
def initialize_forms(self, request): def initialize_form(self, request):
data = request.POST if request.method == 'POST' else None data = request.POST if request.method == 'POST' else None
initial_data = normalize_querydict(request.GET) initial_data = normalize_querydict(request.GET)
form = self.form(data=data, initial=request.GET) form = self.form(data=data, initial=initial_data)
model_form = self.model_form(data=data, initial=initial_data)
# These fields will be set from the pattern values return form
for field_name in self.patterned_fields:
model_form.fields[field_name].widget = HiddenInput()
return form, model_form
def get(self, request): def get(self, request):
form, model_form = self.initialize_forms(request) form = self.initialize_form(request)
instance = self.alter_object(self.queryset.model(), request) instance = self.alter_object(self.queryset.model(), request)
return render(request, self.template_name, { return render(request, self.template_name, {
'object': instance, 'object': instance,
'replication_form': form, 'form': form,
'form': model_form,
'return_url': self.get_return_url(request), 'return_url': self.get_return_url(request),
}) })
def post(self, request): def post(self, request):
logger = logging.getLogger('netbox.views.ComponentCreateView') 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) instance = self.alter_object(self.queryset.model(), request)
if form.is_valid(): if form.is_valid():
new_components = [] new_components = []
data = deepcopy(request.POST) 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 i in range(pattern_count):
for field_name in self.patterned_fields: for field_name in self.form.replication_fields:
if form.cleaned_data.get(f'{field_name}_pattern'): if form.cleaned_data.get(field_name):
data[field_name] = form.cleaned_data[f'{field_name}_pattern'][i] data[field_name] = form.cleaned_data[field_name][i]
if hasattr(form, 'get_iterative_data'): if hasattr(form, 'get_iterative_data'):
data.update(form.get_iterative_data(i)) data.update(form.get_iterative_data(i))
@ -626,7 +619,6 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
return render(request, self.template_name, { return render(request, self.template_name, {
'object': instance, 'object': instance,
'replication_form': form, 'form': form,
'form': model_form,
'return_url': self.get_return_url(request), 'return_url': self.get_return_url(request),
}) })

View File

@ -1,38 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load form_helpers %}
{% block form %}
{% if form.module_type %}
<div class="row mb-2">
<div class="offset-sm-3">
<ul class="nav nav-pills" role="tablist">
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="devicetype_tab" data-bs-toggle="tab" aria-controls="devicetype" data-bs-target="#devicetype" class="nav-link {% if not form.initial.module_type %}active{% endif %}">
Device Type
</button>
</li>
<li role="presentation" class="nav-item">
<button role="tab" type="button" id="moduletype_tab" data-bs-toggle="tab" aria-controls="moduletype" data-bs-target="#moduletype" class="nav-link {% if form.initial.module_type %}active{% endif %}">
Module Type
</button>
</li>
</ul>
</div>
</div>
<div class="tab-content p-0 border-0">
<div class="tab-pane {% if not form.initial.module_type %}active{% endif %}" id="devicetype" role="tabpanel">
{% render_field replication_form.device_type %}
</div>
<div class="tab-pane {% if form.initial.module_type %}active{% endif %}" id="moduletype" role="tabpanel">
{% render_field replication_form.module_type %}
</div>
</div>
{% 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 %}

View File

@ -1,16 +0,0 @@
{% extends 'generic/object_edit.html' %}
{% load form_helpers %}
{% block form %}
<div class="field-group mb-5">
{% if form.instance.device %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end">Device</label>
<div class="col">
<input class="form-control" value="{{ form.instance.device }}" disabled />
</div>
</div>
{% endif %}
{% render_form form %}
</div>
{% endblock form %}

View File

@ -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 %}

View File

@ -1,17 +0,0 @@
{% extends 'dcim/component_create.html' %}
{% load helpers %}
{% load form_helpers %}
{% block replication_fields %}
{{ block.super }}
{% if object.component %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end">
{{ object.component|meta:"verbose_name"|bettertitle }}
</label>
<div class="col">
<input class="form-control" value="{{ object.component }}" disabled />
</div>
</div>
{% endif %}
{% endblock replication_fields %}

View File

@ -1,17 +0,0 @@
{% extends 'dcim/component_template_create.html' %}
{% load helpers %}
{% load form_helpers %}
{% block replication_fields %}
{{ block.super }}
{% if object.component %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end">
{{ object.component|meta:"verbose_name"|bettertitle }}
</label>
<div class="col">
<input class="form-control" value="{{ object.component }}" disabled />
</div>
</div>
{% endif %}
{% endblock replication_fields %}

View File

@ -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 %}

View File

@ -34,7 +34,7 @@
{% for class_name, script in module_scripts.items %} {% for class_name, script in module_scripts.items %}
<tr> <tr>
<td> <td>
<a href="{% url 'extras:script' module=script.module name=class_name %}" name="script.{{ class_name }}">{{ script.name }}</a> <a href="{% url 'extras:script' module=script.root_module name=class_name %}" name="script.{{ class_name }}">{{ script.name }}</a>
</td> </td>
<td> <td>
{% include 'extras/inc/job_label.html' with result=script.result %} {% include 'extras/inc/job_label.html' with result=script.result %}

View File

@ -59,9 +59,11 @@ Context:
{# Render grouped fields according to Form #} {# Render grouped fields according to Form #}
{% for group, fields in form.fieldsets %} {% for group, fields in form.fieldsets %}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> {% if group %}
<h5 class="offset-sm-3">{{ group }}</h5> <div class="row mb-2">
</div> <h5 class="offset-sm-3">{{ group }}</h5>
</div>
{% endif %}
{% for name in fields %} {% for name in fields %}
{% with field=form|getfield:name %} {% with field=form|getfield:name %}
{% if not field.field.widget.is_hidden %} {% if not field.field.widget.is_hidden %}

View File

@ -46,4 +46,12 @@
</div> </div>
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -13,6 +13,16 @@
</div> </div>
{% endif %} {% endif %}
{# Login form errors #}
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Errors</h4>
<p>
{{ form.non_field_errors }}
</p>
</div>
{% endif %}
{# Login form #} {# Login form #}
<div class="form-login"> <div class="form-login">
<form action="{% url 'login' %}" method="post"> <form action="{% url 'login' %}" method="post">
@ -48,16 +58,6 @@
</h5> </h5>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{# Login form errors #}
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Errors</h4>
<p>
{{ form.non_field_errors }}
</p>
</div>
{% endif %}
</main> </main>
{# Page footer #} {# Page footer #}

View File

@ -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 %}
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Interface</h5>
</div>
{% if form.instance.virtual_machine %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end required" for="id_device">Virtual Machine</label>
<div class="col">
<input class="form-control" value="{{ form.instance.virtual_machine }}" disabled />
</div>
</div>
{% endif %}
{% render_field form.name %}
{% render_field form.description %}
{% render_field form.tags %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Addressing</h5>
</div>
{% render_field form.vrf %}
{% render_field form.mac_address %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Operation</h5>
</div>
{% render_field form.mtu %}
{% render_field form.enabled %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Related Interfaces</h5>
</div>
{% render_field form.parent %}
{% render_field form.bridge %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">802.1Q Switching</h5>
</div>
{% render_field form.mode %}
{% render_field form.vlan_group %}
{% render_field form.untagged_vlan %}
{% render_field form.tagged_vlans %}
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5>
</div>
{% render_custom_fields form %}
</div>
{% endif %}
{% endblock %}

View File

@ -47,20 +47,14 @@ class LoginView(View):
'url': f'{url}?{urlencode(params)}', 'url': f'{url}?{urlencode(params)}',
} }
def get(self, request): def get_auth_backends(self, request):
form = LoginForm(request)
if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login')
return self.redirect_to_next(request, logger)
auth_backends = [] auth_backends = []
saml_idps = get_saml_idps() saml_idps = get_saml_idps()
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys(): for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
url = reverse('social:begin', args=[name, ]) url = reverse('social:begin', args=[name])
params = {} params = {}
next = request.GET.get('next') if next := request.GET.get('next'):
if next:
params['next'] = next params['next'] = next
if name.lower() == 'saml' and saml_idps: if name.lower() == 'saml' and saml_idps:
for idp in saml_idps: for idp in saml_idps:
@ -71,9 +65,18 @@ class LoginView(View):
else: else:
auth_backends.append(self.gen_auth_data(name, url, params)) auth_backends.append(self.gen_auth_data(name, url, params))
return auth_backends
def get(self, request):
form = LoginForm(request)
if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login')
return self.redirect_to_next(request, logger)
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'auth_backends': auth_backends, 'auth_backends': self.get_auth_backends(request),
}) })
def post(self, request): def post(self, request):
@ -107,7 +110,7 @@ class LoginView(View):
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS), 'auth_backends': self.get_auth_backends(request),
}) })
def redirect_to_next(self, request, logger): def redirect_to_next(self, request, logger):

View File

@ -22,7 +22,7 @@ class ExpandableNameField(forms.CharField):
if not self.help_text: if not self.help_text:
self.help_text = """ self.help_text = """
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code> are not supported (example: <code>[ge,xe]-0/0/[0-9]</code>).
""" """
def to_python(self, value): def to_python(self, value):

View File

@ -466,6 +466,7 @@ class ViewTestCases:
""" """
bulk_create_count = 3 bulk_create_count = 3
bulk_create_data = {} bulk_create_data = {}
validation_excluded_fields = []
@override_settings(EXEMPT_VIEW_PERMISSIONS=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_create_multiple_objects_without_permission(self): def test_create_multiple_objects_without_permission(self):
@ -500,7 +501,7 @@ class ViewTestCases:
self.assertHttpStatus(response, 302) self.assertHttpStatus(response, 302)
self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count()) 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]: 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=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_create_multiple_objects_with_constrained_permission(self): def test_create_multiple_objects_with_constrained_permission(self):
@ -532,7 +533,7 @@ class ViewTestCases:
self.assertHttpStatus(response, 302) self.assertHttpStatus(response, 302)
self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count()) 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]: 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): class BulkImportObjectsViewTestCase(ModelViewTestCase):
""" """

View File

@ -13,7 +13,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
name_pattern = ExpandableNameField( name = ExpandableNameField(
label='Name' label='Name'
) )
@ -27,4 +27,4 @@ class VMInterfaceBulkCreateForm(
form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']), form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
VirtualMachineBulkAddComponentForm VirtualMachineBulkAddComponentForm
): ):
pass replication_fields = ('name',)

View File

@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError
from dcim.forms.common import InterfaceCommonForm from dcim.forms.common import InterfaceCommonForm
from dcim.forms.models import INTERFACE_MODE_HELP_TEXT from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup 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 ipam.models import IPAddress, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
@ -278,6 +277,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all()
)
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
required=False, required=False,
@ -338,7 +340,6 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
] ]
widgets = { widgets = {
'virtual_machine': forms.HiddenInput(),
'mode': StaticSelect() 'mode': StaticSelect()
} }
labels = { labels = {
@ -347,3 +348,10 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
help_texts = { help_texts = {
'mode': INTERFACE_MODE_HELP_TEXT, '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

View File

@ -1,17 +1,14 @@
from django import forms from utilities.forms import ExpandableNameField
from .models import VMInterfaceForm
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, ExpandableNameField
from .models import VirtualMachine
__all__ = ( __all__ = (
'VMInterfaceCreateForm', 'VMInterfaceCreateForm',
) )
class VMInterfaceCreateForm(BootstrapMixin, forms.Form): class VMInterfaceCreateForm(VMInterfaceForm):
virtual_machine = DynamicModelChoiceField( name = ExpandableNameField()
queryset=VirtualMachine.objects.all() replication_fields = ('name',)
)
name_pattern = ExpandableNameField( class Meta(VMInterfaceForm.Meta):
label='Name' exclude = ('name',)
)

View File

@ -251,6 +251,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = VMInterface model = VMInterface
validation_excluded_fields = ('name',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -290,10 +291,10 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'virtual_machine': virtualmachines[1].pk, 'virtual_machine': virtualmachines[0].pk,
'name': 'Interface X', 'name': 'Interface X',
'enabled': False, 'enabled': False,
'bridge': interfaces[3].pk, 'bridge': interfaces[1].pk,
'mac_address': EUI('01-02-03-04-05-06'), 'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 65000, 'mtu': 65000,
'description': 'New description', 'description': 'New description',
@ -306,7 +307,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
cls.bulk_create_data = { cls.bulk_create_data = {
'virtual_machine': virtualmachines[1].pk, 'virtual_machine': virtualmachines[1].pk,
'name_pattern': 'Interface [4-6]', 'name': 'Interface [4-6]',
'enabled': False, 'enabled': False,
'bridge': interfaces[3].pk, 'bridge': interfaces[3].pk,
'mac_address': EUI('01-02-03-04-05-06'), 'mac_address': EUI('01-02-03-04-05-06'),

View File

@ -451,13 +451,11 @@ class VMInterfaceCreateView(generic.ComponentCreateView):
queryset = VMInterface.objects.all() queryset = VMInterface.objects.all()
form = forms.VMInterfaceCreateForm form = forms.VMInterfaceCreateForm
model_form = forms.VMInterfaceForm model_form = forms.VMInterfaceForm
patterned_fields = ('name',)
class VMInterfaceEditView(generic.ObjectEditView): class VMInterfaceEditView(generic.ObjectEditView):
queryset = VMInterface.objects.all() queryset = VMInterface.objects.all()
form = forms.VMInterfaceForm form = forms.VMInterfaceForm
template_name = 'virtualization/vminterface_edit.html'
class VMInterfaceDeleteView(generic.ObjectDeleteView): class VMInterfaceDeleteView(generic.ObjectDeleteView):

View File

@ -19,13 +19,13 @@ graphene-django==2.15.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.1.2 Jinja2==3.1.2
Markdown==3.4.1 Markdown==3.4.1
mkdocs-material==8.4.2 mkdocs-material==8.5.1
mkdocstrings[python-legacy]==0.19.0 mkdocstrings[python-legacy]==0.19.0
netaddr==0.8.0 netaddr==0.8.0
Pillow==9.2.0 Pillow==9.2.0
psycopg2-binary==2.9.3 psycopg2-binary==2.9.3
PyYAML==6.0 PyYAML==6.0
sentry-sdk==1.9.7 sentry-sdk==1.9.8
social-auth-app-django==5.0.0 social-auth-app-django==5.0.0
social-auth-core==4.3.0 social-auth-core==4.3.0
svgwrite==1.4.3 svgwrite==1.4.3