Parcourir la source

Merge branch 'develop' into v2-develop

Jeremy Stretch il y a 8 ans
Parent
commit
5ca87c0f20
34 fichiers modifiés avec 888 ajouts et 861 suppressions
  1. 3 1
      README.md
  2. 12 3
      netbox/dcim/forms.py
  3. 121 48
      netbox/dcim/views.py
  4. 16 4
      netbox/ipam/filters.py
  5. 54 3
      netbox/ipam/forms.py
  6. 2 1
      netbox/secrets/views.py
  7. 51 66
      netbox/templates/circuits/circuit_import.html
  8. 41 56
      netbox/templates/circuits/provider_import.html
  9. 41 55
      netbox/templates/dcim/console_connections_import.html
  10. 6 2
      netbox/templates/dcim/device_import.html
  11. 6 2
      netbox/templates/dcim/device_import_child.html
  12. 3 2
      netbox/templates/dcim/inc/consoleport.html
  13. 3 2
      netbox/templates/dcim/inc/consoleserverport.html
  14. 5 4
      netbox/templates/dcim/inc/interface.html
  15. 3 2
      netbox/templates/dcim/inc/poweroutlet.html
  16. 3 2
      netbox/templates/dcim/inc/powerport.html
  17. 41 63
      netbox/templates/dcim/interface_connections_import.html
  18. 41 55
      netbox/templates/dcim/power_connections_import.html
  19. 66 81
      netbox/templates/dcim/rack_import.html
  20. 9 8
      netbox/templates/import_success.html
  21. 36 51
      netbox/templates/ipam/aggregate_import.html
  22. 1 0
      netbox/templates/ipam/ipaddress_edit.html
  23. 56 71
      netbox/templates/ipam/ipaddress_import.html
  24. 66 81
      netbox/templates/ipam/prefix_import.html
  25. 56 71
      netbox/templates/ipam/vlan_import.html
  26. 41 56
      netbox/templates/ipam/vrf_import.html
  27. 8 4
      netbox/templates/secrets/secret_import.html
  28. 1 1
      netbox/templates/table.html
  29. 36 51
      netbox/templates/tenancy/tenant_import.html
  30. 34 0
      netbox/templates/utilities/obj_import.html
  31. 7 5
      netbox/utilities/error_handlers.py
  32. 13 0
      netbox/utilities/forms.py
  33. 5 9
      netbox/utilities/views.py
  34. 1 1
      requirements.txt

+ 3 - 1
README.md

@@ -10,7 +10,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
 
 ### Build Status
 
-|             | python 2.7 |
+NetBox is built against both Python 2.7 and 3.5.  Python 3.5 is recommended.
+
+|             | status |
 |-------------|------------|
 | **master** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) |
 | **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) |

+ 12 - 3
netbox/dcim/forms.py

@@ -10,9 +10,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
 from ipam.models import IPAddress
 from tenancy.models import Tenant
 from utilities.forms import (
-    APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
-    CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
-    SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
+    APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
+    BulkImportForm, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField,
+    Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
 )
 
 from .formfields import MACAddressFormField
@@ -271,6 +271,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
     width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
     u_height = forms.IntegerField(required=False, label='Height (U)')
+    desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units')
     comments = CommentField(widget=SmallTextarea)
 
     class Meta:
@@ -374,7 +375,13 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
     manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
     u_height = forms.IntegerField(min_value=1, required=False)
+    is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
     interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
+    is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
+    is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
+    is_network_device = forms.NullBooleanField(
+        required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
+    )
 
     class Meta:
         nullable_fields = []
@@ -483,6 +490,7 @@ class InterfaceTemplateCreateForm(DeviceComponentForm):
 class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
     form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
+    mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
 
     class Meta:
         nullable_fields = []
@@ -1415,6 +1423,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
     device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
     lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
     form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
+    mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
     description = forms.CharField(max_length=100, required=False)
 
     class Meta:

+ 121 - 48
netbox/dcim/views.py

@@ -12,11 +12,12 @@ from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.http import urlencode
+from django.utils.safestring import mark_safe
 from django.views.generic import View
 
 from ipam.models import Prefix, Service, VLAN
 from circuits.models import Circuit
-from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
+from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
@@ -892,12 +893,16 @@ def consoleport_connect(request, pk):
         form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
         if form.is_valid():
             consoleport = form.save()
-            messages.success(request, u"Connected {} {} to {} {}.".format(
-                consoleport.device,
-                consoleport.name,
-                consoleport.cs_port.device,
-                consoleport.cs_port.name,
-            ))
+            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
+                consoleport.device.get_absolute_url(),
+                escape(consoleport.device),
+                escape(consoleport.name),
+                consoleport.cs_port.device.get_absolute_url(),
+                escape(consoleport.cs_port.device),
+                escape(consoleport.cs_port.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, consoleport, msg)
             return redirect('dcim:device', pk=consoleport.device.pk)
 
     else:
@@ -921,17 +926,28 @@ def consoleport_disconnect(request, pk):
     consoleport = get_object_or_404(ConsolePort, pk=pk)
 
     if not consoleport.cs_port:
-        messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything."
-                         .format(consoleport))
+        messages.warning(
+            request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
+        )
         return redirect('dcim:device', pk=consoleport.device.pk)
 
     if request.method == 'POST':
         form = ConfirmationForm(request.POST)
         if form.is_valid():
+            cs_port = consoleport.cs_port
             consoleport.cs_port = None
             consoleport.connection_status = None
             consoleport.save()
-            messages.success(request, u"Console port {} has been disconnected.".format(consoleport))
+            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
+                consoleport.device.get_absolute_url(),
+                escape(consoleport.device),
+                escape(consoleport.name),
+                cs_port.device.get_absolute_url(),
+                escape(cs_port.device),
+                escape(cs_port.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, consoleport, msg)
             return redirect('dcim:device', pk=consoleport.device.pk)
 
     else:
@@ -966,6 +982,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
     form = forms.ConsoleConnectionImportForm
     table = tables.ConsoleConnectionTable
     template_name = 'dcim/console_connections_import.html'
+    default_return_url = 'dcim:console_connections_list'
 
 
 #
@@ -993,12 +1010,16 @@ def consoleserverport_connect(request, pk):
             consoleport.cs_port = consoleserverport
             consoleport.connection_status = form.cleaned_data['connection_status']
             consoleport.save()
