Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions django/contrib/auth/apps.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.apps import AppConfig
from django.core import checks
from django.db.models.query_utils import DeferredAttribute
from django.db.models.signals import post_migrate, pre_migrate
from django.db.models.signals import post_migrate
from django.utils.translation import gettext_lazy as _

from . import get_user_model
from .checks import check_middleware, check_models_permissions, check_user_model
from .management import create_permissions, rename_permissions
from .management import create_permissions
from .signals import user_logged_in


Expand All @@ -20,11 +20,6 @@ def ready(self):
create_permissions,
dispatch_uid="django.contrib.auth.management.create_permissions",
)
pre_migrate.connect(
rename_permissions,
dispatch_uid="django.contrib.auth.management.rename_permissions",
)

last_login_field = getattr(get_user_model(), "last_login", None)
# Register the handler only if UserModel.last_login is a field.
if isinstance(last_login_field, DeferredAttribute):
Expand Down
82 changes: 1 addition & 81 deletions django/contrib/auth/management/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
from django.contrib.auth import get_permission_codename
from django.contrib.contenttypes.management import create_contenttypes
from django.core import exceptions
from django.db import DEFAULT_DB_ALIAS, migrations, router, transaction
from django.db.utils import IntegrityError
from django.utils.text import camel_case_to_spaces
from django.db import DEFAULT_DB_ALIAS, router


