Commit 19acdd31 by Calen Pennington

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
parents 94c8d86a d37006c6
......@@ -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)
......
'''
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)
"""
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("""
......
"""
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;
var time = params.time,
type = params.type;
if (
(typeof newTime !== 'number') ||
(newTime > duration) ||
(newTime < 0)
) {
return;
// After the user seeks, the video will start playing from
// the sought point, and stop playing at the end.
this.videoPlayer.goToStartTime = false;
if (time > this.videoPlayer.endTime || this.videoPlayer.endTime === null) {
this.videoPlayer.stopAtEndTime = false;
}
this.el.off('play.seek');
this.videoPlayer.seekTo(time);
this.videoPlayer.log(
'seek_video',
{
old_time: this.videoPlayer.currentTime,
new_time: newTime,
type: params.type
new_time: time,
type: 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) {
this.videoPlayer.stopAtEndTime = false;
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,
......
"""
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])
\ No newline at end of file
......@@ -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
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;
}
};
\ No newline at end of file
......@@ -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
},
};
......
<%! 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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment