Skip to content

Commit af29c50

Browse files
committed
use source course to clone
1 parent 37e269e commit af29c50

10 files changed

Lines changed: 558 additions & 504 deletions

File tree

run_edx_integration_tests.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,12 @@ run_plugin_tests() {
156156
echo "==============Running $plugin_dir tests=================="
157157
cd "$plugin_dir"
158158

159+
# Run this plugin with CMS settings only.
160+
if [[ "$plugin_dir" == *"ol_openedx_uai_content_customization"* ]]; then
161+
echo "Using CMS settings only for $plugin_dir (skipping LMS run)."
162+
pytest_command="pytest . --cov . --ds=cms.envs.test"
159163
# Check for the existence of settings/test.py
160-
if [ -f "settings/test.py" ]; then
164+
elif [ -f "settings/test.py" ]; then
161165
pytest_command="pytest . --cov . --ds=settings.test"
162166
else
163167
pytest_command="pytest . --cov . --ds=lms.envs.test"

src/ol_openedx_uai_content_customization/README.rst

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ Installation required in:
2222
Overview
2323
--------
2424

25-
Original UAI courses are transformed into multiple custom courses per industry
26-
and length combination:
25+
For each unique (course_key, industry, duration) row group in the CSV the
26+
command clones the source course into a new UAI-specific key, removes every
27+
existing section from the clone, and rebuilds the content from the CSV data.
28+
This produces multiple industry- and length-specific variants per source
29+
course while preserving all course settings (grading policy, certificates,
30+
pacing, advanced settings) from the original.
31+
32+
Supported industry/length combinations:
2733

2834
+--------------+------+-------------+--------+
2935
| Industry | Code | Length code | Length |
@@ -82,21 +88,23 @@ You will need two CSV files:
8288
1. **Customized video metadata CSV** — produced by the video customization
8389
workflow. Required columns:
8490

85-
- ``Course Key`` — the original Open edX course key (e.g.
86-
``course-v1:UAI_SOURCE+UAI.2+1T2026``)
87-
- ``Industry`` — one of: ``Healthcare``, ``Finance``, ``Energy``,
91+
- ``course_key`` — the Open edX course key of the **source course to
92+
clone** (e.g. ``course-v1:UAI_SOURCE+UAI.2+1T2026``). This course
93+
**must already exist** in the CMS modulestore before the command runs.
94+
The command validates all source keys up-front and aborts with an error
95+
if any are missing.
96+
- ``industry`` — one of: ``Healthcare``, ``Finance``, ``Energy``,
8897
``Original industry``
89-
- ``Duration (Minutes)`` — a numeric value (≤30 = Short) or the literal
90-
``long`` (= Full)
91-
- ``Video File Name`` — file name matching the Name column in the assets CSV
92-
- ``Video Title (Lecture Title)`` — display name for the subsection/unit/video
93-
- ``Module Name`` — used to build the course display name
98+
- ``duration`` — ``short`` or ``long``
99+
- ``video_file_name`` — file name matching the ``name`` column in the assets CSV
100+
- ``video_title`` — display name for the subsection/unit/video
101+
- ``module_name`` — used to build the course display name
94102

95103
2. **Open edX video asset CSV** — exported from Studio / OVS after uploading
96104
the customized videos. Required columns:
97105

98-
- ``Name`` — video file name (matches ``Video File Name`` above)
99-
- ``Video ID`` — the Open edX UUID for the video
106+
- ``name`` — video file name (matches ``video_file_name`` above)
107+
- ``video_id`` — the Open edX UUID for the video
100108

101109
Running the Command
102110
~~~~~~~~~~~~~~~~~~~
@@ -129,6 +137,33 @@ Options
129137
Print what would be created without writing anything to the modulestore.
130138
Use this to verify CSV mapping before committing.
131139

140+
How It Works
141+
~~~~~~~~~~~~
142+
143+
For each unique ``(course_key, industry, duration)`` group the command:
144+
145+
1. **Validates** all source course keys against the live modulestore before
146+
making any writes (fail-fast — aborts if any source is missing).
147+
2. **Clones** the source course into the new UAI-specific key, inheriting all
148+
course settings.
149+
3. **Deletes** every existing section (chapter) from the clone.
150+
4. **Rebuilds** the content tree from the CSV rows::
151+
152+
Course (cloned — settings inherited)
153+
└── Lectures (section)
154+
└── <Video Title> (subsection)
155+
└── <Video Title> (unit)
156+
└── <Video Title> (video block)
157+
158+
5. **Publishes** the course.
159+
160+
.. note::
161+
162+
Course creation is **not atomic** (MongoDB is not covered by Django
163+
transactions). If a run fails partway through, already-created courses
164+
remain; subsequent runs will skip them with a ``DuplicateCourseError``
165+
warning.
166+
132167
Development
133168
-----------
134169

src/ol_openedx_uai_content_customization/ol_openedx_uai_content_customization/constants.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
"""Constants for ol-openedx-uai-content-customization plugin."""
22

3-
# Industry short codes used in course key generation.
4-
# "Original industry" has no code — only a length code is appended.
53
INDUSTRY_CODES = {
64
"Healthcare": "HC",
75
"Finance": "F",
86
"Energy": "E",
97
"Original industry": "",
108
}
119

12-
# Duration label → short code used in course key generation.
13-
# Numeric minutes (e.g. "10") are treated as Short; "long" as Full.
1410
DURATION_CODE_SHORT = "S"
1511
DURATION_CODE_FULL = "F"
1612

@@ -19,28 +15,23 @@
1915
"long": DURATION_CODE_FULL,
2016
}
2117

