From 9cdf45f59420b928e89a441a0e9e0cf497dcf980 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 25 Dec 2024 16:11:35 -0500 Subject: [PATCH 1/5] feat: Updates to support save video with transcript in library home * Add error handler on save video to avoid create sjson * Support transcripts without edx_video_id in definition_to_xml --- xmodule/tests/test_video.py | 42 ++++++++++++++++++++++++ xmodule/video_block/transcripts_utils.py | 2 ++ xmodule/video_block/video_block.py | 10 ++++-- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/xmodule/tests/test_video.py b/xmodule/tests/test_video.py index 5e95f77082b1..9ae8fb7c5e88 100644 --- a/xmodule/tests/test_video.py +++ b/xmodule/tests/test_video.py @@ -741,6 +741,48 @@ def test_export_to_xml(self, mock_val_api): course_id=self.block.scope_ids.usage_id.context_key, ) + def test_export_to_xml_without_video_id(self): + """ + Test that we write the correct XML without video_id on export. + """ + self.block.youtube_id_0_75 = 'izygArpw-Qo' + self.block.youtube_id_1_0 = 'p2Q6BrNhdh8' + self.block.youtube_id_1_25 = '1EeWXzPdhSA' + self.block.youtube_id_1_5 = 'rABDYkeK0x8' + self.block.show_captions = False + self.block.start_time = datetime.timedelta(seconds=1.0) + self.block.end_time = datetime.timedelta(seconds=60) + self.block.track = 'http://www.example.com/track' + self.block.handout = 'http://www.example.com/handout' + self.block.download_track = True + self.block.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source1.ogg'] + self.block.download_video = True + self.block.transcripts = {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'} + + xml = self.block.definition_to_xml(self.file_system) + parser = etree.XMLParser(remove_blank_text=True) + xml_string = '''\ + + ''' + expected = etree.XML(xml_string, parser=parser) + self.assertXmlEqual(expected, xml) + @patch('xmodule.video_block.video_block.edxval_api') def test_export_to_xml_val_error(self, mock_val_api): # Export should succeed without VAL data if video does not exist diff --git a/xmodule/video_block/transcripts_utils.py b/xmodule/video_block/transcripts_utils.py index 866edf596812..849134860a1c 100644 --- a/xmodule/video_block/transcripts_utils.py +++ b/xmodule/video_block/transcripts_utils.py @@ -508,6 +508,8 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat ) except TranscriptException: pass + except AttributeError: + pass if reraised_message: item.save_with_metadata(user) raise TranscriptException(reraised_message) diff --git a/xmodule/video_block/video_block.py b/xmodule/video_block/video_block.py index 84d7edcf7263..30af789506ed 100644 --- a/xmodule/video_block/video_block.py +++ b/xmodule/video_block/video_block.py @@ -855,11 +855,15 @@ def definition_to_xml(self, resource_fs): # lint-amnesty, pylint: disable=too-m if new_transcripts.get('en'): xml.set('sub', '') - # Update `transcripts` attribute in the xml - xml.set('transcripts', json.dumps(transcripts, sort_keys=True)) - except edxval_api.ValVideoNotFoundError: pass + else: + if transcripts.get('en'): + xml.set('sub', '') + + if transcripts: + # Update `transcripts` attribute in the xml + xml.set('transcripts', json.dumps(transcripts, sort_keys=True)) # Sorting transcripts for easy testing of resulting xml for transcript_language in sorted(transcripts.keys()): From 3885dd1aadc65277f610bde7202643106e744c75 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 25 Dec 2024 16:22:39 -0500 Subject: [PATCH 2/5] style: Fix lint --- xmodule/video_block/transcripts_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmodule/video_block/transcripts_utils.py b/xmodule/video_block/transcripts_utils.py index 849134860a1c..bb97e331ef45 100644 --- a/xmodule/video_block/transcripts_utils.py +++ b/xmodule/video_block/transcripts_utils.py @@ -509,7 +509,7 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat except TranscriptException: pass except AttributeError: - pass + pass if reraised_message: item.save_with_metadata(user) raise TranscriptException(reraised_message) From a723e9672af63f85f455ed335ff7c386d418eff6 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 15 Jan 2025 14:50:52 -0500 Subject: [PATCH 3/5] feat: Updated code to get english transcript from filename --- xmodule/video_block/transcripts_utils.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/xmodule/video_block/transcripts_utils.py b/xmodule/video_block/transcripts_utils.py index bb97e331ef45..75086d84ba79 100644 --- a/xmodule/video_block/transcripts_utils.py +++ b/xmodule/video_block/transcripts_utils.py @@ -1021,6 +1021,26 @@ def get_transcript_from_contentstore(video, language, output_format, transcripts except (KeyError, NotFoundError): continue + if transcript_content is None and language == 'en': + # `get_transcript_for_video`` can get the transcript using just the filename, + # but in the above loop the filename from 'en' is overwritten. + # + # If it doesn't yet have the transcription and the language is 'en', + # check again but this time using the original filename. + # + # The use case for which this has been implemented is when copying a video from + # a library and pasting it into a course. + # The asset is copied, but we only have the filename to obtain the content. + try: + input_format, base_name, transcript_content = get_transcript_for_video( + video.location, + subs_id=sub_id, + file_name=other_languages['en'], + language=language + ) + except (KeyError, NotFoundError): + pass + if transcript_content is None: raise NotFoundError('No transcript for `{lang}` language'.format( lang=language From 1c2a57547e5801f2dfb0d7ec6ad36d303a4587a2 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 15 Jan 2025 15:31:16 -0500 Subject: [PATCH 4/5] style: Fix lint --- xmodule/video_block/transcripts_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmodule/video_block/transcripts_utils.py b/xmodule/video_block/transcripts_utils.py index 75086d84ba79..9fd8231b2af7 100644 --- a/xmodule/video_block/transcripts_utils.py +++ b/xmodule/video_block/transcripts_utils.py @@ -1034,7 +1034,7 @@ def get_transcript_from_contentstore(video, language, output_format, transcripts try: input_format, base_name, transcript_content = get_transcript_for_video( video.location, - subs_id=sub_id, + subs_id=None, file_name=other_languages['en'], language=language ) From 74f027f2cf82197a4f0df735720577c69fe83ba1 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 17 Jan 2025 18:51:35 -0500 Subject: [PATCH 5/5] feat: Upload transcript file as static asset in Learning Core in library components --- .../core/djangoapps/xblock/rest_api/views.py | 2 +- xmodule/video_block/video_handlers.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/xblock/rest_api/views.py b/openedx/core/djangoapps/xblock/rest_api/views.py index edcbf22e0d3d..05a16adb1a6f 100644 --- a/openedx/core/djangoapps/xblock/rest_api/views.py +++ b/openedx/core/djangoapps/xblock/rest_api/views.py @@ -175,7 +175,7 @@ def xblock_handler( """ # To support sandboxed XBlocks, custom frontends, and other use cases, we # authenticate requests using a secure token in the URL. see - # openedx.core.djangoapps.xblock.utils.get_secure_hash_for_xblock_handler + # openedx.core.djangoapps.xblock.utils.get_secure_token_for_xblock_handler # for details and rationale. if not validate_secure_token_for_xblock_handler(user_id, str(usage_key), secure_token): raise PermissionDenied("Invalid/expired auth token.") diff --git a/xmodule/video_block/video_handlers.py b/xmodule/video_block/video_handlers.py index b7857e881ece..076b073a8566 100644 --- a/xmodule/video_block/video_handlers.py +++ b/xmodule/video_block/video_handlers.py @@ -13,13 +13,15 @@ from django.core.files.base import ContentFile from django.utils.timezone import now from edxval.api import create_external_video, create_or_update_video_transcript, delete_video_transcript -from opaque_keys.edx.locator import CourseLocator +from opaque_keys.edx.locator import CourseLocator, LibraryLocatorV2 +from opaque_keys.edx.keys import UsageKeyV2 from webob import Response from xblock.core import XBlock from xblock.exceptions import JsonHandlerError from xmodule.exceptions import NotFoundError from xmodule.fields import RelativeTime +from openedx.core.djangoapps.content_libraries import api as lib_api from .transcripts_utils import ( Transcript, @@ -517,8 +519,9 @@ def studio_transcript(self, request, dispatch): try: # Convert SRT transcript into an SJSON format # and upload it to S3. + content = transcript_file.read() sjson_subs = Transcript.convert( - content=transcript_file.read().decode('utf-8'), + content=content.decode('utf-8'), input_format=Transcript.SRT, output_format=Transcript.SJSON ).encode() @@ -541,6 +544,17 @@ def studio_transcript(self, request, dispatch): self.transcripts.pop(language_code, None) self.transcripts[new_language_code] = f'{edx_video_id}-{new_language_code}.srt' response = Response(json.dumps(payload), status=201) + + if isinstance(self.scope_ids.usage_id, UsageKeyV2): + usage_key = self.scope_ids.usage_id + if isinstance(usage_key.context_key, LibraryLocatorV2): + # Save transcript as static asset in Learning Core if is a library component + filename = f"static/{self.transcripts[new_language_code]}" + lib_api.add_library_block_static_asset_file( + usage_key, + filename, + content, + ) except (TranscriptsGenerationException, UnicodeDecodeError): response = Response( json={