feat: add support for user feedback on autogenerated transcripts (#33518)
* 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 <mvescovo@2u.com> Co-authored-by: Jesper Hodge <19345795+jesperhodge@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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', {}))
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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', {}))
|
||||
|
||||
|
||||
@@ -48,110 +48,129 @@ from openedx.core.djangolib.js_utils import (
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
% if (download_video_link or track or handout or branding_info or public_sharing_enabled) and not hide_downloads:
|
||||
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
|
||||
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
|
||||
% if download_video_link or public_sharing_enabled:
|
||||
<div class="wrapper-download-video">
|
||||
<h4 class="hd hd-5">${_('Video')}</h4>
|
||||
% if download_video_link:
|
||||
<span class="icon fa fa-download" aria-hidden="true"></span>
|
||||
<a class="btn-link video-sources video-download-button" href="${download_video_link}">
|
||||
${_('Download video file')}
|
||||
</a>
|
||||
% endif
|
||||
% if download_video_link and public_sharing_enabled:
|
||||
<br>
|
||||
% endif
|
||||
% if sharing_sites_info:
|
||||
<div class="wrapper-social-share">
|
||||
<button
|
||||
style="background-image: none; background-color: rgb(0, 38, 43); border-radius: 0px; color: white"
|
||||
class="social-toggle-btn btn"
|
||||
>
|
||||
<span class="icon fa fa-share-alt mr-2" style="text-shadow: none"></span>
|
||||
${_('Share this video')}
|
||||
</button>
|
||||
<div
|
||||
hidden
|
||||
class="container-social-share color-black p-2"
|
||||
style="width: 300px; border-radius: 6px; background-color: white; box-shadow: 0 .5rem 1rem rgba(0,0,0,.15),0 .25rem .625rem rgba(0,0,0,.15)"
|
||||
>
|
||||
${_('Share this video')}
|
||||
<div class="btn-link close-btn float-right">
|
||||
<span style="color: black" class="icon fa fa-close" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
% for sharing_site_info in sharing_sites_info:
|
||||
<a
|
||||
class="btn-link social-share-link"
|
||||
data-source="${sharing_site_info['name']}"
|
||||
href="${sharing_site_info['sharing_url']}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="font-size: 1.5rem"
|
||||
<div class="wrapper-video-bottom-section">
|
||||
% if ((download_video_link or track or handout or branding_info or public_sharing_enabled) and not hide_downloads):
|
||||
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
|
||||
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
|
||||
% if download_video_link or public_sharing_enabled:
|
||||
<div class="wrapper-download-video">
|
||||
<h4 class="hd hd-5">${_('Video')}</h4>
|
||||
% if download_video_link:
|
||||
<span class="icon fa fa-download" aria-hidden="true"></span>
|
||||
<a class="btn-link video-sources video-download-button" href="${download_video_link}">
|
||||
${_('Download video file')}
|
||||
</a>
|
||||
% endif
|
||||
% if download_video_link and public_sharing_enabled:
|
||||
<br>
|
||||
% endif
|
||||
% if sharing_sites_info:
|
||||
<div class="wrapper-social-share">
|
||||
<button
|
||||
style="background-image: none; background-color: rgb(0, 38, 43); border-radius: 0px; color: white"
|
||||
class="social-toggle-btn btn"
|
||||
>
|
||||
<span class="icon fa ${sharing_site_info['fa_icon_name']}" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Share on {site}").format(site=sharing_site_info['name'])}</span>
|
||||
</a>
|
||||
% endfor
|
||||
<br />
|
||||
<div style="background-color: #F2F0EF" class="public-video-url-container p-2">
|
||||
<a href=${public_video_url} class="d-inline-block align-middle" style="width: 200px">
|
||||
<div
|
||||
class="text-nowrap"
|
||||
style="color: black; overflow: hidden; text-overflow: ellipsis; vertical-align: middle"
|
||||
>
|
||||
${public_video_url}
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
class="public-video-copy-btn btn-link d-inline-block float-right"
|
||||
data-url=${public_video_url}
|
||||
>
|
||||
<span class="icon fa fa-link pr-1"></span>
|
||||
<span>${_('Copy')}</span>
|
||||
<span class="icon fa fa-share-alt mr-2" style="text-shadow: none"></span>
|
||||
${_('Share this video')}
|
||||
</button>
|
||||
<div
|
||||
hidden
|
||||
class="container-social-share color-black p-2"
|
||||
style="width: 300px; border-radius: 6px; background-color: white; box-shadow: 0 .5rem 1rem rgba(0,0,0,.15),0 .25rem .625rem rgba(0,0,0,.15)"
|
||||
>
|
||||
${_('Share this video')}
|
||||
<div class="btn-link close-btn float-right">
|
||||
<span style="color: black" class="icon fa fa-close" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
% for sharing_site_info in sharing_sites_info:
|
||||
<a
|
||||
class="btn-link social-share-link"
|
||||
data-source="${sharing_site_info['name']}"
|
||||
href="${sharing_site_info['sharing_url']}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="font-size: 1.5rem"
|
||||
>
|
||||
<span class="icon fa ${sharing_site_info['fa_icon_name']}" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Share on {site}").format(site=sharing_site_info['name'])}</span>
|
||||
</a>
|
||||
% endfor
|
||||
<br />
|
||||
<div style="background-color: #F2F0EF" class="public-video-url-container p-2">
|
||||
<a href=${public_video_url} class="d-inline-block align-middle" style="width: 200px">
|
||||
<div
|
||||
class="text-nowrap"
|
||||
style="color: black; overflow: hidden; text-overflow: ellipsis; vertical-align: middle"
|
||||
>
|
||||
${public_video_url}
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
class="public-video-copy-btn btn-link d-inline-block float-right"
|
||||
data-url=${public_video_url}
|
||||
>
|
||||
<span class="icon fa fa-link pr-1"></span>
|
||||
<span>${_('Copy')}</span>
|
||||
</div>
|
||||
<span>
|
||||
</div>
|
||||
<span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% if track:
|
||||
<div class="wrapper-download-transcripts">
|
||||
<h4 class="hd hd-5">${_('Transcripts')}</h4>
|
||||
% if transcript_download_format:
|
||||
<ul class="list-download-transcripts">
|
||||
% for item in transcript_download_formats_list:
|
||||
<li class="transcript-option">
|
||||
<span class="icon fa fa-download" aria-hidden="true"></span>
|
||||
<% dname = _("Download {file}").format(file=item['display_name']) %>
|
||||
<a class="btn btn-link" href="${track}" data-value="${item['value']}">${dname}</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<a class="btn-link external-track" href="${track}">${_('Download transcript')}</a>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% if track:
|
||||
<div class="wrapper-download-transcripts">
|
||||
<h4 class="hd hd-5">${_('Transcripts')}</h4>
|
||||
% if transcript_download_format:
|
||||
<ul class="list-download-transcripts">
|
||||
% for item in transcript_download_formats_list:
|
||||
<li class="transcript-option">
|
||||
<span class="icon fa fa-download" aria-hidden="true"></span>
|
||||
<% dname = _("Download {file}").format(file=item['display_name']) %>
|
||||
<a class="btn btn-link" href="${track}" data-value="${item['value']}">${dname}</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<a class="btn-link external-track" href="${track}">${_('Download transcript')}</a>
|
||||
% endif
|
||||
% if handout:
|
||||
<div class="wrapper-handouts">
|
||||
<h4 class="hd hd-5">${_('Handouts')}</h4>
|
||||
<a class="btn-link" href="${handout}">${_('Download Handout')}</a>
|
||||
</div>
|
||||
% endif
|
||||
% if branding_info:
|
||||
<div class="branding">
|
||||
<span class="host-tag">${branding_info['logo_tag']}</span>
|
||||
<a href="${branding_info['url']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% if handout:
|
||||
<div class="wrapper-handouts">
|
||||
<h4 class="hd hd-5">${_('Handouts')}</h4>
|
||||
<a class="btn-link" href="${handout}">${_('Download Handout')}</a>
|
||||
</div>
|
||||
% endif
|
||||
% if branding_info:
|
||||
<div class="branding">
|
||||
<span class="host-tag">${branding_info['logo_tag']}</span>
|
||||
<a href="${branding_info['url']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a>
|
||||
% if transcript_feedback_enabled:
|
||||
<div class="wrapper-transcript-feedback" data-video-id='${video_id}' data-user-id='${user_id}'>
|
||||
<h4 class="hd hd-5">${_('How is the transcript quality ?')}</h4>
|
||||
<div class="transcript-feedback-buttons">
|
||||
<div class="transcript-feedback-btn-wrapper">
|
||||
<button class="thumbs-up-btn">
|
||||
<span class="icon fa fa-thumbs-o-up thumbs-up-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="transcript-feedback-btn-wrapper">
|
||||
<button class="thumbs-down-btn">
|
||||
<span class="icon fa fa-thumbs-o-down thumbs-down-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if cdn_eval:
|
||||
|
||||
@@ -15,3 +15,14 @@ WAFFLE_FLAG_NAMESPACE = 'video_config'
|
||||
PUBLIC_VIDEO_SHARE = CourseWaffleFlag(
|
||||
f'{WAFFLE_FLAG_NAMESPACE}.public_video_share', __name__
|
||||
)
|
||||
|
||||
# .. toggle_name: video_config.transcript_feedback
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Gates access to the transcript feedback widget feature.
|
||||
# .. toggle_use_cases: temporary, opt_in
|
||||
# .. toggle_creation_date: 2023-05-10
|
||||
# .. toggle_target_removal_date: None
|
||||
TRANSCRIPT_FEEDBACK = CourseWaffleFlag(
|
||||
f'{WAFFLE_FLAG_NAMESPACE}.transcript_feedback', __name__
|
||||
)
|
||||
|
||||
@@ -89,6 +89,23 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
|
||||
margin: 1em 0 0;
|
||||
}
|
||||
|
||||
.wrapper-video-bottom-section {
|
||||
display: flex;
|
||||
|
||||
.wrapper-download-video,
|
||||
.wrapper-download-transcripts,
|
||||
.wrapper-handouts,
|
||||
.branding,
|
||||
.wrapper-transcript-feedback {
|
||||
flex: 1;
|
||||
margin-top: $baseline;
|
||||
|
||||
@include padding-right($baseline);
|
||||
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-downloads {
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
@@ -98,18 +115,6 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper-download-video,
|
||||
.wrapper-download-transcripts,
|
||||
.wrapper-handouts,
|
||||
.branding {
|
||||
flex: 1;
|
||||
margin-top: $baseline;
|
||||
|
||||
@include padding-right($baseline);
|
||||
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper-download-video {
|
||||
.video-sources {
|
||||
margin: 0;
|
||||
@@ -152,6 +157,22 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.wrapper-transcript-feedback {
|
||||
.transcript-feedback-buttons {
|
||||
display: flex;
|
||||
}
|
||||
.transcript-feedback-btn-wrapper {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.thumbs-up-btn,
|
||||
.thumbs-down-btn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
|
||||
@@ -33,48 +33,49 @@
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts">Downloads and transcripts</h3>
|
||||
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts">
|
||||
<div class="wrapper-download-video">
|
||||
<h4 class="hd hd-5">Video</h4>
|
||||
<div class="wrapper-social-share" data-url="video-share-url">
|
||||
<span class="icon fa fa-share-alt" aria-hidden="true"></span>
|
||||
Share on:
|
||||
<a class="btn-link social-share-link" data-source="twitter" href="#">
|
||||
<div class="wrapper-video-bottom-section">
|
||||
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts">Downloads and transcripts</h3>
|
||||
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts">
|
||||
<div class="wrapper-download-video">
|
||||
<h4 class="hd hd-5">Video</h4>
|
||||
<div class="wrapper-social-share" data-url="video-share-url">
|
||||
<span class="icon fa fa-share-alt" aria-hidden="true"></span>
|
||||
Share on:
|
||||
<a class="btn-link social-share-link" data-source="twitter" href="#">
|
||||
<span class="icon fa fa-linkedin-square" aria-hidden="true"></span>
|
||||
<span class="sr">Share on twitter</span>
|
||||
</a>
|
||||
<a class="btn-link social-share-link" data-source="facebook" href="#">
|
||||
<span class="icon fa fa-linkedin-square" aria-hidden="true"></span>
|
||||
<span class="sr">Share on twitter</span>
|
||||
</a>
|
||||
<a class="btn-link social-share-link" data-source="facebook" href="#">
|
||||
<span class="icon fa fa-linkedin-square" aria-hidden="true"></span>
|
||||
<span class="sr">Share on facebook</span>
|
||||
</a>
|
||||
<a class="btn-link social-share-link" data-source="linkedin" href="#">
|
||||
<span class="icon fa fa-linkedin-square" aria-hidden="true"></span>
|
||||
<span class="sr">Share on linkedin</span>
|
||||
<span class="sr">Share on facebook</span>
|
||||
</a>
|
||||
<a class="btn-link social-share-link" data-source="linkedin" href="#">
|
||||
<span class="icon fa fa-linkedin-square" aria-hidden="true"></span>
|
||||
<span class="sr">Share on linkedin</span>
|
||||
</a>
|
||||
</div>
|
||||
<a class="btn-link video-sources video-download-button" href="#">
|
||||
Download video file
|
||||
</a>
|
||||
</div>
|
||||
<a class="btn-link video-sources video-download-button" href="#">
|
||||
Download video file
|
||||
</a>
|
||||
</div>
|
||||
<div class="wrapper-download-transcripts">
|
||||
<h4 class="hd hd-5">Transcripts</h4>
|
||||
<ul class="list-download-transcripts">
|
||||
<li class="transcript-option">
|
||||
<a href="#" class="btn btn-link" data-href="txt">Download Text (.txt) file</a>
|
||||
</li>
|
||||
<li class="transcript-option">
|
||||
<a href="#" class="btn btn-link" data-href="srt">Download SubRip (.srt) file</a>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="#" class="external-track">Download transcript</a>
|
||||
</div>
|
||||
<div class="wrapper-handouts">
|
||||
<h4 class="hd hd-5">Handouts</h4>
|
||||
<a href="#">Download Handout</a>
|
||||
<div class="wrapper-download-transcripts">
|
||||
<h4 class="hd hd-5">Transcripts</h4>
|
||||
<ul class="list-download-transcripts">
|
||||
<li class="transcript-option">
|
||||
<a href="#" class="btn btn-link" data-href="txt">Download Text (.txt) file</a>
|
||||
</li>
|
||||
<li class="transcript-option">
|
||||
<a href="#" class="btn btn-link" data-href="srt">Download SubRip (.srt) file</a>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="#" class="external-track">Download transcript</a>
|
||||
</div>
|
||||
<div class="wrapper-handouts">
|
||||
<h4 class="hd hd-5">Handouts</h4>
|
||||
<a href="#">Download Handout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
57
xmodule/js/fixtures/video_transcript_feedback.html
Normal file
57
xmodule/js/fixtures/video_transcript_feedback.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<div class="course-content">
|
||||
<div class="vert-mod">
|
||||
<div id="video_example">
|
||||
<div id="example">
|
||||
<div
|
||||
id="video_id"
|
||||
class="video closed vert vert-video_id"
|
||||
data-block-id="block-v1:coursekey+type@video+block@000000000000000000"
|
||||
data-course-id="course-v1:someOrg+thisCOurse+runAway"
|
||||
data-metadata='{"autohideHtml5": "true", "autoplay": "false", "captionDataDir": "", "endTime": "", "generalSpeed": "1.0", "saveStateUrl": "/save_user_state", "savedVideoPosition": "0", "showCaptions": "true", "sources": ["/base/fixtures/test.mp4","/base/fixtures/test.webm","/base/fixtures/test.ogv"], "speed": "1.5", "startTime": "", "streams": "", "sub": "Z5KLxerq05Y", "transcriptAvailableTranslationsUrl": "/transcript/available_translations", "transcriptLanguage": "en", "transcriptLanguages": {"en": "English", "de": "Deutsch", "zh": "普通话"}, "transcriptTranslationUrl": "/transcript/translation/__lang__", "ytApiUrl": "/base/fixtures/youtube_iframe_api.js", "ytImageUrl": "", "ytTestTimeout": "1500", "ytMetadataUrl": "www.googleapis.com/youtube/v3/videos/"}'
|
||||
>
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="Loading video player"></span>
|
||||
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="Play video"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<div id="id"></div>
|
||||
</section>
|
||||
<div class="video-player-post"></div>
|
||||
<div class="closed-captions"></div>
|
||||
<section class="video-controls is-hidden">
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
|
||||
<div class="wrapper-video-bottom-section">
|
||||
<div class="wrapper-transcript-feedback" data-video-id='365b710a-6dd6-11ee-b962-0242ac120002' data-user-id='1'>
|
||||
<h4 class="hd hd-5">How is the transcript quality ?</h4>
|
||||
<div class="transcript-feedback-buttons">
|
||||
<div class="transcript-feedback-btn-wrapper">
|
||||
<button class="thumbs-up-btn">
|
||||
<span class="icon fa fa-thumbs-o-up thumbs-up-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="transcript-feedback-btn-wrapper">
|
||||
<button class="thumbs-down-btn">
|
||||
<span class="icon fa fa-thumbs-o-down thumbs-down-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,6 +46,7 @@ import './spec/video/initialize_spec.js';
|
||||
import './spec/video/iterator_spec.js';
|
||||
import './spec/video/resizer_spec.js';
|
||||
import './spec/video/sjson_spec.js';
|
||||
import './spec/video/social_share_spec.js';
|
||||
import './spec/video/video_autoadvance_spec.js';
|
||||
import './spec/video/video_bumper_spec.js';
|
||||
import './spec/video/video_caption_spec.js';
|
||||
@@ -66,9 +67,9 @@ import './spec/video/video_save_state_plugin_spec.js';
|
||||
import './spec/video/video_skip_control_spec.js';
|
||||
import './spec/video/video_speed_control_spec.js';
|
||||
import './spec/video/video_storage_spec.js';
|
||||
import './spec/video/video_transcript_feedback_spec.js';
|
||||
import './spec/video/video_volume_control_spec.js';
|
||||
import './spec/time_spec.js';
|
||||
import './spec/video/social_share_spec.js';
|
||||
|
||||
// overwrite the loaded method and manually start the karma after a delay
|
||||
// Somehow the code initialized in jQuery's onready doesn't get called before karma auto starts
|
||||
|
||||
@@ -180,6 +180,38 @@
|
||||
return {};
|
||||
} else if (settings.url === '/save_user_state') {
|
||||
return {success: true};
|
||||
} else if (settings.url.match(/.+video-transcript.+$/)) {
|
||||
if (settings.url.match(/.+&video_uuid=notAIGenerated/)) {
|
||||
return settings.success(null);
|
||||
}
|
||||
if (settings.url.match(/.+&video_uuid=inProgress/)) {
|
||||
return settings.success({
|
||||
status: 'In Progress'
|
||||
});
|
||||
}
|
||||
if (settings.url.match(/.+&video_uuid=error/)) {
|
||||
return settings.error();
|
||||
}
|
||||
return settings.success({
|
||||
status: 'Completed'
|
||||
});
|
||||
} else if (settings.url.match(/.+transcript-feedback.+$/) && settings.type === 'GET') {
|
||||
if (settings.url.match(/.+&video_uuid=error.+$/)) {
|
||||
return settings.error();
|
||||
}
|
||||
if (settings.url.match(/.+&video_uuid=negative.+$/)) {
|
||||
return settings.success({
|
||||
value: false
|
||||
});
|
||||
}
|
||||
if (settings.url.match(/.+&video_uuid=none.+$/)) {
|
||||
return settings.success(null);
|
||||
}
|
||||
return settings.success({
|
||||
value: true
|
||||
});
|
||||
} else if (settings.url.match(/.+transcript-feedback.+$/) && settings.type === 'POST') {
|
||||
return settings.success(settings.data.value !== null ? { value: settings.data.value } : null);
|
||||
} else if (settings.url.match(new RegExp(jasmine.getFixtures().fixturesPath + '.+', 'g'))) {
|
||||
return origAjax(settings);
|
||||
} else {
|
||||
|
||||
@@ -833,7 +833,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
msg = 'on succes: language menu is rendered if translations available';
|
||||
msg = 'on success: language menu is rendered if translations available';
|
||||
it(msg, function() {
|
||||
state.config.transcriptLanguages = {
|
||||
en: 'English',
|
||||
@@ -853,7 +853,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
msg = 'on succes: language menu isn\'t rendered if translations unavailable';
|
||||
msg = 'on success: language menu isn\'t rendered if translations unavailable';
|
||||
it(msg, function() {
|
||||
state.config.transcriptLanguages = {
|
||||
en: 'English',
|
||||
|
||||
271
xmodule/js/spec/video/video_transcript_feedback_spec.js
Normal file
271
xmodule/js/spec/video/video_transcript_feedback_spec.js
Normal file
@@ -0,0 +1,271 @@
|
||||
(function() {
|
||||
// eslint-disable-next-line lines-around-directive
|
||||
'use strict';
|
||||
|
||||
describe('VideoTranscriptFeedback', function() {
|
||||
var state;
|
||||
var videoId = "365b710a-6dd6-11ee-b962-0242ac120002";
|
||||
var userId = 1;
|
||||
var currentLanguage = "en";
|
||||
var getAITranscriptUrl = '/video-transcript' + '?transcript_language=' + currentLanguage + '&video_uuid=' + videoId;
|
||||
var getTranscriptFeedbackUrl = '/transcript-feedback' + '?transcript_language=' + currentLanguage + '&video_uuid=' + videoId + '&user_id=' + userId;
|
||||
var sendTranscriptFeedbackUrl = '/transcript-feedback/';
|
||||
|
||||
beforeEach(function() {
|
||||
state = jasmine.initializePlayer('video_transcript_feedback.html');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
if (state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
describe('initialize', function() {
|
||||
it('instantiates widget and handlers along with necessary data', function() {
|
||||
spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callFake(function() {
|
||||
return true;
|
||||
});
|
||||
spyOn(state.videoTranscriptFeedback, 'bindHandlers').and.callFake(function() {
|
||||
return true;
|
||||
});
|
||||
state.videoTranscriptFeedback.initialize();
|
||||
|
||||
expect(state.videoTranscriptFeedback.videoId).toEqual(videoId);
|
||||
expect(state.videoTranscriptFeedback.userId).toEqual(userId);
|
||||
expect(state.videoTranscriptFeedback.currentTranscriptLanguage).toEqual(currentLanguage);
|
||||
expect(state.videoTranscriptFeedback.loadAndSetVisibility).toHaveBeenCalled();
|
||||
expect(state.videoTranscriptFeedback.bindHandlers).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should show widget', function() {
|
||||
it('checks if transcript was AI generated', function() {
|
||||
spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callThrough();
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
|
||||
var getAITranscriptCall = $.ajax.calls.all().find(function(call) {
|
||||
return call.args[0].url.match(/.+video-transcript.+$/);
|
||||
});
|
||||
|
||||
expect(state.videoTranscriptFeedback.loadAndSetVisibility).toHaveBeenCalled();
|
||||
expect(getAITranscriptCall.args[0].url).toEqual(state.videoTranscriptFeedback.aiTranslationsUrl + getAITranscriptUrl);
|
||||
expect(getAITranscriptCall.args[0].type).toEqual('GET');
|
||||
expect(getAITranscriptCall.args[0].async).toEqual(false);
|
||||
expect(getAITranscriptCall.args[0].success).toEqual(jasmine.any(Function));
|
||||
expect(getAITranscriptCall.args[0].error).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('shows widget if transcript is AI generated', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
expect($('.wrapper-transcript-feedback')[0]).toExist();
|
||||
});
|
||||
it('hides widget if transcript is not AI generated', function() {
|
||||
state.videoTranscriptFeedback.videoId = 'notAIGenerated';
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
expect($('.wrapper-transcript-feedback')[0]).toExist();
|
||||
expect($('.wrapper-transcript-feedback')[0].style.display).toEqual('none');
|
||||
});
|
||||
it('hides widget if transcript is AI generated but is still in progress', function() {
|
||||
state.videoTranscriptFeedback.videoId = 'inProgress';
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
expect($('.wrapper-transcript-feedback')[0]).toExist();
|
||||
expect($('.wrapper-transcript-feedback')[0].style.display).toEqual('none');
|
||||
});
|
||||
it('hides widget if query for transcript AI generated fails', function() {
|
||||
state.videoTranscriptFeedback.videoId = 'error';
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
expect($('.wrapper-transcript-feedback')[0]).toExist();
|
||||
expect($('.wrapper-transcript-feedback')[0].style.display).toEqual('none');
|
||||
});
|
||||
it('checks if feedback exists for AI generated transcript', function() {
|
||||
spyOn(state.videoTranscriptFeedback, 'getFeedbackForCurrentTranscript').and.callThrough();
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
|
||||
var getTranscriptFeedbackCall = $.ajax.calls.all().find(function(call) {
|
||||
return call.args[0].url.match(/.+transcript-feedback.+$/);
|
||||
});
|
||||
|
||||
expect(state.videoTranscriptFeedback.getFeedbackForCurrentTranscript).toHaveBeenCalled();
|
||||
expect(getTranscriptFeedbackCall.args[0].url).toEqual(state.videoTranscriptFeedback.aiTranslationsUrl + getTranscriptFeedbackUrl);
|
||||
expect(getTranscriptFeedbackCall.args[0].type).toEqual('GET');
|
||||
expect(getTranscriptFeedbackCall.args[0].success).toEqual(jasmine.any(Function));
|
||||
expect(getTranscriptFeedbackCall.args[0].error).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('get feedback for current transcript', function() {
|
||||
it('marks thumbs up button if feedback exists and it is positive', function() {
|
||||
state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(true);
|
||||
});
|
||||
it('marks thumbs down button if feedback exists and it is negative', function() {
|
||||
state.videoTranscriptFeedback.videoId = 'negative';
|
||||
state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
|
||||
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(false);
|
||||
});
|
||||
it('marks thumbs up buttons as empty if feedback does not exist', function() {
|
||||
state.videoTranscriptFeedback.videoId = 'none';
|
||||
state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
|
||||
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(null);
|
||||
});
|
||||
it('marks thumbs up buttons as empty if query fails', function() {
|
||||
state.videoTranscriptFeedback.videoId = 'error';
|
||||
state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
|
||||
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onHideLanguageMenu', function() {
|
||||
it('calls loadAndSetVisibility if language changed', function() {
|
||||
state.videoTranscriptFeedback.currentTranscriptLanguage = 'es';
|
||||
spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callThrough();
|
||||
state.el.trigger('language_menu:hide', {
|
||||
id: 'id',
|
||||
code: 'code',
|
||||
language: 'en',
|
||||
duration: 10
|
||||
});
|
||||
expect(state.videoTranscriptFeedback.loadAndSetVisibility).toHaveBeenCalled();
|
||||
});
|
||||
it('does not call loadAndSetVisibility if language did not change', function() {
|
||||
state.videoTranscriptFeedback.currentTranscriptLanguage = 'en';
|
||||
spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callThrough();
|
||||
state.el.trigger('language_menu:hide', {
|
||||
id: 'id',
|
||||
code: 'code',
|
||||
language: 'en',
|
||||
duration: 10
|
||||
});
|
||||
expect(state.videoTranscriptFeedback.loadAndSetVisibility).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clicking on thumbs up button', function() {
|
||||
it('sends positive feedback if there is no current feedback', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = undefined;
|
||||
spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
|
||||
return true;
|
||||
});
|
||||
var thumbsUpButton = $('.thumbs-up-btn');
|
||||
thumbsUpButton.trigger('click');
|
||||
expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(true);
|
||||
});
|
||||
it('sends empty feedback if there is a current positive feedback', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = true;
|
||||
spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
|
||||
return true;
|
||||
});
|
||||
var thumbsUpButton = $('.thumbs-up-btn');
|
||||
thumbsUpButton.trigger('click');
|
||||
expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clicking on thumbs down button', function() {
|
||||
it('sends negative feedback if there is no current feedback', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = undefined;
|
||||
spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
|
||||
return true;
|
||||
});
|
||||
var thumbsDownButton = $('.thumbs-down-btn');
|
||||
thumbsDownButton.trigger('click');
|
||||
expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(false);
|
||||
});
|
||||
it('sends empty feedback if there is a current negative feedback', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = false;
|
||||
spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
|
||||
return true;
|
||||
});
|
||||
var thumbsDownButton = $('.thumbs-down-btn');
|
||||
thumbsDownButton.trigger('click');
|
||||
expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calling send transcript feedback', function() {
|
||||
it('sends proper request to ai translation service', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = undefined;
|
||||
state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(true);
|
||||
var sendTranscriptFeedbackCall = $.ajax.calls.all().find(function(call) {
|
||||
return call.args[0].url.match(/.+transcript-feedback.+$/) && call.args[0].type === 'POST';
|
||||
});
|
||||
|
||||
expect(sendTranscriptFeedbackCall.args[0].url).toEqual(state.videoTranscriptFeedback.aiTranslationsUrl + sendTranscriptFeedbackUrl);
|
||||
expect(sendTranscriptFeedbackCall.args[0].type).toEqual('POST');
|
||||
expect(sendTranscriptFeedbackCall.args[0].dataType).toEqual('json');
|
||||
expect(sendTranscriptFeedbackCall.args[0].data).toEqual({
|
||||
transcript_language: currentLanguage,
|
||||
video_uuid: videoId,
|
||||
user_id: userId,
|
||||
value: true,
|
||||
});
|
||||
expect(sendTranscriptFeedbackCall.args[0].success).toEqual(jasmine.any(Function));
|
||||
expect(sendTranscriptFeedbackCall.args[0].error).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('marks thumbs up button as selected if response is positive', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = undefined;
|
||||
state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(true);
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(true);
|
||||
});
|
||||
it('marks thumbs down button as selected if response is negative', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = undefined;
|
||||
state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(false);
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(false);
|
||||
});
|
||||
it('unselects thumbs buttons if response is empty', function() {
|
||||
state.videoTranscriptFeedback.loadAndSetVisibility();
|
||||
state.videoTranscriptFeedback.currentFeedback = true;
|
||||
state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(null);
|
||||
var thumbsUpIcon = $('.thumbs-up-icon')[0];
|
||||
var thumbsDownIcon = $('.thumbs-down-icon')[0];
|
||||
|
||||
expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
|
||||
expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
|
||||
expect(state.videoTranscriptFeedback.currentFeedback).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this);
|
||||
247
xmodule/js/src/video/037_video_transcript_feedback.js
Normal file
247
xmodule/js/src/video/037_video_transcript_feedback.js
Normal file
@@ -0,0 +1,247 @@
|
||||
(function(define) {
|
||||
// VideoTranscriptFeedbackHandler module.
|
||||
|
||||
'use strict';
|
||||
|
||||
define('video/037_video_caption.js', ['underscore'],
|
||||
function(_) {
|
||||
/**
|
||||
* @desc VideoTranscriptFeedbackHandler module exports a function.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @param {object} state - The object containing the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
*/
|
||||
|
||||
var VideoTranscriptFeedbackHandler = function(state) {
|
||||
if (!(this instanceof VideoTranscriptFeedbackHandler)) {
|
||||
return new VideoTranscriptFeedbackHandler(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'destroy', 'getFeedbackForCurrentTranscript', 'markAsPositiveFeedback', 'markAsNegativeFeedback', 'markAsEmptyFeedback',
|
||||
'selectThumbsUp', 'selectThumbsDown', 'unselectThumbsUp', 'unselectThumbsDown', 'thumbsUpClickHandler', 'thumbsDownClickHandler',
|
||||
'sendFeedbackForCurrentTranscript', 'onHideLanguageMenu', 'getCurrentLanguage', 'loadAndSetVisibility', 'showWidget', 'hideWidget'
|
||||
);
|
||||
|
||||
this.state = state;
|
||||
this.state.videoTranscriptFeedback = this;
|
||||
this.currentTranscriptLanguage = this.state.lang;
|
||||
this.transcriptLanguages = this.state.config.transcriptLanguages;
|
||||
|
||||
if (this.state.el.find('.wrapper-transcript-feedback').length) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoTranscriptFeedbackHandler.prototype = {
|
||||
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events);
|
||||
},
|
||||
|
||||
// Initializes the module.
|
||||
initialize: function() {
|
||||
this.el = this.state.el.find('.wrapper-transcript-feedback');
|
||||
|
||||
this.videoId = this.el.data('video-id');
|
||||
this.userId = this.el.data('user-id');
|
||||
this.aiTranslationsUrl = this.state.config.aiTranslationsUrl;
|
||||
|
||||
this.thumbsUpButton = this.el.find('.thumbs-up-btn');
|
||||
this.thumbsDownButton = this.el.find('.thumbs-down-btn');
|
||||
this.thumbsUpButton.on('click', this.thumbsUpClickHandler);
|
||||
this.thumbsDownButton.on('click', this.thumbsDownClickHandler);
|
||||
|
||||
this.events = {
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.loadAndSetVisibility();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
getFeedbackForCurrentTranscript: function() {
|
||||
var self = this;
|
||||
var url = self.aiTranslationsUrl + '/transcript-feedback' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_uuid=' + self.videoId + '&user_id=' + self.userId;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
if (data && data.value === true) {
|
||||
self.markAsPositiveFeedback();
|
||||
self.currentFeedback = true;
|
||||
} else {
|
||||
if (data && data.value === false) {
|
||||
self.markAsNegativeFeedback();
|
||||
self.currentFeedback = false;
|
||||
} else {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
markAsPositiveFeedback: function() {
|
||||
this.selectThumbsUp();
|
||||
this.unselectThumbsDown();
|
||||
},
|
||||
|
||||
markAsNegativeFeedback: function() {
|
||||
this.selectThumbsDown();
|
||||
this.unselectThumbsUp();
|
||||
},
|
||||
|
||||
markAsEmptyFeedback: function() {
|
||||
this.unselectThumbsUp();
|
||||
this.unselectThumbsDown();
|
||||
},
|
||||
|
||||
selectThumbsUp: function() {
|
||||
var thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
|
||||
if (thumbsUpIcon[0].classList.contains('fa-thumbs-o-up')) {
|
||||
thumbsUpIcon[0].classList.remove("fa-thumbs-o-up");
|
||||
thumbsUpIcon[0].classList.add("fa-thumbs-up");
|
||||
}
|
||||
},
|
||||
|
||||
selectThumbsDown: function() {
|
||||
var thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
|
||||
if (thumbsDownIcon[0].classList.contains('fa-thumbs-o-down')) {
|
||||
thumbsDownIcon[0].classList.remove("fa-thumbs-o-down");
|
||||
thumbsDownIcon[0].classList.add("fa-thumbs-down");
|
||||
}
|
||||
},
|
||||
|
||||
unselectThumbsUp: function() {
|
||||
var thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
|
||||
if (thumbsUpIcon[0].classList.contains('fa-thumbs-up')) {
|
||||
thumbsUpIcon[0].classList.remove("fa-thumbs-up");
|
||||
thumbsUpIcon[0].classList.add("fa-thumbs-o-up");
|
||||
}
|
||||
},
|
||||
|
||||
unselectThumbsDown: function() {
|
||||
var thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
|
||||
if (thumbsDownIcon[0].classList.contains('fa-thumbs-down')) {
|
||||
thumbsDownIcon[0].classList.remove("fa-thumbs-down");
|
||||
thumbsDownIcon[0].classList.add("fa-thumbs-o-down");
|
||||
}
|
||||
},
|
||||
|
||||
thumbsUpClickHandler: function() {
|
||||
if (this.currentFeedback) {
|
||||
this.sendFeedbackForCurrentTranscript(null);
|
||||
} else {
|
||||
this.sendFeedbackForCurrentTranscript(true);
|
||||
}
|
||||
},
|
||||
|
||||
thumbsDownClickHandler: function() {
|
||||
if (this.currentFeedback === false) {
|
||||
this.sendFeedbackForCurrentTranscript(null);
|
||||
} else {
|
||||
this.sendFeedbackForCurrentTranscript(false);
|
||||
}
|
||||
},
|
||||
|
||||
sendFeedbackForCurrentTranscript: function(feedbackValue) {
|
||||
var self = this;
|
||||
var url = self.aiTranslationsUrl + '/transcript-feedback/';
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
transcript_language: self.currentTranscriptLanguage,
|
||||
video_uuid: self.videoId,
|
||||
user_id: self.userId,
|
||||
value: feedbackValue,
|
||||
},
|
||||
success: function(data) {
|
||||
if (data && data.value === true) {
|
||||
self.markAsPositiveFeedback();
|
||||
self.currentFeedback = true;
|
||||
} else {
|
||||
if (data && data.value === false) {
|
||||
self.markAsNegativeFeedback();
|
||||
self.currentFeedback = false;
|
||||
} else {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function() {
|
||||
var newLanguageSelected = this.getCurrentLanguage();
|
||||
if (this.currentTranscriptLanguage !== newLanguageSelected) {
|
||||
this.currentTranscriptLanguage = this.getCurrentLanguage();
|
||||
this.loadAndSetVisibility();
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentLanguage: function() {
|
||||
var language = this.state.lang;
|
||||
return language;
|
||||
},
|
||||
|
||||
loadAndSetVisibility: function() {
|
||||
var self = this;
|
||||
var url = self.aiTranslationsUrl + '/video-transcript' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_uuid=' + self.videoId;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
async: false,
|
||||
success: function(data) {
|
||||
if (data && data.status === 'Completed') {
|
||||
self.showWidget();
|
||||
self.getFeedbackForCurrentTranscript();
|
||||
} else {
|
||||
self.hideWidget();
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
self.hideWidget();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showWidget: function() {
|
||||
this.el.show();
|
||||
},
|
||||
|
||||
hideWidget: function() {
|
||||
this.el.hide();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return VideoTranscriptFeedbackHandler;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -65,14 +65,15 @@
|
||||
'video/09_completion.js',
|
||||
'video/10_commands.js',
|
||||
'video/095_video_context_menu.js',
|
||||
'video/036_video_social_sharing.js'
|
||||
'video/036_video_social_sharing.js',
|
||||
'video/037_video_transcript_feedback.js'
|
||||
],
|
||||
function(
|
||||
VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen,
|
||||
VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoAutoAdvanceControl,
|
||||
VideoCaption, VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl,
|
||||
VideoBumper, VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster,
|
||||
VideoCompletionHandler, VideoCommands, VideoContextMenu, VideoSocialSharing
|
||||
VideoCompletionHandler, VideoCommands, VideoContextMenu, VideoSocialSharing, VideoTranscriptFeedback
|
||||
) {
|
||||
/* RequireJS */
|
||||
var youtubeXhr = null,
|
||||
@@ -92,10 +93,10 @@
|
||||
FocusGrabber, VideoControl, VideoPlayPlaceholder,
|
||||
VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl,
|
||||
VideoVolumeControl, VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands,
|
||||
VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler
|
||||
VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler, VideoTranscriptFeedback
|
||||
].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []),
|
||||
bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl,
|
||||
VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin,
|
||||
VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoTranscriptFeedback,
|
||||
VideoEventsBumperPlugin, VideoCompletionHandler],
|
||||
state = {
|
||||
el: el,
|
||||
|
||||
@@ -30,9 +30,9 @@ from xblock.core import XBlock
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE
|
||||
from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE, ATTR_KEY_USER_ID
|
||||
from openedx.core.djangoapps.video_config.models import HLSPlaybackEnabledFlag, CourseYoutubeBlockedFlag
|
||||
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
|
||||
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE, TRANSCRIPT_FEEDBACK
|
||||
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
@@ -448,6 +448,7 @@ class VideoBlock(
|
||||
'transcriptAvailableTranslationsUrl': self.runtime.handler_url(
|
||||
self, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
|
||||
'transcriptLanguage': transcript_language,
|
||||
'transcriptLanguages': sorted_languages,
|
||||
'transcriptTranslationUrl': self.runtime.handler_url(
|
||||
@@ -479,6 +480,8 @@ class VideoBlock(
|
||||
'id': self.location.html_id(),
|
||||
'block_id': str(self.location),
|
||||
'course_id': str(self.location.course_key),
|
||||
'video_id': str(self.edx_video_id),
|
||||
'user_id': self.get_user_id(),
|
||||
'is_embed': is_embed,
|
||||
'license': getattr(self, "license", None),
|
||||
'metadata': json.dumps(OrderedDict(metadata)),
|
||||
@@ -486,6 +489,7 @@ class VideoBlock(
|
||||
'track': track_url,
|
||||
'transcript_download_format': transcript_download_format,
|
||||
'transcript_download_formats_list': self.fields['transcript_download_format'].values, # lint-amnesty, pylint: disable=unsubscriptable-object
|
||||
'transcript_feedback_enabled': self.is_transcript_feedback_enabled(),
|
||||
}
|
||||
if self.is_public_sharing_enabled():
|
||||
public_video_url = self.get_public_video_url()
|
||||
@@ -541,6 +545,21 @@ class VideoBlock(
|
||||
else:
|
||||
return self.public_access
|
||||
|
||||
def is_transcript_feedback_enabled(self):
|
||||
"""
|
||||
Is transcript feedback enabled for this video?
|
||||
"""
|
||||
try:
|
||||
# Video transcript feedback must be enabled in order to show the widget
|
||||
feature_enabled = TRANSCRIPT_FEEDBACK.is_enabled(self.location.course_key)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.exception(f"Error retrieving course for course ID: {self.location.course_key}")
|
||||
return False
|
||||
return feature_enabled
|
||||
|
||||
def get_user_id(self):
|
||||
return self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_ID)
|
||||
|
||||
def get_public_video_url(self):
|
||||
"""
|
||||
Returns the public video url
|
||||
|
||||
Reference in New Issue
Block a user