mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-09 21:32:17 -06:00
Merge branch 'develop' into develop-2.8
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user