Browse Source

VirtualChassis form validation cleanup

Jeremy Stretch 7 years ago
parent
commit
a21bd81681
3 changed files with 99 additions and 43 deletions
  1. 52 24
      netbox/dcim/forms.py
  2. 34 16
      netbox/dcim/views.py
  3. 13 3
      netbox/templates/dcim/virtualchassis_edit.html

+ 52 - 24
netbox/dcim/forms.py

@@ -2277,6 +2277,57 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
         }
         }
 
 
 
 
+class BaseVCMemberFormSet(forms.BaseModelFormSet):
+
+    def clean(self):
+        super(BaseVCMemberFormSet, self).clean()
+
+        # Check for duplicate VC position values
+        vc_position_list = []
+        for form in self.forms:
+            vc_position = form.cleaned_data['vc_position']
+            if vc_position in vc_position_list:
+                error_msg = 'A virtual chassis member already exists in position {}.'.format(vc_position)
+                form.add_error('vc_position', error_msg)
+            vc_position_list.append(vc_position)
+
+
+class DeviceVCMembershipForm(forms.ModelForm):
+
+    class Meta:
+        model = Device
+        fields = ['vc_position', 'vc_priority']
+        labels = {
+            'vc_position': 'Position',
+            'vc_priority': 'Priority',
+        }
+
+    def __init__(self, *args, validate_vc_position=False, **kwargs):
+        super(DeviceVCMembershipForm, self).__init__(*args, **kwargs)
+
+        # Require VC position (only required when the Device is a VirtualChassis member)
+        self.fields['vc_position'].required = True
+
+        # Validation of vc_position is optional. This is only required when adding a new member to an existing
+        # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
+        self.validate_vc_position = validate_vc_position
+
+    def clean_vc_position(self):
+        vc_position = self.cleaned_data['vc_position']
+
+        if self.validate_vc_position:
+            conflicting_members = Device.objects.filter(
+                virtual_chassis=self.instance.virtual_chassis,
+                vc_position=vc_position
+            )
+            if conflicting_members.exists():
+                raise forms.ValidationError(
+                    'A virtual chassis member already exists in position {}.'.format(vc_position)
+                )
+
+        return vc_position
+
+
 class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
 class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
     site = forms.ModelChoiceField(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -2315,27 +2366,4 @@ class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
         device = self.cleaned_data['device']
         device = self.cleaned_data['device']
         if device.virtual_chassis is not None:
         if device.virtual_chassis is not None:
             raise forms.ValidationError("Device {} is already assigned to a virtual chassis.".format(device))
             raise forms.ValidationError("Device {} is already assigned to a virtual chassis.".format(device))
-
-
-class DeviceVCMembershipForm(forms.ModelForm):
-
-    class Meta:
-        model = Device
-        fields = ['vc_position', 'vc_priority']
-        labels = {
-            'vc_position': 'Position',
-            'vc_priority': 'Priority',
-        }
-
-    def __init__(self, *args, **kwargs):
-        super(DeviceVCMembershipForm, self).__init__(*args, **kwargs)
-
-        # Require VC position when assigning a member
-        self.fields['vc_position'].required = True
-
-    def clean_vc_position(self):
-        vc_position = self.cleaned_data['vc_position']
-        if Device.objects.filter(virtual_chassis=self.instance.virtual_chassis, vc_position=vc_position).exists():
-            raise forms.ValidationError("A virtual chassis member already exists in this position.")
-
-        return vc_position
+        return device

+ 34 - 16
netbox/dcim/views.py

@@ -857,7 +857,7 @@ class DeviceView(View):
 
 
         # VirtualChassis members
         # VirtualChassis members
         if device.virtual_chassis is not None:
         if device.virtual_chassis is not None:
-            vc_members = Device.objects.filter(virtual_chassis=device.virtual_chassis)
+            vc_members = Device.objects.filter(virtual_chassis=device.virtual_chassis).order_by('vc_position')
         else:
         else:
             vc_members = []
             vc_members = []
 
 
@@ -2080,20 +2080,26 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
         # Get the list of devices being added to a VirtualChassis
         # Get the list of devices being added to a VirtualChassis
         pk_form = forms.DeviceSelectionForm(request.POST)
         pk_form = forms.DeviceSelectionForm(request.POST)
         pk_form.full_clean()
         pk_form.full_clean()
-        device_list = pk_form.cleaned_data.get('pk')
+        device_queryset = Device.objects.filter(
+            pk__in=pk_form.cleaned_data.get('pk')
+        ).select_related('rack').order_by('vc_position')
 
 
-        if not device_list:
+        if not device_queryset:
             messages.warning(request, "No devices were selected.")
             messages.warning(request, "No devices were selected.")
             return redirect('dcim:device_list')
             return redirect('dcim:device_list')
 
 
-        # TODO: Error if any of the devices already belong to a VC
-
-        VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
+        VCMemberFormSet = modelformset_factory(
+            model=Device,
+            formset=forms.BaseVCMemberFormSet,
+            form=forms.DeviceVCMembershipForm,
+            extra=0
+        )
 
 
         if '_create' in request.POST:
         if '_create' in request.POST:
 
 
             vc_form = forms.VirtualChassisForm(request.POST)
             vc_form = forms.VirtualChassisForm(request.POST)
-            formset = VCMemberFormSet(request.POST)
+            vc_form.fields['master'].queryset = device_queryset
+            formset = VCMemberFormSet(request.POST, queryset=device_queryset)
 
 
             if vc_form.is_valid() and formset.is_valid():
             if vc_form.is_valid() and formset.is_valid():
 
 
@@ -2111,8 +2117,8 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
         else:
         else:
 
 
             vc_form = forms.VirtualChassisForm()
             vc_form = forms.VirtualChassisForm()
-            vc_form.fields['master'].queryset = Device.objects.filter(pk__in=device_list)
-            formset = VCMemberFormSet(queryset=Device.objects.filter(pk__in=device_list))
+            vc_form.fields['master'].queryset = device_queryset
+            formset = VCMemberFormSet(queryset=device_queryset)
 
 
         return render(request, 'dcim/virtualchassis_edit.html', {
         return render(request, 'dcim/virtualchassis_edit.html', {
             'pk_form': pk_form,
             'pk_form': pk_form,
@@ -2128,11 +2134,17 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
-        VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
+        VCMemberFormSet = modelformset_factory(
+            model=Device,
+            form=forms.DeviceVCMembershipForm,
+            formset=forms.BaseVCMemberFormSet,
+            extra=0
+        )
+        members_queryset = virtual_chassis.members.select_related('rack').order_by('vc_position')
 
 
         vc_form = forms.VirtualChassisForm(instance=virtual_chassis)
         vc_form = forms.VirtualChassisForm(instance=virtual_chassis)
-        vc_form.fields['master'].queryset = virtual_chassis.members.all()
-        formset = VCMemberFormSet(queryset=virtual_chassis.members.all())
+        vc_form.fields['master'].queryset = members_queryset
+        formset = VCMemberFormSet(queryset=members_queryset)
 
 
         return render(request, 'dcim/virtualchassis_edit.html', {
         return render(request, 'dcim/virtualchassis_edit.html', {
             'vc_form': vc_form,
             'vc_form': vc_form,
@@ -2143,11 +2155,17 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
     def post(self, request, pk):
     def post(self, request, pk):
 
 
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
-        VCMemberFormSet = modelformset_factory(model=Device, fields=('vc_position', 'vc_priority'), extra=0)
+        VCMemberFormSet = modelformset_factory(
+            model=Device,
+            form=forms.DeviceVCMembershipForm,
+            formset=forms.BaseVCMemberFormSet,
+            extra=0
+        )
+        members_queryset = virtual_chassis.members.select_related('rack').order_by('vc_position')
 
 
         vc_form = forms.VirtualChassisForm(request.POST, instance=virtual_chassis)
         vc_form = forms.VirtualChassisForm(request.POST, instance=virtual_chassis)
-        vc_form.fields['master'].queryset = virtual_chassis.members.all()
-        formset = VCMemberFormSet(request.POST, queryset=virtual_chassis.members.all())
+        vc_form.fields['master'].queryset = members_queryset
+        formset = VCMemberFormSet(request.POST, queryset=members_queryset)
 
 
         if vc_form.is_valid() and formset.is_valid():
         if vc_form.is_valid() and formset.is_valid():
 
 
@@ -2207,7 +2225,7 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi
             device = member_select_form.cleaned_data['device']
             device = member_select_form.cleaned_data['device']
             device.virtual_chassis = virtual_chassis
             device.virtual_chassis = virtual_chassis
             data = {k: request.POST[k] for k in ['vc_position', 'vc_priority']}
             data = {k: request.POST[k] for k in ['vc_position', 'vc_priority']}
-            membership_form = forms.DeviceVCMembershipForm(data, instance=device)
+            membership_form = forms.DeviceVCMembershipForm(data, validate_vc_position=True, instance=device)
 
 
             if membership_form.is_valid():
             if membership_form.is_valid():
 
 

+ 13 - 3
netbox/templates/dcim/virtualchassis_edit.html

@@ -62,8 +62,18 @@
                                                 <span class="text-muted">N/A</span>
                                                 <span class="text-muted">N/A</span>
                                             {% endif %}
                                             {% endif %}
                                         </td>
                                         </td>
-                                        <td>{{ form.vc_position }}</td>
-                                        <td>{{ form.vc_priority }}</td>
+                                        <td>
+                                            {{ form.vc_position }}
+                                            {% if form.vc_position.errors %}
+                                                <br /><small class="text-danger">{{ form.vc_position.errors.0 }}</small>
+                                            {% endif %}
+                                        </td>
+                                        <td>
+                                            {{ form.vc_priority }}
+                                            {% if form.vc_priority.errors %}
+                                                <br /><small class="text-danger">{{ form.vc_priority.errors.0 }}</small>
+                                            {% endif %}
+                                        </td>
                                         <td>
                                         <td>
                                             {% if virtual_chassis.pk %}
                                             {% if virtual_chassis.pk %}
                                                 <a href="{% url 'dcim:virtualchassis_remove_member' pk=device.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=virtual_chassis.pk %}" class="btn btn-danger btn-xs{% if virtual_chassis.master == device %} disabled{% endif %}">
                                                 <a href="{% url 'dcim:virtualchassis_remove_member' pk=device.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=virtual_chassis.pk %}" class="btn btn-danger btn-xs{% if virtual_chassis.master == device %} disabled{% endif %}">
@@ -80,7 +90,7 @@
             </div>
             </div>
         </div>
         </div>
         <div class="row">
         <div class="row">
-            <div class="col-md-6 col-md-offset-3 text-right">
+            <div class="col-md-8 col-md-offset-2 text-right">
                 {% if vc_form.instance.pk %}
                 {% if vc_form.instance.pk %}
                     <button type="submit" name="_update" class="btn btn-primary">Update</button>
                     <button type="submit" name="_update" class="btn btn-primary">Update</button>
                 {% else %}
                 {% else %}