-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat: add data migration to sync DiscussionsConfiguration with modulestore tab.is_hidden #38294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| """ | ||
| Sync DiscussionsConfiguration.enabled and CourseAppStatus.enabled with | ||
| CourseOverviewTab.is_hidden. | ||
|
|
||
| When a course is imported, the discussion tab's is_hidden value is carried over | ||
| from the source course. However, DiscussionsConfiguration.enabled and | ||
| CourseAppStatus.enabled default to True and are not updated from the imported | ||
| tab state, causing a desync. | ||
|
|
||
| This migration reads each discussion tab from CourseOverviewTab (a DB cache of | ||
| course tabs) and sets both DiscussionsConfiguration.enabled and | ||
| CourseAppStatus.enabled to NOT is_hidden. | ||
| """ | ||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| def sync_enabled_from_course_overview_tab(apps, schema_editor): | ||
| CourseOverviewTab = apps.get_model("course_overviews", "CourseOverviewTab") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glad that the CourseOverviewTab model works! Thanks for the suggestion, Dave. |
||
| DiscussionsConfiguration = apps.get_model("discussions", "DiscussionsConfiguration") | ||
| CourseAppStatus = apps.get_model("course_apps", "CourseAppStatus") | ||
|
|
||
| discussion_tabs = CourseOverviewTab.objects.filter(tab_id="discussion").select_related("course_overview") | ||
|
|
||
| for tab in discussion_tabs.iterator(): | ||
| course_key = tab.course_overview_id | ||
| expected_enabled = not tab.is_hidden | ||
| DiscussionsConfiguration.objects.filter( | ||
| context_key=course_key, | ||
| ).exclude( | ||
| enabled=expected_enabled, | ||
| ).update(enabled=expected_enabled) | ||
| CourseAppStatus.objects.filter( | ||
| course_key=course_key, | ||
| app_id="discussion", | ||
| ).exclude( | ||
| enabled=expected_enabled, | ||
| ).update(enabled=expected_enabled) | ||
|
Comment on lines
+33
to
+38
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, there are so many layers 😵 @Anas12091101 When a course is imported, does |
||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("discussions", "0018_auto_20230904_1054"), | ||
| ("course_overviews", "0030_backfill_new_catalog_courseruns"), | ||
| ("course_apps", "0002_alter_historicalcourseappstatus_options"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython(sync_enabled_from_course_overview_tab, migrations.RunPython.noop), | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kdmccormick, you’re absolutely right. Here’s what’s actually happening:
When a course is published (including after an import), two independent Celery tasks are triggered by the
course_publishedsignal. The first isupdate_discussions_settings_from_course_task, which reads the course tabs from the modulestore, derivesenabled = not tab.is_hiddenfor the discussion tab, and passes it throughCourseDiscussionConfigurationData. TheCOURSE_DISCUSSIONS_CHANGEDevent fires, and the handler updatesDiscussionsConfiguration.enabledin the database.The second is
update_course_apps_status, which iterates over all available course apps and calls each app'sis_enabled()method. For the discussion app,DiscussionCourseApp.is_enabled()callsDiscussionsConfiguration.is_enabled(course_key), reading directly from theDiscussionsConfigurationmodel. It then writes the result intoCourseAppStatusviaupdate_status_for_course_app().The Pages & Resources MFE calls
GET /api/course_apps/v1/apps/{course_id}. The serializer first checksCourseAppStatus(bulk-loaded viaget_all_app_status_data_for_course). If no row exists, it falls back tois_course_app_enabled(), which triesCourseAppStatus.objects.get(course_key, app_id)and returns itsenabledvalue if found, otherwise callsDiscussionCourseApp.is_enabled()which reads fromDiscussionsConfiguration.is_enabled().Locally, this was working because
CELERY_ALWAYS_EAGER=True. However, in production with Celery workers,update_discussions_settings_from_course_taskis triggered afterupdate_course_apps_statusdue to theCOURSE_PUBLISH_TASK_DELAY. As a result, update_course_apps_status ends up using stale values from DiscussionsConfiguration, so the data doesn’t get synced as expected.I have added the code here. We have 2 scenarios:
New course: No
CourseAppStatusrow exists yet. The handler creates one with the correct enabled value.Old course: A
CourseAppStatusrow already exists (created by update_course_apps_status on a previous publish, or byinitialize_course_app_statuson first MFE page load). The handler updates it.