Преглед изворни кода

Fixes #11335: Default manager for ObjectChange should filter by installed apps (#11709)

* Fixes #11335: Default manager for ObjectChange should filter by installed apps

* Employ canonical model discovery mechanism

* Move filtering logic to valid_models() queryset method

* fixed import to avoid content type does not exist

* Cleanup

---------

Co-authored-by: Abhimanyu Saharan <desk.abhimanyu@gmail.com>
Jeremy Stretch пре 2 година
родитељ
комит
63ba9fb38c

+ 1 - 1
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
 

+ 2 - 2
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']

+ 13 - 0
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)

+ 2 - 1
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):

+ 4 - 4
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,
         )

+ 3 - 1
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)