Fixed #31357 -- Fixed get_for_models() crash for stale content types when model with the same name exists in another app.

This commit is contained in:
Biel Frontera 2022-03-14 09:53:36 +01:00 committed by Mariusz Felisiak
parent 839d403e50
commit 859a87d873
2 changed files with 32 additions and 6 deletions

View File

@ -2,6 +2,7 @@ from collections import defaultdict
from django.apps import apps from django.apps import apps
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -64,9 +65,8 @@ class ContentTypeManager(models.Manager):
Given *models, return a dictionary mapping {model: content_type}. Given *models, return a dictionary mapping {model: content_type}.
""" """
results = {} results = {}
# Models that aren't already in the cache. # Models that aren't already in the cache grouped by app labels.
needed_app_labels = set() needed_models = defaultdict(set)
needed_models = set()
# Mapping of opts to the list of models requiring it. # Mapping of opts to the list of models requiring it.
needed_opts = defaultdict(list) needed_opts = defaultdict(list)
for model in models: for model in models:
@ -74,14 +74,20 @@ class ContentTypeManager(models.Manager):
try: try:
ct = self._get_from_cache(opts) ct = self._get_from_cache(opts)
except KeyError: except KeyError:
needed_app_labels.add(opts.app_label) needed_models[opts.app_label].add(opts.model_name)
needed_models.add(opts.model_name)
needed_opts[opts].append(model) needed_opts[opts].append(model)
else: else:
results[model] = ct results[model] = ct
if needed_opts: if needed_opts:
# Lookup required content types from the DB. # Lookup required content types from the DB.
cts = self.filter(app_label__in=needed_app_labels, model__in=needed_models) condition = Q(
*(
Q(("app_label", app_label), ("model__in", models), _connector=Q.AND)
for app_label, models in needed_models.items()
),
_connector=Q.OR,
)
cts = self.filter(condition)
for ct in cts: for ct in cts:
opts_models = needed_opts.pop(ct.model_class()._meta, []) opts_models = needed_opts.pop(ct.model_class()._meta, [])
for model in opts_models: for model in opts_models:

View File

@ -248,6 +248,26 @@ class ContentTypesTests(TestCase):
ct_fetched = ContentType.objects.get_for_id(ct.pk) ct_fetched = ContentType.objects.get_for_id(ct.pk)
self.assertIsNone(ct_fetched.model_class()) self.assertIsNone(ct_fetched.model_class())
def test_missing_model_with_existing_model_name(self):
"""
Displaying content types in admin (or anywhere) doesn't break on
leftover content type records in the DB for which no model is defined
anymore, even if a model with the same name exists in another app.
"""
# Create a stale ContentType that matches the name of an existing
# model.
ContentType.objects.create(app_label="contenttypes", model="author")
ContentType.objects.clear_cache()
# get_for_models() should work as expected for existing models.
cts = ContentType.objects.get_for_models(ContentType, Author)
self.assertEqual(
cts,
{
ContentType: ContentType.objects.get_for_model(ContentType),
Author: ContentType.objects.get_for_model(Author),
},
)
def test_str(self): def test_str(self):
ct = ContentType.objects.get(app_label="contenttypes_tests", model="site") ct = ContentType.objects.get(app_label="contenttypes_tests", model="site")
self.assertEqual(str(ct), "contenttypes_tests | site") self.assertEqual(str(ct), "contenttypes_tests | site")