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

View File

@ -682,11 +682,11 @@ class ConsolePortTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
test_create_object = None
test_bulk_edit_objects = None
# TODO
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
def setUpTestData(cls):
@ -704,11 +704,14 @@ class ConsolePortTestCase(StandardTestCases.Views):
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'cable': None,
'connected_endpoint': None,
'connection_status': None,
cls.bulk_create_data = {
'device': device.pk,
'name_pattern': 'Console Port [4-6]',
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console port',
'tags': 'Alpha,Bravo,Charlie',
}
cls.csv_data = (
@ -724,11 +727,10 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
# TODO
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
def setUpTestData(cls):
@ -746,10 +748,20 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
'type': ConsolePortTypeChoices.TYPE_RJ45,
'description': 'A console server port',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'cable': None,
'connection_status': None,
cls.bulk_create_data = {
'device': device.pk,
'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 = (
@ -766,10 +778,10 @@ class PowerPortTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
test_bulk_edit_objects = None
# TODO
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
def setUpTestData(cls):
@ -789,10 +801,16 @@ class PowerPortTestCase(StandardTestCases.Views):
'allocated_draw': 50,
'description': 'A power port',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'cable': None,
'connection_status': None,
cls.bulk_create_data = {
'device': device.pk,
'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 = (
@ -808,11 +826,10 @@ class PowerOutletTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
# TODO
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
def setUpTestData(cls):
@ -838,10 +855,24 @@ class PowerOutletTestCase(StandardTestCases.Views):
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
'description': 'A power outlet',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'cable': None,
'connection_status': None,
cls.bulk_create_data = {
'device': device.pk,
'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 = (
@ -855,20 +886,23 @@ class PowerOutletTestCase(StandardTestCases.Views):
class InterfaceTestCase(StandardTestCases.Views):
model = Interface
# TODO
# Disable inapplicable views
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
def setUpTestData(cls):
device = create_test_device('Device 1')
Interface.objects.bulk_create([
interfaces = (
Interface(device=device, name='Interface 1'),
Interface(device=device, name='Interface 2'),
Interface(device=device, name='Interface 3'),
])
Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG),
)
Interface.objects.bulk_create(interfaces)
vlans = (
VLAN(vid=1, name='VLAN1', site=device.site),
@ -884,7 +918,38 @@ class InterfaceTestCase(StandardTestCases.Views):
'name': 'Interface X',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'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'),
'mtu': 2000,
'mgmt_only': True,
@ -892,11 +957,6 @@ class InterfaceTestCase(StandardTestCases.Views):
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': vlans[0].pk,
'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 = (
@ -912,11 +972,10 @@ class FrontPortTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
# TODO
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
def setUpTestData(cls):
@ -946,9 +1005,22 @@ class FrontPortTestCase(StandardTestCases.Views):
'rear_port_position': 1,
'description': 'New description',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'cable': None,
cls.bulk_create_data = {
'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 = (
@ -964,11 +1036,10 @@ class RearPortTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
# TODO
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
def setUpTestData(cls):
@ -985,11 +1056,22 @@ class RearPortTestCase(StandardTestCases.Views):
'name': 'Rear Port X',
'type': PortTypeChoices.TYPE_8P8C,
'positions': 3,
'description': 'New description',
'description': 'A rear port',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'cable': None,
cls.bulk_create_data = {
'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 = (
@ -1005,11 +1087,13 @@ class DeviceBayTestCase(StandardTestCases.Views):
# Disable inapplicable views
test_get_object = None
test_create_object = None
# TODO
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
def setUpTestData(cls):
@ -1030,9 +1114,13 @@ class DeviceBayTestCase(StandardTestCases.Views):
'name': 'Device Bay X',
'description': 'A device bay',
'tags': 'Alpha,Bravo,Charlie',
}
# Extraneous model fields
'installed_device': None,
cls.bulk_create_data = {
'device': device2.pk,
'name_pattern': 'Device Bay [4-6]',
'description': 'A device bay',
'tags': 'Alpha,Bravo,Charlie',
}
cls.csv_data = (
@ -1076,13 +1164,6 @@ class InventoryItemTestCase(StandardTestCases.Views):
'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 = {
'device': device.pk,
'manufacturer': manufacturer.pk,
@ -1090,6 +1171,13 @@ class InventoryItemTestCase(StandardTestCases.Views):
'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):
model = Cable

View File

@ -169,109 +169,112 @@ urlpatterns = [
# Console ports
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/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: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>/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
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/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: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>/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
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/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: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>/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
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/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: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>/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
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/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: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>/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>/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
# 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/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: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>/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
# 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/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: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>/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
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/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>/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>/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
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/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
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'),
# Cables

View File

@ -1205,8 +1205,6 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
model = ConsolePort
form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm
@ -1234,8 +1232,8 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all()
parent_model = Device
table = tables.ConsolePortTable
default_return_url = 'dcim:consoleport_list'
#
@ -1253,8 +1251,6 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
model = ConsoleServerPort
form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm
@ -1282,7 +1278,6 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all()
parent_model = Device
table = tables.ConsoleServerPortTable
form = forms.ConsoleServerPortBulkEditForm
@ -1302,8 +1297,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
queryset = ConsoleServerPort.objects.all()
parent_model = Device
table = tables.ConsoleServerPortTable
default_return_url = 'dcim:consoleserverport_list'
#
@ -1321,8 +1316,6 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
model = PowerPort
form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm
@ -1350,8 +1343,8 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all()
parent_model = Device
table = tables.PowerPortTable
default_return_url = 'dcim:powerport_list'
#
@ -1369,8 +1362,6 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
model = PowerOutlet
form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm
@ -1398,7 +1389,6 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all()
parent_model = Device
table = tables.PowerOutletTable
form = forms.PowerOutletBulkEditForm
@ -1418,8 +1408,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
queryset = PowerOutlet.objects.all()
parent_model = Device
table = tables.PowerOutletTable
default_return_url = 'dcim:poweroutlet_list'
#
@ -1473,8 +1463,6 @@ class InterfaceView(PermissionRequiredMixin, View):
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
model = Interface
form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm
@ -1503,7 +1491,6 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface'
queryset = Interface.objects.all()
parent_model = Device
table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm
@ -1523,8 +1510,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface'
queryset = Interface.objects.all()
parent_model = Device
table = tables.InterfaceTable
default_return_url = 'dcim:interface_list'
#
@ -1542,8 +1529,6 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontport'
parent_model = Device
parent_field = 'device'
model = FrontPort
form = forms.FrontPortCreateForm
model_form = forms.FrontPortForm
@ -1571,7 +1556,6 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable
form = forms.FrontPortBulkEditForm
@ -1591,8 +1575,8 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_frontport'
queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable
default_return_url = 'dcim:frontport_list'
#
@ -1610,8 +1594,6 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearport'
parent_model = Device
parent_field = 'device'
model = RearPort
form = forms.RearPortCreateForm
model_form = forms.RearPortForm
@ -1639,7 +1621,6 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable
form = forms.RearPortBulkEditForm
@ -1659,8 +1640,8 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rearport'
queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable
default_return_url = 'dcim:rearport_list'
#
@ -1680,8 +1661,6 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'
model = DeviceBay
form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm
@ -1784,8 +1763,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay'
queryset = DeviceBay.objects.all()
parent_model = Device
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>
</button>
<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_consoleserverport %}<li><a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:powerport_add' pk=device.pk %}">Power Ports</a></li>{% 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_interface %}<li><a href="{% url 'dcim:interface_add' pk=device.pk %}">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_frontport %}<li><a href="{% url 'dcim:frontport_add' pk=device.pk %}">Front Ports</a></li>{% 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_devicebay %}<li><a href="{% url 'dcim:devicebay_add' pk=device.pk %}">Device Bays</a></li>{% endif %}
{% if perms.dcim.add_consoleport %}
<li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Ports</a></li>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
<li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Server Ports</a></li>
{% endif %}
{% if perms.dcim.add_powerport %}
<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>
</div>
{% endif %}
@ -333,12 +349,12 @@
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right noprint">
{% 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
</a>
{% endif %}
{% 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
</a>
{% endif %}
@ -524,13 +540,13 @@
</button>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_devicebay %}
<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
</a>
</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">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</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
</button>
{% endif %}
@ -597,13 +613,13 @@
</button>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_interface %}
<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
</a>
</div>
@ -619,6 +635,7 @@
{% if perms.dcim.delete_consoleserverport %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %}
<div class="panel panel-default">
<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">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</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
</button>
<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>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
<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
</a>
</div>
@ -679,6 +696,7 @@
{% if perms.dcim.delete_poweroutlet %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %}
<div class="panel panel-default">
<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">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</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
</button>
<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>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
<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
</a>
</div>
@ -738,7 +756,8 @@
{% endif %}
{% if front_ports %}
<form method="post">
{% csrf_token %}
{% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default">
<div class="panel-heading">
<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">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</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
</button>
<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>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_frontport %}
<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
</a>
</div>
@ -796,7 +815,8 @@
{% endif %}
{% if rear_ports %}
<form method="post">
{% csrf_token %}
{% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default">
<div class="panel-heading">
<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">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</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
</button>
<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>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_rearport %}
<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
</a>
</div>

View File

@ -1,14 +1,11 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
{% block title %}Create {{ component_type }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
<form action="" method="post" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %}
@ -27,7 +24,7 @@
<div class="form-group">
<label class="col-md-3 control-label required">Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ parent }}</p>
<p class="form-control-static">{{ form.device.value }}</p>
</div>
</div>
{% 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">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</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
</button>
{% endif %}
{% 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
</button>
{% endif %}
{% if perms.dcim.add_interface %}
<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
</a>
</div>

View File

@ -5,7 +5,7 @@
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
<form action="" method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">
<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)
# TODO: Remove ComponentForm
class ComponentForm(BootstrapMixin, forms.Form):
"""
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):
return {}
@ -743,10 +740,9 @@ class BulkEditForm(forms.Form):
"""
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)
self.model = model
self.parent_obj = parent_obj
self.nullable_fields = []
# Copy any nullable fields defined in Meta

View File

@ -1,11 +1,12 @@
from django.contrib.auth.models import Permission, User
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.urls import reverse, NoReverseMatch
from rest_framework.test import APIClient
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):
@ -56,6 +57,30 @@ class TestCase(_TestCase):
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):
client_class = APIClient
@ -92,6 +117,9 @@ class StandardTestCases:
# CSV lines used for bulk import of new objects
csv_data = ()
# Form data used when creating multiple objects
bulk_create_data = {}
# Form data to be used when editing multiple objects at once
bulk_edit_data = {}
@ -104,15 +132,26 @@ class StandardTestCases:
if self.model is None:
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):
"""
Return the URL name for a specific action. An instance must be specified for
get/edit/delete views.
"""
url_format = '{}:{}_{{}}'.format(
self.model._meta.app_label,
self.model._meta.model_name
)
url_format = self._get_base_url()
if action in ('list', 'add', 'import', 'bulk_edit', 'bulk_delete'):
return reverse(url_format.format(action))
@ -131,6 +170,13 @@ class StandardTestCases:
else:
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=[])
def test_list_objects(self):
# Attempt to make the request without required permissions
@ -187,7 +233,7 @@ class StandardTestCases:
self.assertEqual(initial_count + 1, self.model.objects.count())
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=[])
def test_edit_object(self):
@ -211,7 +257,7 @@ class StandardTestCases:
self.assertHttpStatus(response, 302)
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=[])
def test_delete_object(self):
@ -263,7 +309,8 @@ class StandardTestCases:
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
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 = {
'path': self._get_url('bulk_edit'),
@ -288,13 +335,8 @@ class StandardTestCases:
response = self.client.post(**request)
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)):
self.assertDictEqual(
model_to_dict(instance, fields=bulk_edit_fields),
self.bulk_edit_data,
msg="Instance {} failed to validate after bulk edit: {}".format(i, instance)
)
self.assertInstanceEqual(instance, self.bulk_edit_data)
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_bulk_delete_objects(self):
@ -323,3 +365,32 @@ class StandardTestCases:
# Check that all objects were deleted
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 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):

View File

@ -4,6 +4,7 @@ from collections import OrderedDict
from django.core.serializers import serialize
from django.db.models import Count, OuterRef, Subquery
from django.http import QueryDict
from jinja2 import Environment
from dcim.choices import CableLengthUnitChoices
@ -209,3 +210,15 @@ def prepare_cloned_fields(instance):
)
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 utilities.exceptions import AbortTransaction
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 .forms import ConfirmationForm, ImportForm
from .paginator import EnhancedPaginator
@ -604,14 +604,12 @@ class BulkEditView(GetReturnURLMixin, View):
Edit objects in bulk.
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
table: The table used to display devices being edited
form: The form class used to edit objects in bulk
template_name: The name of the template
"""
queryset = None
parent_model = None
filterset = None
table = None
form = None
@ -624,20 +622,15 @@ class BulkEditView(GetReturnURLMixin, View):
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
# Create a mutable copy of the POST data
post_data = request.POST.copy()
# Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all') and self.filterset is not None:
pk_list = [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 we are editing *all* objects in the queryset, replace the PK list with all matched objects.
if post_data.get('_all') and self.filterset is not None:
post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
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():
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
@ -651,7 +644,7 @@ class BulkEditView(GetReturnURLMixin, View):
with transaction.atomic():
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.
for name in standard_fields:
@ -719,12 +712,16 @@ class BulkEditView(GetReturnURLMixin, View):
messages.error(self.request, "{} failed validation: {}".format(obj, e))
else:
initial_data = request.POST.copy()
initial_data['pk'] = pk_list
form = self.form(model, parent_obj, initial=initial_data)
# Pass the PK list as initial data to avoid binding the form
initial_data = querydict_to_dict(post_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
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:
messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural))
return redirect(self.get_return_url(request))
@ -742,14 +739,12 @@ class BulkDeleteView(GetReturnURLMixin, View):
Delete objects in bulk.
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
table: The table used to display devices being deleted
form: The form class used to delete objects in bulk
template_name: The name of the template
"""
queryset = None
parent_model = None
filterset = None
table = None
form = None
@ -762,12 +757,6 @@ class BulkDeleteView(GetReturnURLMixin, View):
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?
if request.POST.get('_all'):
if self.filterset is not None:
@ -809,7 +798,6 @@ class BulkDeleteView(GetReturnURLMixin, View):
return render(request, self.template_name, {
'form': form,
'parent_obj': parent_obj,
'obj_type_plural': model._meta.verbose_name_plural,
'table': table,
'return_url': self.get_return_url(request),
@ -832,7 +820,8 @@ class BulkDeleteView(GetReturnURLMixin, View):
# 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.
"""
@ -843,30 +832,23 @@ class ComponentCreateView(View):
model_form = None
template_name = None
def get(self, request, pk):
def get(self, request):
parent = get_object_or_404(self.parent_model, pk=pk)
data = deepcopy(request.GET)
data[self.parent_field] = parent.pk
form = self.form(parent, initial=data)
form = self.form(initial=request.GET)
return render(request, self.template_name, {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'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(parent, request.POST)
form = self.form(request.POST, initial=request.GET)
if form.is_valid():
new_components = []
data = deepcopy(request.POST)
data[self.parent_field] = parent.pk
for i, name in enumerate(form.cleaned_data['name_pattern']):
@ -891,19 +873,18 @@ class ComponentCreateView(View):
for component_form in new_components:
component_form.save()
messages.success(request, "Added {} {} to {}.".format(
len(new_components), self.model._meta.verbose_name_plural, parent
messages.success(request, "Added {} {}".format(
len(new_components), self.model._meta.verbose_name_plural
))
if '_addanother' in request.POST:
return redirect(request.path)
return redirect(request.get_full_path())
else:
return redirect(parent.get_absolute_url())
return redirect(self.get_return_url(request))
return render(request, self.template_name, {
'parent': parent,
'component_type': self.model._meta.verbose_name,
'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):
virtual_machine = forms.ModelChoiceField(
queryset=VirtualMachine.objects.all(),
widget=forms.HiddenInput()
)
name_pattern = ExpandableNameField(
label='Name'
)
@ -748,7 +752,8 @@ class InterfaceCreateForm(ComponentForm):
widget=forms.HiddenInput()
)
enabled = forms.BooleanField(
required=False
required=False,
initial=True
)
mtu = forms.IntegerField(
required=False,
@ -792,14 +797,13 @@ class InterfaceCreateForm(ComponentForm):
)
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)
# 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 = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
@ -811,7 +815,7 @@ class InterfaceCreateForm(ComponentForm):
(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:
# Add non-grouped site VLANs
@ -835,6 +839,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
queryset=Interface.objects.all(),
widget=forms.MultipleHiddenInput()
)
virtual_machine = forms.ModelChoiceField(
queryset=VirtualMachine.objects.all(),
widget=forms.HiddenInput()
)
enabled = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
@ -881,35 +889,39 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
# 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 = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
if self.parent_obj.cluster is not None:
site = getattr(self.parent_obj.cluster, 'site', None)
if site is not None:
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
if parent_obj.cluster is not None:
site = getattr(parent_obj.cluster, 'site', None)
if site is not None:
# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
#

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 virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -187,3 +191,93 @@ class VirtualMachineTestCase(StandardTestCases.Views):
'disk': 8000,
'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
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'virtual-machines/<int:pk>/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/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
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>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),

View File

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