Skip to content

Commit 7ac68eb

Browse files
committed
Fixed #27489 -- Renamed permissions upon model renaming in migrations.
1 parent d4a2809 commit 7ac68eb

File tree

6 files changed

+363
-5
lines changed

6 files changed

+363
-5
lines changed

django/contrib/auth/apps.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from django.apps import AppConfig
22
from django.core import checks
33
from django.db.models.query_utils import DeferredAttribute
4-
from django.db.models.signals import post_migrate
4+
from django.db.models.signals import post_migrate, pre_migrate
55
from django.utils.translation import gettext_lazy as _
66

77
from . import get_user_model
88
from .checks import check_middleware, check_models_permissions, check_user_model
9-
from .management import create_permissions
9+
from .management import create_permissions, rename_permissions
1010
from .signals import user_logged_in
1111

1212

@@ -20,6 +20,13 @@ def ready(self):
2020
create_permissions,
2121
dispatch_uid="django.contrib.auth.management.create_permissions",
2222
)
23+
24+
pre_migrate.connect(
25+
rename_permissions,
26+
dispatch_uid="django.contrib.auth.management.\
27+
rename_permissions",
28+
)
29+
2330
last_login_field = getattr(get_user_model(), "last_login", None)
2431
# Register the handler only if UserModel.last_login is a field.
2532
if isinstance(last_login_field, DeferredAttribute):

django/contrib/auth/management/__init__.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from django.contrib.auth import get_permission_codename
1010
from django.contrib.contenttypes.management import create_contenttypes
1111
from django.core import exceptions
12-
from django.db import DEFAULT_DB_ALIAS, router
12+
from django.db import DEFAULT_DB_ALIAS, migrations, router, transaction
13+
from django.db.utils import IntegrityError
14+
from django.utils.text import camel_case_to_spaces
1315

1416

1517
def _get_all_permissions(opts):
@@ -108,6 +110,98 @@ def create_permissions(
108110
print("Adding permission '%s'" % perm)
109111

110112

113+
class RenamePermission(migrations.RunPython):
114+
def __init__(self, app_label, old_model, new_model):
115+
self.app_label = app_label
116+
self.old_model = old_model
117+
self.new_model = new_model
118+
super(RenamePermission, self).__init__(
119+
self.rename_forward, self.rename_backward
120+
)
121+
122+
def _rename(self, apps, schema_editor, old_model, new_model):
123+
ContentType = apps.get_model("contenttypes", "ContentType")
124+
# live model import since with frozen model we have no fk's
125+
# and permission model has default ordering based on fk's
126+
from django.contrib.auth.models import Permission
127+
128+
db = schema_editor.connection.alias
129+
130+
ctypes = ContentType.objects.filter(
131+
app_label=self.app_label, model__iexact=old_model.lower()
132+
)
133+
134+
permissions = Permission.objects.filter(
135+
content_type_id__in=ctypes.values_list("id", flat=True)
136+
)
137+
138+
for permission in permissions:
139+
prefix = permission.codename.split("_")[0]
140+
default_verbose_name = camel_case_to_spaces(new_model)
141+
142+
new_codename = f"{prefix}_{new_model.lower()}"
143+
new_name = f"Can {prefix} {default_verbose_name}"
144+
145+
# Only update if changes are needed
146+
if permission.codename != new_codename or permission.name != new_name:
147+
# Save original values in case of error
148+
original_codename = permission.codename
149+
original_name = permission.name
150+
151+
permission.codename = new_codename
152+
permission.name = new_name
153+
154+
try:
155+
with transaction.atomic(using=db):
156+
permission.save(update_fields={"name", "codename"})
157+
except IntegrityError:
158+
# Skip conflicting permissions - leave them unchanged
159+
permission.codename = original_codename
160+
permission.name = original_name
161+
162+
def rename_forward(self, apps, schema_editor):
163+
self._rename(apps, schema_editor, self.old_model, self.new_model)
164+
165+
def rename_backward(self, apps, schema_editor):
166+
self._rename(apps, schema_editor, self.new_model, self.old_model)
167+
168+
169+
def rename_permissions(
170+
plan,
171+
verbosity=2,
172+
interactive=True,
173+
using=DEFAULT_DB_ALIAS,
174+
apps=global_apps,
175+
**kwargs,
176+
):
177+
"""
178+
Insert a `RenamePermissionType` operation after every planned `RenameModel`
179+
operation.
180+
"""
181+
try:
182+
Permission = apps.get_model("auth", "Permission")
183+
except LookupError:
184+
return
185+
else:
186+
if not router.allow_migrate_model(using, Permission):
187+
return
188+
189+
for migration, backward in plan:
190+
191+
inserts = []
192+
for index, operation in enumerate(migration.operations):
193+
if isinstance(operation, migrations.RenameModel):
194+
operation = RenamePermission(
195+
migration.app_label,
196+
operation.old_name,
197+
operation.new_name,
198+
)
199+
200+
inserts.append((index + 1, operation))
201+
for inserted, (index, operation) in enumerate(inserts):
202+
migration.operations.insert(inserted + index, operation)
203+
204+
111205
def get_system_username():
112206
"""
113207
Return the current system user's username, or an empty string if the
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
initial = True
10+
dependencies = []
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="OldModel",
15+
fields=[
16+
("id", models.AutoField(primary_key=True)),
17+
],
18+
),
19+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
("auth_tests", "0001_initial"),
8+
]
9+
10+
operations = [
11+
migrations.RenameModel(
12+
old_name="OldModel",
13+
new_name="NewModel",
14+
),
15+
]

tests/auth_tests/operations_migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)