diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 3f796d7f8..f4b5a1433 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -368,7 +368,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet): Retrieve a list of recent changes. """ metadata_class = ContentTypeMetadata - queryset = ObjectChange.objects.prefetch_related('user') + queryset = ObjectChange.objects.valid_models().prefetch_related('user') serializer_class = serializers.ObjectChangeSerializer filterset_class = filtersets.ObjectChangeFilterSet diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index e2b118b84..2cb53ed01 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -5,7 +5,7 @@ from django.db import models from django.urls import reverse from extras.choices import * -from utilities.querysets import RestrictedQuerySet +from ..querysets import ObjectChangeQuerySet __all__ = ( 'ObjectChange', @@ -82,7 +82,7 @@ class ObjectChange(models.Model): null=True ) - objects = RestrictedQuerySet.as_manager() + objects = ObjectChangeQuerySet.as_manager() class Meta: ordering = ['-time'] diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 2b97af0fb..2e6f93b93 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -1,3 +1,5 @@ +from django.apps import apps +from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.aggregates import JSONBAgg from django.db.models import OuterRef, Subquery, Q @@ -151,3 +153,14 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): ) return base_query + + +class ObjectChangeQuerySet(RestrictedQuerySet): + + def valid_models(self): + # Exclude any change records which refer to an instance of a model that's no longer installed. This + # can happen when a plugin is removed but its data remains in the database, for example. + content_type_ids = set( + ct.pk for ct in ContentType.objects.get_for_models(*apps.get_models()).values() + ) + return self.filter(changed_object_type_id__in=content_type_ids) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index b59481a36..086c8e246 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -8,7 +8,6 @@ from rest_framework import status from core.choices import ManagedFileRootPathChoices from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site -from extras.api.views import ReportViewSet, ScriptViewSet from extras.models import * from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script, StringVar @@ -579,6 +578,7 @@ class ReportTest(APITestCase): super().setUp() # Monkey-patch the API viewset's _get_report() method to return our test Report above + from extras.api.views import ReportViewSet ReportViewSet._get_report = self.get_test_report def test_get_report(self): @@ -621,6 +621,7 @@ class ScriptTest(APITestCase): super().setUp() # Monkey-patch the API viewset's _get_script() method to return our test Script above + from extras.api.views import ScriptViewSet ScriptViewSet._get_script = self.get_test_script def test_get_script(self): diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 6cbadf09d..6ba63ab58 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -511,7 +511,7 @@ class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView): # class ObjectChangeListView(generic.ObjectListView): - queryset = ObjectChange.objects.all() + queryset = ObjectChange.objects.valid_models() filterset = filtersets.ObjectChangeFilterSet filterset_form = forms.ObjectChangeFilterForm table = tables.ObjectChangeTable @@ -521,10 +521,10 @@ class ObjectChangeListView(generic.ObjectListView): @register_model_view(ObjectChange) class ObjectChangeView(generic.ObjectView): - queryset = ObjectChange.objects.all() + queryset = ObjectChange.objects.valid_models() def get_extra_context(self, request, instance): - related_changes = ObjectChange.objects.restrict(request.user, 'view').filter( + related_changes = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( request_id=instance.request_id ).exclude( pk=instance.pk @@ -534,7 +534,7 @@ class ObjectChangeView(generic.ObjectView): orderable=False ) - objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter( + objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( changed_object_type=instance.changed_object_type, changed_object_id=instance.changed_object_id, ) diff --git a/netbox/users/views.py b/netbox/users/views.py index a82620914..05648e2e3 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -159,7 +159,9 @@ class ProfileView(LoginRequiredMixin, View): def get(self, request): # Compile changelog table - changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related( + changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( + user=request.user + ).prefetch_related( 'changed_object_type' )[:20] changelog_table = ObjectChangeTable(changelog)