Skip to content

Commit 29051e6

Browse files
Squash cms migrations to final state; DRY menus and test factories
First-pass consolidation before anything ships: 27 migrations across four apps collapse to 6 that express only the end state — three regenerated initials (authapi, content, datastore), authapi/0002_provision_roles (the three groups and every grant), content/0002_provision_site (locales, index pages, home rename, admin-approval workflow), and content/0003_import_legacy_content (the reversible tiptap wrapper). The galleries app is deleted outright — nothing needs its migration history once the graph is rebuilt — along with the gallery_slug_choices stub kept only for a serialized migration reference. The intermediate data migrations (editor/reviewer consolidation, TeamMapGroup conversion, gallerySlug folding) exist nowhere fresh installs can need them; the dev database was rebooked with --fake and its stale galleries content-type rows purged. The full test suite rebuilds the fresh chain every run. DRY: GroupMenuItem moves to core/menu.py (moderation + content share it); the per-module test factory copies (PASSWORD, make_user, make_admin_user, make_team, make_portal, create_mirror_tables) consolidate into core/testing.py; migration-number prose updated to the squashed names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f7be9f8 commit 29051e6

51 files changed

Lines changed: 536 additions & 4355 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cms/authapi/migrations/0001_create_groups.py

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 5.2.15 on 2026-08-06 22:47
2+
3+
import django.db.models.deletion
4+
import modelcluster.fields
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
('datastore', '0001_initial'),
15+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name='Team',
21+
fields=[
22+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23+
('name', models.CharField(max_length=255, unique=True)),
24+
('slug', models.SlugField(help_text="Stable identifier, minted into members' JWT `teams` claim. Changing it revokes group_only gallery access until re-login.", max_length=255, unique=True)),
25+
],
26+
options={
27+
'ordering': ['name'],
28+
},
29+
),
30+
migrations.CreateModel(
31+
name='TeamDistrictrMap',
32+
fields=[
33+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34+
('districtr_map', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='team_links', to='datastore.districtrmap')),
35+
('team', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='districtr_maps', to='authapi.team')),
36+
],
37+
options={
38+
'unique_together': {('team', 'districtr_map')},
39+
},
40+
),
41+
migrations.CreateModel(
42+
name='TeamMembership',
43+
fields=[
44+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
45+
('team', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='authapi.team')),
46+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to=settings.AUTH_USER_MODEL)),
47+
],
48+
options={
49+
'unique_together': {('team', 'user')},
50+
},
51+
),
52+
]
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""
2+
Provision the three roles (final state, squashed 2026-08-06 from the
3+
iterative authapi/content/datastore/galleries grant migrations of the
4+
first-pass branch — no deployment ever ran the intermediate states):
5+
6+
- ``admin``: Wagtail admin access; full page permissions on the root
7+
(add/change/publish/lock/unlock); every datastore model permission; the
8+
Team-management model permissions.
9+
- ``partner``: admin access; root ``add_page`` only — own-content editing
10+
via Wagtail's owner model, publishing via the admin-approval workflow
11+
(content/0002). Comment moderation comes from scopes (authapi/scopes.py),
12+
not Django permissions.
13+
- ``super_partner``: everything partner has, plus the map-module tool
14+
permissions — DistrictrMap/Overlay/DistrictrMapOverlays add+change+view
15+
and GerryDBTable view. GPKG import needs ``add_gerrydbtable``: admin only.
16+
17+
Reverse deletes the groups (cascading their grants and page permissions).
18+
"""
19+
20+
from django.db import migrations
21+
22+
from core.migration_utils import ensure_permissions, model_permissions
23+
24+
GROUPS = ["admin", "partner", "super_partner"]
25+
26+
ADMIN_PAGE_PERMS = ["add_page", "change_page", "publish_page", "lock_page", "unlock_page"]
27+
PARTNER_PAGE_PERMS = ["add_page"]
28+
29+
SUPER_PARTNER_DATASTORE_GRANTS = {
30+
"districtrmap": ["add", "change", "view"],
31+
"overlay": ["add", "change", "view"],
32+
"districtrmapoverlays": ["add", "change", "view"],
33+
"gerrydbtable": ["view"],
34+
}
35+
36+
TEAM_MODELS = ["team", "teammembership", "teamdistrictrmap"]
37+
38+
39+
def _page_permissions(apps, codenames):
40+
Permission = apps.get_model("auth", "Permission")
41+
return Permission.objects.filter(
42+
content_type__app_label="wagtailcore",
43+
content_type__model="page",
44+
codename__in=codenames,
45+
)
46+
47+
48+
def provision_roles(apps, schema_editor):
49+
# post_migrate hasn't fired on a fresh database: materialize the
50+
# Permission rows this migration grants (core/migration_utils docs the
51+
# footgun), including wagtailcore's custom publish/lock/unlock page perms.
52+
for app_label in ("wagtailcore", "datastore", "authapi"):
53+
ensure_permissions(app_label, apps, schema_editor)
54+
55+
Group = apps.get_model("auth", "Group")
56+
Permission = apps.get_model("auth", "Permission")
57+
Page = apps.get_model("wagtailcore", "Page")
58+
GroupPagePermission = apps.get_model("wagtailcore", "GroupPagePermission")
59+
60+
groups = {name: Group.objects.get_or_create(name=name)[0] for name in GROUPS}
61+
62+
access_admin = Permission.objects.get(
63+
content_type__app_label="wagtailadmin", codename="access_admin"
64+
)
65+
for group in groups.values():
66+
group.permissions.add(access_admin)
67+
68+
# Page permissions are tree-scoped GroupPagePermission rows on the root
69+
# (id=1, wagtailcore.0002_initial_data), NOT Django model permissions.
70+
root = Page.objects.get(pk=1)
71+
grants = {
72+
"admin": ADMIN_PAGE_PERMS,
73+
"partner": PARTNER_PAGE_PERMS,
74+
"super_partner": PARTNER_PAGE_PERMS,
75+
}
76+
for name, codenames in grants.items():
77+
for permission in _page_permissions(apps, codenames):
78+
GroupPagePermission.objects.get_or_create(
79+
group=groups[name], page=root, permission=permission
80+
)
81+
82+
groups["admin"].permissions.add(*model_permissions(apps, "datastore"))
83+
for model, actions in SUPER_PARTNER_DATASTORE_GRANTS.items():
84+
groups["super_partner"].permissions.add(
85+
*model_permissions(apps, "datastore", model=model).filter(
86+
codename__in=[f"{action}_{model}" for action in actions]
87+
)
88+
)
89+
90+
for model in TEAM_MODELS:
91+
groups["admin"].permissions.add(
92+
*model_permissions(apps, "authapi", model=model)
93+
)
94+
95+
96+
def remove_roles(apps, schema_editor):
97+
Group = apps.get_model("auth", "Group")
98+
Group.objects.filter(name__in=GROUPS).delete()
99+
100+
101+
class Migration(migrations.Migration):
102+
dependencies = [
103+
("authapi", "0001_initial"),
104+
("datastore", "0001_initial"),
105+
("auth", "0012_alter_user_first_name_max_length"),
106+
("contenttypes", "0002_remove_content_type_name"),
107+
("wagtailadmin", "0001_create_admin_access_permissions"),
108+
# 0002_initial_data for the root page; 0094 pins the modern
109+
# GroupPagePermission shape (permission FK, not permission_type).
110+
("wagtailcore", "0094_alter_page_locale"),
111+
]
112+
113+
operations = [
114+
migrations.RunPython(provision_roles, remove_roles),
115+
]

cms/authapi/migrations/0002_reviewtagassignment.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

cms/authapi/migrations/0003_grant_admin_group_permissions.py

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)