Sfoglia il codice sorgente

Merge pull request #4335 from netbox-community/4325-rack-reservation-import

Closes #4325: Add suport for rack reservations CSV import
Jeremy Stretch 6 anni fa
parent
commit
0eaec6bd83

+ 58 - 0
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(),

+ 13 - 0
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):
         """

+ 7 - 3
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,

+ 1 - 0
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/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),

+ 19 - 2
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()
 

+ 1 - 1
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.
         """

+ 8 - 3
netbox/templates/inc/nav_menu.html

@@ -67,12 +67,17 @@
                             {% endif %}
                             <a href="{% url 'dcim:rackrole_list' %}">Rack Roles</a>
                         </li>
-                        <li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
-                            <a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
-                        </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 %}>
+                            <a href="{% url 'dcim:rack_elevation_list' %}">Elevations</a>
+                        </li>
                         <li class="divider"></li>
                         <li class="dropdown-header">Tenancy</li>
                         <li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>

+ 2 - 2
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():