Просмотр исходного кода

Merge pull request #1201 from digitalocean/develop

Release v2.0.3
Jeremy Stretch 8 лет назад
Родитель
Сommit
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)
 * [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))
 * [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:
 Python 3:
 
 
 ```no-highlight
 ```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
 # update-alternatives --install /usr/bin/python python /usr/bin/python3 1
 ```
 ```
 
 
 Python 2:
 Python 2:
 
 
 ```no-highlight
 ```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**
 **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
     Alias /static /opt/netbox/netbox/static
 
 
+    # Needed to allow token-based API authentication
+    WSGIPassAuthorization on
+
     <Directory /opt/netbox/netbox/static>
     <Directory /opt/netbox/netbox/static>
         Options Indexes FollowSymLinks MultiViews
         Options Indexes FollowSymLinks MultiViews
         AllowOverride None
         AllowOverride None

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

@@ -581,9 +581,18 @@ class WritablePowerPortSerializer(serializers.ModelSerializer):
 # Interfaces
 # 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):
 class InterfaceSerializer(serializers.ModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
     form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
+    lag = NestedInterfaceSerializer()
     connection = serializers.SerializerMethodField(read_only=True)
     connection = serializers.SerializerMethodField(read_only=True)
     connected_interface = 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',
         method='filter_type',
         label='Interface type',
         label='Interface type',
     )
     )
+    lag_id = django_filters.ModelMultipleChoiceFilter(
+        name='lag',
+        queryset=Interface.objects.all(),
+        label='LAG interface (ID)',
+    )
     mac_address = django_filters.CharFilter(
     mac_address = django_filters.CharFilter(
         method='_mac_address',
         method='_mac_address',
         label='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',
         queryset=Platform.objects.all(), required=False, to_field_name='name',
         error_messages={'invalid_choice': 'Invalid platform.'}
         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:
     class Meta:
         fields = []
         fields = []
@@ -692,8 +692,12 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
             except DeviceType.DoesNotExist:
             except DeviceType.DoesNotExist:
                 self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
                 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):
 class DeviceFromCSVForm(BaseDeviceFromCSVForm):
@@ -707,8 +711,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
 
 
     class Meta(BaseDeviceFromCSVForm.Meta):
     class Meta(BaseDeviceFromCSVForm.Meta):
         fields = [
         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):
     def clean(self):
@@ -751,8 +755,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
 
 
     class Meta(BaseDeviceFromCSVForm.Meta):
     class Meta(BaseDeviceFromCSVForm.Meta):
         fields = [
         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):
     def clean(self):
@@ -817,13 +821,15 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     rack_id = FilterChoiceField(
     rack_id = FilterChoiceField(
         queryset=Rack.objects.annotate(filter_count=Count('devices')),
         queryset=Rack.objects.annotate(filter_count=Count('devices')),
         label='Rack',
         label='Rack',
+        null_option=(0, 'None'),
     )
     )
     role = FilterChoiceField(
     role = FilterChoiceField(
         queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
         queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
         to_field_name='slug',
         to_field_name='slug',
     )
     )
     tenant = FilterChoiceField(
     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'),
         null_option=(0, 'None'),
     )
     )
     manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
     manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
@@ -1207,7 +1213,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
     )
     )
     power_outlet = ChainedModelChoiceField(
     power_outlet = ChainedModelChoiceField(
         queryset=PowerOutlet.objects.all(),
         queryset=PowerOutlet.objects.all(),
-        chains={'device': 'device'},
+        chains={'device': 'pdu'},
         label='Outlet',
         label='Outlet',
         widget=APISelect(
         widget=APISelect(
             api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
             api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@@ -1441,7 +1447,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
         label='Interface',
         label='Interface',
         widget=APISelect(
         widget=APISelect(
             api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
             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 import forms
+from django.core.exceptions import ValidationError
 from django.db.models import Count
 from django.db.models import Count
 
 
 from dcim.models import Site, Rack, Device, Interface
 from dcim.models import Site, Rack, Device, Interface
@@ -195,14 +196,16 @@ class PrefixFromCSVForm(forms.ModelForm):
                                   error_messages={'invalid_choice': 'Site not found.'})
                                   error_messages={'invalid_choice': 'Site not found.'})
     vlan_group_name = forms.CharField(required=False)
     vlan_group_name = forms.CharField(required=False)
     vlan_vid = forms.IntegerField(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',
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
                                   error_messages={'invalid_choice': 'Invalid role.'})
                                   error_messages={'invalid_choice': 'Invalid role.'})
 
 
     class Meta:
     class Meta:
         model = Prefix
         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):
     def clean(self):
 
 
@@ -237,12 +240,12 @@ class PrefixFromCSVForm(forms.ModelForm):
             except VLAN.MultipleObjectsReturned:
             except VLAN.MultipleObjectsReturned:
                 self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
                 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):
 class PrefixImportForm(BootstrapMixin, BulkImportForm):
@@ -491,7 +494,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
                                  error_messages={'invalid_choice': 'VRF not found.'})
                                  error_messages={'invalid_choice': 'VRF not found.'})
     tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
     tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
                                     error_messages={'invalid_choice': 'Tenant not found.'})
                                     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',
     device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
                                     error_messages={'invalid_choice': 'Device not found.'})
                                     error_messages={'invalid_choice': 'Device not found.'})
     interface_name = forms.CharField(required=False)
     interface_name = forms.CharField(required=False)
