Repository navigation
feat: standardize Video Management API into authoring v2 #39186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Abdul-Muqadim-Arbisoft
wants to merge
8
commits into
openedx:master
Choose a base branch
from
edly-io:feat/api-adrs-video-management
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,001
−10
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
eab3d42
feat: publish full CMS schema paths and register path converters
Abdul-Muqadim-Arbisoft 0ae573b
feat: add a v2 video usages endpoint to the authoring API
Abdul-Muqadim-Arbisoft f20db7b
feat: add a v2 video archives endpoint to the authoring API
Abdul-Muqadim-Arbisoft cd38a23
test: use the standard library UTC in the video archive tests
Abdul-Muqadim-Arbisoft df3842b
Merge branch 'master' into feat/api-adrs-video-management
Abdul-Muqadim-Arbisoft 050c98e
test: follow master's schema hook and pin the viewset mount's actions
Abdul-Muqadim-Arbisoft ec62c3a
test: list the two video paths as superseded and read the new docs do…
Abdul-Muqadim-Arbisoft c13404a
fix: record that the two legacy video operations are now superseded
Abdul-Muqadim-Arbisoft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
cms/djangoapps/contentstore/rest_api/v1/views/unknown_route.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| """API view answering addresses that no endpoint serves.""" | ||
|
|
||
| from drf_spectacular.utils import extend_schema | ||
| from edx_rest_framework_extensions.mixins import StandardizedErrorMixin | ||
| from rest_framework.exceptions import NotFound | ||
| from rest_framework.permissions import AllowAny | ||
| from rest_framework.views import APIView | ||
|
|
||
|
|
||
| @extend_schema(exclude=True) | ||
| class UnknownRouteView(StandardizedErrorMixin, APIView): | ||
| """ | ||
| Answer a video address whose course key the conforming routes refuse. | ||
|
|
||
| A key that is malformed or in the deprecated slash-separated form fails to | ||
| resolve, and would otherwise be answered by the site's HTML error page, which | ||
| an API client cannot read. Nothing here needs the caller's identity, so the | ||
| answer is the same for everyone: the address holds no resource. | ||
| """ | ||
|
|
||
| permission_classes = (AllowAny,) | ||
|
|
||
| def initial(self, request, *args, **kwargs): | ||
| """Refuse the request before any handler runs; the address holds nothing.""" | ||
| raise NotFound() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| """Contentstore API v2 authoring URLs.""" | ||
|
|
||
| from django.urls import path | ||
| from django.urls import path, re_path | ||
|
|
||
| from cms.djangoapps.contentstore.rest_api.v1.views.unknown_route import UnknownRouteView | ||
| from cms.djangoapps.contentstore.rest_api.v2.views.textbooks import CourseTextbooksViewSet | ||
| from cms.djangoapps.contentstore.rest_api.v2.views.video_archives import CourseVideoArchiveView | ||
| from cms.djangoapps.contentstore.rest_api.v2.views.video_settings import CourseVideoSettingsViewSet | ||
| from cms.djangoapps.contentstore.rest_api.v2.views.video_usages import CourseVideoUsageViewSet | ||
|
|
||
| app_name = "authoring_v2" | ||
|
|
||
|
|
@@ -18,4 +21,44 @@ | |
| CourseVideoSettingsViewSet.as_view({"get": "retrieve"}), | ||
| name="course_video_settings", | ||
| ), | ||
| path( | ||
| "courses/<course_key:course_key>/video_usages/<str:edx_video_id>/", | ||
| CourseVideoUsageViewSet.as_view({"get": "retrieve"}), | ||
| name="course_video_usage_detail", | ||
| ), | ||
| path( | ||
| "courses/<course_key:course_key>/video_archives/", | ||
| CourseVideoArchiveView.as_view(), | ||
| name="course_video_archive_list", | ||
| ), | ||
| # Every other address under these two collections holds nothing: a course | ||
| # key the routes above refuse (malformed, or in the deprecated | ||
| # slash-separated form), the usage collection without a video id, an empty | ||
| # video id, or a path below a usage or below the archives. They answer with | ||
| # the API error body rather than the site's HTML error page. They match | ||
| # addresses under these two collections and nothing else, so a resource | ||
| # added under courses/ is never shadowed, and only slash-terminated ones, so | ||
| # the redirect to the slash-terminated form still happens first. A path | ||
| # converter cannot match an empty segment, hence the patterns for the | ||
| # addresses below each collection. | ||
| path( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ADR 0038: two points on the fallback routes.
Nit: the 10-line comment is long for this URLconf; reviewers asked for this kind of comment to be removed on #39078. |
||
| "courses/<path:course_key>/video_usages/", | ||
| UnknownRouteView.as_view(), | ||
| name="course_video_usage_list_unmatched", | ||
| ), | ||
| re_path( | ||
| r"^courses/(?P<course_key>.+)/video_usages/(?P<subpath>.*)/$", | ||
| UnknownRouteView.as_view(), | ||
| name="course_video_usage_detail_unmatched", | ||
| ), | ||
| path( | ||
| "courses/<path:course_key>/video_archives/", | ||
| UnknownRouteView.as_view(), | ||
| name="course_video_archive_list_unmatched", | ||
| ), | ||
| re_path( | ||
| r"^courses/(?P<course_key>.+)/video_archives/(?P<subpath>.*)/$", | ||
| UnknownRouteView.as_view(), | ||
| name="course_video_archive_detail_unmatched", | ||
| ), | ||
| ] | ||
35 changes: 35 additions & 0 deletions
35
cms/djangoapps/contentstore/rest_api/v2/serializers/video_archives.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """Serializers for zip archives of a course's videos.""" | ||
|
|
||
| from rest_framework import serializers | ||
|
|
||
|
|
||
| class VideoArchiveFileSerializer(serializers.Serializer): # pylint: disable=abstract-method | ||
| """One video to put in the archive.""" | ||
|
|
||
| # Values are used exactly as sent: the URL is matched character for | ||
| # character against the course's video URLs, and the name becomes the entry | ||
| # name in the archive. | ||
| url = serializers.CharField( | ||
| trim_whitespace=False, | ||
| help_text=( | ||
| "Address of one of the course's encoded video files, exactly as the course's video " | ||
| "listing reports it. Any other address is refused." | ||
| ), | ||
| ) | ||
| name = serializers.CharField( | ||
| trim_whitespace=False, | ||
| allow_blank=True, | ||
| help_text=( | ||
| "File name of the video inside the archive. When it does not already end with the " | ||
| "extension of the video's content type, that extension is appended." | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| class VideoArchiveRequestSerializer(serializers.Serializer): # pylint: disable=abstract-method | ||
| """The videos to bundle into one archive.""" | ||
|
|
||
| files = VideoArchiveFileSerializer( | ||
| many=True, | ||
| help_text="The videos to include, in archive order. An empty list gives an empty archive.", | ||
| ) |
34 changes: 34 additions & 0 deletions
34
cms/djangoapps/contentstore/rest_api/v2/serializers/video_usages.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| """Serializers for where a video is used in a course.""" | ||
|
|
||
| from rest_framework import serializers | ||
|
|
||
|
|
||
| class VideoUsageLocationSerializer(serializers.Serializer): # pylint: disable=abstract-method | ||
| """One video component of the course that plays the video.""" | ||
|
|
||
| display_location = serializers.CharField( | ||
| help_text=( | ||
| "Where the component sits, as '<subsection> - <unit> / <component>' display names. " | ||
| "A subsection or unit with no display name set appears as 'None', and a component with " | ||
| "none as 'Video'. The subsection part is empty for a unit that is in no subsection." | ||
| ), | ||
| ) | ||
| url = serializers.CharField( | ||
| help_text=( | ||
| "Studio address of the unit holding the component, anchored at the component: " | ||
| "'/container/<unit usage key>#<component usage key>'." | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| class CourseVideoUsageSerializer(serializers.Serializer): # pylint: disable=abstract-method | ||
| """The places in a course where one video is used.""" | ||
|
|
||
| usage_locations = VideoUsageLocationSerializer( | ||
| many=True, | ||
| help_text=( | ||
| "One entry per video component that references the video, published or not. A " | ||
| "component with no parent in the course outline is left out. Empty when no component " | ||
| "is listed." | ||
| ), | ||
| ) |
41 changes: 41 additions & 0 deletions
41
cms/djangoapps/contentstore/rest_api/v2/video_management_service.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Service layer for the video usage and video archive resources of a course.""" | ||
|
|
||
| from rest_framework.exceptions import NotFound | ||
|
|
||
| from cms.djangoapps.contentstore.video_storage_handlers import create_video_zip, get_video_usage_path | ||
| from openedx.core.djangoapps.content.course_overviews.models import CourseOverview | ||
|
|
||
|
|
||
| def ensure_course_exists(course_key): | ||
| """ | ||
| Raise ``NotFound`` unless a course run exists under ``course_key``. | ||
|
|
||
| Only reads: the course overview table first, then the content store. The | ||
| overview row is never created here, so a read request writes nothing. | ||
| """ | ||
| if not CourseOverview.course_exists(course_key): | ||
| raise NotFound("The course does not exist.") | ||
|
|
||
|
|
||
| def get_course_video_usages(course_key, edx_video_id): | ||
| """ | ||
| Return where the video ``edx_video_id`` is placed in the course. | ||
|
|
||
| The result is ``{"usage_locations": [{"display_location", "url"}, ...]}``, | ||
| one entry per video component that references the id, in content-store | ||
| order. Unpublished components are included; a component with no parent is | ||
| left out. An id that nothing references gives an empty list. | ||
| """ | ||
| ensure_course_exists(course_key) | ||
| return get_video_usage_path(course_key, edx_video_id) | ||
|
|
||
|
|
||
| def create_course_video_archive(course_key, files): | ||
| """ | ||
| Return a streaming zip response holding the requested videos of the course. | ||
|
|
||
| Every ``files[].url`` must be one of the course's own encoded-video URLs; | ||
| otherwise ``ValidationError`` is raised before anything is fetched. The | ||
| archive is assembled while it is sent and is not stored. | ||
| """ | ||
| return create_video_zip(str(course_key), files) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a new module inside the frozen
v1/directory, used only by v2 routes. A neutral location (e.g.contentstore/rest_api/views/oropenedx/core/lib/api/) would fit it better, especially as #39102 carries a byte-identical copy and could share one module.Small one: the docstring says it answers "a video address whose course key the conforming routes refuse", but it also answers valid keys (e.g.
video_usages/with no id, or sub-paths). Something like "an address under a collection that no route serves" would be more accurate.