refactor: use video block utils from xblocks-contrib package (#38088)

* refactor: use bumper_utils from xblocks-contrib package
* refactor: use video_handlers from xblocks-contrib package
* fix: fix test_video_handlers test cases
This commit is contained in:
Muhammad Farhan Khan
2026-03-09 17:13:16 +05:00
committed by GitHub
parent 68a53b8506
commit 9b6445cb7b
9 changed files with 14 additions and 716 deletions

View File

@@ -46,6 +46,7 @@ from openedx.core.djangoapps.video_config.services import VideoConfigService
from openedx.core.djangoapps.discussions.services import DiscussionConfigService
from openedx.core.lib.xblock_services.call_to_action import CallToActionService
from xmodule.contentstore.django import contentstore
from xblocks_contrib.video.exceptions import TranscriptNotFoundError
from xmodule.exceptions import NotFoundError as XModuleNotFoundError
from xmodule.library_tools import LegacyLibraryToolsService
from xmodule.modulestore.django import XBlockI18nService, modulestore
@@ -975,7 +976,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
# If we can't find the block, respond with a 404
except (XModuleNotFoundError, NotFoundError):
except (XModuleNotFoundError, NotFoundError, TranscriptNotFoundError):
log.exception("Module indicating to user that request doesn't exist")
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from

View File

@@ -320,7 +320,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo): # lint-amnesty, p
assert sorted(json.loads(response.body.decode('utf-8'))) == sorted(['en', 'uk'])
@patch('openedx.core.djangoapps.video_config.transcripts_utils.get_video_transcript_content')
@patch('openedx.core.djangoapps.video_config.transcripts_utils.get_available_transcript_languages')
@patch('edxval.api.get_available_transcript_languages')
@ddt.data(
(
['en', 'uk', 'ro'],
@@ -504,7 +504,7 @@ class TestTranscriptDownloadDispatch(TestVideo): # lint-amnesty, pylint: disabl
assert response.status == '404 Not Found'
@patch(
'xmodule.video_block.video_handlers.get_transcript',
'xblocks_contrib.video.video_handlers.get_transcript',
return_value=('Subs!', 'test_filename.srt', 'application/x-subrip; charset=utf-8')
)
def test_download_srt_exist(self, __):
@@ -515,7 +515,7 @@ class TestTranscriptDownloadDispatch(TestVideo): # lint-amnesty, pylint: disabl
assert response.headers['Content-Language'] == 'en'
@patch(
'xmodule.video_block.video_handlers.get_transcript',
'xblocks_contrib.video.video_handlers.get_transcript',
return_value=('Subs!', 'txt', 'text/plain; charset=utf-8')
)
def test_download_txt_exist(self, __):
@@ -545,7 +545,6 @@ class TestTranscriptDownloadDispatch(TestVideo): # lint-amnesty, pylint: disabl
assert response.headers['Content-Disposition'] == 'attachment; filename="en_塞.srt"'
@patch('openedx.core.djangoapps.video_config.transcripts_utils.edxval_api.get_video_transcript_data')
@patch('xmodule.video_block.get_transcript', Mock(side_effect=NotFoundError))
def test_download_fallback_transcript(self, mock_get_video_transcript_data):
"""
Verify val transcript is returned as a fallback if it is not found in the content store.

View File

@@ -49,7 +49,8 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
from xmodule.tests.helpers import mock_render_template, override_descriptor_system # pylint: disable=unused-import
from xmodule.tests.test_import import DummyModuleStoreRuntime
from xmodule.tests.test_video import VideoBlockTestBase
from xmodule.video_block import VideoBlock, bumper_utils, video_utils
from xmodule.video_block import VideoBlock, video_utils
from xblocks_contrib.video import bumper_utils
from openedx.core.djangoapps.video_config.transcripts_utils import Transcript, save_to_store, subs_filename
from xmodule.video_block.video_block import EXPORT_IMPORT_COURSE_DIR, EXPORT_IMPORT_STATIC_DIR
from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
@@ -2323,7 +2324,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
# Use temporary FEATURES in this test without affecting the original
FEATURES = dict(settings.FEATURES)
@patch('xmodule.video_block.bumper_utils.get_bumper_settings')
@patch('xblocks_contrib.video.bumper_utils.get_bumper_settings')
def test_is_bumper_enabled(self, get_bumper_settings):
"""
Check that bumper is (not)shown if ENABLE_VIDEO_BUMPER is (False)True
@@ -2348,8 +2349,8 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
assert not bumper_utils.is_bumper_enabled(self.block)
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch('xmodule.video_block.bumper_utils.is_bumper_enabled')
@patch('xmodule.video_block.bumper_utils.get_bumper_settings')
@patch('xblocks_contrib.video.bumper_utils.is_bumper_enabled')
@patch('xblocks_contrib.video.bumper_utils.get_bumper_settings')
@patch('edxval.api.get_urls_for_profiles')
def test_bumper_metadata(
self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled, mock_render_django_template

View File

@@ -29,6 +29,7 @@ from openedx.core.djangoapps.content_libraries.api import (
add_library_block_static_asset_file,
delete_library_block_static_asset_file,
)
from openedx.core.djangoapps.video_config.sharing_sites import sharing_sites_info_for_video
from openedx.core.djangoapps.video_config.transcripts_utils import (
Transcript,
clean_video_id,
@@ -93,7 +94,6 @@ class VideoConfigService:
organization = get_course_organization(course_key)
from openedx.core.djangoapps.video_config.sharing_sites import sharing_sites_info_for_video
sharing_sites_info = sharing_sites_info_for_video(
public_video_url,
organization=organization

View File

@@ -28,6 +28,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
from xblocks_contrib.video.bumper_utils import get_bumper_settings
from xblocks_contrib.video.exceptions import TranscriptsGenerationException
@@ -786,11 +787,6 @@ class VideoTranscriptsMixin:
is_bumper(bool): If True, the request is for the bumper transcripts
include_val_transcripts(bool): If True, include edx-val transcripts as well
"""
# 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', {}))
sub = transcripts.pop("en", "")

View File

@@ -2,7 +2,6 @@
Container for video block and its utils.
"""
from .bumper_utils import *
from openedx.core.djangoapps.video_config.transcripts_utils import * # lint-amnesty, pylint: disable=redefined-builtin
from .video_block import *
from .video_utils import *

View File

@@ -1,147 +0,0 @@
"""
Utils for video bumper
"""
import copy
import json
import logging
from collections import OrderedDict
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from django.conf import settings
from .video_utils import set_query_parameter
try:
import edxval.api as edxval_api
except ImportError:
edxval_api = None
log = logging.getLogger(__name__)
def get_bumper_settings(video):
"""
Get bumper settings from video instance.
"""
bumper_settings = copy.deepcopy(getattr(video, 'video_bumper', {}))
# clean up /static/ prefix from bumper transcripts
for lang, transcript_url in bumper_settings.get('transcripts', {}).items():
bumper_settings['transcripts'][lang] = transcript_url.replace("/static/", "")
return bumper_settings
def is_bumper_enabled(video):
"""
Check if bumper enabled.
- Feature flag ENABLE_VIDEO_BUMPER should be set to True
- Do not show again button should not be clicked by user.
- Current time minus periodicity must be greater that last time viewed
- edxval_api should be presented
Returns:
bool.
"""
bumper_last_view_date = getattr(video, 'bumper_last_view_date', None)
utc_now = datetime.now(ZoneInfo("UTC"))
periodicity = settings.FEATURES.get('SHOW_BUMPER_PERIODICITY', 0)
has_viewed = any([
video.bumper_do_not_show_again,
(bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now)
])
is_studio = getattr(video.runtime, "is_author_mode", False)
return bool(
not is_studio and
settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and
get_bumper_settings(video) and
edxval_api and
not has_viewed
)
def bumperize(video):
"""
Populate video with bumper settings, if they are presented.
"""
video.bumper = {
'enabled': False,
'edx_video_id': "",
'transcripts': {},
'metadata': None,
}
if not is_bumper_enabled(video):
return
bumper_settings = get_bumper_settings(video)
try:
video.bumper['edx_video_id'] = bumper_settings['video_id']
video.bumper['transcripts'] = bumper_settings['transcripts']
except (TypeError, KeyError):
log.warning(
"Could not retrieve video bumper information from course settings"
)
return
sources = get_bumper_sources(video)
if not sources:
return
video.bumper.update({
'metadata': bumper_metadata(video, sources),
'enabled': True, # Video poster needs this.
})
def get_bumper_sources(video):
"""
Get bumper sources from edxval.
Returns list of sources.
"""
try:
val_profiles = ["desktop_webm", "desktop_mp4"]
val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles)
bumper_sources = [url for url in [val_video_urls[p] for p in val_profiles] if url]
except edxval_api.ValInternalError:
# if no bumper sources, nothing will be showed
log.warning(
"Could not retrieve information from VAL for Bumper edx Video ID: %s.", video.bumper['edx_video_id']
)
return []
return bumper_sources
def bumper_metadata(video, sources):
"""
Generate bumper metadata.
"""
transcripts = video.get_transcripts_info(is_bumper=True)
unused_track_url, bumper_transcript_language, bumper_languages = video.get_transcripts_for_student(transcripts)
metadata = OrderedDict({
'saveStateUrl': video.ajax_url + '/save_user_state',
'showCaptions': json.dumps(video.show_captions),
'sources': sources,
'streams': '',
'transcriptLanguage': bumper_transcript_language,
'transcriptLanguages': bumper_languages,
'transcriptTranslationUrl': set_query_parameter(
video.runtime.handler_url(video, 'transcript', 'translation/__lang__').rstrip('/?'), 'is_bumper', 1
),
'transcriptAvailableTranslationsUrl': set_query_parameter(
video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1
),
'publishCompletionUrl': set_query_parameter(
video.runtime.handler_url(video, 'publish_completion', '').rstrip('?'), 'is_bumper', 1
),
})
return metadata

View File

@@ -48,7 +48,7 @@ from xmodule.x_module import (
XModuleMixin, XModuleToXBlockMixin,
)
from xmodule.xml_block import XmlMixin, deserialize_field, is_pointer_tag, name_to_pathname
from .bumper_utils import bumperize
from xblocks_contrib.video.bumper_utils import bumperize
from openedx.core.djangoapps.video_config.transcripts_utils import (
Transcript,
VideoTranscriptsMixin,
@@ -57,7 +57,7 @@ from openedx.core.djangoapps.video_config.transcripts_utils import (
get_html5_ids,
subs_filename
)
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
from xblocks_contrib.video.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

View File

@@ -1,551 +0,0 @@
"""
Handlers for video block.
StudentViewHandlers are handlers for video block instance.
StudioViewHandlers are handlers for video descriptor instance.
"""
import json
import logging
import math
from django.utils.timezone import now
from opaque_keys.edx.locator import CourseLocator
from webob import Response
from xblock.core import XBlock
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,
)
log = logging.getLogger(__name__)
def get_transcript(
video_block,
lang: str | None = None,
output_format: str = 'srt',
youtube_id: str | None = None,
is_bumper: bool = False,
) -> tuple[bytes, str, str]:
"""
Retrieve a transcript using a video block's configuration service.
Returns:
tuple(bytes, str, str): transcript content, filename, and mimetype.
Raises:
Exception: If the video config service is not available or the transcript cannot be retrieved.
"""
video_config_service = video_block.runtime.service(video_block, 'video_config')
if not video_config_service:
raise Exception("Video config service not found")
return video_config_service.get_transcript(video_block, lang, output_format, youtube_id, is_bumper)
# Disable no-member warning:
# pylint: disable=no-member
def to_boolean(value):
"""
Convert a value from a GET or POST request parameter to a bool
"""
if isinstance(value, bytes):
value = value.decode('ascii', errors='replace')
if isinstance(value, str):
return value.lower() == 'true'
else:
return bool(value)
class VideoStudentViewHandlers:
"""
Handlers for video block instance.
"""
global_speed = None
transcript_language = None
def handle_ajax(self, dispatch, data):
"""
Update values of xfields, that were changed by student.
"""
accepted_keys = [
'speed', 'auto_advance', 'saved_video_position', 'transcript_language',
'transcript_download_format', 'youtube_is_available',
'bumper_last_view_date', 'bumper_do_not_show_again'
]
conversions = {
'speed': json.loads,
'auto_advance': json.loads,
'saved_video_position': RelativeTime.isotime_to_timedelta,
'youtube_is_available': json.loads,
'bumper_last_view_date': to_boolean,
'bumper_do_not_show_again': to_boolean,
}
if dispatch == 'save_user_state':
for key in data:
if key in accepted_keys:
if key in conversions:
value = conversions[key](data[key])
else:
value = data[key]
if key == 'bumper_last_view_date':
value = now()
if key == 'speed' and math.isnan(value):
message = f"Invalid speed value {value}, must be a float."
log.warning(message)
return json.dumps({'success': False, 'error': message})
setattr(self, key, value)
if key == 'speed':
self.global_speed = self.speed
return json.dumps({'success': True})
log.debug(f"GET {data}")
log.debug(f"DISPATCH {dispatch}")
raise NotFoundError('Unexpected dispatch type')
def get_static_transcript(self, request, transcripts):
"""
Courses that are imported with the --nostatic flag do not show
transcripts/captions properly even if those captions are stored inside
their static folder. This adds a last resort method of redirecting to
the static asset path of the course if the transcript can't be found
inside the contentstore and the course has the static_asset_path field
set.
transcripts (dict): A dict with all transcripts and a sub.
"""
response = Response(status=404)
# Only do redirect for English
if not self.transcript_language == 'en':
return response
# If this video lives in library, the code below is not relevant and will error.
if not isinstance(self.course_id, CourseLocator):
return response
video_id = request.GET.get('videoId', None)
if video_id:
transcript_name = video_id
else:
transcript_name = transcripts["sub"]
if transcript_name:
# Get the asset path for course
asset_path = None
course = self.runtime.modulestore.get_course(self.course_id)
if course.static_asset_path:
asset_path = course.static_asset_path
else:
# It seems static_asset_path is not set in any XMLModuleStore courses.
asset_path = getattr(course, 'data_dir', '')
if asset_path:
response = Response(
status=307,
location='/static/{}/{}'.format(
asset_path,
subs_filename(transcript_name, self.transcript_language)
)
)
return response
@XBlock.json_handler
def publish_completion(self, data, dispatch): # pylint: disable=unused-argument
"""
Entry point for completion for student_view.
Parameters:
data: JSON dict:
key: "completion"
value: float in range [0.0, 1.0]
dispatch: Ignored.
Return value: JSON response (200 on success, 400 for malformed data)
"""
completion_service = self.runtime.service(self, 'completion')
if completion_service is None:
raise JsonHandlerError(500, "No completion service found")
if not completion_service.completion_tracking_enabled():
raise JsonHandlerError(404, "Completion tracking is not enabled and API calls are unexpected")
if not isinstance(data['completion'], (int, float)):
message = "Invalid completion value {}. Must be a float in range [0.0, 1.0]"
raise JsonHandlerError(400, message.format(data['completion']))
if not 0.0 <= data['completion'] <= 1.0:
message = "Invalid completion value {}. Must be in range [0.0, 1.0]"
raise JsonHandlerError(400, message.format(data['completion']))
self.runtime.publish(self, "completion", data)
return {"result": "ok"}
@staticmethod
def make_transcript_http_response(content, filename, language, content_type, add_attachment_header=True):
"""
Construct `Response` object.
Arguments:
content (unicode): transcript content
filename (unicode): transcript filename
language (unicode): transcript language
mimetype (unicode): transcript content type
add_attachment_header (bool): whether to add attachment header or not
"""
headerlist = [
('Content-Language', language),
]
if add_attachment_header:
headerlist.append(
(
'Content-Disposition',
f'attachment; filename="{filename}"'
)
)
response = Response(
content,
headerlist=headerlist,
charset='utf8'
)
response.content_type = content_type
return response
@XBlock.handler
def transcript(self, request, dispatch):
"""
Entry point for transcript handlers for student_view.
Request GET contains:
(optional) `videoId` for `translation` dispatch.
`is_bumper=1` flag for bumper case.
Dispatches, (HTTP GET):
/translation/[language_id]
/download
/available_translations/
Explanations:
`download`: returns SRT or TXT file.
`translation`: depends on HTTP methods:
Provide translation for requested language, SJSON format is sent back on success,
Proper language_id should be in url.
`available_translations`:
Returns list of languages, for which transcript files exist.
For 'en' check if SJSON exists. For non-`en` check if SRT file exists.
"""
is_bumper = request.GET.get('is_bumper', False)
transcripts = self.get_transcripts_info(is_bumper)
if dispatch.startswith('translation'):
language = dispatch.replace('translation', '').strip('/')
# Because scrapers hit video blocks, verify that a user exists.
# use the _request attr to get the django request object.
if not request._request.user: # pylint: disable=protected-access
log.info("Transcript: user must be logged or public view enabled to get transcript")
return Response(status=403)
if not language:
log.info("Invalid /translation request: no language.")
return Response(status=400)
if language not in ['en'] + list(transcripts["transcripts"].keys()):
log.info("Video: transcript facilities are not available for given language.")
return Response(status=404)
if language != self.transcript_language:
self.transcript_language = language
try:
youtube_id = None if is_bumper else request.GET.get('videoId')
content, filename, mimetype = get_transcript(
self,
lang=self.transcript_language,
output_format=Transcript.SJSON,
youtube_id=youtube_id,
is_bumper=is_bumper
)
response = self.make_transcript_http_response(
content,
filename,
self.transcript_language,
mimetype,
add_attachment_header=False
)
except TranscriptNotFoundError as exc:
edx_video_id = clean_video_id(self.edx_video_id)
log.warning(
'[Translation Dispatch] %s: %s',
self.location,
exc if is_bumper else f'Transcript not found for {edx_video_id}, lang: {self.transcript_language}',
)
response = self.get_static_transcript(request, transcripts)
elif dispatch == 'download':
lang = request.GET.get('lang', None)
try:
content, filename, mimetype = get_transcript(self, lang, output_format=self.transcript_download_format)
except TranscriptNotFoundError:
return Response(status=404)
response = self.make_transcript_http_response(
content,
filename,
self.transcript_language,
mimetype
)
elif dispatch.startswith('available_translations'):
video_config_service = self.runtime.service(self, 'video_config')
if not video_config_service:
return Response(status=404)
available_translations = video_config_service.available_translations(
self,
transcripts,
verify_assets=True,
is_bumper=is_bumper
)
if available_translations:
response = Response(json.dumps(available_translations))
response.content_type = 'application/json'
else:
response = Response(status=404)
else: # unknown dispatch
log.debug("Dispatch is not allowed")
response = Response(status=404)
return response
@XBlock.handler
def student_view_user_state(self, request, suffix=''): # lint-amnesty, pylint: disable=unused-argument
"""
Endpoint to get user-specific state, like current position and playback speed,
without rendering the full student_view HTML. This is similar to student_view_state,
but that one cannot contain user-specific info.
"""
view_state = self.student_view_data()
view_state.update({
"saved_video_position": self.saved_video_position.total_seconds(),
"speed": self.speed,
})
return Response(
json.dumps(view_state),
content_type='application/json',
charset='UTF-8'
)
@XBlock.handler
def yt_video_metadata(self, request, suffix=''): # lint-amnesty, pylint: disable=unused-argument
"""
Endpoint to get YouTube metadata.
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)
metadata, status_code = load_metadata_from_youtube(video_id=self.youtube_id_1_0, request=request)
response = Response(json.dumps(metadata), status=status_code)
response.content_type = 'application/json'
return response
class VideoStudioViewHandlers:
"""
Handlers for Studio view.
"""
def validate_transcript_upload_data(self, data):
"""
Validates video transcript file.
Arguments:
data: Transcript data to be validated.
Returns:
None or String
If there is error returns error message otherwise None.
"""
error = None
_ = self.runtime.service(self, "i18n").ugettext
# Validate the must have attributes - this error is unlikely to be faced by common users.
must_have_attrs = ['edx_video_id', 'language_code', 'new_language_code']
missing = [attr for attr in must_have_attrs if attr not in data]
# Get available transcript languages.
transcripts = self.get_transcripts_info()
video_config_service = self.runtime.service(self, 'video_config')
if not video_config_service:
return error
available_translations = video_config_service.available_translations(
self,
transcripts,
verify_assets=True
)
if missing:
error = _('The following parameters are required: {missing}.').format(missing=', '.join(missing))
elif (
data['language_code'] != data['new_language_code'] and data['new_language_code'] in available_translations
):
error = _('A transcript with the "{language_code}" language code already exists.').format(
language_code=data['new_language_code'],
)
elif 'file' not in data:
error = _('A transcript file is required.')
return error
@XBlock.handler
def studio_transcript(self, request, dispatch):
"""
Entry point for Studio transcript handlers.
Dispatches:
/translation/[language_id] - language_id sould be in url.
`translation` dispatch support following HTTP methods:
`POST`:
Upload srt file. Check possibility of generation of proper sjson files.
For now, it works only for self.transcripts, not for `en`.
`GET:
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:
Video or asset was deleted from module/contentstore, but request came later.
Seems impossible to be raised. block_render.py catches NotFoundErrors from here.
/translation POST:
TypeError:
Unjsonable filename or content.
TranscriptsGenerationException, TranscriptException:
no SRT extension or not parse-able by PySRT
UnicodeDecodeError: non-UTF8 uploaded file content encoding.
"""
if dispatch.startswith('translation'):
if request.method == 'POST':
response = self._studio_transcript_upload(request)
elif request.method == 'DELETE':
response = self._studio_transcript_delete(request)
elif request.method == 'GET':
response = self._studio_transcript_get(request)
else:
# Any other HTTP method is not allowed.
response = Response(status=404)
else: # unknown dispatch
log.debug("Dispatch is not allowed")
response = Response(status=404)
return response
def _studio_transcript_upload(self, request):
"""
Upload transcript. Used in "POST" method in `studio_transcript`
"""
_ = self.runtime.service(self, "i18n").ugettext
video_config_service = self.runtime.service(self, 'video_config')
if not video_config_service:
return Response(json={'error': _('Runtime does not support transcripts.')}, status=400)
error = self.validate_transcript_upload_data(data=request.POST)
if error:
return Response(json={'error': error}, status=400)
edx_video_id = (request.POST['edx_video_id'] or "").strip()
language_code = request.POST['language_code']
new_language_code = request.POST['new_language_code']
try:
video_config_service.upload_transcript(
video_block=self, # NOTE: .edx_video_id and .transcripts may get mutated
edx_video_id=edx_video_id,
language_code=language_code,
new_language_code=new_language_code,
transcript_file=request.POST['file'].file,
)
return Response(
json.dumps(
{
"edx_video_id": edx_video_id or self.edx_video_id,
"language_code": new_language_code,
}
),
status=201,
)
except (TranscriptsGenerationException, UnicodeDecodeError):
return Response(
json={
'error': _(
'There is a problem with this transcript file. Try to upload a different file.'
)
},
status=400
)
def _studio_transcript_delete(self, request):
"""
Delete transcript. Used in "DELETE" method in `studio_transcript`
"""
_ = self.runtime.service(self, "i18n").ugettext
video_config_service = self.runtime.service(self, 'video_config')
if not video_config_service:
return Response(json={'error': _('Runtime does not support transcripts.')}, status=400)
request_data = request.json
if 'lang' not in request_data or 'edx_video_id' not in request_data:
return Response(status=400)
video_config_service.delete_transcript(
video_block=self,
edx_video_id=request_data['edx_video_id'],
language_code=request_data['lang'],
)
return Response(status=200)
def _studio_transcript_get(self, request):
"""
Get transcript. Used in "GET" method in `studio_transcript`
"""
_ = self.runtime.service(self, "i18n").ugettext
language = request.GET.get('language_code')
if not language:
return Response(json={'error': _('Language is required.')}, status=400)
try:
video_config_service = self.runtime.service(self, 'video_config')
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
)
response = Response(transcript_content, headerlist=[
(
'Content-Disposition',
f'attachment; filename="{transcript_name}"'
),
('Content-Language', language),
('Content-Type', mime_type)
])
except (
UnicodeDecodeError,
TranscriptsGenerationException,
TranscriptNotFoundError
):
response = Response(status=404)
return response