-            messages.success(request, u"Connected {} {} to {} {}.".format(
-                consoleport.device,
-                consoleport.name,
-                consoleserverport.device,
-                consoleserverport.name,
-            ))
+            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
+                consoleport.device.get_absolute_url(),
+                escape(consoleport.device),
+                escape(consoleport.name),
+                consoleserverport.device.get_absolute_url(),
+                escape(consoleserverport.device),
+                escape(consoleserverport.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, consoleport, msg)
             return redirect('dcim:device', pk=consoleserverport.device.pk)
 
     else:
@@ -1022,8 +1043,9 @@ def consoleserverport_disconnect(request, pk):
     consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
 
     if not hasattr(consoleserverport, 'connected_console'):
-        messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it."
-                         .format(consoleserverport))
+        messages.warning(
+            request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
+        )
         return redirect('dcim:device', pk=consoleserverport.device.pk)
 
     if request.method == 'POST':
@@ -1033,7 +1055,16 @@ def consoleserverport_disconnect(request, pk):
             consoleport.cs_port = None
             consoleport.connection_status = None
             consoleport.save()
-            messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport))
+            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
+                consoleport.device.get_absolute_url(),
+                escape(consoleport.device),
+                escape(consoleport.name),
+                consoleserverport.device.get_absolute_url(),
+                escape(consoleserverport.device),
+                escape(consoleserverport.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, consoleport, msg)
             return redirect('dcim:device', pk=consoleserverport.device.pk)
 
     else:
@@ -1085,12 +1116,16 @@ def powerport_connect(request, pk):
         form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
         if form.is_valid():
             powerport = form.save()
-            messages.success(request, u"Connected {} {} to {} {}.".format(
-                powerport.device,
-                powerport.name,
-                powerport.power_outlet.device,
-                powerport.power_outlet.name,
-            ))
+            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
+                powerport.device.get_absolute_url(),
+                escape(powerport.device),
+                escape(powerport.name),
+                powerport.power_outlet.device.get_absolute_url(),
+                escape(powerport.power_outlet.device),
+                escape(powerport.power_outlet.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, powerport, msg)
             return redirect('dcim:device', pk=powerport.device.pk)
 
     else:
@@ -1114,17 +1149,28 @@ def powerport_disconnect(request, pk):
     powerport = get_object_or_404(PowerPort, pk=pk)
 
     if not powerport.power_outlet:
-        messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet."
-                         .format(powerport))
+        messages.warning(
+            request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
+        )
         return redirect('dcim:device', pk=powerport.device.pk)
 
     if request.method == 'POST':
         form = ConfirmationForm(request.POST)
         if form.is_valid():
+            power_outlet = powerport.power_outlet
             powerport.power_outlet = None
             powerport.connection_status = None
             powerport.save()
-            messages.success(request, u"Power port {} has been disconnected.".format(powerport))
+            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
+                powerport.device.get_absolute_url(),
+                escape(powerport.device),
+                escape(powerport.name),
+                power_outlet.device.get_absolute_url(),
+                escape(power_outlet.device),
+                escape(power_outlet.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, powerport, msg)
             return redirect('dcim:device', pk=powerport.device.pk)
 
     else:
@@ -1159,6 +1205,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
     form = forms.PowerConnectionImportForm
     table = tables.PowerConnectionTable
     template_name = 'dcim/power_connections_import.html'
+    default_return_url = 'dcim:power_connections_list'
 
 
 #
@@ -1186,12 +1233,16 @@ def poweroutlet_connect(request, pk):
             powerport.power_outlet = poweroutlet
             powerport.connection_status = form.cleaned_data['connection_status']
             powerport.save()
-            messages.success(request, u"Connected {} {} to {} {}.".format(
-                powerport.device,
-                powerport.name,
-                poweroutlet.device,
-                poweroutlet.name,
-            ))
+            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
+                powerport.device.get_absolute_url(),
+                escape(powerport.device),
+                escape(powerport.name),
+                poweroutlet.device.get_absolute_url(),
+                escape(poweroutlet.device),
+                escape(poweroutlet.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, powerport, msg)
             return redirect('dcim:device', pk=poweroutlet.device.pk)
 
     else:
@@ -1215,7 +1266,9 @@ def poweroutlet_disconnect(request, pk):
     poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
 
     if not hasattr(poweroutlet, 'connected_port'):
-        messages.warning(request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet))
+        messages.warning(
+            request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
+        )
         return redirect('dcim:device', pk=poweroutlet.device.pk)
 
     if request.method == 'POST':
@@ -1225,7 +1278,16 @@ def poweroutlet_disconnect(request, pk):
             powerport.power_outlet = None
             powerport.connection_status = None
             powerport.save()
-            messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet))
+            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
+                powerport.device.get_absolute_url(),
+                escape(powerport.device),
+                escape(powerport.name),
+                poweroutlet.device.get_absolute_url(),
+                escape(poweroutlet.device),
+                escape(poweroutlet.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, powerport, msg)
             return redirect('dcim:device', pk=poweroutlet.device.pk)
 
     else:
@@ -1491,13 +1553,19 @@ def interfaceconnection_add(request, pk):
     if request.method == 'POST':
         form = forms.InterfaceConnectionForm(device, request.POST)
         if form.is_valid():
+
             interfaceconnection = form.save()
-            messages.success(request, u"Connected {} {} to {} {}.".format(
-                interfaceconnection.interface_a.device,
-                interfaceconnection.interface_a,
-                interfaceconnection.interface_b.device,
-                interfaceconnection.interface_b,
-            ))
+            msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
+                interfaceconnection.interface_a.device.get_absolute_url(),
+                escape(interfaceconnection.interface_a.device),
+                escape(interfaceconnection.interface_a.name),
+                interfaceconnection.interface_b.device.get_absolute_url(),
+                escape(interfaceconnection.interface_b.device),
+                escape(interfaceconnection.interface_b.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, interfaceconnection, msg)
+
             if '_addanother' in request.POST:
                 base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
                 device_b = interfaceconnection.interface_b.device
@@ -1535,12 +1603,16 @@ def interfaceconnection_delete(request, pk):
         form = forms.InterfaceConnectionDeletionForm(request.POST)
         if form.is_valid():
             interfaceconnection.delete()
-            messages.success(request, u"Deleted the connection between {} {} and {} {}.".format(
-                interfaceconnection.interface_a.device,
-                interfaceconnection.interface_a,
-                interfaceconnection.interface_b.device,
-                interfaceconnection.interface_b,
-            ))
+            msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
+                interfaceconnection.interface_a.device.get_absolute_url(),
+                escape(interfaceconnection.interface_a.device),
+                escape(interfaceconnection.interface_a.name),
+                interfaceconnection.interface_b.device.get_absolute_url(),
+                escape(interfaceconnection.interface_b.device),
+                escape(interfaceconnection.interface_b.name),
+            )
+            messages.success(request, mark_safe(msg))
+            UserAction.objects.log_edit(request.user, interfaceconnection, msg)
             if form.cleaned_data['device']:
                 return redirect('dcim:device', pk=form.cleaned_data['device'].pk)
             else:
@@ -1570,6 +1642,7 @@ class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView
     form = forms.InterfaceConnectionImportForm
     table = tables.InterfaceConnectionTable
     template_name = 'dcim/interface_connections_import.html'
+    default_return_url = 'dcim:interface_connections_list'
 
 
 #

+ 16 - 4
netbox/ipam/filters.py

@@ -9,7 +9,10 @@ from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
 
-from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
+from .models import (
+    Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
+    VLAN_STATUS_CHOICES, VLANGroup, VRF,
+)
 
 
 class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -153,10 +156,13 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Role (slug)',
     )
+    status = django_filters.MultipleChoiceFilter(
+        choices=PREFIX_STATUS_CHOICES
+    )
 
     class Meta:
         model = Prefix
-        fields = ['family', 'status']
+        fields = ['family']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -237,10 +243,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
         queryset=Interface.objects.all(),
         label='Interface (ID)',
     )
+    status = django_filters.MultipleChoiceFilter(
+        choices=IPADDRESS_STATUS_CHOICES
+    )
 
     class Meta:
         model = IPAddress
-        fields = ['family', 'status']
+        fields = ['family']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -337,10 +346,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Role (slug)',
     )
+    status = django_filters.MultipleChoiceFilter(
+        choices=VLAN_STATUS_CHOICES
+    )
 
     class Meta:
         model = VLAN
-        fields = ['name', 'vid', 'status']
+        fields = ['name', 'vid']
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 54 - 3
netbox/ipam/forms.py

@@ -5,8 +5,8 @@ from dcim.models import Site, Rack, Device, Interface
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from tenancy.models import Tenant
 from utilities.forms import (
-    APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
-    ReturnURLForm, SlugField, add_blank_choice,
+    APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, CSVDataField, ExpandableIPAddressField,
+    FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
 )
 
 from .models import (
@@ -61,6 +61,9 @@ class VRFImportForm(BootstrapMixin, BulkImportForm):
 class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
     tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
+    enforce_unique = forms.NullBooleanField(
+        required=False, widget=BulkEditNullBooleanSelect, label='Enforce unique space'
+    )
     description = forms.CharField(max_length=100, required=False)
 
     class Meta:
@@ -256,6 +259,7 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
     tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
+    is_pool = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a pool')
     description = forms.CharField(max_length=100, required=False)
 
     class Meta:
@@ -340,10 +344,11 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
             query_key='q', query_url='ipam-api:ipaddress-list', field_to_update='nat_inside', obj_label='address'
         )
     )
+    primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
 
     class Meta:
         model = IPAddress
