#36846: ContentType.get_object_for_this_type() does not handle removed models
-------------------------------------+-------------------------------------
Reporter: Maarten ter Huurne | Owner: Vishy
Type: | Algo
Cleanup/optimization | Status: closed
Component: | Version: dev
contrib.contenttypes |
Severity: Normal | Resolution: wontfix
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jacob Walls):
Thanks for the extra details.
> So if get_object_for_this_type() would call apps.get_model() directly
instead of going through model_class(), it would raise LookupError on
removed models. This would avoid a deprecation cycle.
This occurred to me as well, but I glossed over it because it felt like a
smell showing that `model_class()` is not doing what it should if we have
to stop delegating to it. So I was trying to weigh doing something that
feels wrong against some concrete added value.
> In what scenario would the caller of get_object_for_this_type() handle
"model does not exist" differently from "instance does not exist"?
Well, at first blush I thought one is an expected error and the other is a
programming error, so we should only catch one. We have a
[
https://docs.djangoproject.com/en/6.0/ref/django-admin/#django-contrib-
contenttypes command] to remove stale content types that I would expect to
be run when models are deleted or uninstalled.
Just to be sure all can follow, I sketched out a unit test implementing
your suggestion, it passes on main and fails on your proposal, but I take
it you question the premise of the test:
{{{#!diff
diff --git a/django/contrib/contenttypes/models.py
b/django/contrib/contenttypes/models.py
index 1ae45dea95..71f2655bad 100644
--- a/django/contrib/contenttypes/models.py
+++ b/django/contrib/contenttypes/models.py
@@ -1,6 +1,7 @@
from collections import defaultdict
from django.apps import apps
+from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -176,7 +177,9 @@ class ContentType(models.Model):
method. The ObjectNotExist exception, if thrown, will not be
caught,
so code that calls this method should catch it.
"""
- return
self.model_class()._base_manager.using(using).get(**kwargs)
+ if (model_class := self.model_class()) is None:
+ raise ObjectDoesNotExist
+ return model_class._base_manager.using(using).get(**kwargs)
def get_all_objects_for_this_type(self, **kwargs):
"""
diff --git a/tests/contenttypes_tests/test_fields.py
b/tests/contenttypes_tests/test_fields.py
index 76830c3af3..d772a3d717 100644
--- a/tests/contenttypes_tests/test_fields.py
+++ b/tests/contenttypes_tests/test_fields.py
@@ -2,6 +2,7 @@ import json
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.prefetch import GenericPrefetch
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.test import TestCase
from django.test.utils import isolate_apps
@@ -26,6 +27,26 @@ class GenericForeignKeyTests(TestCase):
):
field.get_content_type()
+ def
test_generic_relation_distinguishes_missing_object_from_missing_model(self):
+ question = Question.objects.create(text="test")
+ answer = Answer.objects.create(question=question)
+
+ answer.question.delete()
+ self.assertIsNone(answer.question)
+
+ # Start over.
+ question = Question.objects.create(text="test")
+ answer = Answer.objects.create(question=question)
+
+ # Make the content type stale.
+ ct = ContentType.objects.get_for_model(question)
+ ct.model = "badquestion"
+ ct.save()
+
+ answer = Answer.objects.get(pk=
answer.pk)
+ with self.assertRaises(Exception):
+ answer.question
+
def test_get_object_cache_respects_deleted_objects(self):
question = Question.objects.create(text="Who?")
post = Post.objects.create(title="Answer", parent=question)
}}}
Wouldn't you rather have this feedback (admin crashing) instead of
glossing over the data problem?
--
Ticket URL: <
https://code.djangoproject.com/ticket/36846#comment:4>