Ver código fonte

merge develop

John Anderson 6 anos atrás
pai
commit
9116d74cf7
80 arquivos alterados com 353 adições e 1137 exclusões
  1. 4 0
      base_requirements.txt
  2. 3 3
      docs/configuration/required-settings.md
  3. 23 0
      docs/release-notes/version-2.7.md
  4. 0 3
      netbox/circuits/views.py
  5. 43 15
      netbox/dcim/forms.py
  6. 1 1
      netbox/dcim/migrations/0093_device_component_ordering.py
  7. 1 1
      netbox/dcim/migrations/0094_device_component_template_ordering.py
  8. 1 1
      netbox/dcim/migrations/0095_primary_model_ordering.py
  9. 1 1
      netbox/dcim/migrations/0096_interface_ordering.py
  10. 2 2
      netbox/dcim/models/__init__.py
  11. 12 23
      netbox/dcim/views.py
  12. 0 20
      netbox/extras/apps.py
  13. 1 1
      netbox/extras/management/commands/renaturalize.py
  14. 3 2
      netbox/extras/views.py
  15. 10 0
      netbox/ipam/api/views.py
  16. 1 7
      netbox/ipam/views.py
  17. 1 1
      netbox/netbox/settings.py
  18. 10 7
      netbox/project-static/js/forms.js
  19. 1 2
      netbox/secrets/views.py
  20. 0 21
      netbox/templates/circuits/circuit_list.html
  21. 0 18
      netbox/templates/circuits/circuittype_list.html
  22. 0 21
      netbox/templates/circuits/provider_list.html
  23. 0 20
      netbox/templates/dcim/cable_list.html
  24. 0 17
      netbox/templates/dcim/consoleport_list.html
  25. 0 17
      netbox/templates/dcim/consoleserverport_list.html
  26. 21 18
      netbox/templates/dcim/device_list.html
  27. 0 17
      netbox/templates/dcim/devicebay_list.html
  28. 0 18
      netbox/templates/dcim/devicerole_list.html
  29. 0 21
      netbox/templates/dcim/devicetype_list.html
  30. 0 17
      netbox/templates/dcim/frontport_list.html
  31. 0 24
      netbox/templates/dcim/inc/device_table.html
  32. 0 17
      netbox/templates/dcim/interface_list.html
  33. 0 21
      netbox/templates/dcim/inventoryitem_list.html
  34. 0 18
      netbox/templates/dcim/manufacturer_list.html
  35. 0 18
      netbox/templates/dcim/platform_list.html
  36. 0 21
      netbox/templates/dcim/powerfeed_list.html
  37. 0 17
      netbox/templates/dcim/poweroutlet_list.html
  38. 0 21
      netbox/templates/dcim/powerpanel_list.html
  39. 0 17
      netbox/templates/dcim/powerport_list.html
  40. 0 21
      netbox/templates/dcim/rack_list.html
  41. 0 21
      netbox/templates/dcim/rackgroup_list.html
  42. 0 14
      netbox/templates/dcim/rackreservation_list.html
  43. 0 18
      netbox/templates/dcim/rackrole_list.html
  44. 0 17
      netbox/templates/dcim/rearport_list.html
  45. 0 21
      netbox/templates/dcim/region_list.html
  46. 0 21
      netbox/templates/dcim/site_list.html
  47. 0 18
      netbox/templates/dcim/virtualchassis_list.html
  48. 0 19
      netbox/templates/extras/configcontext_list.html
  49. 6 17
      netbox/templates/extras/objectchange_list.html
  50. 0 14
      netbox/templates/extras/tag_list.html
  51. 1 1
      netbox/templates/inc/nav_menu.html
  52. 10 27
      netbox/templates/ipam/aggregate_list.html
  53. 0 21
      netbox/templates/ipam/ipaddress_list.html
  54. 2 19
      netbox/templates/ipam/prefix_list.html
  55. 9 22
      netbox/templates/ipam/rir_list.html
  56. 0 18
      netbox/templates/ipam/role_list.html
  57. 0 17
      netbox/templates/ipam/service_list.html
  58. 0 21
      netbox/templates/ipam/vlan_list.html
  59. 0 21
      netbox/templates/ipam/vlangroup_list.html
  60. 0 21
      netbox/templates/ipam/vrf_list.html
  61. 0 20
      netbox/templates/secrets/secret_list.html
  62. 0 18
      netbox/templates/secrets/secretrole_list.html
  63. 0 21
      netbox/templates/tenancy/tenant_list.html
  64. 0 18
      netbox/templates/tenancy/tenantgroup_list.html
  65. 79 0
      netbox/templates/utilities/obj_list.html
  66. 0 21
      netbox/templates/virtualization/cluster_list.html
  67. 0 18
      netbox/templates/virtualization/clustergroup_list.html
  68. 0 18
      netbox/templates/virtualization/clustertype_list.html
  69. 0 14
      netbox/templates/virtualization/inc/virtualmachine_table.html
  70. 11 18
      netbox/templates/virtualization/virtualmachine_list.html
  71. 0 2
      netbox/tenancy/views.py
  72. 13 0
      netbox/utilities/constants.py
  73. 7 2
      netbox/utilities/forms.py
  74. 4 4
      netbox/utilities/ordering.py
  75. 17 2
      netbox/utilities/templatetags/helpers.py
  76. 8 2
      netbox/utilities/tests/test_ordering.py
  77. 9 9
      netbox/utilities/views.py
  78. 37 89
      netbox/virtualization/forms.py
  79. 0 3
      netbox/virtualization/views.py
  80. 1 0
      requirements.txt

+ 4 - 0
base_requirements.txt

@@ -22,6 +22,10 @@ django-filter
 # https://github.com/django-mptt/django-mptt
 django-mptt
 
+# Context managers for PostgreSQL advisory locks
+# https://github.com/Xof/django-pglocks
+django-pglocks
+
 # Prometheus metrics library for Django
 # https://github.com/korfuri/django-prometheus
 django-prometheus

+ 3 - 3
docs/configuration/required-settings.md

@@ -80,11 +80,11 @@ REDIS = {
 }
 ```
 
-!!! note:
+!!! note
     If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
     changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
 
-!!! warning:
+!!! note
     It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
     same Redis instance for both may result in webhook processing data being lost during cache flushing events.
 
@@ -124,7 +124,7 @@ REDIS = {
 }
 ```
 
-!!! note:
+!!! note
     It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible
     for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via 
     `SENTINELS`/`SENTINEL_SERVICE`.

+ 23 - 0
docs/release-notes/version-2.7.md

@@ -1,3 +1,26 @@
+# v2.7.7 (FUTURE)
+
+## Enhancements
+
+* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment
+* [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings
+
+## Bug Fixes
+
+* [#2519](https://github.com/netbox-community/netbox/issues/2519) - Avoid race condition when provisioning "next available" IPs/prefixes via the API
+* [#4168](https://github.com/netbox-community/netbox/issues/4168) - Role is not required when creating a virtual machine
+* [#4175](https://github.com/netbox-community/netbox/issues/4175) - Fix potential exception when bulk editing objects from a filtered list
+
+---
+
+# v2.7.6 (2020-02-13)
+
+## Bug Fixes
+
+* [#4166](https://github.com/netbox-community/netbox/issues/4166) - Fix schema migrations to enforce maximum character length for naturalized fields
+
+---
+
 # v2.7.5 (2020-02-13)
 
 **Note:** This release includes several database schema migrations that calculate and store copies of names for certain objects to improve natural ordering performance (see [#3799](https://github.com/netbox-community/netbox/issues/3799)). These migrations may take a few minutes to run if you have a very large number of objects defined in NetBox.

+ 0 - 3
netbox/circuits/views.py

@@ -29,7 +29,6 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ProviderFilterSet
     filterset_form = forms.ProviderFilterForm
     table = tables.ProviderDetailTable
-    template_name = 'circuits/provider_list.html'
 
 
 class ProviderView(PermissionRequiredMixin, View):
@@ -107,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'circuits.view_circuittype'
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
     table = tables.CircuitTypeTable
-    template_name = 'circuits/circuittype_list.html'
 
 
 class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -151,7 +149,6 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.CircuitFilterSet
     filterset_form = forms.CircuitFilterForm
     table = tables.CircuitTable
-    template_name = 'circuits/circuit_list.html'
 
 
 class CircuitView(PermissionRequiredMixin, View):

+ 43 - 15
netbox/dcim/forms.py

@@ -2832,7 +2832,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -2842,7 +2845,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tags = TagField(
@@ -2871,18 +2877,20 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        # Limit LAG choices to interfaces belonging to this device (or VC master)
         if self.is_bound:
             device = Device.objects.get(pk=self.data['device'])
-            self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[device, device.get_vc_master()],
-                type=InterfaceTypeChoices.TYPE_LAG
-            )
         else:
-            self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[self.instance.device, self.instance.device.get_vc_master()],
-                type=InterfaceTypeChoices.TYPE_LAG
-            )
+            device = self.instance.device
+
+        # Limit LAG choices to interfaces belonging to this device (or VC master)
+        self.fields['lag'].queryset = Interface.objects.filter(
+            device__in=[device, device.get_vc_master()],
+            type=InterfaceTypeChoices.TYPE_LAG
+        )
+
+        # Add current site to VLANs query params
+        self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
+        self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
 
 
 class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
@@ -2942,7 +2950,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -2951,7 +2962,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -2967,6 +2981,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
             type=InterfaceTypeChoices.TYPE_LAG
         )
 
+        # Add current site to VLANs query params
+        self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
+        self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
+
 
 class InterfaceCSVForm(forms.ModelForm):
     device = FlexibleModelChoiceField(
@@ -3090,7 +3108,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -3099,7 +3120,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -3118,6 +3142,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
                 device__in=[device, device.get_vc_master()],
                 type=InterfaceTypeChoices.TYPE_LAG
             )
+
+            # Add current site to VLANs query params
+            self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
+            self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
         else:
             self.fields['lag'].choices = ()
             self.fields['lag'].widget.attrs['disabled'] = True

+ 1 - 1
netbox/dcim/migrations/0093_device_component_ordering.py

@@ -6,7 +6,7 @@ import utilities.ordering
 def _update_model_names(model):
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
-        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name))
+        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
 
 
 def naturalize_consoleports(apps, schema_editor):

+ 1 - 1
netbox/dcim/migrations/0094_device_component_template_ordering.py

@@ -6,7 +6,7 @@ import utilities.ordering
 def _update_model_names(model):
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
-        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name))
+        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
 
 
 def naturalize_consoleporttemplates(apps, schema_editor):

+ 1 - 1
netbox/dcim/migrations/0095_primary_model_ordering.py

@@ -6,7 +6,7 @@ import utilities.ordering
 def _update_model_names(model):
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
-        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name))
+        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
 
 
 def naturalize_sites(apps, schema_editor):

+ 1 - 1
netbox/dcim/migrations/0096_interface_ordering.py

@@ -6,7 +6,7 @@ import utilities.ordering
 def _update_model_names(model):
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
-        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name))
+        model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name, max_length=100))
 
 
 def naturalize_interfacetemplates(apps, schema_editor):

+ 2 - 2
netbox/dcim/models/__init__.py

@@ -382,8 +382,8 @@ class RackElevationHelperMixin:
 
         # add gradients
         RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
-        RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#f0f0f0')
-        RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc7c7')
+        RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#d7d7d7')
+        RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc0c0')
 
         return drawing
 

+ 12 - 23
netbox/dcim/views.py

@@ -152,7 +152,6 @@ class RegionListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.RegionFilterSet
     filterset_form = forms.RegionFilterForm
     table = tables.RegionTable
-    template_name = 'dcim/region_list.html'
 
 
 class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -191,7 +190,6 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.SiteFilterSet
     filterset_form = forms.SiteFilterForm
     table = tables.SiteTable
-    template_name = 'dcim/site_list.html'
 
 
 class SiteView(PermissionRequiredMixin, View):
@@ -271,7 +269,6 @@ class RackGroupListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.RackGroupFilterSet
     filterset_form = forms.RackGroupFilterForm
     table = tables.RackGroupTable
-    template_name = 'dcim/rackgroup_list.html'
 
 
 class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -308,7 +305,6 @@ class RackRoleListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_rackrole'
     queryset = RackRole.objects.annotate(rack_count=Count('racks'))
     table = tables.RackRoleTable
-    template_name = 'dcim/rackrole_list.html'
 
 
 class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -350,7 +346,6 @@ class RackListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.RackFilterSet
     filterset_form = forms.RackFilterForm
     table = tables.RackDetailTable
-    template_name = 'dcim/rack_list.html'
 
 
 class RackElevationListView(PermissionRequiredMixin, View):
@@ -474,7 +469,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.RackReservationFilterSet
     filterset_form = forms.RackReservationFilterForm
     table = tables.RackReservationTable
-    template_name = 'dcim/rackreservation_list.html'
+    action_buttons = ()
 
 
 class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -533,7 +528,6 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
         platform_count=Count('platforms', distinct=True),
     )
     table = tables.ManufacturerTable
-    template_name = 'dcim/manufacturer_list.html'
 
 
 class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -571,7 +565,6 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.DeviceTypeFilterSet
     filterset_form = forms.DeviceTypeFilterForm
     table = tables.DeviceTypeTable
-    template_name = 'dcim/devicetype_list.html'
 
 
 class DeviceTypeView(PermissionRequiredMixin, View):
