Merge remote-tracking branch 'edx/master' into opaque-keys-merge-master
Conflicts: cms/djangoapps/contentstore/views/public.py common/djangoapps/external_auth/tests/test_ssl.py common/djangoapps/student/views.py lms/djangoapps/dashboard/sysadmin.py lms/templates/notes.html
This commit is contained in:
@@ -53,7 +53,7 @@ Feature: CMS Transcripts
|
||||
# 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"
|
||||
Then I see status message "not found on edx"
|
||||
# t__eq_exist subs locally not presented at this moment
|
||||
And I see button "import"
|
||||
|
||||
@@ -72,18 +72,18 @@ Feature: CMS Transcripts
|
||||
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 "Transcript (primary)"
|
||||
And I see value "" in the field "Default Timed 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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript 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 "Transcript (primary)"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#4
|
||||
Scenario: Youtube id only: check "Found" state
|
||||
@@ -92,7 +92,7 @@ Feature: CMS Transcripts
|
||||
|
||||
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 "Transcript (primary)"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#5
|
||||
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are equal
|
||||
@@ -102,7 +102,7 @@ Feature: CMS Transcripts
|
||||
|
||||
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 "Transcript (primary)"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#6
|
||||
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are not equal
|
||||
@@ -114,7 +114,7 @@ Feature: CMS Transcripts
|
||||
And I see button "replace"
|
||||
And I click transcript button "replace"
|
||||
And I see status message "found"
|
||||
And I see value "t_neq_exist" in the field "Transcript (primary)"
|
||||
And I see value "t_neq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#7
|
||||
Scenario: html5 source only: check "Not Found" state
|
||||
@@ -123,7 +123,7 @@ Feature: CMS Transcripts
|
||||
|
||||
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 "Transcript (primary)"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
|
||||
#8
|
||||
Scenario: html5 source only: check "Found" state
|
||||
@@ -132,7 +132,7 @@ Feature: CMS Transcripts
|
||||
|
||||
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 "Transcript (primary)"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
#9
|
||||
Scenario: User sets youtube_id w/o server but with local subs and one html5 link w/o subs
|
||||
@@ -144,7 +144,7 @@ Feature: CMS Transcripts
|
||||
|
||||
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 "Transcript (primary)"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
# Disabled 1/29/14 due to flakiness observed in master
|
||||
#10
|
||||
@@ -153,14 +153,14 @@ Feature: CMS Transcripts
|
||||
# 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"
|
||||
# Then I see status message "not found on edx"
|
||||
# And I see button "import"
|
||||
# And I click transcript 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 "Transcript (primary)"
|
||||
# And I see value "t__eq_exist" in the field "Default Timed 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
|
||||
@@ -168,17 +168,17 @@ Feature: CMS Transcripts
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
@@ -205,7 +205,7 @@ Feature: CMS Transcripts
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript button "import"
|
||||
Then I see status message "found"
|
||||
@@ -247,17 +247,17 @@ Feature: CMS Transcripts
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
@@ -267,17 +267,17 @@ Feature: CMS Transcripts
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
|
||||
@@ -287,7 +287,7 @@ Feature: CMS Transcripts
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript button "import"
|
||||
Then I see status message "found"
|
||||
@@ -309,7 +309,7 @@ Feature: CMS Transcripts
|
||||
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"
|
||||
Then I see status message "not found on edx"
|
||||
And I see button "import"
|
||||
And I click transcript button "import"
|
||||
Then I see status message "found"
|
||||
@@ -338,7 +338,7 @@ Feature: CMS Transcripts
|
||||
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 "Transcript (primary)"
|
||||
And I see value "t__eq_exist" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "http://youtu.be/t_not_exist" source to field number 2
|
||||
Then I see status message "found"
|
||||
@@ -359,7 +359,7 @@ Feature: CMS Transcripts
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "uk_transcripts" in the field "Transcript (primary)"
|
||||
And I see value "uk_transcripts" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "t_not_exist.webm" source to field number 2
|
||||
Then I see status message "replace"
|
||||
@@ -367,7 +367,7 @@ Feature: CMS Transcripts
|
||||
And I see choose button "uk_transcripts.mp4" number 1
|
||||
And I see choose button "t_not_exist.webm" number 2
|
||||
And I click transcript button "choose" number 2
|
||||
And I see value "uk_transcripts|t_not_exist" in the field "Transcript (primary)"
|
||||
And I see value "uk_transcripts|t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
# Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927
|
||||
#21
|
||||
@@ -379,7 +379,7 @@ Feature: CMS Transcripts
|
||||
# Then I see status message "found"
|
||||
# And I see button "download_to_edit"
|
||||
# And I see button "upload_new_timed_transcripts"
|
||||
# And I see value "t_not_exist" in the field "Transcript (primary)"
|
||||
# And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
#
|
||||
# And I save changes
|
||||
# And I edit the component
|
||||
@@ -388,13 +388,13 @@ Feature: CMS Transcripts
|
||||
# Then I see status message "use existing"
|
||||
# And I see button "use_existing"
|
||||
# And I click transcript button "use_existing"
|
||||
# And I see value "video_name_2" in the field "Transcript (primary)"
|
||||
# And I see value "video_name_2" in the field "Default Timed 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 transcript button "use_existing"
|
||||
# And I see value "video_name_3" in the field "Transcript (primary)"
|
||||
# And I see value "video_name_3" in the field "Default Timed 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
|
||||
@@ -405,7 +405,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I see value "t_not_exist" in the field "Transcript (primary)"
|
||||
And I see value "t_not_exist" in the field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
And I edit the component
|
||||
@@ -414,7 +414,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_2" in the field "Transcript (primary)"
|
||||
And I see value "video_name_2" in the field "Default Timed Transcript"
|
||||
|
||||
And I enter a "video_name_3.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
@@ -424,7 +424,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_4" in the field "Transcript (primary)"
|
||||
And I see value "video_name_4" in the field "Default Timed 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
|
||||
@@ -447,7 +447,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_2|video_name_3" in the field "Transcript (primary)"
|
||||
And I see value "video_name_2|video_name_3" in the field "Default Timed Transcript"
|
||||
|
||||
#24 Uploading subtitles with different file name than file
|
||||
Scenario: File name and name of subs are different
|
||||
@@ -458,7 +458,7 @@ Feature: CMS Transcripts
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1" in the field "Transcript (primary)"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
@@ -489,11 +489,11 @@ Feature: CMS Transcripts
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1|video_name_2" in the field "Transcript (primary)"
|
||||
And I see value "video_name_1|video_name_2" in the field "Default Timed Transcript"
|
||||
|
||||
And I clear field number 1
|
||||
Then I see status message "found"
|
||||
And I see value "video_name_2" in the field "Transcript (primary)"
|
||||
And I see value "video_name_2" in the field "Default Timed Transcript"
|
||||
|
||||
#27
|
||||
Scenario: Upload button for single youtube id
|
||||
@@ -529,7 +529,7 @@ Feature: CMS Transcripts
|
||||
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 "Transcript (primary)"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
@@ -545,14 +545,14 @@ Feature: CMS Transcripts
|
||||
Then I see status message "not found"
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I set value "t_not_exist" to the field "Transcript (primary)"
|
||||
And I set value "t_not_exist" to the field "Default Timed 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 "Transcript (primary)"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
|
||||
#30
|
||||
Scenario: Check non-ascii (chinise) transcripts
|
||||
@@ -577,7 +577,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "not found"
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I set value "t_not_exist" to the field "Transcript (primary)"
|
||||
And I set value "t_not_exist" to the field "Default Timed Transcript"
|
||||
And I open tab "Basic"
|
||||
Then I see status message "found"
|
||||
|
||||
@@ -586,7 +586,7 @@ Feature: CMS Transcripts
|
||||
And I edit the component
|
||||
|
||||
Then I see status message "found"
|
||||
And I see value "video_name_1" in the field "Transcript (primary)"
|
||||
And I see value "video_name_1" in the field "Default Timed Transcript"
|
||||
|
||||
#32
|
||||
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible w/o saving
|
||||
@@ -599,7 +599,7 @@ Feature: CMS Transcripts
|
||||
Then I see status message "uploaded_successfully"
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I set value "" to the field "Transcript (primary)"
|
||||
And I set value "" to the field "Default Timed Transcript"
|
||||
And I open tab "Basic"
|
||||
Then I see status message "not found"
|
||||
|
||||
@@ -608,7 +608,7 @@ Feature: CMS Transcripts
|
||||
And I edit the component
|
||||
|
||||
Then I see status message "not found"
|
||||
And I see value "" in the field "Transcript (primary)"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
|
||||
#33
|
||||
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible with saving
|
||||
@@ -625,7 +625,7 @@ Feature: CMS Transcripts
|
||||
And I edit the component
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I set value "" to the field "Transcript (primary)"
|
||||
And I set value "" to the field "Default Timed Transcript"
|
||||
And I open tab "Basic"
|
||||
Then I see status message "not found"
|
||||
|
||||
@@ -634,7 +634,7 @@ Feature: CMS Transcripts
|
||||
And I edit the component
|
||||
|
||||
Then I see status message "not found"
|
||||
And I see value "" in the field "Transcript (primary)"
|
||||
And I see value "" in the field "Default Timed Transcript"
|
||||
|
||||
#34
|
||||
Scenario: Video with existing subs - Advanced tab - change to another one subs - Basic tab - Found message - Save - see correct subs
|
||||
@@ -653,7 +653,7 @@ Feature: CMS Transcripts
|
||||
And I edit the component
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I set value "t_not_exist" to the field "Transcript (primary)"
|
||||
And I set value "t_not_exist" to the field "Default Timed Transcript"
|
||||
And I open tab "Basic"
|
||||
Then I see status message "found"
|
||||
|
||||
@@ -676,7 +676,7 @@ Feature: CMS Transcripts
|
||||
And I edit the component
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I revert the transcript field "Transcript (primary)"
|
||||
And I revert the transcript field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
@@ -692,7 +692,7 @@ Feature: CMS Transcripts
|
||||
And I see status message "not found"
|
||||
And I upload the transcripts file "uk_transcripts.srt"
|
||||
Then I see status message "uploaded_successfully"
|
||||
And I see value "video_name_1.1.2" in the field "Transcript (primary)"
|
||||
And I see value "video_name_1.1.2" in the field "Default Timed Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
@@ -17,12 +17,13 @@ TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
DELAY = 0.5
|
||||
|
||||
ERROR_MESSAGES = {
|
||||
'url_format': u'Incorrect URL format.',
|
||||
'file_type': u'Video file types must be unique.',
|
||||
'url_format': u'Incorrect url format.',
|
||||
'file_type': u'Link types should be unique.',
|
||||
}
|
||||
|
||||
STATUSES = {
|
||||
'found': u'Timed Transcript Found',
|
||||
'not found on edx': u'No EdX Timed Transcript',
|
||||
'not found': u'No Timed Transcript',
|
||||
'replace': u'Timed Transcript Conflict',
|
||||
'uploaded_successfully': u'Timed Transcript Uploaded Successfully',
|
||||
@@ -39,13 +40,13 @@ SELECTORS = {
|
||||
|
||||
# button type , button css selector, button message
|
||||
TRANSCRIPTS_BUTTONS = {
|
||||
'import': ('.setting-import', 'Import YouTube Transcript'),
|
||||
'import': ('.setting-import', 'Import YouTube Transcript'),
|
||||
'download_to_edit': ('.setting-download', 'Download Transcript for Editing'),
|
||||
'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download Transcript for Editing'),
|
||||
'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Timed Transcript'),
|
||||
'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Transcript'),
|
||||
'replace': ('.setting-replace', 'Yes, replace the edX transcript with the YouTube transcript'),
|
||||
'choose': ('.setting-choose', 'Timed Transcript from {}'),
|
||||
'use_existing': ('.setting-use-existing', 'Use Existing Timed Transcript'),
|
||||
'use_existing': ('.setting-use-existing', 'Use Current Transcript'),
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +210,8 @@ def check_text_in_the_captions(_step, text):
|
||||
@step('I see value "([^"]*)" in the field "([^"]*)"$')
|
||||
def check_transcripts_field(_step, values, field_name):
|
||||
world.select_editor_tab('Advanced')
|
||||
field_id = '#' + world.browser.find_by_xpath('//label[text()="%s"]' % field_name.strip())[0]['for']
|
||||
tab = world.css_find('#settings-tab').first;
|
||||
field_id = '#' + tab.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.select_editor_tab('Basic')
|
||||
@@ -227,8 +229,9 @@ def open_tab(_step, tab_name):
|
||||
|
||||
@step('I set value "([^"]*)" to the field "([^"]*)"$')
|
||||
def set_value_transcripts_field(_step, value, field_name):
|
||||
XPATH = '//label[text()="{name}"]'.format(name=field_name)
|
||||
SELECTOR = '#' + world.browser.find_by_xpath(XPATH)[0]['for']
|
||||
tab = world.css_find('#settings-tab').first;
|
||||
XPATH = './/label[text()="{name}"]'.format(name=field_name)
|
||||
SELECTOR = '#' + tab.find_by_xpath(XPATH)[0]['for']
|
||||
element = world.css_find(SELECTOR).first
|
||||
if element['type'] == 'text':
|
||||
SCRIPT = '$("{selector}").val("{value}").change()'.format(
|
||||
|
||||
@@ -72,8 +72,8 @@ Feature: CMS Video Component
|
||||
And Make sure captions are closed
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
And I set value "00:00:12" to the field "Start Time"
|
||||
And I set value "00:00:24" to the field "End Time"
|
||||
And I set value "00:00:12" to the field "Video Start Time"
|
||||
And I set value "00:00:24" to the field "Video Stop Time"
|
||||
And I save changes
|
||||
And I click video button "play"
|
||||
Then I see a range on slider
|
||||
@@ -85,8 +85,8 @@ Feature: CMS Video Component
|
||||
# And Make sure captions are closed
|
||||
# And I edit the component
|
||||
# And I open tab "Advanced"
|
||||
# And I set value "00:00:12" to the field "Start Time"
|
||||
# And I set value "00:00:24" to the field "End Time"
|
||||
# And I set value "00:00:12" to the field "Video Start Time"
|
||||
# And I set value "00:00:24" to the field "Video Stop Time"
|
||||
# And I save changes
|
||||
# And I click video button "play"
|
||||
# Then I see a range on slider
|
||||
@@ -103,8 +103,8 @@ Feature: CMS Video Component
|
||||
# And Make sure captions are closed
|
||||
# And I edit the component
|
||||
# And I open tab "Advanced"
|
||||
# And I set value "00:00:12" to the field "Start Time"
|
||||
# And I set value "00:00:24" to the field "End Time"
|
||||
# And I set value "00:00:12" to the field "Video Start Time"
|
||||
# And I set value "00:00:24" to the field "Video Stop Time"
|
||||
# And I save changes
|
||||
# And I click video button "play"
|
||||
# Then I see a range on slider
|
||||
@@ -121,8 +121,8 @@ Feature: CMS Video Component
|
||||
# And Make sure captions are closed
|
||||
# And I edit the component
|
||||
# And I open tab "Advanced"
|
||||
# And I set value "00:00:12" to the field "Start Time"
|
||||
# And I set value "00:00:24" to the field "End Time"
|
||||
# And I set value "00:00:12" to the field "Video Start Time"
|
||||
# And I set value "00:00:24" to the field "Video Stop Time"
|
||||
# And I save changes
|
||||
# And I click video button "play"
|
||||
# Then I see a range on slider
|
||||
|
||||
@@ -15,7 +15,7 @@ Feature: CMS Video Component Editor
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
And I open tab "Advanced"
|
||||
Then I can modify the display name
|
||||
Then I can modify video display name
|
||||
And my video display name change is persisted on save
|
||||
|
||||
# 3
|
||||
|
||||
@@ -11,6 +11,7 @@ from common import upload_file, attach_file
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
DISPLAY_NAME = "Component Display Name"
|
||||
NATIVE_LANGUAGES = {lang: label for lang, label in settings.LANGUAGES if len(lang) == 2}
|
||||
LANGUAGES = {
|
||||
lang: NATIVE_LANGUAGES.get(lang, display)
|
||||
@@ -76,7 +77,7 @@ def success_upload_file(filename):
|
||||
|
||||
|
||||
def get_translations_container():
|
||||
return world.browser.find_by_xpath('//label[text()="Transcript Translations"]/following-sibling::div')
|
||||
return world.browser.find_by_xpath('//label[text()="Transcript Languages"]/following-sibling::div')
|
||||
|
||||
|
||||
def get_setting_container(lang_code):
|
||||
@@ -114,7 +115,7 @@ def set_show_captions(step, setting):
|
||||
|
||||
world.edit_component()
|
||||
world.select_editor_tab('Advanced')
|
||||
world.browser.select('Transcript Display', setting)
|
||||
world.browser.select('Show Transcript', setting)
|
||||
world.save_component()
|
||||
|
||||
|
||||
@@ -136,25 +137,25 @@ def shows_captions(_step, show_captions):
|
||||
def correct_video_settings(_step):
|
||||
expected_entries = [
|
||||
# basic
|
||||
['Display Name', 'Video', False],
|
||||
['Video URL', 'http://youtu.be/OEoXaMPEzfM, , ', False],
|
||||
[DISPLAY_NAME, 'Video', False],
|
||||
['Default Video URL', 'http://youtu.be/OEoXaMPEzfM, , ', False],
|
||||
|
||||
# advanced
|
||||
['Display Name', 'Video', False],
|
||||
['Download Transcript', '', False],
|
||||
['End Time', '00:00:00', False],
|
||||
['Start Time', '00:00:00', False],
|
||||
['Transcript (primary)', '', False],
|
||||
['Transcript Display', 'True', False],
|
||||
['Transcript Download Allowed', 'False', False],
|
||||
['Transcript Translations', '', False],
|
||||
[DISPLAY_NAME, 'Video', False],
|
||||
['Default Timed Transcript', '', False],
|
||||
['Download Transcript Allowed', 'False', False],
|
||||
['Downloadable Transcript URL', '', False],
|
||||
['Show Transcript', 'True', False],
|
||||
['Transcript Languages', '', False],
|
||||
['Upload Handout', '', False],
|
||||
['Video Download Allowed', 'False', False],
|
||||
['Video Sources', '', False],
|
||||
['Youtube ID', 'OEoXaMPEzfM', False],
|
||||
['Youtube ID for .75x speed', '', False],
|
||||
['Youtube ID for 1.25x speed', '', False],
|
||||
['Youtube ID for 1.5x speed', '', False]
|
||||
['Video File URLs', '', False],
|
||||
['Video Start Time', '00:00:00', False],
|
||||
['Video Stop Time', '00:00:00', False],
|
||||
['YouTube ID', 'OEoXaMPEzfM', False],
|
||||
['YouTube ID for .75x speed', '', False],
|
||||
['YouTube ID for 1.25x speed', '', False],
|
||||
['YouTube ID for 1.5x speed', '', False]
|
||||
]
|
||||
world.verify_all_setting_entries(expected_entries)
|
||||
|
||||
@@ -167,11 +168,18 @@ def video_name_persisted(step):
|
||||
world.edit_component()
|
||||
|
||||
world.verify_setting_entry(
|
||||
world.get_setting_entry('Display Name'),
|
||||
'Display Name', '3.4', True
|
||||
world.get_setting_entry(DISPLAY_NAME),
|
||||
DISPLAY_NAME, '3.4', True
|
||||
)
|
||||
|
||||
|
||||
@step('I can modify video display name')
|
||||
def i_can_modify_video_display_name(_step):
|
||||
index = world.get_setting_entry_index(DISPLAY_NAME)
|
||||
world.set_field_value(index, '3.4')
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '3.4', True)
|
||||
|
||||
|
||||
@step('I upload transcript file(?:s)?:$')
|
||||
def upload_transcript(step):
|
||||
input_hidden = '.metadata-video-translations .input'
|
||||
|
||||
@@ -9,7 +9,8 @@ from django.conf import settings
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
|
||||
from external_auth.views import ssl_login_shortcut, ssl_get_cert_from_request
|
||||
from external_auth.views import (ssl_login_shortcut, ssl_get_cert_from_request,
|
||||
redirect_with_get)
|
||||
from microsite_configuration import microsite
|
||||
|
||||
__all__ = ['signup', 'login_page', 'howitworks']
|
||||
@@ -26,7 +27,7 @@ def signup(request):
|
||||
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
|
||||
# Redirect to course to login to process their certificate if SSL is enabled
|
||||
# and registration is disabled.
|
||||
return redirect(reverse('login'))
|
||||
return redirect_with_get('login', request.GET, False)
|
||||
|
||||
return render_to_response('register.html', {'csrf': csrf_token})
|
||||
|
||||
@@ -43,7 +44,15 @@ def login_page(request):
|
||||
# SSL login doesn't require a login view, so redirect
|
||||
# to course now that the user is authenticated via
|
||||
# the decorator.
|
||||
<<<<<<< HEAD
|
||||
return redirect('/course/')
|
||||
=======
|
||||
next_url = request.GET.get('next')
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
else:
|
||||
return redirect('/course')
|
||||
>>>>>>> edx/master
|
||||
if settings.FEATURES.get('AUTH_USE_CAS'):
|
||||
# If CAS is enabled, redirect auth handling to there
|
||||
return redirect(reverse('cas-login'))
|
||||
|
||||
@@ -318,7 +318,7 @@ PIPELINE_CSS = {
|
||||
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
|
||||
'css/vendor/jquery.qtip.min.css',
|
||||
'js/vendor/markitup/skins/simple/style.css',
|
||||
'js/vendor/markitup/sets/wiki/style.css'
|
||||
'js/vendor/markitup/sets/wiki/style.css',
|
||||
],
|
||||
'output_filename': 'css/cms-style-vendor.css',
|
||||
},
|
||||
|
||||
@@ -245,7 +245,6 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI",
|
||||
expect(pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
|
||||
it('should be disabled on an empty page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
@@ -301,6 +300,31 @@ define([ "jquery", "js/spec_helpers/create_sinon", "URI",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Page metadata section", function() {
|
||||
it('shows the correct metadata for the current page', function () {
|
||||
var requests = create_sinon.requests(this),
|
||||
message;
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
message = pagingHeader.$('.meta').html().trim();
|
||||
expect(message).toBe('<p>Showing <span class="count-current-shown">1-3</span>' +
|
||||
' out of <span class="count-total">4 total</span>, ' +
|
||||
'sorted by <span class="sort-order">Date</span> descending</p>');
|
||||
});
|
||||
|
||||
it('shows the correct metadata when sorted ascending', function () {
|
||||
var requests = create_sinon.requests(this),
|
||||
message;
|
||||
pagingView.setPage(0);
|
||||
pagingView.toggleSortOrder('name-col');
|
||||
respondWithMockAssets(requests);
|
||||
message = pagingHeader.$('.meta').html().trim();
|
||||
expect(message).toBe('<p>Showing <span class="count-current-shown">1-3</span>' +
|
||||
' out of <span class="count-total">4 total</span>, ' +
|
||||
'sorted by <span class="sort-order">Name</span> ascending</p>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Asset count label", function () {
|
||||
it('should show correct count on first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
|
||||
@@ -87,12 +87,6 @@ define(["underscore", "js/views/baseview", "js/views/feedback_alert", "gettext"]
|
||||
return sortInfo.displayName;
|
||||
},
|
||||
|
||||
sortDirectionName: function() {
|
||||
var collection = this.collection,
|
||||
ascending = collection.sortDirection === 'asc';
|
||||
return ascending ? gettext("ascending") : gettext("descending");
|
||||
},
|
||||
|
||||
setInitialSortColumn: function(sortColumn) {
|
||||
var collection = this.collection,
|
||||
sortInfo = this.sortableColumns[sortColumn];
|
||||
|
||||
@@ -31,27 +31,48 @@ define(["underscore", "gettext", "js/views/baseview"], function(_, gettext, Base
|
||||
},
|
||||
|
||||
messageHtml: function() {
|
||||
var message;
|
||||
if (this.view.collection.sortDirection === 'asc') {
|
||||
// Translators: sample result: "Showing 0-9 out of 25 total, sorted by Date Added ascending"
|
||||
message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s ascending');
|
||||
} else {
|
||||
// Translators: sample result: "Showing 0-9 out of 25 total, sorted by Date Added descending"
|
||||
message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s descending');
|
||||
}
|
||||
return '<p>' + interpolate(message, {
|
||||
current_item_range: this.currentItemRangeLabel(),
|
||||
total_items_count: this.totalItemsCountLabel(),
|
||||
sort_name: this.sortNameLabel()
|
||||
}, true) + "</p>";
|
||||
},
|
||||
|
||||
currentItemRangeLabel: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
start = collection.start,
|
||||
count = collection.size(),
|
||||
sortName = view.sortDisplayName(),
|
||||
sortDirectionName = view.sortDirectionName(),
|
||||
end = start + count,
|
||||
total = collection.totalCount,
|
||||
fmts = gettext('Showing %(current_span)s%(start)s-%(end)s%(end_span)s out of %(total_span)s%(total)s total%(end_span)s, sorted by %(order_span)s%(sort_order)s%(end_span)s %(sort_direction)s');
|
||||
|
||||
return '<p>' + interpolate(fmts, {
|
||||
end = start + count;
|
||||
return interpolate('<span class="count-current-shown">%(start)s-%(end)s</span>', {
|
||||
start: Math.min(start + 1, end),
|
||||
end: end,
|
||||
total: total,
|
||||
sort_order: sortName,
|
||||
sort_direction: sortDirectionName,
|
||||
current_span: '<span class="count-current-shown">',
|
||||
total_span: '<span class="count-total">',
|
||||
order_span: '<span class="sort-order">',
|
||||
end_span: '</span>'
|
||||
}, true) + "</p>";
|
||||
end: end
|
||||
}, true);
|
||||
},
|
||||
|
||||
totalItemsCountLabel: function() {
|
||||
var totalItemsLabel;
|
||||
// Translators: turns into "25 total" to be used in other sentences, e.g. "Showing 0-9 out of 25 total".
|
||||
totalItemsLabel = interpolate(gettext('%(total_items)s total'), {
|
||||
total_items: this.view.collection.totalCount
|
||||
}, true);
|
||||
return interpolate('<span class="count-total">%(total_items_label)s</span>', {
|
||||
total_items_label: totalItemsLabel
|
||||
}, true);
|
||||
},
|
||||
|
||||
sortNameLabel: function() {
|
||||
return interpolate('<span class="sort-order">%(sort_name)s</span>', {
|
||||
sort_name: this.view.sortDisplayName()
|
||||
}, true);
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<%= gettext("Error.") %>
|
||||
</p>
|
||||
<div class="wrapper-transcripts-buttons">
|
||||
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Timed Transcript") %>" data-tooltip="<%= gettext("Upload New Timed Transcript") %>">
|
||||
<%= gettext("Upload New Timed Transcript") %>
|
||||
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Transcript") %>" data-tooltip="<%= gettext("Upload New Transcript") %>">
|
||||
<%= gettext("Upload New Transcript") %>
|
||||
</button>
|
||||
<a class="action setting-download is-disabled" href="javascropt: void(0);" data-tooltip="<%= gettext("Download Transcript for Editing") %>">
|
||||
<%= gettext("Download Transcript for Editing") %>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<%= gettext("Error.") %>
|
||||
</p>
|
||||
<div class="wrapper-transcripts-buttons">
|
||||
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Timed Transcript") %>" data-tooltip="<%= gettext("Upload New Timed Transcript") %>">
|
||||
<span><%= gettext("Upload New Timed Transcript") %></span>
|
||||
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Transcript") %>" data-tooltip="<%= gettext("Upload New Transcript") %>">
|
||||
<span><%= gettext("Upload New Transcript") %></span>
|
||||
</button>
|
||||
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>&subs_id=<%= subs_id %>" data-tooltip="<%= gettext("Download Transcript for Editing") %>">
|
||||
<span><%= gettext("Download Transcript for Editing") %></span>
|
||||
|
||||
@@ -18,22 +18,22 @@
|
||||
class="action setting-use-existing"
|
||||
type="button"
|
||||
name="setting-use-existing"
|
||||
value="<%= gettext("Use Current Timed Transcript") %>"
|
||||
data-tooltip="<%= gettext("Use Current Timed Transcript") %>"
|
||||
value="<%= gettext("Use Current Transcript") %>"
|
||||
data-tooltip="<%= gettext("Use Current Transcript") %>"
|
||||
>
|
||||
<span>
|
||||
<%= gettext("Use Current Timed Transcript") %>
|
||||
<%= gettext("Use Current Transcript") %>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="action setting-upload"
|
||||
type="button"
|
||||
name="setting-upload"
|
||||
value="<%= gettext("Upload New Timed Transcript") %>"
|
||||
data-tooltip="<%= gettext("Upload New Timed Transcript") %>"
|
||||
value="<%= gettext("Upload New Transcript") %>"
|
||||
data-tooltip="<%= gettext("Upload New Transcript") %>"
|
||||
>
|
||||
<span>
|
||||
<%= gettext("Upload New Timed Transcript") %>
|
||||
<%= gettext("Upload New Transcript") %>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -19,19 +19,33 @@ from mock import Mock
|
||||
from edxmako.middleware import MakoMiddleware
|
||||
from external_auth.models import ExternalAuthMap
|
||||
import external_auth.views
|
||||
from student.roles import CourseStaffRole
|
||||
from student.tests.factories import UserFactory
|
||||
<<<<<<< HEAD
|
||||
from opaque_keys import InvalidKeyError
|
||||
=======
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.exceptions import InsufficientSpecificationError
|
||||
from xmodule.modulestore.tests.django_utils import (ModuleStoreTestCase,
|
||||
mixed_store_config)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
>>>>>>> edx/master
|
||||
|
||||
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
|
||||
FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
|
||||
FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP = FEATURES_WITH_SSL_AUTH.copy()
|
||||
FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP['AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'] = True
|
||||
FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE = FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP.copy()
|
||||
FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
|
||||
FEATURES_WITHOUT_SSL_AUTH = settings.FEATURES.copy()
|
||||
FEATURES_WITHOUT_SSL_AUTH['AUTH_USE_CERTIFICATES'] = False
|
||||
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH)
|
||||
class SSLClientTest(TestCase):
|
||||
class SSLClientTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests SSL Authentication code sections of external_auth
|
||||
"""
|
||||
@@ -168,7 +182,8 @@ class SSLClientTest(TestCase):
|
||||
response = self.client.get(
|
||||
reverse('dashboard'), follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
|
||||
self.assertIn(reverse('dashboard'), response['location'])
|
||||
self.assertEquals(('http://testserver/dashboard', 302),
|
||||
response.redirect_chain[-1])
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@@ -181,7 +196,8 @@ class SSLClientTest(TestCase):
|
||||
response = self.client.get(
|
||||
reverse('register_user'), follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
|
||||
self.assertIn(reverse('dashboard'), response['location'])
|
||||
self.assertEquals(('http://testserver/dashboard', 302),
|
||||
response.redirect_chain[-1])
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
|
||||
@@ -228,7 +244,8 @@ class SSLClientTest(TestCase):
|
||||
response = self.client.get(
|
||||
reverse('signin_user'), follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
|
||||
self.assertIn(reverse('dashboard'), response['location'])
|
||||
self.assertEquals(('http://testserver/dashboard', 302),
|
||||
response.redirect_chain[-1])
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@@ -318,3 +335,94 @@ class SSLClientTest(TestCase):
|
||||
self.assertEqual(1, len(ExternalAuthMap.objects.all()))
|
||||
|
||||
self.assertTrue(self.mock.called)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE,
|
||||
MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
def test_ssl_lms_redirection(self):
|
||||
"""
|
||||
Auto signup auth user and ensure they return to the original
|
||||
url they visited after being logged in.
|
||||
"""
|
||||
course = CourseFactory.create(
|
||||
org='MITx',
|
||||
number='999',
|
||||
display_name='Robot Super Course'
|
||||
)
|
||||
|
||||
external_auth.views.ssl_login(self._create_ssl_request('/'))
|
||||
user = User.objects.get(email=self.USER_EMAIL)
|
||||
CourseEnrollment.enroll(user, course.id)
|
||||
course_private_url = '/courses/MITx/999/Robot_Super_Course/courseware'
|
||||
|
||||
self.assertFalse(SESSION_KEY in self.client.session)
|
||||
|
||||
response = self.client.get(
|
||||
course_private_url,
|
||||
follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL),
|
||||
HTTP_ACCEPT='text/html'
|
||||
)
|
||||
self.assertEqual(('http://testserver{0}'.format(course_private_url), 302),
|
||||
response.redirect_chain[-1])
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
|
||||
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE)
|
||||
def test_ssl_cms_redirection(self):
|
||||
"""
|
||||
Auto signup auth user and ensure they return to the original
|
||||
url they visited after being logged in.
|
||||
"""
|
||||
course = CourseFactory.create(
|
||||
org='MITx',
|
||||
number='999',
|
||||
display_name='Robot Super Course'
|
||||
)
|
||||
|
||||
external_auth.views.ssl_login(self._create_ssl_request('/'))
|
||||
user = User.objects.get(email=self.USER_EMAIL)
|
||||
CourseEnrollment.enroll(user, course.id)
|
||||
|
||||
CourseStaffRole(course.location).add_users(user)
|
||||
location = Location(['i4x', 'MITx', '999', 'course',
|
||||
Location.clean('Robot Super Course'), None])
|
||||
new_location = loc_mapper().translate_location(
|
||||
location.course_id, location, True, True
|
||||
)
|
||||
course_private_url = new_location.url_reverse('course/', '')
|
||||
self.assertFalse(SESSION_KEY in self.client.session)
|
||||
|
||||
response = self.client.get(
|
||||
course_private_url,
|
||||
follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL),
|
||||
HTTP_ACCEPT='text/html'
|
||||
)
|
||||
self.assertEqual(('http://testserver{0}'.format(course_private_url), 302),
|
||||
response.redirect_chain[-1])
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_AUTO_ACTIVATE)
|
||||
def test_ssl_logout(self):
|
||||
"""
|
||||
Because the branding view is cached for anonymous users and we
|
||||
use that to login users, the browser wasn't actually making the
|
||||
request to that view as the redirect was being cached. This caused
|
||||
a redirect loop, and this test confirms that that won't happen.
|
||||
|
||||
Test is only in LMS because we don't use / in studio to login SSL users.
|
||||
"""
|
||||
response = self.client.get(
|
||||
reverse('dashboard'), follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL))
|
||||
self.assertEquals(('http://testserver/dashboard', 302),
|
||||
response.redirect_chain[-1])
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
response = self.client.get(
|
||||
reverse('logout'), follow=True,
|
||||
SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)
|
||||
)
|
||||
# Make sure that even though we logged out, we have logged back in
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
|
||||
@@ -440,7 +440,10 @@ def ssl_login(request):
|
||||
|
||||
(_user, email, fullname) = _ssl_dn_extract_info(cert)
|
||||
|
||||
retfun = functools.partial(redirect, '/')
|
||||
redirect_to = request.GET.get('next')
|
||||
if not redirect_to:
|
||||
redirect_to = '/'
|
||||
retfun = functools.partial(redirect, redirect_to)
|
||||
return _external_login_or_signup(
|
||||
request,
|
||||
external_id=email,
|
||||
@@ -579,14 +582,14 @@ def course_specific_login(request, course_id):
|
||||
course = student.views.course_from_id(course_id)
|
||||
if not course:
|
||||
# couldn't find the course, will just return vanilla signin page
|
||||
return _redirect_with_get_querydict('signin_user', request.GET)
|
||||
return redirect_with_get('signin_user', request.GET)
|
||||
|
||||
# now the dispatching conditionals. Only shib for now
|
||||
if settings.FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX):
|
||||
return _redirect_with_get_querydict('shib-login', request.GET)
|
||||
return redirect_with_get('shib-login', request.GET)
|
||||
|
||||
# Default fallthrough to normal signin page
|
||||
return _redirect_with_get_querydict('signin_user', request.GET)
|
||||
return redirect_with_get('signin_user', request.GET)
|
||||
|
||||
|
||||
def course_specific_register(request, course_id):
|
||||
@@ -598,24 +601,28 @@ def course_specific_register(request, course_id):
|
||||
|
||||
if not course:
|
||||
# couldn't find the course, will just return vanilla registration page
|
||||
return _redirect_with_get_querydict('register_user', request.GET)
|
||||
return redirect_with_get('register_user', request.GET)
|
||||
|
||||
# now the dispatching conditionals. Only shib for now
|
||||
if settings.FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX):
|
||||
# shib-login takes care of both registration and login flows
|
||||
return _redirect_with_get_querydict('shib-login', request.GET)
|
||||
return redirect_with_get('shib-login', request.GET)
|
||||
|
||||
# Default fallthrough to normal registration page
|
||||
return _redirect_with_get_querydict('register_user', request.GET)
|
||||
return redirect_with_get('register_user', request.GET)
|
||||
|
||||
|
||||
def _redirect_with_get_querydict(view_name, get_querydict):
|
||||
def redirect_with_get(view_name, get_querydict, do_reverse=True):
|
||||
"""
|
||||
Helper function to carry over get parameters across redirects
|
||||
Using urlencode(safe='/') because the @login_required decorator generates 'next' queryparams with '/' unencoded
|
||||
"""
|
||||
if do_reverse:
|
||||
url = reverse(view_name)
|
||||
else:
|
||||
url = view_name
|
||||
if get_querydict:
|
||||
return redirect("%s?%s" % (reverse(view_name), get_querydict.urlencode(safe='/')))
|
||||
return redirect("%s?%s" % (url, get_querydict.urlencode(safe='/')))
|
||||
return redirect(view_name)
|
||||
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
'''
|
||||
Firebase - library to generate a token
|
||||
License: https://github.com/firebase/firebase-token-generator-python/blob/master/LICENSE
|
||||
Tweaked and Edited by @danielcebrianr and @lduarte1991
|
||||
|
||||
This library will take either objects or strings and use python's built-in encoding
|
||||
system as specified by RFC 3548. Thanks to the firebase team for their open-source
|
||||
library. This was made specifically for speaking with the annotation_storage_url and
|
||||
can be used and expanded, but not modified by anyone else needing such a process.
|
||||
'''
|
||||
from base64 import urlsafe_b64encode
|
||||
import hashlib
|
||||
import hmac
|
||||
import sys
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
__all__ = ['create_token']
|
||||
|
||||
TOKEN_SEP = '.'
|
||||
|
||||
|
||||
def create_token(secret, data):
|
||||
'''
|
||||
Simply takes in the secret key and the data and
|
||||
passes it to the local function _encode_token
|
||||
'''
|
||||
return _encode_token(secret, data)
|
||||
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
def _encode(bytes_data):
|
||||
'''
|
||||
Takes a json object, string, or binary and
|
||||
uses python's urlsafe_b64encode to encode data
|
||||
and make it safe pass along in a url.
|
||||
To make sure it does not conflict with variables
|
||||
we make sure equal signs are removed.
|
||||
More info: docs.python.org/2/library/base64.html
|
||||
'''
|
||||
encoded = urlsafe_b64encode(bytes(bytes_data))
|
||||
return encoded.decode('utf-8').replace('=', '')
|
||||
else:
|
||||
def _encode(bytes_info):
|
||||
'''
|
||||
Same as above function but for Python 2.7 or later
|
||||
'''
|
||||
encoded = urlsafe_b64encode(bytes_info)
|
||||
return encoded.decode('utf-8').replace('=', '')
|
||||
|
||||
|
||||
def _encode_json(obj):
|
||||
'''
|
||||
Before a python dict object can be properly encoded,
|
||||
it must be transformed into a jason object and then
|
||||
transformed into bytes to be encoded using the function
|
||||
defined above.
|
||||
'''
|
||||
return _encode(bytearray(json.dumps(obj), 'utf-8'))
|
||||
|
||||
|
||||
def _sign(secret, to_sign):
|
||||
'''
|
||||
This function creates a sign that goes at the end of the
|
||||
message that is specific to the secret and not the actual
|
||||
content of the encoded body.
|
||||
More info on hashing: http://docs.python.org/2/library/hmac.html
|
||||
The function creates a hashed values of the secret and to_sign
|
||||
and returns the digested values based the secure hash
|
||||
algorithm, 256
|
||||
'''
|
||||
def portable_bytes(string):
|
||||
'''
|
||||
Simply transforms a string into a bytes object,
|
||||
which is a series of immutable integers 0<=x<=256.
|
||||
Always try to encode as utf-8, unless it is not
|
||||
compliant.
|
||||
'''
|
||||
try:
|
||||
return bytes(string, 'utf-8')
|
||||
except TypeError:
|
||||
return bytes(string)
|
||||
return _encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), hashlib.sha256).digest()) # pylint: disable=E1101
|
||||
|
||||
|
||||
def _encode_token(secret, claims):
|
||||
'''
|
||||
This is the main function that takes the secret token and
|
||||
the data to be transmitted. There is a header created for decoding
|
||||
purposes. Token_SEP means that a period/full stop separates the
|
||||
header, data object/message, and signatures.
|
||||
'''
|
||||
encoded_header = _encode_json({'typ': 'JWT', 'alg': 'HS256'})
|
||||
encoded_claims = _encode_json(claims)
|
||||
secure_bits = '%s%s%s' % (encoded_header, TOKEN_SEP, encoded_claims)
|
||||
sig = _sign(secret, secure_bits)
|
||||
return '%s%s%s' % (secure_bits, TOKEN_SEP, sig)
|
||||
@@ -1,43 +0,0 @@
|
||||
"""
|
||||
This test will run for firebase_token_generator.py.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from student.firebase_token_generator import _encode, _encode_json, _encode_token, create_token
|
||||
|
||||
|
||||
class TokenGenerator(TestCase):
|
||||
"""
|
||||
Tests for the file firebase_token_generator.py
|
||||
"""
|
||||
def test_encode(self):
|
||||
"""
|
||||
This tests makes sure that no matter what version of python
|
||||
you have, the _encode function still returns the appropriate result
|
||||
for a string.
|
||||
"""
|
||||
expected = "dGVzdDE"
|
||||
result = _encode("test1")
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_encode_json(self):
|
||||
"""
|
||||
Same as above, but this one focuses on a python dict type
|
||||
transformed into a json object and then encoded.
|
||||
"""
|
||||
expected = "eyJ0d28iOiAidGVzdDIiLCAib25lIjogInRlc3QxIn0"
|
||||
result = _encode_json({'one': 'test1', 'two': 'test2'})
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_create_token(self):
|
||||
"""
|
||||
Unlike its counterpart in student/views.py, this function
|
||||
just checks for the encoding of a token. The other function
|
||||
will test depending on time and user.
|
||||
"""
|
||||
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.-p1sr7uwCapidTQ0qB7DdU2dbF-hViKpPNN_5vD10t8"
|
||||
result1 = _encode_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
|
||||
result2 = create_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
|
||||
self.assertEqual(expected, result1)
|
||||
self.assertEqual(expected, result2)
|
||||
@@ -27,7 +27,7 @@ from mock import Mock, patch
|
||||
|
||||
from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user
|
||||
from student.views import (process_survey_link, _cert_info,
|
||||
change_enrollment, complete_course_mode_info, token)
|
||||
change_enrollment, complete_course_mode_info)
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
|
||||
import shoppingcart
|
||||
@@ -491,26 +491,3 @@ class AnonymousLookupTable(TestCase):
|
||||
anonymous_id = anonymous_id_for_user(self.user, self.course.id)
|
||||
real_user = user_by_anonymous_id(anonymous_id)
|
||||
self.assertEqual(self.user, real_user)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class Token(ModuleStoreTestCase):
|
||||
"""
|
||||
Test for the token generator. This creates a random course and passes it through the token file which generates the
|
||||
token that will be passed in to the annotation_storage_url.
|
||||
"""
|
||||
request_factory = RequestFactory()
|
||||
COURSE_SLUG = "100"
|
||||
COURSE_NAME = "test_course"
|
||||
COURSE_ORG = "edx"
|
||||
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
|
||||
self.user = User.objects.create(username="username", email="username")
|
||||
self.req = self.request_factory.post('/token?course_id=edx/100/test_course', {'user': self.user})
|
||||
self.req.user = self.user
|
||||
|
||||
def test_token(self):
|
||||
expected = HttpResponse("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAxLTIzVDE5OjM1OjE3LjUyMjEwNC01OjAwIiwgImNvbnN1bWVyS2V5IjogInh4eHh4eHh4LXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCIsICJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.OjWz9mzqJnYuzX-f3uCBllqJUa8PVWJjcDy_McfxLvc", mimetype="text/plain")
|
||||
response = token(self.req)
|
||||
self.assertEqual(expected.content.split('.')[0], response.content.split('.')[0])
|
||||
|
||||
@@ -26,6 +26,7 @@ from django.shortcuts import redirect
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.utils.http import cookie_date, base36_to_int
|
||||
from django.utils.translation import ugettext as _, get_language
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
@@ -43,7 +44,6 @@ from student.models import (
|
||||
create_comments_service_user, PasswordHistory
|
||||
)
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
from student.firebase_token_generator import create_token
|
||||
|
||||
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
@@ -328,7 +328,7 @@ def signin_user(request):
|
||||
# SSL login doesn't require a view, so redirect
|
||||
# branding and allow that to process the login if it
|
||||
# is enabled and the header is in the request.
|
||||
return redirect(reverse('root'))
|
||||
return external_auth.views.redirect_with_get('root', request.GET)
|
||||
if settings.FEATURES.get('AUTH_USE_CAS'):
|
||||
# If CAS is enabled, redirect auth handling to there
|
||||
return redirect(reverse('cas-login'))
|
||||
@@ -361,7 +361,7 @@ def register_user(request, extra_context=None):
|
||||
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
|
||||
# Redirect to branding to process their certificate if SSL is enabled
|
||||
# and registration is disabled.
|
||||
return redirect(reverse('root'))
|
||||
return external_auth.views.redirect_with_get('root', request.GET)
|
||||
|
||||
context = {
|
||||
'course_id': request.GET.get('course_id'),
|
||||
@@ -676,6 +676,7 @@ def _get_course_enrollment_domain(course_id):
|
||||
return course.enrollment_domain
|
||||
|
||||
|
||||
@never_cache
|
||||
@ensure_csrf_cookie
|
||||
def accounts_login(request):
|
||||
"""
|
||||
@@ -685,9 +686,9 @@ def accounts_login(request):
|
||||
if settings.FEATURES.get('AUTH_USE_CAS'):
|
||||
return redirect(reverse('cas-login'))
|
||||
if settings.FEATURES['AUTH_USE_CERTIFICATES']:
|
||||
# SSL login doesn't require a view, so redirect
|
||||
# to branding and allow that to process the login.
|
||||
return redirect(reverse('root'))
|
||||
# SSL login doesn't require a view, so login
|
||||
# directly here
|
||||
return external_auth.views.ssl_login(request)
|
||||
# see if the "next" parameter has been set, whether it has a course context, and if so, whether
|
||||
# there is a course-specific place to redirect
|
||||
redirect_to = request.GET.get('next')
|
||||
@@ -1855,6 +1856,7 @@ def change_email_settings(request):
|
||||
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
|
||||
|
||||
return JsonResponse({"success": True})
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1878,3 +1880,5 @@ def token(request):
|
||||
newtoken = create_token(secret, custom_data)
|
||||
response = HttpResponse(newtoken, mimetype="text/plain")
|
||||
return response
|
||||
=======
|
||||
>>>>>>> edx/master
|
||||
|
||||
@@ -620,6 +620,7 @@ class LoncapaProblem(object):
|
||||
"""
|
||||
context = {}
|
||||
context['seed'] = self.seed
|
||||
context['anonymous_student_id'] = self.capa_system.anonymous_student_id
|
||||
all_code = ''
|
||||
|
||||
python_path = []
|
||||
|
||||
@@ -73,6 +73,24 @@ class CapaHtmlRenderTest(unittest.TestCase):
|
||||
span_element = rendered_html.find('span')
|
||||
self.assertEqual(span_element.text, 'Test text')
|
||||
|
||||
def test_anonymous_student_id(self):
|
||||
# make sure anonymous_student_id is rendered properly as a context variable
|
||||
xml_str = textwrap.dedent("""
|
||||
<problem>
|
||||
<span>Welcome $anonymous_student_id</span>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
# Create the problem
|
||||
problem = new_loncapa_problem(xml_str)
|
||||
|
||||
# Render the HTML
|
||||
rendered_html = etree.XML(problem.get_html())
|
||||
|
||||
# Expect that the anonymous_student_id was converted to "student"
|
||||
span_element = rendered_html.find('span')
|
||||
self.assertEqual(span_element.text, 'Welcome student')
|
||||
|
||||
def test_render_script(self):
|
||||
# Generate some XML with a <script> tag
|
||||
xml_str = textwrap.dedent("""
|
||||
|
||||
32
common/lib/xmodule/xmodule/annotator_token.py
Normal file
32
common/lib/xmodule/xmodule/annotator_token.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
This file contains a function used to retrieve the token for the annotation backend
|
||||
without having to create a view, but just returning a string instead.
|
||||
|
||||
It can be called from other files by using the following:
|
||||
from xmodule.annotator_token import retrieve_token
|
||||
"""
|
||||
import datetime
|
||||
from firebase_token_generator import create_token
|
||||
|
||||
|
||||
def retrieve_token(userid, secret):
|
||||
'''
|
||||
Return a token for the backend of annotations.
|
||||
It uses the course id to retrieve a variable that contains the secret
|
||||
token found in inheritance.py. It also contains information of when
|
||||
the token was issued. This will be stored with the user along with
|
||||
the id for identification purposes in the backend.
|
||||
'''
|
||||
|
||||
# the following five lines of code allows you to include the default timezone in the iso format
|
||||
# for more information: http://stackoverflow.com/questions/3401428/how-to-get-an-isoformat-datetime-string-including-the-default-timezone
|
||||
dtnow = datetime.datetime.now()
|
||||
dtutcnow = datetime.datetime.utcnow()
|
||||
delta = dtnow - dtutcnow
|
||||
newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
|
||||
newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin)
|
||||
# uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a
|
||||
# federated system in the annotation backend server
|
||||
custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400}
|
||||
newtoken = create_token(secret, custom_data)
|
||||
return newtoken
|
||||
@@ -177,59 +177,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('YouTube video in FireFox will cue first', function () {
|
||||
var oldUserAgent;
|
||||
|
||||
beforeEach(function () {
|
||||
oldUserAgent = window.navigator.userAgent;
|
||||
window.navigator.userAgent = 'firefox';
|
||||
|
||||
state = jasmine.initializePlayer('video.html', {
|
||||
start: 10,
|
||||
end: 30
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
window.navigator.userAgent = oldUserAgent;
|
||||
});
|
||||
|
||||
it('cue is called, skipOnEndedStartEndReset is set', function () {
|
||||
state.videoPlayer.updatePlayTime(10);
|
||||
expect(state.videoPlayer.player.cueVideoById).toHaveBeenCalledWith('cogebirgzzM', 10);
|
||||
expect(state.videoPlayer.skipOnEndedStartEndReset).toBe(true);
|
||||
});
|
||||
|
||||
it('when position is not 0: cue is called with stored position value', function () {
|
||||
state.config.savedVideoPosition = 15;
|
||||
|
||||
state.videoPlayer.updatePlayTime(10);
|
||||
expect(state.videoPlayer.player.cueVideoById).toHaveBeenCalledWith('cogebirgzzM', 15);
|
||||
});
|
||||
|
||||
it('Handling cue state', function () {
|
||||
spyOn(state.videoPlayer, 'play');
|
||||
|
||||
state.videoPlayer.seekToTimeOnCued = 10;
|
||||
state.videoPlayer.onStateChange({data: 5});
|
||||
|
||||
expect(state.videoPlayer.player.seekTo).toHaveBeenCalledWith(10, true);
|
||||
expect(state.videoPlayer.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('even when cued, onEnded does not resets start and end time', function () {
|
||||
state.videoPlayer.skipOnEndedStartEndReset = true;
|
||||
state.videoPlayer.onEnded();
|
||||
expect(state.videoPlayer.startTime).toBe(10);
|
||||
expect(state.videoPlayer.endTime).toBe(30);
|
||||
|
||||
state.videoPlayer.skipOnEndedStartEndReset = undefined;
|
||||
state.videoPlayer.onEnded();
|
||||
expect(state.videoPlayer.startTime).toBe(10);
|
||||
expect(state.videoPlayer.endTime).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checking start and end times', function () {
|
||||
var miniTestSuite = [
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
state.storage.clear();
|
||||
window.Video.previousState = null;
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
@@ -37,7 +38,7 @@
|
||||
});
|
||||
|
||||
it('add ARIA attributes to time control', function () {
|
||||
var timeControl = $('div.slider>a');
|
||||
var timeControl = $('div.slider > a');
|
||||
|
||||
expect(timeControl).toHaveAttrs({
|
||||
'role': 'slider',
|
||||
@@ -135,8 +136,6 @@
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
|
||||
state.storage.clear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -389,7 +388,7 @@
|
||||
runs(function () {
|
||||
state = jasmine.initializePlayer({
|
||||
end: 20,
|
||||
savedVideoPosition: 'a'
|
||||
savedVideoPosition: 'a'
|
||||
});
|
||||
sliderEl = state.videoProgressSlider.slider;
|
||||
spyOn(state.videoPlayer, 'duration').andReturn(60);
|
||||
|
||||
@@ -17,6 +17,7 @@ function (VideoPlayer) {
|
||||
afterEach(function () {
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
window.Video.previousState = null;
|
||||
if (state.storage) {
|
||||
state.storage.clear();
|
||||
}
|
||||
@@ -179,6 +180,11 @@ function (VideoPlayer) {
|
||||
it('autoplay the first video', function () {
|
||||
expect(state.videoPlayer.play).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('invalid endTime is reset to null', function () {
|
||||
expect(state.videoPlayer.endTime).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onReady YouTube', function () {
|
||||
@@ -752,17 +758,6 @@ function (VideoPlayer) {
|
||||
isFlashMode: jasmine.createSpy().andReturn(false)
|
||||
};
|
||||
});
|
||||
|
||||
it('invalid endTime is reset to null', function () {
|
||||
VideoPlayer.prototype.updatePlayTime.call(state, 0);
|
||||
|
||||
expect(state.videoPlayer.figureOutStartingTime).toHaveBeenCalled();
|
||||
|
||||
VideoPlayer.prototype.figureOutStartEndTime.call(state, 60);
|
||||
VideoPlayer.prototype.figureOutStartingTime.call(state, 60);
|
||||
|
||||
expect(state.videoPlayer.endTime).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFullScreen', function () {
|
||||
@@ -1087,9 +1082,12 @@ function (VideoPlayer) {
|
||||
isHtml5Mode: jasmine.createSpy().andReturn(true),
|
||||
isYoutubeType: jasmine.createSpy().andReturn(true),
|
||||
setPlayerMode: jasmine.createSpy(),
|
||||
trigger: jasmine.createSpy(),
|
||||
videoPlayer: {
|
||||
currentTime: 60,
|
||||
isPlaying: jasmine.createSpy(),
|
||||
seekTo: jasmine.createSpy(),
|
||||
duration: jasmine.createSpy().andReturn(60),
|
||||
updatePlayTime: jasmine.createSpy(),
|
||||
setPlaybackRate: jasmine.createSpy(),
|
||||
player: jasmine.createSpyObj('player', [
|
||||
@@ -1115,6 +1113,12 @@ function (VideoPlayer) {
|
||||
state.videoPlayer.isPlaying.andReturn(false);
|
||||
VideoPlayer.prototype.setPlaybackRate.call(state, '0.75');
|
||||
expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
|
||||
expect(state.videoPlayer.seekTo).toHaveBeenCalledWith(60);
|
||||
expect(state.trigger).toHaveBeenCalledWith(
|
||||
'videoProgressSlider.updateStartEndTimeRegion',
|
||||
{
|
||||
duration: 60
|
||||
});
|
||||
expect(state.videoPlayer.player.cueVideoById)
|
||||
.toHaveBeenCalledWith('videoId', 60);
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ function (HTML5Video, Resizer) {
|
||||
onVolumeChange: onVolumeChange,
|
||||
pause: pause,
|
||||
play: play,
|
||||
seekTo: seekTo,
|
||||
setPlaybackRate: setPlaybackRate,
|
||||
update: update,
|
||||
figureOutStartEndTime: figureOutStartEndTime,
|
||||
@@ -94,7 +95,7 @@ function (HTML5Video, Resizer) {
|
||||
state.videoPlayer.ready = _.once(function () {
|
||||
$(window).on('unload', state.saveState);
|
||||
|
||||
if (!state.isFlashMode()) {
|
||||
if (!state.isFlashMode() && state.speed != '1.0') {
|
||||
state.videoPlayer.setPlaybackRate(state.speed);
|
||||
}
|
||||
state.videoPlayer.player.setVolume(state.currentVolume);
|
||||
@@ -352,7 +353,8 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
function setPlaybackRate(newSpeed) {
|
||||
var time = this.videoPlayer.currentTime,
|
||||
var duration = this.videoPlayer.duration(),
|
||||
time = this.videoPlayer.currentTime,
|
||||
methodName, youtubeId;
|
||||
|
||||
if (
|
||||
@@ -378,7 +380,22 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
|
||||
this.videoPlayer.player[methodName](youtubeId, time);
|
||||
|
||||
// We need to call play() explicitly because after the call
|
||||
// to functions cueVideoById() followed by seekTo() the video
|
||||
// is in a PAUSED state.
|
||||
//
|
||||
// Why? This is how the YouTube API is implemented.
|
||||
this.videoPlayer.updatePlayTime(time);
|
||||
if (time > 0 && this.isFlashMode()) {
|
||||
this.videoPlayer.seekTo(time);
|
||||
this.trigger(
|
||||
'videoProgressSlider.updateStartEndTimeRegion',
|
||||
{
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,59 +431,62 @@ function (HTML5Video, Resizer) {
|
||||
// It is created on a onPlay event. Cleared on a onPause event.
|
||||
// Reinitialized on a onSeek event.
|
||||
function onSeek(params) {
|
||||
var duration = this.videoPlayer.duration(),
|
||||
newTime = params.time;
|
||||
|
||||
if (
|
||||
(typeof newTime !== 'number') ||
|
||||
(newTime > duration) ||
|
||||
(newTime < 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.off('play.seek');
|
||||
this.videoPlayer.log(
|
||||
'seek_video',
|
||||
{
|
||||
old_time: this.videoPlayer.currentTime,
|
||||
new_time: newTime,
|
||||
type: params.type
|
||||
}
|
||||
);
|
||||
var time = params.time,
|
||||
type = params.type;
|
||||
|
||||
// After the user seeks, the video will start playing from
|
||||
// the sought point, and stop playing at the end.
|
||||
this.videoPlayer.goToStartTime = false;
|
||||
if (newTime > this.videoPlayer.endTime || this.videoPlayer.endTime === null) {
|
||||
if (time > this.videoPlayer.endTime || this.videoPlayer.endTime === null) {
|
||||
this.videoPlayer.stopAtEndTime = false;
|
||||
}
|
||||
|
||||
this.videoPlayer.seekTo(time);
|
||||
|
||||
this.videoPlayer.log(
|
||||
'seek_video',
|
||||
{
|
||||
old_time: this.videoPlayer.currentTime,
|
||||
new_time: time,
|
||||
type: type
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function seekTo(time) {
|
||||
var duration = this.videoPlayer.duration();
|
||||
|
||||
if ((typeof time !== 'number') || (time > duration) || (time < 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el.off('play.seek');
|
||||
|
||||
if (this.videoPlayer.isPlaying()) {
|
||||
this.videoPlayer.stopTimer();
|
||||
} else {
|
||||
this.videoPlayer.currentTime = newTime;
|
||||
this.videoPlayer.currentTime = time;
|
||||
}
|
||||
var isUnplayed = this.videoPlayer.isUnstarted() ||
|
||||
this.videoPlayer.isCued();
|
||||
|
||||
// Use `cueVideoById` method for youtube video that is not played before.
|
||||
if (isUnplayed && this.isYoutubeType()) {
|
||||
this.videoPlayer.player.cueVideoById(this.youtubeId(), newTime);
|
||||
this.videoPlayer.player.cueVideoById(this.youtubeId(), time);
|
||||
} else {
|
||||
// Youtube video cannot be rewinded during bufferization, so wait to
|
||||
// finish bufferization and then rewind the video.
|
||||
if (this.isYoutubeType() && this.videoPlayer.isBuffering()) {
|
||||
this.el.on('play.seek', function () {
|
||||
this.videoPlayer.player.seekTo(newTime, true);
|
||||
this.videoPlayer.player.seekTo(time, true);
|
||||
}.bind(this));
|
||||
} else {
|
||||
// Otherwise, just seek the video
|
||||
this.videoPlayer.player.seekTo(newTime, true);
|
||||
this.videoPlayer.player.seekTo(time, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.videoPlayer.updatePlayTime(newTime, true);
|
||||
this.videoPlayer.updatePlayTime(time, true);
|
||||
this.el.trigger('seek', arguments);
|
||||
}
|
||||
|
||||
@@ -609,6 +629,7 @@ function (HTML5Video, Resizer) {
|
||||
// have 1 speed available, we fall back to Flash.
|
||||
|
||||
_restartUsingFlash(this);
|
||||
return false;
|
||||
} else if (availablePlaybackRates.length > 1) {
|
||||
this.setPlayerMode('html5');
|
||||
|
||||
@@ -646,16 +667,15 @@ function (HTML5Video, Resizer) {
|
||||
this.videoPlayer.player.setPlaybackRate(this.speed);
|
||||
}
|
||||
|
||||
this.el.trigger('ready', arguments);
|
||||
/* The following has been commented out to make sure autoplay is
|
||||
disabled for students.
|
||||
if (
|
||||
!this.isTouch &&
|
||||
$('.video:first').data('autoplay') === 'True'
|
||||
) {
|
||||
this.videoPlayer.play();
|
||||
|
||||
var duration = this.videoPlayer.duration(),
|
||||
time = this.videoPlayer.figureOutStartingTime(duration);
|
||||
|
||||
if (time > 0 && this.videoPlayer.goToStartTime) {
|
||||
this.videoPlayer.seekTo(time);
|
||||
}
|
||||
*/
|
||||
|
||||
this.el.trigger('ready', arguments);
|
||||
}
|
||||
|
||||
function onStateChange(event) {
|
||||
@@ -687,13 +707,9 @@ function (HTML5Video, Resizer) {
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.CUED:
|
||||
this.el.addClass('is-cued');
|
||||
this.videoPlayer.player.seekTo(this.videoPlayer.seekToTimeOnCued, true);
|
||||
// We need to call play() explicitly because after the call
|
||||
// to functions cueVideoById() followed by seekTo() the video
|
||||
// is in a PAUSED state.
|
||||
//
|
||||
// Why? This is how the YouTube API is implemented.
|
||||
this.videoPlayer.play();
|
||||
if (this.isFlashMode()) {
|
||||
this.videoPlayer.play();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -769,57 +785,6 @@ function (HTML5Video, Resizer) {
|
||||
duration = this.videoPlayer.duration(),
|
||||
youTubeId;
|
||||
|
||||
if (duration > 0 && videoPlayer.goToStartTime && !skip_seek) {
|
||||
videoPlayer.goToStartTime = false;
|
||||
|
||||
// The duration might have changed. Update the start-end time region to
|
||||
// reflect this fact.
|
||||
this.trigger(
|
||||
'videoProgressSlider.updateStartEndTimeRegion',
|
||||
{
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
|
||||
time = videoPlayer.figureOutStartingTime(duration);
|
||||
|
||||
// When the video finishes playing, we will start from the
|
||||
// start-time, or from the beginning (rather than from the remembered
|
||||
// position).
|
||||
this.config.savedVideoPosition = 0;
|
||||
|
||||
if (time > 0) {
|
||||
// After a bug came up (BLD-708: "In Firefox YouTube video with
|
||||
// start-time plays from 00:00:00") the video refused to play
|
||||
// from start-time, and only played from the beginning.
|
||||
//
|
||||
// It turned out that for some reason if Firefox you couldn't
|
||||
// seek beyond some amount of time before the video loaded.
|
||||
// Very strange, but in Chrome there is no such bug.
|
||||
//
|
||||
// HTML5 video sources play fine from start-time in both Chrome
|
||||
// and Firefox.
|
||||
if (this.browserIsFirefox && this.isYoutubeType()) {
|
||||
youTubeId = this.youtubeId();
|
||||
|
||||
// When we will call cueVideoById() for some strange reason
|
||||
// an ENDED event will be fired. It really does no damage
|
||||
// except for the fact that the end-time is reset to null.
|
||||
// We do not want this.
|
||||
//
|
||||
// The flag `skipOnEndedStartEndReset` will notify the
|
||||
// onEnded() callback for the ENDED event that there
|
||||
// is no need in resetting the start-time and end-time.
|
||||
videoPlayer.skipOnEndedStartEndReset = true;
|
||||
|
||||
videoPlayer.seekToTimeOnCued = time;
|
||||
videoPlayer.player.cueVideoById(youTubeId, time);
|
||||
} else {
|
||||
videoPlayer.player.seekTo(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.trigger(
|
||||
'videoProgressSlider.updatePlayTime',
|
||||
{
|
||||
|
||||
@@ -57,18 +57,11 @@
|
||||
VideoCaption
|
||||
) {
|
||||
var youtubeXhr = null,
|
||||
oldVideo = window.Video,
|
||||
|
||||
// Because this constructor can be called multiple times on a single page (when the user switches
|
||||
// verticals, the page doesn't reload, but the content changes), we must will check each time if there
|
||||
// is a previous copy of 'state' object. If there is, we will make sure that copy exists cleanly. We
|
||||
// have to do this because when verticals switch, the code does not handle any Xmodule JS code that is
|
||||
// running - it simply removes DOM elements from the page. Any functions that were running during this,
|
||||
// and that will run afterwards (expecting the DOM elements to be present) must be stopped by hand.
|
||||
previousState = null;
|
||||
oldVideo = window.Video;
|
||||
|
||||
window.Video = function (element) {
|
||||
var state;
|
||||
var previousState = window.Video.previousState,
|
||||
state;
|
||||
|
||||
// Check for existance of previous state, uninitialize it if necessary, and create a new state. Store
|
||||
// new state for future invocation of this module consturctor function.
|
||||
@@ -78,7 +71,13 @@
|
||||
}
|
||||
|
||||
state = {};
|
||||
previousState = state;
|
||||
// Because this constructor can be called multiple times on a single page (when the user switches
|
||||
// verticals, the page doesn't reload, but the content changes), we must will check each time if there
|
||||
// is a previous copy of 'state' object. If there is, we will make sure that copy exists cleanly. We
|
||||
// have to do this because when verticals switch, the code does not handle any Xmodule JS code that is
|
||||
// running - it simply removes DOM elements from the page. Any functions that were running during this,
|
||||
// and that will run afterwards (expecting the DOM elements to be present) must be stopped by hand.
|
||||
window.Video.previousState = state;
|
||||
|
||||
state.modules = [
|
||||
FocusGrabber,
|
||||
|
||||
20
common/lib/xmodule/xmodule/tests/test_annotator_token.py
Normal file
20
common/lib/xmodule/xmodule/tests/test_annotator_token.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
This test will run for annotator_token.py
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from xmodule.annotator_token import retrieve_token
|
||||
|
||||
|
||||
class TokenRetriever(unittest.TestCase):
|
||||
"""
|
||||
Tests to make sure that when passed in a username and secret token, that it will be encoded correctly
|
||||
"""
|
||||
def test_token(self):
|
||||
"""
|
||||
Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text.
|
||||
"""
|
||||
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ"
|
||||
response = retrieve_token("username", "fake_secret")
|
||||
self.assertEqual(expected.split('.')[0], response.split('.')[0])
|
||||
self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
|
||||
@@ -38,17 +38,6 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
|
||||
ScopeIds(None, None, None, None)
|
||||
)
|
||||
|
||||
def test_render_content(self):
|
||||
"""
|
||||
Tests to make sure the sample xml is rendered and that it forms a valid xmltree
|
||||
that does not contain a display_name.
|
||||
"""
|
||||
content = self.mod._render_content() # pylint: disable=W0212
|
||||
self.assertIsNotNone(content)
|
||||
element = etree.fromstring(content)
|
||||
self.assertIsNotNone(element)
|
||||
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
|
||||
|
||||
def test_extract_instructions(self):
|
||||
"""
|
||||
Tests to make sure that the instructions are correctly pulled from the sample xml above.
|
||||
@@ -70,5 +59,5 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
|
||||
Tests the function that passes in all the information in the context that will be used in templates/textannotation.html
|
||||
"""
|
||||
context = self.mod.get_html()
|
||||
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']:
|
||||
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token']:
|
||||
self.assertIn(key, context)
|
||||
|
||||
@@ -34,100 +34,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
|
||||
ScopeIds(None, None, None, None)
|
||||
)
|
||||
|
||||
def test_annotation_class_attr_default(self):
|
||||
"""
|
||||
Makes sure that it can detect annotation values in text-form if user
|
||||
decides to add text to the area below video, video functionality is completely
|
||||
found in javascript.
|
||||
"""
|
||||
xml = '<annotation title="x" body="y" problem="0">test</annotation>'
|
||||
element = etree.fromstring(xml)
|
||||
|
||||
expected_attr = {'class': {'value': 'annotatable-span highlight'}}
|
||||
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
|
||||
|
||||
self.assertIsInstance(actual_attr, dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_annotation_class_attr_with_valid_highlight(self):
|
||||
"""
|
||||
Same as above but more specific to an area that is highlightable in the appropriate
|
||||
color designated.
|
||||
"""
|
||||
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
|
||||
|
||||
for color in self.mod.highlight_colors:
|
||||
element = etree.fromstring(xml.format(highlight=color))
|
||||
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
|
||||
|
||||
expected_attr = {'class': {
|
||||
'value': value,
|
||||
'_delete': 'highlight'}
|
||||
}
|
||||
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
|
||||
|
||||
self.assertIsInstance(actual_attr, dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_annotation_class_attr_with_invalid_highlight(self):
|
||||
"""
|
||||
Same as above, but checked with invalid colors.
|
||||
"""
|
||||
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
|
||||
|
||||
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
|
||||
element = etree.fromstring(xml.format(highlight=invalid_color))
|
||||
expected_attr = {'class': {
|
||||
'value': 'annotatable-span highlight',
|
||||
'_delete': 'highlight'}
|
||||
}
|
||||
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
|
||||
|
||||
self.assertIsInstance(actual_attr, dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_annotation_data_attr(self):
|
||||
"""
|
||||
Test that each highlight contains the data information from the annotation itself.
|
||||
"""
|
||||
element = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
|
||||
|
||||
expected_attr = {
|
||||
'data-comment-body': {'value': 'foo', '_delete': 'body'},
|
||||
'data-comment-title': {'value': 'bar', '_delete': 'title'},
|
||||
'data-problem-id': {'value': '0', '_delete': 'problem'}
|
||||
}
|
||||
|
||||
actual_attr = self.mod._get_annotation_data_attr(element) # pylint: disable=W0212
|
||||
|
||||
self.assertIsInstance(actual_attr, dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_render_annotation(self):
|
||||
"""
|
||||
Tests to make sure that the spans designating annotations acutally visually render as annotations.
|
||||
"""
|
||||
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
|
||||
expected_el = etree.fromstring(expected_html)
|
||||
|
||||
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
|
||||
self.mod._render_annotation(actual_el) # pylint: disable=W0212
|
||||
|
||||
self.assertEqual(expected_el.tag, actual_el.tag)
|
||||
self.assertEqual(expected_el.text, actual_el.text)
|
||||
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
|
||||
|
||||
def test_render_content(self):
|
||||
"""
|
||||
Like above, but using the entire text, it makes sure that display_name is removed and that there is only one
|
||||
div encompassing the annotatable area.
|
||||
"""
|
||||
content = self.mod._render_content() # pylint: disable=W0212
|
||||
element = etree.fromstring(content)
|
||||
self.assertIsNotNone(element)
|
||||
self.assertEqual('div', element.tag, 'root tag is a div')
|
||||
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
|
||||
|
||||
def test_extract_instructions(self):
|
||||
"""
|
||||
This test ensures that if an instruction exists it is pulled and
|
||||
@@ -160,6 +66,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
|
||||
"""
|
||||
Tests to make sure variables passed in truly exist within the html once it is all rendered.
|
||||
"""
|
||||
context = self.mod.get_html()
|
||||
for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']:
|
||||
context = self.mod.get_html() # pylint: disable=W0212
|
||||
for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']:
|
||||
self.assertIn(key, context)
|
||||
|
||||
@@ -6,6 +6,7 @@ from pkg_resources import resource_string
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xblock.core import Scope, String
|
||||
from xmodule.annotator_token import retrieve_token
|
||||
|
||||
import textwrap
|
||||
|
||||
@@ -30,7 +31,7 @@ class AnnotatableFields(object):
|
||||
scope=Scope.settings,
|
||||
default='Text Annotation',
|
||||
)
|
||||
tags = String(
|
||||
instructor_tags = String(
|
||||
display_name="Tags for Assignments",
|
||||
help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue",
|
||||
scope=Scope.settings,
|
||||
@@ -43,6 +44,7 @@ class AnnotatableFields(object):
|
||||
default='None',
|
||||
)
|
||||
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
|
||||
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
|
||||
|
||||
|
||||
class TextAnnotationModule(AnnotatableFields, XModule):
|
||||
@@ -59,15 +61,9 @@ class TextAnnotationModule(AnnotatableFields, XModule):
|
||||
|
||||
self.instructions = self._extract_instructions(xmltree)
|
||||
self.content = etree.tostring(xmltree, encoding='unicode')
|
||||
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
|
||||
|
||||
def _render_content(self):
|
||||
""" Renders annotatable content with annotation spans and returns HTML. """
|
||||
xmltree = etree.fromstring(self.content)
|
||||
if 'display_name' in xmltree.attrib:
|
||||
del xmltree.attrib['display_name']
|
||||
|
||||
return etree.tostring(xmltree, encoding='unicode')
|
||||
self.user_email = ""
|
||||
if self.runtime.get_real_user is not None:
|
||||
self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email
|
||||
|
||||
def _extract_instructions(self, xmltree):
|
||||
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
|
||||
@@ -82,13 +78,13 @@ class TextAnnotationModule(AnnotatableFields, XModule):
|
||||
""" Renders parameters to template. """
|
||||
context = {
|
||||
'display_name': self.display_name_with_default,
|
||||
'tag': self.tags,
|
||||
'tag': self.instructor_tags,
|
||||
'source': self.source,
|
||||
'instructions_html': self.instructions,
|
||||
'content_html': self._render_content(),
|
||||
'annotation_storage': self.annotation_storage_url
|
||||
'content_html': self.content,
|
||||
'annotation_storage': self.annotation_storage_url,
|
||||
'token': retrieve_token(self.user_email, self.annotation_token_secret),
|
||||
}
|
||||
|
||||
return self.system.render_template('textannotation.html', context)
|
||||
|
||||
|
||||
@@ -101,6 +97,7 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor):
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([
|
||||
TextAnnotationDescriptor.annotation_storage_url
|
||||
TextAnnotationDescriptor.annotation_storage_url,
|
||||
TextAnnotationDescriptor.annotation_token_secret,
|
||||
])
|
||||
return non_editable_fields
|
||||
|
||||
@@ -7,6 +7,7 @@ from pkg_resources import resource_string
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xblock.core import Scope, String
|
||||
from xmodule.annotator_token import retrieve_token
|
||||
|
||||
import textwrap
|
||||
|
||||
@@ -31,7 +32,7 @@ class AnnotatableFields(object):
|
||||
sourceurl = String(help="The external source URL for the video.", display_name="Source URL", scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4")
|
||||
poster_url = String(help="Poster Image URL", display_name="Poster URL", scope=Scope.settings, default="")
|
||||
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
|
||||
|
||||
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
|
||||
|
||||
class VideoAnnotationModule(AnnotatableFields, XModule):
|
||||
'''Video Annotation Module'''
|
||||
@@ -55,73 +56,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
|
||||
|
||||
self.instructions = self._extract_instructions(xmltree)
|
||||
self.content = etree.tostring(xmltree, encoding='unicode')
|
||||
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
|
||||
|
||||
def _get_annotation_class_attr(self, element):
|
||||
""" Returns a dict with the CSS class attribute to set on the annotation
|
||||
and an XML key to delete from the element.
|
||||
"""
|
||||
|
||||
attr = {}
|
||||
cls = ['annotatable-span', 'highlight']
|
||||
highlight_key = 'highlight'
|
||||
color = element.get(highlight_key)
|
||||
|
||||
if color is not None:
|
||||
if color in self.highlight_colors:
|
||||
cls.append('highlight-' + color)
|
||||
attr['_delete'] = highlight_key
|
||||
attr['value'] = ' '.join(cls)
|
||||
|
||||
return {'class': attr}
|
||||
|
||||
def _get_annotation_data_attr(self, element):
|
||||
""" Returns a dict in which the keys are the HTML data attributes
|
||||
to set on the annotation element. Each data attribute has a
|
||||
corresponding 'value' and (optional) '_delete' key to specify
|
||||
an XML attribute to delete.
|
||||
"""
|
||||
|
||||
data_attrs = {}
|
||||
attrs_map = {
|
||||
'body': 'data-comment-body',
|
||||
'title': 'data-comment-title',
|
||||
'problem': 'data-problem-id'
|
||||
}
|
||||
|
||||
for xml_key in attrs_map.keys():
|
||||
if xml_key in element.attrib:
|
||||
value = element.get(xml_key, '')
|
||||
html_key = attrs_map[xml_key]
|
||||
data_attrs[html_key] = {'value': value, '_delete': xml_key}
|
||||
|
||||
return data_attrs
|
||||
|
||||
def _render_annotation(self, element):
|
||||
""" Renders an annotation element for HTML output. """
|
||||
attr = {}
|
||||
attr.update(self._get_annotation_class_attr(element))
|
||||
attr.update(self._get_annotation_data_attr(element))
|
||||
|
||||
element.tag = 'span'
|
||||
|
||||
for key in attr.keys():
|
||||
element.set(key, attr[key]['value'])
|
||||
if '_delete' in attr[key] and attr[key]['_delete'] is not None:
|
||||
delete_key = attr[key]['_delete']
|
||||
del element.attrib[delete_key]
|
||||
|
||||
def _render_content(self):
|
||||
""" Renders annotatable content with annotation spans and returns HTML. """
|
||||
xmltree = etree.fromstring(self.content)
|
||||
xmltree.tag = 'div'
|
||||
if 'display_name' in xmltree.attrib:
|
||||
del xmltree.attrib['display_name']
|
||||
|
||||
for element in xmltree.findall('.//annotation'):
|
||||
self._render_annotation(element)
|
||||
|
||||
return etree.tostring(xmltree, encoding='unicode')
|
||||
self.user_email = ""
|
||||
if self.runtime.get_real_user is not None:
|
||||
self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email
|
||||
|
||||
def _extract_instructions(self, xmltree):
|
||||
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
|
||||
@@ -154,9 +91,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
|
||||
'sourceUrl': self.sourceurl,
|
||||
'typeSource': extension,
|
||||
'poster': self.poster_url,
|
||||
'alert': self,
|
||||
'content_html': self._render_content(),
|
||||
'annotation_storage': self.annotation_storage_url
|
||||
'content_html': self.content,
|
||||
'annotation_storage': self.annotation_storage_url,
|
||||
'token': retrieve_token(self.user_email, self.annotation_token_secret),
|
||||
}
|
||||
|
||||
return self.system.render_template('videoannotation.html', context)
|
||||
@@ -171,6 +108,7 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor):
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([
|
||||
VideoAnnotationDescriptor.annotation_storage_url
|
||||
VideoAnnotationDescriptor.annotation_storage_url,
|
||||
VideoAnnotationDescriptor.annotation_token_secret,
|
||||
])
|
||||
return non_editable_fields
|
||||
|
||||
22
common/static/js/vendor/ova/annotator-full-firebase-auth.js
vendored
Normal file
22
common/static/js/vendor/ova/annotator-full-firebase-auth.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
Annotator.Plugin.Auth.prototype.haveValidToken = function() {
|
||||
return (
|
||||
this._unsafeToken &&
|
||||
this._unsafeToken.d.issuedAt &&
|
||||
this._unsafeToken.d.ttl &&
|
||||
this._unsafeToken.d.consumerKey &&
|
||||
this.timeToExpiry() > 0
|
||||
);
|
||||
};
|
||||
|
||||
Annotator.Plugin.Auth.prototype.timeToExpiry = function() {
|
||||
var expiry, issue, now, timeToExpiry;
|
||||
now = new Date().getTime() / 1000;
|
||||
issue = createDateFromISO8601(this._unsafeToken.d.issuedAt).getTime() / 1000;
|
||||
expiry = issue + this._unsafeToken.d.ttl;
|
||||
timeToExpiry = expiry - now;
|
||||
if (timeToExpiry > 0) {
|
||||
return timeToExpiry;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,12 @@ def index(request):
|
||||
|
||||
if settings.FEATURES.get('AUTH_USE_CERTIFICATES'):
|
||||
from external_auth.views import ssl_login
|
||||
# Set next URL to dashboard if it isn't set to avoid
|
||||
# caching a redirect to / that causes a redirect loop on logout
|
||||
if not request.GET.get('next'):
|
||||
req_new = request.GET.copy()
|
||||
req_new['next'] = reverse('dashboard')
|
||||
request.GET = req_new
|
||||
return ssl_login(request)
|
||||
|
||||
enable_mktg_site = microsite.get_value(
|
||||
|
||||
@@ -21,7 +21,7 @@ from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
GIT_REPO_DIR = getattr(settings, 'GIT_REPO_DIR', '/opt/edx/course_repos')
|
||||
GIT_REPO_DIR = getattr(settings, 'GIT_REPO_DIR', '/edx/var/app/edxapp/course_repos')
|
||||
GIT_IMPORT_STATIC = getattr(settings, 'GIT_IMPORT_STATIC', True)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from django.views.decorators.http import condition
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from edxmako.shortcuts import render_to_response
|
||||
import mongoengine
|
||||
from path import path
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
import dashboard.git_import as git_import
|
||||
@@ -330,8 +331,12 @@ class Courses(SysadminDashboardView):
|
||||
cmd = ''
|
||||
gdir = settings.DATA_DIR / cdir
|
||||
info = ['', '', '']
|
||||
if not os.path.exists(gdir):
|
||||
return info
|
||||
|
||||
# Try the data dir, then try to find it in the git import dir
|
||||
if not gdir.exists():
|
||||
gdir = path(git_import.GIT_REPO_DIR) / cdir
|
||||
if not gdir.exists():
|
||||
return info
|
||||
|
||||
cmd = ['git', 'log', '-1',
|
||||
'--format=format:{ "commit": "%H", "author": "%an %ae", "date": "%ad"}', ]
|
||||
@@ -345,7 +350,7 @@ class Courses(SysadminDashboardView):
|
||||
|
||||
return info
|
||||
|
||||
def get_course_from_git(self, gitloc, branch, datatable):
|
||||
def get_course_from_git(self, gitloc, branch):
|
||||
"""This downloads and runs the checks for importing a course in git"""
|
||||
|
||||
if not (gitloc.endswith('.git') or gitloc.startswith('http:') or
|
||||
@@ -356,7 +361,7 @@ class Courses(SysadminDashboardView):
|
||||
if self.is_using_mongo:
|
||||
return self.import_mongo_course(gitloc, branch)
|
||||
|
||||
return self.import_xml_course(gitloc, branch, datatable)
|
||||
return self.import_xml_course(gitloc, branch)
|
||||
|
||||
def import_mongo_course(self, gitloc, branch):
|
||||
"""
|
||||
@@ -408,7 +413,7 @@ class Courses(SysadminDashboardView):
|
||||
msg += "<pre>{0}</pre>".format(escape(ret))
|
||||
return msg
|
||||
|
||||
def import_xml_course(self, gitloc, branch, datatable):
|
||||
def import_xml_course(self, gitloc, branch):
|
||||
"""Imports a git course into the XMLModuleStore"""
|
||||
|
||||
msg = u''
|
||||
@@ -475,8 +480,7 @@ class Courses(SysadminDashboardView):
|
||||
msg += u'<li><pre>{0}: {1}</pre></li>'.format(escape(summary),
|
||||
escape(err))
|
||||
msg += u'</ul>'
|
||||
datatable['data'].append([course.display_name, cdir]
|
||||
+ self.git_info_for_course(cdir))
|
||||
|
||||
return msg
|
||||
|
||||
def make_datatable(self):
|
||||
@@ -484,9 +488,17 @@ class Courses(SysadminDashboardView):
|
||||
|
||||
data = []
|
||||
|
||||
<<<<<<< HEAD
|
||||
for course in self.get_courses():
|
||||
gdir = course.id.run
|
||||
data.append([course.display_name, course.id.to_deprecated_string()]
|
||||
=======
|
||||
for (cdir, course) in courses.items():
|
||||
gdir = cdir
|
||||
if '/' in cdir:
|
||||
gdir = cdir.split('/')[1]
|
||||
data.append([course.display_name, cdir]
|
||||
>>>>>>> edx/master
|
||||
+ self.git_info_for_course(gdir))
|
||||
|
||||
return dict(header=[_('Course Name'), _('Directory/ID'),
|
||||
@@ -524,8 +536,7 @@ class Courses(SysadminDashboardView):
|
||||
if action == 'add_course':
|
||||
gitloc = request.POST.get('repo_location', '').strip().replace(' ', '').replace(';', '')
|
||||
branch = request.POST.get('repo_branch', '').strip().replace(' ', '').replace(';', '')
|
||||
datatable = self.make_datatable()
|
||||
self.msg += self.get_course_from_git(gitloc, branch, datatable)
|
||||
self.msg += self.get_course_from_git(gitloc, branch)
|
||||
|
||||
elif action == 'del_course':
|
||||
course_id = request.POST.get('course_id', '').strip()
|
||||
@@ -569,12 +580,17 @@ class Courses(SysadminDashboardView):
|
||||
delete_course(self.def_ms, content_store, course.id, commit)
|
||||
# don't delete user permission groups, though
|
||||
self.msg += \
|
||||
<<<<<<< HEAD
|
||||
u"<font color='red'>{0} {1} ({2})</font>".format(
|
||||
_('Deleted'), course.id.to_deprecated_string(), course.display_name)
|
||||
datatable = self.make_datatable()
|
||||
=======
|
||||
u"<font color='red'>{0} {1} = {2} ({3})</font>".format(
|
||||
_('Deleted'), loc, course.id, course.display_name)
|
||||
>>>>>>> edx/master
|
||||
|
||||
context = {
|
||||
'datatable': datatable,
|
||||
'datatable': self.make_datatable(),
|
||||
'msg': self.msg,
|
||||
'djangopid': os.getpid(),
|
||||
'modeflag': {'courses': 'active-section'},
|
||||
|
||||
@@ -4,6 +4,7 @@ Provide tests for sysadmin dashboard feature in sysadmin.py
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
@@ -458,6 +459,31 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
|
||||
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
||||
self.assertIsNone(course)
|
||||
|
||||
def test_course_info(self):
|
||||
"""
|
||||
Check to make sure we are getting git info for courses
|
||||
"""
|
||||
# Regex of first 3 columns of course information table row for
|
||||
# test course loaded from git. Would not have sha1 if
|
||||
# git_info_for_course failed.
|
||||
table_re = re.compile(r"""
|
||||
<tr>\s+
|
||||
<td>edX\sAuthor\sCourse</td>\s+ # expected test git course name
|
||||
<td>MITx/edx4edx/edx4edx</td>\s+ # expected test git course_id
|
||||
<td>[a-fA-F\d]{40}</td> # git sha1 hash
|
||||
""", re.VERBOSE)
|
||||
|
||||
self._setstaff_login()
|
||||
self._mkdir(getattr(settings, 'GIT_REPO_DIR'))
|
||||
|
||||
# Make sure we don't have any git hashes on the page
|
||||
response = self.client.get(reverse('sysadmin_courses'))
|
||||
self.assertNotRegexpMatches(response.content, table_re)
|
||||
|
||||
# Now add the course and make sure it does match
|
||||
response = self._add_edx4edx()
|
||||
self.assertRegexpMatches(response.content, table_re)
|
||||
|
||||
def test_gitlogs(self):
|
||||
"""
|
||||
Create a log entry and make sure it exists
|
||||
|
||||
@@ -4,6 +4,7 @@ from edxmako.shortcuts import render_to_response
|
||||
from courseware.courses import get_course_with_access
|
||||
from notes.models import Note
|
||||
from notes.utils import notes_enabled_for_course
|
||||
from xmodule.annotator_token import retrieve_token
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -22,7 +23,8 @@ def notes(request, course_id):
|
||||
'course': course,
|
||||
'notes': notes,
|
||||
'student': student,
|
||||
'storage': storage
|
||||
'storage': storage,
|
||||
'token': retrieve_token(student.email, course.annotation_token_secret),
|
||||
}
|
||||
|
||||
return render_to_response('notes.html', context)
|
||||
|
||||
@@ -826,6 +826,7 @@ main_vendor_js = [
|
||||
'js/vendor/swfobject/swfobject.js',
|
||||
'js/vendor/jquery.ba-bbq.min.js',
|
||||
'js/vendor/ova/annotator-full.js',
|
||||
'js/vendor/ova/annotator-full-firebase-auth.js',
|
||||
'js/vendor/ova/video.dev.js',
|
||||
'js/vendor/ova/vjs.youtube.js',
|
||||
'js/vendor/ova/rangeslider.js',
|
||||
|
||||
@@ -64,12 +64,21 @@
|
||||
<section id="catchDIV">
|
||||
<div class="annotationListContainer">${_('You do not have any notes.')}</div>
|
||||
</section>
|
||||
<<<<<<< HEAD
|
||||
<script>
|
||||
|
||||
//Grab uri of the course
|
||||
var parts = window.location.href.split("/"),
|
||||
uri = '';
|
||||
for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url
|
||||
=======
|
||||
<script>
|
||||
|
||||
//Grab uri of the course
|
||||
var parts = window.location.href.split("/"),
|
||||
uri = '';
|
||||
for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url
|
||||
>>>>>>> edx/master
|
||||
var pagination = 100,
|
||||
is_staff = false,
|
||||
options = {
|
||||
@@ -164,6 +173,28 @@
|
||||
extended_valid_elements : "iframe[src|frameborder|style|scrolling|class|width|height|name|align|id]",
|
||||
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media rubric | code ",
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
return true;
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
token: "${token}"
|
||||
},
|
||||
store: {
|
||||
// The endpoint of the store on your server.
|
||||
prefix: "${storage}",
|
||||
|
||||
annotationData: {},
|
||||
|
||||
urls: {
|
||||
// These are the default URLs.
|
||||
create: '/create',
|
||||
read: '/read/:id',
|
||||
update: '/update/:id',
|
||||
destroy: '/delete/:id',
|
||||
search: '/search'
|
||||
>>>>>>> edx/master
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="annotatable-wrapper">
|
||||
<div class="annotatable-header">
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<div class="annotatable-title">${display_name}</div>
|
||||
% endif
|
||||
</div>
|
||||
% if instructions_html is not UNDEFINED and instructions_html is not None:
|
||||
<div class="annotatable-section shaded">
|
||||
<div class="annotatable-section-title">
|
||||
${_('Instructions')}
|
||||
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">${_('Collapse Instructions')}</a>
|
||||
</div>
|
||||
<div class="annotatable-section-body annotatable-instructions">
|
||||
${instructions_html}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div class="annotatable-section">
|
||||
<div class="annotatable-content">
|
||||
<div id="textHolder">${content_html}</div>
|
||||
<div id="sourceCitation">${_('Source:')} ${source}</div>
|
||||
<div id="catchDIV">
|
||||
<div class="annotationListContainer">${_('You do not have any notes.')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotatable-header">
|
||||
% if display_name is not UNDEFINED and display_name is not None:
|
||||
<div class="annotatable-title">${display_name}</div>
|
||||
% endif
|
||||
</div>
|
||||
% if instructions_html is not UNDEFINED and instructions_html is not None:
|
||||
<div class="annotatable-section shaded">
|
||||
<div class="annotatable-section-title">
|
||||
${_('Instructions')}
|
||||
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">${_('Collapse Instructions')}</a>
|
||||
</div>
|
||||
<div class="annotatable-section-body annotatable-instructions">
|
||||
${instructions_html}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div class="annotatable-section">
|
||||
<div class="annotatable-content">
|
||||
<div id="textHolder">${content_html}</div>
|
||||
<div id="sourceCitation">${_('Source:')} ${source}</div>
|
||||
<div id="catchDIV">
|
||||
<div class="annotationListContainer">${_('You do not have any notes.')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function onClickHideInstructions(){
|
||||
//Reset function if there is more than one event handler
|
||||
$(this).off();
|
||||
$(this).on('click',onClickHideInstructions);
|
||||
var hide = $(this).html()=='Collapse Instructions'?true:false,
|
||||
cls, txt,slideMethod;
|
||||
txt = (hide ? 'Expand' : 'Collapse') + ' Instructions';
|
||||
cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']);
|
||||
slideMethod = (hide ? 'slideUp' : 'slideDown');
|
||||
$(this).text(txt).removeClass(cls[0]).addClass(cls[1]);
|
||||
$(this).parents('.annotatable-section:first').find('.annotatable-instructions')[slideMethod]();
|
||||
}
|
||||
$('.annotatable-toggle-instructions').on('click', onClickHideInstructions);
|
||||
|
||||
//Grab uri of the course
|
||||
function onClickHideInstructions(){
|
||||
//Reset function if there is more than one event handler
|
||||
$(this).off();
|
||||
$(this).on('click',onClickHideInstructions);
|
||||
var hide = $(this).html()=='Collapse Instructions'?true:false,
|
||||
cls, txt,slideMethod;
|
||||
txt = (hide ? 'Expand' : 'Collapse') + ' Instructions';
|
||||
cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']);
|
||||
slideMethod = (hide ? 'slideUp' : 'slideDown');
|
||||
$(this).text(txt).removeClass(cls[0]).addClass(cls[1]);
|
||||
$(this).parents('.annotatable-section:first').find('.annotatable-instructions')[slideMethod]();
|
||||
}
|
||||
$('.annotatable-toggle-instructions').on('click', onClickHideInstructions);
|
||||
|
||||
//Grab uri of the course
|
||||
var parts = window.location.href.split("/"),
|
||||
uri = '',
|
||||
courseid;
|
||||
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
|
||||
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
|
||||
//Change uri in cms
|
||||
var lms_location = $('.sidebar .preview-button').attr('href');
|
||||
if (typeof lms_location!='undefined'){
|
||||
courseid = parts[4].split(".").join("/");
|
||||
uri = window.location.protocol;
|
||||
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
|
||||
}
|
||||
var pagination = 100,
|
||||
is_staff = !('${user.is_staff}'=='False'),
|
||||
uri = '';
|
||||
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
|
||||
//Change uri in cms
|
||||
var lms_location = $('.sidebar .preview-button').attr('href');
|
||||
if (typeof lms_location!='undefined'){
|
||||
uri = window.location.protocol;
|
||||
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
|
||||
}
|
||||
var unit_id = $('#sequence-list').find('.active').attr("data-element");
|
||||
uri += unit_id;
|
||||
var pagination = 100,
|
||||
is_staff = !('${user.is_staff}'=='False'),
|
||||
options = {
|
||||
optionsAnnotator: {
|
||||
permissions:{
|
||||
@@ -89,7 +88,7 @@
|
||||
if (annotation.permissions) {
|
||||
tokens = annotation.permissions[action] || [];
|
||||
if (is_staff){
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
if (tokens.length === 0) {
|
||||
return true;
|
||||
@@ -115,7 +114,7 @@
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
|
||||
token: "${token}"
|
||||
},
|
||||
store: {
|
||||
// The endpoint of the store on your server.
|
||||
@@ -140,11 +139,14 @@
|
||||
offset:0,
|
||||
uri:uri,
|
||||
media:'text',
|
||||
userid:'${user.email}',
|
||||
userid:'${user.email}',
|
||||
}
|
||||
},
|
||||
highlightTags:{
|
||||
tag: "${tag}",
|
||||
},
|
||||
diacriticMarks:{
|
||||
diacritics: "${diacritic_marks}"
|
||||
}
|
||||
},
|
||||
optionsVideoJS: {techOrder: ["html5","flash","youtube"]},
|
||||
@@ -161,12 +163,11 @@
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/";
|
||||
tinyMCE.baseURL = "${settings.STATIC_URL}" + "js/vendor/ova";
|
||||
|
||||
//remove old instances
|
||||
var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/";
|
||||
tinyMCE.baseURL = "${settings.STATIC_URL}" + "js/vendor/ova";
|
||||
|
||||
//remove old instances
|
||||
if (Annotator._instances.length !== 0) {
|
||||
$('#textHolder').annotator("destroy");
|
||||
}
|
||||
@@ -174,7 +175,6 @@
|
||||
//Load the plugin Video/Text Annotation
|
||||
var ova = new OpenVideoAnnotation.Annotator($('#textHolder'),options);
|
||||
|
||||
|
||||
//Catch
|
||||
var annotator = ova.annotator,
|
||||
catchOptions = {
|
||||
@@ -183,7 +183,7 @@
|
||||
imageUrlRoot:imgURLRoot,
|
||||
showMediaSelector: false,
|
||||
showPublicPrivate: true,
|
||||
userId:'${user.email}',
|
||||
userId:'${user.email}',
|
||||
pagination:pagination,//Number of Annotations per load in the pagination,
|
||||
flags:is_staff
|
||||
},
|
||||
|
||||
@@ -49,18 +49,16 @@
|
||||
|
||||
//Grab uri of the course
|
||||
var parts = window.location.href.split("/"),
|
||||
uri = '',
|
||||
courseid;
|
||||
uri = '';
|
||||
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
|
||||
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
|
||||
//Change uri in cms
|
||||
var lms_location = $('.sidebar .preview-button').attr('href');
|
||||
if (typeof lms_location!='undefined'){
|
||||
courseid = parts[4].split(".").join("/");
|
||||
uri = window.location.protocol;
|
||||
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
|
||||
}
|
||||
|
||||
var unit_id = $('#sequence-list').find('.active').attr("data-element");
|
||||
uri += unit_id;
|
||||
var pagination = 100,
|
||||
is_staff = !('${user.is_staff}'=='False'),
|
||||
options = {
|
||||
@@ -119,7 +117,7 @@
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
|
||||
token: "${token}"
|
||||
},
|
||||
store: {
|
||||
// The endpoint of the store on your server.
|
||||
@@ -175,8 +173,6 @@
|
||||
var ova = new OpenVideoAnnotation.Annotator($('#videoHolder'),options);
|
||||
|
||||
ova.annotator.addPlugin('Tags');
|
||||
|
||||
|
||||
|
||||
//Catch
|
||||
var annotator = ova.annotator,
|
||||
@@ -186,7 +182,7 @@
|
||||
imageUrlRoot:imgURLRoot,
|
||||
showMediaSelector: false,
|
||||
showPublicPrivate: true,
|
||||
userId:'${user.email}',
|
||||
userId:'${user.email}',
|
||||
pagination:pagination,//Number of Annotations per load in the pagination,
|
||||
flags:is_staff
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ urlpatterns = ('', # nopep8
|
||||
url(r'^request_certificate$', 'certificates.views.request_certificate'),
|
||||
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
|
||||
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
|
||||
url(r'^token$', 'student.views.token', name="token"),
|
||||
url(r'^login$', 'student.views.signin_user', name="signin_user"),
|
||||
url(r'^register$', 'student.views.register_user', name="register_user"),
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ django-method-override==0.1.0
|
||||
djangorestframework==2.3.5
|
||||
django==1.4.12
|
||||
feedparser==5.1.3
|
||||
firebase-token-generator==1.3.2
|
||||
fs==0.4.0
|
||||
GitPython==0.3.2.RC1
|
||||
glob2==0.3
|
||||
|
||||
Reference in New Issue
Block a user