From b628a6b8dcd3ec9f4cc3a10c44fadba8463ed7f3 Mon Sep 17 00:00:00 2001 From: farhan Date: Fri, 20 Feb 2026 11:05:54 +0500 Subject: [PATCH] feat: Update the shifted Video XBlock code to make it run --- requirements/base.in | 3 + xblocks_contrib/common/utils.py | 35 ++ xblocks_contrib/video/ajax_handler_mixin.py | 17 +- xblocks_contrib/video/assets/css/video.css | 555 +++++++++--------- xblocks_contrib/video/assets/package.json | 26 + .../video/assets/webpack.common.config.js | 45 ++ .../video/assets/webpack.dev.config.js | 19 + .../video/assets/webpack.prod.config.js | 20 + xblocks_contrib/video/bumper_utils.py | 2 - xblocks_contrib/video/cache_utils.py | 9 +- xblocks_contrib/video/constants.py | 17 + xblocks_contrib/video/content.py | 9 +- xblocks_contrib/video/mixin.py | 4 +- .../video/studio_metadata_mixin.py | 165 ++++++ xblocks_contrib/video/templates/video.html | 4 +- xblocks_contrib/video/validation.py | 2 - xblocks_contrib/video/video.py | 340 +++++++---- xblocks_contrib/video/video_handlers.py | 26 +- .../video/video_transcripts_utils.py | 22 +- xblocks_contrib/video/video_utils.py | 6 +- xblocks_contrib/video/video_xfields.py | 17 +- 21 files changed, 874 insertions(+), 469 deletions(-) create mode 100644 xblocks_contrib/common/utils.py create mode 100644 xblocks_contrib/video/assets/package.json create mode 100644 xblocks_contrib/video/assets/webpack.common.config.js create mode 100644 xblocks_contrib/video/assets/webpack.dev.config.js create mode 100644 xblocks_contrib/video/assets/webpack.prod.config.js create mode 100644 xblocks_contrib/video/constants.py create mode 100644 xblocks_contrib/video/studio_metadata_mixin.py diff --git a/requirements/base.in b/requirements/base.in index ed641fdc..5bf28fd6 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,9 +2,12 @@ -c constraints.txt django-statici18n +edx-django-utils edx-i18n-tools edx-opaque-keys +edxval nh3 oauthlib openedx-django-pyfs +wrapt XBlock diff --git a/xblocks_contrib/common/utils.py b/xblocks_contrib/common/utils.py new file mode 100644 index 00000000..240d83e9 --- /dev/null +++ b/xblocks_contrib/common/utils.py @@ -0,0 +1,35 @@ +""" +Common utility functions for XBlocks. +""" + +from django.conf import settings + + + +def get_resource_url(xblock, path, package_scope=None): + """ + Return the runtime URL for a static resource in this XBlock's package. + + When the Django pipeline is enabled or REQUIRE_DEBUG is False, uses the + pipeline path (e.g. {package_scope}/public/{path}). Otherwise uses the + dev path (e.g. public/{path}). See platform's xblock_local_resource_url() in: + openedx/core/lib/xblock_utils/__init__.py + + Arguments: + xblock: The XBlock instance (for runtime.local_resource_url). + path (str): Relative path within the package, e.g. "css/video.css". + package_scope (str): Package name prefix, e.g. "video". If None, + both paths are "public/{path}"; otherwise pipeline adds the prefix. + + Returns: + str: URL from xblock.runtime.local_resource_url() for the resource. + """ + pipeline_path = dev_path = f"public/{path}" + if package_scope: + pipeline_path = f"{package_scope}/{pipeline_path}" + pipeline = getattr(settings, 'PIPELINE', {}) + if pipeline.get('PIPELINE_ENABLED', True) or not getattr(settings, 'REQUIRE_DEBUG', False): + resource_path = pipeline_path + else: + resource_path = dev_path + return xblock.runtime.local_resource_url(xblock, resource_path) diff --git a/xblocks_contrib/video/ajax_handler_mixin.py b/xblocks_contrib/video/ajax_handler_mixin.py index a0dd5e9b..11dfd053 100644 --- a/xblocks_contrib/video/ajax_handler_mixin.py +++ b/xblocks_contrib/video/ajax_handler_mixin.py @@ -1,21 +1,24 @@ -# NOTE: Code has been copied from the following source file -# https://github.com/openedx/edx-platform/blob/master/xmodule/x_module.py#L739 +""" Mixin that provides AJAX handling for Video XBlock """ +from webob import Response +from webob.multidict import MultiDict +from xblock.core import XBlock -class XModuleToXBlockMixin: + +class AjaxHandlerMixin: """ - Common code needed by XModule and XBlocks converted from XModules. + Mixin that provides AJAX handling for Video XBlock """ @property def ajax_url(self): """ Returns the URL for the ajax handler. """ - return self.runtime.handler_url(self, 'xmodule_handler', '', '').rstrip('/?') + return self.runtime.handler_url(self, 'ajax_handler', '', '').rstrip('/?') @XBlock.handler - def xmodule_handler(self, request, suffix=None): + def ajax_handler(self, request, suffix=None): """ - XBlock handler that wraps `handle_ajax` + XBlock handler that wraps `ajax_handler` """ class FileObjForWebobFiles: """ diff --git a/xblocks_contrib/video/assets/css/video.css b/xblocks_contrib/video/assets/css/video.css index ca20ba91..4b17328b 100644 --- a/xblocks_contrib/video/assets/css/video.css +++ b/xblocks_contrib/video/assets/css/video.css @@ -1,18 +1,17 @@ - +/* CSS for VideoBlock */ @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700"); -.xmodule_display.xmodule_VideoBlock { +.xblock { margin-bottom: calc((var(--baseline, 20px) * 1.5)); } -.xmodule_display.xmodule_VideoBlock .is-hidden, -.xmodule_display.xmodule_VideoBlock .video.closed .subtitles { +.is-hidden, +.video.closed .subtitles { display: none; } -.xmodule_display.xmodule_VideoBlock .video { +.video { background: whitesmoke; display: block; margin: 0 -12px; @@ -21,111 +20,111 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b outline: none; } -.xmodule_display.xmodule_VideoBlock .video:after { +.video:after { content: ""; display: table; clear: both; } -.xmodule_display.xmodule_VideoBlock .video:focus, -.xmodule_display.xmodule_VideoBlock .video:active, -.xmodule_display.xmodule_VideoBlock .video:hover { +.video:focus, +.video:active, +.video:hover { border: 0; } -.xmodule_display.xmodule_VideoBlock .video.is-initialized .video-wrapper .spinner { +.video.is-initialized .video-wrapper .spinner { display: none; } -.xmodule_display.xmodule_VideoBlock .video.is-pre-roll .slider { +.video.is-pre-roll .slider { visibility: hidden; } -.xmodule_display.xmodule_VideoBlock .video.is-pre-roll .video-player { +.video.is-pre-roll .video-player { position: relative; } -.xmodule_display.xmodule_VideoBlock .video.is-pre-roll .video-player::before { +.video.is-pre-roll .video-player::before { display: block; content: ""; width: 100%; padding-top: 55%; } -.xmodule_display.xmodule_VideoBlock .video .tc-wrapper { +.video .tc-wrapper { position: relative; } -.xmodule_display.xmodule_VideoBlock .video .tc-wrapper:after { +.video .tc-wrapper:after { content: ""; display: table; clear: both; } -.xmodule_display.xmodule_VideoBlock .video .focus_grabber { +.video .focus_grabber { position: relative; display: inline; width: 0; height: 0; } -.xmodule_display.xmodule_VideoBlock .video .downloads-heading { +.video .downloads-heading { margin: 1em 0 0; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-video-bottom-section { +.video .wrapper-video-bottom-section { display: flex; justify-content: space-between; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-video-bottom-section .wrapper-download-video, -.xmodule_display.xmodule_VideoBlock .video .wrapper-video-bottom-section .wrapper-download-transcripts, -.xmodule_display.xmodule_VideoBlock .video .wrapper-video-bottom-section .wrapper-handouts, -.xmodule_display.xmodule_VideoBlock .video .wrapper-video-bottom-section , -.xmodule_display.xmodule_VideoBlock .video .wrapper-video-bottom-section .wrapper-transcript-feedback { +.video .wrapper-video-bottom-section .wrapper-download-video, +.video .wrapper-video-bottom-section .wrapper-download-transcripts, +.video .wrapper-video-bottom-section .wrapper-handouts, +.video .wrapper-video-bottom-section , +.video .wrapper-video-bottom-section .wrapper-transcript-feedback { margin-top: var(--baseline, 20px); padding-right: var(--baseline, 20px); vertical-align: top; } @media (min-width: 768px) { - .xmodule_display.xmodule_VideoBlock .video .wrapper-downloads { + .video .wrapper-downloads { display: flex; } } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .hd { +.video .wrapper-downloads .hd { margin: 0; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .wrapper-download-video .video-sources { +.video .wrapper-downloads .wrapper-download-video .video-sources { margin: 0; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts { +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts { margin: 0; padding: 0; list-style: none; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option { +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option { display: flex; align-items: center; margin: 0; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option a.btn, -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option a.btn-link { +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option a.btn, +.video .wrapper-downloads .wrapper-download-transcripts .list-download-transcripts .transcript-option a.btn-link { font-size: 16px !important; font-weight: unset; padding-left: 4px; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads { +.video .wrapper-downloads { padding-right: 0; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .host-tag { +.video .wrapper-downloads .host-tag { position: absolute; left: -9999em; display: inline-block; @@ -133,7 +132,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b color: var(--body-color, #313131); } -.xmodule_display.xmodule_VideoBlock .video .wrapper-downloads .brand-logo { +.video .wrapper-downloads .brand-logo { display: inline-block; max-width: 100%; max-height: calc((var(--baseline, 20px) * 2)); @@ -141,33 +140,33 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b vertical-align: middle; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-transcript-feedback { +.video .wrapper-transcript-feedback { display: none; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-transcript-feedback .transcript-feedback-buttons { +.video .wrapper-transcript-feedback .transcript-feedback-buttons { display: flex; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-transcript-feedback .transcript-feedback-btn-wrapper { +.video .wrapper-transcript-feedback .transcript-feedback-btn-wrapper { margin-right: 10px; } -.xmodule_display.xmodule_VideoBlock .video .wrapper-transcript-feedback .thumbs-up-btn, -.xmodule_display.xmodule_VideoBlock .video .wrapper-transcript-feedback .thumbs-down-btn { +.video .wrapper-transcript-feedback .thumbs-up-btn, +.video .wrapper-transcript-feedback .thumbs-down-btn { border: none; box-shadow: none; background: transparent; } -.xmodule_display.xmodule_VideoBlock .video .google-disclaimer { +.video .google-disclaimer { display: none; margin-top: var(--baseline, 20px); padding-right: var(--baseline, 20px); vertical-align: top; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper { +.video .video-wrapper { float: left; margin-right: 2.27273%; width: 65.90909%; @@ -175,21 +174,21 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b position: relative; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper:hover .btn-play { +.video .video-wrapper:hover .btn-play { color: #0075b4; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper:hover .btn-play::after { +.video .video-wrapper:hover .btn-play::after { background: #fff; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player-pre, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player-post { +.video .video-wrapper .video-player-pre, +.video .video-wrapper .video-player-post { height: 50px; background-color: #111010; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .spinner { +.video .video-wrapper .spinner { transform: translate(-50%, -50%); position: absolute; z-index: 1; @@ -200,7 +199,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b border-radius: 25%; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .spinner::after { +.video .video-wrapper .spinner::after { animation: rotateCW 3s infinite linear; content: ''; display: block; @@ -212,7 +211,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b position: relative; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .btn-play { +.video .video-wrapper .btn-play { transform: translate(-50%, -50%); position: absolute; z-index: 1; @@ -223,7 +222,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b opacity: 0.1; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .btn-play::after { +.video .video-wrapper .btn-play::after { background: var(--white, #fff); position: absolute; width: 50%; @@ -237,7 +236,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b z-index: -1; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions { +.video .video-wrapper .closed-captions { left: 5%; position: absolute; width: 90%; @@ -246,7 +245,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b text-align: center; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions.is-visible { +.video .video-wrapper .closed-captions.is-visible { max-height: calc((var(--baseline, 20px) * 3)); border-radius: calc((var(--baseline, 20px) / 5)); padding: 8px calc((var(--baseline, 20px) / 2)) 8px calc((var(--baseline, 20px) * 1.5)); @@ -254,7 +253,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b color: var(--yellow, #e2c01f); } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions.is-visible::before { +.video .video-wrapper .closed-captions.is-visible::before { position: absolute; display: inline-block; top: 50%; @@ -266,76 +265,76 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b opacity: 0.5; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions.is-visible:hover, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions.is-visible.is-dragging { +.video .video-wrapper .closed-captions.is-visible:hover, +.video .video-wrapper .closed-captions.is-visible.is-dragging { background: black; cursor: move; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions.is-visible:hover::before, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .closed-captions.is-visible.is-dragging::before { +.video .video-wrapper .closed-captions.is-visible:hover::before, +.video .video-wrapper .closed-captions.is-visible.is-dragging::before { opacity: 1; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player { +.video .video-wrapper .video-player { overflow: hidden; min-height: 158px; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player>div { +.video .video-wrapper .video-player>div { height: 100%; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player>div.hidden { +.video .video-wrapper .video-player>div.hidden { display: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player .video-error, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player .video-hls-error { +.video .video-wrapper .video-player .video-error, +.video .video-wrapper .video-player .video-hls-error { padding: calc((var(--baseline, 20px) / 5)); background: black; color: white !important; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player object, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player iframe, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player video { +.video .video-wrapper .video-player object, +.video .video-wrapper .video-player iframe, +.video .video-wrapper .video-player video { left: 0; display: block; border: none; width: 100%; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player h4 { +.video .video-wrapper .video-player h4 { text-align: center; color: white; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-player h4.hidden { +.video .video-wrapper .video-player h4.hidden { display: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls { +.video .video-wrapper .video-controls { position: relative; border: 0; background: #282c2e; color: #f0f3f5; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls:after { +.video .video-wrapper .video-controls:after { content: ""; display: table; clear: both; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls:hover ul, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls:hover div, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls:focus ul, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls:focus div { +.video .video-wrapper .video-controls:hover ul, +.video .video-wrapper .video-controls:hover div, +.video .video-wrapper .video-controls:focus ul, +.video .video-wrapper .video-controls:focus div { opacity: 1; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .control { +.video .video-wrapper .video-controls .control { display: inline-block; vertical-align: middle; margin: 0; @@ -348,26 +347,26 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b color: #cfd8dc; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .control:hover, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .control:focus { +.video .video-wrapper .video-controls .control:hover, +.video .video-wrapper .video-controls .control:focus { background: #171a1b; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .control:active, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .is-active.control, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .active.control { +.video .video-wrapper .video-controls .control:active, +.video .video-wrapper .video-controls .is-active.control, +.video .video-wrapper .video-controls .active.control { color: #0ea6ec; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .control .icon { +.video .video-wrapper .video-controls .control .icon { width: 1em; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .control .icon.icon-hd { +.video .video-wrapper .video-controls .control .icon.icon-hd { width: auto; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider { +.video .video-wrapper .video-controls .slider { transform-origin: bottom left; transition: height 0.7s ease-in-out 0s; box-sizing: border-box; @@ -383,13 +382,13 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b background: #4f595d; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider:after { +.video .video-wrapper .video-controls .slider:after { content: ""; display: table; clear: both; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider .ui-widget-header { +.video .video-wrapper .video-controls .slider .ui-widget-header { background: #8e3e63; border: 1px solid #8e3e63; box-shadow: none; @@ -397,12 +396,12 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b left: -1px; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider .ui-corner-all.slider-range { +.video .video-wrapper .video-controls .slider .ui-corner-all.slider-range { opacity: 0.3; background-color: #1e91d3; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider .ui-slider-handle { +.video .video-wrapper .video-controls .slider .ui-slider-handle { transform-origin: bottom left; transition: all 0.7s ease-in-out 0s; box-sizing: border-box; @@ -417,13 +416,13 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b box-shadow: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider .ui-slider-handle:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider .ui-slider-handle:hover { +.video .video-wrapper .video-controls .slider .ui-slider-handle:focus, +.video .video-wrapper .video-controls .slider .ui-slider-handle:hover { background-color: #db8baf; border-color: #db8baf; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .vcr { +.video .video-wrapper .video-controls .vcr { float: left; list-style: none; border-right: 1px solid #282c2e; @@ -431,21 +430,21 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b } @media (max-width: 1120px) { - .xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .vcr { + .video .video-wrapper .video-controls .vcr { margin-right: lh(0.5); font-size: 0.875em; } } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .vcr .video_control:focus { +.video .video-wrapper .video-controls .vcr .video_control:focus { position: relative; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .vcr .video_control.skip { +.video .video-wrapper .video-controls .vcr .video_control.skip { white-space: nowrap; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .vcr .vidtime { +.video .video-wrapper .video-controls .vcr .vidtime { padding-left: lh(0.75); display: inline-block; color: #cfd8dc; @@ -453,38 +452,38 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b } @media (max-width: 1120px) { - .xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .vcr .vidtime { + .video .video-wrapper .video-controls .vcr .vidtime { padding-left: lh(0.5); } } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls { +.video .video-wrapper .video-controls .secondary-controls { float: right; border-left: 1px dotted #4f595d; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .add-fullscreen, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .grouped-controls, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .auto-advance, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .quality-control { +.video .video-wrapper .video-controls .secondary-controls .volume, +.video .video-wrapper .video-controls .secondary-controls .add-fullscreen, +.video .video-wrapper .video-controls .secondary-controls .grouped-controls, +.video .video-wrapper .video-controls .secondary-controls .auto-advance, +.video .video-wrapper .video-controls .secondary-controls .quality-control { border-left: 1px dotted #4f595d; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speed-button:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume>.control:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .add-fullscreen:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .auto-advance:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .quality-control:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .toggle-transcript:focus { +.video .video-wrapper .video-controls .secondary-controls .speed-button:focus, +.video .video-wrapper .video-controls .secondary-controls .volume>.control:focus, +.video .video-wrapper .video-controls .secondary-controls .add-fullscreen:focus, +.video .video-wrapper .video-controls .secondary-controls .auto-advance:focus, +.video .video-wrapper .video-controls .secondary-controls .quality-control:focus, +.video .video-wrapper .video-controls .secondary-controls .toggle-transcript:focus { position: relative; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container { +.video .video-wrapper .video-controls .secondary-controls .menu-container { position: relative; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu { +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu { transition: none; position: absolute; display: none; @@ -499,12 +498,12 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b list-style: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li { +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li { color: #e7ecee; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang { +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang { text-align: left; display: block; width: 100%; @@ -520,84 +519,84 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b white-space: nowrap; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option:hover, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option:focus, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang:hover, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang:focus { +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option:hover, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .speed-option:focus, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang:hover, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li .control-lang:focus { background-color: #4f595d; color: #fcfcfc; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li.is-active .speed-option, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li.is-active .control-lang { +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li.is-active .speed-option, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li.is-active .control-lang { border-left: calc(var(--baseline, 20px) / 10) solid #90d7f9; font-weight: var(--font-bold, 700); color: #90d7f9; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container.is-opened .menu { +.video .video-wrapper .video-controls .secondary-controls .menu-container.is-opened .menu { display: block; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speeds, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .lang, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .grouped-controls { +.video .video-wrapper .video-controls .secondary-controls .speeds, +.video .video-wrapper .video-controls .secondary-controls .lang, +.video .video-wrapper .video-controls .secondary-controls .grouped-controls { display: inline-block; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speeds.is-opened .control .icon { +.video .video-wrapper .video-controls .secondary-controls .speeds.is-opened .control .icon { transform: rotate(-90deg); } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .label { +.video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .label { padding: 0 calc((var(--baseline, 20px) / 3)) 0 0; font-family: var(--font-family-sans-serif); color: #e7ecee; } @media (max-width: 1120px) { - .xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .label { + .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .label { position: absolute; clip: rect(1px, 1px, 1px, 1px); } } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .value { +.video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .value { padding: 0 lh(0.5) 0 0; color: #e7ecee; font-weight: bold; } @media (max-width: 1120px) { - .xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .value { + .video .video-wrapper .video-controls .secondary-controls .speeds .speed-button .value { padding: 0 lh(0.5); } } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .lang .language-menu { +.video .video-wrapper .video-controls .secondary-controls .lang .language-menu { width: var(--baseline, 20px); padding: calc((var(--baseline, 20px) / 2)) 0; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .lang.is-opened .control .icon { +.video .video-wrapper .video-controls .secondary-controls .lang.is-opened .control .icon { transform: rotate(90deg); } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume { +.video .video-wrapper .video-controls .secondary-controls .volume { display: inline-block; position: relative; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume.is-opened .volume-slider-container { +.video .video-wrapper .video-controls .secondary-controls .volume.is-opened .volume-slider-container { display: block; opacity: 1; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume:not(:first-child)>a { +.video .video-wrapper .video-controls .secondary-controls .volume:not(:first-child)>a { border-left: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container { +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container { transition: none; display: none; position: absolute; @@ -608,7 +607,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b background-color: #282c2e; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider { +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider { height: 100px; width: calc((var(--baseline, 20px) / 4)); margin: 14px auto; @@ -617,7 +616,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b background: #4f595d; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle { +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle { transition: height var(--tmg-s2, 2s) ease-in-out 0s, width var(--tmg-s2, 2s) ease-in-out 0s; left: -5px; box-sizing: border-box; @@ -630,59 +629,59 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b box-shadow: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle:hover, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle:focus { +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle:hover, +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle:focus { background: #db8baf; border-color: #db8baf; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-range { +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-range { background: #8e3e63; border: 1px solid #8e3e63; left: -1px; bottom: -1px; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .quality-control { +.video .video-wrapper .video-controls .secondary-controls .quality-control { font-weight: 700; letter-spacing: -1px; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .quality-control.active { +.video .video-wrapper .video-controls .secondary-controls .quality-control.active { color: #0ea6ec; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .quality-control.is-hidden, -.xmodule_display.xmodule_VideoBlock .video.closed .video-wrapper .video-controls .secondary-controls .quality-control.subtitles { +.video .video-wrapper .video-controls .secondary-controls .quality-control.is-hidden, +.video.closed .video-wrapper .video-controls .secondary-controls .quality-control.subtitles { display: none !important; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .toggle-transcript.is-active { +.video .video-wrapper .video-controls .secondary-controls .toggle-transcript.is-active { color: #0ea6ec; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .lang>.hide-subtitles { +.video .video-wrapper .video-controls .secondary-controls .lang>.hide-subtitles { transition: none; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper:hover .video-controls .slider { +.video .video-wrapper:hover .video-controls .slider { height: calc((var(--baseline, 20px) / 1.5)); } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper:hover .video-controls .slider .ui-slider-handle { +.video .video-wrapper:hover .video-controls .slider .ui-slider-handle { height: calc((var(--baseline, 20px) / 1.5)); width: calc((var(--baseline, 20px) / 1.5)); } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .closed-captions { +.video.video-fullscreen .closed-captions { width: 65%; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen.closed .closed-captions { +.video.video-fullscreen.closed .closed-captions { width: 90%; } -.xmodule_display.xmodule_VideoBlock .video .subtitles { +.video .subtitles { float: left; overflow: auto; max-height: 460px; @@ -692,18 +691,18 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b visibility: visible; } -.xmodule_display.xmodule_VideoBlock .video .subtitles a { +.video .subtitles a { color: #0074b5; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu { +.video .subtitles .subtitles-menu { height: 100%; margin: 0; padding: 0 3px; list-style: none; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li { +.video .subtitles .subtitles-menu li { margin-bottom: 8px; border: 0; padding: 0; @@ -711,48 +710,48 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b line-height: lh(); } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li:has(> span:empty) { +.video .subtitles .subtitles-menu li:has(> span:empty) { display: none; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li span { +.video .subtitles .subtitles-menu li span { display: block; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li.current { +.video .subtitles .subtitles-menu li.current { color: #333; font-weight: 700; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li.focused { +.video .subtitles .subtitles-menu li.focused { outline: #000 dotted thin; outline-offset: -1px; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li:hover, -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li:focus { +.video .subtitles .subtitles-menu li:hover, +.video .subtitles .subtitles-menu li:focus { text-decoration: underline; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li:empty { +.video .subtitles .subtitles-menu li:empty { margin-bottom: 0; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li.spacing:last-of-type { +.video .subtitles .subtitles-menu li.spacing:last-of-type { position: relative; } -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li.spacing:last-of-type .transcript-end { +.video .subtitles .subtitles-menu li.spacing:last-of-type .transcript-end { position: absolute; bottom: 0; } -.xmodule_display.xmodule_VideoBlock .video.closed .video-wrapper { +.video.closed .video-wrapper { width: 100%; background-color: inherit; } -.xmodule_display.xmodule_VideoBlock .video.closed .video-wrapper .video-controls.html5 { +.video.closed .video-wrapper .video-controls.html5 { bottom: 0; left: 0; right: 0; @@ -760,16 +759,16 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b z-index: 1; } -.xmodule_display.xmodule_VideoBlock .video.closed .video-wrapper .video-player-pre, -.xmodule_display.xmodule_VideoBlock .video.closed .video-wrapper .video-player-post { +.video.closed .video-wrapper .video-player-pre, +.video.closed .video-wrapper .video-player-post { height: 0; } -.xmodule_display.xmodule_VideoBlock .video.closed .video-wrapper .video-player h3 { +.video.closed .video-wrapper .video-player h3 { color: black; } -.xmodule_display.xmodule_VideoBlock .video.closed .subtitles.html5 { +.video.closed .subtitles.html5 { background-color: rgba(243, 243, 243, 0.8); height: 100%; position: absolute; @@ -781,7 +780,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b display: none; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen { +.video.video-fullscreen { background: rgba(0, 0, 0, 0.95); border: 0; bottom: 0; @@ -796,58 +795,58 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b border-radius: 0; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen.closed .tc-wrapper .video-wrapper { +.video.video-fullscreen.closed .tc-wrapper .video-wrapper { width: 100%; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .video-wrapper .video-player-pre, -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .video-wrapper .video-player-post { +.video.video-fullscreen .video-wrapper .video-player-pre, +.video.video-fullscreen .video-wrapper .video-player-post { height: 0; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .video-wrapper { +.video.video-fullscreen .video-wrapper { position: static; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .video-wrapper .video-player h3 { +.video.video-fullscreen .video-wrapper .video-player h3 { color: white; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper { +.video.video-fullscreen .tc-wrapper { width: 100%; height: 100%; position: static; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper:after { +.video.video-fullscreen .tc-wrapper:after { content: ""; display: table; clear: both; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper .video-wrapper { +.video.video-fullscreen .tc-wrapper .video-wrapper { height: 100%; width: 75%; margin-right: 0; vertical-align: middle; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper .video-wrapper object, -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper .video-wrapper iframe, -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper .video-wrapper video { +.video.video-fullscreen .tc-wrapper .video-wrapper object, +.video.video-fullscreen .tc-wrapper .video-wrapper iframe, +.video.video-fullscreen .tc-wrapper .video-wrapper video { position: absolute; width: auto; height: auto; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper .video-controls { +.video.video-fullscreen .tc-wrapper .video-controls { position: absolute; bottom: 0; left: 0; width: 100%; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .subtitles { +.video.video-fullscreen .subtitles { height: 100%; width: 25%; padding: lh(); @@ -857,22 +856,22 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b visibility: visible; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .subtitles li { +.video.video-fullscreen .subtitles li { color: #aaa; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .subtitles li.current { +.video.video-fullscreen .subtitles li.current { color: var(--white, #fff); } -.xmodule_display.xmodule_VideoBlock .video.is-touch .tc-wrapper .video-wrapper object, -.xmodule_display.xmodule_VideoBlock .video.is-touch .tc-wrapper .video-wrapper iframe, -.xmodule_display.xmodule_VideoBlock .video.is-touch .tc-wrapper .video-wrapper video { +.video.is-touch .tc-wrapper .video-wrapper object, +.video.is-touch .tc-wrapper .video-wrapper iframe, +.video.is-touch .tc-wrapper .video-wrapper video { width: 100%; height: 100%; } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll { +.video .video-pre-roll { position: absolute; top: 0; left: 0; @@ -884,11 +883,11 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b background-color: var(--black, #000); } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll.is-html5 { +.video .video-pre-roll.is-html5 { background-size: 15%; } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll .btn-play.btn-pre-roll { +.video .video-pre-roll .btn-play.btn-pre-roll { padding: var(--baseline, 20px); border: none; border-radius: var(--baseline, 20px); @@ -896,69 +895,69 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b box-shadow: none; } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll .btn-play.btn-pre-roll::after { +.video .video-pre-roll .btn-play.btn-pre-roll::after { display: none; } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll .btn-play.btn-pre-roll img { +.video .video-pre-roll .btn-play.btn-pre-roll img { height: calc((var(--baseline, 20px) * 4)); width: calc((var(--baseline, 20px) * 4)); } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll .btn-play.btn-pre-roll:hover, -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll .btn-play.btn-pre-roll:focus { +.video .video-pre-roll .btn-play.btn-pre-roll:hover, +.video .video-pre-roll .btn-play.btn-pre-roll:focus { background: var(--blue, #0075b4); } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .slider .ui-slider-handle, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu li, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle, -.xmodule_display.xmodule_VideoBlock .video .subtitles .subtitles-menu li, -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li { +.video .video-wrapper .video-controls .slider .ui-slider-handle, +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu li, +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container .volume-slider .ui-slider-handle, +.video .subtitles .subtitles-menu li, +.a11y-menu-container .a11y-menu-list li { cursor: pointer; } -.xmodule_display.xmodule_VideoBlock .video.closed .subtitles.html5 { +.video.closed .subtitles.html5 { z-index: 0; } -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .menu-container .menu, -.xmodule_display.xmodule_VideoBlock .video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container { +.video .video-wrapper .video-controls .secondary-controls .menu-container .menu, +.video .video-wrapper .video-controls .secondary-controls .volume .volume-slider-container { z-index: 10; } -.xmodule_display.xmodule_VideoBlock .video .video-pre-roll, -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list { +.video .video-pre-roll, +.a11y-menu-container .a11y-menu-list { z-index: 1000; } -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen, -.xmodule_display.xmodule_VideoBlock .video.video-fullscreen .tc-wrapper .video-controls, -.xmodule_display.xmodule_VideoBlock .overlay { +.video.video-fullscreen, +.video.video-fullscreen .tc-wrapper .video-controls, +.overlay { z-index: 10000; } -.xmodule_display.xmodule_VideoBlock .contextmenu, -.xmodule_display.xmodule_VideoBlock .submenu { +.contextmenu, +.submenu { z-index: 100000; } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container>a::after { +.video-tracks .a11y-menu-container>a::after { font-family: FontAwesome; -webkit-font-smoothing: antialiased; display: inline-block; speak: none; } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container { +.a11y-menu-container { position: relative; } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container.open .a11y-menu-list { +.a11y-menu-container.open .a11y-menu-list { display: block; } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list { +.a11y-menu-container .a11y-menu-list { top: 100%; margin: 0; padding: 0; @@ -969,14 +968,14 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b border: 1px solid #eee; } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li { +.a11y-menu-container .a11y-menu-list li { margin: 0; padding: 0; border-bottom: 1px solid #eee; color: var(--white, #fff); } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li a { +.a11y-menu-container .a11y-menu-list li a { display: block; overflow: hidden; text-overflow: ellipsis; @@ -986,37 +985,37 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b line-height: 23px; } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li a:hover, -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li a:focus { +.a11y-menu-container .a11y-menu-list li a:hover, +.a11y-menu-container .a11y-menu-list li a:focus { color: var(--gray-d1, #5e5e5e); } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li.active a { +.a11y-menu-container .a11y-menu-list li.active a { color: #009fe6; } -.xmodule_display.xmodule_VideoBlock .a11y-menu-container .a11y-menu-list li:last-child { +.a11y-menu-container .a11y-menu-list li:last-child { box-shadow: none; border-bottom: 0; margin-top: 0; } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container { +.video-tracks .a11y-menu-container { display: inline-block; vertical-align: top; border-left: 1px solid #eee; } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container.open>a { +.video-tracks .a11y-menu-container.open>a { background-color: var(--action-primary-active-bg, #0075b4); color: var(--very-light-text, white); } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container.open>a::after { +.video-tracks .a11y-menu-container.open>a::after { color: var(--very-light-text, white); } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container>a { +.video-tracks .a11y-menu-container>a { transition: all var(--tmg-f2, 0.25s) ease-in-out 0s; font-size: 12px; display: block; @@ -1031,7 +1030,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b text-overflow: ellipsis; } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container>a::after { +.video-tracks .a11y-menu-container>a::after { content: "\f0d7"; position: absolute; right: calc((var(--baseline, 20px) * 0.5)); @@ -1039,15 +1038,15 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b color: var(--lighter-base-font-color, #646464); } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container .a11y-menu-list { +.video-tracks .a11y-menu-container .a11y-menu-list { right: 0; } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container .a11y-menu-list li { +.video-tracks .a11y-menu-container .a11y-menu-list li { font-size: 0.875em; } -.xmodule_display.xmodule_VideoBlock .video-tracks .a11y-menu-container .a11y-menu-list li a { +.video-tracks .a11y-menu-container .a11y-menu-list li a { border: 0; display: block; padding: 0.70788em; @@ -1056,8 +1055,8 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b white-space: nowrap; } -.xmodule_display.xmodule_VideoBlock .contextmenu, -.xmodule_display.xmodule_VideoBlock .submenu { +.contextmenu, +.submenu { border: 1px solid #333; background: var(--white, #fff); color: #333; @@ -1072,57 +1071,57 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b white-space: nowrap; } -.xmodule_display.xmodule_VideoBlock .contextmenu.is-opened, -.xmodule_display.xmodule_VideoBlock .submenu.is-opened { +.contextmenu.is-opened, +.submenu.is-opened { display: block; } -.xmodule_display.xmodule_VideoBlock .contextmenu .menu-item, -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item, -.xmodule_display.xmodule_VideoBlock .submenu .menu-item, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item { +.contextmenu .menu-item, +.contextmenu .submenu-item, +.submenu .menu-item, +.submenu .submenu-item { border-top: 1px solid var(--gray-l3, #c8c8c8); padding: calc((var(--baseline, 20px) / 4)) calc((var(--baseline, 20px) / 2)); outline: none; } -.xmodule_display.xmodule_VideoBlock .contextmenu .menu-item>span, -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item>span, -.xmodule_display.xmodule_VideoBlock .submenu .menu-item>span, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item>span { +.contextmenu .menu-item>span, +.contextmenu .submenu-item>span, +.submenu .menu-item>span, +.submenu .submenu-item>span { color: #333; } -.xmodule_display.xmodule_VideoBlock .contextmenu .menu-item:first-child, -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item:first-child, -.xmodule_display.xmodule_VideoBlock .submenu .menu-item:first-child, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item:first-child { +.contextmenu .menu-item:first-child, +.contextmenu .submenu-item:first-child, +.submenu .menu-item:first-child, +.submenu .submenu-item:first-child { border-top: none; } -.xmodule_display.xmodule_VideoBlock .contextmenu .menu-item:focus, -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item:focus, -.xmodule_display.xmodule_VideoBlock .submenu .menu-item:focus, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item:focus { +.contextmenu .menu-item:focus, +.contextmenu .submenu-item:focus, +.submenu .menu-item:focus, +.submenu .submenu-item:focus { background: #333; color: var(--white, #fff); } -.xmodule_display.xmodule_VideoBlock .contextmenu .menu-item:focus>span, -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item:focus>span, -.xmodule_display.xmodule_VideoBlock .submenu .menu-item:focus>span, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item:focus>span { +.contextmenu .menu-item:focus>span, +.contextmenu .submenu-item:focus>span, +.submenu .menu-item:focus>span, +.submenu .submenu-item:focus>span { color: var(--white, #fff); } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item { +.contextmenu .submenu-item, +.submenu .submenu-item { position: relative; padding: calc((var(--baseline, 20px) / 4)) var(--baseline, 20px) calc((var(--baseline, 20px) / 4)) calc((var(--baseline, 20px) / 2)); } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item::after, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item::after { +.contextmenu .submenu-item::after, +.submenu .submenu-item::after { content: '\25B6'; position: absolute; right: 5px; @@ -1130,39 +1129,39 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b font-size: 10px; } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item .submenu, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item .submenu { +.contextmenu .submenu-item .submenu, +.submenu .submenu-item .submenu { display: none; } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item.is-opened, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item.is-opened { +.contextmenu .submenu-item.is-opened, +.submenu .submenu-item.is-opened { background: #333; color: var(--white, #fff); } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item.is-opened>span, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item.is-opened>span { +.contextmenu .submenu-item.is-opened>span, +.submenu .submenu-item.is-opened>span { color: var(--white, #fff); } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item.is-opened>.submenu, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item.is-opened>.submenu { +.contextmenu .submenu-item.is-opened>.submenu, +.submenu .submenu-item.is-opened>.submenu { display: block; } -.xmodule_display.xmodule_VideoBlock .contextmenu .submenu-item .is-selected, -.xmodule_display.xmodule_VideoBlock .submenu .submenu-item .is-selected { +.contextmenu .submenu-item .is-selected, +.submenu .submenu-item .is-selected { font-weight: bold; } -.xmodule_display.xmodule_VideoBlock .contextmenu .is-disabled, -.xmodule_display.xmodule_VideoBlock .submenu .is-disabled { +.contextmenu .is-disabled, +.submenu .is-disabled { pointer-events: none; color: var(--gray-l3, #c8c8c8); } -.xmodule_display.xmodule_VideoBlock .overlay { +.overlay { position: fixed; top: 0; left: 0; @@ -1171,7 +1170,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b background-color: transparent; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .social-toggle-btn { +.wrapper-social-share .social-toggle-btn { background: var(--primary); font-size: 13px; font-weight: 700; @@ -1183,16 +1182,16 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b border: none; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .social-toggle-btn:hover, -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .social-toggle-btn:focus { +.wrapper-social-share .social-toggle-btn:hover, +.wrapper-social-share .social-toggle-btn:focus { background: var(--btn-brand-focus-background); } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .social-toggle-btn .fa { +.wrapper-social-share .social-toggle-btn .fa { margin-right: calc(var(--baseline) * 0.4); } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share { +.wrapper-social-share .container-social-share { padding: calc(var(--baseline) * 0.4); width: 300px; border-radius: 6px; @@ -1200,7 +1199,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b box-shadow: rgba(0, 0, 0, 0.15) 0 0.5rem 1rem, rgba(0, 0, 0, 0.15) 0 0.25rem 0.625rem; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .close-btn { +.wrapper-social-share .container-social-share .close-btn { float: right; cursor: pointer; vertical-align: top; @@ -1209,7 +1208,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b text-decoration: none !important; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .social-share-link { +.wrapper-social-share .container-social-share .social-share-link { margin-right: calc(var(--baseline) * 0.2); font-size: 24px; height: 24px; @@ -1218,14 +1217,14 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b display: inline-flex; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .social-share-link > span > svg { +.wrapper-social-share .container-social-share .social-share-link > span > svg { width: auto; height: 24px; vertical-align: top; display: inline-flex; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .public-video-url-container { +.wrapper-social-share .container-social-share .public-video-url-container { padding: calc(var(--baseline) * 0.4); display: flex; align-items: center; @@ -1233,7 +1232,7 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b background-color: #f2f0ef; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .public-video-url-link { +.wrapper-social-share .container-social-share .public-video-url-link { color: var(--black); overflow: hidden; text-overflow: ellipsis; @@ -1241,18 +1240,18 @@ https://github.com/openedx/edx-platform/blob/master/xmodule/static/css-builtin-b white-space: nowrap; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .public-video-url-link:hover { +.wrapper-social-share .container-social-share .public-video-url-link:hover { text-decoration: underline; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .public-video-copy-btn { +.wrapper-social-share .container-social-share .public-video-copy-btn { margin-left: calc(var(--baseline) * 0.7); flex-shrink: 0; color: var(--primary); cursor: pointer; } -.xmodule_display.xmodule_VideoBlock .wrapper-social-share .container-social-share .public-video-copy-btn:hover { +.wrapper-social-share .container-social-share .public-video-copy-btn:hover { text-decoration: none; color: var(--link-hover-color); } diff --git a/xblocks_contrib/video/assets/package.json b/xblocks_contrib/video/assets/package.json new file mode 100644 index 00000000..cf96b551 --- /dev/null +++ b/xblocks_contrib/video/assets/package.json @@ -0,0 +1,26 @@ +{ + "name": "video-xblock", + "version": "1.0.0", + "directories": { + "test": "tests" + }, + "scripts": { + "build": "webpack --config webpack.prod.config.js", + "build-dev": "webpack --config webpack.dev.config.js" + }, + "dependencies": { + "draggabilly": "^3", + "edx-ui-toolkit": "^1", + "hls.js": "^1", + "underscore": "^1", + "webpack-merge": "^6" + }, + "devDependencies": { + "copy-webpack-plugin": "^13", + "clean-webpack-plugin": "^4", + "webpack": "^5", + "webpack-cli": "^5", + "webpack-manifest-plugin": "^5" + } + } + \ No newline at end of file diff --git a/xblocks_contrib/video/assets/webpack.common.config.js b/xblocks_contrib/video/assets/webpack.common.config.js new file mode 100644 index 00000000..ec92b097 --- /dev/null +++ b/xblocks_contrib/video/assets/webpack.common.config.js @@ -0,0 +1,45 @@ +const path = require('path'); +const webpack = require('webpack'); +const {CleanWebpackPlugin} = require('clean-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const config = { + entry: { + 'video-xblock': path.resolve(__dirname, 'js/src/10_main.js'), + }, + output: { + path: path.resolve(__dirname, '../public/js'), + filename: '[name].js', + clean: true, + publicPath: '/' + }, + resolve: { + alias: { + 'edx-ui-toolkit/js': 'edx-ui-toolkit/src/js', + 'hls': 'hls.js/dist/hls.js' + } + }, + externals: { + $: 'jQuery', + jquery: 'jQuery', + jQuery: 'jQuery' + }, + plugins: [ + new CleanWebpackPlugin(), + new webpack.DefinePlugin({ + 'process.env.JS_ENV_EXTRA_CONFIG': JSON.parse(process.env.JS_ENV_EXTRA_CONFIG || '{}'), + 'CAPTIONS_CONTENT_TO_REPLACE': JSON.stringify(process.env.CAPTIONS_CONTENT_TO_REPLACE || ''), + 'CAPTIONS_CONTENT_REPLACEMENT': JSON.stringify(process.env.CAPTIONS_CONTENT_REPLACEMENT || '') + }), + new CopyWebpackPlugin({ + patterns: [ + { + from: 'css/video.css', + to: path.resolve(__dirname, '../public/css') + } + ] + }) + ] +}; + +module.exports = config diff --git a/xblocks_contrib/video/assets/webpack.dev.config.js b/xblocks_contrib/video/assets/webpack.dev.config.js new file mode 100644 index 00000000..a83b6b2a --- /dev/null +++ b/xblocks_contrib/video/assets/webpack.dev.config.js @@ -0,0 +1,19 @@ +const {merge} = require('webpack-merge'); +const webpack = require('webpack'); +const _ = require('underscore'); +const commonConfig = require('./webpack.common.config.js'); + +module.exports = commonConfig; + +module.exports = merge(commonConfig, { + mode: 'development', + devtool: 'source-map', + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development'), + 'process.env.JS_ENV_EXTRA_CONFIG': process.env.JS_ENV_EXTRA_CONFIG || '{}', + 'CAPTIONS_CONTENT_TO_REPLACE': JSON.stringify(process.env.CAPTIONS_CONTENT_TO_REPLACE || ''), + 'CAPTIONS_CONTENT_REPLACEMENT': JSON.stringify(process.env.CAPTIONS_CONTENT_REPLACEMENT || '') + }) + ] +}); diff --git a/xblocks_contrib/video/assets/webpack.prod.config.js b/xblocks_contrib/video/assets/webpack.prod.config.js new file mode 100644 index 00000000..5a403669 --- /dev/null +++ b/xblocks_contrib/video/assets/webpack.prod.config.js @@ -0,0 +1,20 @@ +const {merge} = require('webpack-merge'); +const webpack = require('webpack'); +const _ = require('underscore'); +const commonConfig = require('./webpack.common.config.js'); + +module.exports = commonConfig; + +module.exports = merge(commonConfig, { + mode: 'production', + devtool: false, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production'), + 'process.env.JS_ENV_EXTRA_CONFIG': process.env.JS_ENV_EXTRA_CONFIG || '{}', + 'CAPTIONS_CONTENT_TO_REPLACE': JSON.stringify(process.env.CAPTIONS_CONTENT_TO_REPLACE || ''), + 'CAPTIONS_CONTENT_REPLACEMENT': JSON.stringify(process.env.CAPTIONS_CONTENT_REPLACEMENT || '') + }) + ] + +}); diff --git a/xblocks_contrib/video/bumper_utils.py b/xblocks_contrib/video/bumper_utils.py index e2792d5e..64b02e91 100644 --- a/xblocks_contrib/video/bumper_utils.py +++ b/xblocks_contrib/video/bumper_utils.py @@ -1,5 +1,3 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/master/xmodule/video_block/bumper_utils.py """ Utils for video bumper """ diff --git a/xblocks_contrib/video/cache_utils.py b/xblocks_contrib/video/cache_utils.py index 45ff22b2..77a97c74 100644 --- a/xblocks_contrib/video/cache_utils.py +++ b/xblocks_contrib/video/cache_utils.py @@ -1,15 +1,8 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/master/openedx/core/lib/cache_utils.py -""" -Utilities related to caching. -""" - - +""" Cache Utils for Video Block """ import itertools import wrapt from django.utils.encoding import force_str - from edx_django_utils.cache import RequestCache diff --git a/xblocks_contrib/video/constants.py b/xblocks_contrib/video/constants.py new file mode 100644 index 00000000..17bf5750 --- /dev/null +++ b/xblocks_contrib/video/constants.py @@ -0,0 +1,17 @@ +""" +Constants used by DjangoXBlockUserService +""" + +# This is the view that will be rendered to display the XBlock in the LMS. +# It will also be used to render the block in "preview" mode in Studio, unless +# the XBlock also implements author_view. +STUDENT_VIEW = 'student_view' + +# This is the view that will be rendered to display the XBlock in the LMS for unenrolled learners. +# Implementations of this view should assume that a user and user data are not available. +PUBLIC_VIEW = 'public_view' + +# The personally identifiable user ID. +ATTR_KEY_USER_ID = 'edx-platform.user_id' +# The country code determined from the user's request IP address. +ATTR_KEY_REQUEST_COUNTRY_CODE = 'edx-platform.request_country_code' diff --git a/xblocks_contrib/video/content.py b/xblocks_contrib/video/content.py index 0cbbdcf4..461e278e 100644 --- a/xblocks_contrib/video/content.py +++ b/xblocks_contrib/video/content.py @@ -1,8 +1,9 @@ -# NOTE: Original code has been copied from the following file: -# https://github.com/openedx/openedx-platform/blob/master/xmodule/contentstore/content.py#L28 +""" Video Block Static Content, class copied from StaticContent class in edx-platform/xmodule/contentstore/content.py """ +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import AssetKey +from opaque_keys.edx.locator import AssetLocator - -class StaticContent: # lint-amnesty, pylint: disable=missing-class-docstring +class VideoBlockStaticContent: # lint-amnesty, pylint: disable=missing-class-docstring def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None, length=None, locked=False, content_digest=None): self.location = loc diff --git a/xblocks_contrib/video/mixin.py b/xblocks_contrib/video/mixin.py index dd212089..b75b466e 100644 --- a/xblocks_contrib/video/mixin.py +++ b/xblocks_contrib/video/mixin.py @@ -1,7 +1,5 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/master/openedx/core/lib/license/mixin.py """ -License mixin for XBlocks and XModules +License mixin for XBlocks """ from xblock.core import XBlockMixin diff --git a/xblocks_contrib/video/studio_metadata_mixin.py b/xblocks_contrib/video/studio_metadata_mixin.py new file mode 100644 index 00000000..1c3808c3 --- /dev/null +++ b/xblocks_contrib/video/studio_metadata_mixin.py @@ -0,0 +1,165 @@ +""" Studio Metadata Mixin""" +from django.conf import settings +from xblock.core import XBlock +from xblock.fields import Dict, Float, Integer, List, Scope, String + +from xblocks_contrib.video.exceptions import TranscriptNotFoundError +from xblocks_contrib.video.video_transcripts_utils import TranscriptExtensions, get_html5_ids +from xblocks_contrib.video.video_xfields import RelativeTime + + +class StudioMetadataMixin: + """ + Mixin providing Studio metadata editing capabilities for XBlocks. + """ + + @property + def non_editable_metadata_fields(self): + """ + Return the list of fields that should not be editable in Studio. + + When overriding, be sure to append to the superclasses' list. + """ + # We are not allowing editing of xblock tag and name fields at this time (for any component). + return [XBlock.tags, XBlock.name] + + def _create_metadata_editor_info(self, field): + """ + Creates the information needed by the metadata editor for a specific field. + """ + + def jsonify_value(field, json_choice): + """ + Convert field value to JSON, if needed. + """ + if isinstance(json_choice, dict): + new_json_choice = dict(json_choice) # make a copy so below doesn't change the original + if "display_name" in json_choice: + new_json_choice["display_name"] = get_text(json_choice["display_name"]) + if "value" in json_choice: + new_json_choice["value"] = field.to_json(json_choice["value"]) + else: + new_json_choice = field.to_json(json_choice) + return new_json_choice + + def get_text(value): + """Localize a text value that might be None.""" + if value is None: + return None + else: + return self.runtime.service(self, "i18n").ugettext(value) + + # gets the 'default_value' and 'explicitly_set' attrs + metadata_field_editor_info = self.runtime.get_field_provenance(self, field) + metadata_field_editor_info["field_name"] = field.name + metadata_field_editor_info["display_name"] = get_text(field.display_name) + metadata_field_editor_info["help"] = get_text(field.help) + metadata_field_editor_info["value"] = field.read_json(self) + + # We support the following editors: + # 1. A select editor for fields with a list of possible values (includes Booleans). + # 2. Number editors for integers and floats. + # 3. A generic string editor for anything else (editing JSON representation of the value). + editor_type = "Generic" + values = field.values + if "values_provider" in field.runtime_options: + values = field.runtime_options["values_provider"](self) + if isinstance(values, (tuple, list)) and len(values) > 0: + editor_type = "Select" + values = [jsonify_value(field, json_choice) for json_choice in values] + elif isinstance(field, Integer): + editor_type = "Integer" + elif isinstance(field, Float): + editor_type = "Float" + elif isinstance(field, List): + editor_type = "List" + elif isinstance(field, Dict): + editor_type = "Dict" + elif isinstance(field, RelativeTime): + editor_type = "RelativeTime" + elif isinstance(field, String) and field.name == "license": + editor_type = "License" + metadata_field_editor_info["type"] = editor_type + metadata_field_editor_info["options"] = [] if values is None else values + + return metadata_field_editor_info + + def _get_editable_metadata_fields(self): + """ + Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`. + + Can be limited by extending `non_editable_metadata_fields`. + """ + metadata_fields = {} + + # Only use the fields from this class, not mixins + fields = getattr(self, "unmixed_class", self.__class__).fields + + for field in fields.values(): + if field in self.non_editable_metadata_fields: + continue + if field.scope not in (Scope.settings, Scope.content): + continue + + metadata_fields[field.name] = self._create_metadata_editor_info(field) + + return metadata_fields + + @property + def editable_metadata_fields(self): + """ + Returns the metadata fields to be edited in Studio. + """ + editable_fields = self._get_editable_metadata_fields() + + settings_service = self.runtime.service(self, 'settings') + if settings_service: + xb_settings = settings_service.get_settings_bucket(self) + if not xb_settings.get("licensing_enabled", False) and "license" in editable_fields: + del editable_fields["license"] + + # Default Timed Transcript a.k.a `sub` has been deprecated and end users shall + # not be able to modify it. + editable_fields.pop('sub') + + languages = [{'label': label, 'code': lang} for lang, label in settings.ALL_LANGUAGES] + languages.sort(key=lambda l: l['label']) + editable_fields['transcripts']['custom'] = True + editable_fields['transcripts']['languages'] = languages + editable_fields['transcripts']['type'] = 'VideoTranslations' + + # We need to send ajax requests to show transcript status + # whenever edx_video_id changes on frontend. Thats why we + # are changing type to `VideoID` so that a specific + # Backbonjs view can handle it. + editable_fields['edx_video_id']['type'] = 'VideoID' + + # `public_access` is a boolean field and by default backbonejs code render it as a dropdown with 2 options + # but in our case we also need to show an input field with dropdown, the input field will show the url to + # be shared with leaners. This is not possible with default rendering logic in backbonjs code, that is why + # we are setting a new type and then do a custom rendering in backbonejs code to render the desired UI. + editable_fields['public_access']['type'] = 'PublicAccess' + editable_fields['public_access']['url'] = self.get_public_video_url() + + # construct transcripts info and also find if `en` subs exist + transcripts_info = self.get_transcripts_info() + possible_sub_ids = [self.sub, self.youtube_id_1_0] + get_html5_ids(self.html5_sources) + video_config_service = self.runtime.service(self, 'video_config') + if video_config_service: + for sub_id in possible_sub_ids: + try: + _, sub_id, _ = video_config_service.get_transcript(self, lang='en', output_format=TranscriptExtensions.TXT) + transcripts_info['transcripts'] = dict(transcripts_info['transcripts'], en=sub_id) + break + except TranscriptNotFoundError: + continue + + editable_fields['transcripts']['value'] = transcripts_info['transcripts'] + editable_fields['transcripts']['urlRoot'] = self.runtime.handler_url( + self, + 'studio_transcript', + 'translation' + ).rstrip('/?') + editable_fields['handout']['type'] = 'FileUploader' + + return editable_fields diff --git a/xblocks_contrib/video/templates/video.html b/xblocks_contrib/video/templates/video.html index 87d3a631..362ae3df 100644 --- a/xblocks_contrib/video/templates/video.html +++ b/xblocks_contrib/video/templates/video.html @@ -1,5 +1,3 @@ - {% load i18n %} {% if display_name is not None and not is_embed %} @@ -16,7 +14,7 @@

{{ display_name|escape }}

data-block-id='{{ block_id|escape }}' data-course-id='{{ course_id|escape }}' tabindex="-1" -> + data-package-name="xblocks-contrib">
diff --git a/xblocks_contrib/video/validation.py b/xblocks_contrib/video/validation.py index 7af278df..2496f54c 100644 --- a/xblocks_contrib/video/validation.py +++ b/xblocks_contrib/video/validation.py @@ -1,5 +1,3 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/master/xmodule/validation.py """ Extension of XBlock Validation class to include information for presentation in Studio. """ diff --git a/xblocks_contrib/video/video.py b/xblocks_contrib/video/video.py index 4d0bcd0c..bcb7f3a9 100644 --- a/xblocks_contrib/video/video.py +++ b/xblocks_contrib/video/video.py @@ -1,4 +1,3 @@ -# NOTE: Code has been copied from the source file https://github.com/openedx/edx-platform/blob/master/xmodule/video_block/video_block.py """Video is ungraded Xmodule for support video content. It's new improved video block, which support additional feature: - Can play non-YouTube video sources via in-browser HTML5 video player. @@ -13,7 +12,6 @@ https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv """ - import copy import json import logging @@ -21,48 +19,49 @@ from operator import itemgetter from django.conf import settings +from django.utils.translation import gettext_noop as _ from edx_django_utils.cache import RequestCache from lxml import etree -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locator import AssetLocator from web_fragments.fragment import Fragment from xblock.completable import XBlockCompletionMode -from xblock.core import XBlock -from xblock.fields import ScopeIds -from xblock.runtime import KvsFieldData -from xblocks_contrib.video import VideoBlock as _ExtractedVideoBlock +from xblock.core import Scope, XBlock +from xblock.fields import ScopeIds, UserScope from xblock.utils.resources import ResourceLoader -from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE, ATTR_KEY_USER_ID -from openedx.core.lib.cache_utils import request_cached -from openedx.core.lib.license import LicenseMixin -from xmodule.contentstore.content import StaticContent -from xmodule.editing_block import EditingMixin -from xmodule.exceptions import NotFoundError -from xmodule.modulestore.inheritance import InheritanceKeyValueStore -from xmodule.raw_block import EmptyDataRawMixin -from xmodule.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_fragment -from xmodule.validation import StudioValidation, StudioValidationMessage -from xmodule.x_module import ( - PUBLIC_VIEW, STUDENT_VIEW, - ResourceTemplates, - XModuleMixin, XModuleToXBlockMixin, +from xblocks_contrib.common.utils import get_resource_url +from xblocks_contrib.common.xml_utils import ( + EdxJSONEncoder, + LegacyXmlMixin, + deserialize_field, + is_pointer_tag, + name_to_pathname, ) -from xmodule.xml_block import XmlMixin, deserialize_field, is_pointer_tag, name_to_pathname -from .bumper_utils import bumperize -from openedx.core.djangoapps.video_config.transcripts_utils import ( - Transcript, +from xblocks_contrib.video.ajax_handler_mixin import AjaxHandlerMixin +from xblocks_contrib.video.bumper_utils import bumperize +from xblocks_contrib.video.cache_utils import request_cached +from xblocks_contrib.video.constants import ATTR_KEY_REQUEST_COUNTRY_CODE, ATTR_KEY_USER_ID, PUBLIC_VIEW, STUDENT_VIEW +from xblocks_contrib.video.content import VideoBlockStaticContent +from xblocks_contrib.video.exceptions import TranscriptNotFoundError +from xblocks_contrib.video.mixin import LicenseMixin +from xblocks_contrib.video.studio_metadata_mixin import StudioMetadataMixin +from xblocks_contrib.video.validation import StudioValidation, StudioValidationMessage +from xblocks_contrib.video.video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers +from xblocks_contrib.video.video_transcripts_utils import ( + TranscriptExtensions, VideoTranscriptsMixin, clean_video_id, get_endonym_or_label, - get_html5_ids, - subs_filename + subs_filename, ) -from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers -from .video_utils import create_youtube_string, format_xml_exception_message, get_poster, rewrite_video_url -from .video_xfields import VideoFields - -from xblocks_contrib.video.exceptions import TranscriptNotFoundError +from xblocks_contrib.video.video_utils import ( + create_youtube_string, + format_xml_exception_message, + get_poster, + rewrite_video_url, +) +from xblocks_contrib.video.video_xfields import VideoFields # The following import/except block for edxval is temporary measure until # edxval is a proper XBlock Runtime Service. @@ -96,7 +95,7 @@ edxval_api = None log = logging.getLogger(__name__) -loader = ResourceLoader("lms") +loader = ResourceLoader(__name__) # Make '_' a no-op so we can scrape strings. Using lambda instead of # `django.utils.translation.ugettext_noop` because Django cannot be imported in this file @@ -107,11 +106,12 @@ @XBlock.wants('settings', 'completion', 'request_cache', 'video_config') -@XBlock.needs('mako', 'user', 'i18n') -class _BuiltInVideoBlock( - VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, VideoStudentViewHandlers, - EmptyDataRawMixin, XmlMixin, EditingMixin, XModuleToXBlockMixin, - ResourceTemplates, XModuleMixin, LicenseMixin): +@XBlock.needs('i18n', 'user') +class VideoBlock( + VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, VideoStudentViewHandlers, + LegacyXmlMixin, XBlock, + AjaxHandlerMixin, StudioMetadataMixin, + LicenseMixin): """ XML source example: """ - is_extracted = False + is_extracted = True has_custom_completion = True completion_mode = XBlockCompletionMode.COMPLETABLE @@ -144,11 +144,56 @@ class _BuiltInVideoBlock( } ] - mako_template = "widgets/tabs-aggregator.html" js_module_name = "TabsEditingDescriptor" uses_xmodule_styles_setup = True + @property + def course_id(self): + return self.location.course_key + + @property + def category(self): + return self.scope_ids.block_type + + @property + def location(self): + return self.scope_ids.usage_id + + @location.setter + def location(self, value): + assert isinstance(value, UsageKey) + self.scope_ids = self.scope_ids._replace( + def_id=value, # Note: assigning a UsageKey as def_id is OK in old mongo / import system but wrong in split + usage_id=value, + ) + + @property + def url_name(self): + return self.location.block_id + + @property + def display_name_with_default(self): + """ + Return a display name for the module: use display_name if defined in + metadata, otherwise convert the url name. + """ + return ( + self.display_name if self.display_name is not None + else self.location.block_id.replace('_', ' ') + ) + + @property + def xblock_kvs(self): + """ + Retrieves the internal KeyValueStore for this XModule. + + Should only be used by the persistence layer. Use with caution. + """ + # if caller wants kvs, caller's assuming it's up to date; so, decache it + self.save() + return self._field_data._kvs # pylint: disable=protected-access + def get_transcripts_for_student(self, transcripts, dest_lang=None): """Return transcript information necessary for rendering the XModule student view. This is more or less a direct extraction from `get_html`. @@ -176,7 +221,7 @@ def get_transcripts_for_student(self, transcripts, dest_lang=None): try: label = get_endonym_or_label(lang_code) languages[lang_code] = label - except NotFoundError: + except TranscriptNotFoundError: continue if not other_lang or (other_lang and sub): @@ -238,14 +283,29 @@ def prioritize_hls(self, youtube_streams, html5_sources): return False - def student_view(self, context): + def studio_view(self, context): # pylint: disable=unused-argument + """ + Returns a fragment having ``data-metadata`` attribute containing + ``editable_metadata_fields`` as JSON. The authoring MFE (frontend-app-authoring) + reads this attribute from the block's HTML to power the video editor UI + (e.g. transcript parsing via ``parseTranscripts``and related thunk actions). + See: https://github.com/openedx/frontend-app-authoring/blob/master/src/editors/data/redux/thunkActions/video.js#L163 + + This matches the contract of the built-in video block: + edx-platform's ``mako_block.studio_view`` (xmodule/mako_block.py) uses + cms/templates/widgets/metadata-edit.html, which also serializes + ``editable_metadata_fields`` to JSON in a ``data-metadata`` attribute. + """ + return Fragment(f'
') + + def student_view(self, context=None): """ Return the student view. """ fragment = Fragment(self.get_html(context=context)) - add_css_to_fragment(fragment, 'VideoBlockDisplay.css') - add_webpack_js_to_fragment(fragment, 'VideoBlockDisplay') - fragment.initialize_js('Video') + fragment.add_css_url(get_resource_url(self, "css/video.css", "video")) + fragment.add_javascript_url(get_resource_url(self, "js/video-xblock.js", "video")) + fragment.initialize_js("Video") return fragment def author_view(self, context): @@ -265,9 +325,9 @@ def public_view(self, context): return self.student_view(context) fragment = Fragment(self.get_html(view=PUBLIC_VIEW, context=context)) - add_css_to_fragment(fragment, 'VideoBlockDisplay.css') - add_webpack_js_to_fragment(fragment, 'VideoBlockDisplay') - fragment.initialize_js('Video') + fragment.add_css_url(get_resource_url(self, "css/video.css", "video")) + fragment.add_javascript_url(get_resource_url(self, "js/video-xblock.js", "video")) + fragment.initialize_js("Video") return fragment def get_html(self, view=STUDENT_VIEW, context=None): # lint-amnesty, pylint: disable=arguments-differ, too-many-statements @@ -500,7 +560,7 @@ def is_transcript_feedback_enabled(self): video_config_service = self.runtime.service(self, 'video_config') feature_enabled = video_config_service.is_transcript_feedback_enabled( self.context_key) if video_config_service else False - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except log.exception(f"Error retrieving course for course ID: {self.context_key}") return False return feature_enabled @@ -572,62 +632,6 @@ def save_with_metadata(self, user_id): self.save() self.runtime.modulestore.update_item(self, user_id) - @property - def editable_metadata_fields(self): - editable_fields = super().editable_metadata_fields - - settings_service = self.runtime.service(self, 'settings') - if settings_service: - xb_settings = settings_service.get_settings_bucket(self) - if not xb_settings.get("licensing_enabled", False) and "license" in editable_fields: - del editable_fields["license"] - - # Default Timed Transcript a.k.a `sub` has been deprecated and end users shall - # not be able to modify it. - editable_fields.pop('sub') - - languages = [{'label': label, 'code': lang} for lang, label in settings.ALL_LANGUAGES] - languages.sort(key=lambda l: l['label']) - editable_fields['transcripts']['custom'] = True - editable_fields['transcripts']['languages'] = languages - editable_fields['transcripts']['type'] = 'VideoTranslations' - - # We need to send ajax requests to show transcript status - # whenever edx_video_id changes on frontend. Thats why we - # are changing type to `VideoID` so that a specific - # Backbonjs view can handle it. - editable_fields['edx_video_id']['type'] = 'VideoID' - - # `public_access` is a boolean field and by default backbonejs code render it as a dropdown with 2 options - # but in our case we also need to show an input field with dropdown, the input field will show the url to - # be shared with leaners. This is not possible with default rendering logic in backbonjs code, that is why - # we are setting a new type and then do a custom rendering in backbonejs code to render the desired UI. - editable_fields['public_access']['type'] = 'PublicAccess' - editable_fields['public_access']['url'] = self.get_public_video_url() - - # construct transcripts info and also find if `en` subs exist - transcripts_info = self.get_transcripts_info() - possible_sub_ids = [self.sub, self.youtube_id_1_0] + get_html5_ids(self.html5_sources) - video_config_service = self.runtime.service(self, 'video_config') - if video_config_service: - for sub_id in possible_sub_ids: - try: - _, sub_id, _ = video_config_service.get_transcript(self, lang='en', output_format=Transcript.TXT) - transcripts_info['transcripts'] = dict(transcripts_info['transcripts'], en=sub_id) - break - except TranscriptNotFoundError: - continue - - editable_fields['transcripts']['value'] = transcripts_info['transcripts'] - editable_fields['transcripts']['urlRoot'] = self.runtime.handler_url( - self, - 'studio_transcript', - 'translation' - ).rstrip('/?') - editable_fields['handout']['type'] = 'FileUploader' - - return editable_fields - @classmethod def parse_xml_new_runtime(cls, node, runtime, keys): """ @@ -660,17 +664,19 @@ def parse_xml(cls, node, runtime, _keys): filepath = cls._format_filepath(node.tag, name_to_pathname(url_name)) node = cls.load_file(filepath, runtime.resources_fs, usage_id) aside_children = runtime.parse_asides(node, definition_id, usage_id, runtime.id_generator) + + scope_ids = ScopeIds(None, block_type, definition_id, usage_id) + video = runtime.construct_xblock_from_class(cls, scope_ids) + field_data = cls.parse_video_xml(node, runtime.id_generator) - kvs = InheritanceKeyValueStore(initial_values=field_data) - field_data = KvsFieldData(kvs) - video = runtime.construct_xblock_from_class( - cls, - # We're loading a descriptor, so student_id is meaningless - # We also don't have separate notions of definition and usage ids yet, - # so we use the location for both - ScopeIds(None, block_type, definition_id, usage_id), - field_data, - ) + + for field_name, value in field_data.items(): + # The 'xml_attributes' field has a special setter logic in its Field class, + # so we must handle it carefully to avoid duplicating data. + if field_name == "xml_attributes": + video.xml_attributes.update(value) + elif field_name in video.fields: + setattr(video, field_name, cls.fields[field_name].from_json(value)) # Update VAL with info extracted from `node` video.edx_video_id = video.import_video_info_into_val( @@ -976,10 +982,10 @@ def parse_video_xml(cls, xml, id_generator=None): course_id = getattr(id_generator, 'target_course_id', None) # Update the handout location with current course_id if 'handout' in field_data and course_id: - handout_location = StaticContent.get_location_from_path(field_data['handout']) + handout_location = VideoBlockStaticContent.get_location_from_path(field_data['handout']) if isinstance(handout_location, AssetLocator): - handout_new_location = StaticContent.compute_location(course_id, handout_location.path) - field_data['handout'] = StaticContent.serialize_asset_key_with_slash(handout_new_location) + handout_new_location = VideoBlockStaticContent.compute_location(course_id, handout_location.path) + field_data['handout'] = VideoBlockStaticContent.serialize_asset_key_with_slash(handout_new_location) # For backwards compatibility: Add `source` if XML doesn't have `download_video` # attribute. @@ -1053,7 +1059,7 @@ def _update_transcript_for_index(language=None): if video_config_service: try: transcript = video_config_service.get_transcript( - self, lang=language, output_format=Transcript.TXT + self, lang=language, output_format=TranscriptExtensions.TXT )[0].replace("\n", " ") transcript_index_name = f"transcript_{language if language else self.transcript_language}" video_body.update({transcript_index_name: transcript}) @@ -1191,9 +1197,95 @@ def _poster(self): ) return None + def bind_for_student(self, user_id, wrappers=None): + """ + Set up this XBlock to act as an XModule instead of an XModuleDescriptor. -VideoBlock = ( - _ExtractedVideoBlock if settings.USE_EXTRACTED_VIDEO_BLOCK - else _BuiltInVideoBlock -) -VideoBlock.__name__ = "VideoBlock" + Arguments: + user_id: The user_id to set in scope_ids + wrappers: These are a list functions that put a wrapper, such as + LmsFieldData or OverrideFieldData, around the field_data. + Note that the functions will be applied in the order in + which they're listed. So [f1, f2] -> f2(f1(field_data)) + """ + + # Skip rebinding if we're already bound a user, and it's this user. + if self.scope_ids.user_id is not None and user_id == self.scope_ids.user_id: + if getattr(self.runtime, "position", None): + self.position = self.runtime.position # update the position of the tab + return + + # If we are switching users mid-request, save the data from the old user. + self.save() + + # Update scope_ids to point to the new user. + self.scope_ids = self.scope_ids._replace(user_id=user_id) + + # Clear out any cached instantiated children. + self.clear_child_cache() + + # Clear out any cached field data scoped to the old user. + for field in self.fields.values(): + if field.scope in (Scope.parent, Scope.children): + continue + + if field.scope.user == UserScope.ONE: + field._del_cached_value(self) # pylint: disable=protected-access + # not the most elegant way of doing this, but if we're removing + # a field from the module's field_data_cache, we should also + # remove it from its _dirty_fields + if field in self._dirty_fields: + del self._dirty_fields[field] + + if wrappers: + # Put user-specific wrappers around the field-data service for this block. + # Note that these are different from modulestore.xblock_field_data_wrappers, which are not user-specific. + wrapped_field_data = self.runtime.service(self, "field-data-unbound") + for wrapper in wrappers: + wrapped_field_data = wrapper(wrapped_field_data) + self._bound_field_data = wrapped_field_data + if getattr(self.runtime, "uses_deprecated_field_data", False): + # This approach is deprecated but OldModuleStoreRuntime still requires it. + # For SplitModuleStoreRuntime, don't set ._field_data this way. + self._field_data = wrapped_field_data + + @classmethod + def definition_from_xml(cls, xml_object, system): # lint-amnesty, pylint: disable=unused-argument + if len(xml_object) == 0 and len(list(xml_object.items())) == 0: + return {"data": ""}, [] + return {"data": etree.tostring(xml_object, pretty_print=True, encoding="unicode")}, [] + + def get_explicitly_set_fields_by_scope(self, scope=Scope.content): + """ + Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including + any set to None.) + """ + result = {} + for field in self.fields.values(): + if field.scope == scope and field.is_set_on(self): + try: + result[field.name] = field.read_json(self) + except TypeError as exception: + exception_message = f"{exception}, Block-location:{self.location}, Field-name:{field.name}" + raise TypeError(exception_message) from exception + return result + + @staticmethod + def workbench_scenarios(): + """Create canned scenario for display in the workbench.""" + return [ + ( + "VideoBlock", + """<_video_extracted/> + """, + ), + ( + "Multiple VideoBlock", + """ + <_video_extracted/> + <_video_extracted/> + <_video_extracted/> + + """, + ), + ] diff --git a/xblocks_contrib/video/video_handlers.py b/xblocks_contrib/video/video_handlers.py index 9c8958f4..d60525ce 100644 --- a/xblocks_contrib/video/video_handlers.py +++ b/xblocks_contrib/video/video_handlers.py @@ -1,5 +1,3 @@ -# NOTE: Original code has been copied from the following files: -# https://github.com/openedx/edx-platform/blob/master/xmodule/video_block/video_handlers.py """ Handlers for video block. @@ -16,20 +14,13 @@ from opaque_keys.edx.locator import CourseLocator from webob import Response from xblock.core import XBlock +from xblock.fields import RelativeTime from xblock.exceptions import JsonHandlerError from xblock.fields import RelativeTime -from xmodule.exceptions import NotFoundError - -from openedx.core.djangoapps.video_config.transcripts_utils import ( - Transcript, - clean_video_id, - subs_filename, -) -from xblocks_contrib.video.exceptions import ( - TranscriptsGenerationException, - TranscriptNotFoundError, -) +from xblocks_contrib.video.exceptions import TranscriptNotFoundError, TranscriptsGenerationException +from xblocks_contrib.video.video_transcripts_utils import TranscriptExtensions, clean_video_id, subs_filename +from xblocks_contrib.video.video_utils import load_metadata_from_youtube log = logging.getLogger(__name__) @@ -123,7 +114,7 @@ def handle_ajax(self, dispatch, data): log.debug(f"GET {data}") log.debug(f"DISPATCH {dispatch}") - raise NotFoundError('Unexpected dispatch type') + raise TranscriptNotFoundError('Unexpected dispatch type') def get_static_transcript(self, request, transcripts): """ @@ -282,7 +273,7 @@ def transcript(self, request, dispatch): content, filename, mimetype = get_transcript( self, lang=self.transcript_language, - output_format=Transcript.SJSON, + output_format=TranscriptExtensions.SJSON, youtube_id=youtube_id, is_bumper=is_bumper ) @@ -362,7 +353,6 @@ def yt_video_metadata(self, request, suffix=''): # lint-amnesty, pylint: disabl This handler is only used in the openedx_content-based runtime. The old runtime uses a similar REST API that's not an XBlock handler. """ - from lms.djangoapps.courseware.views.views import load_metadata_from_youtube if not self.youtube_id_1_0: # TODO: more informational response to explain that yt_video_metadata not supported for non-youtube videos. return Response('{}', status=400) @@ -432,7 +422,7 @@ def studio_transcript(self, request, dispatch): Return filename from storage. SRT format is sent back on success. Filename should be in GET dict. We raise all exceptions right in Studio: - NotFoundError: + TranscriptNotFoundError: Video or asset was deleted from module/contentstore, but request came later. Seems impossible to be raised. block_render.py catches NotFoundErrors from here. @@ -534,7 +524,7 @@ def _studio_transcript_get(self, request): if not video_config_service: return Response(status=404) transcript_content, transcript_name, mime_type = video_config_service.get_transcript( - self, lang=language, output_format=Transcript.SRT + self, lang=language, output_format=TranscriptExtensions.SRT ) response = Response(transcript_content, headerlist=[ ( diff --git a/xblocks_contrib/video/video_transcripts_utils.py b/xblocks_contrib/video/video_transcripts_utils.py index b0ab330e..0a4b9b6a 100644 --- a/xblocks_contrib/video/video_transcripts_utils.py +++ b/xblocks_contrib/video/video_transcripts_utils.py @@ -1,24 +1,17 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/farhan/remove-unused-video-code/openedx/core/djangoapps/video_config/transcripts_utils.py """ -Utility functions for transcripts. -++++++++++++++++++++++++++++++++++ +Utility functions for video block transcripts. +++++++++++++++++++++++++++++++++++++++++++++++ """ + import copy -import html import logging -import os -from functools import wraps -import simplejson as json from django.conf import settings from django.utils.translation import get_language_info -from pysrt import SubRipFile, SubRipItem, SubRipTime -from pysrt.srtexc import Error from xblocks_contrib.video.bumper_utils import get_bumper_settings -from xblocks_contrib.video.exceptions import NotFoundError, TranscriptsGenerationException +from xblocks_contrib.video.exceptions import TranscriptNotFoundError try: from edxval import api as edxval_api @@ -31,9 +24,9 @@ NON_EXISTENT_TRANSCRIPT = 'non_existent_dummy_file_name' -class Transcript: +class TranscriptExtensions: """ - Container for transcript methods. + Video block transcript extensions. """ SRT = 'srt' TXT = 'txt' @@ -134,7 +127,6 @@ def get_transcripts_info(self, is_bumper=False): # TODO: This causes a circular import when imported at the top-level. # This import will be removed as part of the VideoBlock extraction. # https://github.com/openedx/edx-platform/issues/36282 - from xmodule.video_block.bumper_utils import get_bumper_settings if is_bumper: transcripts = copy.deepcopy(get_bumper_settings(self).get('transcripts', {})) @@ -238,4 +230,4 @@ def get_endonym_or_label(language_code): return potential_generic_label log.error("A label was requested for language code `%s` but the code is completely unknown", language_code) - raise NotFoundError(f"Unknown language `{language_code}`") + raise TranscriptNotFoundError(f"Unknown language `{language_code}`") diff --git a/xblocks_contrib/video/video_utils.py b/xblocks_contrib/video/video_utils.py index ddd099c1..b777f0d5 100644 --- a/xblocks_contrib/video/video_utils.py +++ b/xblocks_contrib/video/video_utils.py @@ -1,17 +1,18 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/master/xmodule/video_block/video_utils.py """ Module contains utils specific for video_block but not for transcripts. """ +import json import logging from collections import OrderedDict from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit +import requests from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import URLValidator +from requests.exceptions import Timeout log = logging.getLogger(__name__) @@ -124,7 +125,6 @@ def set_query_parameter(url, param_name, param_value): return urlunsplit((scheme, netloc, path, new_query_string, fragment)) - def load_metadata_from_youtube(video_id, request): """ Get metadata about a YouTube video. diff --git a/xblocks_contrib/video/video_xfields.py b/xblocks_contrib/video/video_xfields.py index 5e6f19d9..09d0a40d 100644 --- a/xblocks_contrib/video/video_xfields.py +++ b/xblocks_contrib/video/video_xfields.py @@ -1,9 +1,22 @@ -# NOTE: Code has been copied from the following source files -# https://github.com/openedx/edx-platform/blob/master/xmodule/video_block/video_xfields.py#L17-L222 +""" # lint-amnesty, pylint: disable=cyclic-import +XFields for video block. +""" + + +import datetime + +from xblock.fields import Boolean, DateTime, Dict, Float, List, RelativeTime, Scope, String + +# TODO: Review the following _ function +# Make '_' a no-op so we can scrape strings. Using lambda instead of +# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file +_ = lambda text: text class VideoFields: """Fields for `VideoBlock`.""" + data = String(default="", scope=Scope.content) + display_name = String( help=_("The display name for this component."), display_name=_("Component Display Name"),