You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Downloads multiple videos for a course as a single zip file. Accepts a body parameter files with an array of objects containing url and name for each video. Returns a zip attachment named {course_id}videos{random_id}.zip on success.
Retrieves usage locations of a specific video within the course. Returns an array of strings indicating XBlocks or subsections where the video is embedded.
PR:#39186 — feat: standardize Video Management API into authoring v2 State: open
What this does
Standardizes the Video Management API (#39071, umbrella #38137) onto two conforming addresses in the authoring API. The existing /api/contentstore/v1/videos/… endpoints keep working exactly as they do today.
New addresses
GET /api/authoring/v2/courses/{course_key}/video_usages/{edx_video_id}/
POST /api/authoring/v2/courses/{course_key}/video_archives/
ADRs applied
10 of 16 decisions applied on this pass; every one left open carries its reason.
0025 serializers
0026 permissions
0027 schema / docs
0028 viewsets
0029 errors
0030 GET idempotence
0034 authentication
0037 versioning
0038 URL structure
OEP-69 conventions
0031 merged endpoints — nothing merged: a usage record and a generated archive are different resources, not an action-URL family on one resource
0032 pagination — n/a: the usage endpoint is a member retrieve whose list is bounded by one video's uses in one course, and the archive is binary
0033 filtering — n/a: neither endpoint takes a filter or sort parameter. The video id is a path identifier
0035 MFE config — n/a: no endpoint in this area serves front-end or site configuration
0036 nested JSON — n/a: the usage body is flat and thin (two strings per entry), and the archive is not JSON
OEP-66 queryset scoping — out of scope as a mechanism, applied as a layering. Neither view has a get_queryset(): data comes from the content store and edxval. The future ORM seam is edxval's get_videos_for_course, which runs Video.objects.filter(courses__course_id=…, courses__is_hidden=False)
The evidence for each tick is the compliance matrix in the PR body; nothing is ticked without a named test or gate line.
Backward compatibility
No pre-existing version file is modified.rest_api/v1/views/videos.py is not in the diff. Versions gate PASS: 11 files added inside pre-existing version directories (see In-place edits), 0 modified, no allowlist.
Every v1 address still resolves to the same view. Urls gate PASS (2144 → 2150 routes). The shared URL name video_usage still reverses to …/download without a video id and to …/usage with one (VideoRoutesTest). Deprecated Org/Course/Run keys still reach v1.
Legacy tests untouched and green. The old-tests gate ran 108: 13 in rest_api/v1/views/tests/test_videos.py and 95 in views/tests/test_videos.py. The whole contentstore/rest_api/ suite passes (772). The tests gate passes the 276 new tests.
Schema: additions and deprecation flags only. The raw compare fails because removing the path trim renames all 56 trimmed base path keys. With the base paths re-prefixed the result is 0 BREAKING / 4 WARN / 4 INFO (Gate report). compare ignores operationIds, tags and servers, so each of the 77 base operations was also compared whole with its re-prefixed head counterpart. The only difference is deprecated: true on the two superseded operations, and no head operationId is duplicated. components gains CourseVideoUsage, ErrorResponse, VideoArchiveFile, VideoArchiveRequest and VideoUsageLocation, and changes or drops none. test_no_authoring_operation_is_given_the_id_of_another checks operationIds before the generator renames collisions (seen red with a colliding route).
Parity (INTENTIONAL_DIFFERENCES = []). Usage bodies are byte-identical at both addresses for these cases: one use, three uses, an unpublished unit, a parentless component, an unused id ({"usage_locations": []}), unset display names ("None - None / Video"), and a unit in no subsection. Archives are byte-identical, with the same Content-Type and Content-Disposition, for these cases: a named extension, an extension from the upstream content type, an unknown type, several files, and an empty list. A fetch that fails part way stops both archives at the same byte (test_a_fetch_failing_part_way_stops_the_archive). The harness was seen red when the usage result or the file list was altered.
Query counts (warm caches, legacy/new): usage 11/11 (staff), 13/13 (session), 11/11 (course staff), 8/7 (403), 8/9 (404), 5/6 (401); archive 11/11 (200), 11/12 (400), 8/7 (403), 8/9 (404), 5/6 (401). Every +1 is a ROLLBACK TO SAVEPOINT from the standard error handling, not a read.
Gates
run_gates.sh --base origin/master --service cms --prefix /api/authoring/v2/ (6 --protect paths, no --allow), Studio container, final tree; no gate skipped
versions PASS 11 added, 0 modified, 0 allowed
hygiene PASS host 0 FAIL / 0 WARN (container: 4 WARN in Tutor-generated files outside the diff)
schema FAIL, reconciled -> 0 BREAKING / 4 WARN / 4 INFO
raw: 56 BREAKING = path keys renamed by removing SCHEMA_PATH_PREFIX_TRIM;
after re-prefixing base paths: no base path missing, 2 operations added,
2 v1 operations newly deprecated; 4 WARN = existing "could not resolve
authenticator" kind on the two new views
urls PASS 2144 -> 2150 routes, 6 new checked
old-tests PASS 108 passed (unedited)
tests PASS 276 passed
GATES FAILED: 1 (schema, reconciled; never reported as PASS)
Deprecations
The two operations GET /api/contentstore/v1/videos/{course_id}/{edx_video_id}/usage and PUT /api/contentstore/v1/videos/{course_id}/download are superseded by the addresses above. They are marked deprecated: true in the Authoring API schema (/authoring-api/schema/, browsable at /authoring-api/ui/). The superseded views are unchanged: no response header and no deprecation notes in code. GET /api/contentstore/v1/videos/{course_id} (course videos) is a different resource and is not marked. The older /api-docs/ document does not flag them (Departures, item 4). Readers of that document learn of the deprecation from #39128.
Tracked on the FC-0118 umbrella DEPR ticket, #39128, which removes them one named release after the release this ships in. This area's entry, with its removal step: #39128 comment.
ADR 0037's docstring marker is deliberately omitted, following review on #39100 and #39102. If one is wanted later, it is the single OEP-21 line Deprecated https://github.com/openedx/openedx-platform/issues/39128. The umbrella's step-1 Warning header is not emitted either, because the v1 route is frozen.
Follow-ups
edly-io/openedx-platform-sdk regeneration adds v2_courses_video_usages_retrieve and v2_courses_video_archives_create (the v1 operations were never tagged). Three things for that PR. It needs content_type_overrides: {application/zip: application/octet-stream} so the archive call returns File; this was checked on an earlier schema of this branch, not the final one. It must regenerate from a document with this PR's settings shape (feat: add GH workflow to generate openapi schema #39025). Its base URL is <CMS_BASE>.
edx-drf-extensions: ErrorResponse schema description is the serializer's maintainer docstring. Issue to be filed (text ready).
edx-drf-extensions: nested validation errors and a stable detail (normalize_validation_errors, flatten_detail). The tests here pin today's text. Issue to be filed (text ready).
edx-drf-extensions: log unhandled exceptions with exc_info and send got_request_exception. Issue to be filed (text ready).
edx-drf-extensions + platform: security schemes for JwtAuthentication/DefaultSessionAuthentication. Once these exist, the 401 descriptions can be shortened. Issue to be filed (text ready, with the platform half).
edx-drf-extensions: the course_key converter should refuse version-only keys. Then HasFullCourseKey can go. Issue to be filed (text ready).
edx-drf-extensions: catalog 405/406/415/malformed JSON instead of typing them internal. Issue to be filed (text ready).
edx-drf-extensions: make register_url_converters() idempotent. Django 5.2 warns on each URLconf reload and Django 6.0 raises. Issue to be filed (text ready).
edx-drf-extensions: a pass-through renderer for binary responses, so Accept: application/zip alone stops getting 406. Issue to be filed (text ready).
The shared video helpers, fixed once for both addresses: unquoted Content-Disposition filename, no upstream fetch timeout, truncated 200 on a failed fetch, and unsanitized or duplicate zip entry names.
A collection form courses/{course_key}/video_usages/ would let the Videos page fetch every video's usage in one call. It is not added here, to keep to the issue's table.
Activity
Abdul-Muqadim-Arbisoft commentedon Oct 4, 2026
Standardization PR open: #39186
PR: #39186 — feat: standardize Video Management API into authoring v2
State: open
What this does
Standardizes the Video Management API (#39071, umbrella #38137) onto two conforming addresses in the authoring API. The existing
/api/contentstore/v1/videos/…endpoints keep working exactly as they do today.New addresses
/api/authoring/v2/courses/{course_key}/video_usages/{edx_video_id}//api/authoring/v2/courses/{course_key}/video_archives/ADRs applied
10 of 16 decisions applied on this pass; every one left open carries its reason.
get_queryset(): data comes from the content store and edxval. The future ORM seam is edxval'sget_videos_for_course, which runsVideo.objects.filter(courses__course_id=…, courses__is_hidden=False)The evidence for each tick is the compliance matrix in the PR body; nothing is ticked without a named test or gate line.
Backward compatibility
rest_api/v1/views/videos.pyis not in the diff. Versions gate PASS: 11 files added inside pre-existing version directories (see In-place edits), 0 modified, no allowlist.video_usagestill reverses to…/downloadwithout a video id and to…/usagewith one (VideoRoutesTest). DeprecatedOrg/Course/Runkeys still reach v1.rest_api/v1/views/tests/test_videos.pyand 95 inviews/tests/test_videos.py. The wholecontentstore/rest_api/suite passes (772). The tests gate passes the 276 new tests.comparefails because removing the path trim renames all 56 trimmed base path keys. With the base paths re-prefixed the result is 0 BREAKING / 4 WARN / 4 INFO (Gate report).compareignores operationIds, tags and servers, so each of the 77 base operations was also compared whole with its re-prefixed head counterpart. The only difference isdeprecated: trueon the two superseded operations, and no head operationId is duplicated.componentsgainsCourseVideoUsage,ErrorResponse,VideoArchiveFile,VideoArchiveRequestandVideoUsageLocation, and changes or drops none.test_no_authoring_operation_is_given_the_id_of_anotherchecks operationIds before the generator renames collisions (seen red with a colliding route).INTENTIONAL_DIFFERENCES = []). Usage bodies are byte-identical at both addresses for these cases: one use, three uses, an unpublished unit, a parentless component, an unused id ({"usage_locations": []}), unset display names ("None - None / Video"), and a unit in no subsection. Archives are byte-identical, with the sameContent-TypeandContent-Disposition, for these cases: a named extension, an extension from the upstream content type, an unknown type, several files, and an empty list. A fetch that fails part way stops both archives at the same byte (test_a_fetch_failing_part_way_stops_the_archive). The harness was seen red when the usage result or the file list was altered.ROLLBACK TO SAVEPOINTfrom the standard error handling, not a read.Gates
Deprecations
The two operations
GET /api/contentstore/v1/videos/{course_id}/{edx_video_id}/usageandPUT /api/contentstore/v1/videos/{course_id}/downloadare superseded by the addresses above. They are markeddeprecated: truein the Authoring API schema (/authoring-api/schema/, browsable at/authoring-api/ui/). The superseded views are unchanged: no response header and no deprecation notes in code.GET /api/contentstore/v1/videos/{course_id}(course videos) is a different resource and is not marked. The older/api-docs/document does not flag them (Departures, item 4). Readers of that document learn of the deprecation from #39128.Tracked on the FC-0118 umbrella DEPR ticket, #39128, which removes them one named release after the release this ships in. This area's entry, with its removal step: #39128 comment.
ADR 0037's docstring marker is deliberately omitted, following review on #39100 and #39102. If one is wanted later, it is the single OEP-21 line
Deprecated https://github.com/openedx/openedx-platform/issues/39128. The umbrella's step-1Warningheader is not emitted either, because the v1 route is frozen.Follow-ups
v2_courses_video_usages_retrieveandv2_courses_video_archives_create(the v1 operations were never tagged). Three things for that PR. It needscontent_type_overrides: {application/zip: application/octet-stream}so the archive call returnsFile; this was checked on an earlier schema of this branch, not the final one. It must regenerate from a document with this PR's settings shape (feat: add GH workflow to generate openapi schema #39025). Its base URL is<CMS_BASE>.ErrorResponseschema description is the serializer's maintainer docstring. Issue to be filed (text ready).detail(normalize_validation_errors,flatten_detail). The tests here pin today's text. Issue to be filed (text ready).exc_infoand sendgot_request_exception. Issue to be filed (text ready).JwtAuthentication/DefaultSessionAuthentication. Once these exist, the 401 descriptions can be shortened. Issue to be filed (text ready, with the platform half).course_keyconverter should refuse version-only keys. ThenHasFullCourseKeycan go. Issue to be filed (text ready).internal. Issue to be filed (text ready).register_url_converters()idempotent. Django 5.2 warns on each URLconf reload and Django 6.0 raises. Issue to be filed (text ready).Accept: application/zipalone stops getting 406. Issue to be filed (text ready).Content-Dispositionfilename, no upstream fetch timeout, truncated 200 on a failed fetch, and unsanitized or duplicate zip entry names.courses/{course_key}/video_usages/would let the Videos page fetch every video's usage in one call. It is not added here, to keep to the issue's table.VideoDownloadThrottleinto v2 beforerest_api/v1/views/videos.pyis removed, because the v2 archive view imports it from there. It is listed in the [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 entry as a removal step.Review on the PR, please — this issue stays open until it merges.
test: use the standard library UTC in the video archive tests