diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 2b5aaee7e..ac8fc40d5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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(), diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 8b84c79d8..af785f0d2 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -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): """ diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 997626152..2263a472b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -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, diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 130a79199..456905691 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -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//edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0a6888884..0bed00d58 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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() diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index d92e4b64d..d66326c68 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -206,7 +206,7 @@ class SecretBulkImportView(BulkImportView): 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. """ diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 11daa2277..d2eb93ebd 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -67,12 +67,17 @@ {% endif %} Rack Roles + + {% if perms.dcim.add_rackreservation %} +
+ +
+ {% endif %} + Reservations + Elevations - - Reservations -
  • diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index e354c4dff..78acefa48 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -544,7 +544,7 @@ class BulkImportView(GetReturnURLMixin, View): 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). """ @@ -573,7 +573,7 @@ class BulkImportView(GetReturnURLMixin, View): for row, data in enumerate(form.cleaned_data['csv'], start=1): obj_form = self.model_form(data) if obj_form.is_valid(): - obj = self._save_obj(obj_form) + obj = self._save_obj(obj_form, request) new_objs.append(obj) else: for field, err in obj_form.errors.items():