Merge pull request #2899 from edx/alex/multiple_transcripts_editor
Alex/multiple transcripts editor
This commit is contained in:
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Blades: Create an upload modal for video transcript translations (BLD-751).
|
||||
|
||||
Studio: Add ability to reorder Pages and hide the Wiki page. STUD-1375
|
||||
|
||||
Blades: Added template for iFrames. BLD-611.
|
||||
|
||||
@@ -332,10 +332,15 @@ def get_codemirror_value(index=0):
|
||||
return $('div.CodeMirror:eq({})').get(0).CodeMirror.getValue();
|
||||
""".format(index))
|
||||
|
||||
def upload_file(filename):
|
||||
path = os.path.join(TEST_ROOT, filename)
|
||||
|
||||
def attach_file(filename, sub_path):
|
||||
path = os.path.join(TEST_ROOT, sub_path, filename)
|
||||
world.browser.execute_script("$('input.file-input').css('display', 'block')")
|
||||
world.browser.attach_file('file', os.path.abspath(path))
|
||||
|
||||
|
||||
def upload_file(filename, sub_path=''):
|
||||
attach_file(filename, sub_path)
|
||||
button_css = '.upload-dialog .action-upload'
|
||||
world.css_click(button_css)
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
|
||||
|
||||
# Check if the web object is a list type
|
||||
# If so, we use a slightly different mechanism for determining its value
|
||||
if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict'):
|
||||
if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict') or setting.has_class('metadata-video-translations'):
|
||||
list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
|
||||
assert_equal(value, list_value)
|
||||
elif setting.has_class('metadata-videolist-enum'):
|
||||
|
||||
@@ -27,7 +27,7 @@ def assert_create_new_textbook_msg(_step):
|
||||
|
||||
@step(u'I upload the textbook "([^"]*)"$')
|
||||
def upload_textbook(_step, file_name):
|
||||
upload_file(file_name)
|
||||
upload_file(file_name, sub_path="uploads/")
|
||||
|
||||
|
||||
@step(u'I click (on )?the New Textbook button')
|
||||
|
||||
@@ -334,7 +334,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
@@ -345,7 +345,7 @@ Feature: CMS Transcripts
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I enter a "test_transcripts.webm" source to field number 3
|
||||
And I enter a "uk_transcripts.webm" source to field number 3
|
||||
Then I see status message "found"
|
||||
|
||||
#20
|
||||
@@ -353,21 +353,21 @@ Feature: CMS Transcripts
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "test_transcripts.mp4" source to field number 1
|
||||
And I enter a "uk_transcripts.mp4" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "test_transcripts" in the field "Transcript (primary)"
|
||||
And I see value "uk_transcripts" in the field "Transcript (primary)"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 2
|
||||
Then I see status message "replace"
|
||||
|
||||
And I see choose button "test_transcripts.mp4" number 1
|
||||
And I see choose button "uk_transcripts.mp4" number 1
|
||||
And I see choose button "t_not_exist.webm" number 2
|
||||
And I click transcript button "choose" number 2
|
||||
And I see value "test_transcripts|t_not_exist" in the field "Transcript (primary)"
|
||||
And I see value "uk_transcripts|t_not_exist" in the field "Transcript (primary)"
|
||||
|
||||
# Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927
|
||||
#21
|
||||
@@ -456,7 +456,7 @@ Feature: CMS Transcripts
|
||||
|
||||
And I enter a "video_name_1.mp4" source to field number 1
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1" in the field "Transcript (primary)"
|
||||
|
||||
@@ -487,7 +487,7 @@ Feature: CMS Transcripts
|
||||
|
||||
And I enter a "video_name_2.webm" source to field number 2
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1|video_name_2" in the field "Transcript (primary)"
|
||||
|
||||
@@ -503,7 +503,7 @@ Feature: CMS Transcripts
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 1
|
||||
Then I see status message "not found"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
|
||||
And I save changes
|
||||
@@ -525,7 +525,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "not found"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I clear field number 1
|
||||
Then I see status message "found"
|
||||
@@ -690,7 +690,7 @@ Feature: CMS Transcripts
|
||||
|
||||
And I enter a "video_name_1.1.2.mp4" source to field number 1
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "test_transcripts.srt"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1.1.2" in the field "Transcript (primary)"
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ def i_enter_a_source(_step, link, index):
|
||||
def upload_file(_step, file_name):
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', file_name.strip())
|
||||
world.browser.execute_script("$('form.file-chooser').show()")
|
||||
world.browser.attach_file('file', os.path.abspath(path))
|
||||
world.browser.attach_file('transcript-file', os.path.abspath(path))
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
Feature: CMS Video Component Editor
|
||||
As a course author, I want to be able to create video components
|
||||
|
||||
# 1
|
||||
Scenario: User can view Video metadata
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
Then I see the correct video settings and default values
|
||||
|
||||
# 2
|
||||
# Safari has trouble saving values on Sauce
|
||||
@skip_safari
|
||||
Scenario: User can modify Video display name
|
||||
@@ -16,6 +18,7 @@ Feature: CMS Video Component Editor
|
||||
Then I can modify the display name
|
||||
And my video display name change is persisted on save
|
||||
|
||||
# 3
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are hidden when "transcript display" is false
|
||||
@@ -23,9 +26,201 @@ Feature: CMS Video Component Editor
|
||||
And I have set "transcript display" to False
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 4
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are shown when "transcript display" is true
|
||||
Given I have created a Video component with subtitles
|
||||
And I have set "transcript display" to True
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
# 5
|
||||
Scenario: Translations uploading works correctly
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "zh"
|
||||
And I upload transcript file "uk_transcripts.srt" for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
|
||||
# 6
|
||||
Scenario: User can upload transcript file with > 1mb size
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "1mb_transcripts.srt" for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 7
|
||||
Scenario: Translations downloading works correctly w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
And video language menu has "uk, zh" translations
|
||||
Then I can download transcript for "zh" language code, that contains text "好 各位同学"
|
||||
And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас."
|
||||
|
||||
# 8
|
||||
Scenario: Translations downloading works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
Then I can download transcript for "zh" language code, that contains text "好 各位同学"
|
||||
And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас."
|
||||
|
||||
# 9
|
||||
Scenario: Translations removing works correctly w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
Then I remove translation for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "zh"
|
||||
Then I remove translation for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 10
|
||||
Scenario: Translations removing works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "uk_transcripts.srt" for "uk" language code
|
||||
And I see translations for "uk"
|
||||
Then I remove translation for "uk" language code
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 11
|
||||
Scenario: Translations clearing works correctly w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
And video language menu has "uk, zh" translations
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "uk, zh"
|
||||
And I click button "Clear"
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 12
|
||||
Scenario: Translations clearing works correctly w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript files:
|
||||
|lang_code|filename |
|
||||
|uk |uk_transcripts.srt |
|
||||
|zh |chinese_transcripts.srt|
|
||||
And I click button "Clear"
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# 13
|
||||
Scenario: User cannot upload translations in sjson format
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I click button "Add"
|
||||
And I choose "uk" language code
|
||||
And I try to upload transcript file "uk_transcripts.sjson"
|
||||
Then I see validation error "Only SRT files can be uploaded. Please select a file ending in .srt to upload."
|
||||
|
||||
# 14
|
||||
Scenario: User can easy replace the translation by another one w/ preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "好 各位同学" text in the captions
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I see translations for "zh"
|
||||
And I replace transcript file for "zh" language code by "uk_transcripts.srt"
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 15
|
||||
Scenario: User can easy replace the translation by another one w/o preliminary saving
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I see translations for "zh"
|
||||
And I replace transcript file for "zh" language code by "uk_transcripts.srt"
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 16
|
||||
Scenario: Upload "zh" file "A" -> Remove "zh" -> Upload "zh" file "B"
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
|
||||
And I see translations for "zh"
|
||||
Then I remove translation for "zh" language code
|
||||
And I upload transcript file "uk_transcripts.srt" for "zh" language code
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
And I see "Привіт, edX вітає вас." text in the captions
|
||||
|
||||
# 17
|
||||
Scenario: User cannot select the same language twice
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I click button "Add"
|
||||
And I choose "zh" language code
|
||||
And I click button "Add"
|
||||
Then I cannot choose "zh" language code
|
||||
|
||||
|
||||
@@ -1,8 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# disable missing docstring
|
||||
# pylint: disable=C0111
|
||||
|
||||
import requests
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_equal, assert_in, assert_not_equal # pylint: disable=E0611
|
||||
from terrain.steps import reload_the_page
|
||||
from django.conf import settings
|
||||
from common import upload_file, attach_file
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
LANGUAGES = {l[0]: l[1] for l in settings.ALL_LANGUAGES}
|
||||
|
||||
TRANSLATION_BUTTONS = {
|
||||
'add': '.metadata-video-translations .create-action',
|
||||
'upload': '.metadata-video-translations .upload-action',
|
||||
'download': '.metadata-video-translations .download-action',
|
||||
'remove': '.metadata-video-translations .remove-action',
|
||||
'clear': '.metadata-video-translations .setting-clear',
|
||||
}
|
||||
|
||||
VIDEO_MENUS = {
|
||||
'language': '.lang .menu',
|
||||
}
|
||||
|
||||
|
||||
class RequestHandlerWithSessionId(object):
|
||||
def get(self, url):
|
||||
"""
|
||||
Sends a request.
|
||||
"""
|
||||
kwargs = dict()
|
||||
|
||||
session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name'] == u'sessionid']
|
||||
if session_id:
|
||||
kwargs.update({
|
||||
'cookies': session_id[0]
|
||||
})
|
||||
|
||||
response = requests.get(url, **kwargs)
|
||||
self.response = response
|
||||
self.status_code = response.status_code
|
||||
self.headers = response.headers
|
||||
self.content = response.content
|
||||
|
||||
return self
|
||||
|
||||
def is_success(self):
|
||||
"""
|
||||
Returns `True` if the response was succeed, otherwise, returns `False`.
|
||||
"""
|
||||
if self.status_code < 400:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_header(self, name, value):
|
||||
"""
|
||||
Returns `True` if the response header exist and has appropriate value,
|
||||
otherwise, returns `False`.
|
||||
"""
|
||||
if value in self.headers.get(name, ''):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def success_upload_file(filename):
|
||||
upload_file(filename, sub_path="uploads/")
|
||||
world.css_has_text('#upload_confirm', 'Success!')
|
||||
world.is_css_not_present('.wrapper-dialog-assetupload', wait_time=30)
|
||||
|
||||
|
||||
def get_translations_container():
|
||||
return world.browser.find_by_xpath('//label[text()="Transcript Translations"]/following-sibling::div')
|
||||
|
||||
|
||||
def get_setting_container(lang_code):
|
||||
try:
|
||||
get_xpath = lambda value: './/descendant::a[@data-lang="{}" and contains(@class,"remove-setting")]/parent::*'.format(value)
|
||||
return get_translations_container().find_by_xpath(get_xpath(lang_code)).first
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_last_dropdown():
|
||||
return get_translations_container().find_by_xpath('.//descendant::select[last()]').last
|
||||
|
||||
|
||||
def choose_option(dropdown, value):
|
||||
dropdown.find_by_value(value)[0].click()
|
||||
|
||||
|
||||
def choose_new_lang(lang_code):
|
||||
world.css_click(TRANSLATION_BUTTONS['add'])
|
||||
choose_option(get_last_dropdown(), lang_code)
|
||||
assert_equal(get_last_dropdown().value, lang_code, "Option with provided value is not available or was not selected")
|
||||
|
||||
|
||||
def open_menu(menu):
|
||||
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
|
||||
selector=VIDEO_MENUS[menu]
|
||||
))
|
||||
|
||||
|
||||
@step('I have set "transcript display" to (.*)$')
|
||||
@@ -22,9 +119,9 @@ def shows_captions(_step, show_captions):
|
||||
world.wait_for_js_variable_truthy("Video")
|
||||
world.wait(0.5)
|
||||
if show_captions == 'does not':
|
||||
assert world.is_css_present('div.video.closed')
|
||||
assert_true(world.is_css_present('div.video.closed'))
|
||||
else:
|
||||
assert world.is_css_not_present('div.video.closed')
|
||||
assert_true(world.is_css_not_present('div.video.closed'))
|
||||
|
||||
# Prevent cookies from overriding course settings
|
||||
world.browser.cookies.delete('hide_captions')
|
||||
@@ -68,3 +165,131 @@ def video_name_persisted(step):
|
||||
world.get_setting_entry('Display Name'),
|
||||
'Display Name', '3.4', True
|
||||
)
|
||||
|
||||
|
||||
@step('I upload transcript file(?:s)?:$')
|
||||
def upload_transcript(step):
|
||||
input_hidden = '.metadata-video-translations .input'
|
||||
# Number of previously added translations
|
||||
initial_index = len(world.css_find(TRANSLATION_BUTTONS['download']))
|
||||
|
||||
if step.hashes:
|
||||
for i, item in enumerate(step.hashes):
|
||||
lang_code = item['lang_code']
|
||||
filename = item['filename']
|
||||
index = initial_index + i
|
||||
|
||||
choose_new_lang(lang_code)
|
||||
|
||||
expected_text = world.css_text(TRANSLATION_BUTTONS['upload'], index=index)
|
||||
assert_equal(expected_text, "Upload")
|
||||
assert_equal(world.css_find(input_hidden).last.value, "")
|
||||
|
||||
world.css_click(TRANSLATION_BUTTONS['upload'], index=index)
|
||||
success_upload_file(filename)
|
||||
|
||||
world.wait_for_visible(TRANSLATION_BUTTONS['download'], index=index)
|
||||
assert_equal(world.css_find(TRANSLATION_BUTTONS['upload']).last.text, "Replace")
|
||||
assert_equal(world.css_find(input_hidden).last.value, filename)
|
||||
|
||||
|
||||
@step('I try to upload transcript file "([^"]*)"$')
|
||||
def try_to_upload_transcript(step, filename):
|
||||
world.css_click(TRANSLATION_BUTTONS['upload'])
|
||||
attach_file(filename, 'uploads/')
|
||||
|
||||
|
||||
@step('I upload transcript file "([^"]*)" for "([^"]*)" language code$')
|
||||
def upload_transcript_for_lang(step, filename, lang_code):
|
||||
get_xpath = lambda value: './/div/a[contains(@class, "upload-action")]'.format(value)
|
||||
container = get_setting_container(lang_code)
|
||||
|
||||
# If translation isn't uploaded, prepare drop-down and try to find container again
|
||||
choose_new_lang(lang_code)
|
||||
container = get_setting_container(lang_code)
|
||||
|
||||
button = container.find_by_xpath(get_xpath(lang_code)).first
|
||||
button.click()
|
||||
success_upload_file(filename)
|
||||
|
||||
|
||||
@step('I replace transcript file for "([^"]*)" language code by "([^"]*)"$')
|
||||
def replace_transcript_for_lang(step, lang_code, filename):
|
||||
get_xpath = lambda value: './/div/a[contains(@class, "upload-action")]'.format(value)
|
||||
container = get_setting_container(lang_code)
|
||||
|
||||
button = container.find_by_xpath(get_xpath(lang_code)).first
|
||||
button.click()
|
||||
success_upload_file(filename)
|
||||
|
||||
|
||||
@step('I see validation error "([^"]*)"$')
|
||||
def verify_validation_error_message(step, error_message):
|
||||
assert_equal(world.css_text('#upload_error'), error_message)
|
||||
|
||||
|
||||
@step('I can download transcript for "([^"]*)" language code, that contains text "([^"]*)"$')
|
||||
def i_can_download_transcript(_step, lang_code, text):
|
||||
MIME_TYPE = 'application/x-subrip'
|
||||
get_xpath = lambda value: './/div/a[contains(text(), "Download")]'.format(value)
|
||||
container = get_setting_container(lang_code)
|
||||
assert container
|
||||
button = container.find_by_xpath(get_xpath(lang_code)).first
|
||||
url = button['href']
|
||||
request = RequestHandlerWithSessionId()
|
||||
assert_true(request.get(url).is_success())
|
||||
assert_true(request.check_header('content-type', MIME_TYPE))
|
||||
assert_in(text.encode('utf-8'), request.content)
|
||||
|
||||
|
||||
@step('I remove translation for "([^"]*)" language code$')
|
||||
def i_can_remove_transcript(_step, lang_code):
|
||||
get_xpath = lambda value: './/descendant::a[@data-lang="{}" and contains(@class,"remove-setting")]'.format(value)
|
||||
container = get_setting_container(lang_code)
|
||||
assert container
|
||||
button = container.find_by_xpath(get_xpath(lang_code)).first
|
||||
button.click()
|
||||
|
||||
|
||||
@step('I see translations for "([^"]*)"$')
|
||||
def verify_translations(_step, lang_codes_string):
|
||||
expected = [l.strip() for l in lang_codes_string.split(',')]
|
||||
actual = [l['data-lang'] for l in world.css_find('.metadata-video-translations .remove-setting')]
|
||||
assert_equal(set(expected), set(actual))
|
||||
|
||||
|
||||
@step('I do not see translations$')
|
||||
def no_translations(_step):
|
||||
assert_true(world.is_css_not_present('.metadata-video-translations .remove-setting'))
|
||||
|
||||
|
||||
@step('I confirm prompt$')
|
||||
def confirm_prompt(_step):
|
||||
world.confirm_studio_prompt()
|
||||
|
||||
|
||||
@step('I (cannot )?choose "([^"]*)" language code$')
|
||||
def i_choose_lang_code(_step, cannot, lang_code):
|
||||
choose_option(get_last_dropdown(), lang_code)
|
||||
if cannot:
|
||||
assert_not_equal(get_last_dropdown().value, lang_code, "Option with provided value was selected, but shouldn't")
|
||||
else:
|
||||
assert_equal(get_last_dropdown().value, lang_code, "Option with provided value is not available or was not selected")
|
||||
|
||||
|
||||
@step('I click button "([^"]*)"$')
|
||||
def click_button(_step, button):
|
||||
world.css_click(TRANSLATION_BUTTONS[button.lower()])
|
||||
|
||||
|
||||
@step('video language menu has "([^"]*)" translations$')
|
||||
def i_see_correct_langs(_step, langs):
|
||||
menu_name = 'language'
|
||||
open_menu(menu_name)
|
||||
items = world.css_find(VIDEO_MENUS[menu_name] + ' li')
|
||||
translations = {t.strip(): LANGUAGES[t.strip()] for t in langs.split(',')}
|
||||
|
||||
assert_equal(len(translations), len(items))
|
||||
for lang_code, label in translations.items():
|
||||
assert_true(any([i.text == label for i in items]))
|
||||
assert_true(any([i['data-lang-code'] == lang_code for i in items]))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Tests for transcripts_utils. """
|
||||
import unittest
|
||||
from uuid import uuid4
|
||||
@@ -548,3 +549,15 @@ class TestTranscript(unittest.TestCase):
|
||||
def test_convert_srt_to_sjson(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson')
|
||||
|
||||
|
||||
class TestSubsFilename(unittest.TestCase):
|
||||
"""
|
||||
Tests for subs_filename funtion.
|
||||
"""
|
||||
|
||||
def test_unicode(self):
|
||||
name = transcripts_utils.subs_filename(u"˙∆©ƒƒƒ")
|
||||
self.assertEqual(name, u'subs_˙∆©ƒƒƒ.srt.sjson')
|
||||
name = transcripts_utils.subs_filename(u"˙∆©ƒƒƒ", 'uk')
|
||||
self.assertEqual(name, u'uk_subs_˙∆©ƒƒƒ.srt.sjson')
|
||||
|
||||
@@ -143,7 +143,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'file': self.good_srt_file,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -162,7 +162,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
|
||||
def test_fail_data_without_id(self):
|
||||
link = reverse('upload_transcripts')
|
||||
resp = self.client.post(link, {'file': self.good_srt_file})
|
||||
resp = self.client.post(link, {'transcript-file': self.good_srt_file})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "locator" form data.')
|
||||
|
||||
@@ -178,7 +178,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': 'BAD_LOCATOR',
|
||||
'file': self.good_srt_file,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -193,7 +193,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR'),
|
||||
'file': self.good_srt_file,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -222,7 +222,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': item_locator,
|
||||
'file': self.good_srt_file,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -240,7 +240,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'file': self.good_srt_file,
|
||||
'transcript-file': self.good_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -257,7 +257,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'file': self.bad_data_srt_file,
|
||||
'transcript-file': self.bad_data_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -272,7 +272,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'file': self.bad_name_srt_file,
|
||||
'transcript-file': self.bad_name_srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
@@ -299,7 +299,7 @@ class TestUploadtranscripts(Basetranscripts):
|
||||
filename = os.path.splitext(os.path.basename(srt_file.name))[0]
|
||||
resp = self.client.post(link, {
|
||||
'locator': self.item_locator,
|
||||
'file': srt_file,
|
||||
'transcript-file': srt_file,
|
||||
'video_list': json.dumps([{
|
||||
'type': 'html5',
|
||||
'video': filename,
|
||||
|
||||
@@ -87,7 +87,7 @@ def upload_transcripts(request):
|
||||
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
|
||||
return error_response(response, "Can't find item by locator.")
|
||||
|
||||
if 'file' not in request.FILES:
|
||||
if 'transcript-file' not in request.FILES:
|
||||
return error_response(response, 'POST data without "file" form data.')
|
||||
|
||||
video_list = request.POST.get('video_list')
|
||||
@@ -99,8 +99,8 @@ def upload_transcripts(request):
|
||||
except ValueError:
|
||||
return error_response(response, 'Invalid video_list JSON.')
|
||||
|
||||
source_subs_filedata = request.FILES['file'].read().decode('utf8')
|
||||
source_subs_filename = request.FILES['file'].name
|
||||
source_subs_filedata = request.FILES['transcript-file'].read().decode('utf8')
|
||||
source_subs_filename = request.FILES['transcript-file'].name
|
||||
|
||||
if '.' not in source_subs_filename:
|
||||
return error_response(response, "Undefined file extension.")
|
||||
|
||||
@@ -203,9 +203,9 @@ define([
|
||||
"coffee/spec/views/overview_spec",
|
||||
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
|
||||
|
||||
"js/spec/transcripts/utils_spec", "js/spec/transcripts/editor_spec",
|
||||
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
|
||||
"js/spec/transcripts/file_uploader_spec",
|
||||
"js/spec/video/transcripts/utils_spec", "js/spec/video/transcripts/editor_spec",
|
||||
"js/spec/video/transcripts/videolist_spec", "js/spec/video/transcripts/message_manager_spec",
|
||||
"js/spec/video/transcripts/file_uploader_spec",
|
||||
|
||||
"js/spec/models/explicit_url_spec"
|
||||
|
||||
|
||||
@@ -174,5 +174,6 @@ requirejs.config({
|
||||
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
|
||||
define([
|
||||
"coffee/spec/views/assets_spec"
|
||||
"coffee/spec/views/assets_spec",
|
||||
"js/spec/video/translations_editor_spec"
|
||||
])
|
||||
|
||||
@@ -14,31 +14,44 @@ define ["js/models/uploads"], (FileUpload) ->
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "is invalid for text files by default", ->
|
||||
file = {"type": "text/plain"}
|
||||
file = {"type": "text/plain", "name": "filename.txt"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeFalsy()
|
||||
|
||||
it "is invalid for PNG files by default", ->
|
||||
file = {"type": "image/png"}
|
||||
file = {"type": "image/png", "name": "filename.png"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeFalsy()
|
||||
|
||||
it "can accept a file type when explicitly set", ->
|
||||
file = {"type": "image/png"}
|
||||
file = {"type": "image/png", "name": "filename.png"}
|
||||
@model.set("mimeTypes": ["image/png"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept a file format when explicitly set", ->
|
||||
file = {"type": "", "name": "filename.png"}
|
||||
@model.set("fileFormats": ["png"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept multiple file types", ->
|
||||
file = {"type": "image/gif"}
|
||||
file = {"type": "image/gif", "name": "filename.gif"}
|
||||
@model.set("mimeTypes": ["image/png", "image/jpeg", "image/gif"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept multiple file formats", ->
|
||||
file = {"type": "image/gif", "name": "filename.gif"}
|
||||
@model.set("fileFormats": ["png", "jpeg", "gif"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
describe "fileTypes", ->
|
||||
it "returns a list of the uploader's file types", ->
|
||||
@model.set('mimeTypes', ['image/png', 'application/json'])
|
||||
expect(@model.fileTypes()).toEqual(['PNG', 'JSON'])
|
||||
@model.set('fileFormats', ['gif', 'srt'])
|
||||
expect(@model.fileTypes()).toEqual(['PNG', 'JSON', 'GIF', 'SRT'])
|
||||
|
||||
describe "formatValidTypes", ->
|
||||
it "returns a map of formatted file types and extensions", ->
|
||||
|
||||
@@ -16,6 +16,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
|
||||
@dialogResponse = dialogResponse = []
|
||||
@view = new UploadDialog(
|
||||
model: @model,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
onSuccess: (response) =>
|
||||
dialogResponse.push(response.response)
|
||||
)
|
||||
|
||||
@@ -9,10 +9,11 @@ var FileUpload = Backbone.Model.extend({
|
||||
"uploadedBytes": 0,
|
||||
"totalBytes": 0,
|
||||
"finished": false,
|
||||
"mimeTypes": []
|
||||
"mimeTypes": [],
|
||||
"fileFormats": []
|
||||
},
|
||||
validate: function(attrs, options) {
|
||||
if(attrs.selectedFile && !_.contains(this.attributes.mimeTypes, attrs.selectedFile.type)) {
|
||||
if(attrs.selectedFile && !this.checkTypeValidity(attrs.selectedFile)) {
|
||||
return {
|
||||
message: _.template(
|
||||
gettext("Only <%= fileTypes %> files can be uploaded. Please select a file ending in <%= fileExtensions %> to upload."),
|
||||
@@ -24,17 +25,37 @@ var FileUpload = Backbone.Model.extend({
|
||||
},
|
||||
// Return a list of this uploader's valid file types
|
||||
fileTypes: function() {
|
||||
return _.map(
|
||||
this.attributes.mimeTypes,
|
||||
function(type) {
|
||||
return type.split('/')[1].toUpperCase();
|
||||
}
|
||||
);
|
||||
var mimeTypes = _.map(
|
||||
this.attributes.mimeTypes,
|
||||
function(type) {
|
||||
return type.split('/')[1].toUpperCase();
|
||||
}
|
||||
),
|
||||
fileFormats = _.map(
|
||||
this.attributes.fileFormats,
|
||||
function(type) {
|
||||
return type.toUpperCase();
|
||||
}
|
||||
);
|
||||
|
||||
return mimeTypes.concat(fileFormats);
|
||||
},
|
||||
checkTypeValidity: function (file) {
|
||||
var attrs = this.attributes,
|
||||
getRegExp = function (formats) {
|
||||
// Creates regular expression like: /(?:.+)\.(jpg|png|gif)$/i
|
||||
return RegExp(('(?:.+)\\.(' + formats.join('|') + ')$'), 'i');
|
||||
};
|
||||
|
||||
return _.contains(attrs.mimeTypes, file.type) ||
|
||||
getRegExp(attrs.fileFormats).test(file.name);
|
||||
},
|
||||
// Return strings for the valid file types and extensions this
|
||||
// uploader accepts, formatted as natural language
|
||||
formatValidTypes: function() {
|
||||
if(this.attributes.mimeTypes.length === 1) {
|
||||
var attrs = this.attributes;
|
||||
|
||||
if(attrs.mimeTypes.concat(attrs.fileFormats).length === 1) {
|
||||
return {
|
||||
fileTypes: this.fileTypes()[0],
|
||||
fileExtensions: '.' + this.fileTypes()[0].toLowerCase()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/editor",
|
||||
"js/views/video/transcripts/utils", "js/views/video/transcripts/editor",
|
||||
"js/views/metadata", "js/models/metadata", "js/collections/metadata",
|
||||
"underscore.string", "xmodule", "js/views/transcripts/metadata_videolist",
|
||||
"underscore.string", "xmodule", "js/views/video/transcripts/metadata_videolist",
|
||||
"jasmine-jquery"
|
||||
],
|
||||
function ($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCollection, _str) {
|
||||
@@ -1,16 +1,16 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "underscore",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/file_uploader",
|
||||
"js/views/video/transcripts/utils", "js/views/video/transcripts/file_uploader",
|
||||
"xmodule", "jquery.form", "jasmine-jquery"
|
||||
],
|
||||
function ($, _, Utils, FileUploader) {
|
||||
describe('Transcripts.FileUploader', function () {
|
||||
var videoListEntryTemplate = readFixtures(
|
||||
'transcripts/metadata-videolist-entry.underscore'
|
||||
'video/transcripts/metadata-videolist-entry.underscore'
|
||||
),
|
||||
fileUploadTemplate = readFixtures(
|
||||
'transcripts/file-upload.underscore'
|
||||
'video/transcripts/file-upload.underscore'
|
||||
),
|
||||
view;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "underscore",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/message_manager",
|
||||
"js/views/transcripts/file_uploader", "sinon", "jasmine-jquery",
|
||||
"js/views/video/transcripts/utils", "js/views/video/transcripts/message_manager",
|
||||
"js/views/video/transcripts/file_uploader", "sinon", "jasmine-jquery",
|
||||
"xmodule"
|
||||
],
|
||||
function ($, _, Utils, MessageManager, FileUploader, sinon) {
|
||||
|
||||
describe('Transcripts.MessageManager', function () {
|
||||
var videoListEntryTemplate = readFixtures(
|
||||
'transcripts/metadata-videolist-entry.underscore'
|
||||
'video/transcripts/metadata-videolist-entry.underscore'
|
||||
),
|
||||
foundTemplate = readFixtures(
|
||||
'transcripts/messages/transcripts-found.underscore'
|
||||
'video/transcripts/messages/transcripts-found.underscore'
|
||||
),
|
||||
handlers = {
|
||||
importHandler: ['replace', 'Error: Import failed.'],
|
||||
@@ -1,7 +1,7 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "underscore",
|
||||
"js/views/transcripts/utils",
|
||||
"js/views/video/transcripts/utils",
|
||||
"underscore.string", "xmodule", "jasmine-jquery"
|
||||
],
|
||||
function ($, _, Utils, _str) {
|
||||
@@ -1,14 +1,14 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "underscore",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/metadata_videolist",
|
||||
"js/views/video/transcripts/utils", "js/views/video/transcripts/metadata_videolist",
|
||||
"js/views/metadata", "js/models/metadata", "js/views/abstract_editor",
|
||||
"sinon", "xmodule", "jasmine-jquery"
|
||||
],
|
||||
function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, sinon) {
|
||||
describe('CMS.Views.Metadata.VideoList', function () {
|
||||
var videoListEntryTemplate = readFixtures(
|
||||
'transcripts/metadata-videolist-entry.underscore'
|
||||
'video/transcripts/metadata-videolist-entry.underscore'
|
||||
),
|
||||
abstractEditor = AbstractEditor.prototype,
|
||||
component_locator = 'component_locator',
|
||||
318
cms/static/js/spec/video/translations_editor_spec.js
Normal file
318
cms/static/js/spec/video/translations_editor_spec.js
Normal file
@@ -0,0 +1,318 @@
|
||||
define(
|
||||
[
|
||||
'jquery', 'underscore', 'js/spec/create_sinon', 'squire'
|
||||
],
|
||||
function ($, _, create_sinon, Squire) {
|
||||
'use strict';
|
||||
describe('VideoTranslations', function () {
|
||||
var TranslationsEntryTemplate = readFixtures(
|
||||
'video/metadata-translations-entry.underscore'
|
||||
),
|
||||
TranslationsItenTemplate = readFixtures(
|
||||
'video/metadata-translations-item.underscore'
|
||||
),
|
||||
feedbackTpl = readFixtures('system-feedback.underscore'),
|
||||
modelStub = {
|
||||
default_value: {
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt'
|
||||
},
|
||||
display_name: 'Transcript Translation',
|
||||
explicitly_set: false,
|
||||
field_name: 'translations',
|
||||
help: 'Specifies the name for this component.',
|
||||
type: 'VideoTranslations',
|
||||
languages: [
|
||||
{code: 'zh', label: 'Chinese'},
|
||||
{code: 'en', label: 'English'},
|
||||
{code: 'fr', label: 'French'},
|
||||
{code: 'ru', label: 'Russian'},
|
||||
{code: 'uk', label: 'Ukrainian'}
|
||||
],
|
||||
value: {
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt',
|
||||
'uk': 'uk.srt',
|
||||
'fr': 'fr.srt'
|
||||
}
|
||||
},
|
||||
self, injector;
|
||||
|
||||
var setValue = function (view, value) {
|
||||
view.setValueInEditor(value);
|
||||
view.updateModel();
|
||||
};
|
||||
|
||||
var createPromptSpy = function (name) {
|
||||
var spy = jasmine.createSpyObj(name, ['constructor', 'show', 'hide']);
|
||||
spy.constructor.andReturn(spy);
|
||||
spy.show.andReturn(spy);
|
||||
spy.extend = jasmine.createSpy().andReturn(spy.constructor);
|
||||
|
||||
return spy;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
self = this;
|
||||
|
||||
this.addMatchers({
|
||||
assertValueInView: function(expected) {
|
||||
var value = this.actual.getValueFromEditor();
|
||||
return this.env.equals_(value, expected);
|
||||
},
|
||||
assertCanUpdateView: function (expected) {
|
||||
var view = this.actual,
|
||||
value;
|
||||
|
||||
view.setValueInEditor(expected);
|
||||
value = view.getValueFromEditor();
|
||||
|
||||
return this.env.equals_(value, expected);
|
||||
},
|
||||
assertClear: function (modelValue) {
|
||||
var env = this.env,
|
||||
view = this.actual,
|
||||
model = view.model;
|
||||
|
||||
return model.getValue() === null &&
|
||||
env.equals_(model.getDisplayValue(), modelValue) &&
|
||||
env.equals_(view.getValueFromEditor(), modelValue);
|
||||
},
|
||||
assertUpdateModel: function (originalValue, newValue) {
|
||||
var env = this.env,
|
||||
view = this.actual,
|
||||
model = view.model,
|
||||
expectOriginal;
|
||||
|
||||
view.setValueInEditor(newValue);
|
||||
expectOriginal = env.equals_(model.getValue(), originalValue);
|
||||
view.updateModel();
|
||||
|
||||
return expectOriginal &&
|
||||
env.equals_(model.getValue(), newValue);
|
||||
},
|
||||
verifyKeysUnique: function (initial, expected, testData) {
|
||||
var env = this.env,
|
||||
view = this.actual,
|
||||
item, value;
|
||||
|
||||
view.setValueInEditor(initial);
|
||||
view.updateModel();
|
||||
view.$el.find('.create-setting').click();
|
||||
item = view.$el.find('.list-settings-item').last();
|
||||
item.find('select').val(testData.key);
|
||||
item.find('input:hidden').val(testData.value);
|
||||
value = view.getValueFromEditor();
|
||||
|
||||
return env.equals_(value, expected);
|
||||
},
|
||||
verifyButtons: function (upload, download, remove, index) {
|
||||
var view = this.actual,
|
||||
items = view.$el.find('.list-settings-item'),
|
||||
item = index ? items.eq(index) : items.last(),
|
||||
uploadBtn = item.find('.upload-setting'),
|
||||
downloadBtn = item.find('.download-setting'),
|
||||
removeBtn = item.find('.remove-setting');
|
||||
|
||||
|
||||
upload = upload ? uploadBtn.length : !uploadBtn.length;
|
||||
download = download ? downloadBtn.length : !downloadBtn.length;
|
||||
remove = remove ? removeBtn.length : !removeBtn.length;
|
||||
|
||||
return upload && download && remove;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
appendSetFixtures($('<script>', {
|
||||
id: 'metadata-translations-entry',
|
||||
type: 'text/template'
|
||||
}).text(TranslationsEntryTemplate));
|
||||
|
||||
appendSetFixtures($('<script>', {
|
||||
id: 'metadata-translations-item',
|
||||
type: 'text/template'
|
||||
}).text(TranslationsItenTemplate));
|
||||
|
||||
this.uploadSpies = createPromptSpy('UploadDialog');
|
||||
|
||||
injector = new Squire();
|
||||
injector.mock('js/views/uploads', function () {
|
||||
return self.uploadSpies;
|
||||
});
|
||||
|
||||
runs(function() {
|
||||
injector.require([
|
||||
'js/models/metadata', 'js/views/video/translations_editor'
|
||||
],
|
||||
function(MetadataModel, Translations) {
|
||||
var model = new MetadataModel($.extend(true, {}, modelStub));
|
||||
self.view = new Translations({model: model});
|
||||
});
|
||||
});
|
||||
|
||||
waitsFor(function() {
|
||||
return self.view;
|
||||
}, 'VideoTranslations was not created', 1000);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
injector.clean();
|
||||
injector.remove();
|
||||
});
|
||||
|
||||
it('returns the initial value upon initialization', function () {
|
||||
expect(this.view).assertValueInView({
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt',
|
||||
'uk': 'uk.srt',
|
||||
'fr': 'fr.srt'
|
||||
});
|
||||
|
||||
expect(this.view).verifyButtons(true, true, true);
|
||||
});
|
||||
|
||||
it('updates its value correctly', function () {
|
||||
expect(this.view).assertCanUpdateView({
|
||||
'ru': 'ru.srt',
|
||||
'uk': 'uk.srt',
|
||||
'fr': 'fr.srt'
|
||||
});
|
||||
});
|
||||
|
||||
it('upload works correctly', function () {
|
||||
var options;
|
||||
|
||||
setValue(this.view, {
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt',
|
||||
'uk': 'uk.srt',
|
||||
'fr': 'fr.srt',
|
||||
'zh': ''
|
||||
});
|
||||
|
||||
expect(this.view).verifyButtons(true, false, true);
|
||||
|
||||
this.view.$el.find('.upload-setting').last().click();
|
||||
|
||||
expect(this.uploadSpies.constructor).toHaveBeenCalled();
|
||||
expect(this.uploadSpies.show).toHaveBeenCalled();
|
||||
|
||||
options = this.uploadSpies.constructor.mostRecentCall.args[0];
|
||||
options.onSuccess({'filename': 'zh.srt'});
|
||||
|
||||
expect(this.view).verifyButtons(true, true, true);
|
||||
|
||||
expect(this.view.getValueFromEditor()).toEqual({
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt',
|
||||
'uk': 'uk.srt',
|
||||
'fr': 'fr.srt',
|
||||
'zh': 'zh.srt'
|
||||
});
|
||||
});
|
||||
|
||||
describe('has a clear method to revert to the model default', function () {
|
||||
it('w/ popup, if values were changed', function (){
|
||||
var requests = create_sinon.requests(this),
|
||||
options;
|
||||
|
||||
setValue(this.view, {
|
||||
'fr': 'fr.srt',
|
||||
'uk': 'uk.srt'
|
||||
});
|
||||
|
||||
this.view.$el.find('.create-setting').click();
|
||||
this.view.clear();
|
||||
|
||||
expect(this.view).assertClear({
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt'
|
||||
});
|
||||
expect(this.view.$el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('w/o popup, if just keys were changed', function (){
|
||||
setValue(this.view, {
|
||||
'fr': 'en.srt',
|
||||
'uk': 'ru.srt'
|
||||
});
|
||||
|
||||
this.view.$el.find('.create-setting').click();
|
||||
|
||||
this.view.clear();
|
||||
|
||||
expect(this.view).assertClear({
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt'
|
||||
});
|
||||
|
||||
expect(this.view.$el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
it('has an update model method', function () {
|
||||
expect(this.view).assertUpdateModel(null, {'fr': 'fr.srt'});
|
||||
});
|
||||
|
||||
it('can add an entry', function () {
|
||||
expect(_.keys(this.view.model.get('value')).length).toEqual(4);
|
||||
this.view.$el.find('.create-setting').click();
|
||||
expect(this.view.$el.find('select').length).toEqual(5);
|
||||
});
|
||||
|
||||
describe('can remove an entry', function () {
|
||||
it('w/ popup, if values were changed', function (){
|
||||
var requests = create_sinon.requests(this),
|
||||
options;
|
||||
|
||||
expect(_.keys(this.view.model.get('value')).length).toEqual(4);
|
||||
this.view.$el.find('.remove-setting').last().click();
|
||||
expect(_.keys(this.view.model.get('value')).length).toEqual(3);
|
||||
});
|
||||
|
||||
it('w/o popup, if just keys were changed', function (){
|
||||
setValue(this.view, {
|
||||
'en': 'en.srt',
|
||||
'ru': 'ru.srt',
|
||||
'fr': ''
|
||||
});
|
||||
expect(_.keys(this.view.model.get('value')).length).toEqual(3);
|
||||
this.view.$el.find('.remove-setting').last().click();
|
||||
expect(_.keys(this.view.model.get('value')).length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('only allows one blank entry at a time', function () {
|
||||
expect(this.view.$el.find('select').length).toEqual(4);
|
||||
this.view.$el.find('.create-setting').click();
|
||||
this.view.$el.find('.create-setting').click();
|
||||
expect(this.view.$el.find('select').length).toEqual(5);
|
||||
});
|
||||
|
||||
it('only allows unique keys', function () {
|
||||
expect(this.view).verifyKeysUnique(
|
||||
{'ru': 'ru.srt'}, {'ru': 'ru.srt'}, {'key': 'ru', 'value': ''}
|
||||
);
|
||||
|
||||
expect(this.view).verifyKeysUnique(
|
||||
{'ru': 'en.srt'}, {'ru': 'ru.srt'}, {'key': 'ru', 'value': 'ru.srt'}
|
||||
);
|
||||
|
||||
expect(this.view).verifyKeysUnique(
|
||||
{'ru': 'ru.srt'}, {'ru': 'ru.srt'}, {'key': '', 'value': ''}
|
||||
);
|
||||
});
|
||||
|
||||
it('re-enables the add setting button after entering a new value', function () {
|
||||
expect(this.view.$el.find('select').length).toEqual(4);
|
||||
this.view.$el.find('.create-setting').click();
|
||||
expect(this.view).verifyButtons(false, false, true);
|
||||
expect(this.view.$el.find('.create-setting')).toHaveClass('is-disabled');
|
||||
this.view.$el.find('select').last().val('zh');
|
||||
this.view.$el.find('select').last().trigger('change');
|
||||
expect(this.view).verifyButtons(true, false, true);
|
||||
expect(this.view.$el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,9 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon"
|
||||
var request = requests[requests.length - 1];
|
||||
expect(request.url).toEqual("/xblock");
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(request.requestBody).toEqual(json);
|
||||
// There was a problem with order of returned parameters in strings.
|
||||
// Changed to compare objects instead strings.
|
||||
expect(JSON.parse(request.requestBody)).toEqual(JSON.parse(json));
|
||||
};
|
||||
|
||||
var verifyComponents = function (unit, locators) {
|
||||
@@ -44,7 +46,7 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon"
|
||||
</li> \
|
||||
</ol> \
|
||||
</div>';
|
||||
|
||||
|
||||
var unit;
|
||||
var clickDuplicate = function (index) {
|
||||
unit.$(".duplicate-button")[index].click();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
define(
|
||||
[
|
||||
"js/views/baseview", "underscore", "js/models/metadata", "js/views/abstract_editor",
|
||||
"js/views/transcripts/metadata_videolist"
|
||||
"js/views/video/transcripts/metadata_videolist",
|
||||
"js/views/video/translations_editor"
|
||||
],
|
||||
function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
|
||||
function(BaseView, _, MetadataModel, AbstractEditor, VideoList, VideoTranslations) {
|
||||
var Metadata = {};
|
||||
|
||||
Metadata.Editor = BaseView.extend({
|
||||
@@ -82,6 +83,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
|
||||
});
|
||||
|
||||
Metadata.VideoList = VideoList;
|
||||
Metadata.VideoTranslations = VideoTranslations;
|
||||
|
||||
Metadata.String = AbstractEditor.extend({
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ var UploadDialog = BaseView.extend({
|
||||
var oldInput = this.$("input[type=file]").get(0);
|
||||
this.$el.html(this.template({
|
||||
shown: this.options.shown,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
url: this.options.url || CMS.URL.UPLOAD_ASSET,
|
||||
title: this.model.escape('title'),
|
||||
message: this.model.escape('message'),
|
||||
selectedFile: selectedFile,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils",
|
||||
"js/views/video/transcripts/utils",
|
||||
"js/views/metadata", "js/collections/metadata",
|
||||
"js/views/transcripts/metadata_videolist"
|
||||
"js/views/video/transcripts/metadata_videolist"
|
||||
],
|
||||
function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils"
|
||||
"js/views/video/transcripts/utils"
|
||||
],
|
||||
function($, Backbone, _, Utils) {
|
||||
var FileUploader = Backbone.View.extend({
|
||||
@@ -1,7 +1,7 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/file_uploader",
|
||||
"js/views/video/transcripts/utils", "js/views/video/transcripts/file_uploader",
|
||||
"gettext"
|
||||
],
|
||||
function($, Backbone, _, Utils, FileUploader, gettext) {
|
||||
@@ -1,11 +1,11 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "backbone", "underscore", "js/views/abstract_editor",
|
||||
"js/views/transcripts/utils", "js/views/transcripts/message_manager",
|
||||
"js/views/video/transcripts/utils", "js/views/video/transcripts/message_manager",
|
||||
"js/views/metadata"
|
||||
],
|
||||
function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
|
||||
VideoList = AbstractEditor.extend({
|
||||
var VideoList = AbstractEditor.extend({
|
||||
// Time that we wait since the last time user typed.
|
||||
inputDelay: 300,
|
||||
|
||||
186
cms/static/js/views/video/translations_editor.js
Normal file
186
cms/static/js/views/video/translations_editor.js
Normal file
@@ -0,0 +1,186 @@
|
||||
define(
|
||||
[
|
||||
"jquery", "underscore",
|
||||
"js/views/abstract_editor", "js/models/uploads", "js/views/uploads"
|
||||
|
||||
],
|
||||
function($, _, AbstractEditor, FileUpload, UploadDialog) {
|
||||
"use strict";
|
||||
|
||||
var VideoUploadDialog = UploadDialog.extend({
|
||||
error: function() {
|
||||
this.model.set({
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"title": gettext("Sorry, there was an error parsing the subtitles that you uploaded. Please check the format and try again.")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var Translations = AbstractEditor.extend({
|
||||
events : {
|
||||
"click .setting-clear" : "clear",
|
||||
"click .create-setting" : "addEntry",
|
||||
"click .remove-setting" : "removeEntry",
|
||||
"click .upload-setting" : "upload",
|
||||
"change select" : "onChangeHandler"
|
||||
},
|
||||
|
||||
templateName: "metadata-translations-entry",
|
||||
templateItemName: "metadata-translations-item",
|
||||
|
||||
initialize: function () {
|
||||
var templateName = _.result(this, 'templateItemName'),
|
||||
tpl = document.getElementById(templateName).text;
|
||||
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template for item: " + templateName);
|
||||
}
|
||||
|
||||
this.templateItem = _.template(tpl);
|
||||
AbstractEditor.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
getDropdown: function () {
|
||||
var dropdown,
|
||||
disableOptions = function (element, values) {
|
||||
var dropdown = $(element).clone();
|
||||
|
||||
_.each(values, function(value, key) {
|
||||
var option = dropdown[0].options.namedItem(key);
|
||||
|
||||
if (option) {
|
||||
option.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
return dropdown;
|
||||
};
|
||||
|
||||
return function (values) {
|
||||
if (dropdown) {
|
||||
return disableOptions(dropdown, values);
|
||||
}
|
||||
|
||||
dropdown = document.createElement('select');
|
||||
dropdown.options.add(new Option());
|
||||
_.each(this.model.get('languages'), function(lang, index) {
|
||||
var option = new Option();
|
||||
|
||||
option.setAttribute('name', lang.code);
|
||||
option.value = lang.code;
|
||||
option.text = lang.label;
|
||||
dropdown.options.add(option);
|
||||
});
|
||||
|
||||
return disableOptions(dropdown, values);
|
||||
};
|
||||
}(),
|
||||
|
||||
getValueFromEditor: function () {
|
||||
var dict = {},
|
||||
items = this.$el.find('ol').find('.list-settings-item');
|
||||
|
||||
_.each(items, function(element, index) {
|
||||
var key = $(element).find('select').val(),
|
||||
value = $(element).find('.input').val();
|
||||
|
||||
// Keys should be unique, so if our keys are duplicated and
|
||||
// second key is empty or key and value are empty just do
|
||||
// nothing. Otherwise, it'll be overwritten by the new value.
|
||||
if (value === '') {
|
||||
if (key === '' || key in dict) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
dict[key] = value;
|
||||
});
|
||||
|
||||
return dict;
|
||||
},
|
||||
|
||||
// @TODO: Use backbone render patterns.
|
||||
setValueInEditor: function (values) {
|
||||
var self = this,
|
||||
frag = document.createDocumentFragment(),
|
||||
dropdown = self.getDropdown(values);
|
||||
|
||||
_.each(values, function(value, key) {
|
||||
var html = $(self.templateItem({
|
||||
'lang': key,
|
||||
'value': value,
|
||||
'url': self.model.get('urlRoot') + '/' + key
|
||||
})).prepend(dropdown.clone().val(key))[0];
|
||||
|
||||
frag.appendChild(html);
|
||||
});
|
||||
|
||||
this.$el.find('ol').html([frag]);
|
||||
},
|
||||
|
||||
addEntry: function(event) {
|
||||
event.preventDefault();
|
||||
// We don't call updateModel here since it's bound to the
|
||||
// change event
|
||||
var dict = $.extend(true, {}, this.model.get('value'));
|
||||
dict[''] = '';
|
||||
this.setValueInEditor(dict);
|
||||
this.$el.find('.create-setting').addClass('is-disabled');
|
||||
},
|
||||
|
||||
removeEntry: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var entry = $(event.currentTarget).data('lang');
|
||||
this.setValueInEditor(_.omit(this.model.get('value'), entry));
|
||||
this.updateModel();
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
},
|
||||
|
||||
upload: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
target = $(event.currentTarget),
|
||||
lang = target.data('lang'),
|
||||
model = new FileUpload({
|
||||
title: gettext('Upload translation.'),
|
||||
fileFormats: ['srt']
|
||||
}),
|
||||
view = new VideoUploadDialog({
|
||||
model: model,
|
||||
url: self.model.get('urlRoot') + '/' + lang,
|
||||
onSuccess: function (response) {
|
||||
if (!response['filename']) { return; }
|
||||
|
||||
var dict = $.extend(true, {}, self.model.get('value'));
|
||||
|
||||
dict[lang] = response['filename'];
|
||||
self.model.setValue(dict);
|
||||
}
|
||||
});
|
||||
|
||||
$('.wrapper-view').after(view.show().el);
|
||||
},
|
||||
|
||||
enableAdd: function() {
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
AbstractEditor.prototype.clear.apply(this, arguments);
|
||||
if (_.isNull(this.model.getValue())) {
|
||||
this.$el.find('.create-setting').removeClass('is-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
onChangeHandler: function (event) {
|
||||
this.showClearButton();
|
||||
this.enableAdd();
|
||||
this.updateModel();
|
||||
}
|
||||
});
|
||||
|
||||
return Translations;
|
||||
});
|
||||
@@ -2,22 +2,21 @@
|
||||
// ====================
|
||||
|
||||
// general - display mode (xblock-student_view or xmodule_display)
|
||||
.xmodule_display, .xblock-student_view {
|
||||
|
||||
.xmodule_display,
|
||||
.xblock-student_view {
|
||||
// font styling
|
||||
i, em {
|
||||
i,
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// Video Alpha
|
||||
// Video
|
||||
.xmodule_VideoModule {
|
||||
|
||||
// display mode
|
||||
&.xblock-student_view {
|
||||
|
||||
// full screen
|
||||
.video-controls .add-fullscreen {
|
||||
display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
|
||||
@@ -35,190 +34,300 @@
|
||||
}
|
||||
|
||||
.xmodule_VideoDescriptor {
|
||||
.wrapper-comp-settings.basic_metadata_edit{
|
||||
.list-input.settings-list {
|
||||
.field.comp-setting-entry {
|
||||
|
||||
.setting-label {
|
||||
vertical-align: top;
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
|
||||
.setting-help{
|
||||
display: block;
|
||||
width: 45%;
|
||||
max-width: auto;
|
||||
margin-left: 25%;
|
||||
padding: 0 13px;
|
||||
}
|
||||
|
||||
.collapse-setting {
|
||||
@extend %t-action3;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
font-weight: 600;
|
||||
|
||||
*[class^="icon-"] {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
.videolist-url-tip.setting-help,
|
||||
.videolist-extra-videos-tip.setting-help{
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.videolist-url-tip.setting-help{
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.wrapper-comp-setting{
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: auto;
|
||||
}
|
||||
|
||||
// inputs and labels
|
||||
.wrapper-videolist-settings {
|
||||
width: 45%;
|
||||
display: inline-block;
|
||||
min-width: ($baseline*5);
|
||||
|
||||
|
||||
// inputs
|
||||
.input {
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.is-disabled,
|
||||
&[disabled="disabled"]{
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-videolist-url{
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.wrapper-videolist-urls{
|
||||
background: $lightGrey;
|
||||
padding: ($baseline/3);
|
||||
|
||||
// enumerated fields
|
||||
.videolist-extra-videos {
|
||||
display: none;
|
||||
|
||||
&.is-visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.videolist-settings-item {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.transcripts-status{
|
||||
margin-top: $baseline;
|
||||
|
||||
&.is-invisible{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wrapper-transcripts-message{
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
min-width: ($baseline*5);
|
||||
margin-top: 10px;
|
||||
|
||||
.transcripts-message{
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.transcripts-message-status{
|
||||
color: $green;
|
||||
font-weight: 700;
|
||||
|
||||
&.status-error{
|
||||
color: $red;
|
||||
}
|
||||
|
||||
[class^="icon-"],
|
||||
[class*=" icon-"]{
|
||||
margin-right: 5px;
|
||||
@include font-size(18);
|
||||
}
|
||||
}
|
||||
|
||||
.transcripts-error-message{
|
||||
background: $red;
|
||||
color: $white;
|
||||
@include font-size(14);
|
||||
padding: ($baseline/3);
|
||||
|
||||
&.is-invisible{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-transcripts-buttons{
|
||||
|
||||
&.is-invisible{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action{
|
||||
@extend %btn-primary-blue;
|
||||
@extend %t-action3;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
.wrapper-comp-settings.basic_metadata_edit {
|
||||
.list-input.settings-list {
|
||||
.field.comp-setting-entry {
|
||||
.setting-label {
|
||||
vertical-align: top;
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
|
||||
// TYPE: enumerated video lists of metadata sets
|
||||
.metadata-videolist-enum {
|
||||
* {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
.setting-help {
|
||||
display: block;
|
||||
width: 45%;
|
||||
max-width: auto;
|
||||
margin-left: 25%;
|
||||
padding: 0 13px;
|
||||
}
|
||||
|
||||
.file-chooser{
|
||||
display: none;
|
||||
.collapse-setting {
|
||||
@extend %t-action3;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
font-weight: 600;
|
||||
|
||||
*[class^="icon-"] {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar{
|
||||
display: block;
|
||||
height: 30px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid $blue;
|
||||
text-align: center;
|
||||
font-size: 1.14em;
|
||||
.videolist-url-tip.setting-help,
|
||||
.videolist-extra-videos-tip.setting-help {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
&.is-invisible {
|
||||
display: none;
|
||||
.videolist-url-tip.setting-help {
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.wrapper-comp-setting {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: auto;
|
||||
}
|
||||
|
||||
// inputs and labels
|
||||
.wrapper-videolist-settings {
|
||||
width: 45%;
|
||||
display: inline-block;
|
||||
min-width: ($baseline*5);
|
||||
|
||||
// inputs
|
||||
.input {
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.is-disabled,
|
||||
[disabled="disabled"] {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
&.loaded {
|
||||
border-color: #66b93d;
|
||||
.wrapper-videolist-url {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
background: #66b93d;
|
||||
}
|
||||
}
|
||||
.wrapper-videolist-urls {
|
||||
background: $lightGrey;
|
||||
padding: ($baseline/3);
|
||||
|
||||
.progress-fill {
|
||||
// enumerated fields
|
||||
.videolist-extra-videos {
|
||||
display: none;
|
||||
|
||||
&.is-visible {
|
||||
display: block;
|
||||
width: 0%;
|
||||
height: 30px;
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.videolist-settings-item {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transcripts-status {
|
||||
margin-top: $baseline;
|
||||
|
||||
&.is-invisible {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wrapper-transcripts-message {
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
min-width: ($baseline*5);
|
||||
margin-top: 10px;
|
||||
|
||||
.transcripts-message {
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.transcripts-message-status {
|
||||
color: $green;
|
||||
font-weight: 700;
|
||||
|
||||
&.status-error {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
[class^="icon-"],
|
||||
[class*=" icon-"] {
|
||||
margin-right: 5px;
|
||||
@include font-size(18);
|
||||
}
|
||||
}
|
||||
|
||||
.transcripts-error-message {
|
||||
background: $red;
|
||||
color: $white;
|
||||
@include font-size(14);
|
||||
padding: ($baseline/3);
|
||||
|
||||
&.is-invisible {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-transcripts-buttons {
|
||||
&.is-invisible {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
@extend %btn-primary-blue;
|
||||
@extend %t-action3;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// TYPE: enumerated video lists of metadata sets
|
||||
.metadata-videolist-enum {
|
||||
* {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
}
|
||||
|
||||
.file-chooser {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: block;
|
||||
height: 30px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid $blue;
|
||||
text-align: center;
|
||||
font-size: 1.14em;
|
||||
|
||||
&.is-invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.loaded {
|
||||
border-color: #66b93d;
|
||||
|
||||
.progress-fill {
|
||||
background: #66b93d;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
display: block;
|
||||
width: 0%;
|
||||
height: 30px;
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-comp-settings {
|
||||
// TYPE: VideoTranslations
|
||||
.list-input.settings-list {
|
||||
.metadata-video-translations {
|
||||
* {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
// label
|
||||
.setting-label {
|
||||
vertical-align: top;
|
||||
margin-top: ($baseline*.25);
|
||||
}
|
||||
|
||||
// inputs and labels
|
||||
.wrapper-translations-settings {
|
||||
width: 45%;
|
||||
display: inline-block;
|
||||
min-width: 240px;
|
||||
|
||||
// enumerated fields
|
||||
.list-settings {
|
||||
margin: 0;
|
||||
|
||||
.list-settings-item {
|
||||
margin-bottom: ($baseline/2);
|
||||
|
||||
select {
|
||||
width: 80%;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
.list-settings-buttons {
|
||||
@extend %cont-truncated;
|
||||
padding: ($baseline/2) 0;
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
}
|
||||
}
|
||||
|
||||
// inputs
|
||||
.input {
|
||||
width: 43%;
|
||||
margin-right: ($baseline/4);
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
&.input-value {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-clear.action {
|
||||
vertical-align: top;
|
||||
margin: ($baseline*.25) ($baseline*.5) 0;
|
||||
}
|
||||
|
||||
.create-setting {
|
||||
@extend %ui-btn-flat-outline;
|
||||
@extend %t-action3;
|
||||
display: block;
|
||||
padding: ($baseline/2);
|
||||
font-weight: 600;
|
||||
|
||||
*[class^="icon-"] {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-setting {
|
||||
@extend %ui-btn-flat-outline;
|
||||
@extend %t-action3;
|
||||
display: inline-block;
|
||||
padding: ($baseline/2);
|
||||
font-weight: 600;
|
||||
width: 49%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.download-setting {
|
||||
@extend %ui-btn-non;
|
||||
@extend %t-action4;
|
||||
display: inline-block;
|
||||
padding: ($baseline/2);
|
||||
font-weight: 600;
|
||||
width: 49%;
|
||||
text-align: center;
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-setting {
|
||||
@include transition(color .25s ease-in-out);
|
||||
@include font-size(20);
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
color: $blue-l3;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,11 +824,6 @@ body.course.unit,.view-unit {
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
.create-action, .remove-action, .setting-clear {
|
||||
|
||||
}
|
||||
|
||||
.setting-clear {
|
||||
vertical-align: top;
|
||||
margin-top: ($baseline/4);
|
||||
@@ -845,11 +840,6 @@ body.course.unit,.view-unit {
|
||||
*[class^="icon-"] {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
// STATE: disabled
|
||||
&.is-disabled {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.remove-setting {
|
||||
@@ -862,11 +852,6 @@ body.course.unit,.view-unit {
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
// STATE: disabled
|
||||
&.is-disabled {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="wrapper-comp-setting metadata-video-translations">
|
||||
<label class="label setting-label"><%= model.get('display_name')%></label>
|
||||
<div class="wrapper-translations-settings">
|
||||
<ol class="list-settings"></ol>
|
||||
<a href="#" class="create-action create-setting">
|
||||
<i class="icon-plus"></i><%= gettext("Add") %> <span class="sr"><%= model.get('display_name')%></span>
|
||||
</a>
|
||||
</div>
|
||||
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i>
|
||||
<span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
12
cms/templates/js/video/metadata-translations-item.underscore
Normal file
12
cms/templates/js/video/metadata-translations-item.underscore
Normal file
@@ -0,0 +1,12 @@
|
||||
<li class="list-settings-item"
|
||||
><a href="#" class="remove-action remove-setting" data-lang="<%= lang %>" data-value="<%= value %>"><i class="icon-remove-sign"></i><span class="sr"><%= gettext("Remove") %></span></a>
|
||||
<input type="hidden" class="input" value="<%= value %>">
|
||||
<div class="list-settings-buttons"><% if (lang) {
|
||||
%><a href="#" class="upload-action upload-setting" data-lang="<%= lang %>" data-value="<%= value %>"><%= value ? gettext("Replace") : gettext("Upload") %>
|
||||
</a><%
|
||||
} %><% if (value) {
|
||||
%><a href="<%= url %>?filename=<%= value %>" class="download-action download-setting"><%= gettext("Download") %>
|
||||
</a><%
|
||||
}
|
||||
%><div>
|
||||
</li>
|
||||
@@ -3,7 +3,7 @@
|
||||
</div>
|
||||
<form class="file-chooser" action="/transcripts/upload"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<input type="file" class="file-input" name="file"
|
||||
<input type="file" class="file-input" name="transcript-file"
|
||||
accept="<%= _.map(ext, function(val){ return '.' + val; }).join(', ') %>">
|
||||
<input type="hidden" name="locator" value="<%= component_locator %>">
|
||||
<input type="hidden" name="video_list" value='<%= JSON.stringify(video_list) %>'>
|
||||
@@ -7,7 +7,7 @@ from xmodule.modulestore.django import loc_mapper
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
<%block name="title">${_("Individual Unit")}</%block>
|
||||
<%block name="bodyclass">is-signedin course unit view-unit</%block>
|
||||
<%block name="bodyclass">is-signedin course unit view-unit feature-upload</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type='text/javascript'>
|
||||
@@ -34,10 +34,13 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/template" id="image-modal-tpl">
|
||||
<%static:include path="js/imageModal.underscore" />
|
||||
</script>
|
||||
<script type="text/template" id="upload-dialog-tpl">
|
||||
<%static:include path="js/upload-dialog.underscore" />
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
<%block name="content">
|
||||
@@ -214,7 +217,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</%block>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<%static:include path="js/metadata-editor.underscore" />
|
||||
</script>
|
||||
|
||||
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]:
|
||||
<script id="${template_name}" type="text/template">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
|
||||
@@ -10,13 +10,18 @@ import json
|
||||
|
||||
% for template_name in ["metadata-videolist-entry", "file-upload"]:
|
||||
<script type="text/template" id="${template_name}">
|
||||
<%static:include path="js/transcripts/${template_name}.underscore" />
|
||||
<%static:include path="js/video/transcripts/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
|
||||
% for template_name in ["transcripts-found", "transcripts-uploaded", "transcripts-use-existing", "transcripts-not-found", "transcripts-replace", "transcripts-import", "transcripts-choose"]:
|
||||
<script type="text/template" id="${template_name}">
|
||||
<%static:include path="js/transcripts/messages/${template_name}.underscore" />
|
||||
<%static:include path="js/video/transcripts/messages/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
% for template_name in ["metadata-translations-entry", "metadata-translations-item"]:
|
||||
<script id="${template_name}" type="text/template">
|
||||
<%static:include path="js/video/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
|
||||
@@ -27,7 +32,7 @@ import json
|
||||
[
|
||||
"domReady!",
|
||||
"jquery",
|
||||
"js/views/transcripts/editor"
|
||||
"js/views/video/transcripts/editor"
|
||||
],
|
||||
|
||||
function(doc, $, Editor) {
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (settings.url == '/transcript/translation') {
|
||||
} else if (settings.url.match(/transcript\/translation\/.+$/)) {
|
||||
return settings.success(jasmine.stubbedCaption);
|
||||
} else if (settings.url == '/transcript/available_translations') {
|
||||
return settings.success(['uk', 'de']);
|
||||
|
||||
@@ -60,16 +60,14 @@
|
||||
|
||||
runs(function () {
|
||||
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
|
||||
url: '/transcript/translation',
|
||||
url: '/transcript/translation/en',
|
||||
notifyOnError: false,
|
||||
data: jasmine.any(Object),
|
||||
data: void(0),
|
||||
success: jasmine.any(Function),
|
||||
error: jasmine.any(Function)
|
||||
});
|
||||
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
|
||||
.toEqual({
|
||||
language: 'en'
|
||||
});
|
||||
.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +84,7 @@
|
||||
|
||||
runs(function () {
|
||||
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
|
||||
url: '/transcript/translation',
|
||||
url: '/transcript/translation/en',
|
||||
notifyOnError: false,
|
||||
data: jasmine.any(Object),
|
||||
success: jasmine.any(Function),
|
||||
@@ -94,7 +92,6 @@
|
||||
});
|
||||
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
|
||||
.toEqual({
|
||||
language: 'en',
|
||||
videoId: 'abcdefghijkl'
|
||||
});
|
||||
});
|
||||
@@ -111,7 +108,7 @@
|
||||
|
||||
runs(function () {
|
||||
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
|
||||
url: '/transcript/translation',
|
||||
url: '/transcript/translation/en',
|
||||
notifyOnError: false,
|
||||
data: jasmine.any(Object),
|
||||
success: jasmine.any(Function),
|
||||
@@ -119,7 +116,6 @@
|
||||
});
|
||||
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
|
||||
.toEqual({
|
||||
language: 'en',
|
||||
videoId: 'cogebirgzzM'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -226,9 +226,8 @@ function () {
|
||||
function fetchCaption() {
|
||||
var self = this,
|
||||
Caption = self.videoCaption,
|
||||
data = {
|
||||
language: this.getCurrentLanguage()
|
||||
};
|
||||
language = this.getCurrentLanguage(),
|
||||
data;
|
||||
|
||||
if (Caption.loaded) {
|
||||
Caption.hideCaptions(false);
|
||||
@@ -241,13 +240,15 @@ function () {
|
||||
}
|
||||
|
||||
if (this.videoType === 'youtube') {
|
||||
data.videoId = this.youtubeId();
|
||||
data = {
|
||||
videoId: this.youtubeId()
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "CC" button
|
||||
Caption.fetchXHR = $.ajaxWithPrefix({
|
||||
url: self.config.transcriptTranslationUrl,
|
||||
url: self.config.transcriptTranslationUrl + '/' + language,
|
||||
notifyOnError: false,
|
||||
data: data,
|
||||
success: function (captions) {
|
||||
|
||||
@@ -132,22 +132,6 @@ class VideoDescriptorTest(unittest.TestCase):
|
||||
field_data=DictFieldData({}),
|
||||
)
|
||||
|
||||
def test_get_context(self):
|
||||
""""test get_context"""
|
||||
correct_tabs = [
|
||||
{
|
||||
'name': "Basic",
|
||||
'template': "video/transcripts.html",
|
||||
'current': True
|
||||
},
|
||||
{
|
||||
'name': 'Advanced',
|
||||
'template': 'tabs/metadata-edit-tab.html'
|
||||
}
|
||||
]
|
||||
rendered_context = self.descriptor.get_context()
|
||||
self.assertListEqual(rendered_context['tabs'], correct_tabs)
|
||||
|
||||
def test_create_youtube_string(self):
|
||||
"""
|
||||
Test that Youtube ID strings are correctly created when writing
|
||||
|
||||
@@ -64,6 +64,17 @@ def generate_subs(speed, source_speed, source_subs):
|
||||
return subs
|
||||
|
||||
|
||||
def save_to_store(content, name, mime_type, location):
|
||||
"""
|
||||
Save named content to store by location.
|
||||
|
||||
Returns location of saved content.
|
||||
"""
|
||||
content_location = Transcript.asset_location(location, name)
|
||||
content = StaticContent(content_location, name, mime_type, content)
|
||||
contentstore().save(content)
|
||||
return content_location
|
||||
|
||||
def save_subs_to_store(subs, subs_id, item, language='en'):
|
||||
"""
|
||||
Save transcripts into `StaticContent`.
|
||||
@@ -76,13 +87,8 @@ def save_subs_to_store(subs, subs_id, item, language='en'):
|
||||
Returns: location of saved subtitles.
|
||||
"""
|
||||
filedata = json.dumps(subs, indent=2)
|
||||
mime_type = 'application/json'
|
||||
filename = subs_filename(subs_id, language)
|
||||
content_location = Transcript.asset_location(item.location, filename)
|
||||
content = StaticContent(content_location, filename, mime_type, filedata)
|
||||
contentstore().save(content)
|
||||
return content_location
|
||||
|
||||
return save_to_store(filedata, filename, 'application/json', item.location)
|
||||
|
||||
def get_transcripts_from_youtube(youtube_id, settings, i18n):
|
||||
"""
|
||||
@@ -193,12 +199,8 @@ def remove_subs_from_store(subs_id, item, lang='en'):
|
||||
"""
|
||||
Remove from store, if transcripts content exists.
|
||||
"""
|
||||
try:
|
||||
content = Transcript.asset(item.location, subs_id, lang)
|
||||
contentstore().delete(content.get_id())
|
||||
log.info("Removed subs %s from store", subs_id)
|
||||
except NotFoundError:
|
||||
pass
|
||||
filename = subs_filename(subs_id, lang)
|
||||
Transcript.delete_asset(item.location, filename)
|
||||
|
||||
|
||||
def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item, language='en'):
|
||||
@@ -339,6 +341,8 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
|
||||
a new version of the SRT file with same name).
|
||||
"""
|
||||
|
||||
_ = item.runtime.service(item, "i18n").ugettext
|
||||
|
||||
# 1.
|
||||
html5_ids = get_html5_ids(item.html5_sources)
|
||||
possible_video_id_list = [item.youtube_id_1_0] + html5_ids
|
||||
@@ -408,10 +412,9 @@ def subs_filename(subs_id, lang='en'):
|
||||
Generate proper filename for storage.
|
||||
"""
|
||||
if lang == 'en':
|
||||
return 'subs_{0}.srt.sjson'.format(subs_id)
|
||||
return u'subs_{0}.srt.sjson'.format(subs_id)
|
||||
else:
|
||||
return '{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
|
||||
|
||||
return u'{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
|
||||
|
||||
|
||||
def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
@@ -420,10 +423,15 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
|
||||
`item` is module object.
|
||||
"""
|
||||
_ = item.runtime.service(item, "i18n").ugettext
|
||||
|
||||
try:
|
||||
srt_transcript = contentstore().find(Transcript.asset_location(item.location, user_filename))
|
||||
srt_transcripts = contentstore().find(Transcript.asset_location(item.location, user_filename))
|
||||
except NotFoundError as ex:
|
||||
raise TranscriptException("{}: Can't find uploaded transcripts: {}".format(ex.message, user_filename))
|
||||
raise TranscriptException(_("{exception_message}: Can't find uploaded transcripts: {user_filename}").format(
|
||||
exception_message=ex.message,
|
||||
user_filename=user_filename
|
||||
))
|
||||
|
||||
if not lang:
|
||||
lang = item.transcript_language
|
||||
@@ -431,7 +439,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
generate_subs_from_source(
|
||||
result_subs_dict,
|
||||
os.path.splitext(user_filename)[1][1:],
|
||||
srt_transcript.data.decode('utf8'),
|
||||
srt_transcripts.data.decode('utf8'),
|
||||
item,
|
||||
lang
|
||||
)
|
||||
@@ -464,6 +472,11 @@ class Transcript(object):
|
||||
"""
|
||||
Container for transcript methods.
|
||||
"""
|
||||
mime_types = {
|
||||
'srt': 'application/x-subrip; charset=utf-8',
|
||||
'txt': 'text/plain; charset=utf-8',
|
||||
'sjson': 'application/json',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def convert(content, input_format, output_format):
|
||||
@@ -504,21 +517,32 @@ class Transcript(object):
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return contentstore().find(
|
||||
Transcript.asset_location(
|
||||
location,
|
||||
subs_filename(subs_id, lang) if not filename else filename
|
||||
)
|
||||
)
|
||||
asset_filename = subs_filename(subs_id, lang) if not filename else filename
|
||||
return Transcript.get_asset(location, asset_filename)
|
||||
|
||||
@staticmethod
|
||||
def get_asset(location, filename):
|
||||
"""
|
||||
Return asset by location and filename.
|
||||
"""
|
||||
return contentstore().find(Transcript.asset_location(location, filename))
|
||||
|
||||
@staticmethod
|
||||
def asset_location(location, filename):
|
||||
"""
|
||||
Return asset location.
|
||||
|
||||
`location` is module location.
|
||||
Return asset location. `location` is module location.
|
||||
"""
|
||||
return StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
return StaticContent.compute_location(location.org, location.course, filename)
|
||||
|
||||
@staticmethod
|
||||
def delete_asset(location, filename):
|
||||
"""
|
||||
Delete asset by location and filename.
|
||||
"""
|
||||
try:
|
||||
content = Transcript.get_asset(location, filename)
|
||||
contentstore().delete(content.get_id())
|
||||
log.info("Transcript asset %s was removed from store.", filename)
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
333
common/lib/xmodule/xmodule/video_module/video_handlers.py
Normal file
333
common/lib/xmodule/xmodule/video_module/video_handlers.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Handlers for video module.
|
||||
|
||||
StudentViewHandlers are handlers for video module instance.
|
||||
StudioViewHandlers are handlers for video descriptor instance.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from webob import Response
|
||||
|
||||
from xblock.core import XBlock
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.fields import RelativeTime
|
||||
|
||||
from .transcripts_utils import (
|
||||
get_or_create_sjson,
|
||||
TranscriptException,
|
||||
TranscriptsGenerationException,
|
||||
generate_sjson_for_all_speeds,
|
||||
youtube_speed_dict,
|
||||
Transcript,
|
||||
save_to_store,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Disable no-member warning:
|
||||
# pylint: disable=E1101
|
||||
|
||||
|
||||
class VideoStudentViewHandlers(object):
|
||||
"""
|
||||
Handlers for video module instance.
|
||||
"""
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
"""
|
||||
Update values of xfields, that were changed by student.
|
||||
"""
|
||||
accepted_keys = [
|
||||
'speed', 'saved_video_position', 'transcript_language',
|
||||
'transcript_download_format', 'youtube_is_available'
|
||||
]
|
||||
|
||||
conversions = {
|
||||
'speed': json.loads,
|
||||
'saved_video_position': RelativeTime.isotime_to_timedelta,
|
||||
'youtube_is_available': json.loads,
|
||||
}
|
||||
|
||||
if dispatch == 'save_user_state':
|
||||
for key in data:
|
||||
if hasattr(self, key) and key in accepted_keys:
|
||||
if key in conversions:
|
||||
value = conversions[key](data[key])
|
||||
else:
|
||||
value = data[key]
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
if key == 'speed':
|
||||
self.global_speed = self.speed
|
||||
|
||||
return json.dumps({'success': True})
|
||||
|
||||
log.debug(u"GET {0}".format(data))
|
||||
log.debug(u"DISPATCH {0}".format(dispatch))
|
||||
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def translation(self, youtube_id):
|
||||
"""
|
||||
This is called to get transcript file for specific language.
|
||||
|
||||
youtube_id: str: must be one of youtube_ids or None if HTML video
|
||||
|
||||
Logic flow:
|
||||
|
||||
If youtube_id doesn't exist, we have a video in HTML5 mode. Otherwise,
|
||||
video video in Youtube or Flash modes.
|
||||
|
||||
if youtube:
|
||||
If english -> give back youtube_id subtitles:
|
||||
Return what we have in contentstore for given youtube_id.
|
||||
If non-english:
|
||||
a) extract youtube_id from srt file name.
|
||||
b) try to find sjson by youtube_id and return if successful.
|
||||
c) generate sjson from srt for all youtube speeds.
|
||||
if non-youtube:
|
||||
If english -> give back `sub` subtitles:
|
||||
Return what we have in contentstore for given subs_if that is stored in self.sub.
|
||||
If non-english:
|
||||
a) try to find previously generated sjson.
|
||||
b) otherwise generate sjson from srt and return it.
|
||||
|
||||
Filenames naming:
|
||||
en: subs_videoid.srt.sjson
|
||||
non_en: uk_subs_videoid.srt.sjson
|
||||
|
||||
Raises:
|
||||
NotFoundError if for 'en' subtitles no asset is uploaded.
|
||||
"""
|
||||
if youtube_id:
|
||||
# Youtube case:
|
||||
if self.transcript_language == 'en':
|
||||
return Transcript.asset(self.location, youtube_id).data
|
||||
|
||||
youtube_ids = youtube_speed_dict(self)
|
||||
assert youtube_id in youtube_ids
|
||||
|
||||
try:
|
||||
sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data
|
||||
except (NotFoundError):
|
||||
log.info("Can't find content in storage for %s transcript: generating.", youtube_id)
|
||||
generate_sjson_for_all_speeds(
|
||||
self,
|
||||
self.transcripts[self.transcript_language],
|
||||
{speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()},
|
||||
self.transcript_language
|
||||
)
|
||||
sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data
|
||||
|
||||
return sjson_transcript
|
||||
else:
|
||||
# HTML5 case
|
||||
if self.transcript_language == 'en':
|
||||
return Transcript.asset(self.location, self.sub).data
|
||||
else:
|
||||
return get_or_create_sjson(self)
|
||||
|
||||
def get_transcript(self, transcript_format='srt'):
|
||||
"""
|
||||
Returns transcript, filename and MIME type.
|
||||
|
||||
Raises:
|
||||
- NotFoundError if cannot find transcript file in storage.
|
||||
- ValueError if transcript file is empty or incorrect JSON.
|
||||
- KeyError if transcript file has incorrect format.
|
||||
|
||||
If language is 'en', self.sub should be correct subtitles name.
|
||||
If language is 'en', but if self.sub is not defined, this means that we
|
||||
should search for video name in order to get proper transcript (old style courses).
|
||||
If language is not 'en', give back transcript in proper language and format.
|
||||
"""
|
||||
lang = self.transcript_language
|
||||
|
||||
if lang == 'en':
|
||||
if self.sub: # HTML5 case and (Youtube case for new style videos)
|
||||
transcript_name = self.sub
|
||||
elif self.youtube_id_1_0: # old courses
|
||||
transcript_name = self.youtube_id_1_0
|
||||
else:
|
||||
log.debug("No subtitles for 'en' language")
|
||||
raise ValueError
|
||||
|
||||
data = Transcript.asset(self.location, transcript_name, lang).data
|
||||
filename = u'{}.{}'.format(transcript_name, transcript_format)
|
||||
content = Transcript.convert(data, 'sjson', transcript_format)
|
||||
else:
|
||||
data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data
|
||||
filename = u'{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format)
|
||||
content = Transcript.convert(data, 'srt', transcript_format)
|
||||
|
||||
if not content:
|
||||
log.debug('no subtitles produced in get_transcript')
|
||||
raise ValueError
|
||||
|
||||
return content, filename, Transcript.mime_types[transcript_format]
|
||||
|
||||
@XBlock.handler
|
||||
def transcript(self, request, dispatch):
|
||||
"""
|
||||
Entry point for transcript handlers for student_view.
|
||||
|
||||
Request GET may contain `videoId` for `translation` dispatch.
|
||||
|
||||
Dispatches, (HTTP GET):
|
||||
/translation/[language_id]
|
||||
/download
|
||||
/available_translations/
|
||||
|
||||
Explanations:
|
||||
`download`: returns SRT or TXT file.
|
||||
`translation`: depends on HTTP methods:
|
||||
Provide translation for requested language, SJSON format is sent back on success,
|
||||
Proper language_id should be in url.
|
||||
`available_translations`:
|
||||
Returns list of languages, for which transcript files exist.
|
||||
For 'en' check if SJSON exists. For non-`en` check if SRT file exists.
|
||||
"""
|
||||
if dispatch.startswith('translation'):
|
||||
|
||||
language = dispatch.replace('translation', '').strip('/')
|
||||
|
||||
if not language:
|
||||
log.info("Invalid /translation request: no language.")
|
||||
return Response(status=400)
|
||||
|
||||
if language not in ['en'] + self.transcripts.keys():
|
||||
log.info("Video: transcript facilities are not available for given language.")
|
||||
return Response(status=404)
|
||||
|
||||
if language != self.transcript_language:
|
||||
self.transcript_language = language
|
||||
try:
|
||||
transcript = self.translation(request.GET.get('videoId', None))
|
||||
except (
|
||||
TranscriptException,
|
||||
NotFoundError,
|
||||
UnicodeDecodeError,
|
||||
TranscriptException,
|
||||
TranscriptsGenerationException
|
||||
) as ex:
|
||||
log.info(ex.message)
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(transcript, headerlist=[('Content-Language', language)])
|
||||
response.content_type = Transcript.mime_types['sjson']
|
||||
|
||||
elif dispatch == 'download':
|
||||
try:
|
||||
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(self.transcript_download_format)
|
||||
except (NotFoundError, ValueError, KeyError, UnicodeDecodeError):
|
||||
log.debug("Video@download exception")
|
||||
return Response(status=404)
|
||||
else:
|
||||
response = Response(
|
||||
transcript_content,
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(transcript_filename.encode('utf8'))),
|
||||
('Content-Language', self.transcript_language),
|
||||
]
|
||||
)
|
||||
response.content_type = transcript_mime_type
|
||||
|
||||
elif dispatch == 'available_translations':
|
||||
available_translations = []
|
||||
if self.sub: # check if sjson exists for 'en'.
|
||||
try:
|
||||
Transcript.asset(self.location, self.sub, 'en')
|
||||
except NotFoundError:
|
||||
pass
|
||||
else:
|
||||
available_translations = ['en']
|
||||
for lang in self.transcripts:
|
||||
try:
|
||||
Transcript.asset(self.location, None, None, self.transcripts[lang])
|
||||
except NotFoundError:
|
||||
continue
|
||||
available_translations.append(lang)
|
||||
if available_translations:
|
||||
response = Response(json.dumps(available_translations))
|
||||
response.content_type = 'application/json'
|
||||
else:
|
||||
response = Response(status=404)
|
||||
else: # unknown dispatch
|
||||
log.debug("Dispatch is not allowed")
|
||||
response = Response(status=404)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class VideoStudioViewHandlers(object):
|
||||
"""
|
||||
Handlers for Studio view.
|
||||
"""
|
||||
@XBlock.handler
|
||||
def studio_transcript(self, request, dispatch):
|
||||
"""
|
||||
Entry point for Studio transcript handlers.
|
||||
|
||||
Dispatches:
|
||||
/translation/[language_id] - language_id sould be in url.
|
||||
|
||||
`translation` dispatch support following HTTP methods:
|
||||
`POST`:
|
||||
Upload srt file. Check possibility of generation of proper sjson files.
|
||||
For now, it works only for self.transcripts, not for `en`.
|
||||
Do not update self.transcripts, as fields are updated on save in Studio.
|
||||
`GET:
|
||||
Return filename from storage. SRT format is sent back on success. Filename should be in GET dict.
|
||||
|
||||
We raise all exceptions right in Studio:
|
||||
NotFoundError:
|
||||
Video or asset was deleted from module/contentstore, but request came later.
|
||||
Seems impossible to be raised. module_render.py catches NotFoundErrors from here.
|
||||
|
||||
/translation POST:
|
||||
TypeError:
|
||||
Unjsonable filename or content.
|
||||
TranscriptsGenerationException, TranscriptException:
|
||||
no SRT extension or not parse-able by PySRT
|
||||
UnicodeDecodeError: non-UTF8 uploaded file content encoding.
|
||||
"""
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
if dispatch.startswith('translation'):
|
||||
language = dispatch.replace('translation', '').strip('/')
|
||||
|
||||
if not language:
|
||||
log.info("Invalid /translation request: no language.")
|
||||
return Response(status=400)
|
||||
|
||||
if request.method == 'POST':
|
||||
subtitles = request.POST['file']
|
||||
save_to_store(subtitles.file.read(), unicode(subtitles.filename), 'application/x-subrip', self.location)
|
||||
generate_sjson_for_all_speeds(self, unicode(subtitles.filename), {}, language)
|
||||
response = {'filename': unicode(subtitles.filename), 'status': 'Success'}
|
||||
return Response(json.dumps(response), status=201)
|
||||
|
||||
elif request.method == 'GET':
|
||||
|
||||
filename = request.GET.get('filename')
|
||||
if not filename:
|
||||
log.info("Invalid /translation request: no filename in request.GET")
|
||||
return Response(status=400)
|
||||
|
||||
content = Transcript.get_asset(self.location, filename).data
|
||||
response = Response(content, headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(filename.encode('utf8'))),
|
||||
('Content-Language', language),
|
||||
])
|
||||
response.content_type = Transcript.mime_types['srt']
|
||||
|
||||
else: # unknown dispatch
|
||||
log.debug("Dispatch is not allowed")
|
||||
response = Response(status=404)
|
||||
|
||||
return response
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=W0223
|
||||
"""Video is ungraded Xmodule for support video content.
|
||||
It's new improved video module, which support additional feature:
|
||||
@@ -9,7 +10,6 @@ in-browser HTML5 video method (when in HTML5 mode).
|
||||
- Navigational subtitles can be disabled altogether via an attribute
|
||||
in XML.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
@@ -17,29 +17,24 @@ from operator import itemgetter
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
import datetime
|
||||
import copy
|
||||
from webob import Response
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import KvsFieldData
|
||||
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
|
||||
from xmodule.x_module import XModule, module_attr
|
||||
from xmodule.editing_module import TabsEditingDescriptor
|
||||
from xmodule.raw_module import EmptyDataRawDescriptor
|
||||
from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict, ScopeIds
|
||||
from xmodule.fields import RelativeTime
|
||||
from .transcripts_utils import (
|
||||
get_or_create_sjson,
|
||||
TranscriptException,
|
||||
generate_sjson_for_all_speeds,
|
||||
youtube_speed_dict,
|
||||
Transcript,
|
||||
)
|
||||
|
||||
from .video_utils import create_youtube_string
|
||||
from .video_xfields import VideoFields
|
||||
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
|
||||
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
|
||||
from xblock.runtime import KvsFieldData
|
||||
@@ -54,140 +49,7 @@ def get_ext(filename):
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VideoFields(object):
|
||||
"""Fields for `VideoModule` and `VideoDescriptor`."""
|
||||
display_name = String(
|
||||
display_name="Display Name", help="Display name for this module.",
|
||||
default="Video",
|
||||
scope=Scope.settings
|
||||
)
|
||||
saved_video_position = RelativeTime(
|
||||
help="Current position in the video",
|
||||
scope=Scope.user_state,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
# TODO: This should be moved to Scope.content, but this will
|
||||
# require data migration to support the old video module.
|
||||
youtube_id_1_0 = String(
|
||||
help="This is the Youtube ID reference for the normal speed video.",
|
||||
display_name="Youtube ID",
|
||||
scope=Scope.settings,
|
||||
default="OEoXaMPEzfM"
|
||||
)
|
||||
youtube_id_0_75 = String(
|
||||
help="Optional, for older browsers: the Youtube ID for the .75x speed video.",
|
||||
display_name="Youtube ID for .75x speed",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
youtube_id_1_25 = String(
|
||||
help="Optional, for older browsers: the Youtube ID for the 1.25x speed video.",
|
||||
display_name="Youtube ID for 1.25x speed",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
youtube_id_1_5 = String(
|
||||
help="Optional, for older browsers: the Youtube ID for the 1.5x speed video.",
|
||||
display_name="Youtube ID for 1.5x speed",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
start_time = RelativeTime( # datetime.timedelta object
|
||||
help="Start time for the video (HH:MM:SS). Max value is 23:59:59.",
|
||||
display_name="Start Time",
|
||||
scope=Scope.settings,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
end_time = RelativeTime( # datetime.timedelta object
|
||||
help="End time for the video (HH:MM:SS). Max value is 23:59:59.",
|
||||
display_name="End Time",
|
||||
scope=Scope.settings,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
#front-end code of video player checks logical validity of (start_time, end_time) pair.
|
||||
|
||||
# `source` is deprecated field and should not be used in future.
|
||||
# `download_video` is used instead.
|
||||
source = String(
|
||||
help="The external URL to download the video.",
|
||||
display_name="Download Video",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
download_video = Boolean(
|
||||
help="Show a link beneath the video to allow students to download the video. Note: You must add at least one video source below.",
|
||||
display_name="Video Download Allowed",
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
html5_sources = List(
|
||||
help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
|
||||
display_name="Video Sources",
|
||||
scope=Scope.settings,
|
||||
)
|
||||
track = String(
|
||||
help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
|
||||
display_name="Download Transcript",
|
||||
scope=Scope.settings,
|
||||
default=''
|
||||
)
|
||||
download_track = Boolean(
|
||||
help="Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.",
|
||||
display_name="Transcript Download Allowed",
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
sub = String(
|
||||
help="The name of the timed transcript track (for non-Youtube videos).",
|
||||
display_name="Transcript (primary)",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
show_captions = Boolean(
|
||||
help="This controls whether or not captions are shown by default.",
|
||||
display_name="Transcript Display",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
|
||||
transcripts = Dict(
|
||||
help="Add additional transcripts in other languages",
|
||||
display_name="Transcript Translations",
|
||||
scope=Scope.settings,
|
||||
default={}
|
||||
)
|
||||
transcript_language = String(
|
||||
help="Preferred language for transcript",
|
||||
display_name="Preferred language for transcript",
|
||||
scope=Scope.preferences,
|
||||
default="en"
|
||||
)
|
||||
transcript_download_format = String(
|
||||
help="Transcript file format to download by user.",
|
||||
scope=Scope.preferences,
|
||||
values=[
|
||||
{"display_name": "SubRip (.srt) file", "value": "srt"},
|
||||
{"display_name": "Text (.txt) file", "value": "txt"}
|
||||
],
|
||||
default='srt',
|
||||
)
|
||||
speed = Float(
|
||||
help="The last speed that was explicitly set by user for the video.",
|
||||
scope=Scope.user_state,
|
||||
)
|
||||
global_speed = Float(
|
||||
help="Default speed in cases when speed wasn't explicitly for specific video",
|
||||
scope=Scope.preferences,
|
||||
default=1.0
|
||||
)
|
||||
youtube_is_available = Boolean(
|
||||
help="The availaibility of YouTube API for the user",
|
||||
scope=Scope.user_info,
|
||||
default=True
|
||||
)
|
||||
|
||||
|
||||
class VideoModule(VideoFields, XModule):
|
||||
class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
|
||||
"""
|
||||
XML source example:
|
||||
|
||||
@@ -230,38 +92,6 @@ class VideoModule(VideoFields, XModule):
|
||||
]}
|
||||
js_module_name = "Video"
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
accepted_keys = [
|
||||
'speed', 'saved_video_position', 'transcript_language',
|
||||
'transcript_download_format', 'youtube_is_available'
|
||||
]
|
||||
|
||||
conversions = {
|
||||
'speed': json.loads,
|
||||
'saved_video_position': lambda v: RelativeTime.isotime_to_timedelta(v),
|
||||
'youtube_is_available': json.loads,
|
||||
}
|
||||
|
||||
if dispatch == 'save_user_state':
|
||||
for key in data:
|
||||
if hasattr(self, key) and key in accepted_keys:
|
||||
if key in conversions:
|
||||
value = conversions[key](data[key])
|
||||
else:
|
||||
value = data[key]
|
||||
|
||||
setattr(self, key, value)
|
||||
|
||||
if key == 'speed':
|
||||
self.global_speed = self.speed
|
||||
|
||||
return json.dumps({'success': True})
|
||||
|
||||
log.debug(u"GET {0}".format(data))
|
||||
log.debug(u"DISPATCH {0}".format(dispatch))
|
||||
|
||||
raise NotFoundError('Unexpected dispatch type')
|
||||
|
||||
def get_html(self):
|
||||
track_url = None
|
||||
transcript_download_format = self.transcript_download_format
|
||||
@@ -279,7 +109,7 @@ class VideoModule(VideoFields, XModule):
|
||||
track_url = self.track
|
||||
transcript_download_format = None
|
||||
elif self.sub or self.transcripts:
|
||||
track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download'
|
||||
track_url = self.runtime.handler_url(self, 'transcript', 'download').rstrip('/?')
|
||||
|
||||
if not self.transcripts:
|
||||
transcript_language = u'en'
|
||||
@@ -331,191 +161,15 @@ class VideoModule(VideoFields, XModule):
|
||||
'transcript_download_formats_list': self.descriptor.fields['transcript_download_format'].values,
|
||||
'transcript_language': transcript_language,
|
||||
'transcript_languages': json.dumps(sorted_languages),
|
||||
'transcript_translation_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/translation',
|
||||
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations',
|
||||
'transcript_translation_url': self.runtime.handler_url(self, 'transcript', 'translation').rstrip('/?'),
|
||||
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript', 'available_translations').rstrip('/?'),
|
||||
})
|
||||
|
||||
def get_transcript(self, transcript_format='srt'):
|
||||
"""
|
||||
Returns transcript, filename and MIME type.
|
||||
|
||||
Raises:
|
||||
- NotFoundError if cannot find transcript file in storage.
|
||||
- ValueError if transcript file is empty or incorrect JSON.
|
||||
- KeyError if transcript file has incorrect format.
|
||||
|
||||
If language is 'en', self.sub should be correct subtitles name.
|
||||
If language is 'en', but if self.sub is not defined, this means that we
|
||||
should search for video name in order to get proper transcript (old style courses).
|
||||
If language is not 'en', give back transcript in proper language and format.
|
||||
"""
|
||||
lang = self.transcript_language
|
||||
|
||||
if lang == 'en':
|
||||
if self.sub: # HTML5 case and (Youtube case for new style videos)
|
||||
transcript_name = self.sub
|
||||
elif self.youtube_id_1_0: # old courses
|
||||
transcript_name = self.youtube_id_1_0
|
||||
else:
|
||||
log.debug("No subtitles for 'en' language")
|
||||
raise ValueError
|
||||
|
||||
data = Transcript.asset(self.location, transcript_name, lang).data
|
||||
filename = '{}.{}'.format(transcript_name, transcript_format)
|
||||
content = Transcript.convert(data, 'sjson', transcript_format)
|
||||
else:
|
||||
data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data
|
||||
filename = '{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format)
|
||||
content = Transcript.convert(data, 'srt', transcript_format)
|
||||
|
||||
if not content:
|
||||
log.debug('no subtitles produced in get_transcript')
|
||||
raise ValueError
|
||||
|
||||
mime_type = 'text/plain' if transcript_format == 'txt' else 'application/x-subrip'
|
||||
|
||||
return content, filename, mime_type
|
||||
|
||||
|
||||
@XBlock.handler
|
||||
def transcript(self, request, dispatch):
|
||||
"""
|
||||
Entry point for transcript handlers.
|
||||
|
||||
Request GET should contains 2-char language code for `download`
|
||||
and additionally `videoId` for `translation`.
|
||||
|
||||
Dispatches:
|
||||
`download`: returns SRT file.
|
||||
`translation`: returns jsoned translation text.
|
||||
`available_translations`: returns list of languages, for which SRT files exist. For 'en' check if SJSON exists.
|
||||
"""
|
||||
if dispatch == 'translation':
|
||||
if 'language' not in request.GET:
|
||||
log.info("Invalid /transcript GET parameters.")
|
||||
return Response(status=400)
|
||||
|
||||
lang = request.GET.get('language')
|
||||
if lang not in ['en'] + self.transcripts.keys():
|
||||
log.info("Video: transcript facilities are not available for given language.")
|
||||
return Response(status=404)
|
||||
if lang != self.transcript_language:
|
||||
self.transcript_language = lang
|
||||
|
||||
try:
|
||||
transcript = self.translation(request.GET.get('videoId', None))
|
||||
except (TranscriptException, NotFoundError) as ex:
|
||||
log.info(ex.message)
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(transcript)
|
||||
response.content_type = 'application/json'
|
||||
|
||||
elif dispatch == 'download':
|
||||
try:
|
||||
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(self.transcript_download_format)
|
||||
except (NotFoundError, ValueError, KeyError):
|
||||
log.debug("Video@download exception")
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(
|
||||
transcript_content,
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(transcript_filename)),
|
||||
]
|
||||
)
|
||||
response.content_type = transcript_mime_type
|
||||
|
||||
elif dispatch == 'available_translations':
|
||||
available_translations = []
|
||||
if self.sub: # check if sjson exists for 'en'.
|
||||
try:
|
||||
Transcript.asset(self.location, self.sub, 'en')
|
||||
except NotFoundError:
|
||||
pass
|
||||
else:
|
||||
available_translations = ['en']
|
||||
for lang in self.transcripts:
|
||||
try:
|
||||
Transcript.asset(self.location, None, None, self.transcripts[lang])
|
||||
except NotFoundError:
|
||||
continue
|
||||
available_translations.append(lang)
|
||||
if available_translations:
|
||||
response = Response(json.dumps(available_translations))
|
||||
response.content_type = 'application/json'
|
||||
else:
|
||||
response = Response(status=404)
|
||||
else: # unknown dispatch
|
||||
log.debug("Dispatch is not allowed")
|
||||
response = Response(status=404)
|
||||
|
||||
return response
|
||||
|
||||
def translation(self, youtube_id):
|
||||
"""
|
||||
This is called to get transcript file for specific language.
|
||||
|
||||
youtube_id: str: must be one of youtube_ids or None if HTML video
|
||||
|
||||
Logic flow:
|
||||
|
||||
If youtube_id doesn't exist, we have a video in HTML5 mode. Otherwise,
|
||||
video video in Youtube or Flash modes.
|
||||
|
||||
if youtube:
|
||||
If english -> give back youtube_id subtitles:
|
||||
Return what we have in contentstore for given youtube_id.
|
||||
If non-english:
|
||||
a) extract youtube_id from srt file name.
|
||||
b) try to find sjson by youtube_id and return if successful.
|
||||
c) generate sjson from srt for all youtube speeds.
|
||||
if non-youtube:
|
||||
If english -> give back `sub` subtitles:
|
||||
Return what we have in contentstore for given subs_if that is stored in self.sub.
|
||||
If non-english:
|
||||
a) try to find previously generated sjson.
|
||||
b) otherwise generate sjson from srt and return it.
|
||||
|
||||
Filenames naming:
|
||||
en: subs_videoid.srt.sjson
|
||||
non_en: uk_subs_videoid.srt.sjson
|
||||
|
||||
Raises:
|
||||
NotFoundError if for 'en' subtitles no asset is uploaded.
|
||||
"""
|
||||
|
||||
if youtube_id:
|
||||
# Youtube case:
|
||||
if self.transcript_language == 'en':
|
||||
return Transcript.asset(self.location, youtube_id).data
|
||||
|
||||
youtube_ids = youtube_speed_dict(self)
|
||||
assert youtube_id in youtube_ids
|
||||
|
||||
try:
|
||||
sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data
|
||||
except (NotFoundError):
|
||||
log.info("Can't find content in storage for %s transcript: generating.", youtube_id)
|
||||
generate_sjson_for_all_speeds(
|
||||
self,
|
||||
self.transcripts[self.transcript_language],
|
||||
{speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()},
|
||||
self.transcript_language
|
||||
)
|
||||
sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data
|
||||
|
||||
return sjson_transcript
|
||||
else:
|
||||
# HTML5 case
|
||||
if self.transcript_language == 'en':
|
||||
return Transcript.asset(self.location, self.sub).data
|
||||
else:
|
||||
return get_or_create_sjson(self)
|
||||
|
||||
|
||||
class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
|
||||
"""Descriptor for `VideoModule`."""
|
||||
class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescriptor, EmptyDataRawDescriptor):
|
||||
"""
|
||||
Descriptor for `VideoModule`.
|
||||
"""
|
||||
module_class = VideoModule
|
||||
transcript = module_attr('transcript')
|
||||
|
||||
@@ -544,14 +198,13 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
field. `download_video` field has value True.
|
||||
"""
|
||||
super(VideoDescriptor, self).__init__(*args, **kwargs)
|
||||
# For backwards compatibility -- if we've got XML data, parse
|
||||
# it out and set the metadata fields
|
||||
# For backwards compatibility -- if we've got XML data, parse it out and set the metadata fields
|
||||
if self.data:
|
||||
field_data = self._parse_video_xml(self.data)
|
||||
self._field_data.set_many(self, field_data)
|
||||
del self.data
|
||||
|
||||
editable_fields = self.editable_metadata_fields
|
||||
editable_fields = super(VideoDescriptor, self).editable_metadata_fields
|
||||
|
||||
self.source_visible = False
|
||||
if self.source:
|
||||
@@ -584,12 +237,16 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
|
||||
def editable_metadata_fields(self):
|
||||
editable_fields = super(VideoDescriptor, self).editable_metadata_fields
|
||||
|
||||
if hasattr(self, 'source_visible'):
|
||||
if self.source_visible:
|
||||
editable_fields['source']['non_editable'] = True
|
||||
else:
|
||||
editable_fields.pop('source')
|
||||
if self.source_visible:
|
||||
editable_fields['source']['non_editable'] = True
|
||||
else:
|
||||
editable_fields.pop('source')
|
||||
|
||||
languages = [{'label': label, 'code': lang} for lang, label in settings.ALL_LANGUAGES if lang != u'en']
|
||||
languages.sort(key=lambda l: l['label'])
|
||||
editable_fields['transcripts']['languages'] = languages
|
||||
editable_fields['transcripts']['type'] = 'VideoTranslations'
|
||||
editable_fields['transcripts']['urlRoot'] = self.runtime.handler_url(self, 'studio_transcript', 'translation').rstrip('/?')
|
||||
return editable_fields
|
||||
|
||||
@classmethod
|
||||
|
||||
141
common/lib/xmodule/xmodule/video_module/video_xfields.py
Normal file
141
common/lib/xmodule/xmodule/video_module/video_xfields.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
XFields for video module.
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict
|
||||
|
||||
from xmodule.fields import RelativeTime
|
||||
|
||||
|
||||
class VideoFields(object):
|
||||
"""Fields for `VideoModule` and `VideoDescriptor`."""
|
||||
display_name = String(
|
||||
display_name="Display Name", help="Display name for this module.",
|
||||
default="Video",
|
||||
scope=Scope.settings
|
||||
)
|
||||
saved_video_position = RelativeTime(
|
||||
help="Current position in the video",
|
||||
scope=Scope.user_state,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
# TODO: This should be moved to Scope.content, but this will
|
||||
# require data migration to support the old video module.
|
||||
youtube_id_1_0 = String(
|
||||
help="This is the Youtube ID reference for the normal speed video.",
|
||||
display_name="Youtube ID",
|
||||
scope=Scope.settings,
|
||||
default="OEoXaMPEzfM"
|
||||
)
|
||||
youtube_id_0_75 = String(
|
||||
help="Optional, for older browsers: the Youtube ID for the .75x speed video.",
|
||||
display_name="Youtube ID for .75x speed",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
youtube_id_1_25 = String(
|
||||
help="Optional, for older browsers: the Youtube ID for the 1.25x speed video.",
|
||||
display_name="Youtube ID for 1.25x speed",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
youtube_id_1_5 = String(
|
||||
help="Optional, for older browsers: the Youtube ID for the 1.5x speed video.",
|
||||
display_name="Youtube ID for 1.5x speed",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
start_time = RelativeTime( # datetime.timedelta object
|
||||
help="Start time for the video (HH:MM:SS). Max value is 23:59:59.",
|
||||
display_name="Start Time",
|
||||
scope=Scope.settings,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
end_time = RelativeTime( # datetime.timedelta object
|
||||
help="End time for the video (HH:MM:SS). Max value is 23:59:59.",
|
||||
display_name="End Time",
|
||||
scope=Scope.settings,
|
||||
default=datetime.timedelta(seconds=0)
|
||||
)
|
||||
#front-end code of video player checks logical validity of (start_time, end_time) pair.
|
||||
|
||||
# `source` is deprecated field and should not be used in future.
|
||||
# `download_video` is used instead.
|
||||
source = String(
|
||||
help="The external URL to download the video.",
|
||||
display_name="Download Video",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
download_video = Boolean(
|
||||
help="Show a link beneath the video to allow students to download the video. Note: You must add at least one video source below.",
|
||||
display_name="Video Download Allowed",
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
html5_sources = List(
|
||||
help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
|
||||
display_name="Video Sources",
|
||||
scope=Scope.settings,
|
||||
)
|
||||
track = String(
|
||||
help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
|
||||
display_name="Download Transcript",
|
||||
scope=Scope.settings,
|
||||
default=''
|
||||
)
|
||||
download_track = Boolean(
|
||||
help="Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.",
|
||||
display_name="Transcript Download Allowed",
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
sub = String(
|
||||
help="The name of the timed transcript track (for non-Youtube videos).",
|
||||
display_name="Transcript (primary)",
|
||||
scope=Scope.settings,
|
||||
default=""
|
||||
)
|
||||
show_captions = Boolean(
|
||||
help="This controls whether or not captions are shown by default.",
|
||||
display_name="Transcript Display",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
|
||||
transcripts = Dict(
|
||||
help="Add additional transcripts in other languages",
|
||||
display_name="Transcript Translations",
|
||||
scope=Scope.settings,
|
||||
default={}
|
||||
)
|
||||
transcript_language = String(
|
||||
help="Preferred language for transcript",
|
||||
display_name="Preferred language for transcript",
|
||||
scope=Scope.preferences,
|
||||
default="en"
|
||||
)
|
||||
transcript_download_format = String(
|
||||
help="Transcript file format to download by user.",
|
||||
scope=Scope.preferences,
|
||||
values=[
|
||||
{"display_name": "SubRip (.srt) file", "value": "srt"},
|
||||
{"display_name": "Text (.txt) file", "value": "txt"}
|
||||
],
|
||||
default='srt',
|
||||
)
|
||||
speed = Float(
|
||||
help="The last speed that was explicitly set by user for the video.",
|
||||
scope=Scope.user_state,
|
||||
)
|
||||
global_speed = Float(
|
||||
help="Default speed in cases when speed wasn't explicitly for specific video",
|
||||
scope=Scope.preferences,
|
||||
default=1.0
|
||||
)
|
||||
youtube_is_available = Boolean(
|
||||
help="The availaibility of YouTube API for the user",
|
||||
scope=Scope.user_info,
|
||||
default=True
|
||||
)
|
||||
59999
common/test/data/uploads/1mb_transcripts.srt
Normal file
59999
common/test/data/uploads/1mb_transcripts.srt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
||||
0
|
||||
00:00:00,270 --> 00:00:02,720
|
||||
LILA FISHER: Hi, welcome to Edx.
|
||||
|
||||
1
|
||||
00:00:02,720 --> 00:00:05,430
|
||||
I'm Lila Fisher, an Edx fellow helping to put
|
||||
|
||||
2
|
||||
00:00:05,430 --> 00:00:07,160
|
||||
together these courses.
|
||||
|
||||
3
|
||||
00:00:07,160 --> 00:00:10,830
|
||||
As you know, our courses are entirely online.
|
||||
|
||||
4
|
||||
00:00:10,830 --> 00:00:12,880
|
||||
So before we start learning about the subjects that
|
||||
|
||||
5
|
||||
00:00:12,880 --> 00:00:15,890
|
||||
brought you here, let's learn about the tools that you will
|
||||
|
||||
6
|
||||
00:00:15,890 --> 00:00:19,000
|
||||
use to navigate through the course material.
|
||||
|
||||
7
|
||||
00:00:19,000 --> 00:00:22,070
|
||||
Let's start with what is on your screen right now.
|
||||
|
||||
8
|
||||
00:00:22,070 --> 00:00:25,170
|
||||
You are watching a video of me talking.
|
||||
|
||||
9
|
||||
00:00:25,170 --> 00:00:27,890
|
||||
You have several tools associated with these videos.
|
||||
|
||||
10
|
||||
00:00:27,890 --> 00:00:30,590
|
||||
Some of them are standard video buttons, like the play
|
||||
4
common/test/data/uploads/uk_transcripts.srt
Normal file
4
common/test/data/uploads/uk_transcripts.srt
Normal file
@@ -0,0 +1,4 @@
|
||||
0
|
||||
00:00:00,100 --> 00:00:02,000
|
||||
Привіт, edX вітає вас.
|
||||
|
||||
@@ -51,7 +51,7 @@ def setUp(scenario):
|
||||
world.video_sequences = {}
|
||||
|
||||
|
||||
class ReuqestHandlerWithSessionId(object):
|
||||
class RequestHandlerWithSessionId(object):
|
||||
def get(self, url):
|
||||
"""
|
||||
Sends a request.
|
||||
@@ -449,7 +449,6 @@ def select_language(_step, code):
|
||||
def click_button(_step, button):
|
||||
world.css_click(VIDEO_BUTTONS[button])
|
||||
|
||||
|
||||
@step('I see video starts playing from "([^"]*)" position$')
|
||||
def start_playing_video_from_n_seconds(_step, position):
|
||||
world.wait_for(
|
||||
@@ -522,7 +521,7 @@ def i_can_download_transcript(_step, format, text):
|
||||
}
|
||||
|
||||
url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href']
|
||||
request = ReuqestHandlerWithSessionId()
|
||||
request = RequestHandlerWithSessionId()
|
||||
assert request.get(url).is_success()
|
||||
assert request.check_header('content-type', formats[format])
|
||||
assert (text.encode('utf-8') in request.content)
|
||||
|
||||
@@ -17,23 +17,44 @@ from .test_video_xml import SOURCE_XML
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
from xmodule.video_module.transcripts_utils import (
|
||||
TranscriptException,
|
||||
TranscriptsGenerationException,
|
||||
)
|
||||
|
||||
SRT_content = textwrap.dedent("""
|
||||
0
|
||||
00:00:00,12 --> 00:00:00,100
|
||||
Привіт, edX вітає вас.
|
||||
""")
|
||||
|
||||
|
||||
def _create_srt_file(content=None):
|
||||
"""
|
||||
Create srt file in filesystem.
|
||||
"""
|
||||
content = content or textwrap.dedent("""
|
||||
0
|
||||
00:00:00,12 --> 00:00:00,100
|
||||
Привіт, edX вітає вас.
|
||||
""")
|
||||
content = content or SRT_content
|
||||
srt_file = tempfile.NamedTemporaryFile(suffix=".srt")
|
||||
srt_file.content_type = 'application/x-subrip'
|
||||
srt_file.content_type = 'application/x-subrip; charset=utf-8'
|
||||
srt_file.write(content)
|
||||
srt_file.seek(0)
|
||||
return srt_file
|
||||
|
||||
|
||||
def _check_asset(location, asset_name):
|
||||
"""
|
||||
Check that asset with asset_name exists in assets.
|
||||
"""
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, asset_name
|
||||
)
|
||||
try:
|
||||
contentstore().find(content_location)
|
||||
except NotFoundError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _clear_assets(location):
|
||||
"""
|
||||
Clear all assets for location.
|
||||
@@ -167,32 +188,33 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
request = Request.blank('/translation')
|
||||
request = Request.blank('/available_translations')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['en'])
|
||||
|
||||
def test_available_translation_non_en(self):
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
|
||||
request = Request.blank('/translation')
|
||||
request = Request.blank('/available_translations')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['uk'])
|
||||
|
||||
def test_multiple_available_translations(self):
|
||||
good_sjson = _create_file(json.dumps(self.subs))
|
||||
|
||||
|
||||
# Upload english transcript.
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
|
||||
# Upload non-english transcript.
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
request = Request.blank('/translation')
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
request = Request.blank('/available_translations')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['en', 'uk'])
|
||||
|
||||
|
||||
class TestTranscriptDownloadDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide translation transcripts.
|
||||
@@ -200,16 +222,14 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
Tests for `download` dispatch.
|
||||
"""
|
||||
|
||||
non_en_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
"""
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
@@ -220,40 +240,45 @@ class TestTranscriptDownloadDispatch(TestVideo):
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
|
||||
def test_language_is_not_supported(self):
|
||||
request = Request.blank('/download?language=ru')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
def test_download_transcript_not_exist(self):
|
||||
request = Request.blank('/download?language=en')
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'test_filename.srt', 'application/x-subrip'))
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'test_filename.srt', 'application/x-subrip; charset=utf-8'))
|
||||
def test_download_srt_exist(self, __):
|
||||
request = Request.blank('/download?language=en')
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.body, 'Subs!')
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip')
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Language'], 'en')
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'txt', 'text/plain'))
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'txt', 'text/plain; charset=utf-8'))
|
||||
def test_download_txt_exist(self, __):
|
||||
self.item.transcript_format = 'txt'
|
||||
request = Request.blank('/download?language=en')
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.body, 'Subs!')
|
||||
self.assertEqual(response.headers['Content-Type'], 'text/plain')
|
||||
self.assertEqual(response.headers['Content-Type'], 'text/plain; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Language'], 'en')
|
||||
|
||||
def test_download_en_no_sub(self):
|
||||
request = Request.blank('/download?language=en')
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript()
|
||||
|
||||
class TestTranscriptTranslationDispatch(TestVideo):
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', u"塞.srt", 'application/x-subrip; charset=utf-8'))
|
||||
def test_download_non_en_non_ascii_filename(self, __):
|
||||
request = Request.blank('/download')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.body, 'Subs!')
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="塞.srt"')
|
||||
|
||||
|
||||
class TestTranscriptTranslationGetDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide translation transcripts.
|
||||
|
||||
@@ -276,7 +301,7 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestTranscriptTranslationDispatch, self).setUp()
|
||||
super(TestTranscriptTranslationGetDispatch, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
@@ -287,13 +312,13 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
# No videoId - HTML5 video with language that is not in available languages
|
||||
request = Request.blank('/translation?language=ru')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/ru')
|
||||
response = self.item.transcript(request=request, dispatch='translation/ru')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
# Language is not in available languages
|
||||
request = Request.blank('/translation?language=ru&videoId=12345')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/ru?videoId=12345')
|
||||
response = self.item.transcript(request=request, dispatch='translation/ru')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
def test_translaton_en_youtube_success(self):
|
||||
@@ -303,8 +328,8 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
self.item.sub = subs_id
|
||||
request = Request.blank('/translation?language=en&videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/en?videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
def test_translation_non_en_youtube_success(self):
|
||||
@@ -321,13 +346,13 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
# youtube 1_0 request, will generate for all speeds for existing ids
|
||||
self.item.youtube_id_1_0 = subs_id
|
||||
self.item.youtube_id_0_75 = '0_75'
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/uk?videoId={}'.format(subs_id))
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
# 0_75 subs are exist
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format('0_75'))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/uk?videoId={}'.format('0_75'))
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
calculated_0_75 = {
|
||||
u'end': [75],
|
||||
u'start': [9],
|
||||
@@ -338,8 +363,8 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
self.assertDictEqual(json.loads(response.body), calculated_0_75)
|
||||
# 1_5 will be generated from 1_0
|
||||
self.item.youtube_id_1_5 = '1_5'
|
||||
request = Request.blank('/translation?language=uk&videoId={}'.format('1_5'))
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/uk?videoId={}'.format('1_5'))
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
calculated_1_5 = {
|
||||
u'end': [150],
|
||||
u'start': [18],
|
||||
@@ -356,8 +381,8 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
self.item.sub = subs_id
|
||||
request = Request.blank('/translation?language=en')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/en')
|
||||
response = self.item.transcript(request=request, dispatch='translation/en')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
def test_translaton_non_en_html5_success(self):
|
||||
@@ -373,11 +398,126 @@ class TestTranscriptTranslationDispatch(TestVideo):
|
||||
|
||||
# manually clean youtube_id_1_0, as it has default value
|
||||
self.item.youtube_id_1_0 = ""
|
||||
request = Request.blank('/translation?language=uk')
|
||||
response = self.item.transcript(request=request, dispatch='translation')
|
||||
request = Request.blank('/translation/uk')
|
||||
response = self.item.transcript(request=request, dispatch='translation/uk')
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
|
||||
class TestStudioTranscriptTranslationGetDispatch(TestVideo):
|
||||
"""
|
||||
Test Studio video handler that provide translation transcripts.
|
||||
|
||||
Tests for `translation` dispatch GET HTTP method.
|
||||
"""
|
||||
non_en_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
<transcript language="zh" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1], u"塞.srt".encode('utf8'))
|
||||
|
||||
MODEL_DATA = {'data': DATA}
|
||||
|
||||
def test_translation_fails(self):
|
||||
# No language
|
||||
request = Request.blank('')
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
# No filename in request.GET
|
||||
request = Request.blank('')
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
# Correct case:
|
||||
filename = os.path.split(self.non_en_file.name)[1]
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, filename)
|
||||
self.non_en_file.seek(0)
|
||||
request = Request.blank(u'translation/uk?filename={}'.format(filename))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.body, self.non_en_file.read())
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(
|
||||
response.headers['Content-Disposition'],
|
||||
'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
self.assertEqual(response.headers['Content-Language'], 'uk')
|
||||
|
||||
# Non ascii file name download:
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, u'塞.srt')
|
||||
self.non_en_file.seek(0)
|
||||
request = Request.blank('translation/zh?filename={}'.format(u'塞.srt'.encode('utf8')))
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/zh')
|
||||
self.assertEqual(response.body, self.non_en_file.read())
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="塞.srt"')
|
||||
self.assertEqual(response.headers['Content-Language'], 'zh')
|
||||
|
||||
|
||||
class TestStudioTranscriptTranslationPostDispatch(TestVideo):
|
||||
"""
|
||||
Test Studio video handler that provide translation transcripts.
|
||||
|
||||
Tests for `translation` dispatch with HTTP POST method.
|
||||
"""
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
|
||||
</video>
|
||||
"""
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
|
||||
METADATA = {}
|
||||
|
||||
def test_studio_transcript_post(self):
|
||||
# Check for exceptons:
|
||||
|
||||
# Language is passed, bad content or filename:
|
||||
|
||||
# should be first, as other tests save transcrips to store.
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', SRT_content)})
|
||||
with patch('xmodule.video_module.video_handlers.save_to_store'):
|
||||
with self.assertRaises(TranscriptException): # transcripts were not saved to store for some reason.
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename', 'content')})
|
||||
with self.assertRaises(TranscriptsGenerationException): # Not an srt filename
|
||||
self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', 'content')})
|
||||
with self.assertRaises(TranscriptsGenerationException): # Content format is not srt.
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', SRT_content.decode('utf8').encode('cp1251'))})
|
||||
with self.assertRaises(UnicodeDecodeError): # Non-UTF8 file content encoding.
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
|
||||
# No language is passed.
|
||||
request = Request.blank('/translation', POST={'file': ('filename', SRT_content)})
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation')
|
||||
self.assertEqual(response.status, '400 Bad Request')
|
||||
|
||||
# Language, good filename and good content.
|
||||
request = Request.blank('/translation/uk', POST={'file': ('filename.srt', SRT_content)})
|
||||
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
|
||||
self.assertEqual(response.status, '201 Created')
|
||||
self.assertDictEqual(json.loads(response.body), {'filename': u'filename.srt', 'status': 'Success'})
|
||||
self.assertDictEqual(self.item_descriptor.transcripts, {})
|
||||
self.assertTrue(_check_asset(self.item_descriptor.location, u'filename.srt'))
|
||||
|
||||
|
||||
class TestGetTranscript(TestVideo):
|
||||
"""
|
||||
Make sure that `get_transcript` method works correctly
|
||||
@@ -390,8 +530,9 @@ class TestGetTranscript(TestVideo):
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
<transcript language="zh" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
""".format(os.path.split(non_en_file.name)[1], u"塞.srt".encode('utf8'))
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
@@ -442,7 +583,7 @@ class TestGetTranscript(TestVideo):
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename[:-4], self.item.sub)
|
||||
self.assertEqual(mime_type, 'application/x-subrip')
|
||||
self.assertEqual(mime_type, 'application/x-subrip; charset=utf-8')
|
||||
|
||||
def test_good_txt_transcript(self):
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
@@ -471,7 +612,7 @@ class TestGetTranscript(TestVideo):
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename, self.item.sub + '.txt')
|
||||
self.assertEqual(mime_type, 'text/plain')
|
||||
self.assertEqual(mime_type, 'text/plain; charset=utf-8')
|
||||
|
||||
def test_en_with_empty_sub(self):
|
||||
|
||||
@@ -518,12 +659,12 @@ class TestGetTranscript(TestVideo):
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename, self.item.youtube_id_1_0 + '.srt')
|
||||
self.assertEqual(mime_type, 'application/x-subrip')
|
||||
self.assertEqual(mime_type, 'application/x-subrip; charset=utf-8')
|
||||
|
||||
def test_non_en(self):
|
||||
self.item.transcript_language = 'uk'
|
||||
def test_non_en_with_non_ascii_filename(self):
|
||||
self.item.transcript_language = 'zh'
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, u"塞.srt")
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
expected_text = textwrap.dedent("""
|
||||
@@ -532,9 +673,9 @@ class TestGetTranscript(TestVideo):
|
||||
Привіт, edX вітає вас.
|
||||
""")
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename, os.path.split(self.non_en_file.name)[1])
|
||||
self.assertEqual(mime_type, 'application/x-subrip')
|
||||
|
||||
self.assertEqual(filename, u"塞.srt")
|
||||
self.assertEqual(mime_type, 'application/x-subrip; charset=utf-8')
|
||||
|
||||
def test_value_error(self):
|
||||
good_sjson = _create_file(content='bad content')
|
||||
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Video xmodule tests in mongo."""
|
||||
from mock import patch, PropertyMock
|
||||
import unittest
|
||||
from mock import patch, PropertyMock, MagicMock
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.field_data import DictFieldData
|
||||
|
||||
from xmodule.video_module import create_youtube_string
|
||||
from xmodule.tests import get_test_descriptor_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.video_module import VideoDescriptor
|
||||
|
||||
from . import BaseTestXmodule
|
||||
from .test_video_xml import SOURCE_XML
|
||||
from .test_video_handlers import TestVideo
|
||||
from django.conf import settings
|
||||
from xmodule.video_module import create_youtube_string
|
||||
|
||||
|
||||
class TestVideoYouTube(TestVideo):
|
||||
@@ -46,11 +55,11 @@ class TestVideoYouTube(TestVideo):
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English", "uk": "Ukrainian"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
@@ -112,11 +121,11 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?')
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
@@ -223,8 +232,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
self.initialize_module(data=DATA)
|
||||
track_url = self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/download'
|
||||
self.item_descriptor, 'transcript', 'download'
|
||||
).rstrip('/?')
|
||||
|
||||
context = self.item_descriptor.render('student_view').content
|
||||
|
||||
@@ -233,11 +242,11 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'transcript_languages': '{"en": "English"}' if not data['transcripts'] else '{"uk": "Ukrainian"}',
|
||||
'transcript_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
|
||||
'sub': data['sub'],
|
||||
@@ -345,11 +354,11 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
expected_context.update({
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
self.item_descriptor, 'transcript', 'translation'
|
||||
).rstrip('/?'),
|
||||
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
self.item_descriptor, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
|
||||
'sources': data['result'],
|
||||
'id': self.item_descriptor.location.html_id(),
|
||||
@@ -399,76 +408,81 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
self.assertTrue(self.item_descriptor.download_video)
|
||||
self.assertFalse(self.item_descriptor.source_visible)
|
||||
|
||||
@patch('xmodule.video_module.VideoDescriptor.editable_metadata_fields', new_callable=PropertyMock)
|
||||
def test_download_video_is_explicitly_set(self, mock_editable_fields):
|
||||
mock_editable_fields.return_value = {
|
||||
'download_video': {
|
||||
'default_value': False,
|
||||
'explicitly_set': True,
|
||||
'display_name': 'Video Download Allowed',
|
||||
'help': 'Show a link beneath the video to allow students to download the video.',
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
'field_name': 'download_video',
|
||||
'options': [
|
||||
{'display_name': "True", "value": True},
|
||||
{'display_name': "False", "value": False}
|
||||
],
|
||||
},
|
||||
'html5_sources': {
|
||||
'default_value': [],
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Video Sources',
|
||||
'help': 'A list of filenames to be used with HTML5 video.',
|
||||
'type': 'List',
|
||||
'value': [u'http://youtu.be/OEoXaMPEzfM.mp4'],
|
||||
'field_name': 'html5_sources',
|
||||
'options': [],
|
||||
},
|
||||
'source': {
|
||||
'default_value': '',
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Download Video',
|
||||
'help': 'The external URL to download the video.',
|
||||
'type': 'Generic',
|
||||
'value': u'http://example.org/video.mp4',
|
||||
'field_name': 'source',
|
||||
'options': [],
|
||||
},
|
||||
'track': {
|
||||
'default_value': '',
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Download Transcript',
|
||||
'help': 'The external URL to download the timed transcript track.',
|
||||
'type': 'Generic',
|
||||
'value': u'http://some_track.srt',
|
||||
'field_name': 'track',
|
||||
'options': [],
|
||||
},
|
||||
'download_track': {
|
||||
'default_value': False,
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Transcript Download Allowed',
|
||||
'help': 'Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.',
|
||||
'type': 'Generic',
|
||||
'value': False,
|
||||
'field_name': 'download_track',
|
||||
'options': [],
|
||||
def test_download_video_is_explicitly_set(self):
|
||||
with patch(
|
||||
'xmodule.editing_module.TabsEditingDescriptor.editable_metadata_fields',
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
'download_video': {
|
||||
'default_value': False,
|
||||
'explicitly_set': True,
|
||||
'display_name': 'Video Download Allowed',
|
||||
'help': 'Show a link beneath the video to allow students to download the video.',
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
'field_name': 'download_video',
|
||||
'options': [
|
||||
{'display_name': "True", "value": True},
|
||||
{'display_name': "False", "value": False}
|
||||
],
|
||||
},
|
||||
'html5_sources': {
|
||||
'default_value': [],
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Video Sources',
|
||||
'help': 'A list of filenames to be used with HTML5 video.',
|
||||
'type': 'List',
|
||||
'value': [u'http://youtu.be/OEoXaMPEzfM.mp4'],
|
||||
'field_name': 'html5_sources',
|
||||
'options': [],
|
||||
},
|
||||
'source': {
|
||||
'default_value': '',
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Download Video',
|
||||
'help': 'The external URL to download the video.',
|
||||
'type': 'Generic',
|
||||
'value': u'http://example.org/video.mp4',
|
||||
'field_name': 'source',
|
||||
'options': [],
|
||||
},
|
||||
'track': {
|
||||
'default_value': '',
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Download Transcript',
|
||||
'help': 'The external URL to download the timed transcript track.',
|
||||
'type': 'Generic',
|
||||
'value': u'http://some_track.srt',
|
||||
'field_name': 'track',
|
||||
'options': [],
|
||||
},
|
||||
'download_track': {
|
||||
'default_value': False,
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Transcript Download Allowed',
|
||||
'help': 'Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.',
|
||||
'type': 'Generic',
|
||||
'value': False,
|
||||
'field_name': 'download_track',
|
||||
'options': [],
|
||||
},
|
||||
'transcripts': {},
|
||||
}
|
||||
):
|
||||
metadata = {
|
||||
'track': u'http://some_track.srt',
|
||||
'source': 'http://example.org/video.mp4',
|
||||
'html5_sources': ['http://youtu.be/OEoXaMPEzfM.mp4'],
|
||||
}
|
||||
}
|
||||
metadata = {
|
||||
'track': u'http://some_track.srt',
|
||||
'source': 'http://example.org/video.mp4',
|
||||
'html5_sources': ['http://youtu.be/OEoXaMPEzfM.mp4'],
|
||||
}
|
||||
|
||||
self.initialize_module(metadata=metadata)
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
self.initialize_module(metadata=metadata)
|
||||
|
||||
self.assertIn('source', fields)
|
||||
self.assertFalse(self.item_descriptor.download_video)
|
||||
self.assertTrue(self.item_descriptor.source_visible)
|
||||
self.assertTrue(self.item_descriptor.download_track)
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
self.assertIn('source', fields)
|
||||
|
||||
self.assertFalse(self.item_descriptor.download_video)
|
||||
self.assertTrue(self.item_descriptor.source_visible)
|
||||
self.assertTrue(self.item_descriptor.download_track)
|
||||
|
||||
def test_source_is_empty(self):
|
||||
metadata = {
|
||||
@@ -481,3 +495,40 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
|
||||
self.assertNotIn('source', fields)
|
||||
self.assertFalse(self.item_descriptor.download_video)
|
||||
|
||||
|
||||
class VideoDescriptorTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for video descriptor that requires access to django settings.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
system = get_test_descriptor_system()
|
||||
location = Location('i4x://org/course/video/name')
|
||||
self.descriptor = system.construct_xblock_from_class(
|
||||
VideoDescriptor,
|
||||
scope_ids=ScopeIds(None, None, location, location),
|
||||
field_data=DictFieldData({}),
|
||||
)
|
||||
self.descriptor.runtime.handler_url = MagicMock()
|
||||
|
||||
def test_get_context(self):
|
||||
""""
|
||||
Test get_context.
|
||||
|
||||
This test is located here and not in xmodule.tests because get_context calls editable_metadata_fields.
|
||||
Which, in turn, uses settings.LANGUAGES from django setttings.
|
||||
"""
|
||||
correct_tabs = [
|
||||
{
|
||||
'name': "Basic",
|
||||
'template': "video/transcripts.html",
|
||||
'current': True
|
||||
},
|
||||
{
|
||||
'name': 'Advanced',
|
||||
'template': 'tabs/metadata-edit-tab.html'
|
||||
}
|
||||
]
|
||||
rendered_context = self.descriptor.get_context()
|
||||
self.assertListEqual(rendered_context['tabs'], correct_tabs)
|
||||
|
||||
@@ -1326,19 +1326,19 @@ ALL_LANGUAGES = (
|
||||
[u"br", u"Breton"],
|
||||
[u"bg", u"Bulgarian"],
|
||||
[u"my", u"Burmese"],
|
||||
[u"ca", u"Catalan; Valencian"],
|
||||
[u"ca", u"Catalan"],
|
||||
[u"ch", u"Chamorro"],
|
||||
[u"ce", u"Chechen"],
|
||||
[u"zh", u"Chinese"],
|
||||
[u"cu", u"Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"],
|
||||
[u"cu", u"Church Slavic"],
|
||||
[u"cv", u"Chuvash"],
|
||||
[u"kw", u"Cornish"],
|
||||
[u"co", u"Corsican"],
|
||||
[u"cr", u"Cree"],
|
||||
[u"cs", u"Czech"],
|
||||
[u"da", u"Danish"],
|
||||
[u"dv", u"Divehi; Dhivehi; Maldivian"],
|
||||
[u"nl", u"Dutch; Flemish"],
|
||||
[u"dv", u"Divehi"],
|
||||
[u"nl", u"Dutch"],
|
||||
[u"dz", u"Dzongkha"],
|
||||
[u"en", u"English"],
|
||||
[u"eo", u"Esperanto"],
|
||||
@@ -1352,14 +1352,14 @@ ALL_LANGUAGES = (
|
||||
[u"ff", u"Fulah"],
|
||||
[u"ka", u"Georgian"],
|
||||
[u"de", u"German"],
|
||||
[u"gd", u"Gaelic; Scottish Gaelic"],
|
||||
[u"gd", u"Gaelic"],
|
||||
[u"ga", u"Irish"],
|
||||
[u"gl", u"Galician"],
|
||||
[u"gv", u"Manx"],
|
||||
[u"el", u"Greek, Modern (1453-)"],
|
||||
[u"el", u"Greek"],
|
||||
[u"gn", u"Guarani"],
|
||||
[u"gu", u"Gujarati"],
|
||||
[u"ht", u"Haitian; Haitian Creole"],
|
||||
[u"ht", u"Haitian"],
|
||||
[u"ha", u"Hausa"],
|
||||
[u"he", u"Hebrew"],
|
||||
[u"hz", u"Herero"],
|
||||
@@ -1370,36 +1370,36 @@ ALL_LANGUAGES = (
|
||||
[u"ig", u"Igbo"],
|
||||
[u"is", u"Icelandic"],
|
||||
[u"io", u"Ido"],
|
||||
[u"ii", u"Sichuan Yi; Nuosu"],
|
||||
[u"ii", u"Sichuan Yi"],
|
||||
[u"iu", u"Inuktitut"],
|
||||
[u"ie", u"Interlingue; Occidental"],
|
||||
[u"ia", u"Interlingua (International Auxiliary Language Association)"],
|
||||
[u"ie", u"Interlingue"],
|
||||
[u"ia", u"Interlingua"],
|
||||
[u"id", u"Indonesian"],
|
||||
[u"ik", u"Inupiaq"],
|
||||
[u"it", u"Italian"],
|
||||
[u"jv", u"Javanese"],
|
||||
[u"ja", u"Japanese"],
|
||||
[u"kl", u"Kalaallisut; Greenlandic"],
|
||||
[u"kl", u"Kalaallisut"],
|
||||
[u"kn", u"Kannada"],
|
||||
[u"ks", u"Kashmiri"],
|
||||
[u"kr", u"Kanuri"],
|
||||
[u"kk", u"Kazakh"],
|
||||
[u"km", u"Central Khmer"],
|
||||
[u"ki", u"Kikuyu; Gikuyu"],
|
||||
[u"ki", u"Kikuyu"],
|
||||
[u"rw", u"Kinyarwanda"],
|
||||
[u"ky", u"Kirghiz; Kyrgyz"],
|
||||
[u"ky", u"Kirghiz"],
|
||||
[u"kv", u"Komi"],
|
||||
[u"kg", u"Kongo"],
|
||||
[u"ko", u"Korean"],
|
||||
[u"kj", u"Kuanyama; Kwanyama"],
|
||||
[u"kj", u"Kuanyama"],
|
||||
[u"ku", u"Kurdish"],
|
||||
[u"lo", u"Lao"],
|
||||
[u"la", u"Latin"],
|
||||
[u"lv", u"Latvian"],
|
||||
[u"li", u"Limburgan; Limburger; Limburgish"],
|
||||
[u"li", u"Limburgan"],
|
||||
[u"ln", u"Lingala"],
|
||||
[u"lt", u"Lithuanian"],
|
||||
[u"lb", u"Luxembourgish; Letzeburgesch"],
|
||||
[u"lb", u"Luxembourgish"],
|
||||
[u"lu", u"Luba-Katanga"],
|
||||
[u"lg", u"Ganda"],
|
||||
[u"mk", u"Macedonian"],
|
||||
@@ -1412,34 +1412,34 @@ ALL_LANGUAGES = (
|
||||
[u"mt", u"Maltese"],
|
||||
[u"mn", u"Mongolian"],
|
||||
[u"na", u"Nauru"],
|
||||
[u"nv", u"Navajo; Navaho"],
|
||||
[u"nr", u"Ndebele, South; South Ndebele"],
|
||||
[u"nd", u"Ndebele, North; North Ndebele"],
|
||||
[u"nv", u"Navajo"],
|
||||
[u"nr", u"Ndebele, South"],
|
||||
[u"nd", u"Ndebele, North"],
|
||||
[u"ng", u"Ndonga"],
|
||||
[u"ne", u"Nepali"],
|
||||
[u"nn", u"Norwegian Nynorsk; Nynorsk, Norwegian"],
|
||||
[u"nb", u"Bokmål, Norwegian; Norwegian Bokmål"],
|
||||
[u"nn", u"Norwegian Nynorsk"],
|
||||
[u"nb", u"Bokmål, Norwegian"],
|
||||
[u"no", u"Norwegian"],
|
||||
[u"ny", u"Chichewa; Chewa; Nyanja"],
|
||||
[u"oc", u"Occitan (post 1500); Provençal"],
|
||||
[u"ny", u"Chichewa"],
|
||||
[u"oc", u"Occitan"],
|
||||
[u"oj", u"Ojibwa"],
|
||||
[u"or", u"Oriya"],
|
||||
[u"om", u"Oromo"],
|
||||
[u"os", u"Ossetian; Ossetic"],
|
||||
[u"pa", u"Panjabi; Punjabi"],
|
||||
[u"os", u"Ossetian"],
|
||||
[u"pa", u"Panjabi"],
|
||||
[u"fa", u"Persian"],
|
||||
[u"pi", u"Pali"],
|
||||
[u"pl", u"Polish"],
|
||||
[u"pt", u"Portuguese"],
|
||||
[u"ps", u"Pushto; Pashto"],
|
||||
[u"ps", u"Pushto"],
|
||||
[u"qu", u"Quechua"],
|
||||
[u"rm", u"Romansh"],
|
||||
[u"ro", u"Romanian; Moldavian; Moldovan"],
|
||||
[u"ro", u"Romanian"],
|
||||
[u"rn", u"Rundi"],
|
||||
[u"ru", u"Russian"],
|
||||
[u"sg", u"Sango"],
|
||||
[u"sa", u"Sanskrit"],
|
||||
[u"si", u"Sinhala; Sinhalese"],
|
||||
[u"si", u"Sinhala"],
|
||||
[u"sk", u"Slovak"],
|
||||
[u"sl", u"Slovenian"],
|
||||
[u"se", u"Northern Sami"],
|
||||
@@ -1448,7 +1448,7 @@ ALL_LANGUAGES = (
|
||||
[u"sd", u"Sindhi"],
|
||||
[u"so", u"Somali"],
|
||||
[u"st", u"Sotho, Southern"],
|
||||
[u"es", u"Spanish; Castilian"],
|
||||
[u"es", u"Spanish"],
|
||||
[u"sc", u"Sardinian"],
|
||||
[u"sr", u"Serbian"],
|
||||
[u"ss", u"Swati"],
|
||||
@@ -1470,7 +1470,7 @@ ALL_LANGUAGES = (
|
||||
[u"tk", u"Turkmen"],
|
||||
[u"tr", u"Turkish"],
|
||||
[u"tw", u"Twi"],
|
||||
[u"ug", u"Uighur; Uyghur"],
|
||||
[u"ug", u"Uighur"],
|
||||
[u"uk", u"Ukrainian"],
|
||||
[u"ur", u"Urdu"],
|
||||
[u"uz", u"Uzbek"],
|
||||
@@ -1483,7 +1483,7 @@ ALL_LANGUAGES = (
|
||||
[u"xh", u"Xhosa"],
|
||||
[u"yi", u"Yiddish"],
|
||||
[u"yo", u"Yoruba"],
|
||||
[u"za", u"Zhuang; Chuang"],
|
||||
[u"za", u"Zhuang"],
|
||||
[u"zu", u"Zulu"]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user