mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -06:00
Merge pull request #4335 from netbox-community/4325-rack-reservation-import
Closes #4325: Add suport for rack reservations CSV import
This commit is contained in:
commit
0eaec6bd83
@ -829,6 +829,64 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
|
|||||||
return unit_choices
|
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):
|
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=RackReservation.objects.all(),
|
queryset=RackReservation.objects.all(),
|
||||||
|
@ -761,6 +761,8 @@ class RackReservation(ChangeLoggedModel):
|
|||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
|
|
||||||
|
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['created']
|
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
|
@property
|
||||||
def unit_list(self):
|
def unit_list(self):
|
||||||
"""
|
"""
|
||||||
|
@ -176,9 +176,6 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
test_get_object = None
|
test_get_object = None
|
||||||
test_create_object = None
|
test_create_object = None
|
||||||
|
|
||||||
# TODO: Fix URL name for view
|
|
||||||
test_import_objects = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
@ -204,6 +201,13 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'description': 'Rack reservation',
|
'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 = {
|
cls.bulk_edit_data = {
|
||||||
'user': user3.pk,
|
'user': user3.pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
|
@ -51,6 +51,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Rack reservations
|
# Rack reservations
|
||||||
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
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/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||||
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
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'),
|
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||||
|
@ -470,7 +470,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
filterset = filters.RackReservationFilterSet
|
filterset = filters.RackReservationFilterSet
|
||||||
filterset_form = forms.RackReservationFilterForm
|
filterset_form = forms.RackReservationFilterForm
|
||||||
table = tables.RackReservationTable
|
table = tables.RackReservationTable
|
||||||
action_buttons = ()
|
action_buttons = ('export',)
|
||||||
|
|
||||||
|
|
||||||
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
@ -500,6 +500,23 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
return obj.rack.get_absolute_url()
|
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):
|
class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_rackreservation'
|
permission_required = 'dcim.change_rackreservation'
|
||||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||||
@ -1245,7 +1262,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
template_name = 'dcim/device_import_child.html'
|
template_name = 'dcim/device_import_child.html'
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
def _save_obj(self, obj_form):
|
def _save_obj(self, obj_form, request):
|
||||||
|
|
||||||
obj = obj_form.save()
|
obj = obj_form.save()
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ class SecretBulkImportView(BulkImportView):
|
|||||||
|
|
||||||
master_key = None
|
master_key = None
|
||||||
|
|
||||||
def _save_obj(self, obj_form):
|
def _save_obj(self, obj_form, request):
|
||||||
"""
|
"""
|
||||||
Encrypt each object before saving it to the database.
|
Encrypt each object before saving it to the database.
|
||||||
"""
|
"""
|
||||||
|
@ -67,12 +67,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a>
|
<a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
|
||||||
|
{% if perms.dcim.add_rackreservation %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'dcim:rackreservation_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
|
||||||
|
</li>
|
||||||
<li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
|
<li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
|
|
||||||
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
|
|
||||||
</li>
|
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Tenancy</li>
|
<li class="dropdown-header">Tenancy</li>
|
||||||
<li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
|
<li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
|
||||||
|
@ -544,7 +544,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
return ImportForm(*args, **kwargs)
|
return ImportForm(*args, **kwargs)
|
||||||
|
|
||||||
def _save_obj(self, obj_form):
|
def _save_obj(self, obj_form, request):
|
||||||
"""
|
"""
|
||||||
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
|
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
|
||||||
"""
|
"""
|
||||||
@ -573,7 +573,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
for row, data in enumerate(form.cleaned_data['csv'], start=1):
|
for row, data in enumerate(form.cleaned_data['csv'], start=1):
|
||||||
obj_form = self.model_form(data)
|
obj_form = self.model_form(data)
|
||||||
if obj_form.is_valid():
|
if obj_form.is_valid():
|
||||||
obj = self._save_obj(obj_form)
|
obj = self._save_obj(obj_form, request)
|
||||||
new_objs.append(obj)
|
new_objs.append(obj)
|
||||||
else:
|
else:
|
||||||
for field, err in obj_form.errors.items():
|
for field, err in obj_form.errors.items():
|
||||||
|
Loading…
Reference in New Issue
Block a user