From ad9dfec89413fb194e05f78f4dbbcaa95b67e8fa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Mar 2020 17:14:26 -0500 Subject: [PATCH 1/5] Started on #4325 (WIP) --- netbox/dcim/forms.py | 64 ++++++++++++++++++++++++++++++++++ netbox/dcim/models/__init__.py | 13 +++++++ netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 9 ++++- 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 2b5aaee7e..e51f50812 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -829,6 +829,70 @@ 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.ModelChoiceField( + queryset=RackGroup.objects.all(), + to_field_name='name', + required=False, + help_text='Name of rack group', + error_messages={ + 'invalid_choice': 'Invalid rack group name.', + } + ) + rack = forms.CharField( + required=True, + help_text='Name of parent rack' + ) + units = forms.CharField( + required=True, + help_text='Rack units' + ) + 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 = RackReservation.csv_headers + fields = ['site', 'rack_group', 'rack', 'units', 'tenant', 'description'] # Can't set user + 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') + + # 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/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..36838acd5 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,13 @@ 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' + + class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rackreservation' queryset = RackReservation.objects.prefetch_related('rack', 'user') From 947affd78fc038ef27ce965d6d68c5f72944908f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Mar 2020 12:39:07 -0400 Subject: [PATCH 2/5] Extend BulkImportView._save_obj to pass request context --- netbox/dcim/forms.py | 19 ++++++------------- netbox/dcim/views.py | 12 +++++++++++- netbox/secrets/views.py | 2 +- netbox/utilities/views.py | 4 ++-- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e51f50812..b0cb8434f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -838,18 +838,12 @@ class RackReservationCSVForm(forms.ModelForm): 'invalid_choice': 'Invalid site name.', } ) - rack_group = forms.ModelChoiceField( - queryset=RackGroup.objects.all(), - to_field_name='name', + rack_group = forms.CharField( required=False, - help_text='Name of rack group', - error_messages={ - 'invalid_choice': 'Invalid rack group name.', - } + help_text="Rack's group (if any)" ) - rack = forms.CharField( - required=True, - help_text='Name of parent rack' + rack_name = forms.CharField( + help_text="Rack name" ) units = forms.CharField( required=True, @@ -867,8 +861,7 @@ class RackReservationCSVForm(forms.ModelForm): class Meta: model = RackReservation - # fields = RackReservation.csv_headers - fields = ['site', 'rack_group', 'rack', 'units', 'tenant', 'description'] # Can't set user + fields = ('site', 'rack_group', 'rack_name', 'units', 'tenant', 'description') help_texts = { } @@ -878,7 +871,7 @@ class RackReservationCSVForm(forms.ModelForm): site = self.cleaned_data.get('site') rack_group = self.cleaned_data.get('rack_group') - rack_name = self.cleaned_data.get('rack') + rack_name = self.cleaned_data.get('rack_name') # Validate rack if site and rack_group and rack_name: diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 36838acd5..0bed00d58 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -506,6 +506,16 @@ class RackReservationImportView(PermissionRequiredMixin, BulkImportView): 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' @@ -1252,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/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(): From 049da81bf2cf95203e280204385a8bd720387a8d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Mar 2020 12:46:12 -0400 Subject: [PATCH 3/5] Convert units to SimpleArrayField --- netbox/dcim/forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b0cb8434f..ac8fc40d5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -845,9 +845,10 @@ class RackReservationCSVForm(forms.ModelForm): rack_name = forms.CharField( help_text="Rack name" ) - units = forms.CharField( + units = SimpleArrayField( + base_field=forms.IntegerField(), required=True, - help_text='Rack units' + help_text='Comma-separated list of individual unit numbers' ) tenant = forms.ModelChoiceField( queryset=Tenant.objects.all(), From e364a25e46547abfb65c0278f62cd5ac9660ed18 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Mar 2020 13:25:36 -0400 Subject: [PATCH 4/5] Add test for RackReservation import --- netbox/dcim/tests/test_views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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, From 8cca22d79cbdcec89e42c281825331f4ed4f0ba0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Mar 2020 13:39:12 -0400 Subject: [PATCH 5/5] Add nav menu entry for rack reservations import --- netbox/templates/inc/nav_menu.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 -