@@ -995,7 +988,6 @@ class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_devicerole'
     queryset = DeviceRole.objects.all()
     table = tables.DeviceRoleTable
-    template_name = 'dcim/devicerole_list.html'
 
 
 class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -1031,7 +1023,6 @@ class PlatformListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_platform'
     queryset = Platform.objects.all()
     table = tables.PlatformTable
-    template_name = 'dcim/platform_list.html'
 
 
 class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -1292,7 +1283,7 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ConsolePortFilterSet
     filterset_form = forms.ConsolePortFilterForm
     table = tables.ConsolePortDetailTable
-    template_name = 'dcim/consoleport_list.html'
+    action_buttons = ('import', 'export')
 
 
 class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1345,7 +1336,7 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ConsoleServerPortFilterSet
     filterset_form = forms.ConsoleServerPortFilterForm
     table = tables.ConsoleServerPortDetailTable
-    template_name = 'dcim/consoleserverport_list.html'
+    action_buttons = ('import', 'export')
 
 
 class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1410,7 +1401,7 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.PowerPortFilterSet
     filterset_form = forms.PowerPortFilterForm
     table = tables.PowerPortDetailTable
-    template_name = 'dcim/powerport_list.html'
+    action_buttons = ('import', 'export')
 
 
 class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1463,7 +1454,7 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.PowerOutletFilterSet
     filterset_form = forms.PowerOutletFilterForm
     table = tables.PowerOutletDetailTable
-    template_name = 'dcim/poweroutlet_list.html'
+    action_buttons = ('import', 'export')
 
 
 class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1528,7 +1519,7 @@ class InterfaceListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.InterfaceFilterSet
     filterset_form = forms.InterfaceFilterForm
     table = tables.InterfaceDetailTable
-    template_name = 'dcim/interface_list.html'
+    action_buttons = ('import', 'export')
 
 
 class InterfaceView(PermissionRequiredMixin, View):
@@ -1630,7 +1621,7 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.FrontPortFilterSet
     filterset_form = forms.FrontPortFilterForm
     table = tables.FrontPortDetailTable
-    template_name = 'dcim/frontport_list.html'
+    action_buttons = ('import', 'export')
 
 
 class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1695,7 +1686,7 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.RearPortFilterSet
     filterset_form = forms.RearPortFilterForm
     table = tables.RearPortDetailTable
-    template_name = 'dcim/rearport_list.html'
+    action_buttons = ('import', 'export')
 
 
 class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1762,7 +1753,7 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.DeviceBayFilterSet
     filterset_form = forms.DeviceBayFilterForm
     table = tables.DeviceBayDetailTable
-    template_name = 'dcim/devicebay_list.html'
+    action_buttons = ('import', 'export')
 
 
 class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -1961,7 +1952,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.CableFilterSet
     filterset_form = forms.CableFilterForm
     table = tables.CableTable
-    template_name = 'dcim/cable_list.html'
+    action_buttons = ('import', 'export')
 
 
 class CableView(PermissionRequiredMixin, View):
@@ -2233,7 +2224,7 @@ class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.InventoryItemFilterSet
     filterset_form = forms.InventoryItemFilterForm
     table = tables.InventoryItemTable
-    template_name = 'dcim/inventoryitem_list.html'
+    action_buttons = ('import', 'export')
 
 
 class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
@@ -2289,7 +2280,7 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
     table = tables.VirtualChassisTable
     filterset = filters.VirtualChassisFilterSet
     filterset_form = forms.VirtualChassisFilterForm
-    template_name = 'dcim/virtualchassis_list.html'
+    action_buttons = ('export',)
 
 
 class VirtualChassisCreateView(PermissionRequiredMixin, View):
@@ -2533,7 +2524,6 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.PowerPanelFilterSet
     filterset_form = forms.PowerPanelFilterForm
     table = tables.PowerPanelTable
-    template_name = 'dcim/powerpanel_list.html'
 
 
 class PowerPanelView(PermissionRequiredMixin, View):
@@ -2602,7 +2592,6 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.PowerFeedFilterSet
     filterset_form = forms.PowerFeedFilterForm
     table = tables.PowerFeedTable
-    template_name = 'dcim/powerfeed_list.html'
 
 
 class PowerFeedView(PermissionRequiredMixin, View):

+ 0 - 20
netbox/extras/apps.py

@@ -1,28 +1,8 @@
 from django.apps import AppConfig
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-import redis
 
 
 class ExtrasConfig(AppConfig):
     name = "extras"
 
     def ready(self):
-
         import extras.signals
-
-        # Check that we can connect to the configured Redis database.
-        try:
-            rs = redis.Redis(
-                host=settings.WEBHOOKS_REDIS_HOST,
-                port=settings.WEBHOOKS_REDIS_PORT,
-                db=settings.WEBHOOKS_REDIS_DATABASE,
-                password=settings.WEBHOOKS_REDIS_PASSWORD or None,
-                ssl=settings.WEBHOOKS_REDIS_SSL,
-            )
-            rs.ping()
-        except redis.exceptions.ConnectionError:
-            raise ImproperlyConfigured(
-                "Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
-                "configuration.py."
-            )

+ 1 - 1
netbox/extras/management/commands/renaturalize.py

@@ -86,7 +86,7 @@ class Command(BaseCommand):
                 # Find all unique values for the field
                 queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
                 for value in queryset:
-                    naturalized_value = naturalize(value)
+                    naturalized_value = naturalize(value, max_length=field.max_length)
 
                     if options['verbosity'] >= 2:
                         self.stdout.write("  {} -> {}".format(value, naturalized_value), ending='')

+ 3 - 2
netbox/extras/views.py

@@ -34,7 +34,7 @@ class TagListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.TagFilterSet
     filterset_form = forms.TagFilterForm
     table = TagTable
-    template_name = 'extras/tag_list.html'
+    action_buttons = ()
 
 
 class TagView(PermissionRequiredMixin, View):
@@ -111,7 +111,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ConfigContextFilterSet
     filterset_form = forms.ConfigContextFilterForm
     table = ConfigContextTable
-    template_name = 'extras/configcontext_list.html'
+    action_buttons = ('add',)
 
 
 class ConfigContextView(PermissionRequiredMixin, View):
@@ -191,6 +191,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
     filterset_form = forms.ObjectChangeFilterForm
     table = ObjectChangeTable
     template_name = 'extras/objectchange_list.html'
+    action_buttons = ('export',)
 
 
 class ObjectChangeView(PermissionRequiredMixin, View):

+ 10 - 0
netbox/ipam/api/views.py

@@ -1,6 +1,7 @@
 from django.conf import settings
 from django.db.models import Count
 from django.shortcuts import get_object_or_404