22-
# Duration threshold: any numeric value at or below this (in minutes) maps to
23-
# "Short" (code "S"). Values above it map to "Full" (code "F").
24-
# The spec defines short as ≤10 min; 30 allows headroom for slightly longer
25-
# short-form variants without requiring a CSV format change.
26-
SHORT_DURATION_THRESHOLD = 30
27-
28-
# Display name for the top-level section added to every generated course
2918
LECTURES_SECTION_DISPLAY_NAME = "Lectures"
3019

31-
# CSV column names — customized video metadata CSV
32-
CSV_COL_COURSE_KEY = "Course Key"
33-
CSV_COL_INDUSTRY = "Industry"
34-
CSV_COL_DURATION = "duration_minutes"
35-
CSV_COL_VIDEO_FILE = "Video File Name"
36-
CSV_COL_VIDEO_TITLE = "Video Title (Lecture Title)"
37-
CSV_COL_MODULE_NAME = "Module Name"
20+
BLOCK_TYPE_CHAPTER = "chapter"
21+
BLOCK_TYPE_SEQUENTIAL = "sequential"
22+
BLOCK_TYPE_VERTICAL = "vertical"
23+
BLOCK_TYPE_VIDEO = "video"
24+
25+
CSV_COL_COURSE_KEY = "course_key"
26+
CSV_COL_INDUSTRY = "industry"
27+
CSV_COL_DURATION = "duration"
28+
CSV_COL_VIDEO_FILE = "video_file_name"
29+
CSV_COL_VIDEO_TITLE = "video_title"
30+
CSV_COL_MODULE_NAME = "module_name"
3831

39-
# CSV column names — Open edX video asset CSV
40-
CSV_COL_ASSET_NAME = "Name"
41-
CSV_COL_ASSET_VIDEO_ID = "Video ID"
32+
CSV_COL_ASSET_NAME = "name"
33+
CSV_COL_ASSET_VIDEO_ID = "video_id"
4234

43-
# Required columns for each CSV — used to give early, clear error messages
4435
REQUIRED_CUSTOMIZED_CSV_COLS = [
4536
CSV_COL_COURSE_KEY,
4637
CSV_COL_INDUSTRY,

src/ol_openedx_uai_content_customization/ol_openedx_uai_content_customization/csv_utils.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""CSV parsing and video-mapping utilities for ol-openedx-uai-content-customization."""
22

33
import csv
4-
import logging
54
from collections import defaultdict
65
from pathlib import Path
76

@@ -13,15 +12,10 @@
1312
CSV_COL_COURSE_KEY,
1413
CSV_COL_DURATION,
1514
CSV_COL_INDUSTRY,
16-
DURATION_CODE_FULL,
17-
DURATION_CODE_SHORT,
1815
DURATION_CODES,
1916
INDUSTRY_CODES,
20-
SHORT_DURATION_THRESHOLD,
2117
)
2218

23-
log = logging.getLogger(__name__)
24-
2519

2620
def parse_csv(path):
2721
"""
@@ -33,7 +27,7 @@ def parse_csv(path):
3327
column header strings as they appear in the file. Both are empty
3428
lists when the file contains no header row at all.
3529
"""
36-
with Path(path).open(newline="") as f:
30+
with Path(path).open(encoding="utf-8", newline="") as f:
3731
reader = csv.DictReader(f)
3832
fieldnames = list(reader.fieldnames or [])
3933
rows = list(reader)
@@ -78,34 +72,28 @@ def resolve_duration_code(duration_value):
7872
"""
7973
Convert a duration cell value into a Short/Full code.
8074
81-
Numeric values at or below SHORT_DURATION_THRESHOLD map to "S" (Short).
82-
The literal string "long" maps to "F" (Full).
83-
Any other string is checked against DURATION_CODES (case-insensitive).
75+
The CSV must provide explicit values: "short" or "long"
76+
(case-insensitive).
8477
8578
Args:
8679
duration_value: Raw string from the Duration column.
8780
8881
Returns:
8982
"S" or "F"
83+
84+
Raises:
85+
ValueError: if the value is not one of "short" or "long".
9086
"""
9187
value = str(duration_value).strip().lower()
9288

9389
if value in DURATION_CODES:
9490
return DURATION_CODES[value]
9591

96-
try:
97-
minutes = float(value)
98-
except ValueError:
99-
log.warning(
100-
"Unrecognised duration value %r - defaulting to Short (S)", duration_value
101-
)
102-
return DURATION_CODE_SHORT
103-
else:
104-
return (
105-
DURATION_CODE_SHORT
106-
if minutes <= SHORT_DURATION_THRESHOLD
107-
else DURATION_CODE_FULL
108-
)
92+
msg = (
93+
"Unrecognised duration value "
94+
f"{duration_value!r}. Expected one of: {', '.join(DURATION_CODES)}"
95+
)
96+
raise ValueError(msg)
10997

11098

11199
def build_new_course_key(original_key, industry, duration_value):

0 commit comments

Comments
 (0)