def _get_all_permissions(opts):
Expand Down Expand Up @@ -110,84 +108,6 @@ def create_permissions(
print("Adding permission '%s'" % perm)


class RenamePermission(migrations.RunPython):
def __init__(self, app_label, old_model, new_model):
self.app_label = app_label
self.old_model = old_model
self.new_model = new_model
super(RenamePermission, self).__init__(
self.rename_forward, self.rename_backward
)

def _rename(self, apps, schema_editor, old_model, new_model):
ContentType = apps.get_model("contenttypes", "ContentType")
# Use the live Permission model instead of the frozen one, since frozen
# models do not retain foreign key constraints.
from django.contrib.auth.models import Permission

db = schema_editor.connection.alias
ctypes = ContentType.objects.using(db).filter(
app_label=self.app_label, model__icontains=old_model.lower()
)
for permission in Permission.objects.using(db).filter(
content_type_id__in=ctypes.values("id")
):
prefix = permission.codename.split("_")[0]
default_verbose_name = camel_case_to_spaces(new_model)

new_codename = f"{prefix}_{new_model.lower()}"
new_name = f"Can {prefix} {default_verbose_name}"

if permission.codename != new_codename or permission.name != new_name:
permission.codename = new_codename
permission.name = new_name
try:
with transaction.atomic(using=db):
permission.save(update_fields={"name", "codename"})
except IntegrityError:
pass

def rename_forward(self, apps, schema_editor):
self._rename(apps, schema_editor, self.old_model, self.new_model)

def rename_backward(self, apps, schema_editor):
self._rename(apps, schema_editor, self.new_model, self.old_model)


def rename_permissions(
plan,
verbosity=2,
interactive=True,
using=DEFAULT_DB_ALIAS,
apps=global_apps,
**kwargs,
):
"""
Insert a `RenamePermissionType` operation after every planned `RenameModel`
operation.
"""
try:
Permission = apps.get_model("auth", "Permission")
except LookupError:
return
else:
if not router.allow_migrate_model(using, Permission):
return

for migration, backward in plan:
inserts = []
for index, operation in enumerate(migration.operations):
if isinstance(operation, migrations.RenameModel):
operation = RenamePermission(
migration.app_label,
operation.old_name,
operation.new_name,
)
inserts.append((index + 1, operation))
for inserted, (index, operation) in enumerate(inserts):
migration.operations.insert(inserted + index, operation)


def get_system_username():
"""
Return the current system user's username, or an empty string if the
Expand Down
7 changes: 7 additions & 0 deletions docs/releases/6.0.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ Bugfixes
* Fixed a regression in Django 6.0 where the :attr:`.Widget.use_fieldset`
attribute of :class:`~django.forms.ClearableFileInput` was flipped
from ``False`` to ``True`` (:ticket:`36829`).

* Reverted an undocumented optimization in Django 6.0 that modified permission
:attr:`~django.contrib.auth.models.Permission.name` and
:attr:`~django.contrib.auth.models.Permission.codename` values when renaming
models via a migration. This change could affect unrelated
:class:`~django.contrib.auth.models.Permission` objects (:ticket:`36843`) and
did not report conflicts (:ticket:`36793`).
14 changes: 0 additions & 14 deletions tests/auth_tests/operations_migrations/0001_initial.py

This file was deleted.

This file was deleted.

Empty file.
199 changes: 2 additions & 197 deletions tests/auth_tests/test_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,15 @@
from unittest import mock

from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_permission_codename, management
from django.contrib.auth.management import (
RenamePermission,
create_permissions,
get_default_username,
)
from django.contrib.auth.management import create_permissions, get_default_username
from django.contrib.auth.management.commands import changepassword, createsuperuser
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.core.management import call_command
from django.core.management.base import CommandError
from django.db import migrations, models
from django.db import migrations
from django.test import TestCase, override_settings
from django.test.testcases import TransactionTestCase
from django.utils.translation import gettext_lazy as _

from .models import (
Expand Down Expand Up @@ -1534,195 +1528,6 @@ def test_permission_with_proxy_content_type_created(self):
)


@override_settings(
MIGRATION_MODULES=dict(
settings.MIGRATION_MODULES,
auth_tests="auth_tests.operations_migrations",
),
)
class PermissionRenameOperationsTests(TransactionTestCase):
available_apps = [
"django.contrib.contenttypes",
"django.contrib.auth",
"auth_tests",
]
databases = {"default", "other"}

def setUp(self):
app_config = apps.get_app_config("auth_tests")
models.signals.post_migrate.connect(
self.assertOperationsInjected, sender=app_config
)
self.addCleanup(
models.signals.post_migrate.disconnect,
self.assertOperationsInjected,
sender=app_config,
)

def assertOperationsInjected(self, plan, **kwargs):
for migration, _backward in plan:
operations = iter(migration.operations)
for operation in operations:
if isinstance(operation, migrations.RenameModel):
next_operation = next(operations)
self.assertIsInstance(next_operation, RenamePermission)
self.assertEqual(next_operation.app_label, migration.app_label)
self.assertEqual(next_operation.old_model, operation.old_name)
self.assertEqual(next_operation.new_model, operation.new_name)

def test_permission_rename(self):
ct = ContentType.objects.create(app_label="auth_tests", model="oldmodel")
actions = ["add", "change", "delete", "view"]
for action in actions:
Permission.objects.create(
codename=f"{action}_oldmodel",
name=f"Can {action} old model",
content_type=ct,
)

call_command("migrate", "auth_tests", verbosity=0)
for action in actions:
self.assertFalse(
Permission.objects.filter(codename=f"{action}_oldmodel").exists()
)
self.assertTrue(
Permission.objects.filter(codename=f"{action}_newmodel").exists()
)

call_command(
"migrate",
"auth_tests",
"zero",
database="default",
interactive=False,
verbosity=0,
)

for action in actions:
self.assertTrue(
Permission.objects.filter(codename=f"{action}_oldmodel").exists()
)
self.assertFalse(
Permission.objects.filter(codename=f"{action}_newmodel").exists()
)

def test_permission_rename_other_db(self):
ct = ContentType.objects.using("default").create(
app_label="auth_tests", model="oldmodel"
)
permission = Permission.objects.using("default").create(
codename="add_oldmodel",
name="Can add old model",
content_type=ct,
)
# RenamePermission respects the database.
call_command("migrate", "auth_tests", verbosity=0, database="other")
permission.refresh_from_db()
self.assertEqual(permission.codename, "add_oldmodel")
self.assertFalse(
Permission.objects.using("other").filter(codename="add_oldmodel").exists()
)
self.assertTrue(
Permission.objects.using("other").filter(codename="add_newmodel").exists()
)

@mock.patch(
"django.db.router.allow_migrate_model",
return_value=False,
)
def test_rename_skipped_if_router_disallows(self, _):
ct = ContentType.objects.create(app_label="auth_tests", model="oldmodel")
Permission.objects.create(
codename="change_oldmodel",
name="Can change old model",
content_type=ct,
)
# The rename operation should not be there when disallowed by router.
app_config = apps.get_app_config("auth_tests")
models.signals.post_migrate.disconnect(
self.assertOperationsInjected, sender=app_config
)

call_command(
"migrate",
"auth_tests",
database="default",
interactive=False,
verbosity=0,
)
self.assertTrue(Permission.objects.filter(codename="change_oldmodel").exists())
self.assertFalse(Permission.objects.filter(codename="change_newmodel").exists())

call_command(
"migrate",
"auth_tests",
"zero",
database="default",
interactive=False,
verbosity=0,
)

def test_rename_backward_does_nothing_if_no_permissions(self):
Permission.objects.filter(content_type__app_label="auth_tests").delete()

call_command(
"migrate",
"auth_tests",
"zero",
database="default",
interactive=False,
verbosity=0,
)
self.assertFalse(
Permission.objects.filter(
codename__in=["change_oldmodel", "change_newmodel"]
).exists()
)

def test_rename_permission_conflict(self):
ct = ContentType.objects.create(app_label="auth_tests", model="oldmodel")
Permission.objects.create(
codename="change_newmodel",
name="Can change new model",
content_type=ct,
)
Permission.objects.create(
codename="change_oldmodel",
name="Can change old model",
content_type=ct,
)

call_command(
"migrate",
"auth_tests",
database="default",
interactive=False,
verbosity=0,
)
self.assertTrue(
Permission.objects.filter(
codename="change_oldmodel",
name="Can change old model",
).exists()
)
self.assertEqual(
Permission.objects.filter(
codename="change_newmodel",
name="Can change new model",
).count(),
1,
)

call_command(
"migrate",
"auth_tests",
"zero",
database="default",
interactive=False,
verbosity=0,
)


class DefaultDBRouter:
"""Route all writes to default."""

Expand Down