@@ -499,7 +502,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = IPAddress
         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):
     def clean(self):
 
 
@@ -522,10 +525,14 @@ class IPAddressFromCSVForm(forms.ModelForm):
         if is_primary and not device:
         if is_primary and not device:
             self.add_error('is_primary', "No device specified; cannot set as primary IP")
             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
         # Set interface
         if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
         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):
 class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
     site = forms.ModelChoiceField(
     site = forms.ModelChoiceField(
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
+        required=False,
         widget=forms.Select(
         widget=forms.Select(
             attrs={'filter-for': 'group', 'nullable': 'true'}
             attrs={'filter-for': 'group', 'nullable': 'true'}
         )
         )
@@ -649,7 +657,7 @@ class VLANFromCSVForm(forms.ModelForm):
         Tenant.objects.all(), to_field_name='name', required=False,
         Tenant.objects.all(), to_field_name='name', required=False,
         error_messages={'invalid_choice': 'Tenant not found.'}
         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(
     role = forms.ModelChoiceField(
         queryset=Role.objects.all(), required=False, to_field_name='name',
         queryset=Role.objects.all(), required=False, to_field_name='name',
         error_messages={'invalid_choice': 'Invalid role.'}
         error_messages={'invalid_choice': 'Invalid role.'}
@@ -657,7 +665,7 @@ class VLANFromCSVForm(forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = VLAN
         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):
     def clean(self):
 
 
@@ -671,6 +679,13 @@ class VLANFromCSVForm(forms.ModelForm):
             except VLANGroup.DoesNotExist:
             except VLANGroup.DoesNotExist:
                 self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
                 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):
     def save(self, *args, **kwargs):
 
 
         vlan = super(VLANFromCSVForm, self).save(commit=False)
         vlan = super(VLANFromCSVForm, self).save(commit=False)
@@ -679,9 +694,6 @@ class VLANFromCSVForm(forms.ModelForm):
         if self.cleaned_data['group_name']:
         if self.cleaned_data['group_name']:
             vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=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'):
         if kwargs.get('commit'):
             vlan.save()
             vlan.save()
         return vlan
         return vlan

+ 2 - 2
netbox/ipam/tables.py

@@ -70,9 +70,9 @@ IPADDRESS_LINK = """
 {% if record.pk %}
 {% if record.pk %}
     <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
     <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
 {% elif perms.ipam.add_ipaddress %}
 {% 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 %}
 {% else %}
-    {{ record.0 }}
+    {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
 {% endif %}
 {% endif %}
 """
 """
 
 

+ 1 - 0
netbox/ipam/views.py

@@ -525,6 +525,7 @@ def prefix_ipaddresses(request, pk):
         'prefix': prefix,
         'prefix': prefix,
         'ip_table': ip_table,
         'ip_table': ip_table,
         'permissions': permissions,
         '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
 # Import local configuration
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

+ 29 - 28
netbox/netbox/views.py

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

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

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

+ 3 - 3
netbox/templates/_base.html

@@ -246,8 +246,8 @@
                 <ul class="nav navbar-nav navbar-right">
                 <ul class="nav navbar-nav navbar-right">
                     {% if request.user.is_authenticated %}
                     {% if request.user.is_authenticated %}
                         <li class="dropdown">
                         <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>
                             </a>
                             <ul class="dropdown-menu">
                             <ul class="dropdown-menu">
                                 <li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
                                 <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>
                         <li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
                     {% endif %}
                     {% endif %}
                 </ul>
                 </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">
                     <div class="input-group">
                         <input type="text" name="q" class="form-control" placeholder="Search">
                         <input type="text" name="q" class="form-control" placeholder="Search">
                         <span class="input-group-btn">
                         <span class="input-group-btn">

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,6 +1,5 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
-{% load render_table from django_tables2 %}
 
 
 {% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
 {% 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 %}
 {% if perms.dcim.change_devicetype %}
     <form method="post">
     <form method="post">
         {% csrf_token %}
         {% csrf_token %}
@@ -19,7 +18,7 @@
                     {% endif %}
                     {% endif %}
                 </div>
                 </div>
             </div>
             </div>
-            {% render_table table 'table.html' %}
+            {% include 'responsive_table.html' %}
             <div class="panel-footer">
             <div class="panel-footer">
                 {% if table.rows %}
                 {% if table.rows %}
                     {% if edit_url %}
                     {% if edit_url %}
@@ -48,6 +47,6 @@
         <div class="panel-heading">
         <div class="panel-heading">
             <strong>{{ title }}</strong>
             <strong>{{ title }}</strong>
         </div>
         </div>
-        {% render_table table 'table.html' %}
+        {% include 'responsive_table.html' %}
     </div>
     </div>
 {% endif %}
 {% endif %}

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

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

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

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

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

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

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

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

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

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

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

@@ -11,24 +11,25 @@
     {% if page %}
     {% if page %}
         <div class="col-md-9">
         <div class="col-md-9">
             <div style="white-space: nowrap; overflow-x: scroll;">
             <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>
                     </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>
             </div>
-            {% include 'paginator.html' %}
+            <br />
+            {% include 'inc/paginator.html' %}
         </div>
         </div>
     {% else %}
     {% else %}
         <div class="col-md-9">
         <div class="col-md-9">

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

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

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

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

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

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

+ 0 - 1
netbox/templates/home.html

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

+ 1 - 2
netbox/templates/import_success.html

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

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

@@ -1,11 +1,11 @@
 {% load helpers %}
 {% load helpers %}
 
 
-<div class="paginator pull-right" style="margin-top: 20px">
+<div class="paginator pull-right">
     {% if paginator.num_pages > 1 %}
     {% if paginator.num_pages > 1 %}
         <nav>
         <nav>
             <ul class="pagination pull-right">
             <ul class="pagination pull-right">
                 {% if page.has_previous %}
                 {% 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 %}
                 {% endif %}
                 {% for p in page.smart_pages %}
                 {% for p in page.smart_pages %}
                     {% if p %}
                     {% if p %}
@@ -15,13 +15,14 @@
                     {% endif %}
                     {% endif %}
                 {% endfor %}
                 {% endfor %}
                 {% if page.has_next %}
                 {% 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 %}
                 {% endif %}
             </ul>
             </ul>
         </nav>
         </nav>
     {% endif %}
     {% 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>
 </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' %}
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 
 {% block title %}Aggregate: {{ aggregate }}{% endblock %}
 {% block title %}Aggregate: {{ aggregate }}{% endblock %}
 
 

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

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

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

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 
 {% block title %}{{ ipaddress }}{% endblock %}
 {% block title %}{{ ipaddress }}{% endblock %}
 
 
@@ -133,17 +132,11 @@
         {% endwith %}
         {% endwith %}
 	</div>
 	</div>
 	<div class="col-md-6">
 	<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 %}
         {% 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 %}
         {% 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>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

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

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

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

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 
 {% block title %}{{ prefix }}{% endblock %}
 {% block title %}{{ prefix }}{% endblock %}
 
 
@@ -134,13 +133,9 @@
 	</div>
 	</div>
 	<div class="col-md-7">
 	<div class="col-md-7">
         {% if duplicate_prefix_table.rows %}
         {% 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 %}
         {% 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>
 </div>
 <div class="row">
 <div class="row">

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

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

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

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

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

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 
 {% block title %}VLAN {{ vlan.display_name }}{% endblock %}
 {% block title %}VLAN {{ vlan.display_name }}{% endblock %}
 
 
@@ -136,7 +135,7 @@
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Prefixes</strong>
                 <strong>Prefixes</strong>
             </div>
             </div>
-            {% render_table prefix_table %}
+            {% include 'responsive_table.html' with table=prefix_table %}
             {% if perms.ipam.add_prefix %}
             {% if perms.ipam.add_prefix %}
                 <div class="panel-footer text-right">
                 <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">
                     <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 panel-default">
         <div class="panel-heading"><strong>VLAN</strong></div>
         <div class="panel-heading"><strong>VLAN</strong></div>
         <div class="panel-body">
         <div class="panel-body">
-            {% render_field form.site %}
-            {% render_field form.group %}
             {% render_field form.vid %}
             {% render_field form.vid %}
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.status %}
             {% render_field form.status %}
+            {% render_field form.site %}
+            {% render_field form.group %}
             {% render_field form.role %}
             {% render_field form.role %}
             {% render_field form.description %}
             {% render_field form.description %}
         </div>
         </div>

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

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

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

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

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

@@ -1,6 +1,4 @@
 {% extends 'utilities/obj_import.html' %}
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 
 {% block title %}VRF Import{% endblock %}
 {% 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' }}">
 <div class="panel panel-{{ panel_class|default:'default' }}">
     {% if heading %}
     {% if heading %}
         <div class="panel-heading">
         <div class="panel-heading">
@@ -12,15 +7,14 @@
         </div>
         </div>
     {% endif %}
     {% endif %}
     {% if table.rows %}
     {% if table.rows %}
-        {{ block.super }}
+        {% render_table table 'inc/table.html' %}
     {% else %}
     {% else %}
         <div class="panel-body text-muted">None</div>
         <div class="panel-body text-muted">None</div>
     {% endif %}
     {% endif %}
 </div>
 </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' %}
 {% extends '_base.html' %}
 {% load static from staticfiles %}
 {% load static from staticfiles %}
-{% load render_table from django_tables2 %}
 {% load form_helpers %}
 {% load form_helpers %}
 
 
 {% block title %}Secret Import{% endblock %}
 {% 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' %}
 {% extends 'utilities/obj_import.html' %}
-{% load render_table from django_tables2 %}
-{% load form_helpers %}
 
 
 {% block title %}Tenant Import{% endblock %}
 {% block title %}Tenant Import{% endblock %}
 
 

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

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

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

@@ -1,4 +1,3 @@
-{% load render_table from django_tables2 %}
 {% load helpers %}
 {% load helpers %}
 {% if permissions.change or permissions.delete %}
 {% if permissions.change or permissions.delete %}
     <form method="post" class="form form-horizontal">
     <form method="post" class="form form-horizontal">
@@ -15,12 +14,12 @@
                     </div>
                     </div>
                     <div class="pull-right">
                     <div class="pull-right">
                         {% if bulk_edit_url and permissions.change %}
                         {% 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
                                 <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit All
                             </button>
                             </button>
                         {% endif %}
                         {% endif %}
                         {% if bulk_delete_url and permissions.delete %}
                         {% 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
                                 <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete All
                             </button>
                             </button>
                         {% endif %}
                         {% endif %}
@@ -28,7 +27,7 @@
                 </div>
                 </div>
             </div>
             </div>
         {% endif %}
         {% endif %}
-        {% render_table table table_template|default:'table.html' %}
+        {% include table_template|default:'responsive_table.html' %}
         {% block extra_actions %}{% endblock %}
         {% block extra_actions %}{% endblock %}
         {% if bulk_edit_url and permissions.change %}
         {% 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">
             <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 %}
         {% endif %}
     </form>
     </form>
 {% else %}
 {% else %}
-    {% render_table table table_template|default:'table.html' %}
+    {% include table_template|default:'responsive_table.html' %}
 {% endif %}
 {% endif %}
 <div class="clearfix"></div>
 <div class="clearfix"></div>

+ 2 - 4
netbox/utilities/forms.py

@@ -443,12 +443,10 @@ class ChainedFieldsMixin(forms.BaseForm):
 
 
                 filters_dict = {}
                 filters_dict = {}
                 for db_field, parent_field in field.chains.items():
                 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):
                     elif self.initial.get(parent_field):
                         filters_dict[db_field] = self.initial[parent_field]
                         filters_dict[db_field] = self.initial[parent_field]
-                    else:
-                        filters_dict[db_field] = None
 
 
                 if filters_dict:
                 if filters_dict:
                     field.queryset = field.queryset.filter(**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.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
 from django.db import transaction, IntegrityError
 from django.db import transaction, IntegrityError
 from django.db.models import ProtectedError
 from django.db.models import ProtectedError
 from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
 from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField