Parcourir la source

Introduced DeviceComponentCreateView

Jeremy Stretch il y a 8 ans
Parent
commit
7b06f5e9fc
4 fichiers modifiés avec 190 ajouts et 329 suppressions
  1. 6 5
      netbox/dcim/forms.py
  2. 6 6
      netbox/dcim/urls.py
  3. 176 316
      netbox/dcim/views.py
  4. 2 2
      netbox/templates/dcim/device_component_add.html

+ 6 - 5
netbox/dcim/forms.py

@@ -13,6 +13,7 @@ from utilities.forms import (
     SlugField,
 )
 
+from formfields import MACAddressFormField
 from .models import (
     DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
@@ -1032,12 +1033,12 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         }
 
 
-class InterfaceCreateForm(BootstrapMixin, forms.ModelForm):
+class InterfaceCreateForm(BootstrapMixin, forms.Form):
     name_pattern = ExpandableNameField(label='Name')
-
-    class Meta:
-        model = Interface
-        fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
+    form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
+    mac_address = MACAddressFormField(required=False, label='MAC Address')
+    mgmt_only = forms.BooleanField(required=False, label='OOB Management')
+    description = forms.CharField(max_length=100, required=False)
 
 
 class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):

+ 6 - 6
netbox/dcim/urls.py

@@ -109,7 +109,7 @@ urlpatterns = [
 
     # Console ports
     url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
-    url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
+    url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'),
     url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
     url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
     url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
@@ -118,7 +118,7 @@ urlpatterns = [
 
     # Console server ports
     url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
-    url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
+    url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'),
     url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
     url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
     url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
@@ -127,7 +127,7 @@ urlpatterns = [
 
     # Power ports
     url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
-    url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
+    url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'),
     url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
     url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
     url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
@@ -136,7 +136,7 @@ urlpatterns = [
 
     # Power outlets
     url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
-    url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
+    url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'),
     url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
     url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
     url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
@@ -145,7 +145,7 @@ urlpatterns = [
 
     # Interfaces
     url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
-    url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
+    url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'),
     url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
     url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
     url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
@@ -155,7 +155,7 @@ urlpatterns = [
 
     # Device bays
     url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
-    url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
+    url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'),
     url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
     url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
     url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),

+ 176 - 316
netbox/dcim/views.py

@@ -694,153 +694,73 @@ def device_lldp_neighbors(request, pk):
     })
 
 
-#
-# Bulk device component creation
-#
-
-class DeviceBulkAddComponentView(View):
-    """
-    Add one or more components (e.g. interfaces) to a selected set of Devices.
-    """
-    form = forms.DeviceBulkAddComponentForm
+class DeviceComponentCreateView(View):
     model = None
+    form = None
     model_form = None
 
-    def get(self):
-        return redirect('dcim:device_list')
-
-    def post(self, request):
-
-        # Are we editing *all* objects in the queryset or just a selected subset?
-        if request.POST.get('_all'):
-            pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
-        else:
-            pk_list = [int(pk) for pk in request.POST.getlist('pk')]
-
-        if '_create' in request.POST:
-            form = self.form(request.POST)
-            if form.is_valid():
-
-                new_components = []
-                data = deepcopy(form.cleaned_data)
-                for device in data['pk']:
-
-                    names = data['name_pattern']
-                    for name in names:
-                        component_data = {
-                            'device': device.pk,
-                            'name': name,
-                        }
-                        component_data.update(data)
-                        component_form = self.model_form(component_data)
-                        if component_form.is_valid():
-                            new_components.append(component_form.save(commit=False))
-                        else:
-                            for field, errors in component_form.errors.as_data().items():
-                                for e in errors:
-                                    form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e)))
-
-                if not form.errors:
-                    self.model.objects.bulk_create(new_components)
-                    messages.success(request, u"Added {} {} to {} devices.".format(
-                        len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
-                    ))
-                    return redirect('dcim:device_list')
-
-        else:
-            form = self.form(initial={'pk': pk_list})
+    def get(self, request, pk):
 
