Fixed #19806 -- Ensure that content types and permissions aren't created for swapped models.

Thanks to rizumu for the report.
This commit is contained in:
Russell Keith-Magee 2012-11-24 13:43:20 +08:00
parent 0a0a0d66b3
commit c8985a8a73
6 changed files with 101 additions and 23 deletions

View File

@ -5,6 +5,7 @@ from django.utils.encoding import smart_text
from django.utils import six
from django.utils.six.moves import input
def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, **kwargs):
"""
Creates content types for models in the given app, removing any model
@ -77,6 +78,7 @@ If you're unsure, answer 'no'.
if verbosity >= 2:
print("Stale content types remain.")
def update_all_contenttypes(verbosity=2, **kwargs):
for app in get_apps():
update_contenttypes(app, None, verbosity, **kwargs)

View File

@ -35,7 +35,10 @@ def get_validation_errors(outfile, app=None):
for (app_name, error) in get_app_errors().items():
e.add(app_name, error)
for cls in models.get_models(app):
inc = set(models.get_models(app, include_swapped=True))
no_inc = set(models.get_models(app))
for cls in models.get_models(app, include_swapped=True):
opts = cls._meta
# Check swappable attribute.
@ -138,16 +141,17 @@ def get_validation_errors(outfile, app=None):
# fields, m2m fields, m2m related objects or related objects
if f.rel:
if f.rel.to not in models.get_models():
# If the related model is swapped, provide a hint;
# otherwise, the model just hasn't been installed.
if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
else:
e.add(opts, "'%s' has a relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
# it is a string and we could not find the model it refers to
# so skip the next section
if isinstance(f.rel.to, six.string_types):
continue
# Make sure the model we're related hasn't been swapped out
if f.rel.to._meta.swapped:
e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
# Make sure the related field specified by a ForeignKey is unique
if not f.rel.to._meta.get_field(f.rel.field_name).unique:
e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__))
@ -184,16 +188,18 @@ def get_validation_errors(outfile, app=None):
# existing fields, m2m fields, m2m related objects or related
# objects
if f.rel.to not in models.get_models():
# If the related model is swapped, provide a hint;
# otherwise, the model just hasn't been installed.
if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
else:
e.add(opts, "'%s' has an m2m relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
# it is a string and we could not find the model it refers to
# so skip the next section
if isinstance(f.rel.to, six.string_types):
continue
# Make sure the model we're related hasn't been swapped out
if f.rel.to._meta.swapped:
e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
# Check that the field is not set to unique. ManyToManyFields do not support unique.
if f.unique:
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)

View File

@ -167,7 +167,7 @@ class AppCache(object):
def get_models(self, app_mod=None,
include_auto_created=False, include_deferred=False,
only_installed=True):
only_installed=True, include_swapped=False):
"""
Given a module containing models, returns a list of the models.
Otherwise returns a list of all installed models.
@ -179,8 +179,16 @@ class AppCache(object):
By default, models created to satisfy deferred attribute
queries are *not* included in the list of models. However, if
you specify include_deferred, they will be.
By default, models that aren't part of installed apps will *not*
be included in the list of models. However, if you specify
only_installed=False, they will be.
By default, models that have been swapped out will *not* be
included in the list of models. However, if you specify
include_swapped, they will be.
"""
cache_key = (app_mod, include_auto_created, include_deferred, only_installed)
cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
try:
return self._get_models_cache[cache_key]
except KeyError:
@ -203,7 +211,8 @@ class AppCache(object):
model_list.extend(
model for model in app.values()
if ((not model._deferred or include_deferred) and
(not model._meta.auto_created or include_auto_created))
(not model._meta.auto_created or include_auto_created) and
(not model._meta.swapped or include_swapped))
)
self._get_models_cache[cache_key] = model_list
return model_list

View File

@ -0,0 +1,15 @@
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=100)
publication_date = models.DateField()
class Meta:
swappable = 'TEST_ARTICLE_MODEL'
class AlternateArticle(models.Model):
title = models.CharField(max_length=100)
publication_date = models.DateField()
byline = models.CharField(max_length=100)

View File

@ -0,0 +1,46 @@
from __future__ import absolute_import, unicode_literals
from django.utils.six import StringIO
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core import management
from django.db.models.loading import cache
from django.test import TestCase
from django.test.utils import override_settings
class SwappableModelTests(TestCase):
def setUp(self):
# This test modifies the installed apps, so we need to make sure
# we're not dealing with a cached app list.
cache._get_models_cache.clear()
def tearDown(self):
# By fiddling with swappable models, we alter the installed models
# cache, so flush it to make sure there are no side effects.
cache._get_models_cache.clear()
@override_settings(TEST_ARTICLE_MODEL='swappable_models.AlternateArticle')
def test_generated_data(self):
"Permissions and content types are not created for a swapped model"
# Delete all permissions and content_types
Permission.objects.all().delete()
ContentType.objects.all().delete()
# Re-run syncdb. This will re-build the permissions and content types.
new_io = StringIO()
management.call_command('syncdb', load_initial_data=False, interactive=False, stdout=new_io)
# Check that content types and permissions exist for the swapped model,
# but not for the swappable model.
apps_models = [(p.content_type.app_label, p.content_type.model)
for p in Permission.objects.all()]
self.assertIn(('swappable_models', 'alternatearticle'), apps_models)
self.assertNotIn(('swappable_models', 'article'), apps_models)
apps_models = [(ct.app_label, ct.model)
for ct in ContentType.objects.all()]
self.assertIn(('swappable_models', 'alternatearticle'), apps_models)
self.assertNotIn(('swappable_models', 'article'), apps_models)