Skip to content

Commit 4872c3a

Browse files
committed
update course sync
1 parent ba29b9d commit 4872c3a

File tree

16 files changed

+193
-95
lines changed

16 files changed

+193
-95
lines changed

src/ol_openedx_course_propagator/BUILD

-28
This file was deleted.

src/ol_openedx_course_propagator/__init__.py

-3
This file was deleted.

src/ol_openedx_course_propagator/models.py

-16
This file was deleted.

src/ol_openedx_course_propagator/signals.py

-41
This file was deleted.

src/ol_openedx_course_sync/BUILD

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
python_sources(
2+
name="ol_openedx_course_sync_source",
3+
dependencies=[
4+
"src/ol_openedx_course_sync/migrations:ol_openedx_course_sync_migrations",
5+
"//:external_dependencies#edx-opaque-keys",
6+
"//:external_dependencies#celery",
7+
],
8+
)
9+
10+
python_distribution(
11+
name="ol_openedx_course_sync_package",
12+
dependencies=[":ol_openedx_course_sync_source"],
13+
provides=setup_py(
14+
name="ol-openedx-course-sync",
15+
version="0.1.0",
16+
description="An edX plugin to sync changes in a course to its child courses",
17+
license="BSD-3-Clause",
18+
author="MIT Office of Digital Learning",
19+
entry_points={
20+
"lms.djangoapp": [],
21+
"cms.djangoapp": [
22+
"ol_openedx_course_sync=ol_openedx_course_sync.apps:OLOpenEdxCourseSyncConfig",
23+
],
24+
},
25+
),
26+
)

src/ol_openedx_course_propagator/README.rst renamed to src/ol_openedx_course_sync/README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
edX Username Changer
22
=======================
33

4-
A plugin to enable course updates propagation to the child/rerun courses.
4+
A plugin to enable course updates sync to the target/rerun courses.
55

66
Version Compatibility
77
---------------------
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# pylint: disable=missing-module-docstring
2+
__version__ = 0.1
3+
default_app_config = "ol_openedx_course_sync.apps.OLOpenEdxCourseSyncConfig"

src/ol_openedx_course_sync/admin.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Django admin pages for ol-openedx-course-sync plugin
3+
"""
4+
5+
from django.contrib import admin
6+
from ol_openedx_course_sync.models import CourseSyncParentOrg, CourseSyncMap
7+
8+
9+
class CourseSyncParentOrgAdmin(admin.ModelAdmin):
10+
list_display = ("organization",)
11+
12+
13+
class CourseSyncMapAdmin(admin.ModelAdmin):
14+
list_display = ("source_course", "target_courses")
15+
16+
17+
admin.site.register(CourseSyncParentOrg, CourseSyncParentOrgAdmin)
18+
admin.site.register(CourseSyncMap, CourseSyncMapAdmin)

src/ol_openedx_course_propagator/apps.py renamed to src/ol_openedx_course_sync/apps.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""
2-
App configuration for ol-openedx-course-propagator plugin
2+
App configuration for ol-openedx-course-sync plugin
33
"""
44

55
from django.apps import AppConfig
6+
from edx_django_utils.plugins import PluginSignals
7+
from openedx.core.djangoapps.plugins.constants import ProjectType
68

79

8-
class OLOpenEdxCoursePropagatorConfig(AppConfig):
9-
name = "ol_openedx_course_propagator"
10-
verbose_name = "Open edX Course Propagator"
10+
class OLOpenEdxCourseSyncConfig(AppConfig):
11+
name = "ol_openedx_course_sync"
12+
verbose_name = "Open edX Course Sync"
1113

1214
plugin_app = {
1315
PluginSignals.CONFIG: {
@@ -16,7 +18,7 @@ class OLOpenEdxCoursePropagatorConfig(AppConfig):
1618
{
1719
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_course_publish",
1820
PluginSignals.SIGNAL_PATH: "xmodule.modulestore.django.COURSE_PUBLISHED", # noqa: E501
19-
PluginSignals.DISPATCH_UID: "ol_openedx_course_propagator.signals.listen_for_course_publish", # noqa: E501
21+
PluginSignals.DISPATCH_UID: "ol_openedx_course_sync.signals.listen_for_course_publish", # noqa: E501
2022
}
2123
],
2224
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.20 on 2025-05-12 12:19
2+
3+
from django.db import migrations, models
4+
import opaque_keys.edx.django.models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
initial = True
10+
11+
dependencies = [
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='CourseSyncMap',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('source_course', opaque_keys.edx.django.models.CourseKeyField(max_length=255)),
20+
('target_courses', models.TextField(blank=True, help_text='Comma separated list of target course keys', null=True)),
21+
],
22+
),
23+
migrations.CreateModel(
24+
name='CourseSyncParentOrg',
25+
fields=[
26+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
27+
('organization', models.CharField(max_length=255, unique=True)),
28+
],
29+
),
30+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_sources(name="ol_openedx_course_sync_migrations")

src/ol_openedx_course_sync/migrations/__init__.py

Whitespace-only changes.

src/ol_openedx_course_sync/models.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.db import models
2+
from model_utils.models import TimeStampedModel
3+
from opaque_keys.edx.django.models import (
4+
CourseKeyField,
5+
)
6+
7+
class CourseSyncParentOrg(models.Model):
8+
"""
9+
Model for source course organizations
10+
11+
Any source course that is part of this organization will sync changes with the child/rerun courses.
12+
This model will help us exclude any organizations, where we don't want to sync source course.
13+
"""
14+
organization = models.CharField(max_length=255, unique=True)
15+
16+
17+
class CourseSyncMap(models.Model):
18+
"""
19+
Model to keep track of source and target courses.
20+
21+
Any changes in the source course sync with the target courses. Target courses are autopopulated
22+
for all the reruns of source courses that are part of any organization added in `CourseSyncParentOrg`.
23+
"""
24+
source_course = CourseKeyField(max_length=255)
25+
target_courses = models.TextField(
26+
null=True,
27+
blank=True,
28+
help_text="Comma separated list of target course keys"
29+
)

src/ol_openedx_course_sync/signals.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
Signal handlers ol-openedx-course-sync plugin
3+
"""
4+
import logging
5+
6+
from xmodule.modulestore.django import modulestore
7+
from xmodule.modulestore import ModuleStoreEnum
8+
from opaque_keys.edx.locator import CourseLocator
9+
from xmodule.modulestore.django import SignalHandler
10+
from django.dispatch import receiver
11+
12+
from ol_openedx_course_sync.models import CourseSyncParentOrg, CourseSyncMap
13+
from ol_openedx_course_sync.tasks import async_course_sync
14+
from django.db.models.signals import post_save
15+
from common.djangoapps.course_action_state.models import CourseRerunState
16+
17+
log = logging.getLogger(__name__)
18+
19+
20+
@receiver(SignalHandler.course_published)
21+
def listen_for_course_publish(
22+
sender, # noqa: ARG001
23+
course_key,
24+
**kwargs, # noqa: ARG001
25+
):
26+
"""
27+
Copy course content from source course to destination course.
28+
"""
29+
if not CourseSyncParentOrg.objects.filter(organization=course_key.org).exists():
30+
return
31+
32+
course_sync_map = CourseSyncMap.objects.filter(
33+
source_course=course_key
34+
).first()
35+
if course_sync_map and course_sync_map.target_courses:
36+
source_course = str(course_sync_map.source_course)
37+
user_id = 2
38+
for target_course_key in course_sync_map.target_courses.split(","):
39+
# Call the async task to copy the course content
40+
async_course_sync.delay(user_id, source_course, target_course_key)
41+
else:
42+
log.warning(
43+
"No mapping found for course %s. Skipping copy.", course_key.id
44+
)
45+
return
46+
47+
48+
@receiver(post_save, sender=CourseRerunState)
49+
def listen_for_course_rerun_state_post_save(sender, instance, **kwargs):
50+
"""
51+
Updates target courses in `CourseSyncMap`
52+
"""
53+
if not instance.state == "succeeded":
54+
return
55+
56+
if not CourseSyncParentOrg.objects.filter(organization=instance.source_course_key.org).exists():
57+
return
58+
59+
# If source key is a child of any other course, we won't make it parent
60+
if CourseSyncMap.objects.filter(target_courses__contains=str(instance.source_course_key)).exists():
61+
return
62+
63+
course_sync_map, _ = CourseSyncMap.objects.get_or_create(
64+
source_course=instance.source_course_key
65+
)
66+
target_courses = course_sync_map.target_courses.split(",") if course_sync_map.target_courses else []
67+
target_courses.append(str(instance.course_key))
68+
course_sync_map.target_courses = ",".join(target_courses)
69+
course_sync_map.save()

src/ol_openedx_course_propagator/tasks.py renamed to src/ol_openedx_course_sync/tasks.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1+
"""
2+
Tasks for the ol-openedx-course-sync plugin.
3+
"""
14
from celery import shared_task # pylint: disable=import-error
25
from celery.utils.log import get_task_logger
36
from cms.djangoapps.contentstore.git_export_utils import GitExportError, export_to_git
47
from opaque_keys.edx.keys import CourseKey
58
from xmodule.modulestore.django import modulestore
9+
from opaque_keys.edx.locator import CourseLocator
10+
from xmodule.modulestore.django import SignalHandler
611

712
LOGGER = get_task_logger(__name__)
813

914

1015
@shared_task
11-
def async_course_propagator(user_id, source_course_id, dest_course_id):
16+
def async_course_sync(user_id, source_course_id, dest_course_id):
17+
"""
18+
Syncs course content from source course to destination course.
19+
"""
1220
module_store = modulestore()
1321
source_course_key = CourseLocator.from_string(source_course_id)
1422
source_course_draft = source_course_key.for_branch("draft-branch")

0 commit comments

Comments
 (0)