Skip to content

Commit

Permalink
[1.5.x] Fixed #19806 -- Ensure that content types and permissions are…
Browse files Browse the repository at this point in the history
…n't created for swapped models.

Thanks to rizumu for the report.

Backport of c8985a8.
  • Loading branch information
freakboy3742 committed Nov 24, 2012
1 parent 158a033 commit 3fd8458
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 23 deletions.
2 changes: 2 additions & 0 deletions django/contrib/contenttypes/management.py
Expand Up @@ -5,6 +5,7 @@
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
Expand Down Expand Up @@ -77,6 +78,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
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)
Expand Down
28 changes: 17 additions & 11 deletions django/core/management/validation.py
Expand Up @@ -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.
Expand Down Expand Up @@ -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():
e.add(opts, "'%s' has a relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to)) # 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 # 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__))
Expand Down Expand Up @@ -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():
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)) # 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 # 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)
Expand Down
33 changes: 21 additions & 12 deletions django/db/models/loading.py
Expand Up @@ -24,24 +24,24 @@ class AppCache(object):
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531. # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531.
__shared_state = dict( __shared_state = dict(
# Keys of app_store are the model modules for each application. # Keys of app_store are the model modules for each application.
app_store = SortedDict(), app_store=SortedDict(),


# Mapping of installed app_labels to model modules for that app. # Mapping of installed app_labels to model modules for that app.
app_labels = {}, app_labels={},


# Mapping of app_labels to a dictionary of model names to model code. # Mapping of app_labels to a dictionary of model names to model code.
# May contain apps that are not installed. # May contain apps that are not installed.
app_models = SortedDict(), app_models=SortedDict(),


# Mapping of app_labels to errors raised when trying to import the app. # Mapping of app_labels to errors raised when trying to import the app.
app_errors = {}, app_errors={},


# -- Everything below here is only used when populating the cache -- # -- Everything below here is only used when populating the cache --
loaded = False, loaded=False,
handled = {}, handled={},
postponed = [], postponed=[],
nesting_level = 0, nesting_level=0,
_get_models_cache = {}, _get_models_cache={},
) )


def __init__(self): def __init__(self):
Expand Down Expand Up @@ -167,7 +167,7 @@ def get_app_errors(self):


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.
Expand All @@ -179,8 +179,16 @@ def get_models(self, app_mod=None,
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:
Expand All @@ -203,7 +211,8 @@ def get_models(self, app_mod=None,
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
Expand Down
Empty file.
15 changes: 15 additions & 0 deletions tests/regressiontests/swappable_models/models.py
@@ -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)
46 changes: 46 additions & 0 deletions tests/regressiontests/swappable_models/tests.py
@@ -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)

0 comments on commit 3fd8458

Please sign in to comment.