Skip to content

Commit a43e13b

Browse files
committed
refactor: no, really, changing the migrations now
1 parent 5caf72c commit a43e13b

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Generated by Django 4.2.18 on 2025-03-28 02:21
2+
3+
import uuid
4+
5+
import django.db.models.deletion
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
import openedx_learning.lib.validators
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
dependencies = [
15+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16+
('oel_publishing', '0005_alter_entitylistrow_options'),
17+
]
18+
19+
operations = [
20+
migrations.CreateModel(
21+
name='DraftChangeLog',
22+
fields=[
23+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24+
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
25+
('changed_at', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
26+
('changed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
27+
('learning_package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage')),
28+
],
29+
options={
30+
'verbose_name': 'Draft Change Log',
31+
'verbose_name_plural': 'Draft Change Logs',
32+
},
33+
),
34+
migrations.CreateModel(
35+
name='DraftChangeLogRecord',
36+
fields=[
37+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
38+
('draft_change_log', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changes', to='oel_publishing.draftchangelog')),
39+
('entity', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='oel_publishing.publishableentity')),
40+
('new_version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to='oel_publishing.publishableentityversion')),
41+
('old_version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='+', to='oel_publishing.publishableentityversion')),
42+
],
43+
options={
44+
'verbose_name': 'Draft Log',
45+
'verbose_name_plural': 'Draft Log',
46+
},
47+
),
48+
migrations.CreateModel(
49+
name='DraftSideEffect',
50+
fields=[
51+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
52+
('cause', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='causes', to='oel_publishing.draftchangelogrecord')),
53+
('effect', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='caused_by', to='oel_publishing.draftchangelogrecord')),
54+
],
55+
),
56+
migrations.AddConstraint(
57+
model_name='draftsideeffect',
58+
constraint=models.UniqueConstraint(fields=('cause', 'effect'), name='oel_pub_dse_uniq_c_e'),
59+
),
60+
migrations.AddIndex(
61+
model_name='draftchangelogrecord',
62+
index=models.Index(fields=['entity', '-draft_change_log'], name='oel_dlr_idx_entity_rdcl'),
63+
),
64+
migrations.AddConstraint(
65+
model_name='draftchangelogrecord',
66+
constraint=models.UniqueConstraint(fields=('draft_change_log', 'entity'), name='oel_dlr_uniq_dcl'),
67+
),
68+
]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Bootstrap DraftChangeLogs
3+
4+
DraftChangeLog and DraftChangeLogRecord are being introduced after Drafts, so
5+
we're going to retroactively make entries for all the changes that were in our
6+
Learning Packages.
7+
8+
This migration will try to reconstruct create, edit, reset-to-published, and
9+
delete operations, but it won't be fully accurate because we only have the
10+
create dates of the versions and the current state of active Drafts to go on.
11+
This means we won't accurately capture when things were deleted and then reset,
12+
or when reset and then later edited. We're also missing the user for a number of
13+
these operations, so we'll add those with null created_by entries. Addressing
14+
these gaps is a big part of why we created DraftChangeLogs in the first place.
15+
"""
16+
# Generated by Django 4.2.18 on 2025-03-13 10:29
17+
import logging
18+
from datetime import datetime, timezone
19+
20+
from django.db import migrations
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
def bootstrap_draft_change_logs(apps, schema_editor):
26+
"""
27+
Create a fake DraftChangeSet that encompasses the state of current Drafts.
28+
"""
29+
LearningPackage = apps.get_model("oel_publishing", "LearningPackage")
30+
PublishableEntityVersion = apps.get_model("oel_publishing", "PublishableEntityVersion")
31+
32+
Draft = apps.get_model("oel_publishing", "Draft")
33+
DraftChangeLogRecord = apps.get_model("oel_publishing", "DraftChangeLogRecord")
34+
DraftChangeLog = apps.get_model("oel_publishing", "DraftChangeLog")
35+
now = datetime.now(tz=timezone.utc)
36+
37+
for learning_package in LearningPackage.objects.all().order_by("key"):
38+
logger.info(f"Creating bootstrap DraftChangeLogs for {learning_package.key}")
39+
pub_ent_versions = PublishableEntityVersion.objects.filter(
40+
entity__learning_package=learning_package
41+
).select_related("entity")
42+
43+
# First cycle though all the simple create/edit operations...
44+
last_version_seen = {} # PublishableEntity.id -> PublishableEntityVersion.id
45+
for pub_ent_version in pub_ent_versions.order_by("pk"):
46+
draft_change_log = DraftChangeLog.objects.create(
47+
learning_package=learning_package,
48+
changed_at=pub_ent_version.created,
49+
changed_by=pub_ent_version.created_by,
50+
)
51+
DraftChangeLogRecord.objects.create(
52+
draft_change_log=draft_change_log,
53+
entity=pub_ent_version.entity,
54+
old_version_id=last_version_seen.get(pub_ent_version.entity.id),
55+
new_version_id=pub_ent_version.id,
56+
)
57+
last_version_seen[pub_ent_version.entity.id] = pub_ent_version.id
58+
59+
# Now that we've created change sets for create/edit operations, we look
60+
# at the latest state of the Draft model in order to determine whether
61+
# we need to apply deletes or resets.
62+
for draft in Draft.objects.filter(entity__learning_package=learning_package).order_by("entity_id"):
63+
last_version_id = last_version_seen.get(draft.entity_id)
64+
if draft.version_id == last_version_id:
65+
continue
66+
# We don't really know who did this or when, so we use None and now.
67+
draft_change_log = DraftChangeLog.objects.create(
68+
learning_package=learning_package,
69+
changed_at=now,
70+
changed_by=None,
71+
)
72+
DraftChangeLogRecord.objects.create(
73+
draft_change_log=draft_change_log,
74+
entity_id=draft.entity_id,
75+
old_version_id=last_version_id,
76+
new_version_id=draft.version_id,
77+
)
78+
79+
80+
def delete_draft_change_logs(apps, schema_editor):
81+
logger.info(f"Deleting all DraftChangeLogs (reverse migration)")
82+
DraftChangeLog = apps.get_model("oel_publishing", "DraftChangeLog")
83+
DraftChangeLog.objects.all().delete()
84+
85+
86+
class Migration(migrations.Migration):
87+
88+
dependencies = [
89+
('oel_publishing', '0006_draftchangelog'),
90+
]
91+
92+
operations = [
93+
migrations.RunPython(bootstrap_draft_change_logs, reverse_code=delete_draft_change_logs)
94+
]

0 commit comments

Comments
 (0)