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 | 203 +++++++------ .../core/djangoapps/video_config/toggles.py | 11 + xmodule/assets/video/_display.scss | 45 ++- 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, 888 insertions(+), 156 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 64c2c24538..917f6859a6 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2655,6 +2655,9 @@ REGISTRATION_EXTRA_FIELDS = { } 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 674416c10f..dd2f4522b6 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -300,6 +300,9 @@ CLOSEST_CLIENT_IP_FROM_HEADERS = [] 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 7e635c402e..d78971882e 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -660,6 +660,9 @@ DISCUSSIONS_MICROFRONTEND_URL = ENV_TOKENS.get('DISCUSSIONS_MICROFRONTEND_URL', ################### 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 1ab77ff93d..85241ead4a 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -4,6 +4,7 @@ Helpers for courseware tests. 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 fa660f133c..c1df491f47 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -124,6 +124,7 @@ class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-clas '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 @@ class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-clas {'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 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests '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 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests {'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 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): '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 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } for data in cases: @@ -595,6 +604,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): {'display_name': 'Text (.txt) file', 'value': 'txt'} ], 'poster': 'null', + 'transcript_feedback_enabled': False, + 'video_id': '', } initial_context['metadata']['duration'] = None @@ -707,6 +718,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): metadata = self.default_metadata_dict metadata['autoplay'] = False metadata['sources'] = "" + initial_context = { 'autoadvance_enabled': False, 'branding_info': None, @@ -730,6 +742,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): ], '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 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): # 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 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): ], 'poster': 'null', 'metadata': metadata, + 'transcript_feedback_enabled': False, + 'video_id': data['edx_video_id'].replace('\t', ' '), } # pylint: disable=invalid-name @@ -1024,6 +1041,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): {'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 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): {'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 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests content = self.block.student_view(None).content sources = ['example.mp4', 'example.webm'] + expected_context = { 'autoadvance_enabled': False, 'branding_info': None, @@ -2391,6 +2413,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests '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 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests '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 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh 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 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh '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 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh {'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 de26b10128..9ba152a70f 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5337,6 +5337,9 @@ NOTIFICATIONS_EXPIRY = 60 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 4a262c7c8b..5c5aa74e39 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -530,6 +530,9 @@ API_ACCESS_FROM_EMAIL = 'api-requests@example.com' 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 1d7d4a5657..9fc0bd54fb 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1112,6 +1112,9 @@ DISCUSSIONS_MICROFRONTEND_URL = ENV_TOKENS.get('DISCUSSIONS_MICROFRONTEND_URL', ################### 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 c3a9e08e03..163a739930 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -48,110 +48,129 @@ from openedx.core.djangolib.js_utils import (
- % if (download_video_link or track or handout or branding_info or public_sharing_enabled) and not hide_downloads: -

${_('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: -