Ver Fonte

Closes #4755: Enable creation of rack reservations directly from navigation menu

Jeremy Stretch há 5 anos atrás
pai
commit
9fc4a4f24a

+ 1 - 0
docs/release-notes/version-2.8.md

@@ -5,6 +5,7 @@
 ### Enhancements
 
 * [#4698](https://github.com/netbox-community/netbox/issues/4698) - Improve display of template code for object in admin UI
+* [#4755](https://github.com/netbox-community/netbox/issues/4755) - Enable creation of rack reservations directly from navigation menu
 
 ### Bug Fixes
 

+ 25 - 31
netbox/dcim/forms.py

@@ -21,10 +21,10 @@ from ipam.models import IPAddress, VLAN
 from tenancy.forms import TenancyFilterForm, TenancyForm
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
-    APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
-    BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField,
-    CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model,
-    JSONField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
+    APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
+    ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm,
+    DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
+    NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
     BOOLEAN_WITH_BLANK_CHOICES,
 )
 from virtualization.models import Cluster, ClusterGroup, VirtualMachine
@@ -729,21 +729,32 @@ class RackElevationFilterForm(RackFilterForm):
 #
 
 class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
-    rack = forms.ModelChoiceField(
-        queryset=Rack.objects.all(),
+    site = DynamicModelChoiceField(
+        queryset=Site.objects.all(),
         required=False,
-        widget=forms.HiddenInput()
+        widget=APISelect(
+            filter_for={
+                'rack_group': 'site_id',
+                'rack': 'site_id',
+            }
+        )
     )
-    # TODO: Change this to an API-backed form field. We can't do this currently because we want to retain
-    # the multi-line <select> widget for easy selection of multiple rack units.
-    units = SimpleArrayField(
-        base_field=forms.IntegerField(),
-        widget=ArrayFieldSelectMultiple(
-            attrs={
-                'size': 10,
+    rack_group = DynamicModelChoiceField(
+        queryset=RackGroup.objects.all(),
+        required=False,
+        widget=APISelect(
+            filter_for={
+                'rack': 'group_id'
             }
         )
     )
+    rack = DynamicModelChoiceField(
+        queryset=Rack.objects.all()
+    )
+    units = NumericArrayField(
+        base_field=forms.IntegerField(),
+        help_text="Comma-separated list of numeric unit IDs. A range may be specified using a hyphen."
+    )
     user = forms.ModelChoiceField(
         queryset=User.objects.order_by(
             'username'
@@ -757,23 +768,6 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
             'rack', 'units', 'user', 'tenant_group', 'tenant', 'description',
         ]
 
-    def __init__(self, *args, **kwargs):
-
-        super().__init__(*args, **kwargs)
-
-        # Populate rack unit choices
-        if hasattr(self.instance, 'rack'):
-            self.fields['units'].widget.choices = self._get_unit_choices()
-
-    def _get_unit_choices(self):
-        rack = self.instance.rack
-        reserved_units = []
-        for resv in rack.reservations.exclude(pk=self.instance.pk):
-            for u in resv.units:
-                reserved_units.append(u)
-        unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
-        return unit_choices
-
 
 class RackReservationCSVForm(CSVModelForm):
     site = CSVModelChoiceField(

+ 1 - 1
netbox/dcim/tests/test_views.py

@@ -198,7 +198,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
         cls.form_data = {
             'rack': rack.pk,
-            'units': [10, 11, 12],
+            'units': "10,11,12",
             'user': user3.pk,
             'tenant': None,
             'description': 'Rack reservation',

+ 10 - 8
netbox/templates/dcim/rackreservation_edit.html

@@ -3,19 +3,21 @@
 
 {% block form %}
     <div class="panel panel-default">
-        <div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
+        <div class="panel-heading"><strong>Rack Reservation</strong></div>
         <div class="panel-body">
-            <div class="form-group">
-                <label class="col-md-3 control-label">Rack</label>
-                <div class="col-md-9">
-                    <p class="form-control-static">{{ obj.rack }}</p>
-                </div>
-            </div>
+            {% render_field form.site %}
+            {% render_field form.rack_group %}
+            {% render_field form.rack %}
             {% render_field form.units %}
             {% render_field form.user %}
+            {% render_field form.description %}
+        </div>
+    </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Tenant Assignment</strong></div>
+        <div class="panel-body">
             {% render_field form.tenant_group %}
             {% render_field form.tenant %}
-            {% render_field form.description %}
         </div>
     </div>
 {% endblock %}

+ 1 - 0
netbox/templates/inc/nav_menu.html

@@ -70,6 +70,7 @@
                         <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_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
                                     <a href="{% url 'dcim:rackreservation_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
                                 </div>
                             {% endif %}

+ 5 - 17
netbox/utilities/forms.py

@@ -7,6 +7,7 @@ import django_filters
 import yaml
 from django import forms
 from django.conf import settings
+from django.contrib.postgres.forms import SimpleArrayField
 from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
 from django.core.exceptions import MultipleObjectsReturned
 from django.db.models import Count
@@ -243,24 +244,11 @@ class ContentTypeSelect(StaticSelect2):
     option_template_name = 'widgets/select_contenttype.html'
 
 
-class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
-    """
-    MultiSelect widget for a SimpleArrayField. Choices must be populated on the widget.
-    """
-    def __init__(self, *args, **kwargs):
-        self.delimiter = kwargs.pop('delimiter', ',')
-        super().__init__(*args, **kwargs)
-
-    def optgroups(self, name, value, attrs=None):
-        # Split the delimited string of values into a list
-        if value:
-            value = value[0].split(self.delimiter)
-        return super().optgroups(name, value, attrs)
+class NumericArrayField(SimpleArrayField):
 
-    def value_from_datadict(self, data, files, name):
-        # Condense the list of selected choices into a delimited string
-        data = super().value_from_datadict(data, files, name)
-        return self.delimiter.join(data)
+    def to_python(self, value):
+        value = ','.join([str(n) for n in parse_numeric_range(value)])
+        return super().to_python(value)
 
 
 class APISelect(SelectWithDisabled):

+ 7 - 0
netbox/utilities/testing/testcases.py

@@ -1,5 +1,6 @@
 from django.contrib.auth.models import Permission, User
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ObjectDoesNotExist
 from django.forms.models import model_to_dict
 from django.test import Client, TestCase as _TestCase, override_settings
@@ -92,6 +93,12 @@ class TestCase(_TestCase):
                 if type(value) is IPNetwork:
                     model_dict[key] = str(value)
 
+            else:
+
+                # Convert ArrayFields to CSV strings
+                if type(instance._meta.get_field(key)) is ArrayField:
+                    model_dict[key] = ','.join([str(v) for v in value])
+
         # Omit any dictionary keys which are not instance attributes
         relevant_data = {
             k: v for k, v in data.items() if hasattr(instance, k)