Commit dfa7c27e by Alexander Kryklia

Add multiple transcripts editor.

Fix donwload subs for non youtube videos and non-en language - continue.
Add acceptance tests.
Add detetion of assets on request.
Updated docstring.
Add fixes and acceptance tests.
Fix acceptance tests.
Update docsrtings and cleanup code, resful for language_id.
Specify exception type in POST.
Fix url in upload module.
Improve exception handling.
Remove 'en' and catching in editable_metadata.
Move descriptor.get_context test to lms tests.
Add query parameter to translation dispatch.
Response to format parameter of translatin GET request.
Fix Acceprance test: Metadata Editor.
move handlers to proper scores.
Split video player into smaller files.
Add ugettext and fix typoes.
Add changelog.
Support for downloading non-ascii filenames.
Change event binding.
Add content-language to download requests.
Reractor POST handler to not update self.transcripts.
parent 90064234
......@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Create an upload modal for video transcript translations (BLD-751).
Studio: Add ability to reorder Pages and hide the Wiki page. STUD-1375
Blades: Added template for iFrames. BLD-611.
......
......@@ -332,10 +332,15 @@ def get_codemirror_value(index=0):
return $('div.CodeMirror:eq({})').get(0).CodeMirror.getValue();
""".format(index))
def upload_file(filename):
path = os.path.join(TEST_ROOT, filename)
def attach_file(filename, sub_path):
path = os.path.join(TEST_ROOT, sub_path, filename)
world.browser.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('file', os.path.abspath(path))
def upload_file(filename, sub_path=''):
attach_file(filename, sub_path)
button_css = '.upload-dialog .action-upload'
world.css_click(button_css)
......
......@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
# Check if the web object is a list type
# If so, we use a slightly different mechanism for determining its value
if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict'):
if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict') or setting.has_class('metadata-video-translations'):
list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
assert_equal(value, list_value)
elif setting.has_class('metadata-videolist-enum'):
......
......@@ -27,7 +27,7 @@ def assert_create_new_textbook_msg(_step):
@step(u'I upload the textbook "([^"]*)"$')
def upload_textbook(_step, file_name):
upload_file(file_name)
upload_file(file_name, sub_path="uploads/")
@step(u'I click (on )?the New Textbook button')
......
......@@ -334,7 +334,7 @@ Feature: CMS Transcripts
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
......@@ -345,7 +345,7 @@ Feature: CMS Transcripts
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "test_transcripts.webm" source to field number 3
And I enter a "uk_transcripts.webm" source to field number 3
Then I see status message "found"
#20
......@@ -353,21 +353,21 @@ Feature: CMS Transcripts
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "test_transcripts.mp4" source to field number 1
And I enter a "uk_transcripts.mp4" source to field number 1
Then I see status message "not found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "test_transcripts" in the field "Transcript (primary)"
And I see value "uk_transcripts" in the field "Transcript (primary)"
And I enter a "t_not_exist.webm" source to field number 2
Then I see status message "replace"
And I see choose button "test_transcripts.mp4" number 1
And I see choose button "uk_transcripts.mp4" number 1
And I see choose button "t_not_exist.webm" number 2
And I click transcript button "choose" number 2
And I see value "test_transcripts|t_not_exist" in the field "Transcript (primary)"
And I see value "uk_transcripts|t_not_exist" in the field "Transcript (primary)"
# Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927
#21
......@@ -456,7 +456,7 @@ Feature: CMS Transcripts
And I enter a "video_name_1.mp4" source to field number 1
And I see status message "not found"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "video_name_1" in the field "Transcript (primary)"
......@@ -487,7 +487,7 @@ Feature: CMS Transcripts
And I enter a "video_name_2.webm" source to field number 2
And I see status message "not found"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "video_name_1|video_name_2" in the field "Transcript (primary)"
......@@ -503,7 +503,7 @@ Feature: CMS Transcripts
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "not found"
And I see button "upload_new_timed_transcripts"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I save changes
......@@ -525,7 +525,7 @@ Feature: CMS Transcripts
Then I see status message "not found"
And I see button "upload_new_timed_transcripts"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I clear field number 1
Then I see status message "found"
......@@ -690,7 +690,7 @@ Feature: CMS Transcripts
And I enter a "video_name_1.1.2.mp4" source to field number 1
And I see status message "not found"
And I upload the transcripts file "test_transcripts.srt"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "video_name_1.1.2" in the field "Transcript (primary)"
......
......@@ -195,7 +195,7 @@ def i_enter_a_source(_step, link, index):
def upload_file(_step, file_name):
path = os.path.join(TEST_ROOT, 'uploads/', file_name.strip())
world.browser.execute_script("$('form.file-chooser').show()")
world.browser.attach_file('file', os.path.abspath(path))
world.browser.attach_file('transcript-file', os.path.abspath(path))
world.wait_for_ajax_complete()
......
......@@ -2,11 +2,13 @@
Feature: CMS Video Component Editor
As a course author, I want to be able to create video components
# 1
Scenario: User can view Video metadata
Given I have created a Video component
And I edit the component
Then I see the correct video settings and default values
# 2
# Safari has trouble saving values on Sauce
@skip_safari
Scenario: User can modify Video display name
......@@ -16,6 +18,7 @@ Feature: CMS Video Component Editor
Then I can modify the display name
And my video display name change is persisted on save
# 3
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden when "transcript display" is false
......@@ -23,9 +26,201 @@ Feature: CMS Video Component Editor
And I have set "transcript display" to False
Then when I view the video it does not show the captions
# 4
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown when "transcript display" is true
Given I have created a Video component with subtitles
And I have set "transcript display" to True
Then when I view the video it does show the captions
# 5
Scenario: Translations uploading works correctly
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
And I save changes
Then when I view the video it does show the captions
And I see "好 各位同学" text in the captions
And I edit the component
And I open tab "Advanced"
And I see translations for "zh"
And I upload transcript file "uk_transcripts.srt" for "uk" language code
And I save changes
Then when I view the video it does show the captions
And I see "好 各位同学" text in the captions
And video language menu has "uk, zh" translations
# 6
Scenario: User can upload transcript file with > 1mb size
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript file "1mb_transcripts.srt" for "uk" language code
And I save changes
Then when I view the video it does show the captions
And I see "Привіт, edX вітає вас." text in the captions
# 7
Scenario: Translations downloading works correctly w/ preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript files:
|lang_code|filename |
|uk |uk_transcripts.srt |
|zh |chinese_transcripts.srt|
And I save changes
And I edit the component
And I open tab "Advanced"
And I see translations for "uk, zh"
And video language menu has "uk, zh" translations
Then I can download transcript for "zh" language code, that contains text "好 各位同学"
And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас."
# 8
Scenario: Translations downloading works correctly w/o preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript files:
|lang_code|filename |
|uk |uk_transcripts.srt |
|zh |chinese_transcripts.srt|
Then I can download transcript for "zh" language code, that contains text "好 各位同学"
And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас."
# 9
Scenario: Translations removing works correctly w/ preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript files:
|lang_code|filename |
|uk |uk_transcripts.srt |
|zh |chinese_transcripts.srt|
And I save changes
Then when I view the video it does show the captions
And I see "Привіт, edX вітає вас." text in the captions
And video language menu has "uk, zh" translations
And I edit the component
And I open tab "Advanced"
And I see translations for "uk, zh"
Then I remove translation for "uk" language code
And I save changes
Then when I view the video it does show the captions
And I see "好 各位同学" text in the captions
And I edit the component
And I open tab "Advanced"
And I see translations for "zh"
Then I remove translation for "zh" language code
And I save changes
Then when I view the video it does not show the captions
# 10
Scenario: Translations removing works correctly w/o preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript file "uk_transcripts.srt" for "uk" language code
And I see translations for "uk"
Then I remove translation for "uk" language code
And I save changes
Then when I view the video it does not show the captions
# 11
Scenario: Translations clearing works correctly w/ preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript files:
|lang_code|filename |
|uk |uk_transcripts.srt |
|zh |chinese_transcripts.srt|
And I save changes
Then when I view the video it does show the captions
And I see "Привіт, edX вітає вас." text in the captions
And video language menu has "uk, zh" translations
And I edit the component
And I open tab "Advanced"
And I see translations for "uk, zh"
And I click button "Clear"
And I save changes
Then when I view the video it does not show the captions
# 12
Scenario: Translations clearing works correctly w/o preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript files:
|lang_code|filename |
|uk |uk_transcripts.srt |
|zh |chinese_transcripts.srt|
And I click button "Clear"
And I save changes
Then when I view the video it does not show the captions
# 13
Scenario: User cannot upload translations in sjson format
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I click button "Add"
And I choose "uk" language code
And I try to upload transcript file "uk_transcripts.sjson"
Then I see validation error "Only SRT files can be uploaded. Please select a file ending in .srt to upload."
# 14
Scenario: User can easy replace the translation by another one w/ preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
And I save changes
Then when I view the video it does show the captions
And I see "好 各位同学" text in the captions
And I edit the component
And I open tab "Advanced"
And I see translations for "zh"
And I replace transcript file for "zh" language code by "uk_transcripts.srt"
And I save changes
Then when I view the video it does show the captions
And I see "Привіт, edX вітає вас." text in the captions
# 15
Scenario: User can easy replace the translation by another one w/o preliminary saving
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
And I see translations for "zh"
And I replace transcript file for "zh" language code by "uk_transcripts.srt"
And I save changes
Then when I view the video it does show the captions
And I see "Привіт, edX вітає вас." text in the captions
# 16
Scenario: Upload "zh" file "A" -> Remove "zh" -> Upload "zh" file "B"
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I upload transcript file "chinese_transcripts.srt" for "zh" language code
And I see translations for "zh"
Then I remove translation for "zh" language code
And I upload transcript file "uk_transcripts.srt" for "zh" language code
And I save changes
Then when I view the video it does show the captions
And I see "Привіт, edX вітає вас." text in the captions
# 17
Scenario: User cannot select the same language twice
Given I have created a Video component
And I edit the component
And I open tab "Advanced"
And I click button "Add"
And I choose "zh" language code
And I click button "Add"
Then I cannot choose "zh" language code
# -*- coding: utf-8 -*-
# disable missing docstring
# pylint: disable=C0111
import requests
from lettuce import world, step
from nose.tools import assert_true, assert_equal, assert_in, assert_not_equal # pylint: disable=E0611
from terrain.steps import reload_the_page
from django.conf import settings
from common import upload_file, attach_file
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
LANGUAGES = {l[0]: l[1] for l in settings.ALL_LANGUAGES}
TRANSLATION_BUTTONS = {
'add': '.metadata-video-translations .create-action',
'upload': '.metadata-video-translations .upload-action',
'download': '.metadata-video-translations .download-action',
'remove': '.metadata-video-translations .remove-action',
'clear': '.metadata-video-translations .setting-clear',
}
VIDEO_MENUS = {
'language': '.lang .menu',
}
class RequestHandlerWithSessionId(object):
def get(self, url):
"""
Sends a request.
"""
kwargs = dict()
session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name'] == u'sessionid']
if session_id:
kwargs.update({
'cookies': session_id[0]
})
response = requests.get(url, **kwargs)
self.response = response
self.status_code = response.status_code
self.headers = response.headers
self.content = response.content
return self
def is_success(self):
"""
Returns `True` if the response was succeed, otherwise, returns `False`.
"""
if self.status_code < 400:
return True
return False
def check_header(self, name, value):
"""
Returns `True` if the response header exist and has appropriate value,
otherwise, returns `False`.
"""
if value in self.headers.get(name, ''):
return True
return False
def success_upload_file(filename):
upload_file(filename, sub_path="uploads/")
world.css_has_text('#upload_confirm', 'Success!')
world.is_css_not_present('.wrapper-dialog-assetupload', wait_time=30)
def get_translations_container():
return world.browser.find_by_xpath('//label[text()="Transcript Translations"]/following-sibling::div')
def get_setting_container(lang_code):
try:
get_xpath = lambda value: './/descendant::a[@data-lang="{}" and contains(@class,"remove-setting")]/parent::*'.format(value)
return get_translations_container().find_by_xpath(get_xpath(lang_code)).first
except Exception:
return None
def get_last_dropdown():
return get_translations_container().find_by_xpath('.//descendant::select[last()]').last
def choose_option(dropdown, value):
dropdown.find_by_value(value)[0].click()
def choose_new_lang(lang_code):
world.css_click(TRANSLATION_BUTTONS['add'])
choose_option(get_last_dropdown(), lang_code)
assert_equal(get_last_dropdown().value, lang_code, "Option with provided value is not available or was not selected")
def open_menu(menu):
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
selector=VIDEO_MENUS[menu]
))
@step('I have set "transcript display" to (.*)$')
......@@ -22,9 +119,9 @@ def shows_captions(_step, show_captions):
world.wait_for_js_variable_truthy("Video")
world.wait(0.5)
if show_captions == 'does not':
assert world.is_css_present('div.video.closed')
assert_true(world.is_css_present('div.video.closed'))
else:
assert world.is_css_not_present('div.video.closed')
assert_true(world.is_css_not_present('div.video.closed'))
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
......@@ -68,3 +165,131 @@ def video_name_persisted(step):
world.get_setting_entry('Display Name'),
'Display Name', '3.4', True
)
@step('I upload transcript file(?:s)?:$')
def upload_transcript(step):
input_hidden = '.metadata-video-translations .input'
# Number of previously added translations
initial_index = len(world.css_find(TRANSLATION_BUTTONS['download']))
if step.hashes:
for i, item in enumerate(step.hashes):
lang_code = item['lang_code']
filename = item['filename']
index = initial_index + i
choose_new_lang(lang_code)
expected_text = world.css_text(TRANSLATION_BUTTONS['upload'], index=index)
assert_equal(expected_text, "Upload")
assert_equal(world.css_find(input_hidden).last.value, "")
world.css_click(TRANSLATION_BUTTONS['upload'], index=index)
success_upload_file(filename)
world.wait_for_visible(TRANSLATION_BUTTONS['download'], index=index)
assert_equal(world.css_find(TRANSLATION_BUTTONS['upload']).last.text, "Replace")
assert_equal(world.css_find(input_hidden).last.value, filename)
@step('I try to upload transcript file "([^"]*)"$')
def try_to_upload_transcript(step, filename):
world.css_click(TRANSLATION_BUTTONS['upload'])
attach_file(filename, 'uploads/')
@step('I upload transcript file "([^"]*)" for "([^"]*)" language code$')
def upload_transcript_for_lang(step, filename, lang_code):
get_xpath = lambda value: './/div/a[contains(@class, "upload-action")]'.format(value)
container = get_setting_container(lang_code)
# If translation isn't uploaded, prepare drop-down and try to find container again
choose_new_lang(lang_code)
container = get_setting_container(lang_code)
button = container.find_by_xpath(get_xpath(lang_code)).first
button.click()
success_upload_file(filename)
@step('I replace transcript file for "([^"]*)" language code by "([^"]*)"$')
def replace_transcript_for_lang(step, lang_code, filename):
get_xpath = lambda value: './/div/a[contains(@class, "upload-action")]'.format(value)
container = get_setting_container(lang_code)
button = container.find_by_xpath(get_xpath(lang_code)).first
button.click()
success_upload_file(filename)
@step('I see validation error "([^"]*)"$')
def verify_validation_error_message(step, error_message):
assert_equal(world.css_text('#upload_error'), error_message)
@step('I can download transcript for "([^"]*)" language code, that contains text "([^"]*)"$')
def i_can_download_transcript(_step, lang_code, text):
MIME_TYPE = 'application/x-subrip'
get_xpath = lambda value: './/div/a[contains(text(), "Download")]'.format(value)
container = get_setting_container(lang_code)
assert container
button = container.find_by_xpath(get_xpath(lang_code)).first
url = button['href']
request = RequestHandlerWithSessionId()
assert_true(request.get(url).is_success())
assert_true(request.check_header('content-type', MIME_TYPE))
assert_in(text.encode('utf-8'), request.content)
@step('I remove translation for "([^"]*)" language code$')
def i_can_remove_transcript(_step, lang_code):
get_xpath = lambda value: './/descendant::a[@data-lang="{}" and contains(@class,"remove-setting")]'.format(value)
container = get_setting_container(lang_code)
assert container
button = container.find_by_xpath(get_xpath(lang_code)).first
button.click()
@step('I see translations for "([^"]*)"$')
def verify_translations(_step, lang_codes_string):
expected = [l.strip() for l in lang_codes_string.split(',')]
actual = [l['data-lang'] for l in world.css_find('.metadata-video-translations .remove-setting')]
assert_equal(set(expected), set(actual))
@step('I do not see translations$')
def no_translations(_step):
assert_true(world.is_css_not_present('.metadata-video-translations .remove-setting'))
@step('I confirm prompt$')
def confirm_prompt(_step):
world.confirm_studio_prompt()
@step('I (cannot )?choose "([^"]*)" language code$')
def i_choose_lang_code(_step, cannot, lang_code):
choose_option(get_last_dropdown(), lang_code)
if cannot:
assert_not_equal(get_last_dropdown().value, lang_code, "Option with provided value was selected, but shouldn't")
else:
assert_equal(get_last_dropdown().value, lang_code, "Option with provided value is not available or was not selected")
@step('I click button "([^"]*)"$')
def click_button(_step, button):
world.css_click(TRANSLATION_BUTTONS[button.lower()])
@step('video language menu has "([^"]*)" translations$')
def i_see_correct_langs(_step, langs):
menu_name = 'language'
open_menu(menu_name)
items = world.css_find(VIDEO_MENUS[menu_name] + ' li')
translations = {t.strip(): LANGUAGES[t.strip()] for t in langs.split(',')}
assert_equal(len(translations), len(items))
for lang_code, label in translations.items():
assert_true(any([i.text == label for i in items]))
assert_true(any([i['data-lang-code'] == lang_code for i in items]))
# -*- coding: utf-8 -*-
""" Tests for transcripts_utils. """
import unittest
from uuid import uuid4
......@@ -548,3 +549,15 @@ class TestTranscript(unittest.TestCase):
def test_convert_srt_to_sjson(self):
with self.assertRaises(NotImplementedError):
transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson')
class TestSubsFilename(unittest.TestCase):
"""
Tests for subs_filename funtion.
"""
def test_unicode(self):
name = transcripts_utils.subs_filename(u"˙∆©ƒƒƒ")
self.assertEqual(name, u'subs_˙∆©ƒƒƒ.srt.sjson')
name = transcripts_utils.subs_filename(u"˙∆©ƒƒƒ", 'uk')
self.assertEqual(name, u'uk_subs_˙∆©ƒƒƒ.srt.sjson')
......@@ -143,7 +143,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'locator': self.item_locator,
'file': self.good_srt_file,
'transcript-file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -162,7 +162,7 @@ class TestUploadtranscripts(Basetranscripts):
def test_fail_data_without_id(self):
link = reverse('upload_transcripts')
resp = self.client.post(link, {'file': self.good_srt_file})
resp = self.client.post(link, {'transcript-file': self.good_srt_file})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "locator" form data.')
......@@ -178,7 +178,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'locator': 'BAD_LOCATOR',
'file': self.good_srt_file,
'transcript-file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -193,7 +193,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'locator': '{0}_{1}'.format(self.item_locator, 'BAD_LOCATOR'),
'file': self.good_srt_file,
'transcript-file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -222,7 +222,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'locator': item_locator,
'file': self.good_srt_file,
'transcript-file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -240,7 +240,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'locator': self.item_locator,
'file': self.good_srt_file,
'transcript-file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -257,7 +257,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0]
resp = self.client.post(link, {
'locator': self.item_locator,
'file': self.bad_data_srt_file,
'transcript-file': self.bad_data_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -272,7 +272,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0]
resp = self.client.post(link, {
'locator': self.item_locator,
'file': self.bad_name_srt_file,
'transcript-file': self.bad_name_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......@@ -299,7 +299,7 @@ class TestUploadtranscripts(Basetranscripts):
filename = os.path.splitext(os.path.basename(srt_file.name))[0]
resp = self.client.post(link, {
'locator': self.item_locator,
'file': srt_file,
'transcript-file': srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
......
......@@ -87,7 +87,7 @@ def upload_transcripts(request):
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
return error_response(response, "Can't find item by locator.")
if 'file' not in request.FILES:
if 'transcript-file' not in request.FILES:
return error_response(response, 'POST data without "file" form data.')
video_list = request.POST.get('video_list')
......@@ -99,8 +99,8 @@ def upload_transcripts(request):
except ValueError:
return error_response(response, 'Invalid video_list JSON.')
source_subs_filedata = request.FILES['file'].read().decode('utf8')
source_subs_filename = request.FILES['file'].name
source_subs_filedata = request.FILES['transcript-file'].read().decode('utf8')
source_subs_filename = request.FILES['transcript-file'].name
if '.' not in source_subs_filename:
return error_response(response, "Undefined file extension.")
......
......@@ -203,9 +203,9 @@ define([
"coffee/spec/views/overview_spec",
"coffee/spec/views/textbook_spec", "coffee/spec/views/upload_spec",
"js/spec/transcripts/utils_spec", "js/spec/transcripts/editor_spec",
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec",
"js/spec/video/transcripts/utils_spec", "js/spec/video/transcripts/editor_spec",
"js/spec/video/transcripts/videolist_spec", "js/spec/video/transcripts/message_manager_spec",
"js/spec/video/transcripts/file_uploader_spec",
"js/spec/models/explicit_url_spec"
......
......@@ -174,5 +174,6 @@ requirejs.config({
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
define([
"coffee/spec/views/assets_spec"
"coffee/spec/views/assets_spec",
"js/spec/video/translations_editor_spec"
])
......@@ -14,31 +14,44 @@ define ["js/models/uploads"], (FileUpload) ->
expect(@model.isValid()).toBeTruthy()
it "is invalid for text files by default", ->
file = {"type": "text/plain"}
file = {"type": "text/plain", "name": "filename.txt"}
@model.set("selectedFile", file);
expect(@model.isValid()).toBeFalsy()
it "is invalid for PNG files by default", ->
file = {"type": "image/png"}
file = {"type": "image/png", "name": "filename.png"}
@model.set("selectedFile", file);
expect(@model.isValid()).toBeFalsy()
it "can accept a file type when explicitly set", ->
file = {"type": "image/png"}
file = {"type": "image/png", "name": "filename.png"}
@model.set("mimeTypes": ["image/png"])
@model.set("selectedFile", file)
expect(@model.isValid()).toBeTruthy()
it "can accept a file format when explicitly set", ->
file = {"type": "", "name": "filename.png"}
@model.set("fileFormats": ["png"])
@model.set("selectedFile", file)
expect(@model.isValid()).toBeTruthy()
it "can accept multiple file types", ->
file = {"type": "image/gif"}
file = {"type": "image/gif", "name": "filename.gif"}
@model.set("mimeTypes": ["image/png", "image/jpeg", "image/gif"])
@model.set("selectedFile", file)
expect(@model.isValid()).toBeTruthy()
it "can accept multiple file formats", ->
file = {"type": "image/gif", "name": "filename.gif"}
@model.set("fileFormats": ["png", "jpeg", "gif"])
@model.set("selectedFile", file)
expect(@model.isValid()).toBeTruthy()
describe "fileTypes", ->
it "returns a list of the uploader's file types", ->
@model.set('mimeTypes', ['image/png', 'application/json'])
expect(@model.fileTypes()).toEqual(['PNG', 'JSON'])
@model.set('fileFormats', ['gif', 'srt'])
expect(@model.fileTypes()).toEqual(['PNG', 'JSON', 'GIF', 'SRT'])
describe "formatValidTypes", ->
it "returns a map of formatted file types and extensions", ->
......
......@@ -16,6 +16,7 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/c
@dialogResponse = dialogResponse = []
@view = new UploadDialog(
model: @model,
url: CMS.URL.UPLOAD_ASSET,
onSuccess: (response) =>
dialogResponse.push(response.response)
)
......
......@@ -9,10 +9,11 @@ var FileUpload = Backbone.Model.extend({
"uploadedBytes": 0,
"totalBytes": 0,
"finished": false,
"mimeTypes": []
"mimeTypes": [],
"fileFormats": []
},
validate: function(attrs, options) {
if(attrs.selectedFile && !_.contains(this.attributes.mimeTypes, attrs.selectedFile.type)) {
if(attrs.selectedFile && !this.checkTypeValidity(attrs.selectedFile)) {
return {
message: _.template(
gettext("Only <%= fileTypes %> files can be uploaded. Please select a file ending in <%= fileExtensions %> to upload."),
......@@ -24,17 +25,37 @@ var FileUpload = Backbone.Model.extend({
},
// Return a list of this uploader's valid file types
fileTypes: function() {
return _.map(
this.attributes.mimeTypes,
function(type) {
return type.split('/')[1].toUpperCase();
}
);
var mimeTypes = _.map(
this.attributes.mimeTypes,
function(type) {
return type.split('/')[1].toUpperCase();
}
),
fileFormats = _.map(
this.attributes.fileFormats,
function(type) {
return type.toUpperCase();
}
);
return mimeTypes.concat(fileFormats);
},
checkTypeValidity: function (file) {
var attrs = this.attributes,
getRegExp = function (formats) {
// Creates regular expression like: /(?:.+)\.(jpg|png|gif)$/i
return RegExp(('(?:.+)\\.(' + formats.join('|') + ')$'), 'i');
};
return _.contains(attrs.mimeTypes, file.type) ||
getRegExp(attrs.fileFormats).test(file.name);
},
// Return strings for the valid file types and extensions this
// uploader accepts, formatted as natural language
formatValidTypes: function() {
if(this.attributes.mimeTypes.length === 1) {
var attrs = this.attributes;
if(attrs.mimeTypes.concat(attrs.fileFormats).length === 1) {
return {
fileTypes: this.fileTypes()[0],
fileExtensions: '.' + this.fileTypes()[0].toLowerCase()
......
define(
[
"jquery", "backbone", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/editor",
"js/views/video/transcripts/utils", "js/views/video/transcripts/editor",
"js/views/metadata", "js/models/metadata", "js/collections/metadata",
"underscore.string", "xmodule", "js/views/transcripts/metadata_videolist",
"underscore.string", "xmodule", "js/views/video/transcripts/metadata_videolist",
"jasmine-jquery"
],
function ($, Backbone, _, Utils, Editor, MetadataView, MetadataModel, MetadataCollection, _str) {
......
define(
[
"jquery", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/file_uploader",
"js/views/video/transcripts/utils", "js/views/video/transcripts/file_uploader",
"xmodule", "jquery.form", "jasmine-jquery"
],
function ($, _, Utils, FileUploader) {
describe('Transcripts.FileUploader', function () {
var videoListEntryTemplate = readFixtures(
'transcripts/metadata-videolist-entry.underscore'
'video/transcripts/metadata-videolist-entry.underscore'
),
fileUploadTemplate = readFixtures(
'transcripts/file-upload.underscore'
'video/transcripts/file-upload.underscore'
),
view;
......
define(
[
"jquery", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/message_manager",
"js/views/transcripts/file_uploader", "sinon", "jasmine-jquery",
"js/views/video/transcripts/utils", "js/views/video/transcripts/message_manager",
"js/views/video/transcripts/file_uploader", "sinon", "jasmine-jquery",
"xmodule"
],
function ($, _, Utils, MessageManager, FileUploader, sinon) {
describe('Transcripts.MessageManager', function () {
var videoListEntryTemplate = readFixtures(
'transcripts/metadata-videolist-entry.underscore'
'video/transcripts/metadata-videolist-entry.underscore'
),
foundTemplate = readFixtures(
'transcripts/messages/transcripts-found.underscore'
'video/transcripts/messages/transcripts-found.underscore'
),
handlers = {
importHandler: ['replace', 'Error: Import failed.'],
......
define(
[
"jquery", "underscore",
"js/views/transcripts/utils",
"js/views/video/transcripts/utils",
"underscore.string", "xmodule", "jasmine-jquery"
],
function ($, _, Utils, _str) {
......
define(
[
"jquery", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/metadata_videolist",
"js/views/video/transcripts/utils", "js/views/video/transcripts/metadata_videolist",
"js/views/metadata", "js/models/metadata", "js/views/abstract_editor",
"sinon", "xmodule", "jasmine-jquery"
],
function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, sinon) {
describe('CMS.Views.Metadata.VideoList', function () {
var videoListEntryTemplate = readFixtures(
'transcripts/metadata-videolist-entry.underscore'
'video/transcripts/metadata-videolist-entry.underscore'
),
abstractEditor = AbstractEditor.prototype,
component_locator = 'component_locator',
......
......@@ -5,7 +5,9 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon"
var request = requests[requests.length - 1];
expect(request.url).toEqual("/xblock");
expect(request.method).toEqual("POST");
expect(request.requestBody).toEqual(json);
// There was a problem with order of returned parameters in strings.
// Changed to compare objects instead strings.
expect(JSON.parse(request.requestBody)).toEqual(JSON.parse(json));
};
var verifyComponents = function (unit, locators) {
......@@ -44,7 +46,7 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon"
</li> \
</ol> \
</div>';
var unit;
var clickDuplicate = function (index) {
unit.$(".duplicate-button")[index].click();
......
define(
[
"js/views/baseview", "underscore", "js/models/metadata", "js/views/abstract_editor",
"js/views/transcripts/metadata_videolist"
"js/views/video/transcripts/metadata_videolist",
"js/views/video/translations_editor"
],
function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
function(BaseView, _, MetadataModel, AbstractEditor, VideoList, VideoTranslations) {
var Metadata = {};
Metadata.Editor = BaseView.extend({
......@@ -82,6 +83,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
});
Metadata.VideoList = VideoList;
Metadata.VideoTranslations = VideoTranslations;
Metadata.String = AbstractEditor.extend({
......
......@@ -15,7 +15,7 @@ var UploadDialog = BaseView.extend({
var oldInput = this.$("input[type=file]").get(0);
this.$el.html(this.template({
shown: this.options.shown,
url: CMS.URL.UPLOAD_ASSET,
url: this.options.url || CMS.URL.UPLOAD_ASSET,
title: this.model.escape('title'),
message: this.model.escape('message'),
selectedFile: selectedFile,
......
define(
[
"jquery", "backbone", "underscore",
"js/views/transcripts/utils",
"js/views/video/transcripts/utils",
"js/views/metadata", "js/collections/metadata",
"js/views/transcripts/metadata_videolist"
"js/views/video/transcripts/metadata_videolist"
],
function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
......
define(
[
"jquery", "backbone", "underscore",
"js/views/transcripts/utils"
"js/views/video/transcripts/utils"
],
function($, Backbone, _, Utils) {
var FileUploader = Backbone.View.extend({
......
define(
[
"jquery", "backbone", "underscore",
"js/views/transcripts/utils", "js/views/transcripts/file_uploader",
"js/views/video/transcripts/utils", "js/views/video/transcripts/file_uploader",
"gettext"
],
function($, Backbone, _, Utils, FileUploader, gettext) {
......
define(
[
"jquery", "backbone", "underscore", "js/views/abstract_editor",
"js/views/transcripts/utils", "js/views/transcripts/message_manager",
"js/views/video/transcripts/utils", "js/views/video/transcripts/message_manager",
"js/views/metadata"
],
function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
VideoList = AbstractEditor.extend({
var VideoList = AbstractEditor.extend({
// Time that we wait since the last time user typed.
inputDelay: 300,
......
define(
[
"jquery", "underscore",
"js/views/abstract_editor", "js/models/uploads", "js/views/uploads"
],
function($, _, AbstractEditor, FileUpload, UploadDialog) {
"use strict";
var VideoUploadDialog = UploadDialog.extend({
error: function() {
this.model.set({
"uploading": false,
"uploadedBytes": 0,
"title": gettext("Sorry, there was an error parsing the subtitles that you uploaded. Please check the format and try again.")
});
}
});
var Translations = AbstractEditor.extend({
events : {
"click .setting-clear" : "clear",
"click .create-setting" : "addEntry",
"click .remove-setting" : "removeEntry",
"click .upload-setting" : "upload",
"change select" : "onChangeHandler"
},
templateName: "metadata-translations-entry",
templateItemName: "metadata-translations-item",
initialize: function () {
var templateName = _.result(this, 'templateItemName'),
tpl = document.getElementById(templateName).text;
if(!tpl) {
console.error("Couldn't load template for item: " + templateName);
}
this.templateItem = _.template(tpl);
AbstractEditor.prototype.initialize.apply(this, arguments);
},
getDropdown: function () {
var dropdown,
disableOptions = function (element, values) {
var dropdown = $(element).clone();
_.each(values, function(value, key) {
var option = dropdown[0].options.namedItem(key);
if (option) {
option.disabled = true;
}
});
return dropdown;
};
return function (values) {
if (dropdown) {
return disableOptions(dropdown, values);
}
dropdown = document.createElement('select');
dropdown.options.add(new Option());
_.each(this.model.get('languages'), function(lang, index) {
var option = new Option();
option.setAttribute('name', lang.code);
option.value = lang.code;
option.text = lang.label;
dropdown.options.add(option);
});
return disableOptions(dropdown, values);
};
}(),
getValueFromEditor: function () {
var dict = {},
items = this.$el.find('ol').find('.list-settings-item');
_.each(items, function(element, index) {
var key = $(element).find('select').val(),
value = $(element).find('.input').val();
// Keys should be unique, so if our keys are duplicated and
// second key is empty or key and value are empty just do
// nothing. Otherwise, it'll be overwritten by the new value.
if (value === '') {
if (key === '' || key in dict) {
return false;
}
}
dict[key] = value;
});
return dict;
},
// @TODO: Use backbone render patterns.
setValueInEditor: function (values) {
var self = this,
frag = document.createDocumentFragment(),
dropdown = self.getDropdown(values);
_.each(values, function(value, key) {
var html = $(self.templateItem({
'lang': key,
'value': value,
'url': self.model.get('urlRoot') + '/' + key
})).prepend(dropdown.clone().val(key))[0];
frag.appendChild(html);
});
this.$el.find('ol').html([frag]);
},
addEntry: function(event) {
event.preventDefault();
// We don't call updateModel here since it's bound to the
// change event
var dict = $.extend(true, {}, this.model.get('value'));
dict[''] = '';
this.setValueInEditor(dict);
this.$el.find('.create-setting').addClass('is-disabled');
},
removeEntry: function(event) {
event.preventDefault();
var entry = $(event.currentTarget).data('lang');
this.setValueInEditor(_.omit(this.model.get('value'), entry));
this.updateModel();
this.$el.find('.create-setting').removeClass('is-disabled');
},
upload: function (event) {
event.preventDefault();
var self = this,
target = $(event.currentTarget),
lang = target.data('lang'),
model = new FileUpload({
title: gettext('Upload translation.'),
fileFormats: ['srt']
}),
view = new VideoUploadDialog({
model: model,
url: self.model.get('urlRoot') + '/' + lang,
onSuccess: function (response) {
if (!response['filename']) { return; }
var dict = $.extend(true, {}, self.model.get('value'));
dict[lang] = response['filename'];
self.model.setValue(dict);
}
});
$('.wrapper-view').after(view.show().el);
},
enableAdd: function() {
this.$el.find('.create-setting').removeClass('is-disabled');
},
clear: function() {
AbstractEditor.prototype.clear.apply(this, arguments);
if (_.isNull(this.model.getValue())) {
this.$el.find('.create-setting').removeClass('is-disabled');
}
},
onChangeHandler: function (event) {
this.showClearButton();
this.enableAdd();
this.updateModel();
}
});
return Translations;
});
......@@ -824,11 +824,6 @@ body.course.unit,.view-unit {
}
}
// actions
.create-action, .remove-action, .setting-clear {
}
.setting-clear {
vertical-align: top;
margin-top: ($baseline/4);
......@@ -845,11 +840,6 @@ body.course.unit,.view-unit {
*[class^="icon-"] {
margin-right: ($baseline/4);
}
// STATE: disabled
&.is-disabled {
}
}
.remove-setting {
......@@ -862,11 +852,6 @@ body.course.unit,.view-unit {
&:hover {
color: $blue;
}
// STATE: disabled
&.is-disabled {
}
}
}
......
<div class="wrapper-comp-setting metadata-video-translations">
<label class="label setting-label"><%= model.get('display_name')%></label>
<div class="wrapper-translations-settings">
<ol class="list-settings"></ol>
<a href="#" class="create-action create-setting">
<i class="icon-plus"></i><%= gettext("Add") %> <span class="sr"><%= model.get('display_name')%></span>
</a>
</div>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i>
<span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
<li class="list-settings-item"
><a href="#" class="remove-action remove-setting" data-lang="<%= lang %>" data-value="<%= value %>"><i class="icon-remove-sign"></i><span class="sr"><%= gettext("Remove") %></span></a>
<input type="hidden" class="input" value="<%= value %>">
<div class="list-settings-buttons"><% if (lang) {
%><a href="#" class="upload-action upload-setting" data-lang="<%= lang %>" data-value="<%= value %>"><%= value ? gettext("Replace") : gettext("Upload") %>
</a><%
} %><% if (value) {
%><a href="<%= url %>?filename=<%= value %>" class="download-action download-setting"><%= gettext("Download") %>
</a><%
}
%><div>
</li>
......@@ -3,7 +3,7 @@
</div>
<form class="file-chooser" action="/transcripts/upload"
method="post" enctype="multipart/form-data">
<input type="file" class="file-input" name="file"
<input type="file" class="file-input" name="transcript-file"
accept="<%= _.map(ext, function(val){ return '.' + val; }).join(', ') %>">
<input type="hidden" name="locator" value="<%= component_locator %>">
<input type="hidden" name="video_list" value='<%= JSON.stringify(video_list) %>'>
......
......@@ -7,7 +7,7 @@ from xmodule.modulestore.django import loc_mapper
<%namespace name='static' file='static_content.html'/>
<%namespace name="units" file="widgets/units.html" />
<%block name="title">${_("Individual Unit")}</%block>
<%block name="bodyclass">is-signedin course unit view-unit</%block>
<%block name="bodyclass">is-signedin course unit view-unit feature-upload</%block>
<%block name="jsextra">
<script type='text/javascript'>
......@@ -34,10 +34,13 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
});
});
</script>
<script type="text/template" id="image-modal-tpl">
<%static:include path="js/imageModal.underscore" />
</script>
<script type="text/template" id="upload-dialog-tpl">
<%static:include path="js/upload-dialog.underscore" />
</script>
</%block>
<%block name="content">
......@@ -214,7 +217,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
</div>
</div>
</div>
</%block>
......@@ -12,7 +12,6 @@
<script id="metadata-editor-tpl" type="text/template">
<%static:include path="js/metadata-editor.underscore" />
</script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]:
<script id="${template_name}" type="text/template">
<%static:include path="js/${template_name}.underscore" />
......
......@@ -10,13 +10,18 @@ import json
% for template_name in ["metadata-videolist-entry", "file-upload"]:
<script type="text/template" id="${template_name}">
<%static:include path="js/transcripts/${template_name}.underscore" />
<%static:include path="js/video/transcripts/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["transcripts-found", "transcripts-uploaded", "transcripts-use-existing", "transcripts-not-found", "transcripts-replace", "transcripts-import", "transcripts-choose"]:
<script type="text/template" id="${template_name}">
<%static:include path="js/transcripts/messages/${template_name}.underscore" />
<%static:include path="js/video/transcripts/messages/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["metadata-translations-entry", "metadata-translations-item"]:
<script id="${template_name}" type="text/template">
<%static:include path="js/video/${template_name}.underscore" />
</script>
% endfor
......@@ -27,7 +32,7 @@ import json
[
"domReady!",
"jquery",
"js/views/transcripts/editor"
"js/views/video/transcripts/editor"
],
function(doc, $, Editor) {
......
......@@ -144,7 +144,7 @@
}
};
}
} else if (settings.url == '/transcript/translation') {
} else if (settings.url.match(/transcript\/translation\/.+$/)) {
return settings.success(jasmine.stubbedCaption);
} else if (settings.url == '/transcript/available_translations') {
return settings.success(['uk', 'de']);
......
......@@ -60,16 +60,14 @@
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
url: '/transcript/translation',
url: '/transcript/translation/en',
notifyOnError: false,
data: jasmine.any(Object),
data: void(0),
success: jasmine.any(Function),
error: jasmine.any(Function)
});
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({
language: 'en'
});
.toBeUndefined();
});
});
......@@ -86,7 +84,7 @@
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
url: '/transcript/translation',
url: '/transcript/translation/en',
notifyOnError: false,
data: jasmine.any(Object),
success: jasmine.any(Function),
......@@ -94,7 +92,6 @@
});
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({
language: 'en',
videoId: 'abcdefghijkl'
});
});
......@@ -111,7 +108,7 @@
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
url: '/transcript/translation',
url: '/transcript/translation/en',
notifyOnError: false,
data: jasmine.any(Object),
success: jasmine.any(Function),
......@@ -119,7 +116,6 @@
});
expect($.ajaxWithPrefix.mostRecentCall.args[0].data)
.toEqual({
language: 'en',
videoId: 'cogebirgzzM'
});
});
......
......@@ -226,9 +226,8 @@ function () {
function fetchCaption() {
var self = this,
Caption = self.videoCaption,
data = {
language: this.getCurrentLanguage()
};
language = this.getCurrentLanguage(),
data;
if (Caption.loaded) {
Caption.hideCaptions(false);
......@@ -241,13 +240,15 @@ function () {
}
if (this.videoType === 'youtube') {
data.videoId = this.youtubeId();
data = {
videoId: this.youtubeId()
};
}
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
Caption.fetchXHR = $.ajaxWithPrefix({
url: self.config.transcriptTranslationUrl,
url: self.config.transcriptTranslationUrl + '/' + language,
notifyOnError: false,
data: data,
success: function (captions) {
......
......@@ -132,22 +132,6 @@ class VideoDescriptorTest(unittest.TestCase):
field_data=DictFieldData({}),
)
def test_get_context(self):
""""test get_context"""
correct_tabs = [
{
'name': "Basic",
'template': "video/transcripts.html",
'current': True
},
{
'name': 'Advanced',
'template': 'tabs/metadata-edit-tab.html'
}
]
rendered_context = self.descriptor.get_context()
self.assertListEqual(rendered_context['tabs'], correct_tabs)
def test_create_youtube_string(self):
"""
Test that Youtube ID strings are correctly created when writing
......
......@@ -64,6 +64,17 @@ def generate_subs(speed, source_speed, source_subs):
return subs
def save_to_store(content, name, mime_type, location):
"""
Save named content to store by location.
Returns location of saved content.
"""
content_location = Transcript.asset_location(location, name)
content = StaticContent(content_location, name, mime_type, content)
contentstore().save(content)
return content_location
def save_subs_to_store(subs, subs_id, item, language='en'):
"""
Save transcripts into `StaticContent`.
......@@ -76,13 +87,8 @@ def save_subs_to_store(subs, subs_id, item, language='en'):
Returns: location of saved subtitles.
"""
filedata = json.dumps(subs, indent=2)
mime_type = 'application/json'
filename = subs_filename(subs_id, language)
content_location = Transcript.asset_location(item.location, filename)
content = StaticContent(content_location, filename, mime_type, filedata)
contentstore().save(content)
return content_location
return save_to_store(filedata, filename, 'application/json', item.location)
def get_transcripts_from_youtube(youtube_id, settings, i18n):
"""
......@@ -193,12 +199,8 @@ def remove_subs_from_store(subs_id, item, lang='en'):
"""
Remove from store, if transcripts content exists.
"""
try:
content = Transcript.asset(item.location, subs_id, lang)
contentstore().delete(content.get_id())
log.info("Removed subs %s from store", subs_id)
except NotFoundError:
pass
filename = subs_filename(subs_id, lang)
Transcript.delete_asset(item.location, filename)
def generate_subs_from_source(speed_subs, subs_type, subs_filedata, item, language='en'):
......@@ -339,6 +341,8 @@ def manage_video_subtitles_save(item, user, old_metadata=None, generate_translat
a new version of the SRT file with same name).
"""
_ = item.runtime.service(item, "i18n").ugettext
# 1.
html5_ids = get_html5_ids(item.html5_sources)
possible_video_id_list = [item.youtube_id_1_0] + html5_ids
......@@ -408,10 +412,9 @@ def subs_filename(subs_id, lang='en'):
Generate proper filename for storage.
"""
if lang == 'en':
return 'subs_{0}.srt.sjson'.format(subs_id)
return u'subs_{0}.srt.sjson'.format(subs_id)
else:
return '{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
return u'{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
......@@ -420,10 +423,15 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
`item` is module object.
"""
_ = item.runtime.service(item, "i18n").ugettext
try:
srt_transcript = contentstore().find(Transcript.asset_location(item.location, user_filename))
srt_transcripts = contentstore().find(Transcript.asset_location(item.location, user_filename))
except NotFoundError as ex:
raise TranscriptException("{}: Can't find uploaded transcripts: {}".format(ex.message, user_filename))
raise TranscriptException(_("{exception_message}: Can't find uploaded transcripts: {user_filename}").format(
exception_message=ex.message,
user_filename=user_filename
))
if not lang:
lang = item.transcript_language
......@@ -431,7 +439,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
generate_subs_from_source(
result_subs_dict,
os.path.splitext(user_filename)[1][1:],
srt_transcript.data.decode('utf8'),
srt_transcripts.data.decode('utf8'),
item,
lang
)
......@@ -464,6 +472,11 @@ class Transcript(object):
"""
Container for transcript methods.
"""
mime_types = {
'srt': 'application/x-subrip; charset=utf-8',
'txt': 'text/plain; charset=utf-8',
'sjson': 'application/json',
}
@staticmethod
def convert(content, input_format, output_format):
......@@ -504,21 +517,32 @@ class Transcript(object):
`location` is module location.
"""
return contentstore().find(
Transcript.asset_location(
location,
subs_filename(subs_id, lang) if not filename else filename
)
)
asset_filename = subs_filename(subs_id, lang) if not filename else filename
return Transcript.get_asset(location, asset_filename)
@staticmethod
def get_asset(location, filename):
"""
Return asset by location and filename.
"""
return contentstore().find(Transcript.asset_location(location, filename))
@staticmethod
def asset_location(location, filename):
"""
Return asset location.
Return asset location. `location` is module location.
"""
return StaticContent.compute_location(location.org, location.course, filename)
`location` is module location.
@staticmethod
def delete_asset(location, filename):
"""
return StaticContent.compute_location(
location.org, location.course, filename
)
Delete asset by location and filename.
"""
try:
content = Transcript.get_asset(location, filename)
contentstore().delete(content.get_id())
log.info("Transcript asset %s was removed from store.", filename)
except NotFoundError:
pass
"""
XFields for video module.
"""
import datetime
from xblock.fields import Scope, String, Float, Boolean, List, Dict
from xmodule.fields import RelativeTime
class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`."""
display_name = String(
display_name="Display Name", help="Display name for this module.",
default="Video",
scope=Scope.settings
)
saved_video_position = RelativeTime(
help="Current position in the video",
scope=Scope.user_state,
default=datetime.timedelta(seconds=0)
)
# TODO: This should be moved to Scope.content, but this will
# require data migration to support the old video module.
youtube_id_1_0 = String(
help="This is the Youtube ID reference for the normal speed video.",
display_name="Youtube ID",
scope=Scope.settings,
default="OEoXaMPEzfM"
)
youtube_id_0_75 = String(
help="Optional, for older browsers: the Youtube ID for the .75x speed video.",
display_name="Youtube ID for .75x speed",
scope=Scope.settings,
default=""
)
youtube_id_1_25 = String(
help="Optional, for older browsers: the Youtube ID for the 1.25x speed video.",
display_name="Youtube ID for 1.25x speed",
scope=Scope.settings,
default=""
)
youtube_id_1_5 = String(
help="Optional, for older browsers: the Youtube ID for the 1.5x speed video.",
display_name="Youtube ID for 1.5x speed",
scope=Scope.settings,
default=""
)
start_time = RelativeTime( # datetime.timedelta object
help="Start time for the video (HH:MM:SS). Max value is 23:59:59.",
display_name="Start Time",
scope=Scope.settings,
default=datetime.timedelta(seconds=0)
)
end_time = RelativeTime( # datetime.timedelta object
help="End time for the video (HH:MM:SS). Max value is 23:59:59.",
display_name="End Time",
scope=Scope.settings,
default=datetime.timedelta(seconds=0)
)
#front-end code of video player checks logical validity of (start_time, end_time) pair.
# `source` is deprecated field and should not be used in future.
# `download_video` is used instead.
source = String(
help="The external URL to download the video.",
display_name="Download Video",
scope=Scope.settings,
default=""
)
download_video = Boolean(
help="Show a link beneath the video to allow students to download the video. Note: You must add at least one video source below.",
display_name="Video Download Allowed",
scope=Scope.settings,
default=False
)
html5_sources = List(
help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
display_name="Video Sources",
scope=Scope.settings,
)
track = String(
help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
display_name="Download Transcript",
scope=Scope.settings,
default=''
)
download_track = Boolean(
help="Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.",
display_name="Transcript Download Allowed",
scope=Scope.settings,
default=False
)
sub = String(
help="The name of the timed transcript track (for non-Youtube videos).",
display_name="Transcript (primary)",
scope=Scope.settings,
default=""
)
show_captions = Boolean(
help="This controls whether or not captions are shown by default.",
display_name="Transcript Display",
scope=Scope.settings,
default=True
)
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
transcripts = Dict(
help="Add additional transcripts in other languages",
display_name="Transcript Translations",
scope=Scope.settings,
default={}
)
transcript_language = String(
help="Preferred language for transcript",
display_name="Preferred language for transcript",
scope=Scope.preferences,
default="en"
)
transcript_download_format = String(
help="Transcript file format to download by user.",
scope=Scope.preferences,
values=[
{"display_name": "SubRip (.srt) file", "value": "srt"},
{"display_name": "Text (.txt) file", "value": "txt"}
],
default='srt',
)
speed = Float(
help="The last speed that was explicitly set by user for the video.",
scope=Scope.user_state,
)
global_speed = Float(
help="Default speed in cases when speed wasn't explicitly for specific video",
scope=Scope.preferences,
default=1.0
)
youtube_is_available = Boolean(
help="The availaibility of YouTube API for the user",
scope=Scope.user_info,
default=True
)
This source diff could not be displayed because it is too large. You can view the blob instead.
0
00:00:00,270 --> 00:00:02,720
LILA FISHER: Hi, welcome to Edx.
1
00:00:02,720 --> 00:00:05,430
I'm Lila Fisher, an Edx fellow helping to put
2
00:00:05,430 --> 00:00:07,160
together these courses.
3
00:00:07,160 --> 00:00:10,830
As you know, our courses are entirely online.
4
00:00:10,830 --> 00:00:12,880
So before we start learning about the subjects that
5
00:00:12,880 --> 00:00:15,890
brought you here, let's learn about the tools that you will
6
00:00:15,890 --> 00:00:19,000
use to navigate through the course material.
7
00:00:19,000 --> 00:00:22,070
Let's start with what is on your screen right now.
8
00:00:22,070 --> 00:00:25,170
You are watching a video of me talking.
9
00:00:25,170 --> 00:00:27,890
You have several tools associated with these videos.
10
00:00:27,890 --> 00:00:30,590
Some of them are standard video buttons, like the play
0
00:00:00,100 --> 00:00:02,000
Привіт, edX вітає вас.
......@@ -51,7 +51,7 @@ def setUp(scenario):
world.video_sequences = {}
class ReuqestHandlerWithSessionId(object):
class RequestHandlerWithSessionId(object):
def get(self, url):
"""
Sends a request.
......@@ -449,7 +449,6 @@ def select_language(_step, code):
def click_button(_step, button):
world.css_click(VIDEO_BUTTONS[button])
@step('I see video starts playing from "([^"]*)" position$')
def start_playing_video_from_n_seconds(_step, position):
world.wait_for(
......@@ -522,7 +521,7 @@ def i_can_download_transcript(_step, format, text):
}
url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href']
request = ReuqestHandlerWithSessionId()
request = RequestHandlerWithSessionId()
assert request.get(url).is_success()
assert request.check_header('content-type', formats[format])
assert (text.encode('utf-8') in request.content)
......
......@@ -1326,19 +1326,19 @@ ALL_LANGUAGES = (
[u"br", u"Breton"],
[u"bg", u"Bulgarian"],
[u"my", u"Burmese"],
[u"ca", u"Catalan; Valencian"],
[u"ca", u"Catalan"],
[u"ch", u"Chamorro"],
[u"ce", u"Chechen"],
[u"zh", u"Chinese"],
[u"cu", u"Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"],
[u"cu", u"Church Slavic"],
[u"cv", u"Chuvash"],
[u"kw", u"Cornish"],
[u"co", u"Corsican"],
[u"cr", u"Cree"],
[u"cs", u"Czech"],
[u"da", u"Danish"],
[u"dv", u"Divehi; Dhivehi; Maldivian"],
[u"nl", u"Dutch; Flemish"],
[u"dv", u"Divehi"],
[u"nl", u"Dutch"],
[u"dz", u"Dzongkha"],
[u"en", u"English"],
[u"eo", u"Esperanto"],
......@@ -1352,14 +1352,14 @@ ALL_LANGUAGES = (
[u"ff", u"Fulah"],
[u"ka", u"Georgian"],
[u"de", u"German"],
[u"gd", u"Gaelic; Scottish Gaelic"],
[u"gd", u"Gaelic"],
[u"ga", u"Irish"],
[u"gl", u"Galician"],
[u"gv", u"Manx"],
[u"el", u"Greek, Modern (1453-)"],
[u"el", u"Greek"],
[u"gn", u"Guarani"],
[u"gu", u"Gujarati"],
[u"ht", u"Haitian; Haitian Creole"],
[u"ht", u"Haitian"],
[u"ha", u"Hausa"],
[u"he", u"Hebrew"],
[u"hz", u"Herero"],
......@@ -1370,36 +1370,36 @@ ALL_LANGUAGES = (
[u"ig", u"Igbo"],
[u"is", u"Icelandic"],
[u"io", u"Ido"],
[u"ii", u"Sichuan Yi; Nuosu"],
[u"ii", u"Sichuan Yi"],
[u"iu", u"Inuktitut"],
[u"ie", u"Interlingue; Occidental"],
[u"ia", u"Interlingua (International Auxiliary Language Association)"],
[u"ie", u"Interlingue"],
[u"ia", u"Interlingua"],
[u"id", u"Indonesian"],
[u"ik", u"Inupiaq"],
[u"it", u"Italian"],
[u"jv", u"Javanese"],
[u"ja", u"Japanese"],
[u"kl", u"Kalaallisut; Greenlandic"],
[u"kl", u"Kalaallisut"],
[u"kn", u"Kannada"],
[u"ks", u"Kashmiri"],
[u"kr", u"Kanuri"],
[u"kk", u"Kazakh"],
[u"km", u"Central Khmer"],
[u"ki", u"Kikuyu; Gikuyu"],
[u"ki", u"Kikuyu"],
[u"rw", u"Kinyarwanda"],
[u"ky", u"Kirghiz; Kyrgyz"],
[u"ky", u"Kirghiz"],
[u"kv", u"Komi"],
[u"kg", u"Kongo"],
[u"ko", u"Korean"],
[u"kj", u"Kuanyama; Kwanyama"],
[u"kj", u"Kuanyama"],
[u"ku", u"Kurdish"],
[u"lo", u"Lao"],
[u"la", u"Latin"],
[u"lv", u"Latvian"],
[u"li", u"Limburgan; Limburger; Limburgish"],
[u"li", u"Limburgan"],
[u"ln", u"Lingala"],
[u"lt", u"Lithuanian"],
[u"lb", u"Luxembourgish; Letzeburgesch"],
[u"lb", u"Luxembourgish"],
[u"lu", u"Luba-Katanga"],
[u"lg", u"Ganda"],
[u"mk", u"Macedonian"],
......@@ -1412,34 +1412,34 @@ ALL_LANGUAGES = (
[u"mt", u"Maltese"],
[u"mn", u"Mongolian"],
[u"na", u"Nauru"],
[u"nv", u"Navajo; Navaho"],
[u"nr", u"Ndebele, South; South Ndebele"],
[u"nd", u"Ndebele, North; North Ndebele"],
[u"nv", u"Navajo"],
[u"nr", u"Ndebele, South"],
[u"nd", u"Ndebele, North"],
[u"ng", u"Ndonga"],
[u"ne", u"Nepali"],
[u"nn", u"Norwegian Nynorsk; Nynorsk, Norwegian"],
[u"nb", u"Bokmål, Norwegian; Norwegian Bokmål"],
[u"nn", u"Norwegian Nynorsk"],
[u"nb", u"Bokmål, Norwegian"],
[u"no", u"Norwegian"],
[u"ny", u"Chichewa; Chewa; Nyanja"],
[u"oc", u"Occitan (post 1500); Provençal"],
[u"ny", u"Chichewa"],
[u"oc", u"Occitan"],
[u"oj", u"Ojibwa"],
[u"or", u"Oriya"],
[u"om", u"Oromo"],
[u"os", u"Ossetian; Ossetic"],
[u"pa", u"Panjabi; Punjabi"],
[u"os", u"Ossetian"],
[u"pa", u"Panjabi"],
[u"fa", u"Persian"],
[u"pi", u"Pali"],
[u"pl", u"Polish"],
[u"pt", u"Portuguese"],
[u"ps", u"Pushto; Pashto"],
[u"ps", u"Pushto"],
[u"qu", u"Quechua"],
[u"rm", u"Romansh"],
[u"ro", u"Romanian; Moldavian; Moldovan"],
[u"ro", u"Romanian"],
[u"rn", u"Rundi"],
[u"ru", u"Russian"],
[u"sg", u"Sango"],
[u"sa", u"Sanskrit"],
[u"si", u"Sinhala; Sinhalese"],
[u"si", u"Sinhala"],
[u"sk", u"Slovak"],
[u"sl", u"Slovenian"],
[u"se", u"Northern Sami"],
......@@ -1448,7 +1448,7 @@ ALL_LANGUAGES = (
[u"sd", u"Sindhi"],
[u"so", u"Somali"],
[u"st", u"Sotho, Southern"],
[u"es", u"Spanish; Castilian"],
[u"es", u"Spanish"],
[u"sc", u"Sardinian"],
[u"sr", u"Serbian"],
[u"ss", u"Swati"],
......@@ -1470,7 +1470,7 @@ ALL_LANGUAGES = (
[u"tk", u"Turkmen"],
[u"tr", u"Turkish"],
[u"tw", u"Twi"],
[u"ug", u"Uighur; Uyghur"],
[u"ug", u"Uighur"],
[u"uk", u"Ukrainian"],
[u"ur", u"Urdu"],
[u"uz", u"Uzbek"],
......@@ -1483,7 +1483,7 @@ ALL_LANGUAGES = (
[u"xh", u"Xhosa"],
[u"yi", u"Yiddish"],
[u"yo", u"Yoruba"],
[u"za", u"Zhuang; Chuang"],
[u"za", u"Zhuang"],
[u"zu", u"Zulu"]
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment