Add Timed Transcripts Editor.

This commit is contained in:
Anton Stupak
2013-08-13 18:10:44 +03:00
committed by polesye
parent 1f7bb112bf
commit aecc20af6b
75 changed files with 8762 additions and 279 deletions

View File

@@ -51,6 +51,13 @@ def create_component_instance(step, category, component_type=None, is_advanced=F
module_count_before + 1))
@world.absorb
def click_new_component_button(step, component_button_css):
step.given('I have clicked the new unit button')
world.css_click(component_button_css)
def _click_advanced():
css = 'ul.problem-type-tabs a[href="#tab2"]'
world.css_click(css)
@@ -122,24 +129,29 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
----------
setting: the WebDriverElement object found in the browser
display_name: the string expected as the label
value: the expected field value
html: the expected field value
explicitly_set: True if the value is expected to have been explicitly set
for the problem, rather than derived from the defaults. This is verified
by the existence of a "Clear" button next to the field value.
"""
assert_equal(display_name, setting.find_by_css('.setting-label')[0].value)
assert_equal(display_name, setting.find_by_css('.setting-label')[0].html)
# 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'):
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'):
list_value = ', '.join(ele.find_by_css('input')[0].value for ele in setting.find_by_css('.videolist-settings-item'))
assert_equal(value, list_value)
else:
assert_equal(value, setting.find_by_css('.setting-input')[0].value)
settingClearButton = setting.find_by_css('.setting-clear')[0]
assert_equal(explicitly_set, settingClearButton.has_class('active'))
assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
# VideoList doesn't have clear button
if not setting.has_class('metadata-videolist-enum'):
settingClearButton = setting.find_by_css('.setting-clear')[0]
assert_equal(explicitly_set, settingClearButton.has_class('active'))
assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
@world.absorb

View File

@@ -0,0 +1,654 @@
Feature: Video Component Editor
As a course author, I want to be able to create video components.
# For transcripts acceptance tests there are 3 available caption
# files. They can be used to test various transcripts features. Two of
# them can be imported from YouTube.
#
# The length of each file name is 11 characters. This is because the
# YouTube's ID length is 11 characters. If file name is not of length 11,
# front-end validation will not pass.
#
# t__eq_exist - this file exists on YouTube, and can be imported
# via the transcripts menu; after import, this file will
# be equal to the one stored locally
# t_neq_exist - same as above, except local file will differ from the
# one stored on YouTube
# t_not_exist - this file does not exist on YouTube; it exists locally
#1
Scenario: Check input error messages
Given I have created a Video component
And I edit the component
#User inputs html5 links with equal extension
And I enter a "123.webm" source to field number 1
And I enter a "456.webm" source to field number 2
Then I see error message "file_type"
# Currently we are working with 2nd field. It means, that if 2nd field
# contain incorrect value, 1st and 3rd fields should be disabled until
# 2nd field will be filled by correct correct value
And I expect 1, 3 inputs are disabled
When I clear fields
And I expect inputs are enabled
#User input URL with incorrect format
And I enter a "htt://link.c" source to field number 1
Then I see error message "url_format"
# Currently we are working with 1st field. It means, that if 1st field
# contain incorrect value, 2nd and 3rd fields should be disabled until
# 1st field will be filled by correct correct value
And I expect 2, 3 inputs are disabled
# We are not clearing fields here,
# Because we changing same field.
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I do not see error message
And I expect inputs are enabled
#2
Scenario: Testing interaction with test youtube server
Given I have created a Video component with subtitles
And I edit the component
# first part of url will be substituted by mock_youtube_server address
# for t__eq_exist id server will respond with transcripts
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
# t__eq_exist subs locally not presented at this moment
And I see button "import"
# for t_not_exist id server will respond with 404
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "not found"
And I do not see button "import"
And I see button "disabled_download_to_edit"
#3
Scenario: Youtube id only: check "not found" and "import" states
Given I have created a Video component with subtitles
And I edit the component
# Not found: w/o local or server subs
And I remove "t_not_exist" transcripts id from store
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 value "" in the field "HTML5 Transcript"
# Import: w/o local but with server subs
And I remove "t__eq_exist" transcripts id from store
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I click button "import"
Then I see status message "found"
And I see button "upload_new_timed_transcripts"
And I see button "download_to_edit"
And I see value "t__eq_exist" in the field "HTML5 Transcript"
#4
Scenario: Youtube id only: check "Found" state
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "found"
And I see value "t_not_exist" in the field "HTML5 Transcript"
#5
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are equal
Given I have created a Video component with subtitles "t__eq_exist"
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
And I see status message "found"
And I see value "t__eq_exist" in the field "HTML5 Transcript"
#6
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are not equal
Given I have created a Video component with subtitles "t_neq_exist"
And I edit the component
And I enter a "http://youtu.be/t_neq_exist" source to field number 1
And I see status message "replace"
And I see button "replace"
And I click button "replace"
And I see status message "found"
And I see value "t_neq_exist" in the field "HTML5 Transcript"
#7
Scenario: html5 source only: check "Not Found" state
Given I have created a Video component
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "not found"
And I see value "" in the field "HTML5 Transcript"
#8
Scenario: html5 source only: check "Found" state
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
And I see value "t_not_exist" in the field "HTML5 Transcript"
#9
Scenario: User sets youtube_id w/o server but with local subs and one html5 link w/o subs
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "found"
And I enter a "test_video_name.mp4" source to field number 2
Then I see status message "found"
And I see value "t_not_exist" in the field "HTML5 Transcript"
#10
Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o subs
Given I have created a Video component
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I click button "import"
Then I see status message "found"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "found"
And I see value "t__eq_exist" in the field "HTML5 Transcript"
#11
Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o transcripts w/o import action, then another one html5 link w/o transcripts
Given I have created a Video component
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.webm" source to field number 3
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
#12
Scenario: Entering youtube (no importing), and 2 html5 sources without transcripts - "Not Found"
Given I have created a Video component
And I edit the component
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 "disabled_download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "not found"
And I see button "upload_new_timed_transcripts"
And I see button "disabled_download_to_edit"
And I enter a "t_not_exist.webm" source to field number 3
Then I see status message "not found"
And I see button "disabled_download_to_edit"
And I see button "upload_new_timed_transcripts"
#13
Scenario: Entering youtube with imported transcripts, and 2 html5 sources without transcripts - "Found"
Given I have created a Video component
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I click button "import"
Then I see status message "found"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.webm" source to field number 3
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
#14
Scenario: Entering youtube w/o transcripts - html5 w/o transcripts - html5 with transcripts
Given I have created a Video component with subtitles "t_neq_exist"
And I edit the component
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 "disabled_download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "not found"
And I see button "disabled_download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "t_neq_exist.webm" source to field number 3
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
#15
Scenario: Entering youtube w/o imported transcripts - html5 w/o transcripts w/o import - html5 with transcripts
Given I have created a Video component with subtitles "t_neq_exist"
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
And I enter a "t_neq_exist.webm" source to field number 3
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
#16
Scenario: Entering youtube w/o imported transcripts - html5 with transcripts - html5 w/o transcripts w/o import
Given I have created a Video component with subtitles "t_neq_exist"
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
And I enter a "t_neq_exist.mp4" source to field number 2
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.webm" source to field number 3
Then I see status message "not found"
And I see button "import"
And I see button "upload_new_timed_transcripts"
#17
Scenario: Entering youtube with imported transcripts - html5 with transcripts - html5 w/o transcripts
Given I have created a Video component with subtitles "t_neq_exist"
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I click button "import"
Then I see status message "found"
And I see button "upload_new_timed_transcripts"
And I enter a "t_neq_exist.mp4" source to field number 2
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.webm" source to field number 3
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
#18
Scenario: Entering youtube with imported transcripts - html5 w/o transcripts - html5 with transcripts
Given I have created a Video component with subtitles "t_neq_exist"
And I edit the component
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found"
And I see button "import"
And I click button "import"
Then I see status message "found"
And I see button "upload_new_timed_transcripts"
And I enter a "t_not_exist.mp4" source to field number 2
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I enter a "t_neq_exist.webm" source to field number 3
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
#19
Scenario: Entering html5 with transcripts - upload - youtube w/o transcripts
Given I have created a Video component with subtitles "t__eq_exist"
And I edit the component
And I enter a "t__eq_exist.mp4" source to field number 1
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"
Then I see status message "uploaded_successfully"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I see value "t__eq_exist" in the field "HTML5 Transcript"
And I enter a "http://youtu.be/t_not_exist" source to field number 2
Then I see status message "found"
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
Then I see status message "found"
#20
Scenario: Enter 2 HTML5 sources with transcripts, they are not the same, choose
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
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"
Then I see status message "uploaded_successfully"
And I see value "test_transcripts" in the field "HTML5 Transcript"
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 "t_not_exist.webm" number 2
And I click button "choose" number 2
And I see value "test_transcripts|t_not_exist" in the field "HTML5 Transcript"
#21
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I see value "t_not_exist" in the field "HTML5 Transcript"
And I save changes
And I edit the component
And I enter a "video_name_2.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I click button "use_existing"
And I see value "video_name_2" in the field "HTML5 Transcript"
And I enter a "video_name_3.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I click button "use_existing"
And I see value "video_name_3" in the field "HTML5 Transcript"
#22
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - click on use existing -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> change it to another one HTML5 source w/o transcripts - click on use existing
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I see value "t_not_exist" in the field "HTML5 Transcript"
And I save changes
And I edit the component
And I enter a "video_name_2.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I click button "use_existing"
And I see value "video_name_2" in the field "HTML5 Transcript"
And I enter a "video_name_3.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I enter a "video_name_4.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I click button "use_existing"
And I see value "video_name_4" in the field "HTML5 Transcript"
#23
Scenario: Work with 2 fields: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> add another one HTML5 source w/o transcripts - click on use existing
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
And I save changes
And I edit the component
And I enter a "video_name_2.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I enter a "video_name_3.webm" source to field number 2
Then I see status message "use existing"
And I see button "use_existing"
And I click button "use_existing"
And I see value "video_name_2|video_name_3" in the field "HTML5 Transcript"
#24 Uploading subtitles with different file name than file
Scenario: File name and name of subs are different
Given I have created a Video component
And I edit the component
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"
Then I see status message "uploaded_successfully"
And I see value "video_name_1" in the field "HTML5 Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
#25
# Video can have filled item.sub, but doesn't have subs file.
# In this case, after changing this video by another one without subs
# `Not found` message should appear ( not `use existing`).
Scenario: Video w/o subs - another video w/o subs - Not found message
Given I have created a Video component
And I edit the component
And I enter a "video_name_1.mp4" source to field number 1
Then I see status message "not found"
#26
Scenario: Subtitles are copied for every html5 video source
Given I have created a Video component
And I edit the component
And I enter a "video_name_1.mp4" source to field number 1
And I see status message "not found"
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"
Then I see status message "uploaded_successfully"
And I see value "video_name_1|video_name_2" in the field "HTML5 Transcript"
And I clear field number 1
Then I see status message "found"
And I see value "video_name_2" in the field "HTML5 Transcript"
#27
Scenario: Upload button for single youtube id.
Given I have created a Video component
And I edit the component
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"
Then I see status message "uploaded_successfully"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
#28
Scenario: Upload button for youtube id with html5 ids.
Given I have created a Video component
And I edit the component
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 enter a "video_name_1.mp4" source to field number 2
Then I see status message "not found"
And I see button "upload_new_timed_transcripts"
And I upload the transcripts file "test_transcripts.srt"
Then I see status message "uploaded_successfully"
And I clear field number 1
Then I see status message "found"
And I see value "video_name_1" in the field "HTML5 Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
#29
Scenario: Change transcripts field in Advanced tab
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "video_name_1.mp4" source to field number 1
Then I see status message "not found"
And I open tab "Advanced"
And I set value "t_not_exist" to the field "HTML5 Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
And I see value "video_name_1" in the field "HTML5 Transcript"
#30
Scenario: Check non-ascii (chinise) transcripts
Given I have created a Video component
And I edit the component
And I enter a "video_name_1.mp4" source to field number 1
Then I see status message "not found"
And I upload the transcripts file "chinese_transcripts.srt"
Then I see status message "uploaded_successfully"
And I save changes
Then when I view the video it does show the captions
#31
Scenario: Check saving module metadata on switching between tabs
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "video_name_1.mp4" source to field number 1
Then I see status message "not found"
And I open tab "Advanced"
And I set value "t_not_exist" to the field "HTML5 Transcript"
And I open tab "Basic"
Then I see status message "found"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
And I see value "video_name_1" in the field "HTML5 Transcript"
#32
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible w/o saving
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
And I open tab "Advanced"
And I set value "" to the field "HTML5 Transcript"
And I open tab "Basic"
Then I see status message "not found"
And I save changes
Then when I view the video it does not show the captions
And I edit the component
Then I see status message "not found"
And I see value "" in the field "HTML5 Transcript"
#33
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible with saving
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
And I save changes
And I edit the component
And I open tab "Advanced"
And I set value "" to the field "HTML5 Transcript"
And I open tab "Basic"
Then I see status message "not found"
And I save changes
Then when I view the video it does not show the captions
And I edit the component
Then I see status message "not found"
And I see value "" in the field "HTML5 Transcript"
#34
Scenario: Video with existing subs - Advanced tab - change to another one subs - Basic tab - Found message - Save - see correct subs
Given I have created a Video component with subtitles "t_not_exist"
And I edit the component
And I enter a "video_name_1.mp4" source to field number 1
Then I see status message "not found"
And I upload the transcripts file "chinese_transcripts.srt"
Then I see status message "uploaded_successfully"
And I save changes
Then when I view the video it does show the captions
And I edit the component
And I open tab "Advanced"
And I set value "t_not_exist" to the field "HTML5 Transcript"
And I open tab "Basic"
Then I see status message "found"
And I save changes
Then when I view the video it does show the captions
And I see "LILA FISHER: Hi, welcome to Edx." text in the captions

View File

@@ -0,0 +1,246 @@
# disable missing docstring
# pylint: disable=C0111
import os
from lettuce import world, step
from django.conf import settings
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
# We should wait 300 ms for event handler invocation + 200ms for safety.
DELAY = 0.5
ERROR_MESSAGES = {
'url_format': u'Incorrect url format.',
'file_type': u'Link types should be unique.',
}
STATUSES = {
'found': u'Timed Transcript Found',
'not found': u'No Timed Transcript',
'replace': u'Timed Transcript Conflict',
'uploaded_successfully': u'Timed Transcript uploaded successfully',
'use existing': u'Timed Transcript Not Updated',
}
SELECTORS = {
'error_bar': '.transcripts-error-message',
'url_inputs': '.videolist-settings-item input.input',
'collapse_link': '.collapse-action.collapse-setting',
'collapse_bar': '.videolist-extra-videos',
'status_bar': '.transcripts-message-status',
}
# button type , button css selector, button message
BUTTONS = {
'import': ('.setting-import', 'Import from YouTube'),
'download_to_edit': ('.setting-download', 'Download to Edit'),
'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download to Edit'),
'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Timed Transcript'),
'replace': ('.setting-replace', 'Yes, Replace EdX Timed Transcript with YouTube Timed Transcript'),
'choose': ('.setting-choose', 'Timed Transcript from {}'),
'use_existing': ('.setting-use-existing', 'Use Existing Timed Transcript'),
}
@step('I clear fields$')
def clear_fields(_step):
js_str = '''
$('{selector}')
.eq({index})
.prop('disabled', false)
.removeClass('is-disabled');
'''
for index in range(1, 4):
js = js_str.format(selector=SELECTORS['url_inputs'], index=index - 1)
world.browser.execute_script(js)
_step.given('I clear field number {0}'.format(index))
@step('I clear field number (.+)$')
def clear_field(_step, index):
index = int(index) - 1
world.css_fill(SELECTORS['url_inputs'], '', index)
# In some reason chromeDriver doesn't trigger 'input' event after filling
# field by an empty value. That's why we trigger it manually via jQuery.
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
@step('I expect (.+) inputs are disabled$')
def inputs_are_disabled(_step, indexes):
index_list = [int(i.strip()) - 1 for i in indexes.split(',')]
for index in index_list:
el = world.css_find(SELECTORS['url_inputs'])[index]
assert el['disabled']
@step('I expect inputs are enabled$')
def inputs_are_enabled(_step):
for index in range(3):
el = world.css_find(SELECTORS['url_inputs'])[index]
assert not el['disabled']
@step('I do not see error message$')
def i_do_not_see_error_message(_step):
world.wait(DELAY)
assert not world.css_visible(SELECTORS['error_bar'])
@step('I see error message "([^"]*)"$')
def i_see_error_message(_step, error):
world.wait(DELAY)
assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error.strip()])
@step('I do not see status message$')
def i_do_not_see_status_message(_step):
world.wait(DELAY)
world.wait_for_ajax_complete()
assert not world.css_visible(SELECTORS['status_bar'])
@step('I see status message "([^"]*)"$')
def i_see_status_message(_step, status):
world.wait(DELAY)
world.wait_for_ajax_complete()
assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status.strip()])
@step('I (.*)see button "([^"]*)"$')
def i_see_button(_step, not_see, button_type):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
if not_see.strip():
assert world.is_css_not_present(BUTTONS[button][0])
else:
assert world.css_has_text(BUTTONS[button][0], BUTTONS[button][1])
@step('I (.*)see (.*)button "([^"]*)" number (\d+)$')
def i_see_button_with_custom_text(_step, not_see, button_type, custom_text, index):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
custom_text = custom_text.strip()
index = int(index.strip()) - 1
if not_see.strip():
assert world.is_css_not_present(BUTTONS[button][0])
else:
assert world.css_has_text(BUTTONS[button][0], BUTTONS[button][1].format(custom_text), index)
@step('I click button "([^"]*)"$')
def click_button(_step, button_type):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
world.css_click(BUTTONS[button][0])
@step('I click button "([^"]*)" number (\d+)$')
def click_button_index(_step, button_type, index):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
index = int(index.strip()) - 1
world.css_click(BUTTONS[button][0], index)
@step('I remove "([^"]+)" transcripts id from store')
def remove_transcripts_from_store(_step, subs_id):
"""Remove from store, if transcripts content exists."""
filename = 'subs_{0}.srt.sjson'.format(subs_id.strip())
content_location = StaticContent.compute_location(
world.scenario_dict['COURSE'].org,
world.scenario_dict['COURSE'].number,
filename
)
try:
content = contentstore().find(content_location)
contentstore().delete(content.get_id())
print('Transcript file was removed from store.')
except NotFoundError:
print('Transcript file was NOT found and not removed.')
@step('I enter a "([^"]+)" source to field number (\d+)$')
def i_enter_a_source(_step, link, index):
world.wait(DELAY)
world.wait_for_ajax_complete()
index = int(index) - 1
if index is not 0 and not world.css_visible(SELECTORS['collapse_bar']):
world.css_click(SELECTORS['collapse_link'])
assert world.css_visible(SELECTORS['collapse_bar'])
world.css_fill(SELECTORS['url_inputs'], link, index)
@step('I upload the transcripts file "([^"]*)"$')
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))
@step('I see "([^"]*)" text in the captions')
def check_text_in_the_captions(_step, text):
assert world.browser.is_text_present(text.strip(), 5)
@step('I see value "([^"]*)" in the field "([^"]*)"$')
def check_transcripts_field(_step, values, field_name):
world.wait(DELAY)
world.wait_for_ajax_complete()
world.click_link_by_text('Advanced')
field_id = '#' + world.browser.find_by_xpath('//label[text()="%s"]' % field_name.strip())[0]['for']
values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')]
assert any(values_list)
world.click_link_by_text('Basic')
@step('I save changes$')
def save_changes(_step):
world.wait(DELAY)
world.wait_for_ajax_complete()
save_css = 'a.save-button'
world.css_click(save_css)
@step('I open tab "([^"]*)"$')
def open_tab(_step, tab_name):
world.click_link_by_text(tab_name.strip())
@step('I set value "([^"]*)" to the field "([^"]*)"$')
def set_value_transcripts_field(_step, value, field_name):
world.wait(DELAY)
world.wait_for_ajax_complete()
field_id = '#' + world.browser.find_by_xpath('//label[text()="%s"]' % field_name.strip())[0]['for']
world.css_fill(field_id, value.strip())

View File

@@ -19,12 +19,12 @@ Feature: CMS.Video Component Editor
@skip_sauce
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component with subtitles
And I have set "show captions" to False
And I have set "show transcript" to False
Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown when "show captions" is true
Given I have created a Video component with subtitles
And I have set "show captions" to True
And I have set "show transcript" to True
Then when I view the video it does show the captions

View File

@@ -5,14 +5,15 @@ from lettuce import world, step
from terrain.steps import reload_the_page
@step('I have set "show captions" to (.*)$')
@step('I have set "show transcript" to (.*)$')
def set_show_captions(step, setting):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
world.css_click('a.edit-button')
world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.browser.select('Show Captions', setting)
world.click_link_by_text('Advanced')
world.browser.select('Show Transcript', setting)
world.css_click('a.save-button')
@@ -33,12 +34,17 @@ def shows_captions(_step, show_captions):
@step('I see the correct video settings and default values$')
def correct_video_settings(_step):
expected_entries = [
# basic
['Display Name', 'Video', False],
['Download Track', '', False],
['Video URL', 'http://youtu.be/OEoXaMPEzfM, , ', False],
# advanced
['Display Name', 'Video', False],
['Download Transcript', '', False],
['Download Video', '', False],
['End Time', '0', False],
['HTML5 Timed Transcript', '', False],
['Show Captions', 'True', False],
['HTML5 Transcript', '', False],
['Show Transcript', 'True', False],
['Start Time', '0', False],
['Video Sources', '', False],
['Youtube ID', 'OEoXaMPEzfM', False],

View File

@@ -43,11 +43,7 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
@step('I have uploaded subtitles "([^"]*)"$')
def i_have_uploaded_subtitles(_step, sub_id):
_step.given('I go to the files and uploads page')
sub_id = sub_id.strip()
if not sub_id:
sub_id = 'OEoXaMPEzfM'
_step.given('I upload the test file "subs_{}.srt.sjson"'.format(sub_id))
_step.given('I upload the test file "subs_{}.srt.sjson"'.format(sub_id.strip()))
@step('when I view the (.*) it does not have autoplay enabled$')

View File

@@ -0,0 +1,45 @@
#pylint: disable=C0111
#pylint: disable=W0621
from xmodule.util.mock_youtube_server.mock_youtube_server import MockYoutubeServer
from lettuce import before, after, world
from django.conf import settings
import threading
from logging import getLogger
logger = getLogger(__name__)
@before.all
def setup_mock_youtube_server():
server_host = '127.0.0.1'
server_port = settings.VIDEO_PORT
address = (server_host, server_port)
# Create the mock server instance
server = MockYoutubeServer(address)
logger.debug("Youtube server started at {} port".format(str(server_port)))
server.time_to_response = 0.1 # seconds
server.address = address
# Start the server running in a separate daemon thread
# Because the thread is a daemon, it will terminate
# when the main thread terminates.
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
# Store the server instance in lettuce's world
# so that other steps can access it
# (and we can shut it down later)
world.youtube_server = server
@after.all
def teardown_mock_youtube_server(total):
# Stop the LTI server and free up the port
world.youtube_server.shutdown()

View File

@@ -1,14 +1,18 @@
from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
from xmodule.capa_module import CapaDescriptor
"""Tests for items views."""
import json
from xmodule.modulestore.django import modulestore
import datetime
from pytz import UTC
from django.core.urlresolvers import reverse
from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore.django import modulestore
class DeleteItem(CourseTestCase):
"""Tests for '/delete_item' url."""
def setUp(self):
""" Creates the test course with a static page in it. """
super(DeleteItem, self).setUp()

View File

@@ -0,0 +1,698 @@
"""Tests for items views."""
import os
import json
import tempfile
from uuid import uuid4
import copy
import textwrap
from pymongo import MongoClient
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.conf import settings
from contentstore import transcripts_utils
from contentstore.tests.utils import CourseTestCase
from cache_toolbox.core import del_cached_content
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class Basetranscripts(CourseTestCase):
"""Base test class for transcripts tests."""
org = 'MITx'
number = '999'
def clear_subs_content(self):
"""Remove, if transcripts content exists."""
for youtube_id in self.get_youtube_ids().values():
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename)
try:
content = contentstore().find(content_location)
contentstore().delete(content.get_id())
except NotFoundError:
pass
def setUp(self):
"""Create initial data."""
super(Basetranscripts, self).setUp()
# Add video module
data = {
'parent_location': str(self.course_location),
'category': 'video',
'type': 'video'
}
resp = self.client.post(reverse('create_item'), data)
self.item_location = json.loads(resp.content).get('id')
self.assertEqual(resp.status_code, 200)
# hI10vDNYz4M - valid Youtube ID with transcripts.
# JMD_ifUUfsU, AKqURZnYqpk, DYpADpL7jAY - valid Youtube IDs without transcripts.
data = '<video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" />'
modulestore().update_item(self.item_location, data)
self.item = modulestore().get_item(self.item_location)
# Remove all transcripts for current module.
self.clear_subs_content()
def get_youtube_ids(self):
"""Return youtube speeds and ids."""
item = modulestore().get_item(self.item_location)
return {
0.75: item.youtube_id_0_75,
1: item.youtube_id_1_0,
1.25: item.youtube_id_1_25,
1.5: item.youtube_id_1_5
}
def tearDown(self):
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
_CONTENTSTORE.clear()
class TestUploadtranscripts(Basetranscripts):
"""Tests for '/transcripts/upload' url."""
def setUp(self):
"""Create initial data."""
super(TestUploadtranscripts, self).setUp()
self.good_srt_file = tempfile.NamedTemporaryFile(suffix='.srt')
self.good_srt_file.write(textwrap.dedent("""
1
00:00:10,500 --> 00:00:13,000
Elephant's Dream
2
00:00:15,000 --> 00:00:18,000
At the left we can see...
"""))
self.good_srt_file.seek(0)
self.bad_data_srt_file = tempfile.NamedTemporaryFile(suffix='.srt')
self.bad_data_srt_file.write('Some BAD data')
self.bad_data_srt_file.seek(0)
self.bad_name_srt_file = tempfile.NamedTemporaryFile(suffix='.BAD')
self.bad_name_srt_file.write(textwrap.dedent("""
1
00:00:10,500 --> 00:00:13,000
Elephant's Dream
2
00:00:15,000 --> 00:00:18,000
At the left we can see...
"""))
self.bad_name_srt_file.seek(0)
def test_success_video_module_source_subs_uploading(self):
data = textwrap.dedent("""
<video youtube="">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</video>
""")
modulestore().update_item(self.item_location, data)
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'id': self.item_location,
'file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 200)
self.assertEqual(json.loads(resp.content).get('status'), 'Success')
item = modulestore().get_item(self.item_location)
self.assertEqual(item.sub, filename)
content_location = StaticContent.compute_location(
self.org, self.number, 'subs_{0}.srt.sjson'.format(filename))
self.assertTrue(contentstore().find(content_location))
def test_fail_data_without_id(self):
link = reverse('upload_transcripts')
resp = self.client.post(link, {'file': self.good_srt_file})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "id" form data.')
def test_fail_data_without_file(self):
link = reverse('upload_transcripts')
resp = self.client.post(link, {'id': self.item_location})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "file" form data.')
def test_fail_data_with_bad_location(self):
# Test for raising `InvalidLocationError` exception.
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'id': 'BAD_LOCATION',
'file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by location.")
# Test for raising `ItemNotFoundError` exception.
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'id': '{0}_{1}'.format(self.item_location, 'BAD_LOCATION'),
'file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by location.")
def test_fail_for_non_video_module(self):
# non_video module: setup
data = {
'parent_location': str(self.course_location),
'category': 'non_video',
'type': 'non_video'
}
resp = self.client.post(reverse('create_item'), data)
item_location = json.loads(resp.content).get('id')
data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />'
modulestore().update_item(item_location, data)
# non_video module: testing
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'id': item_location,
'file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')
def test_fail_bad_xml(self):
data = '<<<video youtube="0.75:JMD_ifUUfsU,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY" />'
modulestore().update_item(self.item_location, data)
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0]
resp = self.client.post(link, {
'id': self.item_location,
'file': self.good_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
# incorrect xml produces incorrect item category error
self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')
def test_fail_bad_data_srt_file(self):
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.bad_data_srt_file.name))[0]
resp = self.client.post(link, {
'id': self.item_location,
'file': self.bad_data_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'Something wrong with SubRip transcripts file during parsing.')
def test_fail_bad_name_srt_file(self):
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(self.bad_name_srt_file.name))[0]
resp = self.client.post(link, {
'id': self.item_location,
'file': self.bad_name_srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'We support only SubRip (*.srt) transcripts format.')
def test_undefined_file_extension(self):
srt_file = tempfile.NamedTemporaryFile(suffix='')
srt_file.write(textwrap.dedent("""
1
00:00:10,500 --> 00:00:13,000
Elephant's Dream
2
00:00:15,000 --> 00:00:18,000
At the left we can see...
"""))
srt_file.seek(0)
link = reverse('upload_transcripts')
filename = os.path.splitext(os.path.basename(srt_file.name))[0]
resp = self.client.post(link, {
'id': self.item_location,
'file': srt_file,
'video_list': json.dumps([{
'type': 'html5',
'video': filename,
'mode': 'mp4',
}])
})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'Undefined file extension.')
def tearDown(self):
super(TestUploadtranscripts, self).tearDown()
self.good_srt_file.close()
self.bad_data_srt_file.close()
self.bad_name_srt_file.close()
class TestDownloadtranscripts(Basetranscripts):
"""Tests for '/transcripts/download' url."""
def save_subs_to_store(self, subs, subs_id):
"""Save transcripts into `StaticContent`."""
filedata = json.dumps(subs, indent=2)
mime_type = 'application/json'
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename)
content = StaticContent(content_location, filename, mime_type, filedata)
contentstore().save(content)
del_cached_content(content_location)
return content_location
def remove_subs_from_store(self, subs_id):
"""Remove from store, if transcripts content exists."""
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename)
try:
content = contentstore().find(content_location)
contentstore().delete(content.get_id())
except NotFoundError:
pass
def test_success_download_youtube(self):
data = '<video youtube="1:JMD_ifUUfsU" />'
modulestore().update_item(self.item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1',
'subs #2',
'subs #3'
]
}
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': self.item_location, 'subs_id': "JMD_ifUUfsU"})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content, """0\n00:00:00,100 --> 00:00:00,200\nsubs #1\n\n1\n00:00:00,200 --> 00:00:00,240\nsubs #2\n\n2\n00:00:00,240 --> 00:00:00,380\nsubs #3\n\n""")
def test_success_download_nonyoutube(self):
subs_id = str(uuid4())
data = textwrap.dedent("""
<video youtube="" sub="{}">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</video>
""".format(subs_id))
modulestore().update_item(self.item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1',
'subs #2',
'subs #3'
]
}
self.save_subs_to_store(subs, subs_id)
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': self.item_location, 'subs_id': subs_id})
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp.content,
'0\n00:00:00,100 --> 00:00:00,200\nsubs #1\n\n1\n00:00:00,200 --> '
'00:00:00,240\nsubs #2\n\n2\n00:00:00,240 --> 00:00:00,380\nsubs #3\n\n'
)
transcripts_utils.remove_subs_from_store(subs_id, self.item)
def test_fail_data_without_file(self):
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': ''})
self.assertEqual(resp.status_code, 404)
resp = self.client.get(link, {})
self.assertEqual(resp.status_code, 404)
def test_fail_data_with_bad_location(self):
# Test for raising `InvalidLocationError` exception.
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': 'BAD_LOCATION'})
self.assertEqual(resp.status_code, 404)
# Test for raising `ItemNotFoundError` exception.
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': '{0}_{1}'.format(self.item_location, 'BAD_LOCATION')})
self.assertEqual(resp.status_code, 404)
def test_fail_for_non_video_module(self):
# Video module: setup
data = {
'parent_location': str(self.course_location),
'category': 'videoalpha',
'type': 'videoalpha'
}
resp = self.client.post(reverse('create_item'), data)
item_location = json.loads(resp.content).get('id')
subs_id = str(uuid4())
data = textwrap.dedent("""
<videoalpha youtube="" sub="{}">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</videoalpha>
""".format(subs_id))
modulestore().update_item(item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1',
'subs #2',
'subs #3'
]
}
self.save_subs_to_store(subs, subs_id)
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': item_location})
self.assertEqual(resp.status_code, 404)
def test_fail_nonyoutube_subs_dont_exist(self):
data = textwrap.dedent("""
<video youtube="" sub="UNDEFINED">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</video>
""")
modulestore().update_item(self.item_location, data)
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': self.item_location})
self.assertEqual(resp.status_code, 404)
def test_empty_youtube_attr_and_sub_attr(self):
data = textwrap.dedent("""
<video youtube="">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</video>
""")
modulestore().update_item(self.item_location, data)
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': self.item_location})
self.assertEqual(resp.status_code, 404)
def test_fail_bad_sjson_subs(self):
subs_id = str(uuid4())
data = textwrap.dedent("""
<video youtube="" sub="{}">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</video>
""".format(subs_id))
modulestore().update_item(self.item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1'
]
}
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
link = reverse('download_transcripts')
resp = self.client.get(link, {'id': self.item_location})
self.assertEqual(resp.status_code, 404)
class TestChecktranscripts(Basetranscripts):
"""Tests for '/transcripts/check' url."""
def save_subs_to_store(self, subs, subs_id):
"""Save transcripts into `StaticContent`."""
filedata = json.dumps(subs, indent=2)
mime_type = 'application/json'
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename)
content = StaticContent(content_location, filename, mime_type, filedata)
contentstore().save(content)
del_cached_content(content_location)
return content_location
def remove_subs_from_store(self, subs_id):
"""Remove from store, if transcripts content exists."""
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename)
try:
content = contentstore().find(content_location)
contentstore().delete(content.get_id())
except NotFoundError:
pass
def test_success_download_nonyoutube(self):
subs_id = str(uuid4())
data = textwrap.dedent("""
<video youtube="" sub="{}">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</video>
""".format(subs_id))
modulestore().update_item(self.item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1',
'subs #2',
'subs #3'
]
}
self.save_subs_to_store(subs, subs_id)
data = {
'id': self.item_location,
'videos': [{
'type': 'html5',
'video': subs_id,
'mode': 'mp4',
}]
}
link = reverse('check_transcripts')
resp = self.client.get(link, {'data': json.dumps(data)})
self.assertEqual(resp.status_code, 200)
self.assertDictEqual(
json.loads(resp.content),
{
u'status': u'Success',
u'subs': unicode(subs_id),
u'youtube_local': False,
u'is_youtube_mode': False,
u'youtube_server': False,
u'command': u'found',
u'current_item_subs': unicode(subs_id),
u'youtube_diff': True,
u'html5_local': [unicode(subs_id)],
u'html5_equal': False,
}
)
transcripts_utils.remove_subs_from_store(subs_id, self.item)
def test_check_youtube(self):
data = '<video youtube="1:JMD_ifUUfsU" />'
modulestore().update_item(self.item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1',
'subs #2',
'subs #3'
]
}
self.save_subs_to_store(subs, 'JMD_ifUUfsU')
link = reverse('check_transcripts')
data = {
'id': self.item_location,
'videos': [{
'type': 'youtube',
'video': 'JMD_ifUUfsU',
'mode': 'youtube',
}]
}
resp = self.client.get(link, {'data': json.dumps(data)})
self.assertEqual(resp.status_code, 200)
self.assertDictEqual(
json.loads(resp.content),
{
u'status': u'Success',
u'subs': u'JMD_ifUUfsU',
u'youtube_local': True,
u'is_youtube_mode': True,
u'youtube_server': False,
u'command': u'found',
u'current_item_subs': None,
u'youtube_diff': True,
u'html5_local': [],
u'html5_equal': False,
}
)
def test_fail_data_without_id(self):
link = reverse('check_transcripts')
data = {
'id': '',
'videos': [{
'type': '',
'video': '',
'mode': '',
}]
}
resp = self.client.get(link, {'data': json.dumps(data)})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by location.")
def test_fail_data_with_bad_location(self):
# Test for raising `InvalidLocationError` exception.
link = reverse('check_transcripts')
data = {
'id': '',
'videos': [{
'type': '',
'video': '',
'mode': '',
}]
}
resp = self.client.get(link, {'data': json.dumps(data)})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by location.")
# Test for raising `ItemNotFoundError` exception.
data = {
'id': '{0}_{1}'.format(self.item_location, 'BAD_LOCATION'),
'videos': [{
'type': '',
'video': '',
'mode': '',
}]
}
resp = self.client.get(link, {'data': json.dumps(data)})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by location.")
def test_fail_for_non_video_module(self):
# Not video module: setup
data = {
'parent_location': str(self.course_location),
'category': 'not_video',
'type': 'not_video'
}
resp = self.client.post(reverse('create_item'), data)
item_location = json.loads(resp.content).get('id')
subs_id = str(uuid4())
data = textwrap.dedent("""
<not_video youtube="" sub="{}">
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/>
<source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/>
</videoalpha>
""".format(subs_id))
modulestore().update_item(item_location, data)
subs = {
'start': [100, 200, 240],
'end': [200, 240, 380],
'text': [
'subs #1',
'subs #2',
'subs #3'
]
}
self.save_subs_to_store(subs, subs_id)
data = {
'id': item_location,
'videos': [{
'type': '',
'video': '',
'mode': '',
}]
}
link = reverse('check_transcripts')
resp = self.client.get(link, {'data': json.dumps(data)})
self.assertEqual(resp.status_code, 400)
self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')

View File

@@ -0,0 +1,407 @@
""" Tests for transcripts_utils. """
import unittest
from uuid import uuid4
import copy
import textwrap
from pymongo import MongoClient
from django.test.utils import override_settings
from django.conf import settings
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.exceptions import NotFoundError
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from contentstore import transcripts_utils
from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
class TestGenerateSubs(unittest.TestCase):
"""Tests for `generate_subs` function."""
def setUp(self):
self.source_subs = {
'start': [100, 200, 240, 390, 1000],
'end': [200, 240, 380, 1000, 1500],
'text': [
'subs #1',
'subs #2',
'subs #3',
'subs #4',
'subs #5'
]
}
def test_generate_subs_increase_speed(self):
subs = transcripts_utils.generate_subs(2, 1, self.source_subs)
self.assertDictEqual(
subs,
{
'start': [200, 400, 480, 780, 2000],
'end': [400, 480, 760, 2000, 3000],
'text': ['subs #1', 'subs #2', 'subs #3', 'subs #4', 'subs #5']
}
)
def test_generate_subs_decrease_speed_1(self):
subs = transcripts_utils.generate_subs(0.5, 1, self.source_subs)
self.assertDictEqual(
subs,
{
'start': [50, 100, 120, 195, 500],
'end': [100, 120, 190, 500, 750],
'text': ['subs #1', 'subs #2', 'subs #3', 'subs #4', 'subs #5']
}
)
def test_generate_subs_decrease_speed_2(self):
"""Test for correct devision during `generate_subs` process."""
subs = transcripts_utils.generate_subs(1, 2, self.source_subs)
self.assertDictEqual(
subs,
{
'start': [50, 100, 120, 195, 500],
'end': [100, 120, 190, 500, 750],
'text': ['subs #1', 'subs #2', 'subs #3', 'subs #4', 'subs #5']
}
)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class TestSaveSubsToStore(ModuleStoreTestCase):
"""Tests for `save_subs_to_store` function."""
org = 'MITx'
number = '999'
display_name = 'Test course'
def clear_subs_content(self):
"""Remove, if subtitles content exists."""
try:
content = contentstore().find(self.content_location)
contentstore().delete(content.get_id())
except NotFoundError:
pass
def setUp(self):
self.course = CourseFactory.create(
org=self.org, number=self.number, display_name=self.display_name)
self.subs = {
'start': [100, 200, 240, 390, 1000],
'end': [200, 240, 380, 1000, 1500],
'text': [
'subs #1',
'subs #2',
'subs #3',
'subs #4',
'subs #5'
]
}
self.subs_id = str(uuid4())
filename = 'subs_{0}.srt.sjson'.format(self.subs_id)
self.content_location = StaticContent.compute_location(
self.org, self.number, filename
)
# incorrect subs
self.unjsonable_subs = set([1]) # set can't be serialized
self.unjsonable_subs_id = str(uuid4())
filename_unjsonable = 'subs_{0}.srt.sjson'.format(self.unjsonable_subs_id)
self.content_location_unjsonable = StaticContent.compute_location(
self.org, self.number, filename_unjsonable
)
self.clear_subs_content()
def test_save_subs_to_store(self):
with self.assertRaises(NotFoundError):
contentstore().find(self.content_location)
result_location = transcripts_utils.save_subs_to_store(
self.subs,
self.subs_id,
self.course)
self.assertTrue(contentstore().find(self.content_location))
self.assertEqual(result_location, self.content_location)
def test_save_unjsonable_subs_to_store(self):
"""
Assures that subs, that can't be dumped, can't be found later.
"""
with self.assertRaises(NotFoundError):
contentstore().find(self.content_location_unjsonable)
with self.assertRaises(TypeError):
transcripts_utils.save_subs_to_store(
self.unjsonable_subs,
self.unjsonable_subs_id,
self.course)
with self.assertRaises(NotFoundError):
contentstore().find(self.content_location_unjsonable)
def tearDown(self):
self.clear_subs_content()
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
_CONTENTSTORE.clear()
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class TestDownloadYoutubeSubs(ModuleStoreTestCase):
"""Tests for `download_youtube_subs` function."""
org = 'MITx'
number = '999'
display_name = 'Test course'
def clear_subs_content(self, youtube_subs):
"""Remove, if subtitles content exists."""
for subs_id in youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename
)
try:
content = contentstore().find(content_location)
contentstore().delete(content.get_id())
except NotFoundError:
pass
def setUp(self):
self.course = CourseFactory.create(
org=self.org, number=self.number, display_name=self.display_name)
def tearDown(self):
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
_CONTENTSTORE.clear()
def test_success_downloading_subs(self):
good_youtube_subs = {
0.5: 'JMD_ifUUfsU',
1.0: 'hI10vDNYz4M',
2.0: 'AKqURZnYqpk'
}
self.clear_subs_content(good_youtube_subs)
# Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
transcripts_utils.download_youtube_subs(good_youtube_subs, self.course)
# Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename
)
self.assertTrue(contentstore().find(content_location))
self.clear_subs_content(good_youtube_subs)
def test_fail_downloading_subs(self):
bad_youtube_subs = {
0.5: 'BAD_YOUTUBE_ID1',
1.0: 'BAD_YOUTUBE_ID2',
2.0: 'BAD_YOUTUBE_ID3'
}
self.clear_subs_content(bad_youtube_subs)
with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course)
# Check assets status after importing subtitles.
for subs_id in bad_youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename
)
with self.assertRaises(NotFoundError):
contentstore().find(content_location)
self.clear_subs_content(bad_youtube_subs)
def test_success_downloading_chinise_transcripts(self):
good_youtube_subs = {
1.0: 'j_jEn79vS3g', # Chinese, utf-8
}
self.clear_subs_content(good_youtube_subs)
# Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
transcripts_utils.download_youtube_subs(good_youtube_subs, self.course)
# Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename
)
self.assertTrue(contentstore().find(content_location))
self.clear_subs_content(good_youtube_subs)
class TestGenerateSubsFromSource(TestDownloadYoutubeSubs):
"""Tests for `generate_subs_from_source` function."""
def test_success_generating_subs(self):
youtube_subs = {
0.5: 'JMD_ifUUfsU',
1.0: 'hI10vDNYz4M',
2.0: 'AKqURZnYqpk'
}
srt_filedata = textwrap.dedent("""
1
00:00:10,500 --> 00:00:13,000
Elephant's Dream
2
00:00:15,000 --> 00:00:18,000
At the left we can see...
""")
self.clear_subs_content(youtube_subs)
# Check transcripts_utils.TranscriptsGenerationException not thrown
transcripts_utils.generate_subs_from_source(youtube_subs, 'srt', srt_filedata, self.course)
# Check assets status after importing subtitles.
for subs_id in youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.org, self.number, filename
)
self.assertTrue(contentstore().find(content_location))
self.clear_subs_content(youtube_subs)
def test_fail_bad_subs_type(self):
youtube_subs = {
0.5: 'JMD_ifUUfsU',
1.0: 'hI10vDNYz4M',
2.0: 'AKqURZnYqpk'
}
srt_filedata = textwrap.dedent("""
1
00:00:10,500 --> 00:00:13,000
Elephant's Dream
2
00:00:15,000 --> 00:00:18,000
At the left we can see...
""")
with self.assertRaises(transcripts_utils.TranscriptsGenerationException) as cm:
transcripts_utils.generate_subs_from_source(youtube_subs, 'BAD_FORMAT', srt_filedata, self.course)
exception_message = cm.exception.message
self.assertEqual(exception_message, "We support only SubRip (*.srt) transcripts format.")
def test_fail_bad_subs_filedata(self):
youtube_subs = {
0.5: 'JMD_ifUUfsU',
1.0: 'hI10vDNYz4M',
2.0: 'AKqURZnYqpk'
}
srt_filedata = """BAD_DATA"""
with self.assertRaises(transcripts_utils.TranscriptsGenerationException) as cm:
transcripts_utils.generate_subs_from_source(youtube_subs, 'srt', srt_filedata, self.course)
exception_message = cm.exception.message
self.assertEqual(exception_message, "Something wrong with SubRip transcripts file during parsing.")
class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs):
"""Tests for `generate_srt_from_sjson` function."""
def test_success_generating_subs(self):
sjson_subs = {
'start': [100, 200, 240, 390, 54000],
'end': [200, 240, 380, 1000, 78400],
'text': [
'subs #1',
'subs #2',
'subs #3',
'subs #4',
'subs #5'
]
}
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1)
self.assertTrue(srt_subs)
expected_subs = [
'00:00:00,100 --> 00:00:00,200\nsubs #1',
'00:00:00,200 --> 00:00:00,240\nsubs #2',
'00:00:00,240 --> 00:00:00,380\nsubs #3',
'00:00:00,390 --> 00:00:01,000\nsubs #4',
'00:00:54,000 --> 00:01:18,400\nsubs #5',
]
for sub in expected_subs:
self.assertIn(sub, srt_subs)
def test_success_generating_subs_speed_up(self):
sjson_subs = {
'start': [100, 200, 240, 390, 54000],
'end': [200, 240, 380, 1000, 78400],
'text': [
'subs #1',
'subs #2',
'subs #3',
'subs #4',
'subs #5'
]
}
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 0.5)
self.assertTrue(srt_subs)
expected_subs = [
'00:00:00,050 --> 00:00:00,100\nsubs #1',
'00:00:00,100 --> 00:00:00,120\nsubs #2',
'00:00:00,120 --> 00:00:00,190\nsubs #3',
'00:00:00,195 --> 00:00:00,500\nsubs #4',
'00:00:27,000 --> 00:00:39,200\nsubs #5',
]
for sub in expected_subs:
self.assertIn(sub, srt_subs)
def test_success_generating_subs_speed_down(self):
sjson_subs = {
'start': [100, 200, 240, 390, 54000],
'end': [200, 240, 380, 1000, 78400],
'text': [
'subs #1',
'subs #2',
'subs #3',
'subs #4',
'subs #5'
]
}
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 2)
self.assertTrue(srt_subs)
expected_subs = [
'00:00:00,200 --> 00:00:00,400\nsubs #1',
'00:00:00,400 --> 00:00:00,480\nsubs #2',
'00:00:00,480 --> 00:00:00,760\nsubs #3',
'00:00:00,780 --> 00:00:02,000\nsubs #4',
'00:01:48,000 --> 00:02:36,800\nsubs #5',
]
for sub in expected_subs:
self.assertIn(sub, srt_subs)
def test_fail_generating_subs(self):
sjson_subs = {
'start': [100, 200],
'end': [100],
'text': [
'subs #1',
'subs #2'
]
}
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1)
self.assertFalse(srt_subs)

View File

@@ -1,12 +1,22 @@
""" Tests for utils. """
from contentstore import utils
import mock
import unittest
import collections
import copy
import json
from uuid import uuid4
from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.exceptions import NotFoundError
class LMSLinksTestCase(TestCase):
""" Tests for LMS links. """
@@ -88,8 +98,10 @@ class ExtraPanelTabTestCase(TestCase):
else:
return []
def get_course_with_tabs(self, tabs=[]):
def get_course_with_tabs(self, tabs=None):
""" Returns a mock course object with a tabs attribute. """
if tabs is None:
tabs = []
course = collections.namedtuple('MockCourse', ['tabs'])
if isinstance(tabs, basestring):
course.tabs = self.get_tab_type_dicts(tabs)

View File

@@ -61,6 +61,7 @@ class CourseTestCase(ModuleStoreTestCase):
number='999',
display_name='Robot Super Course',
)
self.course_location = self.course.location
def createNonStaffAuthedUserClient(self):
"""

View File

@@ -0,0 +1,341 @@
"""
Utility functions for transcripts.
++++++++++++++++++++++++++++++++++
"""
import copy
import json
import requests
import logging
from pysrt import SubRipTime, SubRipItem, SubRipFile
from lxml import etree
from cache_toolbox.core import del_cached_content
from django.conf import settings
from xmodule.exceptions import NotFoundError
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from .utils import get_modulestore
log = logging.getLogger(__name__)
class TranscriptsGenerationException(Exception):
pass
class GetTranscriptsFromYouTubeException(Exception):
pass
class TranscriptsRequestValidationException(Exception):
pass
def generate_subs(speed, source_speed, source_subs):
"""
Generate transcripts from one speed to another speed.
Args:
`speed`: float, for this speed subtitles will be generated,
`source_speed`: float, speed of source_subs
`soource_subs`: dict, existing subtitles for speed `source_speed`.
Returns:
`subs`: dict, actual subtitles.
"""
if speed == source_speed:
return source_subs
coefficient = 1.0 * speed / source_speed
subs = {
'start': [
int(round(timestamp * coefficient)) for
timestamp in source_subs['start']
],
'end': [
int(round(timestamp * coefficient)) for
timestamp in source_subs['end']
],
'text': source_subs['text']}
return subs
def save_subs_to_store(subs, subs_id, item):
"""
Save transcripts into `StaticContent`.
Args:
`subs_id`: str, subtitles id
`item`: video module instance
Returns: location of saved subtitles.
"""
filedata = json.dumps(subs, indent=2)
mime_type = 'application/json'
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
content = StaticContent(content_location, filename, mime_type, filedata)
contentstore().save(content)
del_cached_content(content_location)
return content_location
def get_transcripts_from_youtube(youtube_id):
"""
Gets transcripts from youtube for youtube_id.
Parses only utf-8 encoded transcripts.
Other encodings are not supported at the moment.
Returns (status, transcripts): bool, dict.
"""
utf8_parser = etree.XMLParser(encoding='utf-8')
youtube_api = copy.deepcopy(settings.YOUTUBE_API)
youtube_api['params']['v'] = youtube_id
data = requests.get(youtube_api['url'], params=youtube_api['params'])
if data.status_code != 200 or not data.text:
msg = "Can't receive transcripts from Youtube for {}. Status code: {}.".format(
youtube_id, data.status_code)
raise GetTranscriptsFromYouTubeException(msg)
sub_starts, sub_ends, sub_texts = [], [], []
xmltree = etree.fromstring(data.content, parser=utf8_parser)
for element in xmltree:
if element.tag == "text":
start = float(element.get("start"))
duration = float(element.get("dur", 0)) # dur is not mandatory
text = element.text
end = start + duration
if text:
# Start and end should be ints representing the millisecond timestamp.
sub_starts.append(int(start * 1000))
sub_ends.append(int((end + 0.0001) * 1000))
sub_texts.append(text.replace('\n', ' '))
return {'start': sub_starts, 'end': sub_ends, 'text': sub_texts}
def download_youtube_subs(youtube_subs, item):
"""
Download transcripts from Youtube and save them to assets.
Args:
youtube_subs: dictionary of `speed: youtube_id` key:value pairs.
item: video module instance.
Returns: None, if transcripts were successfully downloaded and saved.
Otherwise raises GetTranscriptsFromYouTubeException.
"""
highest_speed = highest_speed_subs = None
missed_speeds = []
# Iterate from lowest to highest speed and try to do download transcripts
# from the Youtube service.
for speed, youtube_id in sorted(youtube_subs.iteritems()):
if not youtube_id:
continue
try:
subs = get_transcripts_from_youtube(youtube_id)
if not subs: # if empty subs are returned
raise GetTranscriptsFromYouTubeException
except GetTranscriptsFromYouTubeException:
missed_speeds.append(speed)
continue
save_subs_to_store(subs, youtube_id, item)
log.info(
"Transcripts for YouTube id %s (speed %s)"
"are downloaded and saved.", youtube_id, speed
)
highest_speed = speed
highest_speed_subs = subs
if not highest_speed:
raise GetTranscriptsFromYouTubeException("Can't find any transcripts on the Youtube service.")
# When we exit from the previous loop, `highest_speed` and `highest_speed_subs`
# are the transcripts data for the highest speed available on the
# Youtube service. We use the highest speed as main speed for the
# generation other transcripts, cause during calculation timestamps
# for lower speeds we just use multiplication instead of division.
for speed in missed_speeds: # Generate transcripts for missed speeds.
save_subs_to_store(
generate_subs(speed, highest_speed, highest_speed_subs),
youtube_subs[speed],
item
)
log.info(
"Transcripts for YouTube id %s (speed %s)"
"are generated from YouTube id %s (speed %s) and saved",
youtube_subs[speed], speed,
youtube_subs[highest_speed],
highest_speed
)
def remove_subs_from_store(subs_id, item):
"""
Remove from store, if transcripts content exists.
"""
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
try:
content = contentstore().find(content_location)
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):
"""Generate transcripts from source files (like SubRip format, etc.)
and save them to assets for `item` module.
We expect, that speed of source subs equal to 1
:param speed_subs: dictionary {speed: sub_id, ...}
:param subs_type: type of source subs: "srt", ...
:param subs_filedata:unicode, content of source subs.
:param item: module object.
:returns: True, if all subs are generated and saved successfully.
"""
if subs_type != 'srt':
raise TranscriptsGenerationException("We support only SubRip (*.srt) transcripts format.")
try:
srt_subs_obj = SubRipFile.from_string(subs_filedata)
except Exception as e:
raise TranscriptsGenerationException(
"Something wrong with SubRip transcripts file during parsing. "
"Inner message is {}".format(e.message)
)
if not srt_subs_obj:
raise TranscriptsGenerationException("Something wrong with SubRip transcripts file during parsing.")
sub_starts = []
sub_ends = []
sub_texts = []
for sub in srt_subs_obj:
sub_starts.append(sub.start.ordinal)
sub_ends.append(sub.end.ordinal)
sub_texts.append(sub.text.replace('\n', ' '))
subs = {
'start': sub_starts,
'end': sub_ends,
'text': sub_texts}
for speed, subs_id in speed_subs.iteritems():
save_subs_to_store(
generate_subs(speed, 1, subs),
subs_id,
item
)
return subs
def generate_srt_from_sjson(sjson_subs, speed):
"""Generate transcripts with speed = 1.0 from sjson to SubRip (*.srt).
:param sjson_subs: "sjson" subs.
:param speed: speed of `sjson_subs`.
:returns: "srt" subs.
"""
output = ''
equal_len = len(sjson_subs['start']) == len(sjson_subs['end']) == len(sjson_subs['text'])
if not equal_len:
return output
sjson_speed_1 = generate_subs(speed, 1, sjson_subs)
for i in range(len(sjson_speed_1['start'])):
item = SubRipItem(
index=i,
start=SubRipTime(milliseconds=sjson_speed_1['start'][i]),
end=SubRipTime(milliseconds=sjson_speed_1['end'][i]),
text=sjson_speed_1['text'][i]
)
output += (unicode(item))
output += '\n'
return output
def save_module(item):
"""
Proceed with additional save operations.
"""
item.save()
store = get_modulestore(Location(item.id))
store.update_metadata(item.id, own_metadata(item))
def copy_or_rename_transcript(new_name, old_name, item, delete_old=False):
"""
Renames `old_name` transcript file in storage to `new_name`.
If `old_name` is not found in storage, raises `NotFoundError`.
If `delete_old` is True, removes `old_name` files from storage.
"""
filename = 'subs_{0}.srt.sjson'.format(old_name)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
transcripts = contentstore().find(content_location).data
save_subs_to_store(json.loads(transcripts), new_name, item)
item.sub = new_name
save_module(item)
if delete_old:
remove_subs_from_store(old_name, item)
def manage_video_subtitles_save(old_item, new_item):
"""
Does some specific things, that can be done only on save.
Video player item has some video fields: HTML5 ones and Youtube one.
1. If value of `sub` field of `new_item` is different from values of video fields of `new_item`,
and `new_item.sub` file is present, then code in this function creates copies of
`new_item.sub` file with new names. That names are equal to values of video fields of `new_item`
After that `sub` field of `new_item` is changed to one of values of video fields.
This whole action ensures that after user changes video fields, proper `sub` files, corresponding
to new values of video fields, will be presented in system.
old_item is not used here, but is added for future changes.
"""
# 1.
# assume '.' and '/' are not in filenames
html5_ids = [x.split('/')[-1].split('.')[0] for x in new_item.html5_sources]
possible_video_id_list = [new_item.youtube_id_1_0] + html5_ids
sub_name = new_item.sub
for video_id in possible_video_id_list:
if not video_id:
continue
# copy_or_rename_transcript changes item.sub of module
try:
# updates item.sub with `video_id`, if it is successful.
copy_or_rename_transcript(video_id, sub_name, new_item)
except NotFoundError:
# subtitles file `sub_name` is not presented in the system. Nothing to copy or rename.
log.debug(
"Copying %s file content to %s name is failed, "
"original file does not exist.",
sub_name, video_id
)

View File

@@ -1,20 +1,23 @@
#pylint: disable=E1103, E1101
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
import copy
import logging
import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
from django.conf import settings
from django.utils.translation import ugettext as _
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from django_comment_common.utils import unseed_permissions_roles
from auth.authz import _delete_course_group
from xmodule.modulestore.store_utilities import delete_course
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
log = logging.getLogger(__name__)

View File

@@ -16,6 +16,7 @@ from .preview import *
from .public import *
from .user import *
from .tabs import *
from .transcripts_ajax import *
try:
from .dev import *
except ImportError:

View File

@@ -1,26 +1,33 @@
"""Views for items (modules)."""
import logging
from uuid import uuid4
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from util.json_request import expect_json, JsonResponse
from ..transcripts_utils import manage_video_subtitles_save
from ..utils import get_modulestore
from .access import has_access
from .helpers import _xmodule_recurse
from xmodule.x_module import XModuleDescriptor
__all__ = ['save_item', 'create_item', 'delete_item']
log = logging.getLogger(__name__)
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
log = logging.getLogger(__name__)
@login_required
@expect_json
@@ -52,8 +59,13 @@ def save_item(request):
inspect.currentframe().f_back.f_code.co_name,
inspect.currentframe().f_back.f_code.co_filename
)
return HttpResponseBadRequest()
return JsonResponse({"error": "Request missing required attribute 'id'."}, 400)
try:
old_item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location"}, 404)
# check permissions for this user within this course
if not has_access(request.user, item_location):
@@ -101,12 +113,16 @@ def save_item(request):
# commit to datastore
store.update_metadata(item_location, own_metadata(existing_item))
if existing_item.category == 'video':
manage_video_subtitles_save(old_item, existing_item)
return JsonResponse()
@login_required
@expect_json
def create_item(request):
"""View for create items."""
parent_location = Location(request.POST['parent_location'])
category = request.POST['category']
@@ -149,6 +165,7 @@ def create_item(request):
@login_required
@expect_json
def delete_item(request):
"""View for removing items."""
item_location = request.POST['id']
item_location = Location(item_location)

View File

@@ -0,0 +1,555 @@
"""
Actions manager for transcripts ajax calls.
+++++++++++++++++++++++++++++++++++++++++++
Module do not support rollback (pressing "Cancel" button in Studio)
All user changes are saved immediately.
"""
import copy
import os
import logging
import json
import requests
from django.http import HttpResponse, Http404
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.conf import settings
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from util.json_request import JsonResponse
from ..transcripts_utils import (
generate_subs_from_source,
generate_srt_from_sjson, remove_subs_from_store,
download_youtube_subs, get_transcripts_from_youtube,
copy_or_rename_transcript,
save_module,
manage_video_subtitles_save,
TranscriptsGenerationException,
GetTranscriptsFromYouTubeException,
TranscriptsRequestValidationException
)
from .access import has_access
__all__ = [
'upload_transcripts',
'download_transcripts',
'check_transcripts',
'choose_transcripts',
'replace_transcripts',
'rename_transcripts',
'save_transcripts'
]
log = logging.getLogger(__name__)
def error_response(response, message, status_code=400):
"""
Simplify similar actions: log message and return JsonResponse with message included in response.
By default return 400 (Bad Request) Response.
"""
log.debug(message)
response['status'] = message
return JsonResponse(response, status_code)
@login_required
def upload_transcripts(request):
"""
Upload transcripts for current module.
returns: response dict::
status: 'Success' and HTTP 200 or 'Error' and HTTP 400.
subs: Value of uploaded and saved html5 sub field in video item.
"""
response = {
'status': 'Unknown server error',
'subs': '',
}
item_location = request.POST.get('id')
if not item_location:
return error_response(response, 'POST data without "id" form data.')
# This is placed before has_access() to validate item_location,
# because has_access() raises InvalidLocationError if location is invalid.
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
return error_response(response, "Can't find item by location.")
# Check permissions for this user within this course.
if not has_access(request.user, item_location):
raise PermissionDenied()
if 'file' not in request.FILES:
return error_response(response, 'POST data without "file" form data.')
video_list = request.POST.get('video_list')
if not video_list:
return error_response(response, 'POST data without video names.')
try:
video_list = json.loads(video_list)
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
if '.' not in source_subs_filename:
return error_response(response, "Undefined file extension.")
basename = os.path.basename(source_subs_filename)
source_subs_name = os.path.splitext(basename)[0]
source_subs_ext = os.path.splitext(basename)[1][1:]
if item.category != 'video':
return error_response(response, 'Transcripts are supported only for "video" modules.')
# Allow upload only if any video link is presented
if video_list:
sub_attr = source_subs_name
try: # Generate and save for 1.0 speed, will create subs_sub_attr.srt.sjson subtitles file in storage.
generate_subs_from_source({1: sub_attr}, source_subs_ext, source_subs_filedata, item)
except TranscriptsGenerationException as e:
return error_response(response, e.message)
statuses = {}
for video_dict in video_list:
video_name = video_dict['video']
# We are creating transcripts for every video source,
# for the case that in future, some of video sources can be deleted.
statuses[video_name] = copy_or_rename_transcript(video_name, sub_attr, item)
try:
# updates item.sub with `video_name` if it is successful.
copy_or_rename_transcript(video_name, sub_attr, item)
selected_name = video_name # name to write to item.sub field, chosen at random.
except NotFoundError:
# subtitles file `sub_attr` is not presented in the system. Nothing to copy or rename.
return error_response(response, "Can't find transcripts in storage for {}".format(sub_attr))
item.sub = selected_name # write one of new subtitles names to item.sub attribute.
save_module(item)
response['subs'] = item.sub
response['status'] = 'Success'
else:
return error_response(response, 'Empty video sources.')
return JsonResponse(response)
@login_required
def download_transcripts(request):
"""
Passes to user requested transcripts file.
Raises Http404 if unsuccessful.
"""
item_location = request.GET.get('id')
if not item_location:
log.debug('GET data without "id" property.')
raise Http404
# This is placed before has_access() to validate item_location,
# because has_access() raises InvalidLocationError if location is invalid.
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
log.debug("Can't find item by location.")
raise Http404
# Check permissions for this user within this course.
if not has_access(request.user, item_location):
raise PermissionDenied()
subs_id = request.GET.get('subs_id')
if not subs_id:
log.debug('GET data without "subs_id" property.')
raise Http404
if item.category != 'video':
log.debug('transcripts are supported only for video" modules.')
raise Http404
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
try:
sjson_transcripts = contentstore().find(content_location)
log.debug("Downloading subs for %s id", subs_id)
str_subs = generate_srt_from_sjson(json.loads(sjson_transcripts.data), speed=1.0)
if not str_subs:
log.debug('generate_srt_from_sjson produces no subtitles')
raise Http404
response = HttpResponse(str_subs, content_type='application/x-subrip')
response['Content-Disposition'] = 'attachment; filename="{0}.srt"'.format(subs_id)
return response
except NotFoundError:
log.debug("Can't find content in storage for %s subs", subs_id)
raise Http404
@login_required
def check_transcripts(request):
"""
Check state of transcripts availability.
request.GET['data'] has key `videos`, which can contain any of the following::
[
{u'type': u'youtube', u'video': u'OEoXaMPEzfM', u'mode': u'youtube'},
{u'type': u'html5', u'video': u'video1', u'mode': u'mp4'}
{u'type': u'html5', u'video': u'video2', u'mode': u'webm'}
]
`type` is youtube or html5
`video` is html5 or youtube video_id
`mode` is youtube, ,p4 or webm
Returns transcripts_presence dict::
html5_local: list of html5 ids, if subtitles exist locally for them;
is_youtube_mode: bool, if we have youtube_id, and as youtube mode is of higher priority, reflect this with flag;
youtube_local: bool, if youtube transcripts exist locally;
youtube_server: bool, if youtube transcripts exist on server;
youtube_diff: bool, if youtube transcripts exist on youtube server, and are different from local youtube ones;
current_item_subs: string, value of item.sub field;
status: string, 'Error' or 'Success';
subs: string, new value of item.sub field, that should be set in module;
command: string, action to front-end what to do and what to show to user.
"""
transcripts_presence = {
'html5_local': [],
'html5_equal': False,
'is_youtube_mode': False,
'youtube_local': False,
'youtube_server': False,
'youtube_diff': True,
'current_item_subs': None,
'status': 'Error',
}
try:
__, videos, item = validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(transcripts_presence, e.message)
transcripts_presence['status'] = 'Success'
filename = 'subs_{0}.srt.sjson'.format(item.sub)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
try:
local_transcripts = contentstore().find(content_location).data
transcripts_presence['current_item_subs'] = item.sub
except NotFoundError:
pass
# Check for youtube transcripts presence
youtube_id = videos.get('youtube', None)
if youtube_id:
transcripts_presence['is_youtube_mode'] = True
# youtube local
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
try:
local_transcripts = contentstore().find(content_location).data
transcripts_presence['youtube_local'] = True
except NotFoundError:
log.debug("Can't find transcripts in storage for youtube id: %s", youtube_id)
# youtube server
youtube_api = copy.deepcopy(settings.YOUTUBE_API)
youtube_api['params']['v'] = youtube_id
youtube_response = requests.get(youtube_api['url'], params=youtube_api['params'])
if youtube_response.status_code == 200 and youtube_response.text:
transcripts_presence['youtube_server'] = True
#check youtube local and server transcripts for equality
if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
try:
youtube_server_subs = get_transcripts_from_youtube(youtube_id)
if json.loads(local_transcripts) == youtube_server_subs: # check transcripts for equality
transcripts_presence['youtube_diff'] = False
except GetTranscriptsFromYouTubeException:
pass
# Check for html5 local transcripts presence
html5_subs = []
for html5_id in videos['html5']:
filename = 'subs_{0}.srt.sjson'.format(html5_id)
content_location = StaticContent.compute_location(
item.location.org, item.location.course, filename
)
try:
html5_subs.append(contentstore().find(content_location).data)
transcripts_presence['html5_local'].append(html5_id)
except NotFoundError:
log.debug("Can't find transcripts in storage for non-youtube video_id: %s", html5_id)
if len(html5_subs) == 2: # check html5 transcripts for equality
transcripts_presence['html5_equal'] = json.loads(html5_subs[0]) == json.loads(html5_subs[1])
command, subs_to_use = transcripts_logic(transcripts_presence, videos)
transcripts_presence.update({
'command': command,
'subs': subs_to_use,
})
return JsonResponse(transcripts_presence)
def transcripts_logic(transcripts_presence, videos):
"""
By `transcripts_presence` content, figure what show to user:
returns: `command` and `subs`.
`command`: string, action to front-end what to do and what show to user.
`subs`: string, new value of item.sub field, that should be set in module.
`command` is one of::
replace: replace local youtube subtitles with server one's
found: subtitles are found
import: import subtitles from youtube server
choose: choose one from two html5 subtitles
not found: subtitles are not found
"""
command = None
# new value of item.sub field, that should be set in module.
subs = ''
# youtube transcripts are of high priority than html5 by design
if (
transcripts_presence['youtube_diff'] and
transcripts_presence['youtube_local'] and
transcripts_presence['youtube_server']): # youtube server and local exist
command = 'replace'
subs = videos['youtube']
elif transcripts_presence['youtube_local']: # only youtube local exist
command = 'found'
subs = videos['youtube']
elif transcripts_presence['youtube_server']: # only youtube server exist
command = 'import'
else: # html5 part
if transcripts_presence['html5_local']: # can be 1 or 2 html5 videos
if len(transcripts_presence['html5_local']) == 1 or transcripts_presence['html5_equal']:
command = 'found'
subs = transcripts_presence['html5_local'][0]
else:
command = 'choose'
subs = transcripts_presence['html5_local'][0]
else: # html5 source have no subtitles
# check if item sub has subtitles
if transcripts_presence['current_item_subs'] and not transcripts_presence['is_youtube_mode']:
log.debug("Command is use existing %s subs", transcripts_presence['current_item_subs'])
command = 'use_existing'
else:
command = 'not_found'
log.debug(
"Resulted command: %s, current transcripts: %s, youtube mode: %s",
command,
transcripts_presence['current_item_subs'],
transcripts_presence['is_youtube_mode']
)
return command, subs
@login_required
def choose_transcripts(request):
"""
Replaces html5 subtitles, presented for both html5 sources, with chosen one.
Code removes rejected html5 subtitles and updates sub attribute with chosen html5_id.
It does nothing with youtube id's.
Returns: status `Success` and resulted item.sub value or status `Error` and HTTP 400.
"""
response = {
'status': 'Error',
'subs': '',
}
try:
data, videos, item = validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(response, e.message)
html5_id = data.get('html5_id') # html5_id chosen by user
# find rejected html5_id and remove appropriate subs from store
html5_id_to_remove = [x for x in videos['html5'] if x != html5_id]
if html5_id_to_remove:
remove_subs_from_store(html5_id_to_remove, item)
if item.sub != html5_id: # update sub value
item.sub = html5_id
save_module(item)
response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response)
@login_required
def replace_transcripts(request):
"""
Replaces all transcripts with youtube ones.
Downloads subtitles from youtube and replaces all transcripts with downloaded ones.
Returns: status `Success` and resulted item.sub value or status `Error` and HTTP 400.
"""
response = {'status': 'Error', 'subs': ''}
try:
__, videos, item = validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(response, e.message)
youtube_id = videos['youtube']
if not youtube_id:
return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id))
try:
download_youtube_subs({1.0: youtube_id}, item)
except GetTranscriptsFromYouTubeException as e:
return error_response(response, e.message)
item.sub = youtube_id
save_module(item)
response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response)
def validate_transcripts_data(request):
"""
Validates, that request contains all proper data for transcripts processing.
Returns tuple of 3 elements::
data: dict, loaded json from request,
videos: parsed `data` to useful format,
item: video item from storage
Raises `TranscriptsRequestValidationException` if validation is unsuccessful
or `PermissionDenied` if user has no access.
"""
data = json.loads(request.GET.get('data', '{}'))
if not data:
raise TranscriptsRequestValidationException('Incoming video data is empty.')
item_location = data.get('id')
# This is placed before has_access() to validate item_location,
# because has_access() raises InvalidLocationError if location is invalid.
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
raise TranscriptsRequestValidationException("Can't find item by location.")
# Check permissions for this user within this course.
if not has_access(request.user, item_location):
raise PermissionDenied()
if item.category != 'video':
raise TranscriptsRequestValidationException('Transcripts are supported only for "video" modules.')
# parse data form request.GET.['data']['video'] to useful format
videos = {'youtube': '', 'html5': {}}
for video_data in data.get('videos'):
if video_data['type'] == 'youtube':
videos['youtube'] = video_data['video']
else: # do not add same html5 videos
if videos['html5'].get('video') != video_data['video']:
videos['html5'][video_data['video']] = video_data['mode']
return data, videos, item
@login_required
def rename_transcripts(request):
"""
Create copies of existing subtitles with new names of HTML5 sources.
Old subtitles are not deleted now, because we do not have rollback functionality.
If succeed, Item.sub will be chosen randomly from html5 video sources provided by front-end.
"""
response = {'status': 'Error', 'subs': ''}
try:
__, videos, item = validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(response, e.message)
old_name = item.sub
for new_name in videos['html5'].keys(): # copy subtitles for every HTML5 source
try:
# updates item.sub with new_name if it is successful.
copy_or_rename_transcript(new_name, old_name, item)
except NotFoundError:
# subtitles file `item.sub` is not presented in the system. Nothing to copy or rename.
error_response(response, "Can't find transcripts in storage for {}".format(old_name))
response['status'] = 'Success'
response['subs'] = item.sub # item.sub has been changed, it is not equal to old_name.
log.debug("Updated item.sub to %s", item.sub)
return JsonResponse(response)
@login_required
def save_transcripts(request):
"""
Saves video module with updated values of fields.
Returns: status `Success` or status `Error` and HTTP 400.
"""
response = {'status': 'Error'}
data = json.loads(request.GET.get('data', '{}'))
if not data:
return error_response(response, 'Incoming video data is empty.')
item_location = data.get('id')
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
return error_response(response, "Can't find item by location.")
metadata = data.get('metadata')
if metadata is not None:
new_sub = metadata.get('sub')
for metadata_key, value in metadata.items():
setattr(item, metadata_key, value)
save_module(item) # item becomes updated with new values
if new_sub:
manage_video_subtitles_save(None, item)
else:
# If `new_sub` is empty, it means that user explicitly does not want to use
# transcripts for current video ids and we remove all transcripts from storage.
current_subs = data.get('current_subs')
if current_subs is not None:
for sub in current_subs:
remove_subs_from_store(sub, item)
response['status'] = 'Success'
return JsonResponse(response)