-        selected_devices = Device.objects.filter(pk__in=pk_list)
-        if not selected_devices:
-            messages.warning(request, u"No devices were selected.")
-            return redirect('dcim:device_list')
+        device = get_object_or_404(Device, pk=pk)
 
-        return render(request, 'dcim/device_bulk_add_component.html', {
-            'form': form,
-            'component_name': self.model._meta.verbose_name_plural,
-            'selected_devices': selected_devices,
-            'cancel_url': reverse('dcim:device_list'),
+        return render(request, 'dcim/device_component_add.html', {
+            'device': device,
+            'component_type': self.model._meta.verbose_name,
+            'form': self.form(initial=request.GET),
+            'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
         })
 
+    def post(self, request, pk):
 
-class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
-    permission_required = 'dcim.add_consoleport'
-    model = ConsolePort
-    model_form = forms.ConsolePortForm
-
-
-class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
-    permission_required = 'dcim.add_consoleserverport'
-    model = ConsoleServerPort
-    model_form = forms.ConsoleServerPortForm
-
-
-class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
-    permission_required = 'dcim.add_powerport'
-    model = PowerPort
-    model_form = forms.PowerPortForm
-
-
-class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
-    permission_required = 'dcim.add_poweroutlet'
-    model = PowerOutlet
-    model_form = forms.PowerOutletForm
-
-
-class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
-    permission_required = 'dcim.add_interface'
-    form = forms.DeviceBulkAddInterfaceForm
-    model = Interface
-    model_form = forms.InterfaceForm
-
-
-class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
-    permission_required = 'dcim.add_devicebay'
-    model = DeviceBay
-    model_form = forms.DeviceBayForm
-
-
-#
-# Console ports
-#
-
-@permission_required('dcim.add_consoleport')
-def consoleport_add(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
+        device = get_object_or_404(Device, pk=pk)
 
-    if request.method == 'POST':
-        form = forms.ConsolePortCreateForm(request.POST)
+        form = self.form(request.POST)
         if form.is_valid():
 
-            console_ports = []
+            new_components = []
+            data = deepcopy(form.cleaned_data)
+
             for name in form.cleaned_data['name_pattern']:
-                cp_form = forms.ConsolePortForm({
+                component_data = {
                     'device': device.pk,
                     'name': name,
-                })
-                if cp_form.is_valid():
-                    console_ports.append(cp_form.save(commit=False))
+                }
+                component_data.update(data)
+                component_form = self.model_form(component_data)
+                if component_form.is_valid():
+                    new_components.append(component_form.save(commit=False))
                 else:
-                    form.add_error('name_pattern', "Duplicate console port name for this device: {}".format(name))
+                    for field, errors in component_form.errors.as_data().items():
+                        for e in errors:
+                            form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e)))
 
             if not form.errors:
-                ConsolePort.objects.bulk_create(console_ports)
-                messages.success(request, u"Added {} console port(s) to {}.".format(len(console_ports), device))
+                self.model.objects.bulk_create(new_components)
+                messages.success(request, u"Added {} {} to {}.".format(
+                    len(new_components), self.model._meta.verbose_name_plural, device
+                ))
                 if '_addanother' in request.POST:
-                    return redirect('dcim:consoleport_add', pk=device.pk)
+                    return redirect(request.path)
                 else:
                     return redirect('dcim:device', pk=device.pk)
 
-    else:
-        form = forms.ConsolePortCreateForm()
+        return render(request, 'dcim/device_component_add.html', {
+            'device': device,
+            'component_type': self.model._meta.verbose_name,
+            'form': form,
+            'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
+        })
 
-    return render(request, 'dcim/device_component_add.html', {
-        'device': device,
-        'component_type': 'Console Port',
-        'form': form,
-        'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
-    })
+
+#
+# Console ports
+#
+
+class ConsolePortAddView(PermissionRequiredMixin, DeviceComponentCreateView):
+    permission_required = 'dcim.add_consoleport'
+    model = ConsolePort
+    form = forms.ConsolePortCreateForm
+    model_form = forms.ConsolePortForm
 
 
 @permission_required('dcim.change_consoleport')
@@ -930,44 +850,11 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
 # Console server ports
 #
 
-@permission_required('dcim.add_consoleserverport')
-def consoleserverport_add(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-
-    if request.method == 'POST':
-        form = forms.ConsoleServerPortCreateForm(request.POST)
-        if form.is_valid():
-
-            cs_ports = []
-            for name in form.cleaned_data['name_pattern']:
-                csp_form = forms.ConsoleServerPortForm({
-                    'device': device.pk,
-                    'name': name,
-                })
-                if csp_form.is_valid():
-                    cs_ports.append(csp_form.save(commit=False))
-                else:
-                    form.add_error('name_pattern', "Duplicate console server port name for this device: {}"
-                                   .format(name))
-
-            if not form.errors:
-                ConsoleServerPort.objects.bulk_create(cs_ports)
-                messages.success(request, u"Added {} console server port(s) to {}.".format(len(cs_ports), device))
-                if '_addanother' in request.POST:
-                    return redirect('dcim:consoleserverport_add', pk=device.pk)
-                else:
-                    return redirect('dcim:device', pk=device.pk)
-
-    else:
-        form = forms.ConsoleServerPortCreateForm()
-
-    return render(request, 'dcim/device_component_add.html', {
-        'device': device,
-        'component_type': 'Console Server Port',
-        'form': form,
-        'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
-    })
+class ConsoleServerPortAddView(PermissionRequiredMixin, DeviceComponentCreateView):
+    permission_required = 'dcim.add_consoleserverport'
+    model = ConsoleServerPort
+    form = forms.ConsoleServerPortCreateForm
+    model_form = forms.ConsoleServerPortForm
 
 
 @permission_required('dcim.change_consoleserverport')
@@ -1051,43 +938,11 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Power ports
 #
 
-@permission_required('dcim.add_powerport')
-def powerport_add(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-
-    if request.method == 'POST':
-        form = forms.PowerPortCreateForm(request.POST)
-        if form.is_valid():
-
-            power_ports = []
-            for name in form.cleaned_data['name_pattern']:
-                pp_form = forms.PowerPortForm({
-                    'device': device.pk,
-                    'name': name,
-                })
-                if pp_form.is_valid():
-                    power_ports.append(pp_form.save(commit=False))
-                else:
-                    form.add_error('name_pattern', "Duplicate power port name for this device: {}".format(name))
-
-            if not form.errors:
-                PowerPort.objects.bulk_create(power_ports)
-                messages.success(request, u"Added {} power port(s) to {}.".format(len(power_ports), device))
-                if '_addanother' in request.POST:
-                    return redirect('dcim:powerport_add', pk=device.pk)
-                else:
-                    return redirect('dcim:device', pk=device.pk)
-
-    else:
-        form = forms.PowerPortCreateForm()
-
-    return render(request, 'dcim/device_component_add.html', {
-        'device': device,
-        'component_type': 'Power Port',
-        'form': form,
-        'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
-    })
+class PowerPortAddView(PermissionRequiredMixin, DeviceComponentCreateView):
+    permission_required = 'dcim.add_powerport'
+    model = PowerPort
+    form = forms.PowerPortCreateForm
+    model_form = forms.PowerPortForm
 
 
 @permission_required('dcim.change_powerport')
@@ -1177,43 +1032,11 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
 # Power outlets
 #
 
-@permission_required('dcim.add_poweroutlet')
-def poweroutlet_add(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-
-    if request.method == 'POST':
-        form = forms.PowerOutletCreateForm(request.POST)
-        if form.is_valid():
-
-            power_outlets = []
-            for name in form.cleaned_data['name_pattern']:
-                po_form = forms.PowerOutletForm({
-                    'device': device.pk,
-                    'name': name,
-                })
-                if po_form.is_valid():
-                    power_outlets.append(po_form.save(commit=False))
-                else:
-                    form.add_error('name_pattern', "Duplicate power outlet name for this device: {}".format(name))
-
-            if not form.errors:
-                PowerOutlet.objects.bulk_create(power_outlets)
-                messages.success(request, u"Added {} power outlet(s) to {}.".format(len(power_outlets), device))
-                if '_addanother' in request.POST:
-                    return redirect('dcim:poweroutlet_add', pk=device.pk)
-                else:
-                    return redirect('dcim:device', pk=device.pk)
-
-    else:
-        form = forms.PowerOutletCreateForm()
-
-    return render(request, 'dcim/device_component_add.html', {
-        'device': device,
-        'component_type': 'Power Outlet',
-        'form': form,
-        'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
-    })
+class PowerOutletAddView(PermissionRequiredMixin, DeviceComponentCreateView):
+    permission_required = 'dcim.add_poweroutlet'
+    model = PowerOutlet
+    form = forms.PowerOutletCreateForm
+    model_form = forms.PowerOutletForm
 
 
 @permission_required('dcim.change_poweroutlet')
@@ -1296,47 +1119,11 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Interfaces
 #
 
-@permission_required('dcim.add_interface')
-def interface_add(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-
-    if request.method == 'POST':
-        form = forms.InterfaceCreateForm(request.POST)
-        if form.is_valid():
-
-            interfaces = []
-            for name in form.cleaned_data['name_pattern']:
-                iface_form = forms.InterfaceForm({
-                    'device': device.pk,
-                    'name': name,
-                    'form_factor': form.cleaned_data['form_factor'],
-                    'mac_address': form.cleaned_data['mac_address'],
-                    'mgmt_only': form.cleaned_data['mgmt_only'],
-                    'description': form.cleaned_data['description'],
-                })
-                if iface_form.is_valid():
-                    interfaces.append(iface_form.save(commit=False))
-                else:
-                    form.add_error('name_pattern', "Duplicate interface name for this device: {}".format(name))
-
-            if not form.errors:
-                Interface.objects.bulk_create(interfaces)
-                messages.success(request, u"Added {} interface(s) to {}.".format(len(interfaces), device))
-                if '_addanother' in request.POST:
-                    return redirect('dcim:interface_add', pk=device.pk)
-                else:
-                    return redirect('dcim:device', pk=device.pk)
-
-    else:
-        form = forms.InterfaceCreateForm(initial={'mgmt_only': request.GET.get('mgmt_only')})
-
-    return render(request, 'dcim/device_component_add.html', {
-        'device': device,
-        'component_type': 'Interface',
-        'form': form,
-        'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
-    })
+class InterfaceAddView(PermissionRequiredMixin, DeviceComponentCreateView):
+    permission_required = 'dcim.add_interface'
+    model = Interface
+    form = forms.InterfaceCreateForm
+    model_form = forms.InterfaceForm
 
 
 class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
@@ -1368,44 +1155,11 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Device bays
 #
 
-@permission_required('dcim.add_devicebay')
-def devicebay_add(request, pk):
-
-    device = get_object_or_404(Device, pk=pk)
-
-    if request.method == 'POST':
-        form = forms.DeviceBayCreateForm(request.POST)
-        if form.is_valid():
-
-            device_bays = []
-            for name in form.cleaned_data['name_pattern']:
-                devicebay_form = forms.DeviceBayForm({
-                    'device': device.pk,
-                    'name': name,
-                })
-                if devicebay_form.is_valid():
-                    device_bays.append(devicebay_form.save(commit=False))
-                else:
-                    for err in devicebay_form.errors.get('__all__', []):
-                        form.add_error('name_pattern', err)
-
-            if not form.errors:
-                DeviceBay.objects.bulk_create(device_bays)
-                messages.success(request, u"Added {} device bay(s) to {}.".format(len(device_bays), device))
-                if '_addanother' in request.POST:
-                    return redirect('dcim:devicebay_add', pk=device.pk)
-                else:
-                    return redirect('dcim:device', pk=device.pk)
-
-    else:
-        form = forms.DeviceBayCreateForm()
-
-    return render(request, 'dcim/device_component_add.html', {
-        'device': device,
-        'component_type': 'Device Bay',
-        'form': form,
-        'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
-    })
+class DeviceBayAddView(PermissionRequiredMixin, DeviceComponentCreateView):
+    permission_required = 'dcim.add_devicebay'
+    model = DeviceBay
+    form = forms.DeviceBayCreateForm
+    model_form = forms.DeviceBayForm
 
 
 class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
@@ -1476,6 +1230,112 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 
 
 #
+# Bulk device component creation
+#
+
+class DeviceBulkAddComponentView(View):
+    """
+    Add one or more components (e.g. interfaces) to a selected set of Devices.
+    """
+    form = forms.DeviceBulkAddComponentForm
+    model = None
+    model_form = None
+
+    def get(self):
+        return redirect('dcim:device_list')
+
+    def post(self, request):
+
+        # Are we editing *all* objects in the queryset or just a selected subset?
+        if request.POST.get('_all'):
+            pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
+        else:
+            pk_list = [int(pk) for pk in request.POST.getlist('pk')]
+
+        if '_create' in request.POST:
+            form = self.form(request.POST)
+            if form.is_valid():
+
+                new_components = []
+                data = deepcopy(form.cleaned_data)
+                for device in data['pk']:
+
+                    names = data['name_pattern']
+                    for name in names:
+                        component_data = {
+                            'device': device.pk,
+                            'name': name,
+                        }
+                        component_data.update(data)
+                        component_form = self.model_form(component_data)
+                        if component_form.is_valid():
+                            new_components.append(component_form.save(commit=False))
+                        else:
+                            for field, errors in component_form.errors.as_data().items():
+                                for e in errors:
+                                    form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e)))
+
+                if not form.errors:
+                    self.model.objects.bulk_create(new_components)
+                    messages.success(request, u"Added {} {} to {} devices.".format(
+                        len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
+                    ))
+                    return redirect('dcim:device_list')
+
+        else:
+            form = self.form(initial={'pk': pk_list})
+
+        selected_devices = Device.objects.filter(pk__in=pk_list)
+        if not selected_devices:
+            messages.warning(request, u"No devices were selected.")
+            return redirect('dcim:device_list')
+
+        return render(request, 'dcim/device_bulk_add_component.html', {
+            'form': form,
+            'component_name': self.model._meta.verbose_name_plural,
+            'selected_devices': selected_devices,
+            'cancel_url': reverse('dcim:device_list'),
+        })
+
+
+class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
+    permission_required = 'dcim.add_consoleport'
+    model = ConsolePort
+    model_form = forms.ConsolePortForm
+
+
+class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
+    permission_required = 'dcim.add_consoleserverport'
+    model = ConsoleServerPort
+    model_form = forms.ConsoleServerPortForm
+
+
+class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
+    permission_required = 'dcim.add_powerport'
+    model = PowerPort
+    model_form = forms.PowerPortForm
+
+
+class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
+    permission_required = 'dcim.add_poweroutlet'
+    model = PowerOutlet
+    model_form = forms.PowerOutletForm
+
+
+class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
+    permission_required = 'dcim.add_interface'
+    form = forms.DeviceBulkAddInterfaceForm
+    model = Interface
+    model_form = forms.InterfaceForm
+
+
+class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
+    permission_required = 'dcim.add_devicebay'
+    model = DeviceBay
+    model_form = forms.DeviceBayForm
+
+
+#
 # Interface connections
 #
 

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

@@ -3,7 +3,7 @@
 
 {% block title %}Create {{ component_type }} ({{ device }}){% endblock %}
 
-{% block content %}
+{% block content %}{{ form.errors }}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
     <div class="row">
@@ -18,7 +18,7 @@
             {% endif %}
             <div class="panel panel-default">
                 <div class="panel-heading">
-                    <strong>{{ component_type }}</strong>
+                    <strong>{{ component_type|title }}</strong>
                 </div>
                 <div class="panel-body">
                     <div class="form-group">