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, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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 Studio: Add ability to reorder Pages and hide the Wiki page. STUD-1375
Blades: Added template for iFrames. BLD-611. Blades: Added template for iFrames. BLD-611.
......
...@@ -332,10 +332,15 @@ def get_codemirror_value(index=0): ...@@ -332,10 +332,15 @@ def get_codemirror_value(index=0):
return $('div.CodeMirror:eq({})').get(0).CodeMirror.getValue(); return $('div.CodeMirror:eq({})').get(0).CodeMirror.getValue();
""".format(index)) """.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.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('file', os.path.abspath(path)) 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' button_css = '.upload-dialog .action-upload'
world.css_click(button_css) world.css_click(button_css)
......
...@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set): ...@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
# Check if the web object is a list type # Check if the web object is a list type
# If so, we use a slightly different mechanism for determining its value # 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')) list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
assert_equal(value, list_value) assert_equal(value, list_value)
elif setting.has_class('metadata-videolist-enum'): elif setting.has_class('metadata-videolist-enum'):
......
...@@ -27,7 +27,7 @@ def assert_create_new_textbook_msg(_step): ...@@ -27,7 +27,7 @@ def assert_create_new_textbook_msg(_step):
@step(u'I upload the textbook "([^"]*)"$') @step(u'I upload the textbook "([^"]*)"$')
def upload_textbook(_step, file_name): 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') @step(u'I click (on )?the New Textbook button')
......
...@@ -334,7 +334,7 @@ Feature: CMS Transcripts ...@@ -334,7 +334,7 @@ Feature: CMS Transcripts
Then I see status message "found" Then I see status message "found"
And I see button "download_to_edit" And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts" 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" Then I see status message "uploaded_successfully"
And I see button "download_to_edit" And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts" And I see button "upload_new_timed_transcripts"
...@@ -345,7 +345,7 @@ Feature: CMS Transcripts ...@@ -345,7 +345,7 @@ Feature: CMS Transcripts
And I see button "download_to_edit" And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts" 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" Then I see status message "found"
#20 #20
...@@ -353,21 +353,21 @@ Feature: CMS Transcripts ...@@ -353,21 +353,21 @@ Feature: CMS Transcripts
Given I have created a Video component with subtitles "t_not_exist" Given I have created a Video component with subtitles "t_not_exist"
And I edit the component 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" Then I see status message "not found"
And I see button "download_to_edit" And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts" 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" 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 And I enter a "t_not_exist.webm" source to field number 2
Then I see status message "replace" 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 see choose button "t_not_exist.webm" number 2
And I click transcript button "choose" 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 # Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927
#21 #21
...@@ -456,7 +456,7 @@ Feature: CMS Transcripts ...@@ -456,7 +456,7 @@ Feature: CMS Transcripts
And I enter a "video_name_1.mp4" source to field number 1 And I enter a "video_name_1.mp4" source to field number 1
And I see status message "not found" 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" Then I see status message "uploaded_successfully"
And I see value "video_name_1" in the field "Transcript (primary)" And I see value "video_name_1" in the field "Transcript (primary)"
...@@ -487,7 +487,7 @@ Feature: CMS Transcripts ...@@ -487,7 +487,7 @@ Feature: CMS Transcripts
And I enter a "video_name_2.webm" source to field number 2 And I enter a "video_name_2.webm" source to field number 2
And I see status message "not found" 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" Then I see status message "uploaded_successfully"
And I see value "video_name_1|video_name_2" in the field "Transcript (primary)" And I see value "video_name_1|video_name_2" in the field "Transcript (primary)"
...@@ -503,7 +503,7 @@ Feature: CMS Transcripts ...@@ -503,7 +503,7 @@ Feature: CMS Transcripts
And I enter a "http://youtu.be/t_not_exist" source to field number 1 And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "not found" Then I see status message "not found"
And I see button "upload_new_timed_transcripts" 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" Then I see status message "uploaded_successfully"
And I save changes And I save changes
...@@ -525,7 +525,7 @@ Feature: CMS Transcripts ...@@ -525,7 +525,7 @@ Feature: CMS Transcripts
Then I see status message "not found" Then I see status message "not found"
And I see button "upload_new_timed_transcripts" 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" Then I see status message "uploaded_successfully"
And I clear field number 1 And I clear field number 1
Then I see status message "found" Then I see status message "found"
...@@ -690,7 +690,7 @@ Feature: CMS Transcripts ...@@ -690,7 +690,7 @@ Feature: CMS Transcripts
And I enter a "video_name_1.1.2.mp4" source to field number 1 And I enter a "video_name_1.1.2.mp4" source to field number 1
And I see status message "not found" 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" Then I see status message "uploaded_successfully"
And I see value "video_name_1.1.2" in the field "Transcript (primary)" 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): ...@@ -195,7 +195,7 @@ def i_enter_a_source(_step, link, index):
def upload_file(_step, file_name): def upload_file(_step, file_name):
path = os.path.join(TEST_ROOT, 'uploads/', file_name.strip()) path = os.path.join(TEST_ROOT, 'uploads/', file_name.strip())
world.browser.execute_script("$('form.file-chooser').show()") 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() world.wait_for_ajax_complete()
......
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
Feature: CMS Video Component Editor Feature: CMS Video Component Editor
As a course author, I want to be able to create video components As a course author, I want to be able to create video components
# 1
Scenario: User can view Video metadata Scenario: User can view Video metadata
Given I have created a Video component Given I have created a Video component
And I edit the component And I edit the component
Then I see the correct video settings and default values Then I see the correct video settings and default values
# 2
# Safari has trouble saving values on Sauce # Safari has trouble saving values on Sauce
@skip_safari @skip_safari
Scenario: User can modify Video display name Scenario: User can modify Video display name
...@@ -16,6 +18,7 @@ Feature: CMS Video Component Editor ...@@ -16,6 +18,7 @@ Feature: CMS Video Component Editor
Then I can modify the display name Then I can modify the display name
And my video display name change is persisted on save And my video display name change is persisted on save
# 3
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are hidden when "transcript display" is false Scenario: Captions are hidden when "transcript display" is false
...@@ -23,9 +26,201 @@ Feature: CMS Video Component Editor ...@@ -23,9 +26,201 @@ Feature: CMS Video Component Editor
And I have set "transcript display" to False And I have set "transcript display" to False
Then when I view the video it does not show the captions Then when I view the video it does not show the captions
# 4
# Sauce Labs cannot delete cookies # Sauce Labs cannot delete cookies
@skip_sauce @skip_sauce
Scenario: Captions are shown when "transcript display" is true Scenario: Captions are shown when "transcript display" is true
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And I have set "transcript display" to True And I have set "transcript display" to True
Then when I view the video it does show the captions 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 # disable missing docstring
# pylint: disable=C0111 # pylint: disable=C0111
import requests
from lettuce import world, step 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 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 (.*)$') @step('I have set "transcript display" to (.*)$')
...@@ -22,9 +119,9 @@ def shows_captions(_step, show_captions): ...@@ -22,9 +119,9 @@ def shows_captions(_step, show_captions):
world.wait_for_js_variable_truthy("Video") world.wait_for_js_variable_truthy("Video")
world.wait(0.5) world.wait(0.5)
if show_captions == 'does not': if show_captions == 'does not':
assert world.is_css_present('div.video.closed') assert_true(world.is_css_present('div.video.closed'))
else: 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 # Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions') world.browser.cookies.delete('hide_captions')
...@@ -68,3 +165,131 @@ def video_name_persisted(step): ...@@ -68,3 +165,131 @@ def video_name_persisted(step):
world.get_setting_entry('Display Name'), world.get_setting_entry('Display Name'),
'Display Name', '3.4', True '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. """ """ Tests for transcripts_utils. """
import unittest import unittest
from uuid import uuid4 from uuid import uuid4
...@@ -548,3 +549,15 @@ class TestTranscript(unittest.TestCase): ...@@ -548,3 +549,15 @@ class TestTranscript(unittest.TestCase):
def test_convert_srt_to_sjson(self): def test_convert_srt_to_sjson(self):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson') 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): ...@@ -143,7 +143,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': self.item_locator, 'locator': self.item_locator,
'file': self.good_srt_file, 'transcript-file': self.good_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -162,7 +162,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -162,7 +162,7 @@ class TestUploadtranscripts(Basetranscripts):
def test_fail_data_without_id(self): def test_fail_data_without_id(self):
link = reverse('upload_transcripts') 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(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "locator" form data.') self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "locator" form data.')
...@@ -178,7 +178,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -178,7 +178,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': 'BAD_LOCATOR', 'locator': 'BAD_LOCATOR',
'file': self.good_srt_file, 'transcript-file': self.good_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -193,7 +193,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -193,7 +193,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR'), 'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR'),
'file': self.good_srt_file, 'transcript-file': self.good_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -222,7 +222,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -222,7 +222,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': item_locator, 'locator': item_locator,
'file': self.good_srt_file, 'transcript-file': self.good_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -240,7 +240,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -240,7 +240,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': self.item_locator, 'locator': self.item_locator,
'file': self.good_srt_file, 'transcript-file': self.good_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -257,7 +257,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -257,7 +257,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': self.item_locator, 'locator': self.item_locator,
'file': self.bad_data_srt_file, 'transcript-file': self.bad_data_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -272,7 +272,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -272,7 +272,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0] filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': self.item_locator, 'locator': self.item_locator,
'file': self.bad_name_srt_file, 'transcript-file': self.bad_name_srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
...@@ -299,7 +299,7 @@ class TestUploadtranscripts(Basetranscripts): ...@@ -299,7 +299,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(srt_file.name))[0] filename = os.path.splitext(os.path.basename(srt_file.name))[0]
resp = self.client.post(link, { resp = self.client.post(link, {
'locator': self.item_locator, 'locator': self.item_locator,
'file': srt_file, 'transcript-file': srt_file,
'video_list': json.dumps([{ 'video_list': json.dumps([{
'type': 'html5', 'type': 'html5',
'video': filename, 'video': filename,
......
...@@ -87,7 +87,7 @@ def upload_transcripts(request): ...@@ -87,7 +87,7 @@ def upload_transcripts(request):
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError): except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
return error_response(response, "Can't find item by locator.") 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.') return error_response(response, 'POST data without "file" form data.')
video_list = request.POST.get('video_list') video_list = request.POST.get('video_list')
...@@ -99,8 +99,8 @@ def upload_transcripts(request): ...@@ -99,8 +99,8 @@ def upload_transcripts(request):
except ValueError: except ValueError:
return error_response(response, 'Invalid video_list JSON.') return error_response(response, 'Invalid video_list JSON.')
source_subs_filedata = request.FILES['file'].read().decode('utf8') source_subs_filedata = request.FILES['transcript-file'].read().decode('utf8')
source_subs_filename = request.FILES['file'].name source_subs_filename = request.FILES['transcript-file'].name
if '.' not in source_subs_filename: if '.' not in source_subs_filename:
return error_response(response, "Undefined file extension.") return error_response(response, "Undefined file extension.")
......
...@@ -203,9 +203,9 @@ define([ ...@@ -203,9 +203,9 @@ define([
"coffee/spec/views/overview_spec", "coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec", "coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
"js/spec/transcripts/utils_spec", "js/spec/transcripts/editor_spec", "js/spec/video/transcripts/utils_spec", "js/spec/video/transcripts/editor_spec",
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec", "js/spec/video/transcripts/videolist_spec", "js/spec/video/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec", "js/spec/video/transcripts/file_uploader_spec",
"js/spec/models/explicit_url_spec" "js/spec/models/explicit_url_spec"
......
...@@ -174,5 +174,6 @@ requirejs.config({ ...@@ -174,5 +174,6 @@ requirejs.config({
jasmine.getFixtures().fixturesPath += 'coffee/fixtures' jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
define([ 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) -> ...@@ -14,31 +14,44 @@ define ["js/models/uploads"], (FileUpload) ->
expect(@model.isValid()).toBeTruthy() expect(@model.isValid()).toBeTruthy()
it "is invalid for text files by default", -> it "is invalid for text files by default", ->
file = {"type": "text/plain"} file = {"type": "text/plain", "name": "filename.txt"}
@model.set("selectedFile", file); @model.set("selectedFile", file);
expect(@model.isValid()).toBeFalsy() expect(@model.isValid()).toBeFalsy()
it "is invalid for PNG files by default", -> it "is invalid for PNG files by default", ->
file = {"type": "image/png"} file = {"type": "image/png", "name": "filename.png"}
@model.set("selectedFile", file); @model.set("selectedFile", file);
expect(@model.isValid()).toBeFalsy() expect(@model.isValid()).toBeFalsy()
it "can accept a file type when explicitly set", -> 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("mimeTypes": ["image/png"])
@model.set("selectedFile", file) @model.set("selectedFile", file)
expect(@model.isValid()).toBeTruthy() 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", -> 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("mimeTypes": ["image/png", "image/jpeg", "image/gif"])
@model.set("selectedFile", file) @model.set("selectedFile", file)
expect(@model.isValid()).toBeTruthy() 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", -> describe "fileTypes", ->
it "returns a list of the uploader's file types", -> it "returns a list of the uploader's file types", ->
@model.set('mimeTypes', ['image/png', 'application/json']) @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", -> describe "formatValidTypes", ->
it "returns a map of formatted file types and extensions", -> 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 ...@@ -16,6 +16,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
@dialogResponse = dialogResponse = [] @dialogResponse = dialogResponse = []
@view = new UploadDialog( @view = new UploadDialog(
model: @model, model: @model,
url: CMS.URL.UPLOAD_ASSET,
onSuccess: (response) => onSuccess: (response) =>
dialogResponse.push(response.response) dialogResponse.push(response.response)
) )
......
...@@ -9,10 +9,11 @@ var FileUpload = Backbone.Model.extend({ ...@@ -9,10 +9,11 @@ var FileUpload = Backbone.Model.extend({
"uploadedBytes": 0, "uploadedBytes": 0,
"totalBytes": 0, "totalBytes": 0,
"finished": false, "finished": false,
"mimeTypes": [] "mimeTypes": [],
"fileFormats": []
}, },
validate: function(attrs, options) { validate: function(attrs, options) {
if(attrs.selectedFile && !_.contains(this.attributes.mimeTypes, attrs.selectedFile.type)) { if(attrs.selectedFile && !this.checkTypeValidity(attrs.selectedFile)) {
return { return {
message: _.template( message: _.template(
gettext("Only <%= fileTypes %> files can be uploaded. Please select a file ending in <%= fileExtensions %> to upload."), 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({ ...@@ -24,17 +25,37 @@ var FileUpload = Backbone.Model.extend({
}, },
// Return a list of this uploader's valid file types // Return a list of this uploader's valid file types
fileTypes: function() { fileTypes: function() {
return _.map( var mimeTypes = _.map(
this.attributes.mimeTypes, this.attributes.mimeTypes,
function(type) { function(type) {
return type.split('/')[1].toUpperCase(); 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 // Return strings for the valid file types and extensions this
// uploader accepts, formatted as natural language // uploader accepts, formatted as natural language
formatValidTypes: function() { formatValidTypes: function() {
if(this.attributes.mimeTypes.length === 1) { var attrs = this.attributes;
if(attrs.mimeTypes.concat(attrs.fileFormats).length === 1) {
return { return {
fileTypes: this.fileTypes()[0], fileTypes: this.fileTypes()[0],
fileExtensions: '.' + this.fileTypes()[0].toLowerCase() fileExtensions: '.' + this.fileTypes()[0].toLowerCase()
......
define( define(
[ [
"jquery", "backbone", "underscore", "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", "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" "jasmine-jquery"
], ],
function ($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCollection, _str) { function ($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCollection, _str) {
......
define( define(
[ [
"jquery", "underscore", "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" "xmodule", "jquery.form", "jasmine-jquery"
], ],
function ($, _, Utils, FileUploader) { function ($, _, Utils, FileUploader) {
describe('Transcripts.FileUploader', function () { describe('Transcripts.FileUploader', function () {
var videoListEntryTemplate = readFixtures( var videoListEntryTemplate = readFixtures(
'transcripts/metadata-videolist-entry.underscore' 'video/transcripts/metadata-videolist-entry.underscore'
), ),
fileUploadTemplate = readFixtures( fileUploadTemplate = readFixtures(
'transcripts/file-upload.underscore' 'video/transcripts/file-upload.underscore'
), ),
view; view;
......
define( define(
[ [
"jquery", "underscore", "jquery", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/message_manager", "js/views/video/transcripts/utils", "js/views/video/transcripts/message_manager",
"js/views/transcripts/file_uploader", "sinon", "jasmine-jquery", "js/views/video/transcripts/file_uploader", "sinon", "jasmine-jquery",
"xmodule" "xmodule"
], ],
function ($, _, Utils, MessageManager, FileUploader, sinon) { function ($, _, Utils, MessageManager, FileUploader, sinon) {
describe('Transcripts.MessageManager', function () { describe('Transcripts.MessageManager', function () {
var videoListEntryTemplate = readFixtures( var videoListEntryTemplate = readFixtures(
'transcripts/metadata-videolist-entry.underscore' 'video/transcripts/metadata-videolist-entry.underscore'
), ),
foundTemplate = readFixtures( foundTemplate = readFixtures(
'transcripts/messages/transcripts-found.underscore' 'video/transcripts/messages/transcripts-found.underscore'
), ),
handlers = { handlers = {
importHandler: ['replace', 'Error: Import failed.'], importHandler: ['replace', 'Error: Import failed.'],
......
define( define(
[ [
"jquery", "underscore", "jquery", "underscore",
"js/views/transcripts/utils", "js/views/video/transcripts/utils",
"underscore.string", "xmodule", "jasmine-jquery" "underscore.string", "xmodule", "jasmine-jquery"
], ],
function ($, _, Utils, _str) { function ($, _, Utils, _str) {
......
define( define(
[ [
"jquery", "underscore", "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", "js/views/metadata", "js/models/metadata", "js/views/abstract_editor",
"sinon", "xmodule", "jasmine-jquery" "sinon", "xmodule", "jasmine-jquery"
], ],
function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, sinon) { function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, sinon) {
describe('CMS.Views.Metadata.VideoList', function () { describe('CMS.Views.Metadata.VideoList', function () {
var videoListEntryTemplate = readFixtures( var videoListEntryTemplate = readFixtures(
'transcripts/metadata-videolist-entry.underscore' 'video/transcripts/metadata-videolist-entry.underscore'
), ),
abstractEditor = AbstractEditor.prototype, abstractEditor = AbstractEditor.prototype,
component_locator = 'component_locator', component_locator = 'component_locator',
......
...@@ -5,7 +5,9 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon" ...@@ -5,7 +5,9 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon"
var request = requests[requests.length - 1]; var request = requests[requests.length - 1];
expect(request.url).toEqual("/xblock"); expect(request.url).toEqual("/xblock");
expect(request.method).toEqual("POST"); 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) { var verifyComponents = function (unit, locators) {
......
define( define(
[ [
"js/views/baseview", "underscore", "js/models/metadata", "js/views/abstract_editor", "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 = {}; var Metadata = {};
Metadata.Editor = BaseView.extend({ Metadata.Editor = BaseView.extend({
...@@ -82,6 +83,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) { ...@@ -82,6 +83,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
}); });
Metadata.VideoList = VideoList; Metadata.VideoList = VideoList;
Metadata.VideoTranslations = VideoTranslations;
Metadata.String = AbstractEditor.extend({ Metadata.String = AbstractEditor.extend({
......
...@@ -15,7 +15,7 @@ var UploadDialog = BaseView.extend({ ...@@ -15,7 +15,7 @@ var UploadDialog = BaseView.extend({
var oldInput = this.$("input[type=file]").get(0); var oldInput = this.$("input[type=file]").get(0);
this.$el.html(this.template({ this.$el.html(this.template({
shown: this.options.shown, shown: this.options.shown,
url: CMS.URL.UPLOAD_ASSET, url: this.options.url || CMS.URL.UPLOAD_ASSET,
title: this.model.escape('title'), title: this.model.escape('title'),
message: this.model.escape('message'), message: this.model.escape('message'),
selectedFile: selectedFile, selectedFile: selectedFile,
......
define( define(
[ [
"jquery", "backbone", "underscore", "jquery", "backbone", "underscore",
"js/views/transcripts/utils", "js/views/video/transcripts/utils",
"js/views/metadata", "js/collections/metadata", "js/views/metadata", "js/collections/metadata",
"js/views/transcripts/metadata_videolist" "js/views/video/transcripts/metadata_videolist"
], ],
function($, Backbone, _, Utils, MetadataView, MetadataCollection) { function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
......
define( define(
[ [
"jquery", "backbone", "underscore", "jquery", "backbone", "underscore",
"js/views/transcripts/utils" "js/views/video/transcripts/utils"
], ],
function($, Backbone, _, Utils) { function($, Backbone, _, Utils) {
var FileUploader = Backbone.View.extend({ var FileUploader = Backbone.View.extend({
......
define( define(
[ [
"jquery", "backbone", "underscore", "jquery", "backbone", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/file_uploader", "js/views/video/transcripts/utils", "js/views/video/transcripts/file_uploader",
"gettext" "gettext"
], ],
function($, Backbone, _, Utils, FileUploader, gettext) { function($, Backbone, _, Utils, FileUploader, gettext) {
......
define( define(
[ [
"jquery", "backbone", "underscore", "js/views/abstract_editor", "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" "js/views/metadata"
], ],
function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) { function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
VideoList = AbstractEditor.extend({ var VideoList = AbstractEditor.extend({
// Time that we wait since the last time user typed. // Time that we wait since the last time user typed.
inputDelay: 300, 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 @@ ...@@ -2,22 +2,21 @@
// ==================== // ====================
// general - display mode (xblock-student_view or xmodule_display) // general - display mode (xblock-student_view or xmodule_display)
.xmodule_display, .xblock-student_view { .xmodule_display,
.xblock-student_view {
// font styling // font styling
i, em { i,
em {
font-style: italic; font-style: italic;
} }
} }
// ==================== // ====================
// Video Alpha // Video
.xmodule_VideoModule { .xmodule_VideoModule {
// display mode // display mode
&.xblock-student_view { &.xblock-student_view {
// full screen // full screen
.video-controls .add-fullscreen { .video-controls .add-fullscreen {
display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
...@@ -35,16 +34,15 @@ ...@@ -35,16 +34,15 @@
} }
.xmodule_VideoDescriptor { .xmodule_VideoDescriptor {
.wrapper-comp-settings.basic_metadata_edit{ .wrapper-comp-settings.basic_metadata_edit {
.list-input.settings-list { .list-input.settings-list {
.field.comp-setting-entry { .field.comp-setting-entry {
.setting-label { .setting-label {
vertical-align: top; vertical-align: top;
margin-top: ($baseline/2); margin-top: ($baseline/2);
} }
.setting-help{ .setting-help {
display: block; display: block;
width: 45%; width: 45%;
max-width: auto; max-width: auto;
...@@ -63,18 +61,19 @@ ...@@ -63,18 +61,19 @@
margin-right: ($baseline/4); margin-right: ($baseline/4);
} }
} }
.videolist-url-tip.setting-help, .videolist-url-tip.setting-help,
.videolist-extra-videos-tip.setting-help{ .videolist-extra-videos-tip.setting-help {
margin-left: 0; margin-left: 0;
width: 100%; width: 100%;
padding: 0 10px 10px; padding: 0 10px 10px;
} }
.videolist-url-tip.setting-help{ .videolist-url-tip.setting-help {
padding: 0 0 10px; padding: 0 0 10px;
} }
.wrapper-comp-setting{ .wrapper-comp-setting {
width: 100%; width: 100%;
display: block; display: block;
max-width: auto; max-width: auto;
...@@ -86,23 +85,22 @@ ...@@ -86,23 +85,22 @@
display: inline-block; display: inline-block;
min-width: ($baseline*5); min-width: ($baseline*5);
// inputs // inputs
.input { .input {
width: 100%; width: 100%;
vertical-align: middle; vertical-align: middle;
&.is-disabled, &.is-disabled,
&[disabled="disabled"]{ [disabled="disabled"] {
opacity: .5; opacity: .5;
} }
} }
.wrapper-videolist-url{ .wrapper-videolist-url {
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
} }
.wrapper-videolist-urls{ .wrapper-videolist-urls {
background: $lightGrey; background: $lightGrey;
padding: ($baseline/3); padding: ($baseline/3);
...@@ -110,7 +108,7 @@ ...@@ -110,7 +108,7 @@
.videolist-extra-videos { .videolist-extra-videos {
display: none; display: none;
&.is-visible{ &.is-visible {
display: block; display: block;
} }
...@@ -122,58 +120,59 @@ ...@@ -122,58 +120,59 @@
} }
} }
} }
.transcripts-status{
.transcripts-status {
margin-top: $baseline; margin-top: $baseline;
&.is-invisible{ &.is-invisible {
display: none !important; display: none !important;
} }
.wrapper-transcripts-message{ .wrapper-transcripts-message {
width: 60%; width: 60%;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
min-width: ($baseline*5); min-width: ($baseline*5);
margin-top: 10px; margin-top: 10px;
.transcripts-message{ .transcripts-message {
@include font-size(12); @include font-size(12);
} }
.transcripts-message-status{ .transcripts-message-status {
color: $green; color: $green;
font-weight: 700; font-weight: 700;
&.status-error{ &.status-error {
color: $red; color: $red;
} }
[class^="icon-"], [class^="icon-"],
[class*=" icon-"]{ [class*=" icon-"] {
margin-right: 5px; margin-right: 5px;
@include font-size(18); @include font-size(18);
} }
} }
.transcripts-error-message{ .transcripts-error-message {
background: $red; background: $red;
color: $white; color: $white;
@include font-size(14); @include font-size(14);
padding: ($baseline/3); padding: ($baseline/3);
&.is-invisible{ &.is-invisible {
display: none; display: none;
} }
} }
.wrapper-transcripts-buttons{ .wrapper-transcripts-buttons {
&.is-invisible {
&.is-invisible{
display: none; display: none;
} }
} }
} }
.action{
.action {
@extend %btn-primary-blue; @extend %btn-primary-blue;
@extend %t-action3; @extend %t-action3;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
...@@ -187,11 +186,11 @@ ...@@ -187,11 +186,11 @@
} }
} }
.file-chooser{ .file-chooser {
display: none; display: none;
} }
.progress-bar{ .progress-bar {
display: block; display: block;
height: 30px; height: 30px;
margin: 10px 0; margin: 10px 0;
...@@ -221,4 +220,114 @@ ...@@ -221,4 +220,114 @@
} }
} }
} }
.wrapper-comp-settings {
// TYPE: VideoTranslations
.list-input.settings-list {
.metadata-video-translations {
* {
@include box-sizing(border-box);
}
// label
.setting-label {
vertical-align: top;
margin-top: ($baseline*.25);
}
// inputs and labels
.wrapper-translations-settings {
width: 45%;
display: inline-block;
min-width: 240px;
// enumerated fields
.list-settings {
margin: 0;
.list-settings-item {
margin-bottom: ($baseline/2);
select {
width: 80%;
margin-right: ($baseline/2);
}
.list-settings-buttons {
@extend %cont-truncated;
padding: ($baseline/2) 0;
border-bottom: 1px solid $gray-l4;
}
}
// inputs
.input {
width: 43%;
margin-right: ($baseline/4);
vertical-align: middle;
display: inline-block;
&.input-value {
margin-right: ($baseline/2);
}
}
}
}
.setting-clear.action {
vertical-align: top;
margin: ($baseline*.25) ($baseline*.5) 0;
}
.create-setting {
@extend %ui-btn-flat-outline;
@extend %t-action3;
display: block;
padding: ($baseline/2);
font-weight: 600;
*[class^="icon-"] {
margin-right: ($baseline/4);
}
}
.upload-setting {
@extend %ui-btn-flat-outline;
@extend %t-action3;
display: inline-block;
padding: ($baseline/2);
font-weight: 600;
width: 49%;
margin-right: 2%;
}
.download-setting {
@extend %ui-btn-non;
@extend %t-action4;
display: inline-block;
padding: ($baseline/2);
font-weight: 600;
width: 49%;
text-align: center;
color: $blue;
&:hover {
background-color: $blue;
}
}
.remove-setting {
@include transition(color .25s ease-in-out);
@include font-size(20);
display: inline-block;
background: transparent;
color: $blue-l3;
&:hover {
color: $blue;
}
}
}
}
}
} }
...@@ -824,11 +824,6 @@ body.course.unit,.view-unit { ...@@ -824,11 +824,6 @@ body.course.unit,.view-unit {
} }
} }
// actions
.create-action, .remove-action, .setting-clear {
}
.setting-clear { .setting-clear {
vertical-align: top; vertical-align: top;
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -845,11 +840,6 @@ body.course.unit,.view-unit { ...@@ -845,11 +840,6 @@ body.course.unit,.view-unit {
*[class^="icon-"] { *[class^="icon-"] {
margin-right: ($baseline/4); margin-right: ($baseline/4);
} }
// STATE: disabled
&.is-disabled {
}
} }
.remove-setting { .remove-setting {
...@@ -862,11 +852,6 @@ body.course.unit,.view-unit { ...@@ -862,11 +852,6 @@ body.course.unit,.view-unit {
&:hover { &:hover {
color: $blue; 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 @@ ...@@ -3,7 +3,7 @@
</div> </div>
<form class="file-chooser" action="/transcripts/upload" <form class="file-chooser" action="/transcripts/upload"
method="post" enctype="multipart/form-data"> 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(', ') %>"> accept="<%= _.map(ext, function(val){ return '.' + val; }).join(', ') %>">
<input type="hidden" name="locator" value="<%= component_locator %>"> <input type="hidden" name="locator" value="<%= component_locator %>">
<input type="hidden" name="video_list" value='<%= JSON.stringify(video_list) %>'> <input type="hidden" name="video_list" value='<%= JSON.stringify(video_list) %>'>
......
...@@ -7,7 +7,7 @@ from xmodule.modulestore.django import loc_mapper ...@@ -7,7 +7,7 @@ from xmodule.modulestore.django import loc_mapper
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%namespace name="units" file="widgets/units.html" /> <%namespace name="units" file="widgets/units.html" />
<%block name="title">${_("Individual Unit")}</%block> <%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"> <%block name="jsextra">
<script type='text/javascript'> <script type='text/javascript'>
...@@ -38,6 +38,9 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -38,6 +38,9 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
<script type="text/template" id="image-modal-tpl"> <script type="text/template" id="image-modal-tpl">
<%static:include path="js/imageModal.underscore" /> <%static:include path="js/imageModal.underscore" />
</script> </script>
<script type="text/template" id="upload-dialog-tpl">
<%static:include path="js/upload-dialog.underscore" />
</script>
</%block> </%block>
<%block name="content"> <%block name="content">
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
<script id="metadata-editor-tpl" type="text/template"> <script id="metadata-editor-tpl" type="text/template">
<%static:include path="js/metadata-editor.underscore" /> <%static:include path="js/metadata-editor.underscore" />
</script> </script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]: % 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"> <script id="${template_name}" type="text/template">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
......
...@@ -10,13 +10,18 @@ import json ...@@ -10,13 +10,18 @@ import json
% for template_name in ["metadata-videolist-entry", "file-upload"]: % for template_name in ["metadata-videolist-entry", "file-upload"]:
<script type="text/template" id="${template_name}"> <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> </script>
% endfor % endfor
% for template_name in ["transcripts-found", "transcripts-uploaded", "transcripts-use-existing", "transcripts-not-found", "transcripts-replace", "transcripts-import", "transcripts-choose"]: % 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}"> <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> </script>
% endfor % endfor
...@@ -27,7 +32,7 @@ import json ...@@ -27,7 +32,7 @@ import json
[ [
"domReady!", "domReady!",
"jquery", "jquery",
"js/views/transcripts/editor" "js/views/video/transcripts/editor"
], ],
function(doc, $, Editor) { function(doc, $, Editor) {
......
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
} }
}; };
} }
} else if (settings.url == '/transcript/translation') { } else if (settings.url.match(/transcript\/translation\/.+$/)) {
return settings.success(jasmine.stubbedCaption); return settings.success(jasmine.stubbedCaption);
} else if (settings.url == '/transcript/available_translations') { } else if (settings.url == '/transcript/available_translations') {
return settings.success(['uk', 'de']); return settings.success(['uk', 'de']);
......
...@@ -60,16 +60,14 @@ ...@@ -60,16 +60,14 @@
runs(function () { runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({ expect($.ajaxWithPrefix).toHaveBeenCalledWith({
url: '/transcript/translation', url: '/transcript/translation/en',
notifyOnError: false, notifyOnError: false,
data: jasmine.any(Object), data: void(0),
success: jasmine.any(Function), success: jasmine.any(Function),
error: jasmine.any(Function) error: jasmine.any(Function)
}); });
expect($.ajaxWithPrefix.mostRecentCall.args[0].data) expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({ .toBeUndefined();
language: 'en'
});
}); });
}); });
...@@ -86,7 +84,7 @@ ...@@ -86,7 +84,7 @@
runs(function () { runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({ expect($.ajaxWithPrefix).toHaveBeenCalledWith({
url: '/transcript/translation', url: '/transcript/translation/en',
notifyOnError: false, notifyOnError: false,
data: jasmine.any(Object), data: jasmine.any(Object),
success: jasmine.any(Function), success: jasmine.any(Function),
...@@ -94,7 +92,6 @@ ...@@ -94,7 +92,6 @@
}); });
expect($.ajaxWithPrefix.mostRecentCall.args[0].data) expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({ .toEqual({
language: 'en',
videoId: 'abcdefghijkl' videoId: 'abcdefghijkl'
}); });
}); });
...@@ -111,7 +108,7 @@ ...@@ -111,7 +108,7 @@
runs(function () { runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({ expect($.ajaxWithPrefix).toHaveBeenCalledWith({
url: '/transcript/translation', url: '/transcript/translation/en',
notifyOnError: false, notifyOnError: false,
data: jasmine.any(Object), data: jasmine.any(Object),
success: jasmine.any(Function), success: jasmine.any(Function),
...@@ -119,7 +116,6 @@ ...@@ -119,7 +116,6 @@
}); });
expect($.ajaxWithPrefix.mostRecentCall.args[0].data) expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({ .toEqual({
language: 'en',
videoId: 'cogebirgzzM' videoId: 'cogebirgzzM'
}); });
}); });
......
...@@ -226,9 +226,8 @@ function () { ...@@ -226,9 +226,8 @@ function () {
function fetchCaption() { function fetchCaption() {
var self = this, var self = this,
Caption = self.videoCaption, Caption = self.videoCaption,
data = { language = this.getCurrentLanguage(),
language: this.getCurrentLanguage() data;
};
if (Caption.loaded) { if (Caption.loaded) {
Caption.hideCaptions(false); Caption.hideCaptions(false);
...@@ -241,13 +240,15 @@ function () { ...@@ -241,13 +240,15 @@ function () {
} }
if (this.videoType === 'youtube') { 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 // Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button // occurred, then we hide the captions panel, and the "CC" button
Caption.fetchXHR = $.ajaxWithPrefix({ Caption.fetchXHR = $.ajaxWithPrefix({
url: self.config.transcriptTranslationUrl, url: self.config.transcriptTranslationUrl + '/' + language,
notifyOnError: false, notifyOnError: false,
data: data, data: data,
success: function (captions) { success: function (captions) {
......
...@@ -132,22 +132,6 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -132,22 +132,6 @@ class VideoDescriptorTest(unittest.TestCase):
field_data=DictFieldData({}), 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): def test_create_youtube_string(self):
""" """
Test that Youtube ID strings are correctly created when writing Test that Youtube ID strings are correctly created when writing
......
...@@ -64,6 +64,17 @@ def generate_subs(speed, source_speed, source_subs): ...@@ -64,6 +64,17 @@ def generate_subs(speed, source_speed, source_subs):
return 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'): def save_subs_to_store(subs, subs_id, item, language='en'):
""" """
Save transcripts into `StaticContent`. Save transcripts into `StaticContent`.
...@@ -76,13 +87,8 @@ def save_subs_to_store(subs, subs_id, item, language='en'): ...@@ -76,13 +87,8 @@ def save_subs_to_store(subs, subs_id, item, language='en'):
Returns: location of saved subtitles. Returns: location of saved subtitles.
""" """
filedata = json.dumps(subs, indent=2) filedata = json.dumps(subs, indent=2)
mime_type = 'application/json'
filename = subs_filename(subs_id, language) filename = subs_filename(subs_id, language)
content_location = Transcript.asset_location(item.location, filename) return save_to_store(filedata, filename, 'application/json', item.location)
content = StaticContent(content_location, filename, mime_type, filedata)
contentstore().save(content)
return content_location
def get_transcripts_from_youtube(youtube_id, settings, i18n): def get_transcripts_from_youtube(youtube_id, settings, i18n):
""" """
...@@ -193,12 +199,8 @@ def remove_subs_from_store(subs_id, item, lang='en'): ...@@ -193,12 +199,8 @@ def remove_subs_from_store(subs_id, item, lang='en'):
""" """
Remove from store, if transcripts content exists. Remove from store, if transcripts content exists.
""" """
try: filename = subs_filename(subs_id, lang)
content = Transcript.asset(item.location, subs_id, lang) Transcript.delete_asset(item.location, filename)
contentstore().delete(content.get_id())
log.info("Removed subs %s from store", subs_id)
except NotFoundError:
pass
def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item, language='en'): 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 ...@@ -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). a new version of the SRT file with same name).
""" """
_ = item.runtime.service(item, "i18n").ugettext
# 1. # 1.
html5_ids = get_html5_ids(item.html5_sources) html5_ids = get_html5_ids(item.html5_sources)
possible_video_id_list = [item.youtube_id_1_0] + html5_ids possible_video_id_list = [item.youtube_id_1_0] + html5_ids
...@@ -408,10 +412,9 @@ def subs_filename(subs_id, lang='en'): ...@@ -408,10 +412,9 @@ def subs_filename(subs_id, lang='en'):
Generate proper filename for storage. Generate proper filename for storage.
""" """
if lang == 'en': if lang == 'en':
return 'subs_{0}.srt.sjson'.format(subs_id) return u'subs_{0}.srt.sjson'.format(subs_id)
else: 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): 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): ...@@ -420,10 +423,15 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
`item` is module object. `item` is module object.
""" """
_ = item.runtime.service(item, "i18n").ugettext
try: 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: 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: if not lang:
lang = item.transcript_language lang = item.transcript_language
...@@ -431,7 +439,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang): ...@@ -431,7 +439,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
generate_subs_from_source( generate_subs_from_source(
result_subs_dict, result_subs_dict,
os.path.splitext(user_filename)[1][1:], os.path.splitext(user_filename)[1][1:],
srt_transcript.data.decode('utf8'), srt_transcripts.data.decode('utf8'),
item, item,
lang lang
) )
...@@ -464,6 +472,11 @@ class Transcript(object): ...@@ -464,6 +472,11 @@ class Transcript(object):
""" """
Container for transcript methods. Container for transcript methods.
""" """
mime_types = {
'srt': 'application/x-subrip; charset=utf-8',
'txt': 'text/plain; charset=utf-8',
'sjson': 'application/json',
}
@staticmethod @staticmethod
def convert(content, input_format, output_format): def convert(content, input_format, output_format):
...@@ -504,21 +517,32 @@ class Transcript(object): ...@@ -504,21 +517,32 @@ class Transcript(object):
`location` is module location. `location` is module location.
""" """
return contentstore().find( asset_filename = subs_filename(subs_id, lang) if not filename else filename
Transcript.asset_location( return Transcript.get_asset(location, asset_filename)
location,
subs_filename(subs_id, lang) if not filename else filename
)
)
@staticmethod
def get_asset(location, filename):
"""
Return asset by location and filename.
"""
return contentstore().find(Transcript.asset_location(location, filename))
@staticmethod @staticmethod
def asset_location(location, filename): 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( Delete asset by location and filename.
location.org, location.course, 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
"""
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): ...@@ -51,7 +51,7 @@ def setUp(scenario):
world.video_sequences = {} world.video_sequences = {}
class ReuqestHandlerWithSessionId(object): class RequestHandlerWithSessionId(object):
def get(self, url): def get(self, url):
""" """
Sends a request. Sends a request.
...@@ -449,7 +449,6 @@ def select_language(_step, code): ...@@ -449,7 +449,6 @@ def select_language(_step, code):
def click_button(_step, button): def click_button(_step, button):
world.css_click(VIDEO_BUTTONS[button]) world.css_click(VIDEO_BUTTONS[button])
@step('I see video starts playing from "([^"]*)" position$') @step('I see video starts playing from "([^"]*)" position$')
def start_playing_video_from_n_seconds(_step, position): def start_playing_video_from_n_seconds(_step, position):
world.wait_for( world.wait_for(
...@@ -522,7 +521,7 @@ def i_can_download_transcript(_step, format, text): ...@@ -522,7 +521,7 @@ def i_can_download_transcript(_step, format, text):
} }
url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href'] url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href']
request = ReuqestHandlerWithSessionId() request = RequestHandlerWithSessionId()
assert request.get(url).is_success() assert request.get(url).is_success()
assert request.check_header('content-type', formats[format]) assert request.check_header('content-type', formats[format])
assert (text.encode('utf-8') in request.content) assert (text.encode('utf-8') in request.content)
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Video xmodule tests in mongo.""" """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 . import BaseTestXmodule
from .test_video_xml import SOURCE_XML from .test_video_xml import SOURCE_XML
from .test_video_handlers import TestVideo from .test_video_handlers import TestVideo
from django.conf import settings
from xmodule.video_module import create_youtube_string
class TestVideoYouTube(TestVideo): class TestVideoYouTube(TestVideo):
...@@ -46,11 +55,11 @@ class TestVideoYouTube(TestVideo): ...@@ -46,11 +55,11 @@ class TestVideoYouTube(TestVideo):
'transcript_language': u'en', 'transcript_language': u'en',
'transcript_languages': '{"en": "English", "uk": "Ukrainian"}', 'transcript_languages': '{"en": "English", "uk": "Ukrainian"}',
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'translation'
).rstrip('/?') + '/translation', ).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?') + '/available_translations', ).rstrip('/?'),
} }
self.assertEqual( self.assertEqual(
...@@ -112,11 +121,11 @@ class TestVideoNonYouTube(TestVideo): ...@@ -112,11 +121,11 @@ class TestVideoNonYouTube(TestVideo):
'transcript_language': u'en', 'transcript_language': u'en',
'transcript_languages': '{"en": "English"}', 'transcript_languages': '{"en": "English"}',
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'translation'
).rstrip('/?') + '/translation', ).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?') + '/available_translations', ).rstrip('/?')
} }
self.assertEqual( self.assertEqual(
...@@ -223,8 +232,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -223,8 +232,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.initialize_module(data=DATA) self.initialize_module(data=DATA)
track_url = self.item_descriptor.xmodule_runtime.handler_url( track_url = self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'download'
).rstrip('/?') + '/download' ).rstrip('/?')
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
...@@ -233,11 +242,11 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -233,11 +242,11 @@ class TestGetHtmlMethod(BaseTestXmodule):
'transcript_languages': '{"en": "English"}' if not data['transcripts'] else '{"uk": "Ukrainian"}', '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_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'translation'
).rstrip('/?') + '/translation', ).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?') + '/available_translations', ).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', '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'], 'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
'sub': data['sub'], 'sub': data['sub'],
...@@ -345,11 +354,11 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -345,11 +354,11 @@ class TestGetHtmlMethod(BaseTestXmodule):
expected_context.update({ expected_context.update({
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'translation'
).rstrip('/?') + '/translation', ).rstrip('/?'),
'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript' self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?') + '/available_translations', ).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'sources': data['result'], 'sources': data['result'],
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
...@@ -399,9 +408,11 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -399,9 +408,11 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
self.assertTrue(self.item_descriptor.download_video) self.assertTrue(self.item_descriptor.download_video)
self.assertFalse(self.item_descriptor.source_visible) 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):
def test_download_video_is_explicitly_set(self, mock_editable_fields): with patch(
mock_editable_fields.return_value = { 'xmodule.editing_module.TabsEditingDescriptor.editable_metadata_fields',
new_callable=PropertyMock,
return_value={
'download_video': { 'download_video': {
'default_value': False, 'default_value': False,
'explicitly_set': True, 'explicitly_set': True,
...@@ -454,8 +465,10 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -454,8 +465,10 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
'value': False, 'value': False,
'field_name': 'download_track', 'field_name': 'download_track',
'options': [], 'options': [],
},
'transcripts': {},
} }
} ):
metadata = { metadata = {
'track': u'http://some_track.srt', 'track': u'http://some_track.srt',
'source': 'http://example.org/video.mp4', 'source': 'http://example.org/video.mp4',
...@@ -463,9 +476,10 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -463,9 +476,10 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
} }
self.initialize_module(metadata=metadata) self.initialize_module(metadata=metadata)
fields = self.item_descriptor.editable_metadata_fields
fields = self.item_descriptor.editable_metadata_fields
self.assertIn('source', fields) self.assertIn('source', fields)
self.assertFalse(self.item_descriptor.download_video) self.assertFalse(self.item_descriptor.download_video)
self.assertTrue(self.item_descriptor.source_visible) self.assertTrue(self.item_descriptor.source_visible)
self.assertTrue(self.item_descriptor.download_track) self.assertTrue(self.item_descriptor.download_track)
...@@ -481,3 +495,40 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -481,3 +495,40 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
self.assertNotIn('source', fields) self.assertNotIn('source', fields)
self.assertFalse(self.item_descriptor.download_video) 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 = ( ...@@ -1326,19 +1326,19 @@ ALL_LANGUAGES = (
[u"br", u"Breton"], [u"br", u"Breton"],
[u"bg", u"Bulgarian"], [u"bg", u"Bulgarian"],
[u"my", u"Burmese"], [u"my", u"Burmese"],
[u"ca", u"Catalan; Valencian"], [u"ca", u"Catalan"],
[u"ch", u"Chamorro"], [u"ch", u"Chamorro"],
[u"ce", u"Chechen"], [u"ce", u"Chechen"],
[u"zh", u"Chinese"], [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"cv", u"Chuvash"],
[u"kw", u"Cornish"], [u"kw", u"Cornish"],
[u"co", u"Corsican"], [u"co", u"Corsican"],
[u"cr", u"Cree"], [u"cr", u"Cree"],
[u"cs", u"Czech"], [u"cs", u"Czech"],
[u"da", u"Danish"], [u"da", u"Danish"],
[u"dv", u"Divehi; Dhivehi; Maldivian"], [u"dv", u"Divehi"],
[u"nl", u"Dutch; Flemish"], [u"nl", u"Dutch"],
[u"dz", u"Dzongkha"], [u"dz", u"Dzongkha"],
[u"en", u"English"], [u"en", u"English"],
[u"eo", u"Esperanto"], [u"eo", u"Esperanto"],
...@@ -1352,14 +1352,14 @@ ALL_LANGUAGES = ( ...@@ -1352,14 +1352,14 @@ ALL_LANGUAGES = (
[u"ff", u"Fulah"], [u"ff", u"Fulah"],
[u"ka", u"Georgian"], [u"ka", u"Georgian"],
[u"de", u"German"], [u"de", u"German"],
[u"gd", u"Gaelic; Scottish Gaelic"], [u"gd", u"Gaelic"],
[u"ga", u"Irish"], [u"ga", u"Irish"],
[u"gl", u"Galician"], [u"gl", u"Galician"],
[u"gv", u"Manx"], [u"gv", u"Manx"],
[u"el", u"Greek, Modern (1453-)"], [u"el", u"Greek"],
[u"gn", u"Guarani"], [u"gn", u"Guarani"],
[u"gu", u"Gujarati"], [u"gu", u"Gujarati"],
[u"ht", u"Haitian; Haitian Creole"], [u"ht", u"Haitian"],
[u"ha", u"Hausa"], [u"ha", u"Hausa"],
[u"he", u"Hebrew"], [u"he", u"Hebrew"],
[u"hz", u"Herero"], [u"hz", u"Herero"],
...@@ -1370,36 +1370,36 @@ ALL_LANGUAGES = ( ...@@ -1370,36 +1370,36 @@ ALL_LANGUAGES = (
[u"ig", u"Igbo"], [u"ig", u"Igbo"],
[u"is", u"Icelandic"], [u"is", u"Icelandic"],
[u"io", u"Ido"], [u"io", u"Ido"],
[u"ii", u"Sichuan Yi; Nuosu"], [u"ii", u"Sichuan Yi"],
[u"iu", u"Inuktitut"], [u"iu", u"Inuktitut"],
[u"ie", u"Interlingue; Occidental"], [u"ie", u"Interlingue"],
[u"ia", u"Interlingua (International Auxiliary Language Association)"], [u"ia", u"Interlingua"],
[u"id", u"Indonesian"], [u"id", u"Indonesian"],
[u"ik", u"Inupiaq"], [u"ik", u"Inupiaq"],
[u"it", u"Italian"], [u"it", u"Italian"],
[u"jv", u"Javanese"], [u"jv", u"Javanese"],
[u"ja", u"Japanese"], [u"ja", u"Japanese"],
[u"kl", u"Kalaallisut; Greenlandic"], [u"kl", u"Kalaallisut"],
[u"kn", u"Kannada"], [u"kn", u"Kannada"],
[u"ks", u"Kashmiri"], [u"ks", u"Kashmiri"],
[u"kr", u"Kanuri"], [u"kr", u"Kanuri"],
[u"kk", u"Kazakh"], [u"kk", u"Kazakh"],
[u"km", u"Central Khmer"], [u"km", u"Central Khmer"],
[u"ki", u"Kikuyu; Gikuyu"], [u"ki", u"Kikuyu"],
[u"rw", u"Kinyarwanda"], [u"rw", u"Kinyarwanda"],
[u"ky", u"Kirghiz; Kyrgyz"], [u"ky", u"Kirghiz"],
[u"kv", u"Komi"], [u"kv", u"Komi"],
[u"kg", u"Kongo"], [u"kg", u"Kongo"],
[u"ko", u"Korean"], [u"ko", u"Korean"],
[u"kj", u"Kuanyama; Kwanyama"], [u"kj", u"Kuanyama"],
[u"ku", u"Kurdish"], [u"ku", u"Kurdish"],
[u"lo", u"Lao"], [u"lo", u"Lao"],
[u"la", u"Latin"], [u"la", u"Latin"],
[u"lv", u"Latvian"], [u"lv", u"Latvian"],
[u"li", u"Limburgan; Limburger; Limburgish"], [u"li", u"Limburgan"],
[u"ln", u"Lingala"], [u"ln", u"Lingala"],
[u"lt", u"Lithuanian"], [u"lt", u"Lithuanian"],
[u"lb", u"Luxembourgish; Letzeburgesch"], [u"lb", u"Luxembourgish"],
[u"lu", u"Luba-Katanga"], [u"lu", u"Luba-Katanga"],
[u"lg", u"Ganda"], [u"lg", u"Ganda"],
[u"mk", u"Macedonian"], [u"mk", u"Macedonian"],
...@@ -1412,34 +1412,34 @@ ALL_LANGUAGES = ( ...@@ -1412,34 +1412,34 @@ ALL_LANGUAGES = (
[u"mt", u"Maltese"], [u"mt", u"Maltese"],
[u"mn", u"Mongolian"], [u"mn", u"Mongolian"],
[u"na", u"Nauru"], [u"na", u"Nauru"],
[u"nv", u"Navajo; Navaho"], [u"nv", u"Navajo"],
[u"nr", u"Ndebele, South; South Ndebele"], [u"nr", u"Ndebele, South"],
[u"nd", u"Ndebele, North; North Ndebele"], [u"nd", u"Ndebele, North"],
[u"ng", u"Ndonga"], [u"ng", u"Ndonga"],
[u"ne", u"Nepali"], [u"ne", u"Nepali"],
[u"nn", u"Norwegian Nynorsk; Nynorsk, Norwegian"], [u"nn", u"Norwegian Nynorsk"],
[u"nb", u"Bokmål, Norwegian; Norwegian Bokmål"], [u"nb", u"Bokmål, Norwegian"],
[u"no", u"Norwegian"], [u"no", u"Norwegian"],
[u"ny", u"Chichewa; Chewa; Nyanja"], [u"ny", u"Chichewa"],
[u"oc", u"Occitan (post 1500); Provençal"], [u"oc", u"Occitan"],
[u"oj", u"Ojibwa"], [u"oj", u"Ojibwa"],
[u"or", u"Oriya"], [u"or", u"Oriya"],
[u"om", u"Oromo"], [u"om", u"Oromo"],
[u"os", u"Ossetian; Ossetic"], [u"os", u"Ossetian"],
[u"pa", u"Panjabi; Punjabi"], [u"pa", u"Panjabi"],
[u"fa", u"Persian"], [u"fa", u"Persian"],
[u"pi", u"Pali"], [u"pi", u"Pali"],
[u"pl", u"Polish"], [u"pl", u"Polish"],
[u"pt", u"Portuguese"], [u"pt", u"Portuguese"],
[u"ps", u"Pushto; Pashto"], [u"ps", u"Pushto"],
[u"qu", u"Quechua"], [u"qu", u"Quechua"],
[u"rm", u"Romansh"], [u"rm", u"Romansh"],
[u"ro", u"Romanian; Moldavian; Moldovan"], [u"ro", u"Romanian"],
[u"rn", u"Rundi"], [u"rn", u"Rundi"],
[u"ru", u"Russian"], [u"ru", u"Russian"],
[u"sg", u"Sango"], [u"sg", u"Sango"],
[u"sa", u"Sanskrit"], [u"sa", u"Sanskrit"],
[u"si", u"Sinhala; Sinhalese"], [u"si", u"Sinhala"],
[u"sk", u"Slovak"], [u"sk", u"Slovak"],
[u"sl", u"Slovenian"], [u"sl", u"Slovenian"],
[u"se", u"Northern Sami"], [u"se", u"Northern Sami"],
...@@ -1448,7 +1448,7 @@ ALL_LANGUAGES = ( ...@@ -1448,7 +1448,7 @@ ALL_LANGUAGES = (
[u"sd", u"Sindhi"], [u"sd", u"Sindhi"],
[u"so", u"Somali"], [u"so", u"Somali"],
[u"st", u"Sotho, Southern"], [u"st", u"Sotho, Southern"],
[u"es", u"Spanish; Castilian"], [u"es", u"Spanish"],
[u"sc", u"Sardinian"], [u"sc", u"Sardinian"],
[u"sr", u"Serbian"], [u"sr", u"Serbian"],
[u"ss", u"Swati"], [u"ss", u"Swati"],
...@@ -1470,7 +1470,7 @@ ALL_LANGUAGES = ( ...@@ -1470,7 +1470,7 @@ ALL_LANGUAGES = (
[u"tk", u"Turkmen"], [u"tk", u"Turkmen"],
[u"tr", u"Turkish"], [u"tr", u"Turkish"],
[u"tw", u"Twi"], [u"tw", u"Twi"],
[u"ug", u"Uighur; Uyghur"], [u"ug", u"Uighur"],
[u"uk", u"Ukrainian"], [u"uk", u"Ukrainian"],
[u"ur", u"Urdu"], [u"ur", u"Urdu"],
[u"uz", u"Uzbek"], [u"uz", u"Uzbek"],
...@@ -1483,7 +1483,7 @@ ALL_LANGUAGES = ( ...@@ -1483,7 +1483,7 @@ ALL_LANGUAGES = (
[u"xh", u"Xhosa"], [u"xh", u"Xhosa"],
[u"yi", u"Yiddish"], [u"yi", u"Yiddish"],
[u"yo", u"Yoruba"], [u"yo", u"Yoruba"],
[u"za", u"Zhuang; Chuang"], [u"za", u"Zhuang"],
[u"zu", u"Zulu"] [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