Răsfoiți Sursa

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

Upgrade to Django 3.1 (v2.9)
Jeremy Stretch 5 ani în urmă
părinte
comite
de6202c160

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

@@ -19,7 +19,7 @@ from . import serializers
 class ProviderViewSet(CustomFieldModelViewSet):
     queryset = Provider.objects.prefetch_related('tags').annotate(
         circuit_count=Count('circuits')
-    )
+    ).order_by(*Provider._meta.ordering)
     serializer_class = serializers.ProviderSerializer
     filterset_class = filters.ProviderFilterSet
 
@@ -41,7 +41,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
 class CircuitTypeViewSet(ModelViewSet):
     queryset = CircuitType.objects.annotate(
         circuit_count=Count('circuits')
-    )
+    ).order_by(*CircuitType._meta.ordering)
     serializer_class = serializers.CircuitTypeSerializer
     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,
         null=True
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     port_speed = models.PositiveIntegerField(
         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):
-    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_form = forms.ProviderFilterForm
     table = tables.ProviderTable
@@ -73,14 +73,14 @@ class ProviderBulkImportView(BulkImportView):
 
 
 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
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
 
 
 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
     table = tables.ProviderTable
 
@@ -90,7 +90,7 @@ class ProviderBulkDeleteView(BulkDeleteView):
 #
 
 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
 
 
@@ -110,7 +110,7 @@ class CircuitTypeBulkImportView(BulkImportView):
 
 
 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
 
 

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

@@ -74,8 +74,12 @@ class CableTraceMixin(object):
 #
 
 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
     filterset_class = filters.RegionFilterSet
@@ -95,7 +99,7 @@ class SiteViewSet(CustomFieldModelViewSet):
         vlan_count=get_subquery(VLAN, 'site'),
         circuit_count=get_subquery(Circuit, 'terminations__site'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
-    )
+    ).order_by(*Site._meta.ordering)
     serializer_class = serializers.SiteSerializer
     filterset_class = filters.SiteFilterSet
 
@@ -115,9 +119,13 @@ class SiteViewSet(CustomFieldModelViewSet):
 #
 
 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
     filterset_class = filters.RackGroupFilterSet
 
@@ -129,7 +137,7 @@ class RackGroupViewSet(ModelViewSet):
 class RackRoleViewSet(ModelViewSet):
     queryset = RackRole.objects.annotate(
         rack_count=Count('racks')
-    )
+    ).order_by(*RackRole._meta.ordering)
     serializer_class = serializers.RackRoleSerializer
     filterset_class = filters.RackRoleFilterSet
 
@@ -144,7 +152,7 @@ class RackViewSet(CustomFieldModelViewSet):
     ).annotate(
         device_count=get_subquery(Device, 'rack'),
         powerfeed_count=get_subquery(PowerFeed, 'rack')
-    )
+    ).order_by(*Rack._meta.ordering)
     serializer_class = serializers.RackSerializer
     filterset_class = filters.RackFilterSet
 
@@ -217,7 +225,7 @@ class ManufacturerViewSet(ModelViewSet):
         devicetype_count=get_subquery(DeviceType, 'manufacturer'),
         inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
         platform_count=get_subquery(Platform, 'manufacturer')
-    )
+    ).order_by(*Manufacturer._meta.ordering)
     serializer_class = serializers.ManufacturerSerializer
     filterset_class = filters.ManufacturerFilterSet
 
@@ -229,7 +237,7 @@ class ManufacturerViewSet(ModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
         device_count=Count('instances')
-    )
+    ).order_by(*DeviceType._meta.ordering)
     serializer_class = serializers.DeviceTypeSerializer
     filterset_class = filters.DeviceTypeFilterSet
 
@@ -294,7 +302,7 @@ class DeviceRoleViewSet(ModelViewSet):
     queryset = DeviceRole.objects.annotate(
         device_count=get_subquery(Device, 'device_role'),
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
-    )
+    ).order_by(*DeviceRole._meta.ordering)
     serializer_class = serializers.DeviceRoleSerializer
     filterset_class = filters.DeviceRoleFilterSet
 
@@ -307,7 +315,7 @@ class PlatformViewSet(ModelViewSet):
     queryset = Platform.objects.annotate(
         device_count=get_subquery(Device, 'platform'),
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
-    )
+    ).order_by(*Platform._meta.ordering)
     serializer_class = serializers.PlatformSerializer
     filterset_class = filters.PlatformFilterSet
 
@@ -583,7 +591,7 @@ class CableViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
         member_count=Count('members')
-    )
+    ).order_by(*VirtualChassis._meta.ordering)
     serializer_class = serializers.VirtualChassisSerializer
     filterset_class = filters.VirtualChassisFilterSet
 
@@ -597,7 +605,7 @@ class PowerPanelViewSet(ModelViewSet):
         'site', 'rack_group'
     ).annotate(
         powerfeed_count=Count('powerfeeds')
-    )
+    ).order_by(*PowerPanel._meta.ordering)
     serializer_class = serializers.PowerPanelSerializer
     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.contenttypes.fields import GenericForeignKey, GenericRelation
 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.validators import MaxValueValidator, MinValueValidator
 from django.db import models
@@ -1280,7 +1280,7 @@ class Platform(ChangeLoggedModel):
         verbose_name='NAPALM driver',
         help_text='The name of the NAPALM driver to use when interacting with devices'
     )
-    napalm_args = JSONField(
+    napalm_args = models.JSONField(
         blank=True,
         null=True,
         verbose_name='NAPALM arguments',
@@ -1905,9 +1905,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
         blank=True,
         null=True
     )
-    connection_status = models.NullBooleanField(
+    connection_status = models.BooleanField(
         choices=CONNECTION_STATUS_CHOICES,
-        blank=True
+        blank=True,
+        null=True
     )
     name = models.CharField(
         max_length=50

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

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

+ 35 - 13
netbox/dcim/views.py

@@ -133,7 +133,13 @@ class RegionBulkImportView(BulkImportView):
 
 
 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
     table = tables.RegionTable
 
@@ -238,7 +244,13 @@ class RackGroupBulkImportView(BulkImportView):
 
 
 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
     table = tables.RackGroupTable
 
@@ -248,7 +260,7 @@ class RackGroupBulkDeleteView(BulkDeleteView):
 #
 
 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
 
 
@@ -268,7 +280,7 @@ class RackRoleBulkImportView(BulkImportView):
 
 
 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
 
 
@@ -281,7 +293,7 @@ class RackListView(ObjectListView):
         'site', 'group', 'tenant', 'role', 'devices__device_type'
     ).annotate(
         device_count=Count('devices')
-    )
+    ).order_by(*Rack._meta.ordering)
     filterset = filters.RackFilterSet
     filterset_form = forms.RackFilterForm
     table = tables.RackDetailTable
@@ -465,7 +477,7 @@ class ManufacturerListView(ObjectListView):
         devicetype_count=Count('device_types', distinct=True),
         inventoryitem_count=Count('inventory_items', distinct=True),
         platform_count=Count('platforms', distinct=True),
-    )
+    ).order_by(*Manufacturer._meta.ordering)
     table = tables.ManufacturerTable
 
 
@@ -485,7 +497,9 @@ class ManufacturerBulkImportView(BulkImportView):
 
 
 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
 
 
@@ -494,7 +508,9 @@ class ManufacturerBulkDeleteView(BulkDeleteView):
 #
 
 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_form = forms.DeviceTypeFilterForm
     table = tables.DeviceTypeTable
@@ -602,14 +618,18 @@ class DeviceTypeImportView(ObjectImportView):
 
 
 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
     table = tables.DeviceTypeTable
     form = forms.DeviceTypeBulkEditForm
 
 
 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
     table = tables.DeviceTypeTable
 
@@ -2152,7 +2172,9 @@ class InterfaceConnectionsListView(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
     filterset = filters.VirtualChassisFilterSet
     filterset_form = forms.VirtualChassisFilterForm
@@ -2385,7 +2407,7 @@ class PowerPanelListView(ObjectListView):
         'site', 'rack_group'
     ).annotate(
         powerfeed_count=Count('powerfeeds')
-    )
+    ).order_by(*PowerPanel._meta.ordering)
     filterset = filters.PowerPanelFilterSet
     filterset_form = forms.PowerPanelFilterForm
     table = tables.PowerPanelTable
@@ -2437,7 +2459,7 @@ class PowerPanelBulkDeleteView(BulkDeleteView):
         'site', 'rack_group'
     ).annotate(
         rack_count=Count('powerfeeds')
-    )
+    ).order_by(*PowerPanel._meta.ordering)
     filterset = filters.PowerPanelFilterSet
     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.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.urls import reverse
 
@@ -104,7 +103,7 @@ class ObjectChange(models.Model):
         max_length=200,
         editable=False
     )
-    object_data = JSONField(
+    object_data = models.JSONField(
         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.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import JSONField
 from django.core.validators import ValidationError
 from django.db import models
 from django.http import HttpResponse
@@ -499,7 +498,7 @@ class ConfigContext(ChangeLoggedModel):
         related_name='+',
         blank=True
     )
-    data = JSONField()
+    data = models.JSONField()
 
     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
     ConfigContexts.
     """
-    local_context_data = JSONField(
+    local_context_data = models.JSONField(
         blank=True,
         null=True,
     )
@@ -627,7 +626,7 @@ class JobResult(models.Model):
         choices=JobResultStatusChoices,
         default=JobResultStatusChoices.STATUS_PENDING
     )
-    data = JSONField(
+    data = models.JSONField(
         null=True,
         blank=True
     )

+ 1 - 1
netbox/extras/scripts.py

@@ -11,7 +11,7 @@ from django import forms
 from django.conf import settings
 from django.core.validators import RegexValidator
 from django.db import transaction
-from django.utils.decorators import classproperty
+from django.utils.functional import classproperty
 from django_rq import job
 
 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(
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         prefix_count=get_subquery(Prefix, 'vrf')
-    )
+    ).order_by(*VRF._meta.ordering)
     serializer_class = serializers.VRFSerializer
     filterset_class = filters.VRFFilterSet
 
@@ -36,7 +36,7 @@ class VRFViewSet(CustomFieldModelViewSet):
 class RIRViewSet(ModelViewSet):
     queryset = RIR.objects.annotate(
         aggregate_count=Count('aggregates')
-    )
+    ).order_by(*RIR._meta.ordering)
     serializer_class = serializers.RIRSerializer
     filterset_class = filters.RIRFilterSet
 
@@ -59,7 +59,7 @@ class RoleViewSet(ModelViewSet):
     queryset = Role.objects.annotate(
         prefix_count=get_subquery(Prefix, 'role'),
         vlan_count=get_subquery(VLAN, 'role')
-    )
+    ).order_by(*Role._meta.ordering)
     serializer_class = serializers.RoleSerializer
     filterset_class = filters.RoleFilterSet
 
@@ -246,7 +246,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
         vlan_count=Count('vlans')
-    )
+    ).order_by(*VLANGroup._meta.ordering)
     serializer_class = serializers.VLANGroupSerializer
     filterset_class = filters.VLANGroupFilterSet
 
@@ -260,7 +260,7 @@ class VLANViewSet(CustomFieldModelViewSet):
         'site', 'group', 'tenant', 'role', 'tags'
     ).annotate(
         prefix_count=get_subquery(Prefix, 'vlan')
-    )
+    ).order_by(*VLAN._meta.ordering)
     serializer_class = serializers.VLANSerializer
     filterset_class = filters.VLANFilterSet
 

+ 9 - 5
netbox/ipam/views.py

@@ -78,7 +78,7 @@ class VRFBulkDeleteView(BulkDeleteView):
 #
 
 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_form = forms.RIRFilterForm
     table = tables.RIRDetailTable
@@ -171,7 +171,7 @@ class RIRBulkImportView(BulkImportView):
 
 
 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
     table = tables.RIRTable
 
@@ -183,7 +183,7 @@ class RIRBulkDeleteView(BulkDeleteView):
 class AggregateListView(ObjectListView):
     queryset = Aggregate.objects.prefetch_related('rir').annotate(
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
-    )
+    ).order_by(*Aggregate._meta.ordering)
     filterset = filters.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateDetailTable
@@ -650,7 +650,9 @@ class IPAddressBulkDeleteView(BulkDeleteView):
 #
 
 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_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
@@ -672,7 +674,9 @@ class VLANGroupBulkImportView(BulkImportView):
 
 
 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
     table = tables.VLANGroupTable
 

+ 12 - 4
netbox/netbox/views.py

@@ -46,7 +46,9 @@ SEARCH_MAX_RESULTS = 15
 SEARCH_TYPES = OrderedDict((
     # Circuits
     ('provider', {
-        'queryset': Provider.objects.annotate(count_circuits=Count('circuits')),
+        'queryset': Provider.objects.annotate(
+            count_circuits=Count('circuits')
+        ).order_by(*Provider._meta.ordering),
         'filterset': ProviderFilterSet,
         'table': ProviderTable,
         'url': 'circuits:provider_list',
@@ -73,13 +75,17 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:rack_list',
     }),
     ('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,
         'table': RackGroupTable,
         'url': 'dcim:rackgroup_list',
     }),
     ('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,
         'table': DeviceTypeTable,
         'url': 'dcim:devicetype_list',
@@ -93,7 +99,9 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:device_list',
     }),
     ('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,
         'table': VirtualChassisTable,
         'url': 'dcim:virtualchassis_list',

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

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

+ 2 - 2
netbox/secrets/views.py

@@ -29,7 +29,7 @@ def get_session_key(request):
 #
 
 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
 
 
@@ -49,7 +49,7 @@ class SecretRoleBulkImportView(BulkImportView):
 
 
 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
 
 

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

@@ -9,7 +9,7 @@
                 {% endif %}
                 {% for p in page.smart_pages %}
                     {% 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 %}
                         <li class="disabled"><span>&hellip;</span></li>
                     {% endif %}

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

@@ -9,21 +9,21 @@
 <div class="row">
     <div class="col-sm-3 col-md-2 col-md-offset-1">
         <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>
             </li>
-            <li{% ifequal active_tab "preferences" %} class="active"{% endifequal %}>
+            <li{% if active_tab == "preferences" %} class="active"{% endif %}>
                 <a href="{% url 'user:preferences' %}">Preferences</a>
             </li>
             {% 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>
                 </li>
             {% 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>
             </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>
             </li>
         </ul>

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

@@ -15,8 +15,12 @@ from . import serializers
 #
 
 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
     filterset_class = filters.TenantGroupFilterSet

+ 7 - 1
netbox/tenancy/views.py

@@ -43,7 +43,13 @@ class TenantGroupBulkImportView(BulkImportView):
 
 
 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
 
 

+ 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.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.db import models
 from django.db.models.signals import post_save
@@ -56,7 +56,7 @@ class UserConfig(models.Model):
         on_delete=models.CASCADE,
         related_name='config'
     )
-    data = JSONField(
+    data = models.JSONField(
         default=dict
     )
 
@@ -265,7 +265,7 @@ class ObjectPermission(models.Model):
         base_field=models.CharField(max_length=30),
         help_text="The list of actions granted by this permission"
     )
-    constraints = JSONField(
+    constraints = models.JSONField(
         blank=True,
         null=True,
         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:
             return super().dispatch(request, *args, **kwargs)
         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)
             return self.finalize_response(
                 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.
     """
-    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
     dependent_objects = []
-    for obj in e.protected_objects:
+    for dependent in protected_objects:
         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:
-            dependent_objects.append(str(obj))
+            dependent_objects.append(str(dependent))
     err_message += ', '.join(dependent_objects)
 
     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.conf import settings
 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.db.models import Count
 from django.forms import BoundField

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

@@ -22,7 +22,7 @@ from . import serializers
 class ClusterTypeViewSet(ModelViewSet):
     queryset = ClusterType.objects.annotate(
         cluster_count=Count('clusters')
-    )
+    ).order_by(*ClusterType._meta.ordering)
     serializer_class = serializers.ClusterTypeSerializer
     filterset_class = filters.ClusterTypeFilterSet
 
@@ -30,7 +30,7 @@ class ClusterTypeViewSet(ModelViewSet):
 class ClusterGroupViewSet(ModelViewSet):
     queryset = ClusterGroup.objects.annotate(
         cluster_count=Count('clusters')
-    )
+    ).order_by(*ClusterGroup._meta.ordering)
     serializer_class = serializers.ClusterGroupSerializer
     filterset_class = filters.ClusterGroupFilterSet
 
@@ -41,7 +41,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
     ).annotate(
         device_count=get_subquery(Device, 'cluster'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
-    )
+    ).order_by(*Cluster._meta.ordering)
     serializer_class = serializers.ClusterSerializer
     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):
-    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters'))
+    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
     table = tables.ClusterTypeTable
 
 
@@ -42,7 +42,7 @@ class ClusterTypeBulkImportView(BulkImportView):
 
 
 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
 
 
@@ -51,7 +51,7 @@ class ClusterTypeBulkDeleteView(BulkDeleteView):
 #
 
 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
 
 
@@ -71,7 +71,7 @@ class ClusterGroupBulkImportView(BulkImportView):
 
 
 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
 
 

+ 1 - 1
requirements.txt

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