+from django_pglocks import advisory_lock
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
@@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
 from ipam import filters
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.api import FieldChoicesViewSet, ModelViewSet
+from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.utils import get_subquery
 from . import serializers
 
@@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
     filterset_class = filters.PrefixFilterSet
 
     @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
     def available_prefixes(self, request, pk=None):
         """
         A convenience method for returning available child prefixes within a parent.
+
+        The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
+        invoked in parallel, which results in a race condition where multiple insertions can occur.
         """
         prefix = get_object_or_404(Prefix, pk=pk)
         available_prefixes = prefix.get_available_prefixes()
@@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
             return Response(serializer.data)
 
     @action(detail=True, url_path='available-ips', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
     def available_ips(self, request, pk=None):
         """
         A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
         returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
         however results will not be paginated.
+
+        The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
+        invoked in parallel, which results in a race condition where multiple insertions can occur.
         """
         prefix = get_object_or_404(Prefix, pk=pk)
 

+ 1 - 7
netbox/ipam/views.py

@@ -118,7 +118,6 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.VRFFilterSet
     filterset_form = forms.VRFFilterForm
     table = tables.VRFTable
-    template_name = 'ipam/vrf_list.html'
 
 
 class VRFView(PermissionRequiredMixin, View):
@@ -293,7 +292,6 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
     queryset = Aggregate.objects.prefetch_related('rir').annotate(
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
     )
-
     filterset = filters.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateDetailTable
@@ -411,7 +409,6 @@ class RoleListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_role'
     queryset = Role.objects.all()
     table = tables.RoleTable
-    template_name = 'ipam/role_list.html'
 
 
 class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -644,7 +641,6 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.IPAddressFilterSet
     filterset_form = forms.IPAddressFilterForm
     table = tables.IPAddressDetailTable
-    template_name = 'ipam/ipaddress_list.html'
 
 
 class IPAddressView(PermissionRequiredMixin, View):
@@ -817,7 +813,6 @@ class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.VLANGroupFilterSet
     filterset_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
-    template_name = 'ipam/vlangroup_list.html'
 
 
 class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -893,7 +888,6 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.VLANFilterSet
     filterset_form = forms.VLANFilterForm
     table = tables.VLANDetailTable
-    template_name = 'ipam/vlan_list.html'
 
 
 class VLANView(PermissionRequiredMixin, View):
@@ -989,7 +983,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ServiceFilterSet
     filterset_form = forms.ServiceFilterForm
     table = tables.ServiceTable
-    template_name = 'ipam/service_list.html'
+    action_buttons = ('export',)
 
 
 class ServiceView(PermissionRequiredMixin, View):

+ 1 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
 # Environment setup
 #
 
-VERSION = '2.7.6-dev'
+VERSION = '2.7.7-dev'
 
 # Hostname
 HOSTNAME = platform.node()

+ 10 - 7
netbox/project-static/js/forms.js

@@ -190,15 +190,18 @@ $(document).ready(function() {
                 $.each(element.attributes, function(index, attr){
                     if (attr.name.includes("data-additional-query-param-")){
                         var param_name = attr.name.split("data-additional-query-param-")[1];
-                        if (param_name in parameters) {
-                            if (Array.isArray(parameters[param_name])) {
-                                parameters[param_name].push(attr.value)
+
+                        $.each($.parseJSON(attr.value), function(index, value) {
+                            if (param_name in parameters) {
+                                if (Array.isArray(parameters[param_name])) {
+                                    parameters[param_name].push(value);
+                                } else {
+                                    parameters[param_name] = [parameters[param_name], value];
+                                }
                             } else {
-                                parameters[param_name] = [parameters[param_name], attr.value]
+                                parameters[param_name] = value;
                             }
-                        } else {
-                            parameters[param_name] = attr.value;
-                        }
+                        });
                     }
                 });
 

+ 1 - 2
netbox/secrets/views.py

@@ -35,7 +35,6 @@ class SecretRoleListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'secrets.view_secretrole'
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
     table = tables.SecretRoleTable
-    template_name = 'secrets/secretrole_list.html'
 
 
 class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -73,7 +72,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.SecretFilterSet
     filterset_form = forms.SecretFilterForm
     table = tables.SecretTable
-    template_name = 'secrets/secret_list.html'
+    action_buttons = ('import', 'export')
 
 
 class SecretView(PermissionRequiredMixin, View):

+ 0 - 21
netbox/templates/circuits/circuit_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.circuits.add_circuit %}
-        {% add_button 'circuits:circuit_add' %}
-        {% import_button 'circuits:circuit_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Circuits{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/circuits/circuittype_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.circuits.add_circuittype %}
-        {% add_button 'circuits:circuittype_add' %}
-        {% import_button 'circuits:circuittype_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Circuit Types{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/circuits/provider_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.circuits.add_provider %}
-        {% add_button 'circuits:provider_add' %}
-        {% import_button 'circuits:provider_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Providers{% endblock %}</h1>
-<div class="row">
-    <div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 20
netbox/templates/dcim/cable_list.html

@@ -1,20 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_cable %}
-        {% import_button 'dcim:cable_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Cables{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:cable_bulk_edit' bulk_delete_url='dcim:cable_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/consoleport_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Console Ports{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:consoleport_bulk_edit' bulk_delete_url='dcim:consoleport_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/consoleserverport_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Console Server Ports{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:consoleserverport_bulk_edit' bulk_delete_url='dcim:consoleserverport_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 21 - 18
netbox/templates/dcim/device_list.html

@@ -1,21 +1,24 @@
-{% extends '_base.html' %}
-{% load buttons %}
+{% extends 'utilities/obj_list.html' %}
 
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_device %}
-        {% add_button 'dcim:device_add' %}
-        {% import_button 'dcim:device_import' %}
+{% block bulk_buttons %}
+    {% if perms.dcim.change_device %}
+        <div class="btn-group">
+            <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu">
+                {% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
+                {% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
+                {% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
+                {% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
+                {% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
+                {% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
+            </ul>
+        </div>
+    {% endif %}
+    {% if perms.dcim.add_virtualchassis %}
+        <button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
+            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
+        </button>
     {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Devices{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
 {% endblock %}

+ 0 - 17
netbox/templates/dcim/devicebay_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Device Bays{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicebay_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/dcim/devicerole_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_devicerole %}
-        {% add_button 'dcim:devicerole_add' %}
-        {% import_button 'dcim:devicerole_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Device Roles{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/devicetype_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_devicetype %}
-        {% add_button 'dcim:devicetype_add' %}
-        {% import_button 'dcim:devicetype_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Device Types{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/frontport_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Front Ports{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:frontport_bulk_edit' bulk_delete_url='dcim:frontport_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 24
netbox/templates/dcim/inc/device_table.html

@@ -1,24 +0,0 @@
-{% extends 'utilities/obj_table.html' %}
-
-{% block extra_actions %}
-    {% if perms.dcim.change_device %}
-        <div class="btn-group">
-            <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
-            </button>
-            <ul class="dropdown-menu">
-                {% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
-                {% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
-                {% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
-                {% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
-                {% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
-                {% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
-            </ul>
-        </div>
-    {% endif %}
-    {% if perms.dcim.add_virtualchassis %}
-        <button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
-        </button>
-    {% endif %}
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/interface_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Interfaces{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:interface_bulk_edit' bulk_delete_url='dcim:interface_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/inventoryitem_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-{% load helpers %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_devicetype %}
-        {% import_button 'dcim:inventoryitem_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Inventory Items{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/dcim/manufacturer_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_manufacturer %}
-        {% add_button 'dcim:manufacturer_add' %}
-        {% import_button 'dcim:manufacturer_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Manufacturers{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/dcim/platform_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_platform %}
-        {% add_button 'dcim:platform_add' %}
-        {% import_button 'dcim:platform_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Platforms{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/powerfeed_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_powerfeed %}
-        {% add_button 'dcim:powerfeed_add' %}
-        {% import_button 'dcim:powerfeed_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Power Feeds{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/poweroutlet_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Power Outlets{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:poweroutlet_bulk_edit' bulk_delete_url='dcim:poweroutlet_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/powerpanel_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_powerpanel %}
-        {% add_button 'dcim:powerpanel_add' %}
-        {% import_button 'dcim:powerpanel_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Power Panels{% endblock %}</h1>
-<div class="row">
-    <div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %}
-    </div>
-    <div class="col-md-3">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/powerport_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Power Ports{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerport_bulk_edit' bulk_delete_url='dcim:powerport_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/rack_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_rack %}
-        {% add_button 'dcim:rack_add' %}
-        {% import_button 'dcim:rack_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Racks{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/rackgroup_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_rackgroup %}
-        {% add_button 'dcim:rackgroup_add' %}
-        {% import_button 'dcim:rackgroup_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Rack Groups{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 14
netbox/templates/dcim/rackreservation_list.html

@@ -1,14 +0,0 @@
-{% extends '_base.html' %}
-{% load helpers %}
-
-{% block content %}
-<h1>{% block title %}Rack Reservations{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rackreservation_bulk_edit' bulk_delete_url='dcim:rackreservation_bulk_delete' %}
-    </div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/dcim/rackrole_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_rackrole %}
-        {% add_button 'dcim:rackrole_add' %}
-        {% import_button 'dcim:rackrole_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Rack Roles{% endblock %}</h1>
-<div class="row">
-    <div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/dcim/rearport_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Rear Ports{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rearport_bulk_edit' bulk_delete_url='dcim:rearport_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/region_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_region %}
-        {% add_button 'dcim:region_add' %}
-        {% import_button 'dcim:region_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Regions{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
-    </div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/dcim/site_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.dcim.add_site %}
-        {% add_button 'dcim:site_add' %}
-        {% import_button 'dcim:site_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Sites{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' bulk_delete_url='dcim:site_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/dcim/virtualchassis_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-{% load helpers %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Virtual Chassis{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 19
netbox/templates/extras/configcontext_list.html

@@ -1,19 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-    <div class="pull-right noprint">
-        {% if perms.extras.add_configcontext %}
-            {% add_button 'extras:configcontext_add' %}
-        {% endif %}
-    </div>
-    <h1>{% block title %}Config Contexts{% endblock %}</h1>
-    <div class="row">
-        <div class="col-md-9">
-            {% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
-        </div>
-        <div class="col-md-3 noprint">
-            {% include 'inc/search_panel.html' %}
-        </div>
-    </div>
-{% endblock %}

+ 6 - 17
netbox/templates/extras/objectchange_list.html

@@ -1,20 +1,9 @@
-{% extends '_base.html' %}
-{% load buttons %}
+{% extends 'utilities/obj_list.html' %}
 
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Changelog{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' %}
-        <div class="text-muted text-right">
-            Changelog retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
-        </div>
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
+{% block title %}Change Log{% endblock %}
+
+{% block sidebar %}
+    <div class="text-muted">
+        Change log retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
     </div>
-</div>
 {% endblock %}

+ 0 - 14
netbox/templates/extras/tag_list.html

@@ -1,14 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<h1>{% block title %}Tags{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='extras:tag_bulk_edit' bulk_delete_url='extras:tag_bulk_delete' %}
-    </div>
-    <div class="col-md-3">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 1 - 1
netbox/templates/inc/nav_menu.html

@@ -473,7 +473,7 @@
                     <ul class="dropdown-menu">
                         <li class="dropdown-header">Logging</li>
                         <li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
-                            <a href="{% url 'extras:objectchange_list' %}">Changelog</a>
+                            <a href="{% url 'extras:objectchange_list' %}">Change Log</a>
                         </li>
                         <li class="divider"></li>
                         <li class="dropdown-header">Miscellaneous</li>

+ 10 - 27
netbox/templates/ipam/aggregate_list.html

@@ -1,31 +1,14 @@
-{% extends '_base.html' %}
-{% load buttons %}
+{% extends 'utilities/obj_list.html' %}
 {% load humanize %}
 
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.ipam.add_aggregate %}
-        {% add_button 'ipam:aggregate_add' %}
-        {% import_button 'ipam:aggregate_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Aggregates{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}
-	</div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong><i class="fa fa-bar-chart"></i> Statistics</strong>
-            </div>
-            <ul class="list-group">
-                <li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
-                <li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
-            </ul>
+{% block sidebar %}
+    <div class="panel panel-default">
+        <div class="panel-heading">
+            <strong><i class="fa fa-bar-chart"></i> Statistics</strong>
         </div>
-	</div>
-</div>
+        <ul class="list-group">
+            <li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
+            <li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
+        </ul>
+    </div>
 {% endblock %}

+ 0 - 21
netbox/templates/ipam/ipaddress_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.ipam.add_ipaddress %}
-        {% add_button 'ipam:ipaddress_add' %}
-        {% import_button 'ipam:ipaddress_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}IP Addresses{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
-	</div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}

+ 2 - 19
netbox/templates/ipam/prefix_list.html

@@ -1,26 +1,9 @@
-{% extends '_base.html' %}
-{% load buttons %}
+{% extends 'utilities/obj_list.html' %}
 {% load helpers %}
 
-{% block content %}
-<div class="pull-right noprint">
+{% block buttons %}
     <div class="btn-group" role="group">
         <a href="{% url 'ipam:prefix_list' %}{% querystring request expand=None page=1 %}" class="btn btn-default{% if not request.GET.expand %} active{% endif %}">Collapse</a>
         <a href="{% url 'ipam:prefix_list' %}{% querystring request expand='on' page=1 %}" class="btn btn-default{% if request.GET.expand %} active{% endif %}">Expand</a>
     </div>
-    {% if perms.ipam.add_prefix %}
-        {% add_button 'ipam:prefix_add' %}
-        {% import_button 'ipam:prefix_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Prefixes{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
-	</div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
 {% endblock %}

+ 9 - 22
netbox/templates/ipam/rir_list.html

@@ -1,9 +1,6 @@
-{% extends '_base.html' %}
-{% load buttons %}
-{% load humanize %}
+{% extends 'utilities/obj_list.html' %}
 
-{% block content %}
-<div class="pull-right noprint">
+{% block buttons %}
     {% if request.GET.family == '6' %}
         <a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
             <span class="fa fa-table" aria-hidden="true"></span>
@@ -15,22 +12,12 @@
             IPv6 Stats
         </a>
     {% endif %}
-    {% if perms.ipam.add_rir %}
-        {% add_button 'ipam:rir_add' %}
-        {% import_button 'ipam:rir_import' %}
+{% endblock %}
+
+{% block sidebar %}
+    {% if request.GET.family == '6' %}
+        <div class="alert alert-info">
+            <i class="fa fa-info-circle"></i> Numbers shown indicate /64 prefixes.
+        </div>
     {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}RIRs{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
-        {% if request.GET.family == '6' %}
-            <div class="alert alert-info pull-right"><strong>Note:</strong> Numbers shown indicate /64 prefixes.</div>
-        {% endif %}
-    </div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
 {% endblock %}

+ 0 - 18
netbox/templates/ipam/role_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.ipam.add_role %}
-        {% add_button 'ipam:role_add' %}
-        {% import_button 'ipam:role_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:role_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 17
netbox/templates/ipam/service_list.html

@@ -1,17 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Services{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:service_bulk_edit' bulk_delete_url='ipam:service_bulk_delete' %}
-	</div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/ipam/vlan_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.ipam.add_vlan %}
-        {% add_button 'ipam:vlan_add' %}
-        {% import_button 'ipam:vlan_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}VLANs{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
-	</div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/ipam/vlangroup_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.ipam.add_vlangroup %}
-        {% add_button 'ipam:vlangroup_add' %}
-        {% import_button 'ipam:vlangroup_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}VLAN Groups{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/ipam/vrf_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.ipam.add_vrf %}
-        {% add_button 'ipam:vrf_add' %}
-        {% import_button 'ipam:vrf_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}VRFs{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vrf_bulk_edit' bulk_delete_url='ipam:vrf_bulk_delete' %}
-	</div>
-	<div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-	</div>
-</div>
-{% endblock %}

+ 0 - 20
netbox/templates/secrets/secret_list.html

@@ -1,20 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.secrets.add_secret %}
-        {% import_button 'secrets:secret_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Secrets{% endblock %}</h1>
-<div class="row">
-    <div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='secrets:secret_bulk_edit' bulk_delete_url='secrets:secret_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/secrets/secretrole_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.secrets.add_secretrole %}
-        {% add_button 'secrets:secretrole_add' %}
-        {% import_button 'secrets:secretrole_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Secret Roles{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='secrets:secretrole_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 21
netbox/templates/tenancy/tenant_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.tenancy.add_tenant %}
-        {% add_button 'tenancy:tenant_add' %}
-        {% import_button 'tenancy:tenant_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Tenants{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='tenancy:tenant_bulk_edit' bulk_delete_url='tenancy:tenant_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/tenancy/tenantgroup_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.tenancy.add_tenantgroup %}
-        {% add_button 'tenancy:tenantgroup_add' %}
-        {% import_button 'tenancy:tenantgroup_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Tenant Groups{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='tenancy:tenantgroup_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 79 - 0
netbox/templates/utilities/obj_list.html

@@ -0,0 +1,79 @@
+{% extends '_base.html' %}
+{% load buttons %}
+{% load helpers %}
+
+{% block content %}
+<div class="pull-right noprint">
+    {% block buttons %}{% endblock %}
+    {% if permissions.add and 'add' in action_buttons %}
+        {% add_button content_type.model_class|url_name:"add" %}
+    {% endif %}
+    {% if permissions.add and 'import' in action_buttons %}
+        {% import_button content_type.model_class|url_name:"import" %}
+    {% endif %}
+    {% if 'export' in action_buttons %}
+        {% export_button content_type %}
+    {% endif %}
+</div>
+<h1>{% block title %}{{ content_type.model_class|model_name_plural|bettertitle }}{% endblock %}</h1>
+<div class="row">
+    <div class="col-md-9">
+        {% with bulk_edit_url=content_type.model_class|url_name:"bulk_edit" bulk_delete_url=content_type.model_class|url_name:"bulk_delete" %}
+        {% if permissions.change or permissions.delete %}
+            <form method="post" class="form form-horizontal">
+                {% csrf_token %}
+                <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
+                {% if table.paginator.num_pages > 1 %}
+                    <div id="select_all_box" class="hidden panel panel-default noprint">
+                        <div class="panel-body">
+                            <div class="checkbox-inline">
+                                <label for="select_all">
+                                    <input type="checkbox" id="select_all" name="_all" />
+                                    Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
+                                </label>
+                            </div>
+                            <div class="pull-right">
+                                {% if bulk_edit_url and permissions.change %}
+                                    <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 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 %}
+                            </div>
+                        </div>
+                    </div>
+                {% endif %}
+                {% include table_template|default:'responsive_table.html' %}
+                <div class="pull-left noprint">
+                    {% block bulk_buttons %}{% endblock %}
+                    {% if bulk_edit_url and permissions.change %}
+                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}" class="btn btn-warning btn-sm">
+                            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
+                        </button>
+                    {% endif %}
+                    {% if bulk_delete_url and permissions.delete %}
+                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}" class="btn btn-danger btn-sm">
+                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
+                        </button>
+                    {% endif %}
+                </div>
+            </form>
+        {% else %}
+            {% include table_template|default:'responsive_table.html' %}
+        {% endif %}
+        {% endwith %}
+        {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+        <div class="clearfix"></div>
+    </div>
+    <div class="col-md-3 noprint">
+        {% if filter_form %}
+            {% include 'inc/search_panel.html' %}
+        {% endif %}
+        {% block sidebar %}{% endblock %}
+    </div>
+</div>
+{% endblock %}

+ 0 - 21
netbox/templates/virtualization/cluster_list.html

@@ -1,21 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.virtualization.add_cluster %}
-        {% add_button 'virtualization:cluster_add' %}
-        {% import_button 'virtualization:cluster_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Clusters{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'utilities/obj_table.html' with bulk_edit_url='virtualization:cluster_bulk_edit' bulk_delete_url='virtualization:cluster_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/virtualization/clustergroup_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.virtualization.add_clustergroup %}
-        {% add_button 'virtualization:clustergroup_add' %}
-        {% import_button 'virtualization:clustergroup_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Cluster Groups{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='virtualization:clustergroup_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 18
netbox/templates/virtualization/clustertype_list.html

@@ -1,18 +0,0 @@
-{% extends '_base.html' %}
-{% load buttons %}
-
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.virtualization.add_clustertype %}
-        {% add_button 'virtualization:clustertype_add' %}
-        {% import_button 'virtualization:clustertype_import' %}
-    {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Cluster Types{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-12">
-        {% include 'utilities/obj_table.html' with bulk_delete_url='virtualization:clustertype_bulk_delete' %}
-    </div>
-</div>
-{% endblock %}

+ 0 - 14
netbox/templates/virtualization/inc/virtualmachine_table.html

@@ -1,14 +0,0 @@
-{% extends 'utilities/obj_table.html' %}
-
-{% block extra_actions %}
-    {% if perms.virtualization.change_virtualmachine %}
-        <div class="btn-group">
-            <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
-            </button>
-            <ul class="dropdown-menu">
-                {% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
-            </ul>
-        </div>
-    {% endif %}
-{% endblock %}

+ 11 - 18
netbox/templates/virtualization/virtualmachine_list.html

@@ -1,21 +1,14 @@
-{% extends '_base.html' %}
-{% load buttons %}
+{% extends 'utilities/obj_list.html' %}
 
-{% block content %}
-<div class="pull-right noprint">
-    {% if perms.virtualization.add_virtualmachine %}
-        {% add_button 'virtualization:virtualmachine_add' %}
-        {% import_button 'virtualization:virtualmachine_import' %}
+{% block bulk_buttons %}
+    {% if perms.virtualization.change_virtualmachine %}
+        <div class="btn-group">
+            <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu">
+                {% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
+            </ul>
+        </div>
     {% endif %}
-    {% export_button content_type %}
-</div>
-<h1>{% block title %}Virtual Machines{% endblock %}</h1>
-<div class="row">
-	<div class="col-md-9">
-        {% include 'virtualization/inc/virtualmachine_table.html' with bulk_edit_url='virtualization:virtualmachine_bulk_edit' bulk_delete_url='virtualization:virtualmachine_bulk_delete' %}
-    </div>
-    <div class="col-md-3 noprint">
-		{% include 'inc/search_panel.html' %}
-    </div>
-</div>
 {% endblock %}

+ 0 - 2
netbox/tenancy/views.py

@@ -22,7 +22,6 @@ class TenantGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'tenancy.view_tenantgroup'
     queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
     table = tables.TenantGroupTable
-    template_name = 'tenancy/tenantgroup_list.html'
 
 
 class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -60,7 +59,6 @@ class TenantListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.TenantFilterSet
     filterset_form = forms.TenantFilterForm
     table = tables.TenantTable
-    template_name = 'tenancy/tenant_list.html'
 
 
 class TenantView(PermissionRequiredMixin, View):

+ 13 - 0
netbox/utilities/constants.py

@@ -69,3 +69,16 @@ FILTER_LOOKUP_HELP_TEXT_MAP = dict(
     gte='greater than or equal',
     n='negated'
 )
+
+
+# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
+# the advisory_lock contextmanager. When a lock is acquired,
+# one of these keys will be used to identify said lock.
+#
+# When adding a new key, pick something arbitrary and unique so
+# that it is easily searchable in query logs.
+
+ADVISORY_LOCK_KEYS = {
+    'available-prefixes': 100100,
+    'available-ips': 100200,
+}

+ 7 - 2
netbox/utilities/forms.py

@@ -309,12 +309,17 @@ class APISelect(SelectWithDisabled):
 
     def add_additional_query_param(self, name, value):
         """
-        Add details for an additional query param in the form of a data-* attribute.
+        Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
 
         :param name: The name of the query param
         :param value: The value of the query param
         """
-        self.attrs['data-additional-query-param-{}'.format(name)] = value
+        key = 'data-additional-query-param-{}'.format(name)
+
+        values = json.loads(self.attrs.get(key, '[]'))
+        values.append(value)
+
+        self.attrs[key] = json.dumps(values)
 
     def add_conditional_query_param(self, condition, value):
         """

+ 4 - 4
netbox/utilities/ordering.py

@@ -10,7 +10,7 @@ INTERFACE_NAME_REGEX = r'(^(?P<type>[^\d\.:]+)?)' \
                        r'(.(?P<vc>\d+)$)?'
 
 
-def naturalize(value, max_length=None, integer_places=8):
+def naturalize(value, max_length, integer_places=8):
     """
     Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings
     are ordered naturally. For example:
@@ -39,10 +39,10 @@ def naturalize(value, max_length=None, integer_places=8):
             output.append(segment)
     ret = ''.join(output)
 
-    return ret[:max_length] if max_length else ret
+    return ret[:max_length]
 
 
-def naturalize_interface(value, max_length=None):
+def naturalize_interface(value, max_length):
     """
     Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old
     InterfaceManager.
@@ -77,4 +77,4 @@ def naturalize_interface(value, max_length=None):
             output.append('000000')
 
     ret = ''.join(output)
-    return ret[:max_length] if max_length else ret
+    return ret[:max_length]

+ 17 - 2
netbox/utilities/templatetags/helpers.py

@@ -1,9 +1,10 @@
 import datetime
 import json
 import re
-import yaml
 
+import yaml
 from django import template
+from django.urls import NoReverseMatch, reverse
 from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
 from markdown import markdown
@@ -11,7 +12,6 @@ from markdown import markdown
 from utilities.choices import unpack_grouped_choices
 from utilities.utils import foreground_color
 
-
 register = template.Library()
 
 
@@ -101,6 +101,21 @@ def model_name_plural(obj):
     return obj._meta.verbose_name_plural
 
 
+@register.filter()
+def url_name(model, action):
+    """
+    Return the URL name for the given model and action, or None if invalid.
+    """
+    url_name = '{}:{}_{}'.format(model._meta.app_label, model._meta.model_name, action)
+    try:
+        # Validate and return the URL name. We don't return the actual URL yet because many of the templates
+        # are written to pass a name to {% url %}.
+        reverse(url_name)
+        return url_name
+    except NoReverseMatch:
+        return None
+
+
 @register.filter()
 def contains(value, arg):
     """

+ 8 - 2
netbox/utilities/tests/test_ordering.py

@@ -21,7 +21,10 @@ class NaturalizationTestCase(TestCase):
         )
 
         for origin, naturalized in data:
-            self.assertEqual(naturalize(origin), naturalized)
+            self.assertEqual(naturalize(origin, max_length=50), naturalized)
+
+    def test_naturalize_max_length(self):
+        self.assertEqual(naturalize('abc123def456', max_length=10), 'abc0000012')
 
     def test_naturalize_interface(self):
 
@@ -40,4 +43,7 @@ class NaturalizationTestCase(TestCase):
         )
 
         for origin, naturalized in data:
-            self.assertEqual(naturalize_interface(origin), naturalized)
+            self.assertEqual(naturalize_interface(origin, max_length=50), naturalized)
+
+    def test_naturalize_interface_max_length(self):
+        self.assertEqual(naturalize_interface('Gi1/2/3', max_length=20), '0001000299999999Gi00')

+ 9 - 9
netbox/utilities/views.py

@@ -71,7 +71,8 @@ class ObjectListView(View):
     filterset = None
     filterset_form = None
     table = None
-    template_name = None
+    template_name = 'utilities/obj_list.html'
+    action_buttons = ('add', 'import', 'export')
 
     def queryset_to_yaml(self):
         """
@@ -156,9 +157,11 @@ class ObjectListView(View):
         # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
         self.queryset = self.alter_queryset(request)
 
-        # Compile user model permissions for access from within the template
-        perm_base_name = '{}.{{}}_{}'.format(model._meta.app_label, model._meta.model_name)
-        permissions = {p: request.user.has_perm(perm_base_name.format(p)) for p in ['add', 'change', 'delete']}
+        # Compile a dictionary indicating which permissions are available to the current user for this model
+        permissions = {}
+        for action in ('add', 'change', 'delete', 'view'):
+            perm_name = '{}.{}_{}'.format(model._meta.app_label, action, model._meta.model_name)
+            permissions[action] = request.user.has_perm(perm_name)
 
         # Construct the table based on the user's permissions
         table = self.table(self.queryset)
@@ -176,6 +179,7 @@ class ObjectListView(View):
             'content_type': content_type,
             'table': table,
             'permissions': permissions,
+            'action_buttons': self.action_buttons,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
         }
         context.update(self.extra_context())
@@ -630,7 +634,7 @@ class BulkEditView(GetReturnURLMixin, View):
             post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
 
         if '_apply' in request.POST:
-            form = self.form(model, request.POST, initial=request.GET)
+            form = self.form(model, request.POST)
             if form.is_valid():
 
                 custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
@@ -714,10 +718,6 @@ class BulkEditView(GetReturnURLMixin, View):
         else:
             # Pass the PK list as initial data to avoid binding the form
             initial_data = querydict_to_dict(post_data)
-
-            # Append any normal initial data (passed as GET parameters)
-            initial_data.update(request.GET)
-
             form = self.form(model, initial=initial_data)
 
         # Retrieve objects being edited

+ 37 - 89
netbox/virtualization/forms.py

@@ -351,6 +351,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.all(),
+        required=False,
         widget=APISelect(
             api_url="/api/dcim/device-roles/",
             additional_query_params={
@@ -658,7 +659,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -667,7 +671,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tags = TagField(
@@ -695,35 +702,12 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-
+        # Add current site to VLANs query params
         site = getattr(self.instance.parent, 'site', None)
         if site is not None:
-
-            # Add non-grouped site VLANs
-            site_vlans = VLAN.objects.filter(site=site, group=None)
-            vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-            # Add grouped site VLANs
-            for group in VLANGroup.objects.filter(site=site):
-                site_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append((
-                    '{} / {}'.format(group.site.name, group.name),
-                    [(vlan.pk, vlan) for vlan in site_group_vlans]
-                ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
+            # Add current site to VLANs query params
+            self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
+            self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
 
     def clean(self):
         super().clean()
@@ -784,7 +768,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -793,7 +780,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tags = TagField(
@@ -807,35 +797,11 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
             pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
         )
 
-        # Limit VLAN choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-
         site = getattr(virtual_machine.cluster, 'site', None)
         if site is not None:
-
-            # Add non-grouped site VLANs
-            site_vlans = VLAN.objects.filter(site=site, group=None)
-            vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-            # Add grouped site VLANs
-            for group in VLANGroup.objects.filter(site=site):
-                site_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append((
-                    '{} / {}'.format(group.site.name, group.name),
-                    [(vlan.pk, vlan) for vlan in site_group_vlans]
-                ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
+            # Add current site to VLANs query params
+            self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
+            self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
 
 
 class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -872,7 +838,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -881,7 +850,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -897,35 +869,11 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         if 'virtual_machine' in self.initial:
             parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
 
-            # Limit VLAN choices to global VLANs, VLANs in global groups, the current site's group, the current site
-            vlan_choices = []
-            global_vlans = VLAN.objects.filter(site=None, group=None)
-            vlan_choices.append(
-                ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-            )
-            for group in VLANGroup.objects.filter(site=None):
-                global_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append(
-                    (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-                )
-            if parent_obj.cluster is not None:
-                site = getattr(parent_obj.cluster, 'site', None)
-                if site is not None:
-
-                    # Add non-grouped site VLANs
-                    site_vlans = VLAN.objects.filter(site=site, group=None)
-                    vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-                    # Add grouped site VLANs
-                    for group in VLANGroup.objects.filter(site=site):
-                        site_group_vlans = VLAN.objects.filter(group=group)
-                        vlan_choices.append((
-                            '{} / {}'.format(group.site.name, group.name),
-                            [(vlan.pk, vlan) for vlan in site_group_vlans]
-                        ))
-
-            self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-            self.fields['tagged_vlans'].choices = vlan_choices
+            site = getattr(parent_obj.cluster, 'site', None)
+            if site is not None:
+                # Add current site to VLANs query params
+                self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
+                self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
 
 
 #

+ 0 - 3
netbox/virtualization/views.py

@@ -26,7 +26,6 @@ class ClusterTypeListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'virtualization.view_clustertype'
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
     table = tables.ClusterTypeTable
-    template_name = 'virtualization/clustertype_list.html'
 
 
 class ClusterTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -62,7 +61,6 @@ class ClusterGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'virtualization.view_clustergroup'
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
     table = tables.ClusterGroupTable
-    template_name = 'virtualization/clustergroup_list.html'
 
 
 class ClusterGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -100,7 +98,6 @@ class ClusterListView(PermissionRequiredMixin, ObjectListView):
     table = tables.ClusterTable
     filterset = filters.ClusterFilterSet
     filterset_form = forms.ClusterFilterForm
-    template_name = 'virtualization/cluster_list.html'
 
 
 class ClusterView(PermissionRequiredMixin, View):

+ 1 - 0
requirements.txt

@@ -4,6 +4,7 @@ django-cors-headers==3.2.1
 django-debug-toolbar==2.1
 django-filter==2.2.0
 django-mptt==0.9.1
+django-pglocks==1.0.4
 django-prometheus==1.1.0
 django-rq==2.2.0
 django-tables2==2.2.1