From 8dd41b771ee89f17c0be40eeb05451079657c07f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Jul 2020 11:54:08 -0400 Subject: [PATCH 1/8] Update import locations for Django 3.1 --- netbox/extras/scripts.py | 2 +- netbox/utilities/forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index dd096c392..9c8ef7b09 100644 --- a/netbox/extras/scripts.py +++ b/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 diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 5c4b58213..0e1768387 100644 --- a/netbox/utilities/forms.py +++ b/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 From 68ecddccdb148176fb25347a1af6abd78fe0513b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Jul 2020 11:56:35 -0400 Subject: [PATCH 2/8] Convert NullBooleanField to BooleanField(null=True) --- .../0019_nullbooleanfield_to_booleanfield.py | 18 ++++++++ netbox/circuits/models.py | 5 ++- .../0113_nullbooleanfield_to_booleanfield.py | 43 +++++++++++++++++++ netbox/dcim/models/__init__.py | 5 ++- netbox/dcim/models/device_components.py | 25 ++++++----- 5 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py create mode 100644 netbox/dcim/migrations/0113_nullbooleanfield_to_booleanfield.py diff --git a/netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py b/netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py new file mode 100644 index 000000000..c8e844284 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 63491ca20..9ec90d110 100644 --- a/netbox/circuits/models.py +++ b/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)' diff --git a/netbox/dcim/migrations/0113_nullbooleanfield_to_booleanfield.py b/netbox/dcim/migrations/0113_nullbooleanfield_to_booleanfield.py new file mode 100644 index 000000000..b96e2dcd4 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 9b1019990..9f4b23603 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -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 diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 1ebb0c4d5..6764f8bcf 100644 --- a/netbox/dcim/models/device_components.py +++ b/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', From 21a750e8ec7bb02e7ab471e685ab931f8f8ec82e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Jul 2020 12:02:49 -0400 Subject: [PATCH 3/8] Change Postgres-specific JSONField to stock Django field --- .../dcim/migrations/0114_update_jsonfield.py | 23 +++++++++++++++ netbox/dcim/models/__init__.py | 4 +-- .../migrations/0046_update_jsonfield.py | 28 +++++++++++++++++++ netbox/extras/models/change_logging.py | 3 +- netbox/extras/models/models.py | 7 ++--- .../users/migrations/0010_update_jsonfield.py | 23 +++++++++++++++ netbox/users/models.py | 6 ++-- .../migrations/0017_update_jsonfield.py | 18 ++++++++++++ 8 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 netbox/dcim/migrations/0114_update_jsonfield.py create mode 100644 netbox/extras/migrations/0046_update_jsonfield.py create mode 100644 netbox/users/migrations/0010_update_jsonfield.py create mode 100644 netbox/virtualization/migrations/0017_update_jsonfield.py diff --git a/netbox/dcim/migrations/0114_update_jsonfield.py b/netbox/dcim/migrations/0114_update_jsonfield.py new file mode 100644 index 000000000..5a971bced --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 9f4b23603..5e3f57e0e 100644 --- a/netbox/dcim/models/__init__.py +++ b/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', diff --git a/netbox/extras/migrations/0046_update_jsonfield.py b/netbox/extras/migrations/0046_update_jsonfield.py new file mode 100644 index 000000000..a06302840 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index 3260c5302..bec8e2b75 100644 --- a/netbox/extras/models/change_logging.py +++ b/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 ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index edd13a123..fcfcfefb3 100644 --- a/netbox/extras/models/models.py +++ b/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 ) diff --git a/netbox/users/migrations/0010_update_jsonfield.py b/netbox/users/migrations/0010_update_jsonfield.py new file mode 100644 index 000000000..1935e58b7 --- /dev/null +++ b/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), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index f210cb4d0..9e890cfdf 100644 --- a/netbox/users/models.py +++ b/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)" diff --git a/netbox/virtualization/migrations/0017_update_jsonfield.py b/netbox/virtualization/migrations/0017_update_jsonfield.py new file mode 100644 index 000000000..2a23deef6 --- /dev/null +++ b/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), + ), + ] From 82cd24a7dec61a40057ffd8ba474bb60b2117031 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Jul 2020 13:01:31 -0400 Subject: [PATCH 4/8] Remove deprecated ifequal template tags --- netbox/templates/inc/paginator.html | 2 +- netbox/templates/users/base.html | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/templates/inc/paginator.html b/netbox/templates/inc/paginator.html index c0baef070..50d7e06d3 100644 --- a/netbox/templates/inc/paginator.html +++ b/netbox/templates/inc/paginator.html @@ -9,7 +9,7 @@ {% endif %} {% for p in page.smart_pages %} {% if p %} - {{ p }} + {{ p }} {% else %}
  • {% endif %} diff --git a/netbox/templates/users/base.html b/netbox/templates/users/base.html index 15d81ae0f..e9b4532e1 100644 --- a/netbox/templates/users/base.html +++ b/netbox/templates/users/base.html @@ -9,21 +9,21 @@
    From bdf41451eb42aa2b31a35305e99e8f584a39a249 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Jul 2020 13:04:19 -0400 Subject: [PATCH 5/8] Pin Django version to 3.1-beta1 for v2.9 beta --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51f857d2a..8f7cbefd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=3.0,<3.1 +Django==3.1b1 django-cacheops==5.0.1 django-cors-headers==3.4.0 django-debug-toolbar==2.2 From 1dbf776279abfc645e31bb8dea4a76dfab699ea3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Jul 2020 13:45:02 -0400 Subject: [PATCH 6/8] Fix handling of ProtectedError exceptions --- netbox/utilities/api.py | 7 +++---- netbox/utilities/error_handlers.py | 26 ++++++-------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 980de7672..83405a3c1 100644 --- a/netbox/utilities/api.py +++ b/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, diff --git a/netbox/utilities/error_handlers.py b/netbox/utilities/error_handlers.py index da8510950..7f912dcb1 100644 --- a/netbox/utilities/error_handlers.py +++ b/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} {obj}. " \ + 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('{}'.format(obj.get_absolute_url(), escape(obj))) + dependent_objects.append(f'{escape(dependent)}') else: - dependent_objects.append(str(obj)) + dependent_objects.append(str(dependent)) err_message += ', '.join(dependent_objects) messages.error(request, mark_safe(err_message)) From 5b43fa0e12e98cc9a401c21ef9c75c2c80a35697 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 20 Jul 2020 11:10:55 -0400 Subject: [PATCH 7/8] Upgrade Django to 3.1RC1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8f7cbefd3..d7f215d12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==3.1b1 +Django==3.1rc1 django-cacheops==5.0.1 django-cors-headers==3.4.0 django-debug-toolbar==2.2 From 0f679e1f0374ddc14e2aa3ce4c4689233472be37 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 20 Jul 2020 12:07:19 -0400 Subject: [PATCH 8/8] Closes #4871: Specify ordering for querysets using annotate() to count related objects --- netbox/circuits/api/views.py | 4 +-- netbox/circuits/views.py | 10 +++---- netbox/dcim/api/views.py | 36 +++++++++++++--------- netbox/dcim/views.py | 48 ++++++++++++++++++++++-------- netbox/ipam/api/views.py | 10 +++---- netbox/ipam/views.py | 14 +++++---- netbox/netbox/views.py | 16 +++++++--- netbox/secrets/api/views.py | 2 +- netbox/secrets/views.py | 4 +-- netbox/tenancy/api/views.py | 8 +++-- netbox/tenancy/views.py | 8 ++++- netbox/virtualization/api/views.py | 6 ++-- netbox/virtualization/views.py | 8 ++--- 13 files changed, 113 insertions(+), 61 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 1575a181b..79c4452d2 100644 --- a/netbox/circuits/api/views.py +++ b/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 diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 55ff70327..df90a3671 100644 --- a/netbox/circuits/views.py +++ b/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 diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index fdd58ef12..b5d68cebe 100644 --- a/netbox/dcim/api/views.py +++ b/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 diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 889b4d94e..2216bafd3 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 77ac36683..80b29a76e 100644 --- a/netbox/ipam/api/views.py +++ b/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 diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 8a9c647f0..2a4188df4 100644 --- a/netbox/ipam/views.py +++ b/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 diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 548173a32..a743947fe 100644 --- a/netbox/netbox/views.py +++ b/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', diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 9e330b782..3ad87a8ff 100644 --- a/netbox/secrets/api/views.py +++ b/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 diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index e9ea1835f..2872616b8 100644 --- a/netbox/secrets/views.py +++ b/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 diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 148058a33..652544b21 100644 --- a/netbox/tenancy/api/views.py +++ b/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 diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 9ef44206c..b129177eb 100644 --- a/netbox/tenancy/views.py +++ b/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 diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 867413357..df48e92e6 100644 --- a/netbox/virtualization/api/views.py +++ b/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 diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 176c89f2e..a622265fc 100644 --- a/netbox/virtualization/views.py +++ b/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