-        fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'nat_inside', 'description']
+        fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside']
         widgets = {
             'interface': APISelect(api_url='/api/dcim/devices/interfaces/?device_id={{interface_device}}'),
             'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
@@ -384,6 +389,15 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
         else:
             self.fields['interface'].choices = []
 
+        # Initialize primary_for_device if IP address is already assigned
+        if self.instance.interface is not None:
+            device = self.instance.interface.device
+            if (
+                self.instance.address.version == 4 and device.primary_ip4 == self.instance or
+                self.instance.address.version == 6 and device.primary_ip6 == self.instance
+            ):
+                self.initial['primary_for_device'] = True
+
         if self.instance.nat_inside:
             nat_inside = self.instance.nat_inside
             # If the IP is assigned to an interface, populate site/device fields accordingly
@@ -416,6 +430,43 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
             else:
                 self.fields['nat_inside'].choices = []
 
+    def clean(self):
+        super(IPAddressForm, self).clean()
+
+        # Primary IP assignment is only available if an interface has been assigned.
+        if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
+            self.add_error(
+                'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
+            )
+
+    def save(self, *args, **kwargs):
+
+        ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
+
+        # Assign this IPAddress as the primary for the associated Device.
+        if self.cleaned_data['primary_for_device']:
+            device = self.cleaned_data['interface'].device
+            if ipaddress.address.version == 4:
+                device.primary_ip4 = ipaddress
+            else:
+                device.primary_ip6 = ipaddress
+            device.save()
+
+        # Clear assignment as primary for device if set.
+        else:
+            try:
+                if ipaddress.address.version == 4:
+                    device = ipaddress.primary_ip4_for
+                    device.primary_ip4 = None
+                else:
+                    device = ipaddress.primary_ip6_for
+                    device.primary_ip6 = None
+                device.save()
+            except Device.DoesNotExist:
+                pass
+
+        return ipaddress
+
 
 class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
     address_pattern = ExpandableIPAddressField(label='Address Pattern')

+ 2 - 1
netbox/secrets/views.py

@@ -221,6 +221,7 @@ def secret_import(request):
 
                     return render(request, 'import_success.html', {
                         'table': table,
+                        'return_url': 'secrets:secret_list',
                     })
 
                 except IntegrityError as e:
@@ -231,7 +232,7 @@ def secret_import(request):
 
     return render(request, 'secrets/secret_import.html', {
         'form': form,
-        'return_url': reverse('secrets:secret_list'),
+        'return_url': 'secrets:secret_list',
     })
 
 

+ 51 - 66
netbox/templates/circuits/circuit_import.html

@@ -1,72 +1,57 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Circuit Import{% endblock %}
 
-{% block content %}
-<h1>Circuit Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Circuit ID</td>
-					<td>Alphanumeric circuit identifier</td>
-					<td>IC-603122</td>
-				</tr>
-				<tr>
-					<td>Provider</td>
-					<td>Name of circuit provider</td>
-					<td>TeliaSonera</td>
-				</tr>
-				<tr>
-					<td>Type</td>
-					<td>Circuit type</td>
-					<td>Transit</td>
-				</tr>
-				<tr>
-					<td>Tenant</td>
-					<td>Name of tenant (optional)</td>
-					<td>Strickland Propane</td>
-				</tr>
-				<tr>
-					<td>Install Date</td>
-					<td>Date in YYYY-MM-DD format (optional)</td>
-					<td>2016-02-23</td>
-				</tr>
-				<tr>
-					<td>Commit rate</td>
-					<td>Commited rate in Kbps (optional)</td>
-					<td>2000</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Short description (optional)</td>
-					<td>Primary for voice</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Circuit ID</td>
+                <td>Alphanumeric circuit identifier</td>
+                <td>IC-603122</td>
+            </tr>
+            <tr>
+                <td>Provider</td>
+                <td>Name of circuit provider</td>
+                <td>TeliaSonera</td>
+            </tr>
+            <tr>
+                <td>Type</td>
+                <td>Circuit type</td>
+                <td>Transit</td>
+            </tr>
+            <tr>
+                <td>Tenant</td>
+                <td>Name of tenant (optional)</td>
+                <td>Strickland Propane</td>
+            </tr>
+            <tr>
+                <td>Install Date</td>
+                <td>Date in YYYY-MM-DD format (optional)</td>
+                <td>2016-02-23</td>
+            </tr>
+            <tr>
+                <td>Commit rate</td>
+                <td>Commited rate in Kbps (optional)</td>
+                <td>2000</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Short description (optional)</td>
+                <td>Primary for voice</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
 {% endblock %}

+ 41 - 56
netbox/templates/circuits/provider_import.html

@@ -1,62 +1,47 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Provider Import{% endblock %}
 
-{% block content %}
-<h1>Provider Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Name</td>
-					<td>Provider's proper name</td>
-					<td>Level 3</td>
-				</tr>
-				<tr>
-					<td>Slug</td>
-					<td>URL-friendly name</td>
-					<td>level3</td>
-				</tr>
-				<tr>
-					<td>ASN</td>
-					<td>Autonomous system number (optional)</td>
-					<td>3356</td>
-				</tr>
-				<tr>
-					<td>Account</td>
-					<td>Account number (optional)</td>
-					<td>08931544</td>
-				</tr>
-				<tr>
-					<td>Portal URL</td>
-					<td>Customer service portal URL (optional)</td>
-					<td>https://mylevel3.net</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Name</td>
+                <td>Provider's proper name</td>
+                <td>Level 3</td>
+            </tr>
+            <tr>
+                <td>Slug</td>
+                <td>URL-friendly name</td>
+                <td>level3</td>
+            </tr>
+            <tr>
+                <td>ASN</td>
+                <td>Autonomous system number (optional)</td>
+                <td>3356</td>
+            </tr>
+            <tr>
+                <td>Account</td>
+                <td>Account number (optional)</td>
+                <td>08931544</td>
+            </tr>
+            <tr>
+                <td>Portal URL</td>
+                <td>Customer service portal URL (optional)</td>
+                <td>https://mylevel3.net</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
 {% endblock %}

+ 41 - 55
netbox/templates/dcim/console_connections_import.html

@@ -1,61 +1,47 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Console Connections Import{% endblock %}
 
-{% block content %}
-<h1>Console Connections Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-            <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-            </div>
-		</form>
-    </div>
-    <div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Console server</td>
-					<td>Device name or {ID}</td>
-					<td>abc1-cs3</td>
-				</tr>
-				<tr>
-					<td>Console server port</td>
-					<td>Full CS port name</td>
-					<td>Port 35</td>
-				</tr>
-				<tr>
-					<td>Device</td>
-					<td>Device name or {ID}</td>
-					<td>abc1-switch7</td>
-				</tr>
-				<tr>
-					<td>Console Port</td>
-					<td>Console port name</td>
-					<td>Console</td>
-				</tr>
-				<tr>
-					<td>Connection Status</td>
-					<td>"planned" or "connected"</td>
-					<td>planned</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Console server</td>
+                <td>Device name or {ID}</td>
+                <td>abc1-cs3</td>
+            </tr>
+            <tr>
+                <td>Console server port</td>
+                <td>Full CS port name</td>
+                <td>Port 35</td>
+            </tr>
+            <tr>
+                <td>Device</td>
+                <td>Device name or {ID}</td>
+                <td>abc1-switch7</td>
+            </tr>
+            <tr>
+                <td>Console Port</td>
+                <td>Console port name</td>
+                <td>Console</td>
+            </tr>
+            <tr>
+                <td>Connection Status</td>
+                <td>"planned" or "connected"</td>
+                <td>planned</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
 {% endblock %}

+ 6 - 2
netbox/templates/dcim/device_import.html

@@ -12,8 +12,12 @@
 		    {% csrf_token %}
 		    {% render_form form %}
             <div class="form-group">
-                <button type="submit" class="btn btn-primary">Submit</button>
-                <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
+                <div class="col-md-12 text-right">
+		            <button type="submit" class="btn btn-primary">Submit</button>
+		            {% if return_url %}
+                        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
+                    {% endif %}
+                </div>
             </div>
 		</form>
 		<h4>CSV Format</h4>

+ 6 - 2
netbox/templates/dcim/device_import_child.html

@@ -12,8 +12,12 @@
 		    {% csrf_token %}
 		    {% render_form form %}
             <div class="form-group">
-                <button type="submit" class="btn btn-primary">Submit</button>
-                <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
+                <div class="col-md-12 text-right">
+		            <button type="submit" class="btn btn-primary">Submit</button>
+		            {% if return_url %}
+                        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
+                    {% endif %}
+                </div>
             </div>
 		</form>
 		<h4>CSV Format</h4>

+ 3 - 2
netbox/templates/dcim/inc/consoleport.html

@@ -7,6 +7,7 @@
     <td>
         <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
     </td>
+    <td></td>
     {% if cp.cs_port %}
         <td>
             <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
@@ -32,11 +33,11 @@
                     </a>
                 {% endif %}
                 <a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
-                    <i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
+                    <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
                 </a>
             {% else %}
                 <a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
-                    <i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
+                    <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
                 </a>
             {% endif %}
             <a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">

+ 3 - 2
netbox/templates/dcim/inc/consoleserverport.html

@@ -7,6 +7,7 @@
     <td>
         <i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
     </td>
+    <td></td>
     {% if csp.connected_console %}
         <td>
             <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
@@ -32,11 +33,11 @@
                     </a>
                 {% endif %}
                 <a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
-                    <i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
+                    <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
                 </a>
             {% else %}
                 <a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
-                    <i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
+                    <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
                 </a>
             {% endif %}
             <a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">

+ 5 - 4
netbox/templates/dcim/inc/interface.html

@@ -12,12 +12,13 @@
         {% if iface.description %}
             <i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
         {% endif %}
-        {% if iface.is_lag %}
-            <br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
-        {% endif %}
     </td>
+    <td>{{ iface.mac_address|default:"" }}</td>
     {% if iface.is_lag %}
-        <td colspan="2" class="text-muted">LAG interface</td>
+        <td colspan="2" class="text-muted">
+            LAG interface<br />
+            <small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
+        </td>
     {% elif iface.is_virtual %}
         <td colspan="2" class="text-muted">Virtual interface</td>
     {% elif iface.connection %}

+ 3 - 2
netbox/templates/dcim/inc/poweroutlet.html

@@ -7,6 +7,7 @@
     <td>
         <i class="fa fa-fw fa-bolt"></i> {{ po.name }}
     </td>
+    <td></td>
     {% if po.connected_port %}
         <td>
             <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
@@ -32,11 +33,11 @@
                     </a>
                 {% endif %}
                 <a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
-                    <i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
+                    <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
                 </a>
             {% else %}
                 <a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
-                    <i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
+                    <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
                 </a>
             {% endif %}
             <a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">

+ 3 - 2
netbox/templates/dcim/inc/powerport.html

@@ -7,6 +7,7 @@
     <td>
         <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
     </td>
+    <td></td>
     {% if pp.power_outlet %}
         <td>
             <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
@@ -32,11 +33,11 @@
                     </a>
                 {% endif %}
             <a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
-                <i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
+                <i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
             </a>
             {% else %}
                 <a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
-                    <i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
+                    <i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
                 </a>
             {% endif %}
             <a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">

+ 41 - 63
netbox/templates/dcim/interface_connections_import.html

@@ -1,69 +1,47 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Interface Connections Import{% endblock %}
 
-{% block content %}
-<h1>Interface Connections Import</h1>
-<div class="row">
-	<div class="col-md-6">
-        {% if form.non_field_errors %}
-            <div class="panel panel-danger">
-                <div class="panel-heading"><strong>Errors</strong></div>
-                <div class="panel-body">
-                    {{ form.non_field_errors }}
-                </div>
-            </div>
-        {% endif %}
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-            <div class="form-group">
-                <button type="submit" class="btn btn-primary">Submit</button>
-            </div>
-		</form>
-    </div>
-    <div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Device A</td>
-					<td>Device name or {ID}</td>
-					<td>abc1-core1</td>
-				</tr>
-				<tr>
-					<td>Interface A</td>
-					<td>Interface name</td>
-					<td>xe-0/0/6</td>
-				</tr>
-				<tr>
-					<td>Device B</td>
-					<td>Device name or {ID}</td>
-					<td>abc1-switch7</td>
-				</tr>
-				<tr>
-					<td>Interface B</td>
-					<td>Interface name</td>
-					<td>xe-0/0/0</td>
-				</tr>
-				<tr>
-					<td>Connection Status</td>
-					<td>"planned" or "connected"</td>
-					<td>planned</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Device A</td>
+                <td>Device name or {ID}</td>
+                <td>abc1-core1</td>
+            </tr>
+            <tr>
+                <td>Interface A</td>
+                <td>Interface name</td>
+                <td>xe-0/0/6</td>
+            </tr>
+            <tr>
+                <td>Device B</td>
+                <td>Device name or {ID}</td>
+                <td>abc1-switch7</td>
+            </tr>
+            <tr>
+                <td>Interface B</td>
+                <td>Interface name</td>
+                <td>xe-0/0/0</td>
+            </tr>
+            <tr>
+                <td>Connection Status</td>
+                <td>"planned" or "connected"</td>
+                <td>planned</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
 {% endblock %}

+ 41 - 55
netbox/templates/dcim/power_connections_import.html

@@ -1,61 +1,47 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Power Connections Import{% endblock %}
 
-{% block content %}
-<h1>Power Connections Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-            <div class="form-group">
-                <button type="submit" class="btn btn-primary">Submit</button>
-            </div>
-		</form>
-    </div>
-    <div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>PDU</td>
-					<td>Device name or {ID}</td>
-					<td>abc1-pdu1</td>
-				</tr>
-				<tr>
-					<td>Power Outlet</td>
-					<td>Power outlet name</td>
-					<td>AC4</td>
-				</tr>
-				<tr>
-					<td>Device</td>
-					<td>Device name or {ID}</td>
-					<td>abc1-switch7</td>
-				</tr>
-				<tr>
-					<td>Power Port</td>
-					<td>Power port name</td>
-					<td>PSU0</td>
-				</tr>
-				<tr>
-					<td>Connection Status</td>
-					<td>"planned" or "connected"</td>
-					<td>connected</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>PDU</td>
+                <td>Device name or {ID}</td>
+                <td>abc1-pdu1</td>
+            </tr>
+            <tr>
+                <td>Power Outlet</td>
+                <td>Power outlet name</td>
+                <td>AC4</td>
+            </tr>
+            <tr>
+                <td>Device</td>
+                <td>Device name or {ID}</td>
+                <td>abc1-switch7</td>
+            </tr>
+            <tr>
+                <td>Power Port</td>
+                <td>Power port name</td>
+                <td>PSU0</td>
+            </tr>
+            <tr>
+                <td>Connection Status</td>
+                <td>"planned" or "connected"</td>
+                <td>connected</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
 {% endblock %}

+ 66 - 81
netbox/templates/dcim/rack_import.html

@@ -1,87 +1,72 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Rack Import{% endblock %}
 
-{% block content %}
-<h1>Rack Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-                <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Site</td>
-					<td>Name of the assigned site</td>
-					<td>DC-4</td>
-				</tr>
-				<tr>
-					<td>Group</td>
-					<td>Rack group name (optional)</td>
-					<td>Cage 1400</td>
-				</tr>
-				<tr>
-					<td>Name</td>
-					<td>Internal rack name</td>
-					<td>R101</td>
-				</tr>
-				<tr>
-					<td>Facility ID</td>
-					<td>Rack ID assigned by the facility (optional)</td>
-					<td>J12.100</td>
-				</tr>
-				<tr>
-					<td>Tenant</td>
-					<td>Name of tenant (optional)</td>
-					<td>Pied Piper</td>
-				</tr>
-				<tr>
-					<td>Role</td>
-					<td>Functional role (optional)</td>
-					<td>Compute</td>
-				</tr>
-				<tr>
-					<td>Type</td>
-					<td>Rack type (optional)</td>
-					<td>4-post cabinet</td>
-				</tr>
-				<tr>
-					<td>Width</td>
-					<td>Rail-to-rail width (19 or 23 inches)</td>
-					<td>19</td>
-				</tr>
-				<tr>
-					<td>Height</td>
-					<td>Height in rack units</td>
-					<td>42</td>
-				</tr>
-				<tr>
-					<td>Descending units</td>
-					<td>Units are numbered top-to-bottom</td>
-					<td>False</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Site</td>
+                <td>Name of the assigned site</td>
+                <td>DC-4</td>
+            </tr>
+            <tr>
+                <td>Group</td>
+                <td>Rack group name (optional)</td>
+                <td>Cage 1400</td>
+            </tr>
+            <tr>
+                <td>Name</td>
+                <td>Internal rack name</td>
+                <td>R101</td>
+            </tr>
+            <tr>
+                <td>Facility ID</td>
+                <td>Rack ID assigned by the facility (optional)</td>
+                <td>J12.100</td>
+            </tr>
+            <tr>
+                <td>Tenant</td>
+                <td>Name of tenant (optional)</td>
+                <td>Pied Piper</td>
+            </tr>
+            <tr>
+                <td>Role</td>
+                <td>Functional role (optional)</td>
+                <td>Compute</td>
+            </tr>
+            <tr>
+                <td>Type</td>
+                <td>Rack type (optional)</td>
+                <td>4-post cabinet</td>
+            </tr>
+            <tr>
+                <td>Width</td>
+                <td>Rail-to-rail width (19 or 23 inches)</td>
+                <td>19</td>
+            </tr>
+            <tr>
+                <td>Height</td>
+                <td>Height in rack units</td>
+                <td>42</td>
+            </tr>
+            <tr>
+                <td>Descending units</td>
+                <td>Units are numbered top-to-bottom</td>
+                <td>False</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
 {% endblock %}

+ 9 - 8
netbox/templates/import_success.html

@@ -1,13 +1,14 @@
 {% extends '_base.html' %}
 {% load render_table from django_tables2 %}
 
-{% block title %}Import Completed{% endblock %}
-
 {% block content %}
-<h1>Import Completed</h1>
-{% render_table table %}
-<a href="{{ request.path }}" class="btn btn-primary">
-	<span class="fa fa-download" aria-hidden="true"></span>
-	Import more
-</a>
+    <h1>{% block title %}Import Completed{% endblock %}</h1>
+    {% render_table table %}
+    <a href="{{ request.path }}" class="btn btn-primary">
+        <span class="fa fa-download" aria-hidden="true"></span>
+        Import more
+    </a>
+    {% if return_url %}
+        <a href="{% url return_url %}" class="btn btn-default">View All</a>
+    {% endif %}
 {% endblock %}

+ 36 - 51
netbox/templates/ipam/aggregate_import.html

@@ -1,57 +1,42 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Aggregate Import{% endblock %}
 
-{% block content %}
-<h1>Aggregate Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Prefix</td>
-					<td>IPv4 or IPv6 network</td>
-					<td>172.16.0.0/12</td>
-				</tr>
-				<tr>
-					<td>RIR</td>
-					<td>Name of RIR</td>
-					<td>RFC 1918</td>
-				</tr>
-				<tr>
-					<td>Date Added</td>
-					<td>Date in YYYY-MM-DD format (optional)</td>
-					<td>2016-02-23</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Short description (optional)</td>
-					<td>Private IPv4 space</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Prefix</td>
+                <td>IPv4 or IPv6 network</td>
+                <td>172.16.0.0/12</td>
+            </tr>
+            <tr>
+                <td>RIR</td>
+                <td>Name of RIR</td>
+                <td>RFC 1918</td>
+            </tr>
+            <tr>
+                <td>Date Added</td>
+                <td>Date in YYYY-MM-DD format (optional)</td>
+                <td>2016-02-23</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Short description (optional)</td>
+                <td>Private IPv4 space</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
 {% endblock %}

+ 1 - 0
netbox/templates/ipam/ipaddress_edit.html

@@ -28,6 +28,7 @@
             {% render_field form.interface_rack %}
             {% render_field form.interface_device %}
             {% render_field form.interface %}
+            {% render_field form.primary_for_device %}
         </div>
     </div>
     <div class="panel panel-default">

+ 56 - 71
netbox/templates/ipam/ipaddress_import.html

@@ -1,77 +1,62 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}IP Address Import{% endblock %}
 
-{% block content %}
-<h1>IP Address Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Address</td>
-					<td>IPv4 or IPv6 address</td>
-					<td>192.0.2.42/24</td>
-				</tr>
-				<tr>
-					<td>VRF</td>
-					<td>VRF route distinguisher (optional)</td>
-					<td>65000:123</td>
-				</tr>
-				<tr>
-					<td>Tenant</td>
-					<td>Name of tenant (optional)</td>
-					<td>ABC01</td>
-				</tr>
-				<tr>
-					<td>Status</td>
-					<td>Current status</td>
-					<td>Active</td>
-				</tr>
-				<tr>
-					<td>Device</td>
-					<td>Device name (optional)</td>
-					<td>switch12</td>
-				</tr>
-				<tr>
-					<td>Interface</td>
-					<td>Interface name (optional)</td>
-					<td>ge-0/0/31</td>
-				</tr>
-				<tr>
-					<td>Is Primary</td>
-					<td>If "true", IP will be primary for device (optional)</td>
-					<td>True</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Short description (optional)</td>
-					<td>Management IP</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Address</td>
+                <td>IPv4 or IPv6 address</td>
+                <td>192.0.2.42/24</td>
+            </tr>
+            <tr>
+                <td>VRF</td>
+                <td>VRF route distinguisher (optional)</td>
+                <td>65000:123</td>
+            </tr>
+            <tr>
+                <td>Tenant</td>
+                <td>Name of tenant (optional)</td>
+                <td>ABC01</td>
+            </tr>
+            <tr>
+                <td>Status</td>
+                <td>Current status</td>
+                <td>Active</td>
+            </tr>
+            <tr>
+                <td>Device</td>
+                <td>Device name (optional)</td>
+                <td>switch12</td>
+            </tr>
+            <tr>
+                <td>Interface</td>
+                <td>Interface name (optional)</td>
+                <td>ge-0/0/31</td>
+            </tr>
+            <tr>
+                <td>Is Primary</td>
+                <td>If "true", IP will be primary for device (optional)</td>
+                <td>True</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Short description (optional)</td>
+                <td>Management IP</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
 {% endblock %}

+ 66 - 81
netbox/templates/ipam/prefix_import.html

@@ -1,87 +1,72 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Prefix Import{% endblock %}
 
-{% block content %}
-<h1>Prefix Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Prefix</td>
-					<td>IPv4 or IPv6 network</td>
-					<td>192.168.42.0/24</td>
-				</tr>
-				<tr>
-					<td>VRF</td>
-					<td>VRF route distinguisher (optional)</td>
-					<td>65000:123</td>
-				</tr>
-				<tr>
-					<td>Tenant</td>
-					<td>Name of tenant (optional)</td>
-					<td>ABC01</td>
-				</tr>
-				<tr>
-					<td>Site</td>
-					<td>Name of assigned site (optional)</td>
-					<td>HQ</td>
-				</tr>
-				<tr>
-					<td>VLAN Group</td>
-					<td>Name of group for VLAN selection (optional)</td>
-					<td>Customers</td>
-				</tr>
-				<tr>
-					<td>VLAN ID</td>
-					<td>Numeric VLAN ID (optional)</td>
-					<td>801</td>
-				</tr>
-				<tr>
-					<td>Status</td>
-					<td>Current status</td>
-					<td>Active</td>
-				</tr>
-				<tr>
-					<td>Role</td>
-					<td>Functional role (optional)</td>
-					<td>Customer</td>
-				</tr>
-				<tr>
-					<td>Is a pool</td>
-					<td>True if all IPs are considered usable</td>
-					<td>False</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Short description (optional)</td>
-					<td>7th floor WiFi</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Prefix</td>
+                <td>IPv4 or IPv6 network</td>
+                <td>192.168.42.0/24</td>
+            </tr>
+            <tr>
+                <td>VRF</td>
+                <td>VRF route distinguisher (optional)</td>
+                <td>65000:123</td>
+            </tr>
+            <tr>
+                <td>Tenant</td>
+                <td>Name of tenant (optional)</td>
+                <td>ABC01</td>
+            </tr>
+            <tr>
+                <td>Site</td>
+                <td>Name of assigned site (optional)</td>
+                <td>HQ</td>
+            </tr>
+            <tr>
+                <td>VLAN Group</td>
+                <td>Name of group for VLAN selection (optional)</td>
+                <td>Customers</td>
+            </tr>
+            <tr>
+                <td>VLAN ID</td>
+                <td>Numeric VLAN ID (optional)</td>
+                <td>801</td>
+            </tr>
+            <tr>
+                <td>Status</td>
+                <td>Current status</td>
+                <td>Active</td>
+            </tr>
+            <tr>
+                <td>Role</td>
+                <td>Functional role (optional)</td>
+                <td>Customer</td>
+            </tr>
+            <tr>
+                <td>Is a pool</td>
+                <td>True if all IPs are considered usable</td>
+                <td>False</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Short description (optional)</td>
+                <td>7th floor WiFi</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
 {% endblock %}

+ 56 - 71
netbox/templates/ipam/vlan_import.html

@@ -1,77 +1,62 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}VLAN Import{% endblock %}
 
-{% block content %}
-<h1>VLAN Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Site</td>
-					<td>Name of assigned site</td>
-					<td>LAS2</td>
-				</tr>
-				<tr>
-					<td>Group</td>
-					<td>Name of VLAN group (optional)</td>
-					<td>Backend Network</td>
-				</tr>
-				<tr>
-					<td>ID</td>
-					<td>Configured VLAN ID</td>
-					<td>1400</td>
-				</tr>
-				<tr>
-					<td>Name</td>
-					<td>Configured VLAN name</td>
-					<td>Cameras</td>
-				</tr>
-				<tr>
-					<td>Tenant</td>
-					<td>Name of tenant (optional)</td>
-					<td>Internal</td>
-				</tr>
-				<tr>
-					<td>Status</td>
-					<td>Current status</td>
-					<td>Active</td>
-				</tr>
-				<tr>
-					<td>Role</td>
-					<td>Functional role (optional)</td>
-					<td>Security</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Short description (optional)</td>
-					<td>Security team only</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Site</td>
+                <td>Name of assigned site</td>
+                <td>LAS2</td>
+            </tr>
+            <tr>
+                <td>Group</td>
+                <td>Name of VLAN group (optional)</td>
+                <td>Backend Network</td>
+            </tr>
+            <tr>
+                <td>ID</td>
+                <td>Configured VLAN ID</td>
+                <td>1400</td>
+            </tr>
+            <tr>
+                <td>Name</td>
+                <td>Configured VLAN name</td>
+                <td>Cameras</td>
+            </tr>
+            <tr>
+                <td>Tenant</td>
+                <td>Name of tenant (optional)</td>
+                <td>Internal</td>
+            </tr>
+            <tr>
+                <td>Status</td>
+                <td>Current status</td>
+                <td>Active</td>
+            </tr>
+            <tr>
+                <td>Role</td>
+                <td>Functional role (optional)</td>
+                <td>Security</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Short description (optional)</td>
+                <td>Security team only</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
 {% endblock %}

+ 41 - 56
netbox/templates/ipam/vrf_import.html

@@ -1,62 +1,47 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}VRF Import{% endblock %}
 
-{% block content %}
-<h1>VRF Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Name</td>
-					<td>Name of VRF</td>
-					<td>Customer_ABC</td>
-				</tr>
-				<tr>
-					<td>RD</td>
-					<td>Route distinguisher</td>
-					<td>65000:123456</td>
-				</tr>
-				<tr>
-					<td>Tenant</td>
-					<td>Name of tenant (optional)</td>
-					<td>ABC01</td>
-				</tr>
-				<tr>
-					<td>Enforce uniqueness</td>
-					<td>Prevent duplicate prefixes/IP addresses</td>
-					<td>True</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Short description (optional)</td>
-					<td>Native VRF for customer ABC</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Name</td>
+                <td>Name of VRF</td>
+                <td>Customer_ABC</td>
+            </tr>
+            <tr>
+                <td>RD</td>
+                <td>Route distinguisher</td>
+                <td>65000:123456</td>
+            </tr>
+            <tr>
+                <td>Tenant</td>
+                <td>Name of tenant (optional)</td>
+                <td>ABC01</td>
+            </tr>
+            <tr>
+                <td>Enforce uniqueness</td>
+                <td>Prevent duplicate prefixes/IP addresses</td>
+                <td>True</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Short description (optional)</td>
+                <td>Native VRF for customer ABC</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
 {% endblock %}

+ 8 - 4
netbox/templates/secrets/secret_import.html

@@ -20,10 +20,14 @@
 		<form action="." method="post" class="form">
 		    {% csrf_token %}
 		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
-		    </div>
+            <div class="form-group">
+                <div class="col-md-12 text-right">
+		            <button type="submit" class="btn btn-primary">Submit</button>
+		            {% if return_url %}
+                        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
+                    {% endif %}
+                </div>
+            </div>
 		</form>
 	</div>
 	<div class="col-md-6">

+ 1 - 1
netbox/templates/table.html

@@ -1,4 +1,4 @@
-{% extends 'django_tables2/table.html' %}
+{% extends 'django_tables2/bootstrap-responsive.html' %}
 {% load django_tables2 %}
 
 {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #}

+ 36 - 51
netbox/templates/tenancy/tenant_import.html

@@ -1,57 +1,42 @@
-{% extends '_base.html' %}
+{% extends 'utilities/obj_import.html' %}
 {% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Tenant Import{% endblock %}
 
-{% block content %}
-<h1>Tenant Import</h1>
-<div class="row">
-	<div class="col-md-6">
-		<form action="." method="post" class="form">
-		    {% csrf_token %}
-		    {% render_form form %}
-		    <div class="form-group">
-		        <button type="submit" class="btn btn-primary">Submit</button>
-		        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
-		    </div>
-		</form>
-	</div>
-	<div class="col-md-6">
-		<h4>CSV Format</h4>
-		<table class="table">
-			<thead>
-				<tr>
-					<th>Field</th>
-					<th>Description</th>
-					<th>Example</th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr>
-					<td>Name</td>
-					<td>Tenant name</td>
-					<td>WIDG01</td>
-				</tr>
-				<tr>
-					<td>Slug</td>
-					<td>URL-friendly name</td>
-					<td>widg01</td>
-				</tr>
-				<tr>
-					<td>Group</td>
-					<td>Tenant group (optional)</td>
-					<td>Customers</td>
-				</tr>
-				<tr>
-					<td>Description</td>
-					<td>Long-form name or other text (optional)</td>
-					<td>Widgets Inc.</td>
-				</tr>
-			</tbody>
-		</table>
-		<h4>Example</h4>
-		<pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
-	</div>
-</div>
+{% block instructions %}
+    <h4>CSV Format</h4>
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Field</th>
+                <th>Description</th>
+                <th>Example</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>Name</td>
+                <td>Tenant name</td>
+                <td>WIDG01</td>
+            </tr>
+            <tr>
+                <td>Slug</td>
+                <td>URL-friendly name</td>
+                <td>widg01</td>
+            </tr>
+            <tr>
+                <td>Group</td>
+                <td>Tenant group (optional)</td>
+                <td>Customers</td>
+            </tr>
+            <tr>
+                <td>Description</td>
+                <td>Long-form name or other text (optional)</td>
+                <td>Widgets Inc.</td>
+            </tr>
+        </tbody>
+    </table>
+    <h4>Example</h4>
+    <pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
 {% endblock %}

+ 34 - 0
netbox/templates/utilities/obj_import.html

@@ -0,0 +1,34 @@
+{% extends '_base.html' %}
+{% load render_table from django_tables2 %}
+{% load form_helpers %}
+
+{% block content %}
+<h1>{% block title %}{% endblock %}</h1>
+<div class="row">
+	<div class="col-md-6">
+        {% if form.non_field_errors %}
+            <div class="panel panel-danger">
+                <div class="panel-heading"><strong>Errors</strong></div>
+                <div class="panel-body">
+                    {{ form.non_field_errors }}
+                </div>
+            </div>
+        {% endif %}
+		<form action="." method="post" class="form">
+		    {% csrf_token %}
+		    {% render_form form %}
+            <div class="form-group">
+                <div class="col-md-12 text-right">
+		            <button type="submit" class="btn btn-primary">Submit</button>
+		            {% if return_url %}
+                        <a href="{% url return_url %}" class="btn btn-default">Cancel</a>
+                    {% endif %}
+                </div>
+            </div>
+		</form>
+	</div>
+	<div class="col-md-6">
+        {% block instructions %}{% endblock %}
+	</div>
+</div>
+{% endblock %}

+ 7 - 5
netbox/utilities/error_handlers.py

@@ -1,4 +1,6 @@
 from django.contrib import messages
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
 
 
 def handle_protectederror(obj, request, e):
@@ -25,11 +27,11 @@ def handle_protectederror(obj, request, e):
 
     # Append dependent objects to error message
     dependent_objects = []
-    for o in e.protected_objects:
-        if hasattr(o, 'get_absolute_url'):
-            dependent_objects.append(u'<a href="{}">{}</a>'.format(o.get_absolute_url(), o))
+    for obj in e.protected_objects:
+        if hasattr(obj, 'get_absolute_url'):
+            dependent_objects.append(u'<a href="{}">{}</a>'.format(obj.get_absolute_url(), escape(obj)))
         else:
-            dependent_objects.append(str(o))
+            dependent_objects.append(str(obj))
     err_message += u', '.join(dependent_objects)
 
-    messages.error(request, err_message)
+    messages.error(request, mark_safe(err_message))

+ 13 - 0
netbox/utilities/forms.py

@@ -125,6 +125,19 @@ class ColorSelect(forms.Select):
         super(ColorSelect, self).__init__(*args, **kwargs)
 
 
+class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
+
+    def __init__(self, *args, **kwargs):
+        super(BulkEditNullBooleanSelect, self).__init__(*args, **kwargs)
+
+        # Override the built-in choice labels
+        self.choices = (
+            ('1', '---------'),
+            ('2', 'Yes'),
+            ('3', 'No'),
+        )
+
+
 class SelectWithDisabled(forms.Select):
     """
     Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include

+ 5 - 9
netbox/utilities/views.py

@@ -17,7 +17,6 @@ from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 
-from extras.forms import CustomFieldForm
 from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
 
 from .error_handlers import handle_protectederror
@@ -195,12 +194,8 @@ class ObjectEditView(GetReturnURLMixin, View):
         form = self.form_class(request.POST, request.FILES, instance=obj)
 
         if form.is_valid():
-            obj = form.save(commit=False)
-            obj_created = not obj.pk
-            obj.save()
-            form.save_m2m()
-            if isinstance(form, CustomFieldForm):
-                form.save_custom_fields()
+            obj_created = not form.instance.pk
+            obj = form.save()
 
             msg = u'Created ' if obj_created else u'Modified '
             msg += self.model._meta.verbose_name
@@ -400,6 +395,7 @@ class BulkImportView(View):
 
                 return render(request, "import_success.html", {
                     'table': obj_table,
+                    'return_url': self.default_return_url,
                 })
 
             except IntegrityError as e:
@@ -423,7 +419,7 @@ class BulkEditView(View):
     filter: FilterSet to apply when deleting by QuerySet
     form: The form class used to edit objects in bulk
     template_name: The name of the template
-    default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overriden by
+    default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overridden by
                         POSTing return_url)
     """
     cls = None
@@ -475,7 +471,7 @@ class BulkEditView(View):
                             fields_to_update[field] = ''
                         else:
                             fields_to_update[field] = None
-                    elif form.cleaned_data[field]:
+                    elif form.cleaned_data[field] not in (None, ''):
                         fields_to_update[field] = form.cleaned_data[field]
                 updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
 

+ 1 - 1
requirements.txt

@@ -6,7 +6,7 @@ django-debug-toolbar>=1.7
 django-filter>=1.0.2
 django-mptt==0.8.7
 django-rest-swagger>=2.1.0
-django-tables2>=1.4.0
+django-tables2>=1.6.0
 djangorestframework>=3.6.2
 graphviz>=0.6
 Markdown>=2.6.7