From e51c01bf4ec69d254e4fd4fb79a341f99309fd38 Mon Sep 17 00:00:00 2001 From: Rodrigo Martin Date: Mon, 6 Nov 2023 13:33:53 -0300 Subject: [PATCH] feat: add support for user feedback on autogenerated transcripts (#33518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: WIP transcript feedback * feat: Add UI mock for Transcript Feedbacks (#33416) * feat: Add UI mock for Transcript Feedbacks * fix: Fix mongo tests * feat: Get video_uuid, user_uuid and language for request (#33445) * feat: make call to ai-translations to obtain feedback * feat: Show widget if transcript was AI generated * feat: bind all class methods * fix: async calls * feat: send request when choosing feedback * feat: update showing condition (#33474) * fix: ajax success lint * fix: video caption specs errors fixed * feat: add coverage to feedback widget * chore: connect XT to LMS and CMS * feat: use url * chore: add vars to devstack * chore: fix url name * feat: update unit tests regarding env vars * fix: fix test_video_mongo * feat: add more tests * feat: remove console log Co-authored-by: Jesper Hodge <19345795+jesperhodge@users.noreply.github.com> * fix: rename shouldShowWidget to loadAndSetVisibility --------- Co-authored-by: María Guillermina Véscovo Co-authored-by: Jesper Hodge <19345795+jesperhodge@users.noreply.github.com> --- cms/envs/common.py | 3 + cms/envs/devstack.py | 3 + cms/envs/production.py | 3 + lms/djangoapps/courseware/tests/helpers.py | 13 +- .../courseware/tests/test_video_mongo.py | 33 ++- lms/envs/common.py | 3 + lms/envs/devstack.py | 3 + lms/envs/production.py | 3 + lms/templates/video.html | 197 +++++++------ .../core/djangoapps/video_config/toggles.py | 11 + xmodule/assets/video/_display.scss | 39 ++- xmodule/js/fixtures/video_all.html | 75 ++--- .../fixtures/video_transcript_feedback.html | 57 ++++ xmodule/js/karma_runner_webpack.js | 3 +- xmodule/js/spec/helper.js | 32 +++ xmodule/js/spec/video/video_caption_spec.js | 4 +- .../video/video_transcript_feedback_spec.js | 271 ++++++++++++++++++ .../video/037_video_transcript_feedback.js | 247 ++++++++++++++++ xmodule/js/src/video/10_main.js | 9 +- xmodule/video_block/video_block.py | 23 +- 20 files changed, 882 insertions(+), 150 deletions(-) create mode 100644 xmodule/js/fixtures/video_transcript_feedback.html create mode 100644 xmodule/js/spec/video/video_transcript_feedback_spec.js create mode 100644 xmodule/js/src/video/037_video_transcript_feedback.js diff --git a/cms/envs/common.py b/cms/envs/common.py index 64c2c24538dd..917f6859a613 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2655,6 +2655,9 @@ } EDXAPP_PARSE_KEYS = {} +############################ AI_TRANSLATIONS ################################## +AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' + ###################### DEPRECATED URLS ########################## # .. toggle_name: DISABLE_DEPRECATED_SIGNIN_URL diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 674416c10f7d..dd2f4522b648 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -300,6 +300,9 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing CREDENTIALS_INTERNAL_SERVICE_URL = 'http://localhost:18150' CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:18150' +############################ AI_TRANSLATIONS ################################## +AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' + #################### Event bus backend ######################## EVENT_BUS_PRODUCER = 'edx_event_bus_redis.create_producer' diff --git a/cms/envs/production.py b/cms/envs/production.py index 7e635c402e23..d78971882e54 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -660,6 +660,9 @@ def get_env_setting(setting): ################### Discussions micro frontend Feedback URL################### DISCUSSIONS_MFE_FEEDBACK_URL = ENV_TOKENS.get('DISCUSSIONS_MFE_FEEDBACK_URL', DISCUSSIONS_MFE_FEEDBACK_URL) +############################ AI_TRANSLATIONS URL ################################## +AI_TRANSLATIONS_API_URL = ENV_TOKENS.get('AI_TRANSLATIONS_API_URL', AI_TRANSLATIONS_API_URL) + ############## DRF overrides ############## REST_FRAMEWORK.update(ENV_TOKENS.get('REST_FRAMEWORK', {})) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 1ab77ff93da5..85241ead4a5b 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -4,6 +4,7 @@ import ast +import re import json from collections import OrderedDict from datetime import timedelta @@ -450,11 +451,15 @@ def get_context_dict_from_string(data): Retrieve dictionary from string. """ # Replace tuple and un-necessary info from inside string and get the dictionary. - cleaned_data = ast.literal_eval(data.split('((\'video.html\',')[1].replace("),\n {})", '').strip()) - cleaned_data['metadata'] = OrderedDict( - sorted(json.loads(cleaned_data['metadata']).items(), key=lambda t: t[0]) + cleaned_data = data.split('((\'video.html\',')[1].replace("),\n {})", '').strip() + # Omit user_id validation + cleaned_data_without_user = re.sub(".*user_id.*\n?", '', cleaned_data) + + validated_data = ast.literal_eval(cleaned_data_without_user) + validated_data['metadata'] = OrderedDict( + sorted(json.loads(validated_data['metadata']).items(), key=lambda t: t[0]) ) - return cleaned_data + return validated_data def set_preview_mode(preview_mode: bool): diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index fa660f133c74..c1df491f47a4 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -124,6 +124,7 @@ def test_video_constructor(self): 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), + 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, 'autohideHtml5': False, 'recordedYoutubeIsAvailable': True, 'completionEnabled': False, @@ -138,6 +139,8 @@ def test_video_constructor(self): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } mako_service = self.block.runtime.service(self.block, 'mako') @@ -209,6 +212,7 @@ def test_video_constructor(self): 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), + 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, 'autohideHtml5': False, 'recordedYoutubeIsAvailable': True, 'completionEnabled': False, @@ -223,6 +227,8 @@ def test_video_constructor(self): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } mako_service = self.block.runtime.service(self.block, 'mako') @@ -365,6 +371,7 @@ def setUp(self): 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), + 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, 'autohideHtml5': False, 'recordedYoutubeIsAvailable': True, 'completionEnabled': False, @@ -465,6 +472,8 @@ def test_get_html_track(self): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } for data in cases: @@ -595,6 +604,8 @@ def test_get_html_source(self): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } initial_context['metadata']['duration'] = None @@ -707,6 +718,7 @@ def test_get_html_with_mocked_edx_video_id(self): metadata = self.default_metadata_dict metadata['autoplay'] = False metadata['sources'] = "" + initial_context = { 'autoadvance_enabled': False, 'branding_info': None, @@ -730,6 +742,8 @@ def test_get_html_with_mocked_edx_video_id(self): ], 'poster': 'null', 'metadata': metadata, + 'transcript_feedback_enabled': False, + 'video_id': 'mock item', } DATA = SOURCE_XML.format( # lint-amnesty, pylint: disable=invalid-name @@ -884,6 +898,7 @@ def helper_get_html_with_edx_video_id(self, data): # Video found for edx_video_id metadata = self.default_metadata_dict metadata['sources'] = "" + initial_context = { 'autoadvance_enabled': False, 'branding_info': None, @@ -907,6 +922,8 @@ def helper_get_html_with_edx_video_id(self, data): ], 'poster': 'null', 'metadata': metadata, + 'transcript_feedback_enabled': False, + 'video_id': data['edx_video_id'].replace('\t', ' '), } # pylint: disable=invalid-name @@ -1024,6 +1041,8 @@ def side_effect(*args, **kwargs): # lint-amnesty, pylint: disable=unused-argume {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': 'vid-v1:12345', } initial_context['metadata']['duration'] = None @@ -1122,6 +1141,8 @@ def test_get_html_cdn_source_external_video(self): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': 'vid-v1:12345', } initial_context['metadata']['duration'] = None @@ -2336,6 +2357,7 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum content = self.block.student_view(None).content sources = ['example.mp4', 'example.webm'] + expected_context = { 'autoadvance_enabled': False, 'branding_info': None, @@ -2391,6 +2413,7 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), + 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, 'autohideHtml5': False, 'recordedYoutubeIsAvailable': True, 'completionEnabled': False, @@ -2407,7 +2430,9 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum 'poster': json.dumps(OrderedDict({ 'url': 'http://img.youtube.com/vi/ZwkTiUPN0mg/0.jpg', 'type': 'youtube' - })) + })), + 'transcript_feedback_enabled': False, + 'video_id': '', } mako_service = self.block.runtime.service(self.block, 'mako') @@ -2431,6 +2456,7 @@ def prepare_expected_context(self, autoadvanceenabled_flag, autoadvance_flag): Build a dictionary with data expected by some operations in this test. Only parameters related to auto-advance are variable, rest is fixed. """ + context = { 'autoadvance_enabled': autoadvanceenabled_flag, 'branding_info': None, @@ -2474,6 +2500,7 @@ def prepare_expected_context(self, autoadvanceenabled_flag, autoadvance_flag): 'transcriptAvailableTranslationsUrl': self.block.runtime.handler_url( self.block, 'transcript', 'available_translations' ).rstrip('/?'), + 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, 'autohideHtml5': False, 'recordedYoutubeIsAvailable': True, 'completionEnabled': False, @@ -2487,7 +2514,9 @@ def prepare_expected_context(self, autoadvanceenabled_flag, autoadvance_flag): {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'} ], - 'poster': 'null' + 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } return context diff --git a/lms/envs/common.py b/lms/envs/common.py index de26b101287e..9ba152a70fb2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5337,6 +5337,9 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000 NOTIFICATION_CREATION_BATCH_SIZE = 99 +############################ AI_TRANSLATIONS ################################## +AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' + #### django-simple-history## # disable indexing on date field its coming from django-simple-history. SIMPLE_HISTORY_DATE_INDEX = False diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 4a262c7c8bef..5c5aa74e3912 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -530,6 +530,9 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing API_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/' AUTH_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html' +############################ AI_TRANSLATIONS ################################## +AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' + ################# New settings must go ABOVE this line ################# ######################################################################## # See if the developer has any local overrides. diff --git a/lms/envs/production.py b/lms/envs/production.py index 1d7d4a5657c2..9fc0bd54fba4 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1112,6 +1112,9 @@ def get_env_setting(setting): ################### Discussions micro frontend Feedback URL################### DISCUSSIONS_MFE_FEEDBACK_URL = ENV_TOKENS.get('DISCUSSIONS_MFE_FEEDBACK_URL', DISCUSSIONS_MFE_FEEDBACK_URL) +############################ AI_TRANSLATIONS URL ################################## +AI_TRANSLATIONS_API_URL = ENV_TOKENS.get('AI_TRANSLATIONS_API_URL', AI_TRANSLATIONS_API_URL) + ############## DRF overrides ############## REST_FRAMEWORK.update(ENV_TOKENS.get('REST_FRAMEWORK', {})) diff --git a/lms/templates/video.html b/lms/templates/video.html index c3a9e08e0344..163a739930aa 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -48,110 +48,129 @@

${_('Downloads and transcripts')}

-
- % if download_video_link or public_sharing_enabled: -
-

${_('Video')}

- % if download_video_link: - - - ${_('Download video file')} - - % endif - % if download_video_link and public_sharing_enabled: -
- % endif - % if sharing_sites_info: -