2
0
Эх сурвалжийг харах

Merge pull request #4861 from netbox-community/django-31

Upgrade to Django 3.1 (v2.9)
Jeremy Stretch 5 жил өмнө
parent
commit
de6202c160

+ 2 - 2
netbox/circuits/api/views.py

@@ -19,7 +19,7 @@ from . import serializers
 class ProviderViewSet(CustomFieldModelViewSet):
 class ProviderViewSet(CustomFieldModelViewSet):
     queryset = Provider.objects.prefetch_related('tags').annotate(
     queryset = Provider.objects.prefetch_related('tags').annotate(
         circuit_count=Count('circuits')
         circuit_count=Count('circuits')
-    )
+    ).order_by(*Provider._meta.ordering)
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
     filterset_class = filters.ProviderFilterSet
     filterset_class = filters.ProviderFilterSet
 
 
@@ -41,7 +41,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
 class CircuitTypeViewSet(ModelViewSet):
 class CircuitTypeViewSet(ModelViewSet):
     queryset = CircuitType.objects.annotate(
     queryset = CircuitType.objects.annotate(
         circuit_count=Count('circuits')
         circuit_count=Count('circuits')
-    )
+    ).order_by(*CircuitType._meta.ordering)
     serializer_class = serializers.CircuitTypeSerializer
     serializer_class = serializers.CircuitTypeSerializer
     filterset_class = filters.CircuitTypeFilterSet
     filterset_class = filters.CircuitTypeFilterSet
 
 

+ 18 - 0
netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.1b1 on 2020-07-16 15:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('circuits', '0018_standardize_description'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='circuittermination',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+    ]

+ 3 - 2
netbox/circuits/models.py

@@ -275,9 +275,10 @@ class CircuitTermination(CableTermination):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     port_speed = models.PositiveIntegerField(
     port_speed = models.PositiveIntegerField(
         verbose_name='Port speed (Kbps)'
         verbose_name='Port speed (Kbps)'

+ 5 - 5
netbox/circuits/views.py

@@ -21,7 +21,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 #
 #
 
 
 class ProviderListView(ObjectListView):
 class ProviderListView(ObjectListView):
-    queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
+    queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     filterset_form = forms.ProviderFilterForm
     filterset_form = forms.ProviderFilterForm
     table = tables.ProviderTable
     table = tables.ProviderTable
@@ -73,14 +73,14 @@ class ProviderBulkImportView(BulkImportView):
 
 
 
 
 class ProviderBulkEditView(BulkEditView):
 class ProviderBulkEditView(BulkEditView):
-    queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
+    queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
     form = forms.ProviderBulkEditForm
 
 
 
 
 class ProviderBulkDeleteView(BulkDeleteView):
 class ProviderBulkDeleteView(BulkDeleteView):
-    queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
+    queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
 
 
@@ -90,7 +90,7 @@ class ProviderBulkDeleteView(BulkDeleteView):
 #
 #
 
 
 class CircuitTypeListView(ObjectListView):
 class CircuitTypeListView(ObjectListView):
-    queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
+    queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 
@@ -110,7 +110,7 @@ class CircuitTypeBulkImportView(BulkImportView):
 
 
 
 
 class CircuitTypeBulkDeleteView(BulkDeleteView):
 class CircuitTypeBulkDeleteView(BulkDeleteView):
-    queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
+    queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 

+ 22 - 14
netbox/dcim/api/views.py

@@ -74,8 +74,12 @@ class CableTraceMixin(object):
 #
 #
 
 
 class RegionViewSet(ModelViewSet):
 class RegionViewSet(ModelViewSet):
-    queryset = Region.objects.annotate(
-        site_count=Count('sites')
+    queryset = Region.objects.add_related_count(
+        Region.objects.all(),
+        Site,
+        'region',
+        'site_count',
+        cumulative=True
     )
     )
     serializer_class = serializers.RegionSerializer
     serializer_class = serializers.RegionSerializer
     filterset_class = filters.RegionFilterSet
     filterset_class = filters.RegionFilterSet
@@ -95,7 +99,7 @@ class SiteViewSet(CustomFieldModelViewSet):
         vlan_count=get_subquery(VLAN, 'site'),
         vlan_count=get_subquery(VLAN, 'site'),
         circuit_count=get_subquery(Circuit, 'terminations__site'),
         circuit_count=get_subquery(Circuit, 'terminations__site'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
-    )
+    ).order_by(*Site._meta.ordering)
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
     filterset_class = filters.SiteFilterSet
     filterset_class = filters.SiteFilterSet
 
 
@@ -115,9 +119,13 @@ class SiteViewSet(CustomFieldModelViewSet):
 #
 #
 
 
 class RackGroupViewSet(ModelViewSet):
 class RackGroupViewSet(ModelViewSet):
-    queryset = RackGroup.objects.prefetch_related('site').annotate(
-        rack_count=Count('racks')
-    )
+    queryset = RackGroup.objects.add_related_count(
+        RackGroup.objects.all(),
+        Rack,
+        'group',
+        'rack_count',
+        cumulative=True
+    ).prefetch_related('site')
     serializer_class = serializers.RackGroupSerializer
     serializer_class = serializers.RackGroupSerializer
     filterset_class = filters.RackGroupFilterSet
     filterset_class = filters.RackGroupFilterSet
 
 
@@ -129,7 +137,7 @@ class RackGroupViewSet(ModelViewSet):
 class RackRoleViewSet(ModelViewSet):
 class RackRoleViewSet(ModelViewSet):
     queryset = RackRole.objects.annotate(
     queryset = RackRole.objects.annotate(
         rack_count=Count('racks')
         rack_count=Count('racks')
-    )
+    ).order_by(*RackRole._meta.ordering)
     serializer_class = serializers.RackRoleSerializer
     serializer_class = serializers.RackRoleSerializer
     filterset_class = filters.RackRoleFilterSet
     filterset_class = filters.RackRoleFilterSet
 
 
@@ -144,7 +152,7 @@ class RackViewSet(CustomFieldModelViewSet):
     ).annotate(
     ).annotate(
         device_count=get_subquery(Device, 'rack'),
         device_count=get_subquery(Device, 'rack'),
         powerfeed_count=get_subquery(PowerFeed, 'rack')
         powerfeed_count=get_subquery(PowerFeed, 'rack')
-    )
+    ).order_by(*Rack._meta.ordering)
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
     filterset_class = filters.RackFilterSet
     filterset_class = filters.RackFilterSet
 
 
