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:
parent
0a0a0d66b3
commit
c8985a8a73
|
@ -5,6 +5,7 @@ from django.utils.encoding import smart_text
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.six.moves import input
|
from django.utils.six.moves import input
|
||||||
|
|
||||||
|
|
||||||
def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, **kwargs):
|
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
|
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:
|
if verbosity >= 2:
|
||||||
print("Stale content types remain.")
|
print("Stale content types remain.")
|
||||||
|
|
||||||
|
|
||||||
def update_all_contenttypes(verbosity=2, **kwargs):
|
def update_all_contenttypes(verbosity=2, **kwargs):
|
||||||
for app in get_apps():
|
for app in get_apps():
|
||||||
update_contenttypes(app, None, verbosity, **kwargs)
|
update_contenttypes(app, None, verbosity, **kwargs)
|
||||||
|
|
|
@ -35,7 +35,10 @@ def get_validation_errors(outfile, app=None):
|
||||||
for (app_name, error) in get_app_errors().items():
|
for (app_name, error) in get_app_errors().items():
|
||||||
e.add(app_name, error)
|
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
|
opts = cls._meta
|
||||||
|
|
||||||
# Check swappable attribute.
|
# Check swappable attribute.
|
||||||
|
@ -138,16 +141,17 @@ def get_validation_errors(outfile, app=None):
|
||||||
# fields, m2m fields, m2m related objects or related objects
|
# fields, m2m fields, m2m related objects or related objects
|
||||||
if f.rel:
|
if f.rel:
|
||||||
if f.rel.to not in models.get_models():
|
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))
|
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
|
# it is a string and we could not find the model it refers to
|
||||||
# so skip the next section
|
# so skip the next section
|
||||||
if isinstance(f.rel.to, six.string_types):
|
if isinstance(f.rel.to, six.string_types):
|
||||||
continue
|
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
|
# Make sure the related field specified by a ForeignKey is unique
|
||||||
if not f.rel.to._meta.get_field(f.rel.field_name).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__))
|
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
|
# existing fields, m2m fields, m2m related objects or related
|
||||||
# objects
|
# objects
|
||||||
if f.rel.to not in models.get_models():
|
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))
|
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
|
# it is a string and we could not find the model it refers to
|
||||||
# so skip the next section
|
# so skip the next section
|
||||||
if isinstance(f.rel.to, six.string_types):
|
if isinstance(f.rel.to, six.string_types):
|
||||||
continue
|
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.
|
# Check that the field is not set to unique. ManyToManyFields do not support unique.
|
||||||
if f.unique:
|
if f.unique:
|
||||||
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
|
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
|
||||||
|
|
|
@ -167,7 +167,7 @@ class AppCache(object):
|
||||||
|
|
||||||
def get_models(self, app_mod=None,
|
def get_models(self, app_mod=None,
|
||||||
include_auto_created=False, include_deferred=False,
|
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.
|
Given a module containing models, returns a list of the models.
|
||||||
Otherwise returns a list of all installed models.
|
Otherwise returns a list of all installed models.
|
||||||
|
@ -179,8 +179,16 @@ class AppCache(object):
|
||||||
By default, models created to satisfy deferred attribute
|
By default, models created to satisfy deferred attribute
|
||||||
queries are *not* included in the list of models. However, if
|
queries are *not* included in the list of models. However, if
|
||||||
you specify include_deferred, they will be.
|
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:
|
try:
|
||||||
return self._get_models_cache[cache_key]
|
return self._get_models_cache[cache_key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -203,7 +211,8 @@ class AppCache(object):
|
||||||
model_list.extend(
|
model_list.extend(
|
||||||
model for model in app.values()
|
model for model in app.values()
|
||||||
if ((not model._deferred or include_deferred) and
|
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
|
self._get_models_cache[cache_key] = model_list
|
||||||
return model_list
|
return model_list
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue