ol-openedx-course-sync 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ol-openedx-course-sync-0.1.0/MANIFEST.in +1 -0
- ol-openedx-course-sync-0.1.0/PKG-INFO +7 -0
- ol-openedx-course-sync-0.1.0/__init__.py +0 -0
- ol-openedx-course-sync-0.1.0/backend_shim.py +31 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/__init__.py +5 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/admin.py +90 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/apps.py +30 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/constants.py +5 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/migrations/0001_initial.py +54 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/migrations/__init__.py +0 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/models.py +101 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/signals.py +80 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/tasks.py +57 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync/utils.py +27 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/PKG-INFO +7 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/SOURCES.txt +21 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/dependency_links.txt +1 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/entry_points.txt +2 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/namespace_packages.txt +1 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/requires.txt +3 -0
- ol-openedx-course-sync-0.1.0/ol_openedx_course_sync.egg-info/top_level.txt +2 -0
- ol-openedx-course-sync-0.1.0/setup.cfg +4 -0
- ol-openedx-course-sync-0.1.0/setup.py +33 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include *.py
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
# DO NOT EDIT THIS FILE -- AUTOGENERATED BY PANTS
|
|
3
|
+
|
|
4
|
+
import errno
|
|
5
|
+
import os
|
|
6
|
+
import setuptools.build_meta
|
|
7
|
+
|
|
8
|
+
backend = setuptools.build_meta.__legacy__
|
|
9
|
+
|
|
10
|
+
dist_dir = "dist/"
|
|
11
|
+
build_wheel = True
|
|
12
|
+
build_sdist = True
|
|
13
|
+
wheel_config_settings = {
|
|
14
|
+
}
|
|
15
|
+
sdist_config_settings = {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Python 2.7 doesn't have the exist_ok arg on os.makedirs().
|
|
19
|
+
try:
|
|
20
|
+
os.makedirs(dist_dir)
|
|
21
|
+
except OSError as e:
|
|
22
|
+
if e.errno != errno.EEXIST:
|
|
23
|
+
raise
|
|
24
|
+
|
|
25
|
+
wheel_path = backend.build_wheel(dist_dir, wheel_config_settings) if build_wheel else None
|
|
26
|
+
sdist_path = backend.build_sdist(dist_dir, sdist_config_settings) if build_sdist else None
|
|
27
|
+
|
|
28
|
+
if wheel_path:
|
|
29
|
+
print("wheel: {wheel_path}".format(wheel_path=wheel_path))
|
|
30
|
+
if sdist_path:
|
|
31
|
+
print("sdist: {sdist_path}".format(sdist_path=sdist_path))
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django admin for ol-openedx-course-sync plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from django import forms
|
|
8
|
+
from django.contrib import admin
|
|
9
|
+
from ol_openedx_course_sync.models import CourseSyncMapping, CourseSyncOrganization
|
|
10
|
+
from ol_openedx_course_sync.tasks import async_course_sync
|
|
11
|
+
from organizations.models import Organization
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CourseSyncOrganizationForm(forms.ModelForm):
|
|
17
|
+
"""
|
|
18
|
+
Form for CourseSyncOrganization model
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
class Meta:
|
|
22
|
+
model = CourseSyncOrganization
|
|
23
|
+
fields = "__all__" # noqa: DJ007
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
org_choices = [
|
|
28
|
+
(org.name, org.name) for org in Organization.objects.filter(active=True)
|
|
29
|
+
]
|
|
30
|
+
self.fields["organization"] = forms.ChoiceField(choices=org_choices)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CourseSyncOrganizationAdmin(admin.ModelAdmin):
|
|
34
|
+
"""
|
|
35
|
+
Admin for CourseSyncOrganization model
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
form = CourseSyncOrganizationForm
|
|
39
|
+
list_display = ("organization", "is_active")
|
|
40
|
+
|
|
41
|
+
def has_delete_permission(self, request, obj=None):
|
|
42
|
+
"""
|
|
43
|
+
Disable delete permission if CourseSyncMapping is not clean
|
|
44
|
+
"""
|
|
45
|
+
if obj and not obj.can_be_deleted():
|
|
46
|
+
return False
|
|
47
|
+
return super().has_delete_permission(request, obj)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CourseSyncMappingAdmin(admin.ModelAdmin):
|
|
51
|
+
"""
|
|
52
|
+
Admin for CourseSyncMapping model
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
list_display = ("source_course", "target_course", "is_active")
|
|
56
|
+
search_fields = ("source_course", "target_course")
|
|
57
|
+
list_filter = ("is_active",)
|
|
58
|
+
actions = ("sync_course_content",)
|
|
59
|
+
|
|
60
|
+
def get_readonly_fields(self, request, obj=None): # noqa: ARG002
|
|
61
|
+
"""
|
|
62
|
+
Make source_course readonly if object already exists.
|
|
63
|
+
"""
|
|
64
|
+
if obj:
|
|
65
|
+
return (*self.readonly_fields, "source_course")
|
|
66
|
+
return self.readonly_fields
|
|
67
|
+
|
|
68
|
+
@admin.action(description="Sync Course Content")
|
|
69
|
+
def sync_course_content(self, request, queryset):
|
|
70
|
+
"""
|
|
71
|
+
Sync course content for selected CourseSyncMapping(s)
|
|
72
|
+
"""
|
|
73
|
+
for course_sync_mapping in queryset:
|
|
74
|
+
log.info(
|
|
75
|
+
"Initializing course content sync through admin actions from %s to %s",
|
|
76
|
+
course_sync_mapping.source_course,
|
|
77
|
+
course_sync_mapping.target_course,
|
|
78
|
+
)
|
|
79
|
+
async_course_sync.delay(
|
|
80
|
+
str(course_sync_mapping.source_course),
|
|
81
|
+
str(course_sync_mapping.target_course),
|
|
82
|
+
)
|
|
83
|
+
self.message_user(
|
|
84
|
+
request,
|
|
85
|
+
"Course sync started",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
admin.site.register(CourseSyncOrganization, CourseSyncOrganizationAdmin)
|
|
90
|
+
admin.site.register(CourseSyncMapping, CourseSyncMappingAdmin)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App configuration for ol-openedx-course-sync plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from edx_django_utils.plugins import PluginSignals
|
|
7
|
+
from openedx.core.djangoapps.plugins.constants import ProjectType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OLOpenEdxCourseSyncConfig(AppConfig):
|
|
11
|
+
"""
|
|
12
|
+
App configuration for the ol-openedx-course-sync app.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "ol_openedx_course_sync"
|
|
16
|
+
verbose_name = "Open edX Course Sync"
|
|
17
|
+
|
|
18
|
+
plugin_app = {
|
|
19
|
+
PluginSignals.CONFIG: {
|
|
20
|
+
ProjectType.CMS: {
|
|
21
|
+
PluginSignals.RECEIVERS: [
|
|
22
|
+
{
|
|
23
|
+
PluginSignals.RECEIVER_FUNC_NAME: "listen_for_course_publish",
|
|
24
|
+
PluginSignals.SIGNAL_PATH: "xmodule.modulestore.django.COURSE_PUBLISHED", # noqa: E501
|
|
25
|
+
PluginSignals.DISPATCH_UID: "ol_openedx_course_sync.signals.listen_for_course_publish", # noqa: E501
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated by Django 4.2.20 on 2025-05-23 09:28
|
|
2
|
+
|
|
3
|
+
import opaque_keys.edx.django.models
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [] # type: ignore[var-annotated]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.CreateModel(
|
|
14
|
+
name="CourseSyncMapping",
|
|
15
|
+
fields=[
|
|
16
|
+
(
|
|
17
|
+
"id",
|
|
18
|
+
models.AutoField(
|
|
19
|
+
auto_created=True,
|
|
20
|
+
primary_key=True,
|
|
21
|
+
serialize=False,
|
|
22
|
+
verbose_name="ID",
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
(
|
|
26
|
+
"source_course",
|
|
27
|
+
opaque_keys.edx.django.models.CourseKeyField(max_length=255),
|
|
28
|
+
),
|
|
29
|
+
(
|
|
30
|
+
"target_course",
|
|
31
|
+
opaque_keys.edx.django.models.CourseKeyField(
|
|
32
|
+
max_length=255, unique=True
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
("is_active", models.BooleanField(default=True)),
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
migrations.CreateModel(
|
|
39
|
+
name="CourseSyncOrganization",
|
|
40
|
+
fields=[
|
|
41
|
+
(
|
|
42
|
+
"id",
|
|
43
|
+
models.AutoField(
|
|
44
|
+
auto_created=True,
|
|
45
|
+
primary_key=True,
|
|
46
|
+
serialize=False,
|
|
47
|
+
verbose_name="ID",
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
("organization", models.CharField(max_length=255, unique=True)),
|
|
51
|
+
("is_active", models.BooleanField(default=True)),
|
|
52
|
+
],
|
|
53
|
+
),
|
|
54
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models for ol-openedx-course-sync plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.core.exceptions import ValidationError
|
|
6
|
+
from django.db import models
|
|
7
|
+
from opaque_keys.edx.django.models import (
|
|
8
|
+
CourseKeyField,
|
|
9
|
+
)
|
|
10
|
+
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CourseSyncOrganization(models.Model):
|
|
14
|
+
"""
|
|
15
|
+
Model for source course organizations
|
|
16
|
+
|
|
17
|
+
Any course that is part of this organization
|
|
18
|
+
will sync the content changes to the target/rerun courses.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
organization = models.CharField(max_length=255, unique=True)
|
|
22
|
+
is_active = models.BooleanField(default=True)
|
|
23
|
+
|
|
24
|
+
class Meta:
|
|
25
|
+
app_label = "ol_openedx_course_sync"
|
|
26
|
+
|
|
27
|
+
def __str__(self):
|
|
28
|
+
return f"{self.organization} Course Sync Organization"
|
|
29
|
+
|
|
30
|
+
def delete(self, *args, **kwargs):
|
|
31
|
+
"""
|
|
32
|
+
Override delete method to perform custom validations.
|
|
33
|
+
"""
|
|
34
|
+
if not self.can_be_deleted():
|
|
35
|
+
raise ValidationError( # noqa: TRY003
|
|
36
|
+
"Cannot delete organization with existing CourseSyncMapping objects." # noqa: EM101
|
|
37
|
+
)
|
|
38
|
+
super().delete(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
def can_be_deleted(self):
|
|
41
|
+
"""
|
|
42
|
+
Check if the organization can be deleted.
|
|
43
|
+
"""
|
|
44
|
+
return not CourseSyncMapping.objects.filter(
|
|
45
|
+
models.Q(source_course__contains=self.organization)
|
|
46
|
+
| models.Q(target_course__contains=self.organization)
|
|
47
|
+
).exists()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CourseSyncMapping(models.Model):
|
|
51
|
+
"""
|
|
52
|
+
Model to keep track of source and target courses.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
source_course = CourseKeyField(max_length=255)
|
|
56
|
+
target_course = CourseKeyField(max_length=255, unique=True)
|
|
57
|
+
is_active = models.BooleanField(default=True)
|
|
58
|
+
|
|
59
|
+
class Meta:
|
|
60
|
+
app_label = "ol_openedx_course_sync"
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return f"{self.source_course} Course Sync Mapping"
|
|
64
|
+
|
|
65
|
+
def save(self, *args, **kwargs):
|
|
66
|
+
"""
|
|
67
|
+
Override save method to perform custom validations.
|
|
68
|
+
"""
|
|
69
|
+
self.full_clean()
|
|
70
|
+
super().save(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
def clean(self):
|
|
73
|
+
"""
|
|
74
|
+
Override clean method to perform custom validations.
|
|
75
|
+
"""
|
|
76
|
+
super().clean()
|
|
77
|
+
|
|
78
|
+
if not CourseOverview.objects.filter(id=self.source_course).exists():
|
|
79
|
+
raise ValidationError({"source_course": "Source course does not exist"})
|
|
80
|
+
|
|
81
|
+
if not CourseOverview.objects.filter(id=self.target_course).exists():
|
|
82
|
+
raise ValidationError({"target_course": "Target course does not exist"})
|
|
83
|
+
|
|
84
|
+
conflicting_target = CourseSyncMapping.objects.filter(
|
|
85
|
+
target_course=self.source_course
|
|
86
|
+
).first()
|
|
87
|
+
if conflicting_target:
|
|
88
|
+
raise ValidationError(
|
|
89
|
+
{
|
|
90
|
+
"source_course": f"This course is already used as target course of: " # noqa: E501
|
|
91
|
+
f"{conflicting_target.source_course}"
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
conflicting_source = CourseSyncMapping.objects.filter(
|
|
96
|
+
source_course=self.target_course
|
|
97
|
+
).first()
|
|
98
|
+
if conflicting_source:
|
|
99
|
+
raise ValidationError(
|
|
100
|
+
{"target_course": "This course is already a source course"}
|
|
101
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signal handlers for ol-openedx-course-sync plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from common.djangoapps.course_action_state.models import CourseRerunState
|
|
8
|
+
from django.core.exceptions import ValidationError
|
|
9
|
+
from django.db.models.signals import post_save
|
|
10
|
+
from django.dispatch import receiver
|
|
11
|
+
from ol_openedx_course_sync.constants import COURSE_RERUN_STATE_SUCCEEDED
|
|
12
|
+
from ol_openedx_course_sync.models import CourseSyncMapping, CourseSyncOrganization
|
|
13
|
+
from ol_openedx_course_sync.tasks import async_course_sync
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def listen_for_course_publish(
|
|
19
|
+
sender, # noqa: ARG001
|
|
20
|
+
course_key,
|
|
21
|
+
**kwargs, # noqa: ARG001
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Listen for course publish signal and trigger course sync task
|
|
25
|
+
"""
|
|
26
|
+
if not CourseSyncOrganization.objects.filter(
|
|
27
|
+
organization=course_key.org, is_active=True
|
|
28
|
+
).exists():
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
course_sync_mappings = CourseSyncMapping.objects.filter(
|
|
32
|
+
source_course=course_key, is_active=True
|
|
33
|
+
)
|
|
34
|
+
if not course_sync_mappings:
|
|
35
|
+
log.info("No mapping found for course %s. Skipping sync.", str(course_key))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
for course_sync_mapping in course_sync_mappings:
|
|
39
|
+
log.info(
|
|
40
|
+
"Initializing course content sync from %s to %s",
|
|
41
|
+
course_sync_mapping.source_course,
|
|
42
|
+
course_sync_mapping.target_course,
|
|
43
|
+
)
|
|
44
|
+
async_course_sync.delay(
|
|
45
|
+
str(course_sync_mapping.source_course),
|
|
46
|
+
str(course_sync_mapping.target_course),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@receiver(post_save, sender=CourseRerunState)
|
|
51
|
+
def listen_for_course_rerun_state_post_save(sender, instance, **kwargs): # noqa: ARG001
|
|
52
|
+
"""
|
|
53
|
+
Listen for `CourseRerunState` post_save and
|
|
54
|
+
create target courses in `CourseSyncMapping`
|
|
55
|
+
"""
|
|
56
|
+
if instance.state != COURSE_RERUN_STATE_SUCCEEDED:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if not CourseSyncOrganization.objects.filter(
|
|
60
|
+
organization=instance.source_course_key.org, is_active=True
|
|
61
|
+
).exists():
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
course_sync_mapping = CourseSyncMapping.objects.create(
|
|
66
|
+
source_course=instance.source_course_key,
|
|
67
|
+
target_course=instance.course_key,
|
|
68
|
+
)
|
|
69
|
+
except ValidationError:
|
|
70
|
+
log.exception(
|
|
71
|
+
"Failed to create CourseSyncMapping for %s",
|
|
72
|
+
instance.source_course_key,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
# Trigger course sync to sync the published changes.
|
|
76
|
+
# When a course clone or rerun is created, published changes are not synced.
|
|
77
|
+
async_course_sync.delay(
|
|
78
|
+
str(course_sync_mapping.source_course),
|
|
79
|
+
str(course_sync_mapping.target_course),
|
|
80
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tasks for the ol-openedx-course-sync plugin.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from celery import shared_task # pylint: disable=import-error
|
|
6
|
+
from celery.utils.log import get_task_logger
|
|
7
|
+
from celery_utils.persist_on_failure import LoggedPersistOnFailureTask
|
|
8
|
+
from ol_openedx_course_sync.apps import OLOpenEdxCourseSyncConfig
|
|
9
|
+
from ol_openedx_course_sync.utils import copy_course_content
|
|
10
|
+
from opaque_keys.edx.locator import CourseLocator
|
|
11
|
+
from xmodule.modulestore import ModuleStoreEnum
|
|
12
|
+
from xmodule.modulestore.django import SignalHandler
|
|
13
|
+
|
|
14
|
+
logger = get_task_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@shared_task(
|
|
18
|
+
base=LoggedPersistOnFailureTask,
|
|
19
|
+
autoretry_for=(Exception,),
|
|
20
|
+
max_retries=3,
|
|
21
|
+
default_retry_delay=30,
|
|
22
|
+
)
|
|
23
|
+
def async_course_sync(source_course_id, dest_course_id):
|
|
24
|
+
"""
|
|
25
|
+
Sync course content from source course to destination course.
|
|
26
|
+
"""
|
|
27
|
+
logger.info("Starting course sync from %s to %s", source_course_id, dest_course_id)
|
|
28
|
+
source_course_key = CourseLocator.from_string(source_course_id)
|
|
29
|
+
dest_course_key = CourseLocator.from_string(dest_course_id)
|
|
30
|
+
|
|
31
|
+
logger.info(
|
|
32
|
+
"Copying draft course content from %s to %s", source_course_key, dest_course_key
|
|
33
|
+
)
|
|
34
|
+
# Copy draft branch content
|
|
35
|
+
copy_course_content(
|
|
36
|
+
source_course_key, dest_course_key, ModuleStoreEnum.BranchName.draft
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger.info(
|
|
40
|
+
"Copying published course content from %s to %s",
|
|
41
|
+
source_course_key,
|
|
42
|
+
dest_course_key,
|
|
43
|
+
)
|
|
44
|
+
# copy published branch content
|
|
45
|
+
copy_course_content(
|
|
46
|
+
source_course_key,
|
|
47
|
+
dest_course_key,
|
|
48
|
+
ModuleStoreEnum.BranchName.published,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# trigger course publish signal to trigger outline and relevant updates
|
|
52
|
+
SignalHandler.course_published.send(
|
|
53
|
+
sender=OLOpenEdxCourseSyncConfig, course_key=dest_course_key
|
|
54
|
+
)
|
|
55
|
+
logger.debug(
|
|
56
|
+
"Finished course sync from %s to %s", source_course_key, dest_course_key
|
|
57
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for the ol-openedx-course-sync plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from xmodule.modulestore.django import modulestore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def copy_course_content(source_course_key, target_course_key, branch):
|
|
9
|
+
"""
|
|
10
|
+
Copy course content from source_course to target_course
|
|
11
|
+
on the specified branch.
|
|
12
|
+
"""
|
|
13
|
+
module_store = modulestore()
|
|
14
|
+
subtree_list = [module_store.make_course_usage_key(source_course_key)]
|
|
15
|
+
source_course_key_for_branch = source_course_key.for_branch(branch)
|
|
16
|
+
target_course_key_for_branch = target_course_key.for_branch(branch)
|
|
17
|
+
|
|
18
|
+
source_modulestore = module_store._get_modulestore_for_courselike(source_course_key) # noqa: SLF001
|
|
19
|
+
target_modulestore = module_store._get_modulestore_for_courselike(target_course_key) # noqa: SLF001
|
|
20
|
+
if source_modulestore == target_modulestore:
|
|
21
|
+
user_id = None
|
|
22
|
+
source_modulestore.copy(
|
|
23
|
+
user_id,
|
|
24
|
+
source_course_key_for_branch,
|
|
25
|
+
target_course_key_for_branch,
|
|
26
|
+
subtree_list,
|
|
27
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
__init__.py
|
|
3
|
+
backend_shim.py
|
|
4
|
+
setup.py
|
|
5
|
+
ol_openedx_course_sync/__init__.py
|
|
6
|
+
ol_openedx_course_sync/admin.py
|
|
7
|
+
ol_openedx_course_sync/apps.py
|
|
8
|
+
ol_openedx_course_sync/constants.py
|
|
9
|
+
ol_openedx_course_sync/models.py
|
|
10
|
+
ol_openedx_course_sync/signals.py
|
|
11
|
+
ol_openedx_course_sync/tasks.py
|
|
12
|
+
ol_openedx_course_sync/utils.py
|
|
13
|
+
ol_openedx_course_sync.egg-info/PKG-INFO
|
|
14
|
+
ol_openedx_course_sync.egg-info/SOURCES.txt
|
|
15
|
+
ol_openedx_course_sync.egg-info/dependency_links.txt
|
|
16
|
+
ol_openedx_course_sync.egg-info/entry_points.txt
|
|
17
|
+
ol_openedx_course_sync.egg-info/namespace_packages.txt
|
|
18
|
+
ol_openedx_course_sync.egg-info/requires.txt
|
|
19
|
+
ol_openedx_course_sync.egg-info/top_level.txt
|
|
20
|
+
ol_openedx_course_sync/migrations/0001_initial.py
|
|
21
|
+
ol_openedx_course_sync/migrations/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
# DO NOT EDIT THIS FILE -- AUTOGENERATED BY PANTS
|
|
3
|
+
# Target: src/ol_openedx_course_sync:ol_openedx_course_sync_package
|
|
4
|
+
|
|
5
|
+
from setuptools import setup
|
|
6
|
+
|
|
7
|
+
setup(**{
|
|
8
|
+
'author': 'MIT Office of Digital Learning',
|
|
9
|
+
'description': 'An Open edX plugin to sync course changes to its reruns.',
|
|
10
|
+
'entry_points': {
|
|
11
|
+
'cms.djangoapp': [
|
|
12
|
+
'ol_openedx_course_sync = ol_openedx_course_sync.apps:OLOpenEdxCourseSyncConfig',
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
'install_requires': (
|
|
16
|
+
'Django>2.0',
|
|
17
|
+
'celery>=4.4.7',
|
|
18
|
+
'edx-django-utils>4.0.0',
|
|
19
|
+
),
|
|
20
|
+
'license': 'BSD-3-Clause',
|
|
21
|
+
'name': 'ol-openedx-course-sync',
|
|
22
|
+
'namespace_packages': (
|
|
23
|
+
),
|
|
24
|
+
'package_data': {
|
|
25
|
+
},
|
|
26
|
+
'packages': (
|
|
27
|
+
'',
|
|
28
|
+
'ol_openedx_course_sync',
|
|
29
|
+
'ol_openedx_course_sync.migrations',
|
|
30
|
+
),
|
|
31
|
+
'python_requires': '>=3.8',
|
|
32
|
+
'version': '0.1.0',
|
|
33
|
+
})
|