mirror of
				https://github.com/django/django.git
				synced 2025-11-03 21:25:09 +00:00 
			
		
		
		
	Fixed #24075 -- Used post-migration models in contrib apps receivers.
Thanks Markus and Tim for the review.
This commit is contained in:
		
							parent
							
								
									f937c9ec97
								
							
						
					
					
						commit
						61a16e0270
					
				
					 8 changed files with 62 additions and 59 deletions
				
			
		| 
						 | 
					@ -6,7 +6,7 @@ from __future__ import unicode_literals
 | 
				
			||||||
import getpass
 | 
					import getpass
 | 
				
			||||||
import unicodedata
 | 
					import unicodedata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps as global_apps
 | 
				
			||||||
from django.contrib.auth import get_permission_codename
 | 
					from django.contrib.auth import get_permission_codename
 | 
				
			||||||
from django.core import exceptions
 | 
					from django.core import exceptions
 | 
				
			||||||
from django.db import DEFAULT_DB_ALIAS, router
 | 
					from django.db import DEFAULT_DB_ALIAS, router
 | 
				
			||||||
| 
						 | 
					@ -37,11 +37,14 @@ def _get_builtin_permissions(opts):
 | 
				
			||||||
    return perms
 | 
					    return perms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
 | 
					def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
 | 
				
			||||||
    if not app_config.models_module:
 | 
					    if not app_config.models_module:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app_label = app_config.label
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
 | 
					        app_config = apps.get_app_config(app_label)
 | 
				
			||||||
 | 
					        ContentType = apps.get_model('contenttypes', 'ContentType')
 | 
				
			||||||
        Permission = apps.get_model('auth', 'Permission')
 | 
					        Permission = apps.get_model('auth', 'Permission')
 | 
				
			||||||
    except LookupError:
 | 
					    except LookupError:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
| 
						 | 
					@ -49,8 +52,6 @@ def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_
 | 
				
			||||||
    if not router.allow_migrate_model(using, Permission):
 | 
					    if not router.allow_migrate_model(using, Permission):
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from django.contrib.contenttypes.models import ContentType
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # This will hold the permissions we're looking for as
 | 
					    # This will hold the permissions we're looking for as
 | 
				
			||||||
    # (content_type, (codename, name))
 | 
					    # (content_type, (codename, name))
 | 
				
			||||||
    searched_perms = list()
 | 
					    searched_perms = list()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,10 @@
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps as global_apps
 | 
				
			||||||
from django.db import DEFAULT_DB_ALIAS, router
 | 
					from django.db import DEFAULT_DB_ALIAS, router
 | 
				
			||||||
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_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
 | 
					def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **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
 | 
				
			||||||
    entries that no longer have a matching model class.
 | 
					    entries that no longer have a matching model class.
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,9 @@ def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT
 | 
				
			||||||
    if not app_config.models_module:
 | 
					    if not app_config.models_module:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app_label = app_config.label
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
 | 
					        app_config = apps.get_app_config(app_label)
 | 
				
			||||||
        ContentType = apps.get_model('contenttypes', 'ContentType')
 | 
					        ContentType = apps.get_model('contenttypes', 'ContentType')
 | 
				
			||||||
    except LookupError:
 | 
					    except LookupError:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
| 
						 | 
					@ -21,8 +23,9 @@ def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ContentType.objects.clear_cache()
 | 
					    ContentType.objects.clear_cache()
 | 
				
			||||||
 | 
					    # Always clear the global content types cache.
 | 
				
			||||||
    app_label = app_config.label
 | 
					    if apps is not global_apps:
 | 
				
			||||||
 | 
					        global_apps.get_model('contenttypes', 'ContentType').objects.clear_cache()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app_models = {
 | 
					    app_models = {
 | 
				
			||||||
        model._meta.model_name: model
 | 
					        model._meta.model_name: model
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
 | 
					 | 
				
			||||||
from django.utils.encoding import force_text, python_2_unicode_compatible
 | 
					from django.utils.encoding import force_text, python_2_unicode_compatible
 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,24 +47,15 @@ class ContentTypeManager(models.Manager):
 | 
				
			||||||
        # The ContentType entry was not found in the cache, therefore we
 | 
					        # The ContentType entry was not found in the cache, therefore we
 | 
				
			||||||
        # proceed to load or create it.
 | 
					        # proceed to load or create it.
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            try:
 | 
					            # Start with get() and not get_or_create() in order to use
 | 
				
			||||||
                # We start with get() and not get_or_create() in order to use
 | 
					            # the db_for_read (see #20401).
 | 
				
			||||||
                # the db_for_read (see #20401).
 | 
					            ct = self.get(app_label=opts.app_label, model=opts.model_name)
 | 
				
			||||||
                ct = self.get(app_label=opts.app_label, model=opts.model_name)
 | 
					        except self.model.DoesNotExist:
 | 
				
			||||||
            except self.model.DoesNotExist:
 | 
					            # Not found in the database; we proceed to create it. This time
 | 
				
			||||||
                # Not found in the database; we proceed to create it.  This time we
 | 
					            # use get_or_create to take care of any race conditions.
 | 
				
			||||||
                # use get_or_create to take care of any race conditions.
 | 
					            ct, created = self.get_or_create(
 | 
				
			||||||
                ct, created = self.get_or_create(
 | 
					                app_label=opts.app_label,
 | 
				
			||||||
                    app_label=opts.app_label,
 | 
					                model=opts.model_name,
 | 
				
			||||||
                    model=opts.model_name,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        except (OperationalError, ProgrammingError, IntegrityError):
 | 
					 | 
				
			||||||
            # It's possible to migrate a single app before contenttypes,
 | 
					 | 
				
			||||||
            # as it's not a required initial dependency (it's contrib!)
 | 
					 | 
				
			||||||
            # Have a nice error for this.
 | 
					 | 
				
			||||||
            raise RuntimeError(
 | 
					 | 
				
			||||||
                "Error creating new content types. Please make sure contenttypes "
 | 
					 | 
				
			||||||
                "is migrated before trying to migrate apps individually."
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        self._add_to_cache(self.db, ct)
 | 
					        self._add_to_cache(self.db, ct)
 | 
				
			||||||
        return ct
 | 
					        return ct
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,13 @@
 | 
				
			||||||
Creates the default Site object.
 | 
					Creates the default Site object.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps as global_apps
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core.management.color import no_style
 | 
					from django.core.management.color import no_style
 | 
				
			||||||
from django.db import DEFAULT_DB_ALIAS, connections, router
 | 
					from django.db import DEFAULT_DB_ALIAS, connections, router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_default_site(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs):
 | 
					def create_default_site(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        Site = apps.get_model('sites', 'Site')
 | 
					        Site = apps.get_model('sites', 'Site')
 | 
				
			||||||
    except LookupError:
 | 
					    except LookupError:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ from django.contrib.auth.models import Group, Permission, User
 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
from django.core.management import call_command
 | 
					from django.core.management import call_command
 | 
				
			||||||
from django.core.management.base import CommandError
 | 
					from django.core.management.base import CommandError
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
from django.test import TestCase, mock, override_settings
 | 
					from django.test import TestCase, mock, override_settings
 | 
				
			||||||
from django.utils import six
 | 
					from django.utils import six
 | 
				
			||||||
from django.utils.encoding import force_str
 | 
					from django.utils.encoding import force_str
 | 
				
			||||||
| 
						 | 
					@ -561,11 +562,12 @@ class MultiDBCreatesuperuserTestCase(TestCase):
 | 
				
			||||||
        self.assertEqual(user.email, 'joe@somewhere.org')
 | 
					        self.assertEqual(user.email, 'joe@somewhere.org')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PermissionTestCase(TestCase):
 | 
					class CreatePermissionsTests(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        self._original_permissions = Permission._meta.permissions[:]
 | 
					        self._original_permissions = Permission._meta.permissions[:]
 | 
				
			||||||
        self._original_default_permissions = Permission._meta.default_permissions
 | 
					        self._original_default_permissions = Permission._meta.default_permissions
 | 
				
			||||||
 | 
					        self.app_config = apps.get_app_config('auth')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def tearDown(self):
 | 
					    def tearDown(self):
 | 
				
			||||||
        Permission._meta.permissions = self._original_permissions
 | 
					        Permission._meta.permissions = self._original_permissions
 | 
				
			||||||
| 
						 | 
					@ -573,13 +575,11 @@ class PermissionTestCase(TestCase):
 | 
				
			||||||
        ContentType.objects.clear_cache()
 | 
					        ContentType.objects.clear_cache()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_default_permissions(self):
 | 
					    def test_default_permissions(self):
 | 
				
			||||||
        auth_app_config = apps.get_app_config('auth')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission')
 | 
					        permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission')
 | 
				
			||||||
        Permission._meta.permissions = [
 | 
					        Permission._meta.permissions = [
 | 
				
			||||||
            ('my_custom_permission', 'Some permission'),
 | 
					            ('my_custom_permission', 'Some permission'),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        create_permissions(auth_app_config, verbosity=0)
 | 
					        create_permissions(self.app_config, verbosity=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # add/change/delete permission by default + custom permission
 | 
					        # add/change/delete permission by default + custom permission
 | 
				
			||||||
        self.assertEqual(Permission.objects.filter(
 | 
					        self.assertEqual(Permission.objects.filter(
 | 
				
			||||||
| 
						 | 
					@ -588,9 +588,23 @@ class PermissionTestCase(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Permission.objects.filter(content_type=permission_content_type).delete()
 | 
					        Permission.objects.filter(content_type=permission_content_type).delete()
 | 
				
			||||||
        Permission._meta.default_permissions = []
 | 
					        Permission._meta.default_permissions = []
 | 
				
			||||||
        create_permissions(auth_app_config, verbosity=0)
 | 
					        create_permissions(self.app_config, verbosity=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # custom permission only since default permissions is empty
 | 
					        # custom permission only since default permissions is empty
 | 
				
			||||||
        self.assertEqual(Permission.objects.filter(
 | 
					        self.assertEqual(Permission.objects.filter(
 | 
				
			||||||
            content_type=permission_content_type,
 | 
					            content_type=permission_content_type,
 | 
				
			||||||
        ).count(), 1)
 | 
					        ).count(), 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_unvailable_models(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        #24075 - Permissions shouldn't be created or deleted if the ContentType
 | 
				
			||||||
 | 
					        or Permission models aren't available.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        state = migrations.state.ProjectState()
 | 
				
			||||||
 | 
					        # Unvailable contenttypes.ContentType
 | 
				
			||||||
 | 
					        with self.assertNumQueries(0):
 | 
				
			||||||
 | 
					            create_permissions(self.app_config, verbosity=0, apps=state.apps)
 | 
				
			||||||
 | 
					        # Unvailable auth.Permission
 | 
				
			||||||
 | 
					        state = migrations.state.ProjectState(real_apps=['contenttypes'])
 | 
				
			||||||
 | 
					        with self.assertNumQueries(0):
 | 
				
			||||||
 | 
					            create_permissions(self.app_config, verbosity=0, apps=state.apps)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,9 +3,8 @@ from __future__ import unicode_literals
 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
 | 
					from django.contrib.contenttypes.models import ContentType, ContentTypeManager
 | 
				
			||||||
from django.contrib.contenttypes.views import shortcut
 | 
					from django.contrib.contenttypes.views import shortcut
 | 
				
			||||||
from django.contrib.sites.shortcuts import get_current_site
 | 
					from django.contrib.sites.shortcuts import get_current_site
 | 
				
			||||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
 | 
					 | 
				
			||||||
from django.http import Http404, HttpRequest
 | 
					from django.http import Http404, HttpRequest
 | 
				
			||||||
from django.test import TestCase, mock, override_settings
 | 
					from django.test import TestCase, override_settings
 | 
				
			||||||
from django.utils import six
 | 
					from django.utils import six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import (
 | 
					from .models import (
 | 
				
			||||||
| 
						 | 
					@ -243,26 +242,3 @@ class ContentTypesTests(TestCase):
 | 
				
			||||||
        # Instead, just return the ContentType object and let the app detect stale states.
 | 
					        # Instead, just return the ContentType object and let the app detect stale states.
 | 
				
			||||||
        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())
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @mock.patch('django.contrib.contenttypes.models.ContentTypeManager.get_or_create')
 | 
					 | 
				
			||||||
    @mock.patch('django.contrib.contenttypes.models.ContentTypeManager.get')
 | 
					 | 
				
			||||||
    def test_message_if_get_for_model_fails(self, mocked_get, mocked_get_or_create):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Check that `RuntimeError` with nice error message is raised if
 | 
					 | 
				
			||||||
        `get_for_model` fails because of database errors.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def _test_message(mocked_method):
 | 
					 | 
				
			||||||
            for ExceptionClass in (IntegrityError, OperationalError, ProgrammingError):
 | 
					 | 
				
			||||||
                mocked_method.side_effect = ExceptionClass
 | 
					 | 
				
			||||||
                with self.assertRaisesMessage(
 | 
					 | 
				
			||||||
                    RuntimeError,
 | 
					 | 
				
			||||||
                    "Error creating new content types. Please make sure contenttypes "
 | 
					 | 
				
			||||||
                    "is migrated before trying to migrate apps individually."
 | 
					 | 
				
			||||||
                ):
 | 
					 | 
				
			||||||
                    ContentType.objects.get_for_model(ContentType)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        _test_message(mocked_get)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        mocked_get.side_effect = ContentType.DoesNotExist
 | 
					 | 
				
			||||||
        _test_message(mocked_get_or_create)
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -404,6 +404,16 @@ class UpdateContentTypesTests(TestCase):
 | 
				
			||||||
        self.assertIn("Stale content types remain.", stdout.getvalue())
 | 
					        self.assertIn("Stale content types remain.", stdout.getvalue())
 | 
				
			||||||
        self.assertEqual(ContentType.objects.count(), self.before_count + 1)
 | 
					        self.assertEqual(ContentType.objects.count(), self.before_count + 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_unavailable_content_type_model(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        #24075 - A ContentType shouldn't be created or deleted if the model
 | 
				
			||||||
 | 
					        isn't available.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        apps = Apps()
 | 
				
			||||||
 | 
					        with self.assertNumQueries(0):
 | 
				
			||||||
 | 
					            management.update_contenttypes(self.app_config, interactive=False, verbosity=0, apps=apps)
 | 
				
			||||||
 | 
					        self.assertEqual(ContentType.objects.count(), self.before_count + 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestRouter(object):
 | 
					class TestRouter(object):
 | 
				
			||||||
    def db_for_read(self, model, **hints):
 | 
					    def db_for_read(self, model, **hints):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.contrib.sites import models
 | 
					from django.contrib.sites import models
 | 
				
			||||||
from django.contrib.sites.management import create_default_site
 | 
					from django.contrib.sites.management import create_default_site
 | 
				
			||||||
| 
						 | 
					@ -293,6 +294,14 @@ class CreateDefaultSiteTests(TestCase):
 | 
				
			||||||
        create_default_site(self.app_config, verbosity=0)
 | 
					        create_default_site(self.app_config, verbosity=0)
 | 
				
			||||||
        self.assertEqual(Site.objects.get().pk, 1)
 | 
					        self.assertEqual(Site.objects.get().pk, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_unavailable_site_model(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        #24075 - A Site shouldn't be created if the model isn't available.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        apps = Apps()
 | 
				
			||||||
 | 
					        create_default_site(self.app_config, verbosity=0, apps=apps)
 | 
				
			||||||
 | 
					        self.assertFalse(Site.objects.exists())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MiddlewareTest(TestCase):
 | 
					class MiddlewareTest(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue