Merge pull request #4098 from netbox-community/4086-device-component-urls

Closes #4086: Rename device component create/edit/delete URLs
This commit is contained in:
Jeremy Stretch 2020-02-05 16:03:14 -05:00 committed by GitHub
commit b1e78fa3c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 593 additions and 331 deletions

View File

@ -2180,6 +2180,10 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
class ConsolePortCreateForm(ComponentForm): class ConsolePortCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -2238,6 +2242,10 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
class ConsoleServerPortCreateForm(ComponentForm): class ConsoleServerPortCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -2331,6 +2339,10 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
class PowerPortCreateForm(ComponentForm): class PowerPortCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -2412,6 +2424,10 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
class PowerOutletCreateForm(ComponentForm): class PowerOutletCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -2437,11 +2453,13 @@ class PowerOutletCreateForm(ComponentForm):
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit power_port choices to those on the parent device # Limit power_port queryset to PowerPorts which belong to the parent Device
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
class PowerOutletCSVForm(forms.ModelForm): class PowerOutletCSVForm(forms.ModelForm):
@ -2499,6 +2517,10 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(PowerOutletTypeChoices), choices=add_blank_choice(PowerOutletTypeChoices),
required=False required=False
@ -2525,7 +2547,9 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit power_port queryset to PowerPorts which belong to the parent Device # Limit power_port queryset to PowerPorts which belong to the parent Device
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj) if 'device' in self.initial:
device = Device.objects.filter(pk=self.initial['device']).first()
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
class PowerOutletBulkRenameForm(BulkRenameForm): class PowerOutletBulkRenameForm(BulkRenameForm):
@ -2625,7 +2649,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
widget=StaticSelect2(), widget=StaticSelect2(),
) )
enabled = forms.BooleanField( enabled = forms.BooleanField(
required=False required=False,
initial=True
) )
lag = forms.ModelChoiceField( lag = forms.ModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
@ -2680,21 +2705,16 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {}).copy()
kwargs['initial'].update({'enabled': True})
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device (or its VC master) # Limit LAG choices to interfaces which belong to the parent device (or VC master)
if self.parent is not None: device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
self.fields['lag'].queryset = Interface.objects.filter( self.fields['lag'].queryset = Interface.objects.filter(
device__in=[self.parent, self.parent.get_vc_master()], device__in=[device, device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG type=InterfaceTypeChoices.TYPE_LAG
) )
else:
self.fields['lag'].queryset = Interface.objects.none()
class InterfaceCSVForm(forms.ModelForm): class InterfaceCSVForm(forms.ModelForm):
@ -2769,6 +2789,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
type = forms.ChoiceField( type = forms.ChoiceField(
choices=add_blank_choice(InterfaceTypeChoices), choices=add_blank_choice(InterfaceTypeChoices),
required=False, required=False,
@ -2836,14 +2860,12 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces which belong to the parent device (or VC master) # Limit LAG choices to interfaces which belong to the parent device (or VC master)
device = self.parent_obj if 'device' in self.initial:
if device is not None: device = Device.objects.filter(pk=self.initial['device']).first()
self.fields['lag'].queryset = Interface.objects.filter( self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()], device__in=[device, device.get_vc_master()],
type=InterfaceTypeChoices.TYPE_LAG type=InterfaceTypeChoices.TYPE_LAG
) )
else:
self.fields['lag'].choices = []
def clean(self): def clean(self):
@ -2909,6 +2931,10 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
class FrontPortCreateForm(ComponentForm): class FrontPortCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -2928,15 +2954,20 @@ class FrontPortCreateForm(ComponentForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings. device = Device.objects.get(
pk=self.initial.get('device') or self.data.get('device')
)
# Determine which rear port positions are occupied. These will be excluded from the list of available
# mappings.
occupied_port_positions = [ occupied_port_positions = [
(front_port.rear_port_id, front_port.rear_port_position) (front_port.rear_port_id, front_port.rear_port_position)
for front_port in self.parent.frontports.all() for front_port in device.frontports.all()
] ]
# Populate rear port choices # Populate rear port choices
choices = [] choices = []
rear_ports = RearPort.objects.filter(device=self.parent) rear_ports = RearPort.objects.filter(device=device)
for rear_port in rear_ports: for rear_port in rear_ports:
for i in range(1, rear_port.positions + 1): for i in range(1, rear_port.positions + 1):
if (rear_port.pk, i) not in occupied_port_positions: if (rear_port.pk, i) not in occupied_port_positions:
@ -3076,6 +3107,10 @@ class RearPortForm(BootstrapMixin, forms.ModelForm):
class RearPortCreateForm(ComponentForm): class RearPortCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -3680,6 +3715,10 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
class DeviceBayCreateForm(ComponentForm): class DeviceBayCreateForm(ComponentForm):
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )

View File

@ -682,11 +682,11 @@ class ConsolePortTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
test_create_object = None
test_bulk_edit_objects = None test_bulk_edit_objects = None
# TODO def test_bulk_create_objects(self):
test_create_object = None return self._test_bulk_create_objects(expected_count=3)
test_bulk_delete_objects = None
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -704,11 +704,14 @@ class ConsolePortTestCase(StandardTestCases.Views):
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port', 'description': 'A console port',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'cable': None, 'device': device.pk,
'connected_endpoint': None, 'name_pattern': 'Console Port [4-6]',
'connection_status': None, 'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port',
'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = ( cls.csv_data = (
@ -724,11 +727,10 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
# TODO
test_create_object = None test_create_object = None
test_bulk_edit_objects = None
test_bulk_delete_objects = None def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -746,10 +748,20 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
'type': ConsolePortTypeChoices.TYPE_RJ45, 'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console server port', 'description': 'A console server port',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'cable': None, 'device': device.pk,
'connection_status': None, 'name_pattern': 'Console Server Port [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console server port',
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_edit_data = {
'device': device.pk,
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'New description',
} }
cls.csv_data = ( cls.csv_data = (
@ -766,10 +778,10 @@ class PowerPortTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
test_bulk_edit_objects = None test_bulk_edit_objects = None
# TODO
test_create_object = None test_create_object = None
test_bulk_delete_objects = None
def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -789,10 +801,16 @@ class PowerPortTestCase(StandardTestCases.Views):
'allocated_draw': 50, 'allocated_draw': 50,
'description': 'A power port', 'description': 'A power port',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'cable': None, 'device': device.pk,
'connection_status': None, 'name_pattern': 'Power Port [4-6]]',
'type': PowerPortTypeChoices.TYPE_IEC_C14,
'maximum_draw': 100,
'allocated_draw': 50,
'description': 'A power port',
'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = ( cls.csv_data = (
@ -808,11 +826,10 @@ class PowerOutletTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
# TODO
test_create_object = None test_create_object = None
test_bulk_edit_objects = None
test_bulk_delete_objects = None def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -838,10 +855,24 @@ class PowerOutletTestCase(StandardTestCases.Views):
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B, 'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
'description': 'A power outlet', 'description': 'A power outlet',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'cable': None, 'device': device.pk,
'connection_status': None, 'name_pattern': 'Power Outlet [4-6]',
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
'power_port': powerports[1].pk,
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
'description': 'A power outlet',
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_edit_data = {
'device': device.pk,
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
'power_port': powerports[1].pk,
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
'description': 'New description',
} }
cls.csv_data = ( cls.csv_data = (
@ -855,20 +886,23 @@ class PowerOutletTestCase(StandardTestCases.Views):
class InterfaceTestCase(StandardTestCases.Views): class InterfaceTestCase(StandardTestCases.Views):
model = Interface model = Interface
# TODO # Disable inapplicable views
test_create_object = None test_create_object = None
test_bulk_edit_objects = None
test_bulk_delete_objects = None def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
device = create_test_device('Device 1') device = create_test_device('Device 1')
Interface.objects.bulk_create([ interfaces = (
Interface(device=device, name='Interface 1'), Interface(device=device, name='Interface 1'),
Interface(device=device, name='Interface 2'), Interface(device=device, name='Interface 2'),
Interface(device=device, name='Interface 3'), Interface(device=device, name='Interface 3'),
]) Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG),
)
Interface.objects.bulk_create(interfaces)
vlans = ( vlans = (
VLAN(vid=1, name='VLAN1', site=device.site), VLAN(vid=1, name='VLAN1', site=device.site),
@ -884,7 +918,38 @@ class InterfaceTestCase(StandardTestCases.Views):
'name': 'Interface X', 'name': 'Interface X',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC, 'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False, 'enabled': False,
'lag': None, 'lag': interfaces[3].pk,
'mac_address': EUI('01:02:03:04:05:06'),
'mtu': 2000,
'mgmt_only': True,
'description': 'A front port',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]],
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Interface [4-6]',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False,
'lag': interfaces[3].pk,
'mac_address': EUI('01:02:03:04:05:06'),
'mtu': 2000,
'mgmt_only': True,
'description': 'A front port',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]],
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_edit_data = {
'device': device.pk,
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'enabled': False,
'lag': interfaces[3].pk,
'mac_address': EUI('01:02:03:04:05:06'), 'mac_address': EUI('01:02:03:04:05:06'),
'mtu': 2000, 'mtu': 2000,
'mgmt_only': True, 'mgmt_only': True,
@ -892,11 +957,6 @@ class InterfaceTestCase(StandardTestCases.Views):
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk, 'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]], 'tagged_vlans': [v.pk for v in vlans[1:4]],
'tags': 'Alpha,Bravo,Charlie',
# Extraneous model fields
'cable': None,
'connection_status': None,
} }
cls.csv_data = ( cls.csv_data = (
@ -912,11 +972,10 @@ class FrontPortTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
# TODO
test_create_object = None test_create_object = None
test_bulk_edit_objects = None
test_bulk_delete_objects = None def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -946,9 +1005,22 @@ class FrontPortTestCase(StandardTestCases.Views):
'rear_port_position': 1, 'rear_port_position': 1,
'description': 'New description', 'description': 'New description',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'cable': None, 'device': device.pk,
'name_pattern': 'Front Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'rear_port_set': [
'{}:1'.format(rp.pk) for rp in rearports[3:6]
],
'description': 'New description',
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_edit_data = {
'type': PortTypeChoices.TYPE_8P8C,
'description': 'New description',
} }
cls.csv_data = ( cls.csv_data = (
@ -964,11 +1036,10 @@ class RearPortTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
# TODO
test_create_object = None test_create_object = None
test_bulk_edit_objects = None
test_bulk_delete_objects = None def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -985,11 +1056,22 @@ class RearPortTestCase(StandardTestCases.Views):
'name': 'Rear Port X', 'name': 'Rear Port X',
'type': PortTypeChoices.TYPE_8P8C, 'type': PortTypeChoices.TYPE_8P8C,
'positions': 3, 'positions': 3,
'description': 'New description', 'description': 'A rear port',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'cable': None, 'device': device.pk,
'name_pattern': 'Rear Port [4-6]',
'type': PortTypeChoices.TYPE_8P8C,
'positions': 3,
'description': 'A rear port',
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_edit_data = {
'type': PortTypeChoices.TYPE_8P8C,
'description': 'New description',
} }
cls.csv_data = ( cls.csv_data = (
@ -1005,11 +1087,13 @@ class DeviceBayTestCase(StandardTestCases.Views):
# Disable inapplicable views # Disable inapplicable views
test_get_object = None test_get_object = None
test_create_object = None
# TODO # TODO
test_create_object = None
test_bulk_edit_objects = None test_bulk_edit_objects = None
test_bulk_delete_objects = None
def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1030,9 +1114,13 @@ class DeviceBayTestCase(StandardTestCases.Views):
'name': 'Device Bay X', 'name': 'Device Bay X',
'description': 'A device bay', 'description': 'A device bay',
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields cls.bulk_create_data = {
'installed_device': None, 'device': device2.pk,
'name_pattern': 'Device Bay [4-6]',
'description': 'A device bay',
'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = ( cls.csv_data = (
@ -1076,13 +1164,6 @@ class InventoryItemTestCase(StandardTestCases.Views):
'tags': 'Alpha,Bravo,Charlie', 'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = (
"device,name",
"Device 1,Inventory Item 4",
"Device 1,Inventory Item 5",
"Device 1,Inventory Item 6",
)
cls.bulk_edit_data = { cls.bulk_edit_data = {
'device': device.pk, 'device': device.pk,
'manufacturer': manufacturer.pk, 'manufacturer': manufacturer.pk,
@ -1090,6 +1171,13 @@ class InventoryItemTestCase(StandardTestCases.Views):
'description': 'New description', 'description': 'New description',
} }
cls.csv_data = (
"device,name",
"Device 1,Inventory Item 4",
"Device 1,Inventory Item 5",
"Device 1,Inventory Item 6",
)
class CableTestCase(StandardTestCases.Views): class CableTestCase(StandardTestCases.Views):
model = Cable model = Cable

View File

@ -169,109 +169,112 @@ urlpatterns = [
# Console ports # Console ports
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
path(r'console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
# TODO: Bulk edit view for ConsolePorts
path(r'console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
# Console server ports # Console server ports
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'), path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
path(r'console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
# Power ports # Power ports
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'), path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
path(r'power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
# TODO: Bulk edit view for PowerPorts
path(r'power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
# Power outlets # Power outlets
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'), path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
path(r'power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
# Interfaces # Interfaces
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'), path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
path(r'interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'), path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path(r'interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path(r'interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
# Front ports # Front ports
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'), path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
path(r'front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
# Rear ports # Rear ports
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'), path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
path(r'rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
# Device bays # Device bays
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# TODO: Bulk edit view for DeviceBays
path(r'device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# Inventory items # Inventory items
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
@ -280,6 +283,7 @@ urlpatterns = [
path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
path(r'inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), path(r'inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
path(r'inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), path(r'inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
# TODO: Replace below with InventoryItemCreateView
path(r'devices/<int:device>/inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), path(r'devices/<int:device>/inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
# Cables # Cables

View File

@ -1205,8 +1205,6 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport' permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
model = ConsolePort model = ConsolePort
form = forms.ConsolePortCreateForm form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm model_form = forms.ConsolePortForm
@ -1234,8 +1232,8 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport' permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
parent_model = Device
table = tables.ConsolePortTable table = tables.ConsolePortTable
default_return_url = 'dcim:consoleport_list'
# #
@ -1253,8 +1251,6 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport' permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
model = ConsoleServerPort model = ConsoleServerPort
form = forms.ConsoleServerPortCreateForm form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm model_form = forms.ConsoleServerPortForm
@ -1282,7 +1278,6 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport' permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
parent_model = Device
table = tables.ConsoleServerPortTable table = tables.ConsoleServerPortTable
form = forms.ConsoleServerPortBulkEditForm form = forms.ConsoleServerPortBulkEditForm
@ -1302,8 +1297,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport' permission_required = 'dcim.delete_consoleserverport'
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
parent_model = Device
table = tables.ConsoleServerPortTable table = tables.ConsoleServerPortTable
default_return_url = 'dcim:consoleserverport_list'
# #
@ -1321,8 +1316,6 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport' permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
model = PowerPort model = PowerPort
form = forms.PowerPortCreateForm form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm model_form = forms.PowerPortForm
@ -1350,8 +1343,8 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport' permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
parent_model = Device
table = tables.PowerPortTable table = tables.PowerPortTable
default_return_url = 'dcim:powerport_list'
# #
@ -1369,8 +1362,6 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet' permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
model = PowerOutlet model = PowerOutlet
form = forms.PowerOutletCreateForm form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm model_form = forms.PowerOutletForm
@ -1398,7 +1389,6 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet' permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
parent_model = Device
table = tables.PowerOutletTable table = tables.PowerOutletTable
form = forms.PowerOutletBulkEditForm form = forms.PowerOutletBulkEditForm
@ -1418,8 +1408,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet' permission_required = 'dcim.delete_poweroutlet'
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
parent_model = Device
table = tables.PowerOutletTable table = tables.PowerOutletTable
default_return_url = 'dcim:poweroutlet_list'
# #
@ -1473,8 +1463,6 @@ class InterfaceView(PermissionRequiredMixin, View):
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface' permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
model = Interface model = Interface
form = forms.InterfaceCreateForm form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm model_form = forms.InterfaceForm
@ -1503,7 +1491,6 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
parent_model = Device
table = tables.InterfaceTable table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm form = forms.InterfaceBulkEditForm
@ -1523,8 +1510,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface' permission_required = 'dcim.delete_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
parent_model = Device
table = tables.InterfaceTable table = tables.InterfaceTable
default_return_url = 'dcim:interface_list'
# #
@ -1542,8 +1529,6 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontport' permission_required = 'dcim.add_frontport'
parent_model = Device
parent_field = 'device'
model = FrontPort model = FrontPort
form = forms.FrontPortCreateForm form = forms.FrontPortCreateForm
model_form = forms.FrontPortForm model_form = forms.FrontPortForm
@ -1571,7 +1556,6 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport' permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable table = tables.FrontPortTable
form = forms.FrontPortBulkEditForm form = forms.FrontPortBulkEditForm
@ -1591,8 +1575,8 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_frontport' permission_required = 'dcim.delete_frontport'
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable table = tables.FrontPortTable
default_return_url = 'dcim:frontport_list'
# #
@ -1610,8 +1594,6 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearport' permission_required = 'dcim.add_rearport'
parent_model = Device
parent_field = 'device'
model = RearPort model = RearPort
form = forms.RearPortCreateForm form = forms.RearPortCreateForm
model_form = forms.RearPortForm model_form = forms.RearPortForm
@ -1639,7 +1621,6 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport' permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable table = tables.RearPortTable
form = forms.RearPortBulkEditForm form = forms.RearPortBulkEditForm
@ -1659,8 +1640,8 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rearport' permission_required = 'dcim.delete_rearport'
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable table = tables.RearPortTable
default_return_url = 'dcim:rearport_list'
# #
@ -1680,8 +1661,6 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay' permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'
model = DeviceBay model = DeviceBay
form = forms.DeviceBayCreateForm form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm model_form = forms.DeviceBayForm
@ -1784,8 +1763,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay' permission_required = 'dcim.delete_devicebay'
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()
parent_model = Device
table = tables.DeviceBayTable table = tables.DeviceBayTable
default_return_url = 'dcim:devicebay_list'
# #

View File

@ -48,14 +48,30 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:consoleport_add' pk=device.pk %}">Console Ports</a></li>{% endif %} {% if perms.dcim.add_consoleport %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}">Console Server Ports</a></li>{% endif %} <li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Ports</a></li>
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:powerport_add' pk=device.pk %}">Power Ports</a></li>{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}">Power Outlets</a></li>{% endif %} {% if perms.dcim.add_consoleserverport %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:interface_add' pk=device.pk %}">Interfaces</a></li>{% endif %} <li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Server Ports</a></li>
{% if perms.dcim.add_frontport %}<li><a href="{% url 'dcim:frontport_add' pk=device.pk %}">Front Ports</a></li>{% endif %} {% endif %}
{% if perms.dcim.add_rearport %}<li><a href="{% url 'dcim:rearport_add' pk=device.pk %}">Rear Ports</a></li>{% endif %} {% if perms.dcim.add_powerport %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:devicebay_add' pk=device.pk %}">Device Bays</a></li>{% endif %} <li><a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Ports</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
<li><a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Outlets</a></li>
{% endif %}
{% if perms.dcim.add_interface %}
<li><a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Interfaces</a></li>
{% endif %}
{% if perms.dcim.add_frontport %}
<li><a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Front Ports</a></li>
{% endif %}
{% if perms.dcim.add_rearport %}
<li><a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Rear Ports</a></li>
{% endif %}
{% if perms.dcim.add_devicebay %}
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -333,12 +349,12 @@
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right noprint"> <div class="panel-footer text-right noprint">
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.add_powerport %} {% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a> </a>
{% endif %} {% endif %}
@ -524,13 +540,13 @@
</button> </button>
{% endif %} {% endif %}
{% if device_bays and perms.dcim.delete_devicebay %} {% if device_bays and perms.dcim.delete_devicebay %}
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
</a> </a>
</div> </div>
@ -587,7 +603,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
{% endif %} {% endif %}
@ -597,13 +613,13 @@
</button> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interface %} {% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
</a> </a>
</div> </div>
@ -619,6 +635,7 @@
{% if perms.dcim.delete_consoleserverport %} {% if perms.dcim.delete_consoleserverport %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@ -649,7 +666,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -657,13 +674,13 @@
</button> </button>
{% endif %} {% endif %}
{% if consoleserverports and perms.dcim.delete_consoleserverport %} {% if consoleserverports and perms.dcim.delete_consoleserverport %}
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
</a> </a>
</div> </div>
@ -679,6 +696,7 @@
{% if perms.dcim.delete_poweroutlet %} {% if perms.dcim.delete_poweroutlet %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@ -710,7 +728,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -718,13 +736,13 @@
</button> </button>
{% endif %} {% endif %}
{% if poweroutlets and perms.dcim.delete_poweroutlet %} {% if poweroutlets and perms.dcim.delete_poweroutlet %}
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
</a> </a>
</div> </div>
@ -739,6 +757,7 @@
{% if front_ports %} {% if front_ports %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Front Ports</strong> <strong>Front Ports</strong>
@ -770,7 +789,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -778,13 +797,13 @@
</button> </button>
{% endif %} {% endif %}
{% if front_ports and perms.dcim.delete_frontport %} {% if front_ports and perms.dcim.delete_frontport %}
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_frontport %} {% if perms.dcim.add_frontport %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:frontport_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
</a> </a>
</div> </div>
@ -797,6 +816,7 @@
{% if rear_ports %} {% if rear_ports %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Rear Ports</strong> <strong>Rear Ports</strong>
@ -827,7 +847,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -835,13 +855,13 @@
</button> </button>
{% endif %} {% endif %}
{% if rear_ports and perms.dcim.delete_rearport %} {% if rear_ports and perms.dcim.delete_rearport %}
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_rearport %} {% if perms.dcim.add_rearport %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:rearport_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
</a> </a>
</div> </div>

View File

@ -1,14 +1,11 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %} {% block title %}Create {{ component_type }}{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="" method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %} {% if form.non_field_errors %}
@ -27,7 +24,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label required">Device</label> <label class="col-md-3 control-label required">Device</label>
<div class="col-md-9"> <div class="col-md-9">
<p class="form-control-static">{{ parent }}</p> <p class="form-control-static">{{ form.device.value }}</p>
</div> </div>
</div> </div>
{% render_form form %} {% render_form form %}

View File

@ -288,18 +288,18 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interface %} {% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'virtualization:interface_add' pk=virtualmachine.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'virtualization:interface_add' %}?virtual_machine={{ virtualmachine.pk }}&return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
</a> </a>
</div> </div>

View File

@ -5,7 +5,7 @@
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %} {% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="" method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">

View File

@ -727,14 +727,11 @@ class ConfirmationForm(BootstrapMixin, ReturnURLForm):
confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True) confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True)
# TODO: Remove ComponentForm
class ComponentForm(BootstrapMixin, forms.Form): class ComponentForm(BootstrapMixin, forms.Form):
""" """
Allow inclusion of the parent Device/VirtualMachine as context for limiting field choices. Allow inclusion of the parent Device/VirtualMachine as context for limiting field choices.
""" """
def __init__(self, parent, *args, **kwargs):
self.parent = parent
super().__init__(*args, **kwargs)
def get_iterative_data(self, iteration): def get_iterative_data(self, iteration):
return {} return {}
@ -743,10 +740,9 @@ class BulkEditForm(forms.Form):
""" """
Base form for editing multiple objects in bulk Base form for editing multiple objects in bulk
""" """
def __init__(self, model, parent_obj=None, *args, **kwargs): def __init__(self, model, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.model = model self.model = model
self.parent_obj = parent_obj
self.nullable_fields = [] self.nullable_fields = []
# Copy any nullable fields defined in Meta # Copy any nullable fields defined in Meta

View File

@ -1,11 +1,12 @@
from django.contrib.auth.models import Permission, User from django.contrib.auth.models import Permission, User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.forms.models import model_to_dict
from django.test import Client, TestCase as _TestCase, override_settings from django.test import Client, TestCase as _TestCase, override_settings
from django.urls import reverse, NoReverseMatch from django.urls import reverse, NoReverseMatch
from rest_framework.test import APIClient from rest_framework.test import APIClient
from users.models import Token from users.models import Token
from .utils import disable_warnings, model_to_dict, post_data from .utils import disable_warnings, post_data
class TestCase(_TestCase): class TestCase(_TestCase):
@ -56,6 +57,30 @@ class TestCase(_TestCase):
expected_status, response.status_code, getattr(response, 'data', 'No data') expected_status, response.status_code, getattr(response, 'data', 'No data')
)) ))
def assertInstanceEqual(self, instance, data):
"""
Compare a model instance to a dictionary, checking that its attribute values match those specified
in the dictionary.
"""
model_dict = model_to_dict(instance, fields=data.keys())
for key in list(model_dict.keys()):
# TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
if key == 'tags':
model_dict[key] = ','.join(sorted([tag.name for tag in model_dict['tags']]))
# Convert ManyToManyField to list of instance PKs
elif model_dict[key] and type(model_dict[key]) in (list, tuple) and hasattr(model_dict[key][0], 'pk'):
model_dict[key] = [obj.pk for obj in model_dict[key]]
# Omit any dictionary keys which are not instance attributes
relevant_data = {
k: v for k, v in data.items() if hasattr(instance, k)
}
self.assertDictEqual(model_dict, relevant_data)
class APITestCase(TestCase): class APITestCase(TestCase):
client_class = APIClient client_class = APIClient
@ -92,6 +117,9 @@ class StandardTestCases:
# CSV lines used for bulk import of new objects # CSV lines used for bulk import of new objects
csv_data = () csv_data = ()
# Form data used when creating multiple objects
bulk_create_data = {}
# Form data to be used when editing multiple objects at once # Form data to be used when editing multiple objects at once
bulk_edit_data = {} bulk_edit_data = {}
@ -104,15 +132,26 @@ class StandardTestCases:
if self.model is None: if self.model is None:
raise Exception("Test case requires model to be defined") raise Exception("Test case requires model to be defined")
#
# URL functions
#
def _get_base_url(self):
"""
Return the base format for a URL for the test's model. Override this to test for a model which belongs
to a different app (e.g. testing Interfaces within the virtualization app).
"""
return '{}:{}_{{}}'.format(
self.model._meta.app_label,
self.model._meta.model_name
)
def _get_url(self, action, instance=None): def _get_url(self, action, instance=None):
""" """
Return the URL name for a specific action. An instance must be specified for Return the URL name for a specific action. An instance must be specified for
get/edit/delete views. get/edit/delete views.
""" """
url_format = '{}:{}_{{}}'.format( url_format = self._get_base_url()
self.model._meta.app_label,
self.model._meta.model_name
)
if action in ('list', 'add', 'import', 'bulk_edit', 'bulk_delete'): if action in ('list', 'add', 'import', 'bulk_edit', 'bulk_delete'):
return reverse(url_format.format(action)) return reverse(url_format.format(action))
@ -131,6 +170,13 @@ class StandardTestCases:
else: else:
raise Exception("Invalid action for URL resolution: {}".format(action)) raise Exception("Invalid action for URL resolution: {}".format(action))
#
# Standard view tests
# These methods will run by default. To disable a test, nullify its method on the subclasses TestCase:
#
# test_list_objects = None
#
@override_settings(EXEMPT_VIEW_PERMISSIONS=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_list_objects(self): def test_list_objects(self):
# Attempt to make the request without required permissions # Attempt to make the request without required permissions
@ -187,7 +233,7 @@ class StandardTestCases:
self.assertEqual(initial_count + 1, self.model.objects.count()) self.assertEqual(initial_count + 1, self.model.objects.count())
instance = self.model.objects.order_by('-pk').first() instance = self.model.objects.order_by('-pk').first()
self.assertDictEqual(model_to_dict(instance), self.form_data) self.assertInstanceEqual(instance, self.form_data)
@override_settings(EXEMPT_VIEW_PERMISSIONS=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_edit_object(self): def test_edit_object(self):
@ -211,7 +257,7 @@ class StandardTestCases:
self.assertHttpStatus(response, 302) self.assertHttpStatus(response, 302)
instance = self.model.objects.get(pk=instance.pk) instance = self.model.objects.get(pk=instance.pk)
self.assertDictEqual(model_to_dict(instance), self.form_data) self.assertInstanceEqual(instance, self.form_data)
@override_settings(EXEMPT_VIEW_PERMISSIONS=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_delete_object(self): def test_delete_object(self):
@ -263,7 +309,8 @@ class StandardTestCases:
@override_settings(EXEMPT_VIEW_PERMISSIONS=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_bulk_edit_objects(self): def test_bulk_edit_objects(self):
pk_list = self.model.objects.values_list('pk', flat=True) # Bulk edit the first three objects only
pk_list = self.model.objects.values_list('pk', flat=True)[:3]
request = { request = {
'path': self._get_url('bulk_edit'), 'path': self._get_url('bulk_edit'),
@ -288,13 +335,8 @@ class StandardTestCases:
response = self.client.post(**request) response = self.client.post(**request)
self.assertHttpStatus(response, 302) self.assertHttpStatus(response, 302)
bulk_edit_fields = self.bulk_edit_data.keys()
for i, instance in enumerate(self.model.objects.filter(pk__in=pk_list)): for i, instance in enumerate(self.model.objects.filter(pk__in=pk_list)):
self.assertDictEqual( self.assertInstanceEqual(instance, self.bulk_edit_data)
model_to_dict(instance, fields=bulk_edit_fields),
self.bulk_edit_data,
msg="Instance {} failed to validate after bulk edit: {}".format(i, instance)
)
@override_settings(EXEMPT_VIEW_PERMISSIONS=[]) @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_bulk_delete_objects(self): def test_bulk_delete_objects(self):
@ -323,3 +365,32 @@ class StandardTestCases:
# Check that all objects were deleted # Check that all objects were deleted
self.assertEqual(self.model.objects.count(), 0) self.assertEqual(self.model.objects.count(), 0)
#
# Optional view tests
# These methods will run only if the required data
#
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def _test_bulk_create_objects(self, expected_count):
initial_count = self.model.objects.count()
request = {
'path': self._get_url('add'),
'data': post_data(self.bulk_create_data),
'follow': False, # Do not follow 302 redirects
}
# Attempt to make the request without required permissions
with disable_warnings('django.request'):
self.assertHttpStatus(self.client.post(**request), 403)
# Assign the required permission and submit again
self.add_permissions(
'{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
)
response = self.client.post(**request)
self.assertHttpStatus(response, 302)
self.assertEqual(initial_count + expected_count, self.model.objects.count())
for instance in self.model.objects.order_by('-pk')[:expected_count]:
self.assertInstanceEqual(instance, self.bulk_create_data)

View File

@ -2,35 +2,6 @@ import logging
from contextlib import contextmanager from contextlib import contextmanager
from django.contrib.auth.models import Permission, User from django.contrib.auth.models import Permission, User
from django.forms.models import model_to_dict as _model_to_dict
def model_to_dict(instance, fields=None, exclude=None):
"""
Customized wrapper for Django's built-in model_to_dict(). Does the following:
- Excludes the instance ID field
- Exclude any fields prepended with an underscore
- Convert any assigned tags to a comma-separated string
"""
_exclude = ['id']
if exclude is not None:
_exclude += exclude
model_dict = _model_to_dict(instance, fields=fields, exclude=_exclude)
for key in list(model_dict.keys()):
if key.startswith('_'):
del model_dict[key]
# TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
elif key == 'tags':
model_dict[key] = ','.join(sorted([tag.name for tag in model_dict['tags']]))
# Convert ManyToManyField to list of instance PKs
elif model_dict[key] and type(model_dict[key]) in (list, tuple) and hasattr(model_dict[key][0], 'pk'):
model_dict[key] = [obj.pk for obj in model_dict[key]]
return model_dict
def post_data(data): def post_data(data):

View File

@ -4,6 +4,7 @@ from collections import OrderedDict
from django.core.serializers import serialize from django.core.serializers import serialize
from django.db.models import Count, OuterRef, Subquery from django.db.models import Count, OuterRef, Subquery
from django.http import QueryDict
from jinja2 import Environment from jinja2 import Environment
from dcim.choices import CableLengthUnitChoices from dcim.choices import CableLengthUnitChoices
@ -209,3 +210,15 @@ def prepare_cloned_fields(instance):
) )
return param_string return param_string
def querydict_to_dict(querydict):
"""
Convert a django.http.QueryDict object to a regular Python dictionary, preserving lists of multiple values.
(QueryDict.dict() will return only the last value in a list for each key.)
"""
assert isinstance(querydict, QueryDict)
return {
key: querydict.get(key) if len(value) == 1 else querydict.getlist(key)
for key, value in querydict.lists()
}

View File

@ -25,7 +25,7 @@ from extras.models import CustomField, CustomFieldValue, ExportTemplate
from extras.querysets import CustomFieldQueryset from extras.querysets import CustomFieldQueryset
from utilities.exceptions import AbortTransaction from utilities.exceptions import AbortTransaction
from utilities.forms import BootstrapMixin, CSVDataField from utilities.forms import BootstrapMixin, CSVDataField
from utilities.utils import csv_format, prepare_cloned_fields from utilities.utils import csv_format, prepare_cloned_fields, querydict_to_dict
from .error_handlers import handle_protectederror from .error_handlers import handle_protectederror
from .forms import ConfirmationForm, ImportForm from .forms import ConfirmationForm, ImportForm
from .paginator import EnhancedPaginator from .paginator import EnhancedPaginator
@ -604,14 +604,12 @@ class BulkEditView(GetReturnURLMixin, View):
Edit objects in bulk. Edit objects in bulk.
queryset: Custom queryset to use when retrieving objects (e.g. to select related objects) queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
parent_model: The model of the parent object (if any)
filter: FilterSet to apply when deleting by QuerySet filter: FilterSet to apply when deleting by QuerySet
table: The table used to display devices being edited table: The table used to display devices being edited
form: The form class used to edit objects in bulk form: The form class used to edit objects in bulk
template_name: The name of the template template_name: The name of the template
""" """
queryset = None queryset = None
parent_model = None
filterset = None filterset = None
table = None table = None
form = None form = None
@ -624,20 +622,15 @@ class BulkEditView(GetReturnURLMixin, View):
model = self.queryset.model model = self.queryset.model
# Attempt to derive parent object if a parent class has been given # Create a mutable copy of the POST data
if self.parent_model: post_data = request.POST.copy()
parent_obj = get_object_or_404(self.parent_model, **kwargs)
else:
parent_obj = None
# Are we editing *all* objects in the queryset or just a selected subset? # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
if request.POST.get('_all') and self.filterset is not None: if post_data.get('_all') and self.filterset is not None:
pk_list = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs] post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
if '_apply' in request.POST: if '_apply' in request.POST:
form = self.form(model, parent_obj, request.POST) form = self.form(model, request.POST, initial=request.GET)
if form.is_valid(): if form.is_valid():
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else [] custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
@ -651,7 +644,7 @@ class BulkEditView(GetReturnURLMixin, View):
with transaction.atomic(): with transaction.atomic():
updated_count = 0 updated_count = 0
for obj in model.objects.filter(pk__in=pk_list): for obj in model.objects.filter(pk__in=form.cleaned_data['pk']):
# Update standard fields. If a field is listed in _nullify, delete its value. # Update standard fields. If a field is listed in _nullify, delete its value.
for name in standard_fields: for name in standard_fields:
@ -719,12 +712,16 @@ class BulkEditView(GetReturnURLMixin, View):
messages.error(self.request, "{} failed validation: {}".format(obj, e)) messages.error(self.request, "{} failed validation: {}".format(obj, e))
else: else:
initial_data = request.POST.copy() # Pass the PK list as initial data to avoid binding the form
initial_data['pk'] = pk_list initial_data = querydict_to_dict(post_data)
form = self.form(model, parent_obj, initial=initial_data)
# Append any normal initial data (passed as GET parameters)
initial_data.update(request.GET)
form = self.form(model, initial=initial_data)
# Retrieve objects being edited # Retrieve objects being edited
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False) table = self.table(self.queryset.filter(pk__in=post_data.getlist('pk')), orderable=False)
if not table.rows: if not table.rows:
messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural)) messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural))
return redirect(self.get_return_url(request)) return redirect(self.get_return_url(request))
@ -742,14 +739,12 @@ class BulkDeleteView(GetReturnURLMixin, View):
Delete objects in bulk. Delete objects in bulk.
queryset: Custom queryset to use when retrieving objects (e.g. to select related objects) queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
parent_model: The model of the parent object (if any)
filter: FilterSet to apply when deleting by QuerySet filter: FilterSet to apply when deleting by QuerySet
table: The table used to display devices being deleted table: The table used to display devices being deleted
form: The form class used to delete objects in bulk form: The form class used to delete objects in bulk
template_name: The name of the template template_name: The name of the template
""" """
queryset = None queryset = None
parent_model = None
filterset = None filterset = None
table = None table = None
form = None form = None
@ -762,12 +757,6 @@ class BulkDeleteView(GetReturnURLMixin, View):
model = self.queryset.model model = self.queryset.model
# Attempt to derive parent object if a parent class has been given
if self.parent_model:
parent_obj = get_object_or_404(self.parent_model, **kwargs)
else:
parent_obj = None
# Are we deleting *all* objects in the queryset or just a selected subset? # Are we deleting *all* objects in the queryset or just a selected subset?
if request.POST.get('_all'): if request.POST.get('_all'):
if self.filterset is not None: if self.filterset is not None:
@ -809,7 +798,6 @@ class BulkDeleteView(GetReturnURLMixin, View):
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'parent_obj': parent_obj,
'obj_type_plural': model._meta.verbose_name_plural, 'obj_type_plural': model._meta.verbose_name_plural,
'table': table, 'table': table,
'return_url': self.get_return_url(request), 'return_url': self.get_return_url(request),
@ -832,7 +820,8 @@ class BulkDeleteView(GetReturnURLMixin, View):
# Device/VirtualMachine components # Device/VirtualMachine components
# #
class ComponentCreateView(View): # TODO: Replace with BulkCreateView
class ComponentCreateView(GetReturnURLMixin, View):
""" """
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.
""" """
@ -843,30 +832,23 @@ class ComponentCreateView(View):
model_form = None model_form = None
template_name = None template_name = None
def get(self, request, pk): def get(self, request):
parent = get_object_or_404(self.parent_model, pk=pk) form = self.form(initial=request.GET)
data = deepcopy(request.GET)
data[self.parent_field] = parent.pk
form = self.form(parent, initial=data)
return render(request, self.template_name, { return render(request, self.template_name, {
'parent': parent,
'component_type': self.model._meta.verbose_name, 'component_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'return_url': parent.get_absolute_url(), 'return_url': self.get_return_url(request),
}) })
def post(self, request, pk): def post(self, request):
parent = get_object_or_404(self.parent_model, pk=pk) form = self.form(request.POST, initial=request.GET)
form = self.form(parent, request.POST)
if form.is_valid(): if form.is_valid():
new_components = [] new_components = []
data = deepcopy(request.POST) data = deepcopy(request.POST)
data[self.parent_field] = parent.pk
for i, name in enumerate(form.cleaned_data['name_pattern']): for i, name in enumerate(form.cleaned_data['name_pattern']):
@ -891,19 +873,18 @@ class ComponentCreateView(View):
for component_form in new_components: for component_form in new_components:
component_form.save() component_form.save()
messages.success(request, "Added {} {} to {}.".format( messages.success(request, "Added {} {}".format(
len(new_components), self.model._meta.verbose_name_plural, parent len(new_components), self.model._meta.verbose_name_plural
)) ))
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect(request.path) return redirect(request.get_full_path())
else: else:
return redirect(parent.get_absolute_url()) return redirect(self.get_return_url(request))
return render(request, self.template_name, { return render(request, self.template_name, {
'parent': parent,
'component_type': self.model._meta.verbose_name, 'component_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'return_url': parent.get_absolute_url(), 'return_url': self.get_return_url(request),
}) })

View File

@ -739,6 +739,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class InterfaceCreateForm(ComponentForm): class InterfaceCreateForm(ComponentForm):
virtual_machine = forms.ModelChoiceField(
queryset=VirtualMachine.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField( name_pattern = ExpandableNameField(
label='Name' label='Name'
) )
@ -748,7 +752,8 @@ class InterfaceCreateForm(ComponentForm):
widget=forms.HiddenInput() widget=forms.HiddenInput()
) )
enabled = forms.BooleanField( enabled = forms.BooleanField(
required=False required=False,
initial=True
) )
mtu = forms.IntegerField( mtu = forms.IntegerField(
required=False, required=False,
@ -792,14 +797,13 @@ class InterfaceCreateForm(ComponentForm):
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {}).copy()
kwargs['initial'].update({'enabled': True})
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site virtual_machine = VirtualMachine.objects.get(
pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
)
# Limit VLAN choices to those in: global vlans, global groups, the current site's group, the current site
vlan_choices = [] vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None) global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append( vlan_choices.append(
@ -811,7 +815,7 @@ class InterfaceCreateForm(ComponentForm):
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
) )
site = getattr(self.parent.cluster, 'site', None) site = getattr(virtual_machine.cluster, 'site', None)
if site is not None: if site is not None:
# Add non-grouped site VLANs # Add non-grouped site VLANs
@ -835,6 +839,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
virtual_machine = forms.ModelChoiceField(
queryset=VirtualMachine.objects.all(),
widget=forms.HiddenInput()
)
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
@ -881,7 +889,11 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site # Limit available VLANs based on the parent VirtualMachine
if 'virtual_machine' in self.initial:
parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
# Limit VLAN choices to global VLANs, VLANs in global groups, the current site's group, the current site
vlan_choices = [] vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None) global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append( vlan_choices.append(
@ -892,8 +904,8 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
vlan_choices.append( vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
) )
if self.parent_obj.cluster is not None: if parent_obj.cluster is not None:
site = getattr(self.parent_obj.cluster, 'site', None) site = getattr(parent_obj.cluster, 'site', None)
if site is not None: if site is not None:
# Add non-grouped site VLANs # Add non-grouped site VLANs

View File

@ -1,4 +1,8 @@
from dcim.models import DeviceRole, Platform, Site from netaddr import EUI
from dcim.choices import InterfaceModeChoices
from dcim.models import DeviceRole, Interface, Platform, Site
from ipam.models import VLAN
from utilities.testing import StandardTestCases from utilities.testing import StandardTestCases
from virtualization.choices import * from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -187,3 +191,93 @@ class VirtualMachineTestCase(StandardTestCases.Views):
'disk': 8000, 'disk': 8000,
'comments': 'New comments', 'comments': 'New comments',
} }
class InterfaceTestCase(StandardTestCases.Views):
model = Interface
# Disable inapplicable tests
test_list_objects = None
test_create_object = None
test_import_objects = None
def test_bulk_create_objects(self):
return self._test_bulk_create_objects(expected_count=3)
def _get_base_url(self):
# Interface belongs to the DCIM app, so we have to override the base URL
return 'virtualization:interface_{}'
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site)
virtualmachines = (
VirtualMachine(name='Virtual Machine 1', cluster=cluster, role=devicerole),
VirtualMachine(name='Virtual Machine 2', cluster=cluster, role=devicerole),
)
VirtualMachine.objects.bulk_create(virtualmachines)
Interface.objects.bulk_create([
Interface(virtual_machine=virtualmachines[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(virtual_machine=virtualmachines[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(virtual_machine=virtualmachines[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
])
vlans = (
VLAN(vid=1, name='VLAN1', site=site),
VLAN(vid=101, name='VLAN101', site=site),
VLAN(vid=102, name='VLAN102', site=site),
VLAN(vid=103, name='VLAN103', site=site),
)
VLAN.objects.bulk_create(vlans)
cls.form_data = {
'virtual_machine': virtualmachines[1].pk,
'name': 'Interface X',
'type': InterfaceTypeChoices.TYPE_VIRTUAL,
'enabled': False,
'mgmt_only': False,
'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 2000,
'description': 'New description',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]],
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_create_data = {
'virtual_machine': virtualmachines[1].pk,
'name_pattern': 'Interface [4-6]',
'type': InterfaceTypeChoices.TYPE_VIRTUAL,
'enabled': False,
'mgmt_only': False,
'mac_address': EUI('01-02-03-04-05-06'),
'mtu': 2000,
'description': 'New description',
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'tagged_vlans': [v.pk for v in vlans[1:4]],
'tags': 'Alpha,Bravo,Charlie',
}
cls.bulk_edit_data = {
'virtual_machine': virtualmachines[1].pk,
'enabled': False,
'mtu': 2000,
'description': 'New description',
'mode': InterfaceModeChoices.MODE_TAGGED,
# 'untagged_vlan': vlans[0].pk,
# 'tagged_vlans': [v.pk for v in vlans[1:4]],
}
cls.csv_data = (
"device,name,type",
"Device 1,Interface 4,1000BASE-T (1GE)",
"Device 1,Interface 5,1000BASE-T (1GE)",
"Device 1,Interface 6,1000BASE-T (1GE)",
)

View File

@ -52,9 +52,10 @@ urlpatterns = [
# VM interfaces # VM interfaces
path(r'virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), path(r'virtual-machines/interfaces/add/', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'),
path(r'virtual-machines/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), path(r'interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'virtual-machines/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), path(r'interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'virtual-machines/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), path(r'interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
# TODO: Rename vm-interfaces to interfaces
path(r'vm-interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path(r'vm-interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path(r'vm-interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path(r'vm-interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),

View File

@ -330,8 +330,6 @@ class VirtualMachineBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface' permission_required = 'dcim.add_interface'
parent_model = VirtualMachine
parent_field = 'virtual_machine'
model = Interface model = Interface
form = forms.InterfaceCreateForm form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm model_form = forms.InterfaceForm
@ -353,7 +351,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
parent_model = VirtualMachine
table = tables.InterfaceTable table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm form = forms.InterfaceBulkEditForm
@ -361,7 +358,6 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface' permission_required = 'dcim.delete_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
parent_model = VirtualMachine
table = tables.InterfaceTable table = tables.InterfaceTable