fix: scope external course readable_id to the vendor - #4090
AhtishamShahid merged 2 commits into
Conversation
`Course.readable_id` for an external course was generated from only the tag
half of the vendor course code (`<MXP | MO>-<COURSE_TAG>`), discarding the
prefix that identifies the vendor. Since `readable_id` is unique across all
courses, two vendors offering the same course tag produced the same readable
ID: Global Alumni's `MXP-MCPO` and Emeritus' `MO-MCPO` both mapped to
`course-v1:xPRO+MCPO`.
The course lookup is scoped by `(external_course_id, platform, is_external)`,
so it correctly missed the other vendor's row and tried to INSERT, which then
failed on `courses_course_readable_id_23dff66f_uniq`. This is deterministic,
not a race: it fails permanently for any course tag that arrives from a second
vendor.
Keep the vendor prefix in the readable ID (`course-v1:xPRO+MO_MCPO`). The
prefix is already in the payload, so no vendor mapping has to be maintained
and a third vendor needs no code change. `readable_id` is only written in
`get_or_create(defaults=...)`, so existing courses keep their IDs and every
`/courses/<readable_id>/` URL that resolves today keeps resolving. This also
fixes the collision on the derived `CourseRun.courseware_id`, and stops
`split("-")[1]` silently truncating tags that contain a dash.
7df80f4 to
60a4c47
Compare
60a4c47 to
1d0fbd0
Compare
An `IntegrityError` on one course escaped the per-course loop up to `task_sync_external_course_runs`, where `except Exception` sits inside the per-platform loop. So a single bad course abandoned the rest of that vendor's catalog, and `send_external_data_sync_email` never ran: the ops stats email for the vendor silently disappeared, leaving Sentry as the only signal. Wrap each course in `atomic_or_record_failure`, which rolls the course back, logs it and records it in a new `courses_failed` stat surfaced in the sync email. Implemented as a context manager replacing `transaction.atomic()` at the call site so the per-course block keeps its original indentation, which keeps the diff to a single changed line and avoids re-surfacing unrelated static-analysis findings on untouched log statements. The `except` sits outside `transaction.atomic()`, so the failed course is rolled back cleanly before it is recorded and the loop continues.
1d0fbd0 to
5cb44d3
Compare
|
@AhtishamShahid could you comment for @cachob's review telling:
I want to get Bon's approval on this before we proceed next. |
FYI @cachob |
|
@asadali145 - If I remember it correctly you were involved when we worked on the api integration for the xPRO external courses. I remember that we put in some logic and reason as to why we created the current naming convention. Will this proposed PR create any issues? |
Hi @cachob, yes, I worked on it. I don't recall any specific reason for omitting the platform code from the Run ID; The platform code is I have reviewed the changes in this PR, and it looks good to me. |
What are the relevant tickets?
Fixes mitodl/hq#13223 (Sentry XPRO-8EF).
Description (What does it do?)
External courses from different vendors could receive the same readable ID because the vendor prefix was removed. For example,
MXP-MCPOandMO-MCPOboth becamecourse-v1:xPRO+MCPO, causing a database error that stopped the remaining courses for that vendor from syncing.course-v1:xPRO+MO_MCPO, and replaces dots and hyphens with underscores.How can this be tested?
Regression tests cover distinct IDs for different vendors, course-code normalization, syncing alongside an existing course with a legacy ID, and continuing after an integrity error.
Validation completed:
courses/,cms/, andmail/.course-v1:xPRO+MCPOcourse and a newcourse-v1:xPRO+MO_MCPOcourse coexist, and both URLs return HTTP 200.To validate, run
uv run pytest courses/sync_external_courses/external_course_sync_api_test.py. For a manual check, sync two vendors with the same course tag and confirm they create separate courses. Trigger a course ID collision and confirm the remaining courses sync and the failed course appears in the sync email.Additional Context
New course URLs include the vendor prefix. Courses with the same tag from different vendors remain separate catalog entries; whether they should appear as one offering is a separate product decision.