Commit dfa7c27e by Alexander Kryklia

Add multiple transcripts editor.

Fix donwload subs for non youtube videos and non-en language - continue.
Add acceptance tests.
Add detetion of assets on request.
Updated docstring.
Add fixes and acceptance tests.
Fix acceptance tests.
Update docsrtings and cleanup code, resful for language_id.
Specify exception type in POST.
Fix url in upload module.
Improve exception handling.
Remove 'en' and catching in editable_metadata.
Move descriptor.get_context test to lms tests.
Add query parameter to translation dispatch.
Response to format parameter of translatin GET request.
Fix Acceprance test: Metadata Editor.
move handlers to proper scores.
Split video player into smaller files.
Add ugettext and fix typoes.
Add changelog.
Support for downloading non-ascii filenames.
Change event binding.
Add content-language to download requests.
Reractor POST handler to not update self.transcripts.
parent 90064234
......@@ -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
# -*- 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]))
# -*- 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()
......
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) {
......
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;
......
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.'],
......
define(
[
"jquery", "underscore",
"js/views/transcripts/utils",
"js/views/video/transcripts/utils",
"underscore.string", "xmodule", "jasmine-jquery"
],
function ($, _, Utils, _str) {
......
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',
......
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();
......
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,
......
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) {
......
define(
[
"jquery", "backbone", "underscore",
"js/views/transcripts/utils"
"js/views/video/transcripts/utils"
],
function($, Backbone, _, Utils) {
var FileUploader = Backbone.View.extend({
......
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) {
......
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,
......
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);
}
}
}
}
}
.wrapper-comp-settings.basic_metadata_edit {
.list-input.settings-list {
.field.comp-setting-entry {
.setting-label {
vertical-align: top;
margin-top: ($baseline/2);
}
.transcripts-status{
margin-top: $baseline;
&.is-invisible{
display: none !important;
}
.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;
.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;
}
}
*[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;
}
.action{
@extend %btn-primary-blue;
@extend %t-action3;
}
.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);
}
}
}
}
}
}
// TYPE: enumerated video lists of metadata sets
.metadata-videolist-enum {
* {
@include box-sizing(border-box);
}
.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;
}
}
.file-chooser{
.wrapper-transcripts-buttons {
&.is-invisible {
display: none;
}
}
}
.progress-bar{
display: block;
height: 30px;
margin: 10px 0;
border: 1px solid $blue;
text-align: center;
font-size: 1.14em;
.action {
@extend %btn-primary-blue;
@extend %t-action3;
margin-bottom: ($baseline/2);
}
}
&.is-invisible {
display: none;
}
// 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;
}
}
}
&.loaded {
border-color: #66b93d;
.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);
}
.progress-fill {
background: #66b93d;
}
// 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;
}
}
.progress-fill {
display: block;
width: 0%;
height: 30px;
background: $blue;
color: #fff;
line-height: 28px;
// 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 {
}
}
}
......
<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>
<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.
Return asset location. `location` is module location.
"""
return StaticContent.compute_location(location.org, location.course, filename)
`location` is module location.
@staticmethod
def delete_asset(location, filename):
"""
return StaticContent.compute_location(
location.org, location.course, 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
"""
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
# -*- 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
......
"""
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
)
This source diff could not be displayed because it is too large. You can view the blob instead.
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
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,
)
def _create_srt_file(content=None):
"""
Create srt file in filesystem.
"""
content = content or textwrap.dedent("""
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 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')
......
# -*- 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"]
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment