Преглед изворни кода

Merge pull request #1201 from digitalocean/develop

Release v2.0.3
Jeremy Stretch пре 8 година
родитељ
комит
ad95b86fdd
59 измењених фајлова са 215 додато и 229 уклоњено
  1. 1 0
      README.md
  2. 2 2
      docs/installation/netbox.md
  3. 3 0
      docs/installation/web-server.md
  4. 9 0
      netbox/dcim/api/serializers.py
  5. 5 0
      netbox/dcim/filters.py
  6. 16 10
      netbox/dcim/forms.py
  7. 31 19
      netbox/ipam/forms.py
  8. 2 2
      netbox/ipam/tables.py
  9. 1 0
      netbox/ipam/views.py
  10. 1 1
      netbox/netbox/settings.py
  11. 29 28
      netbox/netbox/views.py
  12. 7 0
      netbox/project-static/css/base.css
  13. 1 1
      netbox/project-static/js/secrets.js
  14. 3 3
      netbox/templates/_base.html
  15. 0 2
      netbox/templates/circuits/circuit_import.html
  16. 0 2
      netbox/templates/circuits/provider_import.html
  17. 0 2
      netbox/templates/dcim/console_connections_import.html
  18. 1 2
      netbox/templates/dcim/console_connections_list.html
  19. 0 1
      netbox/templates/dcim/device.html
  20. 0 1
      netbox/templates/dcim/device_import.html
  21. 0 1
      netbox/templates/dcim/device_import_child.html
  22. 1 0
      netbox/templates/dcim/device_list.html
  23. 0 1
      netbox/templates/dcim/devicetype.html
  24. 2 3
      netbox/templates/dcim/inc/devicetype_component_table.html
  25. 0 2
      netbox/templates/dcim/interface_connections_import.html
  26. 1 2
      netbox/templates/dcim/interface_connections_list.html
  27. 0 2
      netbox/templates/dcim/power_connections_import.html
  28. 1 2
      netbox/templates/dcim/power_connections_list.html
  29. 0 1
      netbox/templates/dcim/rack.html
  30. 17 16
      netbox/templates/dcim/rack_elevation_list.html
  31. 0 2
      netbox/templates/dcim/rack_import.html
  32. 0 1
      netbox/templates/dcim/site.html
  33. 0 1
      netbox/templates/dcim/site_import.html
  34. 0 1
      netbox/templates/home.html
  35. 1 2
      netbox/templates/import_success.html
  36. 8 7
      netbox/templates/inc/paginator.html
  37. 41 0
      netbox/templates/inc/table.html
  38. 0 1
      netbox/templates/ipam/aggregate.html
  39. 0 2
      netbox/templates/ipam/aggregate_import.html
  40. 3 10
      netbox/templates/ipam/ipaddress.html
  41. 0 2
      netbox/templates/ipam/ipaddress_import.html
  42. 2 7
      netbox/templates/ipam/prefix.html
  43. 0 2
      netbox/templates/ipam/prefix_import.html
  44. 0 1
      netbox/templates/ipam/prefix_ipaddresses.html
  45. 1 2
      netbox/templates/ipam/vlan.html
  46. 2 2
      netbox/templates/ipam/vlan_edit.html
  47. 1 3
      netbox/templates/ipam/vlan_import.html
  48. 1 2
      netbox/templates/ipam/vrf.html
  49. 0 2
      netbox/templates/ipam/vrf_import.html
  50. 7 13
      netbox/templates/panel_table.html
  51. 8 0
      netbox/templates/responsive_table.html
  52. 0 1
      netbox/templates/secrets/secret_import.html
  53. 0 10
      netbox/templates/table.html
  54. 0 36
      netbox/templates/table_paginator.html
  55. 0 2
      netbox/templates/tenancy/tenant_import.html
  56. 0 1
      netbox/templates/utilities/obj_import.html
  57. 4 5
      netbox/templates/utilities/obj_table.html
  58. 2 4
      netbox/utilities/forms.py
  59. 0 1
      netbox/utilities/views.py

+ 1 - 0
README.md

@@ -33,3 +33,4 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for inst
 
 * [Docker container](https://github.com/digitalocean/netbox-docker)
 * [Heroku deployment](https://heroku.com/deploy?template=https://github.com/BILDQUADRAT/netbox/tree/heroku) (via [@mraerino](https://github.com/BILDQUADRAT/netbox/tree/heroku))
+* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant)

+ 2 - 2
docs/installation/netbox.md

@@ -5,14 +5,14 @@
 Python 3:
 
 ```no-highlight
-# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
+# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
 # update-alternatives --install /usr/bin/python python /usr/bin/python3 1
 ```
 
 Python 2:
 
 ```no-highlight
-# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
+# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
 ```
 
 **CentOS/RHEL**

+ 3 - 0
docs/installation/web-server.md

@@ -73,6 +73,9 @@ Once Apache is installed, proceed with the following configuration (Be sure to m
 
     Alias /static /opt/netbox/netbox/static
 
+    # Needed to allow token-based API authentication
+    WSGIPassAuthorization on
+
     <Directory /opt/netbox/netbox/static>
         Options Indexes FollowSymLinks MultiViews
         AllowOverride None

+ 9 - 0
netbox/dcim/api/serializers.py

@@ -581,9 +581,18 @@ class WritablePowerPortSerializer(serializers.ModelSerializer):
 # Interfaces
 #
 
+class NestedInterfaceSerializer(serializers.ModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
+
+    class Meta:
+        model = Interface
+        fields = ['id', 'url', 'name']
+
+
 class InterfaceSerializer(serializers.ModelSerializer):
     device = NestedDeviceSerializer()
     form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
+    lag = NestedInterfaceSerializer()
     connection = serializers.SerializerMethodField(read_only=True)
     connected_interface = serializers.SerializerMethodField(read_only=True)
 

+ 5 - 0
netbox/dcim/filters.py

@@ -477,6 +477,11 @@ class InterfaceFilter(DeviceComponentFilterSet):
         method='filter_type',
         label='Interface type',
     )
+    lag_id = django_filters.ModelMultipleChoiceFilter(
+        name='lag',
+        queryset=Interface.objects.all(),
+        label='LAG interface (ID)',
+    )
     mac_address = django_filters.CharFilter(
         method='_mac_address',
         label='MAC address',

+ 16 - 10
netbox/dcim/forms.py

@@ -674,7 +674,7 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
         queryset=Platform.objects.all(), required=False, to_field_name='name',
         error_messages={'invalid_choice': 'Invalid platform.'}
     )
-    status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in STATUS_CHOICES])
+    status = forms.CharField()
 
     class Meta:
         fields = []
@@ -692,8 +692,12 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
             except DeviceType.DoesNotExist:
                 self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
 
-    def clean_status_name(self):
-        return dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
+    def clean_status(self):
+        status_choices = {s[1].lower(): s[0] for s in STATUS_CHOICES}
+        try:
+            return status_choices[self.cleaned_data['status'].lower()]
+        except KeyError:
+            raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
 
 
 class DeviceFromCSVForm(BaseDeviceFromCSVForm):
@@ -707,8 +711,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
 
     class Meta(BaseDeviceFromCSVForm.Meta):
         fields = [
-            'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
-            'status_name', 'site', 'rack_name', 'position', 'face',
+            'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
+            'site', 'rack_name', 'position', 'face',
         ]
 
     def clean(self):
@@ -751,8 +755,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
 
     class Meta(BaseDeviceFromCSVForm.Meta):
         fields = [
-            'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
-            'status_name', 'parent', 'device_bay_name',
+            'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
+            'parent', 'device_bay_name',
         ]
 
     def clean(self):
@@ -817,13 +821,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     rack_id = FilterChoiceField(
         queryset=Rack.objects.annotate(filter_count=Count('devices')),
         label='Rack',
+        null_option=(0, 'None'),
     )
     role = FilterChoiceField(
         queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
         to_field_name='slug',
     )
     tenant = FilterChoiceField(
-        queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
+        queryset=Tenant.objects.annotate(filter_count=Count('devices')),
+        to_field_name='slug',
         null_option=(0, 'None'),
     )
     manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
@@ -1207,7 +1213,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
     )
     power_outlet = ChainedModelChoiceField(
         queryset=PowerOutlet.objects.all(),
-        chains={'device': 'device'},
+        chains={'device': 'pdu'},
         label='Outlet',
         widget=APISelect(
             api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@@ -1441,7 +1447,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
         label='Interface',
         widget=APISelect(
             api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
-            disabled_indicator='is_connected'
+            disabled_indicator='connection'
         )
     )
 

+ 31 - 19
netbox/ipam/forms.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.core.exceptions import ValidationError
 from django.db.models import Count
 
 from dcim.models import Site, Rack, Device, Interface
@@ -195,14 +196,16 @@ class PrefixFromCSVForm(forms.ModelForm):
                                   error_messages={'invalid_choice': 'Site not found.'})
     vlan_group_name = forms.CharField(required=False)
     vlan_vid = forms.IntegerField(required=False)
-    status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
+    status = forms.CharField()
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
                                   error_messages={'invalid_choice': 'Invalid role.'})
 
     class Meta:
         model = Prefix
-        fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
-                  'description']
+        fields = [
+            'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
+            'description',
+        ]
 
     def clean(self):
 
@@ -237,12 +240,12 @@ class PrefixFromCSVForm(forms.ModelForm):
             except VLAN.MultipleObjectsReturned:
                 self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
 
-    def save(self, *args, **kwargs):
-
-        # Assign Prefix status by name
-        self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
-
-        return super(PrefixFromCSVForm, self).save(*args, **kwargs)
+    def clean_status(self):
+        status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
+        try:
+            return status_choices[self.cleaned_data['status'].lower()]
+        except KeyError:
+            raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
 
 
 class PrefixImportForm(BootstrapMixin, BulkImportForm):
@@ -491,7 +494,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
                                  error_messages={'invalid_choice': 'VRF not found.'})
     tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
                                     error_messages={'invalid_choice': 'Tenant not found.'})
-    status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
+    status = forms.CharField()
     device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
                                     error_messages={'invalid_choice': 'Device not found.'})
     interface_name = forms.CharField(required=False)
@@ -499,7 +502,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
 
     class Meta:
         model = IPAddress
-        fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
+        fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
 
     def clean(self):
 
@@ -522,10 +525,14 @@ class IPAddressFromCSVForm(forms.ModelForm):
         if is_primary and not device:
             self.add_error('is_primary', "No device specified; cannot set as primary IP")
 
-    def save(self, *args, **kwargs):
+    def clean_status(self):
+        status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
+        try:
+            return status_choices[self.cleaned_data['status'].lower()]
+        except KeyError:
+            raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
 
-        # Assign status by name
-        self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
+    def save(self, *args, **kwargs):
 
         # Set interface
         if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
@@ -612,6 +619,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
+        required=False,
         widget=forms.Select(
             attrs={'filter-for': 'group', 'nullable': 'true'}
         )
@@ -649,7 +657,7 @@ class VLANFromCSVForm(forms.ModelForm):
         Tenant.objects.all(), to_field_name='name', required=False,
         error_messages={'invalid_choice': 'Tenant not found.'}
     )
-    status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
+    status = forms.CharField()
     role = forms.ModelChoiceField(
         queryset=Role.objects.all(), required=False, to_field_name='name',
         error_messages={'invalid_choice': 'Invalid role.'}
@@ -657,7 +665,7 @@ class VLANFromCSVForm(forms.ModelForm):
 
     class Meta:
         model = VLAN
-        fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
+        fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
 
     def clean(self):
 
@@ -671,6 +679,13 @@ class VLANFromCSVForm(forms.ModelForm):
             except VLANGroup.DoesNotExist:
                 self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
 
+    def clean_status(self):
+        status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
+        try:
+            return status_choices[self.cleaned_data['status'].lower()]
+        except KeyError:
+            raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
+
     def save(self, *args, **kwargs):
 
         vlan = super(VLANFromCSVForm, self).save(commit=False)
@@ -679,9 +694,6 @@ class VLANFromCSVForm(forms.ModelForm):
         if self.cleaned_data['group_name']:
             vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
 
-        # Assign VLAN status by name
-        vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
-
         if kwargs.get('commit'):
             vlan.save()
         return vlan

+ 2 - 2
netbox/ipam/tables.py

@@ -70,9 +70,9 @@ IPADDRESS_LINK = """
 {% if record.pk %}
     <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
 {% elif perms.ipam.add_ipaddress %}
-    <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
+    <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
 {% else %}
-    {{ record.0 }}
+    {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
 {% endif %}
 """
 

+ 1 - 0
netbox/ipam/views.py

@@ -525,6 +525,7 @@ def prefix_ipaddresses(request, pk):
         'prefix': prefix,
         'ip_table': ip_table,
         'permissions': permissions,
+        'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
     })
 
 

+ 1 - 1
netbox/netbox/settings.py

@@ -13,7 +13,7 @@ except ImportError:
     )
 
 
-VERSION = '2.0.2'
+VERSION = '2.0.3'
 
 # Import local configuration
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

+ 29 - 28
netbox/netbox/views.py

@@ -1,3 +1,4 @@
+from collections import OrderedDict
 import sys
 
 from rest_framework.views import APIView
@@ -27,91 +28,91 @@ from .forms import SearchForm
 
 
 SEARCH_MAX_RESULTS = 15
-SEARCH_TYPES = {
+SEARCH_TYPES = OrderedDict((
     # Circuits
-    'provider': {
+    ('provider', {
         'queryset': Provider.objects.all(),
         'filter': ProviderFilter,
         'table': ProviderSearchTable,
         'url': 'circuits:provider_list',
-    },
-    'circuit': {
+    }),
+    ('circuit', {
         'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
         'filter': CircuitFilter,
         'table': CircuitSearchTable,
         'url': 'circuits:circuit_list',
-    },
+    }),
     # DCIM
-    'site': {
+    ('site', {
         'queryset': Site.objects.select_related('region', 'tenant'),
         'filter': SiteFilter,
         'table': SiteSearchTable,
         'url': 'dcim:site_list',
-    },
-    'rack': {
+    }),
+    ('rack', {
         'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
         'filter': RackFilter,
         'table': RackSearchTable,
         'url': 'dcim:rack_list',
-    },
-    'devicetype': {
+    }),
+    ('devicetype', {
         'queryset': DeviceType.objects.select_related('manufacturer'),
         'filter': DeviceTypeFilter,
         'table': DeviceTypeSearchTable,
         'url': 'dcim:devicetype_list',
-    },
-    'device': {
+    }),
+    ('device', {
         'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'),
         'filter': DeviceFilter,
         'table': DeviceSearchTable,
         'url': 'dcim:device_list',
-    },
+    }),
     # IPAM
-    'vrf': {
+    ('vrf', {
         'queryset': VRF.objects.select_related('tenant'),
         'filter': VRFFilter,
         'table': VRFSearchTable,
         'url': 'ipam:vrf_list',
-    },
-    'aggregate': {
+    }),
+    ('aggregate', {
         'queryset': Aggregate.objects.select_related('rir'),
         'filter': AggregateFilter,
         'table': AggregateSearchTable,
         'url': 'ipam:aggregate_list',
-    },
-    'prefix': {
+    }),
+    ('prefix', {
         'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
         'filter': PrefixFilter,
         'table': PrefixSearchTable,
         'url': 'ipam:prefix_list',
-    },
-    'ipaddress': {
+    }),
+    ('ipaddress', {
         'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
         'filter': IPAddressFilter,
         'table': IPAddressSearchTable,
         'url': 'ipam:ipaddress_list',
-    },
-    'vlan': {
+    }),
+    ('vlan', {
         'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
         'filter': VLANFilter,
         'table': VLANSearchTable,
         'url': 'ipam:vlan_list',
-    },
+    }),
     # Secrets
-    'secret': {
+    ('secret', {
         'queryset': Secret.objects.select_related('role', 'device'),
         'filter': SecretFilter,
         'table': SecretSearchTable,
         'url': 'secrets:secret_list',
-    },
+    }),
     # Tenancy
-    'tenant': {
+    ('tenant', {
         'queryset': Tenant.objects.select_related('group'),
         'filter': TenantFilter,
         'table': TenantSearchTable,
         'url': 'tenancy:tenant_list',
-    },
-}
+    }),
+))
 
 
 def home(request):

+ 7 - 0
netbox/project-static/css/base.css

@@ -74,6 +74,13 @@ footer p {
     }
 }
 
