Procházet zdrojové kódy

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 před 2 roky
rodič
revize
63ba9fb38c

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

@@ -368,7 +368,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
     Retrieve a list of recent changes.
     Retrieve a list of recent changes.
     """
     """
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
-    queryset = ObjectChange.objects.prefetch_related('user')
+    queryset = ObjectChange.objects.valid_models().prefetch_related('user')
     serializer_class = serializers.ObjectChangeSerializer
     serializer_class = serializers.ObjectChangeSerializer
     filterset_class = filtersets.ObjectChangeFilterSet
     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 django.urls import reverse
 
 
 from extras.choices import *
 from extras.choices import *
-from utilities.querysets import RestrictedQuerySet
+from ..querysets import ObjectChangeQuerySet
 
 
 __all__ = (
 __all__ = (
     'ObjectChange',
     'ObjectChange',
@@ -82,7 +82,7 @@ class ObjectChange(models.Model):
         null=True
         null=True
     )
     )
 
 
-    objects = RestrictedQuerySet.as_manager()
+    objects = ObjectChangeQuerySet.as_manager()
 
 
     class Meta:
     class Meta:
         ordering = ['-time']
         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.contrib.postgres.aggregates import JSONBAgg
 from django.db.models import OuterRef, Subquery, Q
 from django.db.models import OuterRef, Subquery, Q
 
 
@@ -151,3 +153,14 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
         )
         )
 
 
         return base_query
         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 core.choices import ManagedFileRootPathChoices
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
 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.models import *
 from extras.reports import Report
 from extras.reports import Report
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
 from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
@@ -579,6 +578,7 @@ class ReportTest(APITestCase):
         super().setUp()
         super().setUp()
 
 
         # Monkey-patch the API viewset's _get_report() method to return our test Report above
         # 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
         ReportViewSet._get_report = self.get_test_report
 
 
     def test_get_report(self):
     def test_get_report(self):
@@ -621,6 +621,7 @@ class ScriptTest(APITestCase):
         super().setUp()
         super().setUp()
 
 
         # Monkey-patch the API viewset's _get_script() method to return our test Script above
         # 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
         ScriptViewSet._get_script = self.get_test_script
 
 
     def test_get_script(self):
     def test_get_script(self):

+ 4 - 4
netbox/extras/views.py

@@ -511,7 +511,7 @@ class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
 #
 #
 
 
 class ObjectChangeListView(generic.ObjectListView):
 class ObjectChangeListView(generic.ObjectListView):
-    queryset = ObjectChange.objects.all()
+    queryset = ObjectChange.objects.valid_models()
     filterset = filtersets.ObjectChangeFilterSet
     filterset = filtersets.ObjectChangeFilterSet
     filterset_form = forms.ObjectChangeFilterForm
     filterset_form = forms.ObjectChangeFilterForm
     table = tables.ObjectChangeTable
     table = tables.ObjectChangeTable
@@ -521,10 +521,10 @@ class ObjectChangeListView(generic.ObjectListView):
 
 
 @register_model_view(ObjectChange)
 @register_model_view(ObjectChange)
 class ObjectChangeView(generic.ObjectView):
 class ObjectChangeView(generic.ObjectView):
-    queryset = ObjectChange.objects.all()
+    queryset = ObjectChange.objects.valid_models()
 
 
     def get_extra_context(self, request, instance):
     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
             request_id=instance.request_id
         ).exclude(
         ).exclude(
             pk=instance.pk
             pk=instance.pk
@@ -534,7 +534,7 @@ class ObjectChangeView(generic.ObjectView):
             orderable=False
             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_type=instance.changed_object_type,
             changed_object_id=instance.changed_object_id,
             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):
     def get(self, request):
 
 
         # Compile changelog table
         # 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'
             'changed_object_type'
         )[:20]
         )[:20]
         changelog_table = ObjectChangeTable(changelog)
         changelog_table = ObjectChangeTable(changelog)