@@ -217,7 +225,7 @@ class ManufacturerViewSet(ModelViewSet):
         devicetype_count=get_subquery(DeviceType, 'manufacturer'),
         devicetype_count=get_subquery(DeviceType, 'manufacturer'),
         inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
         inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
         platform_count=get_subquery(Platform, 'manufacturer')
         platform_count=get_subquery(Platform, 'manufacturer')
-    )
+    ).order_by(*Manufacturer._meta.ordering)
     serializer_class = serializers.ManufacturerSerializer
     serializer_class = serializers.ManufacturerSerializer
     filterset_class = filters.ManufacturerFilterSet
     filterset_class = filters.ManufacturerFilterSet
 
 
@@ -229,7 +237,7 @@ class ManufacturerViewSet(ModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
         device_count=Count('instances')
         device_count=Count('instances')
-    )
+    ).order_by(*DeviceType._meta.ordering)
     serializer_class = serializers.DeviceTypeSerializer
     serializer_class = serializers.DeviceTypeSerializer
     filterset_class = filters.DeviceTypeFilterSet
     filterset_class = filters.DeviceTypeFilterSet
 
 
@@ -294,7 +302,7 @@ class DeviceRoleViewSet(ModelViewSet):
     queryset = DeviceRole.objects.annotate(
     queryset = DeviceRole.objects.annotate(
         device_count=get_subquery(Device, 'device_role'),
         device_count=get_subquery(Device, 'device_role'),
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
-    )
+    ).order_by(*DeviceRole._meta.ordering)
     serializer_class = serializers.DeviceRoleSerializer
     serializer_class = serializers.DeviceRoleSerializer
     filterset_class = filters.DeviceRoleFilterSet
     filterset_class = filters.DeviceRoleFilterSet
 
 
@@ -307,7 +315,7 @@ class PlatformViewSet(ModelViewSet):
     queryset = Platform.objects.annotate(
     queryset = Platform.objects.annotate(
         device_count=get_subquery(Device, 'platform'),
         device_count=get_subquery(Device, 'platform'),
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
-    )
+    ).order_by(*Platform._meta.ordering)
     serializer_class = serializers.PlatformSerializer
     serializer_class = serializers.PlatformSerializer
     filterset_class = filters.PlatformFilterSet
     filterset_class = filters.PlatformFilterSet
 
 
@@ -583,7 +591,7 @@ class CableViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
         member_count=Count('members')
         member_count=Count('members')
-    )
+    ).order_by(*VirtualChassis._meta.ordering)
     serializer_class = serializers.VirtualChassisSerializer
     serializer_class = serializers.VirtualChassisSerializer
     filterset_class = filters.VirtualChassisFilterSet
     filterset_class = filters.VirtualChassisFilterSet
 
 
@@ -597,7 +605,7 @@ class PowerPanelViewSet(ModelViewSet):
         'site', 'rack_group'
         'site', 'rack_group'
     ).annotate(
     ).annotate(
         powerfeed_count=Count('powerfeeds')
         powerfeed_count=Count('powerfeeds')
-    )
+    ).order_by(*PowerPanel._meta.ordering)
     serializer_class = serializers.PowerPanelSerializer
     serializer_class = serializers.PowerPanelSerializer
     filterset_class = filters.PowerPanelFilterSet
     filterset_class = filters.PowerPanelFilterSet
 
 

+ 43 - 0
netbox/dcim/migrations/0113_nullbooleanfield_to_booleanfield.py

@@ -0,0 +1,43 @@
+# Generated by Django 3.1b1 on 2020-07-16 15:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0112_standardize_components'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='consoleport',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='consoleserverport',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='interface',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='powerfeed',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='poweroutlet',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='powerport',
+            name='connection_status',
+            field=models.BooleanField(blank=True, null=True),
+        ),
+    ]

+ 23 - 0
netbox/dcim/migrations/0114_update_jsonfield.py

@@ -0,0 +1,23 @@
+# Generated by Django 3.1b1 on 2020-07-16 16:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0113_nullbooleanfield_to_booleanfield'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='device',
+            name='local_context_data',
+            field=models.JSONField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='platform',
+            name='napalm_args',
+            field=models.JSONField(blank=True, null=True),
+        ),
+    ]

+ 5 - 4
netbox/dcim/models/__init__.py

@@ -6,7 +6,7 @@ from django.conf import settings
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import ArrayField, JSONField
+from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
@@ -1280,7 +1280,7 @@ class Platform(ChangeLoggedModel):
         verbose_name='NAPALM driver',
         verbose_name='NAPALM driver',
         help_text='The name of the NAPALM driver to use when interacting with devices'
         help_text='The name of the NAPALM driver to use when interacting with devices'
     )
     )
-    napalm_args = JSONField(
+    napalm_args = models.JSONField(
         blank=True,
         blank=True,
         null=True,
         null=True,
         verbose_name='NAPALM arguments',
         verbose_name='NAPALM arguments',
@@ -1905,9 +1905,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     name = models.CharField(
     name = models.CharField(
         max_length=50
         max_length=50

+ 15 - 10
netbox/dcim/models/device_components.py

@@ -264,9 +264,10 @@ class ConsolePort(CableTermination, ComponentModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
@@ -304,9 +305,10 @@ class ConsoleServerPort(CableTermination, ComponentModel):
         blank=True,
         blank=True,
         help_text='Physical port type'
         help_text='Physical port type'
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
@@ -370,9 +372,10 @@ class PowerPort(CableTermination, ComponentModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
@@ -505,9 +508,10 @@ class PowerOutlet(CableTermination, ComponentModel):
         blank=True,
         blank=True,
         help_text="Phase (for three-phase feeds)"
         help_text="Phase (for three-phase feeds)"
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     tags = TaggableManager(through=TaggedItem)
     tags = TaggableManager(through=TaggedItem)
 
 
@@ -598,9 +602,10 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     )
     lag = models.ForeignKey(
     lag = models.ForeignKey(
         to='self',
         to='self',

+ 35 - 13
netbox/dcim/views.py

@@ -133,7 +133,13 @@ class RegionBulkImportView(BulkImportView):
 
 
 
 
 class RegionBulkDeleteView(BulkDeleteView):
 class RegionBulkDeleteView(BulkDeleteView):
-    queryset = Region.objects.all()
+    queryset = Region.objects.add_related_count(
+        Region.objects.all(),
+        Site,
+        'region',
+        'site_count',
+        cumulative=True
+    )
     filterset = filters.RegionFilterSet
     filterset = filters.RegionFilterSet
     table = tables.RegionTable
     table = tables.RegionTable
 
 
@@ -238,7 +244,13 @@ class RackGroupBulkImportView(BulkImportView):
 
 
 
 
 class RackGroupBulkDeleteView(BulkDeleteView):
 class RackGroupBulkDeleteView(BulkDeleteView):
-    queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
+    queryset = RackGroup.objects.add_related_count(
+        RackGroup.objects.all(),
+        Rack,
+        'group',
+        'rack_count',
+        cumulative=True
+    ).prefetch_related('site')
     filterset = filters.RackGroupFilterSet
     filterset = filters.RackGroupFilterSet
     table = tables.RackGroupTable
     table = tables.RackGroupTable
 
 
@@ -248,7 +260,7 @@ class RackGroupBulkDeleteView(BulkDeleteView):
 #
 #
 
 
 class RackRoleListView(ObjectListView):
 class RackRoleListView(ObjectListView):
-    queryset = RackRole.objects.annotate(rack_count=Count('racks'))
+    queryset = RackRole.objects.annotate(rack_count=Count('racks')).order_by(*RackRole._meta.ordering)
     table = tables.RackRoleTable
     table = tables.RackRoleTable
 
 
 
 
@@ -268,7 +280,7 @@ class RackRoleBulkImportView(BulkImportView):
 
 
 
 
 class RackRoleBulkDeleteView(BulkDeleteView):
 class RackRoleBulkDeleteView(BulkDeleteView):
-    queryset = RackRole.objects.annotate(rack_count=Count('racks'))
+    queryset = RackRole.objects.annotate(rack_count=Count('racks')).order_by(*RackRole._meta.ordering)
     table = tables.RackRoleTable
     table = tables.RackRoleTable
 
 
 
 
@@ -281,7 +293,7 @@ class RackListView(ObjectListView):
         'site', 'group', 'tenant', 'role', 'devices__device_type'
         'site', 'group', 'tenant', 'role', 'devices__device_type'
     ).annotate(
     ).annotate(
         device_count=Count('devices')
         device_count=Count('devices')
-    )
+    ).order_by(*Rack._meta.ordering)
     filterset = filters.RackFilterSet
     filterset = filters.RackFilterSet
     filterset_form = forms.RackFilterForm
     filterset_form = forms.RackFilterForm
     table = tables.RackDetailTable
     table = tables.RackDetailTable
@@ -465,7 +477,7 @@ class ManufacturerListView(ObjectListView):
         devicetype_count=Count('device_types', distinct=True),
         devicetype_count=Count('device_types', distinct=True),
         inventoryitem_count=Count('inventory_items', distinct=True),
         inventoryitem_count=Count('inventory_items', distinct=True),
         platform_count=Count('platforms', distinct=True),
         platform_count=Count('platforms', distinct=True),
-    )
+    ).order_by(*Manufacturer._meta.ordering)
     table = tables.ManufacturerTable
     table = tables.ManufacturerTable
 
 
 
 
@@ -485,7 +497,9 @@ class ManufacturerBulkImportView(BulkImportView):
 
 
 
 
 class ManufacturerBulkDeleteView(BulkDeleteView):
 class ManufacturerBulkDeleteView(BulkDeleteView):
-    queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
+    queryset = Manufacturer.objects.annotate(
+        devicetype_count=Count('device_types')
+    ).order_by(*Manufacturer._meta.ordering)
     table = tables.ManufacturerTable
     table = tables.ManufacturerTable
 
 
 
 
@@ -494,7 +508,9 @@ class ManufacturerBulkDeleteView(BulkDeleteView):
 #
 #
 
 
 class DeviceTypeListView(ObjectListView):
 class DeviceTypeListView(ObjectListView):
-    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
+        instance_count=Count('instances')
+    ).order_by(*DeviceType._meta.ordering)
     filterset = filters.DeviceTypeFilterSet
     filterset = filters.DeviceTypeFilterSet
     filterset_form = forms.DeviceTypeFilterForm
     filterset_form = forms.DeviceTypeFilterForm
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
@@ -602,14 +618,18 @@ class DeviceTypeImportView(ObjectImportView):
 
 
 
 
 class DeviceTypeBulkEditView(BulkEditView):
 class DeviceTypeBulkEditView(BulkEditView):
-    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
+        instance_count=Count('instances')
+    ).order_by(*DeviceType._meta.ordering)
     filterset = filters.DeviceTypeFilterSet
     filterset = filters.DeviceTypeFilterSet
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
     form = forms.DeviceTypeBulkEditForm
     form = forms.DeviceTypeBulkEditForm
 
 
 
 
 class DeviceTypeBulkDeleteView(BulkDeleteView):
 class DeviceTypeBulkDeleteView(BulkDeleteView):
-    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
+        instance_count=Count('instances')
+    ).order_by(*DeviceType._meta.ordering)
     filterset = filters.DeviceTypeFilterSet
     filterset = filters.DeviceTypeFilterSet
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
 
 
@@ -2152,7 +2172,9 @@ class InterfaceConnectionsListView(ObjectListView):
 #
 #
 
 
 class VirtualChassisListView(ObjectListView):
 class VirtualChassisListView(ObjectListView):
-    queryset = VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members'))
+    queryset = VirtualChassis.objects.prefetch_related('master').annotate(
+        member_count=Count('members')
+    ).order_by(*VirtualChassis._meta.ordering)
     table = tables.VirtualChassisTable
     table = tables.VirtualChassisTable
     filterset = filters.VirtualChassisFilterSet
     filterset = filters.VirtualChassisFilterSet
     filterset_form = forms.VirtualChassisFilterForm
     filterset_form = forms.VirtualChassisFilterForm
@@ -2385,7 +2407,7 @@ class PowerPanelListView(ObjectListView):
         'site', 'rack_group'
         'site', 'rack_group'
     ).annotate(
     ).annotate(
         powerfeed_count=Count('powerfeeds')
         powerfeed_count=Count('powerfeeds')
-    )
+    ).order_by(*PowerPanel._meta.ordering)
     filterset = filters.PowerPanelFilterSet
     filterset = filters.PowerPanelFilterSet
     filterset_form = forms.PowerPanelFilterForm
     filterset_form = forms.PowerPanelFilterForm
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
@@ -2437,7 +2459,7 @@ class PowerPanelBulkDeleteView(BulkDeleteView):
         'site', 'rack_group'
         'site', 'rack_group'
     ).annotate(
     ).annotate(
         rack_count=Count('powerfeeds')
         rack_count=Count('powerfeeds')
-    )
+    ).order_by(*PowerPanel._meta.ordering)
     filterset = filters.PowerPanelFilterSet
     filterset = filters.PowerPanelFilterSet
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
 
 

+ 28 - 0
netbox/extras/migrations/0046_update_jsonfield.py

@@ -0,0 +1,28 @@
+# Generated by Django 3.1b1 on 2020-07-16 16:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0045_configcontext_changelog'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='configcontext',
+            name='data',
+            field=models.JSONField(),
+        ),
+        migrations.AlterField(
+            model_name='jobresult',
+            name='data',
+            field=models.JSONField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='objectchange',
+            name='object_data',
+            field=models.JSONField(editable=False),
+        ),
+    ]

+ 1 - 2
netbox/extras/models/change_logging.py

@@ -1,7 +1,6 @@
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
@@ -104,7 +103,7 @@ class ObjectChange(models.Model):
         max_length=200,
         max_length=200,
         editable=False
         editable=False
     )
     )
-    object_data = JSONField(
+    object_data = models.JSONField(
         editable=False
         editable=False
     )
     )
 
 

+ 3 - 4
netbox/extras/models/models.py

@@ -5,7 +5,6 @@ from collections import OrderedDict
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import JSONField
 from django.core.validators import ValidationError
 from django.core.validators import ValidationError
 from django.db import models
 from django.db import models
 from django.http import HttpResponse
 from django.http import HttpResponse
@@ -499,7 +498,7 @@ class ConfigContext(ChangeLoggedModel):
         related_name='+',
         related_name='+',
         blank=True
         blank=True
     )
     )
-    data = JSONField()
+    data = models.JSONField()
 
 
     objects = ConfigContextQuerySet.as_manager()
     objects = ConfigContextQuerySet.as_manager()
 
 
@@ -526,7 +525,7 @@ class ConfigContextModel(models.Model):
     A model which includes local configuration context data. This local data will override any inherited data from
     A model which includes local configuration context data. This local data will override any inherited data from
     ConfigContexts.
     ConfigContexts.
     """
     """
-    local_context_data = JSONField(
+    local_context_data = models.JSONField(
         blank=True,
         blank=True,
         null=True,
         null=True,
     )
     )
@@ -627,7 +626,7 @@ class JobResult(models.Model):
         choices=JobResultStatusChoices,
         choices=JobResultStatusChoices,
         default=JobResultStatusChoices.STATUS_PENDING
         default=JobResultStatusChoices.STATUS_PENDING
     )
     )
-    data = JSONField(
+    data = models.JSONField(
         null=True,
         null=True,
         blank=True
         blank=True
     )
     )

+ 1 - 1
netbox/extras/scripts.py

@@ -11,7 +11,7 @@ from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.core.validators import RegexValidator
 from django.core.validators import RegexValidator
 from django.db import transaction
 from django.db import transaction
-from django.utils.decorators import classproperty
+from django.utils.functional import classproperty
 from django_rq import job
 from django_rq import job
 
 
 from extras.api.serializers import ScriptOutputSerializer
 from extras.api.serializers import ScriptOutputSerializer

+ 5 - 5
netbox/ipam/api/views.py

@@ -24,7 +24,7 @@ class VRFViewSet(CustomFieldModelViewSet):
     queryset = VRF.objects.prefetch_related('tenant').prefetch_related('tags').annotate(
     queryset = VRF.objects.prefetch_related('tenant').prefetch_related('tags').annotate(
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         prefix_count=get_subquery(Prefix, 'vrf')
         prefix_count=get_subquery(Prefix, 'vrf')
-    )
+    ).order_by(*VRF._meta.ordering)
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
     filterset_class = filters.VRFFilterSet
     filterset_class = filters.VRFFilterSet
 
 
@@ -36,7 +36,7 @@ class VRFViewSet(CustomFieldModelViewSet):
 class RIRViewSet(ModelViewSet):
 class RIRViewSet(ModelViewSet):
     queryset = RIR.objects.annotate(
     queryset = RIR.objects.annotate(
         aggregate_count=Count('aggregates')
         aggregate_count=Count('aggregates')
-    )
+    ).order_by(*RIR._meta.ordering)
     serializer_class = serializers.RIRSerializer
     serializer_class = serializers.RIRSerializer
     filterset_class = filters.RIRFilterSet
     filterset_class = filters.RIRFilterSet
 
 
@@ -59,7 +59,7 @@ class RoleViewSet(ModelViewSet):
     queryset = Role.objects.annotate(
     queryset = Role.objects.annotate(
         prefix_count=get_subquery(Prefix, 'role'),
         prefix_count=get_subquery(Prefix, 'role'),
         vlan_count=get_subquery(VLAN, 'role')
         vlan_count=get_subquery(VLAN, 'role')
-    )
+    ).order_by(*Role._meta.ordering)
     serializer_class = serializers.RoleSerializer
     serializer_class = serializers.RoleSerializer
     filterset_class = filters.RoleFilterSet
     filterset_class = filters.RoleFilterSet
 
 
@@ -246,7 +246,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
         vlan_count=Count('vlans')
         vlan_count=Count('vlans')
-    )
+    ).order_by(*VLANGroup._meta.ordering)
     serializer_class = serializers.VLANGroupSerializer
     serializer_class = serializers.VLANGroupSerializer
     filterset_class = filters.VLANGroupFilterSet
     filterset_class = filters.VLANGroupFilterSet
 
 
@@ -260,7 +260,7 @@ class VLANViewSet(CustomFieldModelViewSet):
         'site', 'group', 'tenant', 'role', 'tags'
         'site', 'group', 'tenant', 'role', 'tags'
     ).annotate(
     ).annotate(
         prefix_count=get_subquery(Prefix, 'vlan')
         prefix_count=get_subquery(Prefix, 'vlan')
-    )
+    ).order_by(*VLAN._meta.ordering)
     serializer_class = serializers.VLANSerializer
     serializer_class = serializers.VLANSerializer
     filterset_class = filters.VLANFilterSet
     filterset_class = filters.VLANFilterSet
 
 

+ 9 - 5
netbox/ipam/views.py

@@ -78,7 +78,7 @@ class VRFBulkDeleteView(BulkDeleteView):
 #
 #
 
 
 class RIRListView(ObjectListView):
 class RIRListView(ObjectListView):
-    queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
+    queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
     filterset = filters.RIRFilterSet
     filterset = filters.RIRFilterSet
     filterset_form = forms.RIRFilterForm
     filterset_form = forms.RIRFilterForm
     table = tables.RIRDetailTable
     table = tables.RIRDetailTable
@@ -171,7 +171,7 @@ class RIRBulkImportView(BulkImportView):
 
 
 
 
 class RIRBulkDeleteView(BulkDeleteView):
 class RIRBulkDeleteView(BulkDeleteView):
-    queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
+    queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
     filterset = filters.RIRFilterSet
     filterset = filters.RIRFilterSet
     table = tables.RIRTable
     table = tables.RIRTable
 
 
@@ -183,7 +183,7 @@ class RIRBulkDeleteView(BulkDeleteView):
 class AggregateListView(ObjectListView):
 class AggregateListView(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', ())
-    )
+    ).order_by(*Aggregate._meta.ordering)
     filterset = filters.AggregateFilterSet
     filterset = filters.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateDetailTable
     table = tables.AggregateDetailTable
@@ -650,7 +650,9 @@ class IPAddressBulkDeleteView(BulkDeleteView):
 #
 #
 
 
 class VLANGroupListView(ObjectListView):
 class VLANGroupListView(ObjectListView):
-    queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
+    queryset = VLANGroup.objects.prefetch_related('site').annotate(
+        vlan_count=Count('vlans')
+    ).order_by(*VLANGroup._meta.ordering)
     filterset = filters.VLANGroupFilterSet
     filterset = filters.VLANGroupFilterSet
     filterset_form = forms.VLANGroupFilterForm
     filterset_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
@@ -672,7 +674,9 @@ class VLANGroupBulkImportView(BulkImportView):
 
 
 
 
 class VLANGroupBulkDeleteView(BulkDeleteView):
 class VLANGroupBulkDeleteView(BulkDeleteView):
-    queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
+    queryset = VLANGroup.objects.prefetch_related('site').annotate(
+        vlan_count=Count('vlans')
+    ).order_by(*VLANGroup._meta.ordering)
     filterset = filters.VLANGroupFilterSet
     filterset = filters.VLANGroupFilterSet
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 

+ 12 - 4
netbox/netbox/views.py

@@ -46,7 +46,9 @@ SEARCH_MAX_RESULTS = 15
 SEARCH_TYPES = OrderedDict((
 SEARCH_TYPES = OrderedDict((
     # Circuits
     # Circuits
     ('provider', {
     ('provider', {
-        'queryset': Provider.objects.annotate(count_circuits=Count('circuits')),
+        'queryset': Provider.objects.annotate(
+            count_circuits=Count('circuits')
+        ).order_by(*Provider._meta.ordering),
         'filterset': ProviderFilterSet,
         'filterset': ProviderFilterSet,
         'table': ProviderTable,
         'table': ProviderTable,
         'url': 'circuits:provider_list',
         'url': 'circuits:provider_list',
@@ -73,13 +75,17 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:rack_list',
         'url': 'dcim:rack_list',
     }),
     }),
     ('rackgroup', {
     ('rackgroup', {
-        'queryset': RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')),
+        'queryset': RackGroup.objects.prefetch_related('site').annotate(
+            rack_count=Count('racks')
+        ).order_by(*RackGroup._meta.ordering),
         'filterset': RackGroupFilterSet,
         'filterset': RackGroupFilterSet,
         'table': RackGroupTable,
         'table': RackGroupTable,
         'url': 'dcim:rackgroup_list',
         'url': 'dcim:rackgroup_list',
     }),
     }),
     ('devicetype', {
     ('devicetype', {
-        'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')),
+        'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
+            instance_count=Count('instances')
+        ).order_by(*DeviceType._meta.ordering),
         'filterset': DeviceTypeFilterSet,
         'filterset': DeviceTypeFilterSet,
         'table': DeviceTypeTable,
         'table': DeviceTypeTable,
         'url': 'dcim:devicetype_list',
         'url': 'dcim:devicetype_list',
@@ -93,7 +99,9 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:device_list',
         'url': 'dcim:device_list',
     }),
     }),
     ('virtualchassis', {
     ('virtualchassis', {
-        'queryset': VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members')),
+        'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
+            member_count=Count('members')
+        ).order_by(*VirtualChassis._meta.ordering),
         'filterset': VirtualChassisFilterSet,
         'filterset': VirtualChassisFilterSet,
         'table': VirtualChassisTable,
         'table': VirtualChassisTable,
         'url': 'dcim:virtualchassis_list',
         'url': 'dcim:virtualchassis_list',

+ 1 - 1
netbox/secrets/api/views.py

@@ -27,7 +27,7 @@ ERR_PRIVKEY_INVALID = "Invalid private key."
 class SecretRoleViewSet(ModelViewSet):
 class SecretRoleViewSet(ModelViewSet):
     queryset = SecretRole.objects.annotate(
     queryset = SecretRole.objects.annotate(
         secret_count=Count('secrets')
         secret_count=Count('secrets')
-    )
+    ).order_by(*SecretRole._meta.ordering)
     serializer_class = serializers.SecretRoleSerializer
     serializer_class = serializers.SecretRoleSerializer
     filterset_class = filters.SecretRoleFilterSet
     filterset_class = filters.SecretRoleFilterSet
 
 

+ 2 - 2
netbox/secrets/views.py

@@ -29,7 +29,7 @@ def get_session_key(request):
 #
 #
 
 
 class SecretRoleListView(ObjectListView):
 class SecretRoleListView(ObjectListView):
-    queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
+    queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 
@@ -49,7 +49,7 @@ class SecretRoleBulkImportView(BulkImportView):
 
 
 
 
 class SecretRoleBulkDeleteView(BulkDeleteView):
 class SecretRoleBulkDeleteView(BulkDeleteView):
-    queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
+    queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 

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

@@ -9,7 +9,7 @@
                 {% endif %}
                 {% endif %}
                 {% for p in page.smart_pages %}
                 {% for p in page.smart_pages %}
                     {% if p %}
                     {% if p %}
-                        <li{% ifequal page.number p %} class="active"{% endifequal %}><a href="{% querystring request page=p %}">{{ p }}</a></li>
+                        <li{% if page.number == p %} class="active"{% endif %}><a href="{% querystring request page=p %}">{{ p }}</a></li>
                     {% else %}
                     {% else %}
                         <li class="disabled"><span>&hellip;</span></li>
                         <li class="disabled"><span>&hellip;</span></li>
                     {% endif %}
                     {% endif %}

+ 5 - 5
netbox/templates/users/base.html

@@ -9,21 +9,21 @@
 <div class="row">
 <div class="row">
     <div class="col-sm-3 col-md-2 col-md-offset-1">
     <div class="col-sm-3 col-md-2 col-md-offset-1">
         <ul class="nav nav-pills nav-stacked">
         <ul class="nav nav-pills nav-stacked">
-            <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
+            <li{% if active_tab == "profile" %} class="active"{% endif %}>
                 <a href="{% url 'user:profile' %}">Profile</a>
                 <a href="{% url 'user:profile' %}">Profile</a>
             </li>
             </li>
-            <li{% ifequal active_tab "preferences" %} class="active"{% endifequal %}>
+            <li{% if active_tab == "preferences" %} class="active"{% endif %}>
                 <a href="{% url 'user:preferences' %}">Preferences</a>
                 <a href="{% url 'user:preferences' %}">Preferences</a>
             </li>
             </li>
             {% if not request.user.ldap_username %}
             {% if not request.user.ldap_username %}
-                <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}>
+                <li{% if active_tab == "change_password" %} class="active"{% endif %}>
                     <a href="{% url 'user:change_password' %}">Change Password</a>
                     <a href="{% url 'user:change_password' %}">Change Password</a>
                 </li>
                 </li>
             {% endif %}
             {% endif %}
-            <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}>
+            <li{% if active_tab == "api_tokens" %} class="active"{% endif %}>
                 <a href="{% url 'user:token_list' %}">API Tokens</a>
                 <a href="{% url 'user:token_list' %}">API Tokens</a>
             </li>
             </li>
-            <li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}>
+            <li{% if active_tab == "userkey" %} class="active"{% endif %}>
                 <a href="{% url 'user:userkey' %}">User Key</a>
                 <a href="{% url 'user:userkey' %}">User Key</a>
             </li>
             </li>
         </ul>
         </ul>

+ 6 - 2
netbox/tenancy/api/views.py

@@ -15,8 +15,12 @@ from . import serializers
 #
 #
 
 
 class TenantGroupViewSet(ModelViewSet):
 class TenantGroupViewSet(ModelViewSet):
-    queryset = TenantGroup.objects.annotate(
-        tenant_count=get_subquery(Tenant, 'group')
+    queryset = TenantGroup.objects.add_related_count(
+        TenantGroup.objects.all(),
+        Tenant,
+        'group',
+        'tenant_count',
+        cumulative=True
     )
     )
     serializer_class = serializers.TenantGroupSerializer
     serializer_class = serializers.TenantGroupSerializer
     filterset_class = filters.TenantGroupFilterSet
     filterset_class = filters.TenantGroupFilterSet

+ 7 - 1
netbox/tenancy/views.py

@@ -43,7 +43,13 @@ class TenantGroupBulkImportView(BulkImportView):
 
 
 
 
 class TenantGroupBulkDeleteView(BulkDeleteView):
 class TenantGroupBulkDeleteView(BulkDeleteView):
-    queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
+    queryset = TenantGroup.objects.add_related_count(
+        TenantGroup.objects.all(),
+        Tenant,
+        'group',
+        'tenant_count',
+        cumulative=True
+    )
     table = tables.TenantGroupTable
     table = tables.TenantGroupTable
 
 
 
 

+ 23 - 0
netbox/users/migrations/0010_update_jsonfield.py

@@ -0,0 +1,23 @@
+# Generated by Django 3.1b1 on 2020-07-16 16:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0009_replicate_permissions'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='objectpermission',
+            name='constraints',
+            field=models.JSONField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='userconfig',
+            name='data',
+            field=models.JSONField(default=dict),
+        ),
+    ]

+ 3 - 3
netbox/users/models.py

@@ -3,7 +3,7 @@ import os
 
 
 from django.contrib.auth.models import Group, User
 from django.contrib.auth.models import Group, User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import ArrayField, JSONField
+from django.contrib.postgres.fields import ArrayField
 from django.core.validators import MinLengthValidator
 from django.core.validators import MinLengthValidator
 from django.db import models
 from django.db import models
 from django.db.models.signals import post_save
 from django.db.models.signals import post_save
@@ -56,7 +56,7 @@ class UserConfig(models.Model):
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
         related_name='config'
         related_name='config'
     )
     )
-    data = JSONField(
+    data = models.JSONField(
         default=dict
         default=dict
     )
     )
 
 
@@ -265,7 +265,7 @@ class ObjectPermission(models.Model):
         base_field=models.CharField(max_length=30),
         base_field=models.CharField(max_length=30),
         help_text="The list of actions granted by this permission"
         help_text="The list of actions granted by this permission"
     )
     )
-    constraints = JSONField(
+    constraints = models.JSONField(
         blank=True,
         blank=True,
         null=True,
         null=True,
         help_text="Queryset filter matching the applicable objects of the selected type(s)"
         help_text="Queryset filter matching the applicable objects of the selected type(s)"

+ 3 - 4
netbox/utilities/api.py

@@ -345,10 +345,9 @@ class ModelViewSet(_ModelViewSet):
         try:
         try:
             return super().dispatch(request, *args, **kwargs)
             return super().dispatch(request, *args, **kwargs)
         except ProtectedError as e:
         except ProtectedError as e:
-            models = [
-                '{} ({})'.format(o, o._meta) for o in e.protected_objects.all()
-            ]
-            msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
+            protected_objects = list(e.protected_objects)
+            msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
+            msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
             logger.warning(msg)
             logger.warning(msg)
             return self.finalize_response(
             return self.finalize_response(
                 request,
                 request,

+ 6 - 20
netbox/utilities/error_handlers.py

@@ -7,31 +7,17 @@ def handle_protectederror(obj, request, e):
     """
     """
     Generate a user-friendly error message in response to a ProtectedError exception.
     Generate a user-friendly error message in response to a ProtectedError exception.
     """
     """
-    try:
-        dep_class = e.protected_objects[0]._meta.verbose_name_plural
-    except IndexError:
-        raise e
-
-    # Grammar for single versus multiple triggering objects
-    if type(obj) in (list, tuple):
-        err_message = "Unable to delete the requested {}. The following dependent {} were found: ".format(
-            obj[0]._meta.verbose_name_plural,
-            dep_class,
-        )
-    else:
-        err_message = "Unable to delete {} {}. The following dependent {} were found: ".format(
-            obj._meta.verbose_name,
-            obj,
-            dep_class,
-        )
+    protected_objects = list(e.protected_objects)
+    err_message = f"Unable to delete {obj._meta.verbose_name} <strong>{obj}</strong>. " \
+                  f"{len(protected_objects)} dependent objects were found: "
 
 
     # Append dependent objects to error message
     # Append dependent objects to error message
     dependent_objects = []
     dependent_objects = []
-    for obj in e.protected_objects:
+    for dependent in protected_objects:
         if hasattr(obj, 'get_absolute_url'):
         if hasattr(obj, 'get_absolute_url'):
-            dependent_objects.append('<a href="{}">{}</a>'.format(obj.get_absolute_url(), escape(obj)))
+            dependent_objects.append(f'<a href="{dependent.get_absolute_url()}">{escape(dependent)}</a>')
         else:
         else:
-            dependent_objects.append(str(obj))
+            dependent_objects.append(str(dependent))
     err_message += ', '.join(dependent_objects)
     err_message += ', '.join(dependent_objects)
 
 
     messages.error(request, mark_safe(err_message))
     messages.error(request, mark_safe(err_message))

+ 1 - 1
netbox/utilities/forms.py

@@ -8,7 +8,7 @@ import yaml
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.contrib.postgres.forms import SimpleArrayField
 from django.contrib.postgres.forms import SimpleArrayField
-from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
+from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
 from django.core.exceptions import MultipleObjectsReturned
 from django.core.exceptions import MultipleObjectsReturned
 from django.db.models import Count
 from django.db.models import Count
 from django.forms import BoundField
 from django.forms import BoundField

+ 3 - 3
netbox/virtualization/api/views.py

@@ -22,7 +22,7 @@ from . import serializers
 class ClusterTypeViewSet(ModelViewSet):
 class ClusterTypeViewSet(ModelViewSet):
     queryset = ClusterType.objects.annotate(
     queryset = ClusterType.objects.annotate(
         cluster_count=Count('clusters')
         cluster_count=Count('clusters')
-    )
+    ).order_by(*ClusterType._meta.ordering)
     serializer_class = serializers.ClusterTypeSerializer
     serializer_class = serializers.ClusterTypeSerializer
     filterset_class = filters.ClusterTypeFilterSet
     filterset_class = filters.ClusterTypeFilterSet
 
 
@@ -30,7 +30,7 @@ class ClusterTypeViewSet(ModelViewSet):
 class ClusterGroupViewSet(ModelViewSet):
 class ClusterGroupViewSet(ModelViewSet):
     queryset = ClusterGroup.objects.annotate(
     queryset = ClusterGroup.objects.annotate(
         cluster_count=Count('clusters')
         cluster_count=Count('clusters')
-    )
+    ).order_by(*ClusterGroup._meta.ordering)
     serializer_class = serializers.ClusterGroupSerializer
     serializer_class = serializers.ClusterGroupSerializer
     filterset_class = filters.ClusterGroupFilterSet
     filterset_class = filters.ClusterGroupFilterSet
 
 
@@ -41,7 +41,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
     ).annotate(
     ).annotate(
         device_count=get_subquery(Device, 'cluster'),
         device_count=get_subquery(Device, 'cluster'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
-    )
+    ).order_by(*Cluster._meta.ordering)
     serializer_class = serializers.ClusterSerializer
     serializer_class = serializers.ClusterSerializer
     filterset_class = filters.ClusterFilterSet
     filterset_class = filters.ClusterFilterSet
 
 

+ 18 - 0
netbox/virtualization/migrations/0017_update_jsonfield.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.1b1 on 2020-07-16 16:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('virtualization', '0016_replicate_interfaces'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='virtualmachine',
+            name='local_context_data',
+            field=models.JSONField(blank=True, null=True),
+        ),
+    ]

+ 4 - 4
netbox/virtualization/views.py

@@ -22,7 +22,7 @@ from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterf
 #
 #
 
 
 class ClusterTypeListView(ObjectListView):
 class ClusterTypeListView(ObjectListView):
-    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
+    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
@@ -42,7 +42,7 @@ class ClusterTypeBulkImportView(BulkImportView):
 
 
 
 
 class ClusterTypeBulkDeleteView(BulkDeleteView):
 class ClusterTypeBulkDeleteView(BulkDeleteView):
-    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
+    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
@@ -51,7 +51,7 @@ class ClusterTypeBulkDeleteView(BulkDeleteView):
 #
 #
 
 
 class ClusterGroupListView(ObjectListView):
 class ClusterGroupListView(ObjectListView):
-    queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
+    queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 
@@ -71,7 +71,7 @@ class ClusterGroupBulkImportView(BulkImportView):
 
 
 
 
 class ClusterGroupBulkDeleteView(BulkDeleteView):
 class ClusterGroupBulkDeleteView(BulkDeleteView):
-    queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters'))
+    queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 

+ 1 - 1
requirements.txt

@@ -1,4 +1,4 @@
-Django>=3.0,<3.1
+Django==3.1rc1
 django-cacheops==5.0.1
 django-cacheops==5.0.1
 django-cors-headers==3.4.0
 django-cors-headers==3.4.0
 django-debug-toolbar==2.2
 django-debug-toolbar==2.2