+/* Hide the nav search bar on displays less than 1600px wide */
+@media (max-width: 1599px) {
+    #navbar_search {
+        display: none;
+    }
+}
+
 /* Forms */
 label {
     font-weight: normal;

+ 1 - 1
netbox/project-static/js/secrets.js

@@ -16,7 +16,7 @@ $(document).ready(function() {
 
     // Adding/editing a secret
     $('form').submit(function(event) {
-        $(this).find('input.requires-session-key').each(function() {
+        $(this).find('.requires-session-key').each(function() {
             if (this.value && document.cookie.indexOf('session_key') == -1) {
                 console.log('Field ' + this.value + ' requires a session key');
                 $('#privkey_modal').modal('show');

+ 3 - 3
netbox/templates/_base.html

@@ -246,8 +246,8 @@
                 <ul class="nav navbar-nav navbar-right">
                     {% if request.user.is_authenticated %}
                         <li class="dropdown">
-                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
-                                {{ request.user }} <span class="caret"></span>
+                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" title="{{ request.user }}" role="button" aria-haspopup="true" aria-expanded="false">
+                                {{ request.user|truncatechars:"30" }} <span class="caret"></span>
                             </a>
                             <ul class="dropdown-menu">
                                 <li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
@@ -262,7 +262,7 @@
                         <li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
                     {% endif %}
                 </ul>
-                <form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" role="search">
+                <form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">
                     <div class="input-group">
                         <input type="text" name="q" class="form-control" placeholder="Search">
                         <span class="input-group-btn">

+ 0 - 2
netbox/templates/circuits/circuit_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Circuit Import{% endblock %}
 

+ 0 - 2
netbox/templates/circuits/provider_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Provider Import{% endblock %}
 

+ 0 - 2
netbox/templates/dcim/console_connections_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Console Connections Import{% endblock %}
 

+ 1 - 2
netbox/templates/dcim/console_connections_list.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}Console Connections{% endblock %}
 
@@ -16,7 +15,7 @@
 <h1>Console Connections</h1>
 <div class="row">
 	<div class="col-md-9">
-        {% render_table table 'table.html' %}
+        {% include 'responsive_table.html' %}
     </div>
     <div class="col-md-3">
 		{% include 'inc/search_panel.html' %}

+ 0 - 1
netbox/templates/dcim/device.html

@@ -1,6 +1,5 @@
 {% extends '_base.html' %}
 {% load static from staticfiles %}
-{% load render_table from django_tables2 %}
 {% load helpers %}
 
 {% block title %}{{ device }}{% endblock %}

+ 0 - 1
netbox/templates/dcim/device_import.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Device Import{% endblock %}

+ 0 - 1
netbox/templates/dcim/device_import_child.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Device Import{% endblock %}

+ 1 - 0
netbox/templates/dcim/device_list.html

@@ -76,6 +76,7 @@ $(document).ready(function() {
 
             // Update rack options
             rack_list.empty();
+            rack_list.append($("<option></option>").attr("value", "0").text("None"));
             $.ajax({
                 url: netbox_api_path + 'dcim/racks/?limit=500&site=' + selected_sites.join('&site='),
                 dataType: 'json',

+ 0 - 1
netbox/templates/dcim/devicetype.html

@@ -1,6 +1,5 @@
 {% extends '_base.html' %}
 {% load helpers %}
-{% load render_table from django_tables2 %}
 
 {% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
 

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

@@ -1,4 +1,3 @@
-{% load render_table from django_tables2 %}
 {% if perms.dcim.change_devicetype %}
     <form method="post">
         {% csrf_token %}
@@ -19,7 +18,7 @@
                     {% endif %}
                 </div>
             </div>
-            {% render_table table 'table.html' %}
+            {% include 'responsive_table.html' %}
             <div class="panel-footer">
                 {% if table.rows %}
                     {% if edit_url %}
@@ -48,6 +47,6 @@
         <div class="panel-heading">
             <strong>{{ title }}</strong>
         </div>
-        {% render_table table 'table.html' %}
+        {% include 'responsive_table.html' %}
     </div>
 {% endif %}

+ 0 - 2
netbox/templates/dcim/interface_connections_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Interface Connections Import{% endblock %}
 

+ 1 - 2
netbox/templates/dcim/interface_connections_list.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}Interface Connections{% endblock %}
 
@@ -16,7 +15,7 @@
 <h1>Interface Connections</h1>
 <div class="row">
 	<div class="col-md-9">
-        {% render_table table 'table.html' %}
+        {% include 'responsive_table.html' %}
     </div>
     <div class="col-md-3">
 		{% include 'inc/search_panel.html' %}

+ 0 - 2
netbox/templates/dcim/power_connections_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Power Connections Import{% endblock %}
 

+ 1 - 2
netbox/templates/dcim/power_connections_list.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}Power Connections{% endblock %}
 
@@ -16,7 +15,7 @@
 <h1>Power Connections</h1>
 <div class="row">
 	<div class="col-md-9">
-        {% render_table table 'table.html' %}
+        {% include 'responsive_table.html' %}
     </div>
     <div class="col-md-3">
 		{% include 'inc/search_panel.html' %}

+ 0 - 1
netbox/templates/dcim/rack.html

@@ -1,6 +1,5 @@
 {% extends '_base.html' %}
 {% load helpers %}
-{% load render_table from django_tables2 %}
 
 {% block title %}{{ rack.site }} - Rack {{ rack.name }}{% endblock %}
 

+ 17 - 16
netbox/templates/dcim/rack_elevation_list.html

@@ -11,24 +11,25 @@
     {% if page %}
         <div class="col-md-9">
             <div style="white-space: nowrap; overflow-x: scroll;">
-            {% for rack in page %}
-                <div style="display: inline-block; width: 266px">
-                    <div class="rack_header">
-                        <h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
+                {% for rack in page %}
+                    <div style="display: inline-block; width: 266px">
+                        <div class="rack_header">
+                            <h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
+                        </div>
+                        {% if face_id %}
+                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
+                        {% else %}
+                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
+                        {% endif %}
+                        <div class="clearfix"></div>
+                        <div class="rack_header">
+                            <h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
+                        </div>
                     </div>
-                    {% if face_id %}
-                        {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
-                    {% else %}
-                        {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
-                    {% endif %}
-                    <div class="clearfix"></div>
-                    <div class="rack_header">
-                        <h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
-                    </div>
-                </div>
-            {% endfor %}
+                {% endfor %}
             </div>
-            {% include 'paginator.html' %}
+            <br />
+            {% include 'inc/paginator.html' %}
         </div>
     {% else %}
         <div class="col-md-9">

+ 0 - 2
netbox/templates/dcim/rack_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Rack Import{% endblock %}
 

+ 0 - 1
netbox/templates/dcim/site.html

@@ -1,6 +1,5 @@
 {% extends '_base.html' %}
 {% load static from staticfiles %}
-{% load render_table from django_tables2 %}
 {% load helpers %}
 
 {% block title %}{{ site }}{% endblock %}

+ 0 - 1
netbox/templates/dcim/site_import.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Site Import{% endblock %}

+ 0 - 1
netbox/templates/home.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block content %}
 {% include 'search_form.html' %}

+ 1 - 2
netbox/templates/import_success.html

@@ -1,9 +1,8 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block content %}
     <h1>{% block title %}Import Completed{% endblock %}</h1>
-    {% render_table table %}
+    {% include 'responsive_table.html' %}
     <a href="{{ request.path }}" class="btn btn-primary">
         <span class="fa fa-download" aria-hidden="true"></span>
         Import more

+ 8 - 7
netbox/templates/paginator.html → netbox/templates/inc/paginator.html

@@ -1,11 +1,11 @@
 {% load helpers %}
 
-<div class="paginator pull-right" style="margin-top: 20px">
+<div class="paginator pull-right">
     {% if paginator.num_pages > 1 %}
         <nav>
             <ul class="pagination pull-right">
                 {% if page.has_previous %}
-                    <li><a href="{% querystring request page=page.previous_page_number %}">&laquo;</a></li>
+                    <li><a href="{% querystring request page=page.previous_page_number %}"><i class="fa fa-angle-double-left"></i></a></li>
                 {% endif %}
                 {% for p in page.smart_pages %}
                     {% if p %}
@@ -15,13 +15,14 @@
                     {% endif %}
                 {% endfor %}
                 {% if page.has_next %}
-                    <li><a href="{% querystring request page=page.next_page_number %}">&raquo;</a></li>
+                    <li><a href="{% querystring request page=page.next_page_number %}"><i class="fa fa-angle-double-right"></i></a></li>
                 {% endif %}
             </ul>
         </nav>
     {% endif %}
-    <div class="clearfix"></div>
-    <div class="text-right text-muted">
-        Showing {{ page.start_index }}-{{ page.end_index }} of {{ total_count }}
-    </div>
+    {% if page %}
+        <div class="text-right text-muted">
+            Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
+        </div>
+    {% endif %}
 </div>

+ 41 - 0
netbox/templates/inc/table.html

@@ -0,0 +1,41 @@
+{% load django_tables2 %}
+
+<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
+    {% if table.show_header %}
+        <thead>
+            <tr>
+                {% for column in table.columns %}
+                    {% if column.orderable %}
+                        <th {{ column.attrs.th.as_html }}><a href="{% querystring page=column.order_by_alias.next %}">{{ column.header }}</a></th>
+                    {% else %}
+                        <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
+                    {% endif %}
+                {% endfor %}
+            </tr>
+        </thead>
+    {% endif %}
+    <tbody>
+        {% for row in table.page.object_list|default:table.rows %}
+            <tr {{ row.attrs.as_html }}>
+                {% for column, cell in row.items %}
+                    <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
+                {% endfor %}
+            </tr>
+        {% empty %}
+            {% if table.empty_text %}
+                <tr>
+                    <td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td>
+                </tr>
+            {% endif %}
+        {% endfor %}
+    </tbody>
+    {% if table.has_footer %}
+        <tfoot>
+            <tr>
+                {% for column in table.columns %}
+                    <td>{{ column.footer }}</td>
+                {% endfor %}
+            </tr>
+        </tfoot>
+    {% endif %}
+</table>

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

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}Aggregate: {{ aggregate }}{% endblock %}
 

+ 0 - 2
netbox/templates/ipam/aggregate_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Aggregate Import{% endblock %}
 

+ 3 - 10
netbox/templates/ipam/ipaddress.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}{{ ipaddress }}{% endblock %}
 
@@ -133,17 +132,11 @@
         {% endwith %}
 	</div>
 	<div class="col-md-6">
-        {% with heading='Parent Prefixes' %}
-            {% render_table parent_prefixes_table 'panel_table.html' %}
-        {% endwith %}
+        {% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
         {% if duplicate_ips_table.rows %}
-            {% with heading='Duplicate IP Addresses' panel_class='danger' %}
-                {% render_table duplicate_ips_table 'panel_table.html' %}
-            {% endwith %}
+            {% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
         {% endif %}
-        {% with heading='Related IP Addresses' %}
-            {% render_table related_ips_table 'panel_table.html' %}
-        {% endwith %}
+        {% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
 	</div>
 </div>
 {% endblock %}

+ 0 - 2
netbox/templates/ipam/ipaddress_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}IP Address Import{% endblock %}
 

+ 2 - 7
netbox/templates/ipam/prefix.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}{{ prefix }}{% endblock %}
 
@@ -134,13 +133,9 @@
 	</div>
 	<div class="col-md-7">
         {% if duplicate_prefix_table.rows %}
-            {% with heading='Duplicate Prefixes' panel_class='danger' %}
-                {% render_table duplicate_prefix_table 'panel_table.html' %}
-            {% endwith %}
+            {% include 'panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' panel_class='danger' %}
         {% endif %}
-        {% with heading='Parent Prefixes' %}
-            {% render_table parent_prefix_table 'panel_table.html' %}
-        {% endwith %}
+        {% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
 	</div>
 </div>
 <div class="row">

+ 0 - 2
netbox/templates/ipam/prefix_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Prefix Import{% endblock %}
 

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

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}{{ prefix }}{% endblock %}
 

+ 1 - 2
netbox/templates/ipam/vlan.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}VLAN {{ vlan.display_name }}{% endblock %}
 
@@ -136,7 +135,7 @@
             <div class="panel-heading">
                 <strong>Prefixes</strong>
             </div>
-            {% render_table prefix_table %}
+            {% include 'responsive_table.html' with table=prefix_table %}
             {% if perms.ipam.add_prefix %}
                 <div class="panel-footer text-right">
                     <a href="{% url 'ipam:prefix_add' %}?{% if vlan.tenant %}tenant={{ vlan.tenant.pk }}&{% endif %}site={{ vlan.site.pk }}&vlan={{ vlan.pk }}" class="btn btn-primary btn-xs">

+ 2 - 2
netbox/templates/ipam/vlan_edit.html

@@ -5,11 +5,11 @@
     <div class="panel panel-default">
         <div class="panel-heading"><strong>VLAN</strong></div>
         <div class="panel-body">
-            {% render_field form.site %}
-            {% render_field form.group %}
             {% render_field form.vid %}
             {% render_field form.name %}
             {% render_field form.status %}
+            {% render_field form.site %}
+            {% render_field form.group %}
             {% render_field form.role %}
             {% render_field form.description %}
         </div>

+ 1 - 3
netbox/templates/ipam/vlan_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}VLAN Import{% endblock %}
 
@@ -17,7 +15,7 @@
         <tbody>
             <tr>
                 <td>Site</td>
-                <td>Name of assigned site</td>
+                <td>Name of assigned site (optional)</td>
                 <td>LAS2</td>
             </tr>
             <tr>

+ 1 - 2
netbox/templates/ipam/vrf.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 {% block title %}VRF {{ vrf }}{% endblock %}
 
@@ -92,7 +91,7 @@
             <div class="panel-heading">
                 <strong>Prefixes</strong>
             </div>
-            {% render_table prefix_table %}
+            {% include 'responsive_table.html' with table=prefix_table %}
         </div>
 	</div>
 </div>

+ 0 - 2
netbox/templates/ipam/vrf_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}VRF Import{% endblock %}
 

+ 7 - 13
netbox/templates/panel_table.html

@@ -1,10 +1,5 @@
-{% extends 'django_tables2/table.html' %}
-{% load django_tables2 %}
-{% load i18n %}
+{% load render_table from django_tables2 %}
 
-{# Wraps a table inside a Bootstrap panel and includes custom pagination rendering #}
-
-{% block table %}
 <div class="panel panel-{{ panel_class|default:'default' }}">
     {% if heading %}
         <div class="panel-heading">
@@ -12,15 +7,14 @@
         </div>
     {% endif %}
     {% if table.rows %}
-        {{ block.super }}
+        {% render_table table 'inc/table.html' %}
     {% else %}
         <div class="panel-body text-muted">None</div>
     {% endif %}
 </div>
-{% endblock %}
 
-{% block pagination %}
-    {% if not hide_paginator %}
-        {% include 'table_paginator.html' %}
-    {% endif %}
-{% endblock pagination %}
+{% if table.rows and not hide_paginator %}
+    {% with paginator=table.paginator page=table.page %}
+        {% include 'inc/paginator.html' %}
+    {% endwith %}
+{% endif %}

+ 8 - 0
netbox/templates/responsive_table.html

@@ -0,0 +1,8 @@
+{% load render_table from django_tables2 %}
+
+<div class="table-responsive">
+    {% render_table table 'inc/table.html' %}
+</div>
+{% with paginator=table.paginator page=table.page %}
+    {% include 'inc/paginator.html' %}
+{% endwith %}

+ 0 - 1
netbox/templates/secrets/secret_import.html

@@ -1,6 +1,5 @@
 {% extends '_base.html' %}
 {% load static from staticfiles %}
-{% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block title %}Secret Import{% endblock %}

+ 0 - 10
netbox/templates/table.html

@@ -1,10 +0,0 @@
-{% extends 'django_tables2/bootstrap-responsive.html' %}
-{% load django_tables2 %}
-
-{# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #}
-
-{% block pagination %}
-    {% if not hide_paginator %}
-        {% include 'table_paginator.html' %}
-    {% endif %}
-{% endblock pagination %}

+ 0 - 36
netbox/templates/table_paginator.html

@@ -1,36 +0,0 @@
-{% load django_tables2 %}
-
-{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
-
-<div class="paginator pull-right">
-    {% if table.paginator.num_pages > 1 %}
-        <nav>
-            <ul class="pagination pull-right">
-                {% if table.page.has_previous %}
-                    <li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"><i class="fa fa-angle-double-left"></i></a></li>
-                {% endif %}
-                {% for p in table.page.smart_pages %}
-                    {% if p %}
-                        <li{% ifequal table.page.number p %} class="active"{% endifequal %}><a href="{% querystring table.prefixed_page_field=p %}">{{ p }}</a></li>
-                    {% else %}
-                        <li class="disabled"><span>&hellip;</span></li>
-                    {% endif %}
-                {% endfor %}
-                {% if table.page.has_next %}
-                    <li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"><i class="fa fa-angle-double-right"></i></a></li>
-                {% endif %}
-            </ul>
-        </nav>
-    {% endif %}
-    <div class="clearfix"></div>
-    <div class="text-right text-muted">
-        {% with table.page.paginator.count as total %}
-            Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
-            {% if total == 1 %}
-                {{ table.data.verbose_name }}
-            {% else %}
-                {{ table.data.verbose_name_plural }}
-            {% endif %}
-        {% endwith %}
-    </div>
-</div>

+ 0 - 2
netbox/templates/tenancy/tenant_import.html

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 {% block title %}Tenant Import{% endblock %}
 

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

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 {% load form_helpers %}
 
 {% block content %}

+ 4 - 5
netbox/templates/utilities/obj_table.html

@@ -1,4 +1,3 @@
-{% load render_table from django_tables2 %}
 {% load helpers %}
 {% if permissions.change or permissions.delete %}
     <form method="post" class="form form-horizontal">
@@ -15,12 +14,12 @@
                     </div>
                     <div class="pull-right">
                         {% if bulk_edit_url and permissions.change %}
-                            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
+                            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
                                 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
                             </button>
                         {% endif %}
                         {% if bulk_delete_url and permissions.delete %}
-                            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
+                            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
                                 <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
                             </button>
                         {% endif %}
@@ -28,7 +27,7 @@
                 </div>
             </div>
         {% endif %}
-        {% render_table table table_template|default:'table.html' %}
+        {% include table_template|default:'responsive_table.html' %}
         {% block extra_actions %}{% endblock %}
         {% if bulk_edit_url and permissions.change %}
             <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
@@ -42,6 +41,6 @@
         {% endif %}
     </form>
 {% else %}
-    {% render_table table table_template|default:'table.html' %}
+    {% include table_template|default:'responsive_table.html' %}
 {% endif %}
 <div class="clearfix"></div>

+ 2 - 4
netbox/utilities/forms.py

@@ -443,12 +443,10 @@ class ChainedFieldsMixin(forms.BaseForm):
 
                 filters_dict = {}
                 for db_field, parent_field in field.chains.items():
-                    if self.is_bound:
-                        filters_dict[db_field] = self.data.get(parent_field) or None
+                    if self.is_bound and self.data.get(parent_field):
+                        filters_dict[db_field] = self.data[parent_field]
                     elif self.initial.get(parent_field):
                         filters_dict[db_field] = self.initial[parent_field]
-                    else:
-                        filters_dict[db_field] = None
 
                 if filters_dict:
                     field.queryset = field.queryset.filter(**filters_dict)

+ 0 - 1
netbox/utilities/views.py

@@ -4,7 +4,6 @@ from django_tables2 import RequestConfig
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
 from django.db import transaction, IntegrityError
 from django.db.models import ProtectedError
 from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField