Merge branch 'develop' into develop-2.8

This commit is contained in:
Jeremy Stretch
2020-03-10 15:06:37 -04:00
156 changed files with 1561 additions and 1674 deletions

View File

@@ -829,6 +829,64 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
return unit_choices
class RackReservationCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Invalid site name.',
}
)
rack_group = forms.CharField(
required=False,
help_text="Rack's group (if any)"
)
rack_name = forms.CharField(
help_text="Rack name"
)
units = SimpleArrayField(
base_field=forms.IntegerField(),
required=True,
help_text='Comma-separated list of individual unit numbers'
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Name of assigned tenant',
error_messages={
'invalid_choice': 'Tenant not found.',
}
)
class Meta:
model = RackReservation
fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description')
help_texts = {
}
def clean(self):
super().clean()
site = self.cleaned_data.get('site')
rack_group = self.cleaned_data.get('rack_group')
rack_name = self.cleaned_data.get('rack_name')
# Validate rack
if site and rack_group and rack_name:
try:
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
except Rack.DoesNotExist:
raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
elif site and rack_name:
try:
self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
except Rack.DoesNotExist:
raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RackReservation.objects.all(),
@@ -4621,6 +4679,35 @@ class PowerPanelCSVForm(forms.ModelForm):
)
class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
widget=forms.MultipleHiddenInput
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/sites/",
filter_for={
'rack_group': 'site_id',
}
)
)
rack_group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/rack-groups/"
)
)
class Meta:
nullable_fields = (
'rack_group',
)
class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = PowerPanel
q = forms.CharField(

View File

@@ -761,6 +761,8 @@ class RackReservation(ChangeLoggedModel):
max_length=100
)
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
class Meta:
ordering = ['created']
@@ -793,6 +795,17 @@ class RackReservation(ChangeLoggedModel):
)
})
def to_csv(self):
return (
self.rack.site.name,
self.rack.group if self.rack.group else None,
self.rack.name,
','.join([str(u) for u in self.units]),
self.tenant.name if self.tenant else None,
self.user.username,
self.description
)
@property
def unit_list(self):
"""

View File

@@ -19,14 +19,9 @@ class NaturalOrderingTestCase(TestCase):
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
)
def _compare_names(self, queryset, names):
for i, obj in enumerate(queryset):
self.assertEqual(obj.name, names[i])
def test_interface_ordering_numeric(self):
INTERFACES = (
INTERFACES = [
'0',
'0.1',
'0.2',
@@ -53,17 +48,20 @@ class NaturalOrderingTestCase(TestCase):
'1:2.1',
'1:2.2',
'1:2.10',
)
]
for name in INTERFACES:
iface = Interface(device=self.device, name=name)
iface.save()
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
self.assertListEqual(
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
INTERFACES
)
def test_interface_ordering_linux(self):
INTERFACES = (
INTERFACES = [
'eth0',
'eth0.1',
'eth0.2',
@@ -74,17 +72,20 @@ class NaturalOrderingTestCase(TestCase):
'eth1.2',
'eth1.100',
'lo0',
)
]
for name in INTERFACES:
iface = Interface(device=self.device, name=name)
iface.save()
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
self.assertListEqual(
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
INTERFACES
)
def test_interface_ordering_junos(self):
INTERFACES = (
INTERFACES = [
'xe-0/0/0',
'xe-0/0/1',
'xe-0/0/2',
@@ -124,17 +125,20 @@ class NaturalOrderingTestCase(TestCase):
'irb.10',
'irb.100',
'lo0',
)
]
for name in INTERFACES:
iface = Interface(device=self.device, name=name)
iface.save()
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
self.assertListEqual(
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
INTERFACES
)
def test_interface_ordering_ios(self):
INTERFACES = (
INTERFACES = [
'GigabitEthernet0/1',
'GigabitEthernet0/2',
'GigabitEthernet0/10',
@@ -148,10 +152,13 @@ class NaturalOrderingTestCase(TestCase):
'FastEthernet1',
'FastEthernet2',
'FastEthernet10',
)
]
for name in INTERFACES:
iface = Interface(device=self.device, name=name)
iface.save()
self._compare_names(Interface.objects.filter(device=self.device), INTERFACES)
self.assertListEqual(
list(Interface.objects.filter(device=self.device).values_list('name', flat=True)),
INTERFACES
)

View File

@@ -176,9 +176,6 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
test_get_object = None
test_create_object = None
# TODO: Fix URL name for view
test_import_objects = None
@classmethod
def setUpTestData(cls):
@@ -204,6 +201,13 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'Rack reservation',
}
cls.csv_data = (
'site,rack_name,units,description',
'Site 1,Rack 1,"10,11,12",Reservation 1',
'Site 1,Rack 1,"13,14,15",Reservation 2',
'Site 1,Rack 1,"16,17,18",Reservation 3',
)
cls.bulk_edit_data = {
'user': user3.pk,
'tenant': None,
@@ -1553,9 +1557,6 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = PowerPanel
# Disable inapplicable tests
test_bulk_edit_objects = None
@classmethod
def setUpTestData(cls):
@@ -1590,6 +1591,11 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"Site 1,Rack Group 1,Power Panel 6",
)
cls.bulk_edit_data = {
'site': sites[1].pk,
'rack_group': rackgroups[1].pk,
}
class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = PowerFeed

View File

@@ -51,6 +51,7 @@ urlpatterns = [
# Rack reservations
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
path('rack-reservations/import/', views.RackReservationImportView.as_view(), name='rackreservation_import'),
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
@@ -331,6 +332,7 @@ urlpatterns = [
path('power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'),
path('power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
path('power-panels/edit/', views.PowerPanelBulkEditView.as_view(), name='powerpanel_bulk_edit'),
path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
path('power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),

View File

@@ -470,7 +470,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackReservationFilterSet
filterset_form = forms.RackReservationFilterForm
table = tables.RackReservationTable
action_buttons = ()
action_buttons = ('export',)
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -500,6 +500,23 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
return obj.rack.get_absolute_url()
class RackReservationImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rackreservation'
model_form = forms.RackReservationCSVForm
table = tables.RackReservationTable
default_return_url = 'dcim:rackreservation_list'
def _save_obj(self, obj_form, request):
"""
Assign the currently authenticated user to the RackReservation.
"""
instance = obj_form.save(commit=False)
instance.user = request.user
instance.save()
return instance
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rackreservation'
queryset = RackReservation.objects.prefetch_related('rack', 'user')
@@ -1245,7 +1262,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
template_name = 'dcim/device_import_child.html'
default_return_url = 'dcim:device_list'
def _save_obj(self, obj_form):
def _save_obj(self, obj_form, request):
obj = obj_form.save()
@@ -1316,6 +1333,7 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleport'
queryset = ConsolePort.objects.all()
filterset = filters.ConsolePortFilterSet
table = tables.ConsolePortTable
form = forms.ConsolePortBulkEditForm
@@ -1323,6 +1341,7 @@ class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all()
filterset = filters.ConsolePortFilterSet
table = tables.ConsolePortTable
default_return_url = 'dcim:consoleport_list'
@@ -1369,6 +1388,7 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all()
filterset = filters.ConsoleServerPortFilterSet
table = tables.ConsoleServerPortTable
form = forms.ConsoleServerPortBulkEditForm
@@ -1388,6 +1408,7 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
queryset = ConsoleServerPort.objects.all()
filterset = filters.ConsoleServerPortFilterSet
table = tables.ConsoleServerPortTable
default_return_url = 'dcim:consoleserverport_list'
@@ -1434,6 +1455,7 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_powerport'
queryset = PowerPort.objects.all()
filterset = filters.PowerPortFilterSet
table = tables.PowerPortTable
form = forms.PowerPortBulkEditForm
@@ -1441,6 +1463,7 @@ class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all()
filterset = filters.PowerPortFilterSet
table = tables.PowerPortTable
default_return_url = 'dcim:powerport_list'
@@ -1487,6 +1510,7 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all()
filterset = filters.PowerOutletFilterSet
table = tables.PowerOutletTable
form = forms.PowerOutletBulkEditForm
@@ -1506,6 +1530,7 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
queryset = PowerOutlet.objects.all()
filterset = filters.PowerOutletFilterSet
table = tables.PowerOutletTable
default_return_url = 'dcim:poweroutlet_list'
@@ -1589,6 +1614,7 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface'
queryset = Interface.objects.all()
filterset = filters.InterfaceFilterSet
table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm
@@ -1608,6 +1634,7 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface'
queryset = Interface.objects.all()
filterset = filters.InterfaceFilterSet
table = tables.InterfaceTable
default_return_url = 'dcim:interface_list'
@@ -1654,6 +1681,7 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
filterset = filters.FrontPortFilterSet
table = tables.FrontPortTable
form = forms.FrontPortBulkEditForm
@@ -1673,6 +1701,7 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_frontport'
queryset = FrontPort.objects.all()
filterset = filters.FrontPortFilterSet
table = tables.FrontPortTable
default_return_url = 'dcim:frontport_list'
@@ -1719,6 +1748,7 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()
filterset = filters.RearPortFilterSet
table = tables.RearPortTable
form = forms.RearPortBulkEditForm
@@ -1738,6 +1768,7 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rearport'
queryset = RearPort.objects.all()
filterset = filters.RearPortFilterSet
table = tables.RearPortTable
default_return_url = 'dcim:rearport_list'
@@ -1861,6 +1892,7 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay'
queryset = DeviceBay.objects.all()
filterset = filters.DeviceBayFilterSet
table = tables.DeviceBayTable
default_return_url = 'dcim:devicebay_list'
@@ -2569,6 +2601,15 @@ class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView):
default_return_url = 'dcim:powerpanel_list'
class PowerPanelBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_powerpanel'
queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
filterset = filters.PowerPanelFilterSet
table = tables.PowerPanelTable
form = forms.PowerPanelBulkEditForm
default_return_url = 'dcim:powerpanel_list'
class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerpanel'
queryset = PowerPanel.objects.prefetch_related(