Explorar o código

merge develop

John Anderson %!s(int64=6) %!d(string=hai) anos
pai
achega
9116d74cf7
Modificáronse 80 ficheiros con 353 adicións e 1137 borrados
  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
 # https://github.com/django-mptt/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
 # Prometheus metrics library for Django
 # https://github.com/korfuri/django-prometheus
 # https://github.com/korfuri/django-prometheus
 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
     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
     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
     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.
     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
     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 
     for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via 
     `SENTINELS`/`SENTINEL_SERVICE`.
     `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)
 # 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.
 **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 = filters.ProviderFilterSet
     filterset_form = forms.ProviderFilterForm
     filterset_form = forms.ProviderFilterForm
     table = tables.ProviderDetailTable
     table = tables.ProviderDetailTable
-    template_name = 'circuits/provider_list.html'
 
 
 
 
 class ProviderView(PermissionRequiredMixin, View):
 class ProviderView(PermissionRequiredMixin, View):
@@ -107,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'circuits.view_circuittype'
     permission_required = 'circuits.view_circuittype'
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
     queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
-    template_name = 'circuits/circuittype_list.html'
 
 
 
 
 class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
 class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -151,7 +149,6 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.CircuitFilterSet
     filterset = filters.CircuitFilterSet
     filterset_form = forms.CircuitFilterForm
     filterset_form = forms.CircuitFilterForm
     table = tables.CircuitTable
     table = tables.CircuitTable
-    template_name = 'circuits/circuit_list.html'
 
 
 
 
 class CircuitView(PermissionRequiredMixin, View):
 class CircuitView(PermissionRequiredMixin, View):

+ 43 - 15
netbox/dcim/forms.py

@@ -2832,7 +2832,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -2842,7 +2845,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tags = TagField(
     tags = TagField(
@@ -2871,18 +2877,20 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
-        # Limit LAG choices to interfaces belonging to this device (or VC master)
         if self.is_bound:
         if self.is_bound:
             device = Device.objects.get(pk=self.data['device'])
             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:
         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):
 class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
@@ -2942,7 +2950,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -2951,7 +2962,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             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
             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):
 class InterfaceCSVForm(forms.ModelForm):
     device = FlexibleModelChoiceField(
     device = FlexibleModelChoiceField(
@@ -3090,7 +3108,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -3099,7 +3120,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             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()],
                 device__in=[device, device.get_vc_master()],
                 type=InterfaceTypeChoices.TYPE_LAG
                 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:
         else:
             self.fields['lag'].choices = ()
             self.fields['lag'].choices = ()
             self.fields['lag'].widget.attrs['disabled'] = True
             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):
 def _update_model_names(model):
     # Update each unique field value in bulk
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
     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):
 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):
 def _update_model_names(model):
     # Update each unique field value in bulk
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
     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):
 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):
 def _update_model_names(model):
     # Update each unique field value in bulk
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
     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):
 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):
 def _update_model_names(model):
     # Update each unique field value in bulk
     # Update each unique field value in bulk
     for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
     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):
 def naturalize_interfacetemplates(apps, schema_editor):

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

@@ -382,8 +382,8 @@ class RackElevationHelperMixin:
 
 
         # add gradients
         # add gradients
         RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
         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
         return drawing
 
 

+ 12 - 23
netbox/dcim/views.py

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

+ 0 - 20
netbox/extras/apps.py

@@ -1,28 +1,8 @@
 from django.apps import AppConfig
 from django.apps import AppConfig
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-import redis
 
 
 
 
 class ExtrasConfig(AppConfig):
 class ExtrasConfig(AppConfig):
     name = "extras"
     name = "extras"
 
 
     def ready(self):
     def ready(self):
-
         import extras.signals
         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
                 # Find all unique values for the field
                 queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
                 queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
                 for value in queryset:
                 for value in queryset:
-                    naturalized_value = naturalize(value)
+                    naturalized_value = naturalize(value, max_length=field.max_length)
 
 
                     if options['verbosity'] >= 2:
                     if options['verbosity'] >= 2:
                         self.stdout.write("  {} -> {}".format(value, naturalized_value), ending='')
                         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 = filters.TagFilterSet
     filterset_form = forms.TagFilterForm
     filterset_form = forms.TagFilterForm
     table = TagTable
     table = TagTable
-    template_name = 'extras/tag_list.html'
+    action_buttons = ()
 
 
 
 
 class TagView(PermissionRequiredMixin, View):
 class TagView(PermissionRequiredMixin, View):
@@ -111,7 +111,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ConfigContextFilterSet
     filterset = filters.ConfigContextFilterSet
     filterset_form = forms.ConfigContextFilterForm
     filterset_form = forms.ConfigContextFilterForm
     table = ConfigContextTable
     table = ConfigContextTable
-    template_name = 'extras/configcontext_list.html'
+    action_buttons = ('add',)
 
 
 
 
 class ConfigContextView(PermissionRequiredMixin, View):
 class ConfigContextView(PermissionRequiredMixin, View):
@@ -191,6 +191,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
     filterset_form = forms.ObjectChangeFilterForm
     filterset_form = forms.ObjectChangeFilterForm
     table = ObjectChangeTable
     table = ObjectChangeTable
     template_name = 'extras/objectchange_list.html'
     template_name = 'extras/objectchange_list.html'
+    action_buttons = ('export',)
 
 
 
 
 class ObjectChangeView(PermissionRequiredMixin, View):
 class ObjectChangeView(PermissionRequiredMixin, View):

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

@@ -1,6 +1,7 @@
 from django.conf import settings
 from django.conf import settings
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
+from django_pglocks import advisory_lock
 from rest_framework import status
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
@@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
 from ipam import filters
 from ipam import filters
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.api import FieldChoicesViewSet, ModelViewSet
 from utilities.api import FieldChoicesViewSet, ModelViewSet
+from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.utils import get_subquery
 from utilities.utils import get_subquery
 from . import serializers
 from . import serializers
 
 
@@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
     filterset_class = filters.PrefixFilterSet
     filterset_class = filters.PrefixFilterSet
 
 
     @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
     @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
     def available_prefixes(self, request, pk=None):
     def available_prefixes(self, request, pk=None):
         """
         """
         A convenience method for returning available child prefixes within a parent.
         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)
         prefix = get_object_or_404(Prefix, pk=pk)
         available_prefixes = prefix.get_available_prefixes()
         available_prefixes = prefix.get_available_prefixes()
@@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
             return Response(serializer.data)
             return Response(serializer.data)
 
 
     @action(detail=True, url_path='available-ips', methods=['get', 'post'])
     @action(detail=True, url_path='available-ips', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
     def available_ips(self, request, pk=None):
     def available_ips(self, request, pk=None):
         """
         """
         A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
         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,
         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.
         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)
         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 = filters.VRFFilterSet
     filterset_form = forms.VRFFilterForm
     filterset_form = forms.VRFFilterForm
     table = tables.VRFTable
     table = tables.VRFTable
-    template_name = 'ipam/vrf_list.html'
 
 
 
 
 class VRFView(PermissionRequiredMixin, View):
 class VRFView(PermissionRequiredMixin, View):
@@ -293,7 +292,6 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
     queryset = Aggregate.objects.prefetch_related('rir').annotate(
     queryset = Aggregate.objects.prefetch_related('rir').annotate(
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
     )
     )
-
     filterset = filters.AggregateFilterSet
     filterset = filters.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateDetailTable
     table = tables.AggregateDetailTable
@@ -411,7 +409,6 @@ class RoleListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'ipam.view_role'
     permission_required = 'ipam.view_role'
     queryset = Role.objects.all()
     queryset = Role.objects.all()
     table = tables.RoleTable
     table = tables.RoleTable
-    template_name = 'ipam/role_list.html'
 
 
 
 
 class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
 class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -644,7 +641,6 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.IPAddressFilterSet
     filterset = filters.IPAddressFilterSet
     filterset_form = forms.IPAddressFilterForm
     filterset_form = forms.IPAddressFilterForm
     table = tables.IPAddressDetailTable
     table = tables.IPAddressDetailTable
-    template_name = 'ipam/ipaddress_list.html'
 
 
 
 
 class IPAddressView(PermissionRequiredMixin, View):
 class IPAddressView(PermissionRequiredMixin, View):
@@ -817,7 +813,6 @@ class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.VLANGroupFilterSet
     filterset = filters.VLANGroupFilterSet
     filterset_form = forms.VLANGroupFilterForm
     filterset_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
-    template_name = 'ipam/vlangroup_list.html'
 
 
 
 
 class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
 class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -893,7 +888,6 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.VLANFilterSet
     filterset = filters.VLANFilterSet
     filterset_form = forms.VLANFilterForm
     filterset_form = forms.VLANFilterForm
     table = tables.VLANDetailTable
     table = tables.VLANDetailTable
-    template_name = 'ipam/vlan_list.html'
 
 
 
 
 class VLANView(PermissionRequiredMixin, View):
 class VLANView(PermissionRequiredMixin, View):
@@ -989,7 +983,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.ServiceFilterSet
     filterset = filters.ServiceFilterSet
     filterset_form = forms.ServiceFilterForm
     filterset_form = forms.ServiceFilterForm
     table = tables.ServiceTable
     table = tables.ServiceTable
-    template_name = 'ipam/service_list.html'
+    action_buttons = ('export',)
 
 
 
 
 class ServiceView(PermissionRequiredMixin, View):
 class ServiceView(PermissionRequiredMixin, View):

+ 1 - 1
netbox/netbox/settings.py

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

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

@@ -190,15 +190,18 @@ $(document).ready(function() {
                 $.each(element.attributes, function(index, attr){
                 $.each(element.attributes, function(index, attr){
                     if (attr.name.includes("data-additional-query-param-")){
                     if (attr.name.includes("data-additional-query-param-")){
                         var param_name = attr.name.split("data-additional-query-param-")[1];
                         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 {
                             } 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'
     permission_required = 'secrets.view_secretrole'
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
     queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
-    template_name = 'secrets/secretrole_list.html'
 
 
 
 
 class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
 class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -73,7 +72,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.SecretFilterSet
     filterset = filters.SecretFilterSet
     filterset_form = forms.SecretFilterForm
     filterset_form = forms.SecretFilterForm
     table = tables.SecretTable
     table = tables.SecretTable
-    template_name = 'secrets/secret_list.html'
+    action_buttons = ('import', 'export')
 
 
 
 
 class SecretView(PermissionRequiredMixin, View):
 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 %}
     {% 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 %}
 {% 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>
-</div>
 {% endblock %}
 {% 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">
                     <ul class="dropdown-menu">
                         <li class="dropdown-header">Logging</li>
                         <li class="dropdown-header">Logging</li>
                         <li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
                         <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>
                         <li class="divider"></li>
                         <li class="divider"></li>
                         <li class="dropdown-header">Miscellaneous</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 %}
 {% 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>
-</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 %}
 {% 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 %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="pull-right noprint">
+{% block buttons %}
     <div class="btn-group" role="group">
     <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=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>
         <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>
     </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 %}
 {% 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' %}
     {% if request.GET.family == '6' %}
         <a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
         <a href="{% url 'ipam:rir_list' %}" class="btn btn-default">
             <span class="fa fa-table" aria-hidden="true"></span>
             <span class="fa fa-table" aria-hidden="true"></span>
@@ -15,22 +12,12 @@
             IPv6 Stats
             IPv6 Stats
         </a>
         </a>
     {% endif %}
     {% 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 %}
     {% 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 %}
 {% 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 %}
     {% 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 %}
 {% endblock %}

+ 0 - 2
netbox/tenancy/views.py

@@ -22,7 +22,6 @@ class TenantGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'tenancy.view_tenantgroup'
     permission_required = 'tenancy.view_tenantgroup'
     queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
     queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
     table = tables.TenantGroupTable
     table = tables.TenantGroupTable
-    template_name = 'tenancy/tenantgroup_list.html'
 
 
 
 
 class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
 class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -60,7 +59,6 @@ class TenantListView(PermissionRequiredMixin, ObjectListView):
     filterset = filters.TenantFilterSet
     filterset = filters.TenantFilterSet
     filterset_form = forms.TenantFilterForm
     filterset_form = forms.TenantFilterForm
     table = tables.TenantTable
     table = tables.TenantTable
-    template_name = 'tenancy/tenant_list.html'
 
 
 
 
 class TenantView(PermissionRequiredMixin, View):
 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',
     gte='greater than or equal',
     n='negated'
     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):
     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 name: The name of the query param
         :param value: The value 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):
     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+)$)?'
                        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
     Take an alphanumeric string and prepend all integers to `integer_places` places to ensure the strings
     are ordered naturally. For example:
     are ordered naturally. For example:
@@ -39,10 +39,10 @@ def naturalize(value, max_length=None, integer_places=8):
             output.append(segment)
             output.append(segment)
     ret = ''.join(output)
     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
     Similar in nature to naturalize(), but takes into account a particular naming format adapted from the old
     InterfaceManager.
     InterfaceManager.
@@ -77,4 +77,4 @@ def naturalize_interface(value, max_length=None):
             output.append('000000')
             output.append('000000')
 
 
     ret = ''.join(output)
     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 datetime
 import json
 import json
 import re
 import re
-import yaml
 
 
+import yaml
 from django import template
 from django import template
+from django.urls import NoReverseMatch, reverse
 from django.utils.html import strip_tags
 from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from markdown import markdown
 from markdown import markdown
@@ -11,7 +12,6 @@ from markdown import markdown
 from utilities.choices import unpack_grouped_choices
 from utilities.choices import unpack_grouped_choices
 from utilities.utils import foreground_color
 from utilities.utils import foreground_color
 
 
-
 register = template.Library()
 register = template.Library()
 
 
 
 
@@ -101,6 +101,21 @@ def model_name_plural(obj):
     return obj._meta.verbose_name_plural
     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()
 @register.filter()
 def contains(value, arg):
 def contains(value, arg):
     """
     """

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

@@ -21,7 +21,10 @@ class NaturalizationTestCase(TestCase):
         )
         )
 
 
         for origin, naturalized in data:
         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):
     def test_naturalize_interface(self):
 
 
@@ -40,4 +43,7 @@ class NaturalizationTestCase(TestCase):
         )
         )
 
 
         for origin, naturalized in data:
         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 = None
     filterset_form = None
     filterset_form = None
     table = None
     table = None
-    template_name = None
+    template_name = 'utilities/obj_list.html'
+    action_buttons = ('add', 'import', 'export')
 
 
     def queryset_to_yaml(self):
     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
         # Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
         self.queryset = self.alter_queryset(request)
         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
         # Construct the table based on the user's permissions
         table = self.table(self.queryset)
         table = self.table(self.queryset)
@@ -176,6 +179,7 @@ class ObjectListView(View):
             'content_type': content_type,
             'content_type': content_type,
             'table': table,
             'table': table,
             'permissions': permissions,
             'permissions': permissions,
+            'action_buttons': self.action_buttons,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
         }
         }
         context.update(self.extra_context())
         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]
             post_data['pk'] = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
 
 
         if '_apply' in request.POST:
         if '_apply' in request.POST:
-            form = self.form(model, request.POST, initial=request.GET)
+            form = self.form(model, request.POST)
             if form.is_valid():
             if form.is_valid():
 
 
                 custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
                 custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
@@ -714,10 +718,6 @@ class BulkEditView(GetReturnURLMixin, View):
         else:
         else:
             # Pass the PK list as initial data to avoid binding the form
             # Pass the PK list as initial data to avoid binding the form
             initial_data = querydict_to_dict(post_data)
             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)
             form = self.form(model, initial=initial_data)
 
 
         # Retrieve objects being edited
         # Retrieve objects being edited

+ 37 - 89
netbox/virtualization/forms.py

@@ -351,6 +351,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     )
     role = DynamicModelChoiceField(
     role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.all(),
         queryset=DeviceRole.objects.all(),
+        required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/dcim/device-roles/",
             api_url="/api/dcim/device-roles/",
             additional_query_params={
             additional_query_params={
@@ -658,7 +659,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -667,7 +671,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tags = TagField(
     tags = TagField(
@@ -695,35 +702,12 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*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)
         site = getattr(self.instance.parent, 'site', None)
         if site is not 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):
     def clean(self):
         super().clean()
         super().clean()
@@ -784,7 +768,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -793,7 +780,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tags = TagField(
     tags = TagField(
@@ -807,35 +797,11 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
             pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
             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)
         site = getattr(virtual_machine.cluster, 'site', None)
         if site is not 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):
 class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -872,7 +838,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=APISelect(
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
         )
     )
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
     tagged_vlans = DynamicModelMultipleChoiceField(
@@ -881,7 +850,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=APISelectMultiple(
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             api_url="/api/ipam/vlans/",
             display_field='display_name',
             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:
         if 'virtual_machine' in self.initial:
             parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
             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'
     permission_required = 'virtualization.view_clustertype'
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
     queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
-    template_name = 'virtualization/clustertype_list.html'
 
 
 
 
 class ClusterTypeCreateView(PermissionRequiredMixin, ObjectEditView):
 class ClusterTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -62,7 +61,6 @@ class ClusterGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'virtualization.view_clustergroup'
     permission_required = 'virtualization.view_clustergroup'
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
     queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
-    template_name = 'virtualization/clustergroup_list.html'
 
 
 
 
 class ClusterGroupCreateView(PermissionRequiredMixin, ObjectEditView):
 class ClusterGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@@ -100,7 +98,6 @@ class ClusterListView(PermissionRequiredMixin, ObjectListView):
     table = tables.ClusterTable
     table = tables.ClusterTable
     filterset = filters.ClusterFilterSet
     filterset = filters.ClusterFilterSet
     filterset_form = forms.ClusterFilterForm
     filterset_form = forms.ClusterFilterForm
-    template_name = 'virtualization/cluster_list.html'
 
 
 
 
 class ClusterView(PermissionRequiredMixin, View):
 class ClusterView(PermissionRequiredMixin, View):

+ 1 - 0
requirements.txt

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