Browse Source

Added virtual chassis member add view

Jeremy Stretch 7 years ago
parent
commit
f1da517c84

+ 3 - 0
netbox/dcim/apps.py

@@ -6,3 +6,6 @@ from django.apps import AppConfig
 class DCIMConfig(AppConfig):
     name = "dcim"
     verbose_name = "DCIM"
+
+    def ready(self):
+        import dcim.signals

+ 51 - 35
netbox/dcim/forms.py

@@ -2276,38 +2276,54 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
         fields = ['master', 'domain']
 
 
-# class VCAddMemberForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
-#     site = forms.ModelChoiceField(
-#         queryset=Site.objects.all(),
-#         label='Site',
-#         required=False,
-#         widget=forms.Select(
-#             attrs={'filter-for': 'rack'}
-#         )
-#     )
-#     rack = ChainedModelChoiceField(
-#         queryset=Rack.objects.all(),
-#         chains=(
-#             ('site', 'site'),
-#         ),
-#         label='Rack',
-#         required=False,
-#         widget=APISelect(
-#             api_url='/api/dcim/racks/?site_id={{site}}',
-#             attrs={'filter-for': 'device', 'nullable': 'true'}
-#         )
-#     )
-#     device = ChainedModelChoiceField(
-#         queryset=Device.objects.all(),
-#         chains=(
-#             ('site', 'site'),
-#             ('rack', 'rack'),
-#         ),
-#         label='Device',
-#         widget=APISelect(
-#             api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
-#             display_field='display_name'
-#         )
-#     )
-#     vc_position = forms.IntegerField(label='Position')
-#     vc_priority = forms.IntegerField(required=False, label='Priority')
+class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
+    site = forms.ModelChoiceField(
+        queryset=Site.objects.all(),
+        label='Site',
+        required=False,
+        widget=forms.Select(
+            attrs={'filter-for': 'rack'}
+        )
+    )
+    rack = ChainedModelChoiceField(
+        queryset=Rack.objects.all(),
+        chains=(
+            ('site', 'site'),
+        ),
+        label='Rack',
+        required=False,
+        widget=APISelect(
+            api_url='/api/dcim/racks/?site_id={{site}}',
+            attrs={'filter-for': 'device', 'nullable': 'true'}
+        )
+    )
+    device = ChainedModelChoiceField(
+        queryset=Device.objects.all(),
+        chains=(
+            ('site', 'site'),
+            ('rack', 'rack'),
+        ),
+        label='Device',
+        widget=APISelect(
+            api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
+            display_field='display_name'
+        )
+    )
+
+
+class DeviceVCMembershipForm(forms.ModelForm):
+
+    class Meta:
+        model = Device
+        fields = ['vc_position', 'vc_priority']
+        labels = {
+            'vc_position': 'Position',
+            'vc_priority': 'Priority',
+        }
+
+    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

+ 6 - 0
netbox/dcim/models.py

@@ -1006,6 +1006,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
                 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
             })
 
+        # Validate virtual chassis assignment
+        if self.virtual_chassis and not self.vc_position:
+            raise ValidationError({
+                'vc_position': "A device assigned to a virtual chassis must have its position defined."
+            })
+
     def save(self, *args, **kwargs):
 
         is_new = not bool(self.pk)

+ 14 - 0
netbox/dcim/signals.py

@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+
+from django.db.models.signals import pre_delete
+from django.dispatch import receiver
+
+from .models import Device, VirtualChassis
+
+
+@receiver(pre_delete, sender=VirtualChassis)
+def clear_virtualchassis_members(instance, **kwargs):
+    """
+    When a VirtualChassis is deleted, nullify the vc_position and vc_priority fields of its prior members.
+    """
+    Device.objects.filter(virtual_chassis=instance.pk).update(vc_position=None, vc_priority=None)

+ 1 - 1
netbox/dcim/urls.py

@@ -220,6 +220,6 @@ urlpatterns = [
     url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
     url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
     url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
-    # url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
+    url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
 
 ]

+ 56 - 1
netbox/dcim/views.py

@@ -2158,7 +2158,7 @@ class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
 
             return redirect(vc_form.cleaned_data['master'].get_absolute_url())
 
-        return render(request, 'dcim/virtualchassis_add.html', {
+        return render(request, 'dcim/virtualchassis_edit.html', {
             'vc_form': vc_form,
             'formset': formset,
             'return_url': self.get_return_url(request, virtual_chassis),
@@ -2169,3 +2169,58 @@ class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     permission_required = 'dcim.delete_virtualchassis'
     model = VirtualChassis
     default_return_url = 'dcim:device_list'
+
+
+class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
+    permission_required = 'dcim.change_device'
+
+    def get(self, request, pk):
+
+        virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
+
+        initial_data = {k: request.GET[k] for k in request.GET}
+        member_select_form = forms.VCMemberSelectForm(initial=initial_data)
+        membership_form = forms.DeviceVCMembershipForm(initial=initial_data)
+
+        return render(request, 'dcim/virtualchassis_add_member.html', {
+            'virtual_chassis': virtual_chassis,
+            'member_select_form': member_select_form,
+            'membership_form': membership_form,
+            'return_url': self.get_return_url(request, virtual_chassis),
+        })
+
+    def post(self, request, pk):
+
+        virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
+
+        member_select_form = forms.VCMemberSelectForm(request.POST)
+
+        if member_select_form.is_valid():
+
+            device = member_select_form.cleaned_data['device']
+            device.virtual_chassis = virtual_chassis
+            data = {k: request.POST[k] for k in ['vc_position', 'vc_priority']}
+            membership_form = forms.DeviceVCMembershipForm(data, instance=device)
+
+            if membership_form.is_valid():
+
+                membership_form.save()
+                msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
+                messages.success(request, mark_safe(msg))
+                UserAction.objects.log_edit(request.user, device, msg)
+
+                if '_addanother' in request.POST:
+                    return redirect(request.get_full_path())
+
+                return redirect(self.get_return_url(request, device))
+
+        else:
+
+            membership_form = forms.DeviceVCMembershipForm(request.POST)
+
+        return render(request, 'dcim/virtualchassis_add_member.html', {
+            'virtual_chassis': virtual_chassis,
+            'member_select_form': member_select_form,
+            'membership_form': membership_form,
+            'return_url': self.get_return_url(request, virtual_chassis),
+        })

+ 35 - 0
netbox/templates/dcim/virtualchassis_add_member.html

@@ -0,0 +1,35 @@
+{% extends '_base.html' %}
+{% load form_helpers %}
+
+{% block content %}
+    <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
+        {% csrf_token %}
+        <div class="row">
+            <div class="col-md-6 col-md-offset-3">
+                <h3>{% block title %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblock %}</h3>
+                {% if membership_form.non_field_errors %}
+                    <div class="panel panel-danger">
+                        <div class="panel-heading"><strong>Errors</strong></div>
+                        <div class="panel-body">
+                            {{ membership_form.non_field_errors }}
+                        </div>
+                    </div>
+                {% endif %}
+                <div class="panel panel-default">
+                    <div class="panel-heading"><strong>Add New Member</strong></div>
+                    <div class="table panel-body">
+                        {% render_form member_select_form %}
+                        {% render_form membership_form %}
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col-md-6 col-md-offset-3 text-right">
+                <button type="submit" name="_save" class="btn btn-primary">Save</button>
+                <button type="submit" name="_addanother" class="btn btn-primary">Add Another</button>
+                <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
+            </div>
+        </div>
+    </form>
+{% endblock %}

+ 2 - 2
netbox/templates/dcim/virtualchassis_edit.html

@@ -35,8 +35,8 @@
                         </thead>
                         <tbody>
                             {% for form in formset %}
-                                {% for hidden in form.hidden_fields %}
-                                    {{ hidden }}
+                                {% for field in form.hidden_fields %}
+                                    {{ field }}
                                 {% endfor %}
                                 <tr>
                                     <td>{{ form.instance.name }}</td>