Commit d6f68ca5 by Clinton Blackburn

Merge pull request #12307 from edx/rc/2016-05-03

Release Candidate rc/2016-05-03
parents 2d6604a0 0bf5a3d7
......@@ -14,14 +14,17 @@ source =
omit =
cms/envs/*
cms/manage.py
cms/djangoapps/contentstore/views/dev.py
cms/djangoapps/*/migrations/*
cms/djangoapps/*/features/*
lms/debug/*
lms/envs/*
lms/djangoapps/*/migrations/*
lms/djangoapps/*/features/*
common/djangoapps/terrain/*
common/djangoapps/*/migrations/*
openedx/core/djangoapps/*/migrations/*
openedx/core/djangoapps/debug/*
[report]
ignore_errors = True
......
......@@ -29,10 +29,6 @@ class TestArgParsing(unittest.TestCase):
with self.assertRaises(CommandError):
self.command.handle("foo", "user@foo.org", "org", "course", "run")
def test_xml_store(self):
with self.assertRaises(CommandError):
self.command.handle(ModuleStoreEnum.Type.xml, "user@foo.org", "org", "course", "run")
def test_nonexistent_user_id(self):
errstring = "No user 99 found"
with self.assertRaisesRegexp(CommandError, errstring):
......
......@@ -83,7 +83,8 @@ def register_special_exams(course_key):
due_date=timed_exam.due,
is_proctored=timed_exam.is_proctored_exam,
is_practice_exam=timed_exam.is_practice_exam,
is_active=True
is_active=True,
hide_after_due=timed_exam.hide_after_due,
)
msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
log.info(msg)
......@@ -97,7 +98,8 @@ def register_special_exams(course_key):
due_date=timed_exam.due,
is_proctored=timed_exam.is_proctored_exam,
is_practice_exam=timed_exam.is_practice_exam,
is_active=True
is_active=True,
hide_after_due=timed_exam.hide_after_due,
)
msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id)
log.info(msg)
......
......@@ -124,15 +124,6 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
{
'NAME': 'xml',
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'xblock_mixins': modulestore_options['xblock_mixins'],
}
},
],
'xblock_mixins': modulestore_options['xblock_mixins'],
}
......
......@@ -53,6 +53,10 @@ class TestProctoredExams(ModuleStoreTestCase):
exam_review_policy = get_review_policy_by_exam_id(exam['id'])
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
if not exam['is_proctored'] and not exam['is_practice_exam']:
# the hide after due value only applies to timed exams
self.assertEqual(exam['hide_after_due'], sequence.hide_after_due)
self.assertEqual(exam['course_id'], unicode(self.course.id))
self.assertEqual(exam['content_id'], unicode(sequence.location))
self.assertEqual(exam['exam_name'], sequence.display_name)
......@@ -62,13 +66,14 @@ class TestProctoredExams(ModuleStoreTestCase):
self.assertEqual(exam['is_active'], expected_active)
@ddt.data(
(True, 10, True, False, True, False),
(True, 10, False, False, True, False),
(True, 10, True, True, True, True),
(True, 10, True, False, True, False, False),
(True, 10, False, False, True, False, False),
(True, 10, False, False, True, False, True),
(True, 10, True, True, True, True, False),
)
@ddt.unpack
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes,
is_proctored_exam, is_practice_exam, expected_active, republish):
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes, is_proctored_exam,
is_practice_exam, expected_active, republish, hide_after_due):
"""
Happy path testing to see that when a course is published which contains
a proctored exam, it will also put an entry into the exam tables
......@@ -85,7 +90,8 @@ class TestProctoredExams(ModuleStoreTestCase):
is_proctored_exam=is_proctored_exam,
is_practice_exam=is_practice_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
exam_review_rules="allow_use_of_paper"
exam_review_rules="allow_use_of_paper",
hide_after_due=hide_after_due,
)
listen_for_course_publish(self, self.course.id)
......@@ -117,7 +123,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_exam=True
is_proctored_exam=True,
hide_after_due=False,
)
listen_for_course_publish(self, self.course.id)
......@@ -147,7 +154,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_exam=True
is_proctored_exam=True,
hide_after_due=False,
)
listen_for_course_publish(self, self.course.id)
......@@ -182,7 +190,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_exam=True
is_proctored_exam=True,
hide_after_due=False,
)
listen_for_course_publish(self, self.course.id)
......@@ -218,7 +227,8 @@ class TestProctoredExams(ModuleStoreTestCase):
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_exam=True,
exam_review_rules="allow_use_of_paper"
exam_review_rules="allow_use_of_paper",
hide_after_due=False,
)
listen_for_course_publish(self, self.course.id)
......
......@@ -91,7 +91,7 @@ from util.organizations_helpers import (
organizations_enabled,
)
from util.string_utils import _has_non_ascii_characters
from util.course_key_utils import course_key_from_string_or_404
from util.course_key_utils import from_string_or_404
from xmodule.contentstore.content import StaticContent
from xmodule.course_module import CourseFields
from xmodule.course_module import DEFAULT_START_DATE
......@@ -875,7 +875,7 @@ def course_info_handler(request, course_key_string):
GET
html: return html for editing the course info handouts and updates.
"""
course_key = course_key_from_string_or_404(course_key_string)
course_key = from_string_or_404(course_key_string)
with modulestore().bulk_operations(course_key):
course_module = get_course_and_check_access(course_key, request.user)
......
......@@ -12,17 +12,3 @@ from django.http import HttpResponseNotFound
def dev_mode(request):
"Sample static view"
return render_to_response("dev/dev_mode.html")
def dev_show_template(request, template):
"""
Shows the specified template as an HTML page.
e.g. /template/ux/reference/container.html shows the template under ux/reference/container.html
Note: dynamic parameters can also be passed to the page.
e.g. /template/ux/reference/container.html?name=Foo
"""
try:
return render_to_response(template, request.GET.dict())
except TopLevelLookupException:
return HttpResponseNotFound("Couldn't find template {tpl}".format(tpl=template))
......@@ -935,7 +935,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"is_practice_exam": xblock.is_practice_exam,
"is_time_limited": xblock.is_time_limited,
"exam_review_rules": xblock.exam_review_rules,
"default_time_limit_minutes": xblock.default_time_limit_minutes
"default_time_limit_minutes": xblock.default_time_limit_minutes,
"hide_after_due": xblock.hide_after_due,
})
# Update with gating info
......
......@@ -14,8 +14,7 @@ from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.tabs import CourseTabList, CourseTab, InvalidTabsException, StaticTab
from opaque_keys.edx.keys import UsageKey
from util.course_key_utils import course_key_from_string_or_404
from opaque_keys.edx.keys import CourseKey, UsageKey
from ..utils import get_lms_link_for_item
......@@ -40,7 +39,7 @@ def tabs_handler(request, course_key_string):
Creating a tab, deleting a tab, or changing its contents is not supported through this method.
Instead use the general xblock URL (see item.xblock_handler).
"""
course_key = course_key_from_string_or_404(course_key_string)
course_key = CourseKey.from_string(course_key_string)
if not has_course_author_access(request.user, course_key):
raise PermissionDenied()
......
......@@ -7,12 +7,9 @@ from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url
from xmodule.x_module import STUDENT_VIEW
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from django.test.client import RequestFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.tabs import CourseTabList
from xmodule.modulestore.django import modulestore
from django.http import Http404
from contentstore.views.tabs import tabs_handler
class TabsPageTests(CourseTestCase):
......@@ -194,14 +191,6 @@ class TabsPageTests(CourseTestCase):
self.assertIn('<span class="sr">Delete this component</span>', html)
self.assertIn('<span data-tooltip="Drag to reorder" class="drag-handle action"></span>', html)
def test_invalid_course_id(self):
""" Asserts that Http404 is raised when the course id is not valid. """
request_factory = RequestFactory()
request = request_factory.get('/dummy-url')
request.user = self.user
with self.assertRaises(Http404):
tabs_handler(request, "/some.invalid.key/course-v1:TTT+CS01+2015_T0")
class PrimitiveTabEdit(ModuleStoreTestCase):
"""Tests for the primitive tab edit data manipulations"""
......
......@@ -9,9 +9,6 @@ from django.contrib.auth.models import User
from student.models import CourseEnrollment
from student.roles import CourseStaffRole, CourseInstructorRole
from student import auth
from django.http import Http404
from contentstore.views.user import course_team_handler
from django.test.client import RequestFactory
class UsersTestCase(CourseTestCase):
......@@ -318,11 +315,3 @@ class UsersTestCase(CourseTestCase):
CourseEnrollment.is_enrolled(self.ext_user, self.course.id),
'User ext_user should have been enrolled in the course'
)
def test_invalid_course_id(self):
""" Asserts that Http404 is raised when the course id is not valid. """
request_factory = RequestFactory()
request = request_factory.get('/dummy-url')
request.user = self.user
with self.assertRaises(Http404):
course_team_handler(request, "/some.invalid.key/course-v1:TTT+CS01+2015_T0")
......@@ -8,7 +8,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from util.course_key_utils import course_key_from_string_or_404
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from util.json_request import JsonResponse, expect_json
from student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole
......@@ -49,7 +49,7 @@ def course_team_handler(request, course_key_string=None, email=None):
DELETE:
json: remove a particular course team member from the course team (email is required).
"""
course_key = course_key_from_string_or_404(course_key_string) if course_key_string else None
course_key = CourseKey.from_string(course_key_string) if course_key_string else None
# No permissions check here - each helper method does its own check.
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
......
......@@ -50,6 +50,7 @@ class CourseMetadata(object):
'is_time_limited',
'is_practice_exam',
'exam_review_rules',
'hide_after_due',
'self_paced',
'chrome',
'default_tab',
......
......@@ -485,7 +485,6 @@ STATIC_ROOT = ENV_ROOT / "staticfiles" / EDX_PLATFORM_REVISION
STATICFILES_DIRS = [
COMMON_ROOT / "static",
PROJECT_ROOT / "static",
LMS_ROOT / "static",
# This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images"),
......@@ -685,8 +684,11 @@ REQUIRE_DEBUG = False
REQUIRE_EXCLUDE = ("build.txt",)
# The execution environment in which to run r.js: auto, node or rhino.
# auto will autodetect the environment and make use of node if available and rhino if not.
# It can also be a path to a custom class that subclasses require.environments.Environment and defines some "args" function that returns a list with the command arguments to execute.
# auto will autodetect the environment and make use of node if available and
# rhino if not.
# It can also be a path to a custom class that subclasses
# require.environments.Environment and defines some "args" function that
# returns a list with the command arguments to execute.
REQUIRE_ENVIRONMENT = "node"
......@@ -957,7 +959,7 @@ EVENT_TRACKING_BACKENDS = {
},
'processors': [
{'ENGINE': 'track.shim.LegacyFieldMappingProcessor'},
{'ENGINE': 'track.shim.VideoEventProcessor'}
{'ENGINE': 'track.shim.PrefixedEventProcessor'}
]
}
},
......
......@@ -52,7 +52,7 @@
"text": 'js/vendor/requirejs/text',
"underscore": "common/js/vendor/underscore",
"underscore.string": "common/js/vendor/underscore.string",
"backbone": "js/vendor/backbone-min",
"backbone": "common/js/vendor/backbone",
"backbone-relational" : "js/vendor/backbone-relational.min",
"backbone.associations": "js/vendor/backbone-associations-min",
"backbone.paginator": "js/vendor/backbone.paginator.min",
......
......@@ -29,7 +29,7 @@ requirejs.config({
"text": "xmodule_js/common_static/js/vendor/requirejs/text",
"underscore": "xmodule_js/common_static/common/js/vendor/underscore",
"underscore.string": "xmodule_js/common_static/common/js/vendor/underscore.string",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone": "xmodule_js/common_static/common/js/vendor/backbone",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
"backbone.paginator": "xmodule_js/common_static/js/vendor/backbone.paginator.min",
"backbone-relational": "xmodule_js/common_static/js/vendor/backbone-relational.min",
......@@ -283,6 +283,8 @@ while i < testFiles.length
testFiles[i] = '/base/' + testFiles[i] + '.js'
i++
require testFiles, ->
# Jasmine has a global stack for creating a tree of specs. We need to load
# spec files one by one, otherwise some end up getting nested under others.
requireSerial testFiles, ->
# start test run, once Require.js is done
window.__karma__.start()
......@@ -26,7 +26,7 @@ requirejs.config({
"text": "xmodule_js/common_static/js/vendor/requirejs/text",
"underscore": "common/js/vendor/underscore",
"underscore.string": "common/js/vendor/underscore.string",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone": "xmodule_js/common_static/common/js/vendor/backbone",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
"backbone.paginator": "xmodule_js/common_static/js/vendor/backbone.paginator.min",
"tinymce": "xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min",
......@@ -202,6 +202,9 @@ i = 0
while i < testFiles.length
testFiles[i] = '/base/' + testFiles[i] + '.js'
i++
require testFiles, ->
# Jasmine has a global stack for creating a tree of specs. We need to load
# spec files one by one, otherwise some end up getting nested under others.
requireSerial testFiles, ->
# start test run, once Require.js is done
window.__karma__.start()
define ["jquery", "jquery.ui", "backbone", "common/js/components/views/feedback_prompt",
define ["underscore", "jquery", "jquery.ui", "backbone", "common/js/components/views/feedback_prompt",
"common/js/components/views/feedback_notification", "coffee/src/views/module_edit", "js/models/module_info", "js/utils/module"],
($, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) ->
(_, $, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) ->
class TabsEdit extends Backbone.View
initialize: =>
initialize: (options) =>
@$('.component').each((idx, element) =>
model = new ModuleModel({
id: $(element).data('locator')
......@@ -15,7 +15,7 @@ define ["jquery", "jquery.ui", "backbone", "common/js/components/views/feedback_
model: model
)
)
@options = _.extend({}, options)
@options.mast.find('.new-tab').on('click', @addNewTab)
$('.add-pages .new-tab').on('click', @addNewTab)
$('.toggle-checkbox').on('click', @toggleVisibilityOfTab)
......
......@@ -62,8 +62,8 @@ define(["jquery", "URI", "common/js/spec_helpers/ajax_helpers", "common/js/compo
mockCssUrl = "mock.css",
headHtml;
postXBlockRequest(requests, [
["hash1", { mimetype: "text/css", kind: "text", data: mockCssText }],
["hash2", { mimetype: "text/css", kind: "url", data: mockCssUrl }]
["xblock_spec_hash1", { mimetype: "text/css", kind: "text", data: mockCssText }],
["xblock_spec_hash2", { mimetype: "text/css", kind: "url", data: mockCssUrl }]
]);
headHtml = $('head').html();
expect(headHtml).toContain(mockCssText);
......@@ -73,7 +73,9 @@ define(["jquery", "URI", "common/js/spec_helpers/ajax_helpers", "common/js/compo
it('can render an xblock with required JavaScript', function() {
var requests = AjaxHelpers.requests(this);
postXBlockRequest(requests, [
["hash3", { mimetype: "application/javascript", kind: "text", data: "window.test = 100;" }]
["xblock_spec_hash3", {
mimetype: "application/javascript", kind: "text", data: "window.test = 100;"
}]
]);
expect(window.test).toBe(100);
});
......@@ -82,7 +84,7 @@ define(["jquery", "URI", "common/js/spec_helpers/ajax_helpers", "common/js/compo
var requests = AjaxHelpers.requests(this),
mockHeadTag = "<title>Test Title</title>";
postXBlockRequest(requests, [
["hash4", { mimetype: "text/html", placement: "head", data: mockHeadTag }]
["xblock_spec_hash4", { mimetype: "text/html", placement: "head", data: mockHeadTag }]
]);
expect($('head').html()).toContain(mockHeadTag);
});
......@@ -93,7 +95,9 @@ define(["jquery", "URI", "common/js/spec_helpers/ajax_helpers", "common/js/compo
promise;
spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().reject().promise());
promise = postXBlockRequest(requests, [
["hash5", { mimetype: "application/javascript", kind: "url", data: missingJavaScriptUrl }]
["xblock_spec_hash5", {
mimetype: "application/javascript", kind: "url", data: missingJavaScriptUrl
}]
]);
expect(promise.isRejected()).toBe(true);
});
......
......@@ -26,6 +26,14 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b
//override the constructor function
constructor: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
// Merge passed options and view's options property and
// attach to the view's options property
if (this.options) {
options = _.extend({}, _.result(this, 'options'), options);
}
this.options = options;
var _this = this;
this.render = _.wrap(this.render, function (render, options) {
_this.beforeRender();
......
......@@ -100,8 +100,8 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview",
};
var roleEvents = {};
var self = this;
for (var i = 0; i < self.roles.length; i++) {
var role_name = self.roles[i].key;
for (var i = 0; i < self.options.roles.length; i++) {
var role_name = self.options.roles[i].key;
var role_selector = 'click .user-actions .make-' + role_name;
(function (role) {
......
......@@ -26,7 +26,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
'click .action-cancel': 'cancel'
},
options: $.extend({}, BaseView.prototype.options, {
options: _.extend({}, BaseView.prototype.options, {
type: 'prompt',
closeIcon: false,
icon: false,
......
......@@ -17,9 +17,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
VerificationAccessEditor, TimedExaminationPreferenceEditor, AccessEditor;
CourseOutlineXBlockModal = BaseModal.extend({
events : {
events : _.extend({}, BaseModal.prototype.events, {
'click .action-save': 'save'
},
}),
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'course-outline',
......@@ -32,7 +32,6 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
initialize: function() {
BaseModal.prototype.initialize.call(this);
this.events = $.extend({}, BaseModal.prototype.events, this.events);
this.template = this.loadTemplate('course-outline-modal');
this.options.title = this.getTitle();
},
......@@ -154,10 +153,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
},
events: {
events: _.extend({}, CourseOutlineXBlockModal.prototype.events, {
'click .action-save': 'save',
'click .settings-tab-button': 'handleShowTab',
},
'click .settings-tab-button': 'handleShowTab'
}),
/**
* Return request data.
......@@ -185,9 +184,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
PublishXBlockModal = CourseOutlineXBlockModal.extend({
events : {
events : _.extend({}, CourseOutlineXBlockModal.prototype.events, {
'click .action-publish': 'save'
},
}),
initialize: function() {
CourseOutlineXBlockModal.prototype.initialize.call(this);
......@@ -333,41 +332,47 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
templateName: 'timed-examination-preference-editor',
className: 'edit-settings-timed-examination',
events : {
'change #id_not_timed': 'notTimedExam',
'change #id_timed_exam': 'setTimedExam',
'change #id_practice_exam': 'setPracticeExam',
'change #id_proctored_exam': 'setProctoredExam',
'focusout #id_time_limit': 'timeLimitFocusout'
'change input.no_special_exam': 'notTimedExam',
'change input.timed_exam': 'setTimedExam',
'change input.practice_exam': 'setPracticeExam',
'change input.proctored_exam': 'setProctoredExam',
'focusout .field-time-limit input': 'timeLimitFocusout'
},
notTimedExam: function (event) {
event.preventDefault();
this.$('#id_time_limit_div').hide();
this.$('.exam-review-rules-list-fields').hide();
this.$('#id_time_limit').val('00:00');
},
selectSpecialExam: function (showRulesField) {
this.$('#id_time_limit_div').show();
if (!this.isValidTimeLimit(this.$('#id_time_limit').val())) {
this.$('#id_time_limit').val('00:30');
this.$('.exam-options').hide();
this.$('.field-time-limit input').val('00:00');
},
selectSpecialExam: function (showRulesField, showHideAfterDueField) {
this.$('.exam-options').show();
this.$('.field-time-limit').show();
if (!this.isValidTimeLimit(this.$('.field-time-limit input').val())) {
this.$('.field-time-limit input').val('00:30');
}
if (showRulesField) {
this.$('.exam-review-rules-list-fields').show();
this.$('.field-exam-review-rules').show();
}
else {
this.$('.field-exam-review-rules').hide();
}
if (showHideAfterDueField) {
this.$('.field-hide-after-due').show();
}
else {
this.$('.exam-review-rules-list-fields').hide();
this.$('.field-hide-after-due').hide();
}
},
setTimedExam: function (event) {
event.preventDefault();
this.selectSpecialExam(false);
this.selectSpecialExam(false, true);
},
setPracticeExam: function (event) {
event.preventDefault();
this.selectSpecialExam(false);
this.selectSpecialExam(false, false);
},
setProctoredExam: function (event) {
event.preventDefault();
this.selectSpecialExam(true);
this.selectSpecialExam(true, false);
},
timeLimitFocusout: function(event) {
event.preventDefault();
......@@ -390,43 +395,51 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.setExamTime(this.model.get('default_time_limit_minutes'));
this.setReviewRules(this.model.get('exam_review_rules'));
this.setHideAfterDue(this.model.get('hide_after_due'));
},
setExamType: function(is_time_limited, is_proctored_exam, is_practice_exam) {
this.$('.field-time-limit').hide();
this.$('.field-exam-review-rules').hide();
this.$('.field-hide-after-due').hide();
if (!is_time_limited) {
this.$("#id_not_timed").prop('checked', true);
this.$('input.no_special_exam').prop('checked', true);
return;
}
this.$('#id_time_limit_div').show();
this.$('.exam-review-rules-list-fields').hide();
this.$('.field-time-limit').show();
if (this.options.enable_proctored_exams && is_proctored_exam) {
if (is_practice_exam) {
this.$('#id_practice_exam').prop('checked', true);
this.$('input.practice_exam').prop('checked', true);
} else {
this.$('#id_proctored_exam').prop('checked', true);
this.$('.exam-review-rules-list-fields').show();
this.$('input.proctored_exam').prop('checked', true);
this.$('.field-exam-review-rules').show();
}
} else {
// Since we have an early exit at the top of the method
// if the subsection is not time limited, then
// here we rightfully assume that it just a timed exam
this.$("#id_timed_exam").prop('checked', true);
this.$('input.timed_exam').prop('checked', true);
this.$('.field-hide-after-due').show();
}
},
setExamTime: function(value) {
var time = this.convertTimeLimitMinutesToString(value);
this.$('#id_time_limit').val(time);
this.$('.field-time-limit input').val(time);
},
setReviewRules: function (value) {
this.$('#id_exam_review_rules').val(value);
this.$('.field-exam-review-rules textarea').val(value);
},
setHideAfterDue: function(value) {
this.$('.field-hide-after-due input').prop('checked', value);
},
isValidTimeLimit: function(time_limit) {
var pattern = new RegExp('^\\d{1,2}:[0-5][0-9]$');
return pattern.test(time_limit) && time_limit !== "00:00";
},
getExamTimeLimit: function () {
return this.$('#id_time_limit').val();
return this.$('.field-time-limit input').val();
},
convertTimeLimitMinutesToString: function (timeLimitMinutes) {
var hoursStr = "" + Math.floor(timeLimitMinutes / 60);
......@@ -445,21 +458,22 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var is_practice_exam;
var is_proctored_exam;
var time_limit = this.getExamTimeLimit();
var exam_review_rules = this.$('#id_exam_review_rules').val();
var exam_review_rules = this.$('.field-exam-review-rules textarea').val();
var hide_after_due = this.$('.field-hide-after-due input').is(':checked');
if (this.$("#id_not_timed").is(':checked')){
if (this.$('input.no_special_exam').is(':checked')){
is_time_limited = false;
is_practice_exam = false;
is_proctored_exam = false;
} else if (this.$("#id_timed_exam").is(':checked')){
} else if (this.$('input.timed_exam').is(':checked')){
is_time_limited = true;
is_practice_exam = false;
is_proctored_exam = false;
} else if (this.$("#id_proctored_exam").is(':checked')){
} else if (this.$('input.proctored_exam').is(':checked')){
is_time_limited = true;
is_practice_exam = false;
is_proctored_exam = true;
} else if (this.$("#id_practice_exam").is(':checked')){
} else if (this.$('input.practice_exam').is(':checked')){
is_time_limited = true;
is_practice_exam = true;
is_proctored_exam = true;
......@@ -470,6 +484,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'is_practice_exam': is_practice_exam,
'is_time_limited': is_time_limited,
'exam_review_rules': exam_review_rules,
'hide_after_due': hide_after_due,
// We have to use the legacy field name
// as the Ajax handler directly populates
// the xBlocks fields. We will have to
......
......@@ -9,10 +9,10 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "common
"strict mode";
var EditXBlockModal = BaseModal.extend({
events : {
events: _.extend({}, BaseModal.prototype.events, {
"click .action-save": "save",
"click .action-modes a": "changeMode"
},
}),
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'edit-xblock',
......@@ -25,7 +25,6 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "common
initialize: function() {
BaseModal.prototype.initialize.call(this);
this.events = _.extend({}, BaseModal.prototype.events, this.events);
this.template = this.loadTemplate('edit-xblock-modal');
this.editorModeButtonTemplate = this.loadTemplate('editor-mode-button');
},
......
......@@ -38,7 +38,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'],
return this.template({
response: this.response,
num_errors: this.response.length,
num_errors: this.response.length
});
},
......@@ -57,7 +57,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'],
// hide the modal
BaseModal.prototype.hide.call(this);
},
}
});
return ValidationErrorModal;
......
......@@ -6,12 +6,14 @@ define(["jquery", "underscore", "gettext", "js/views/pages/container", "js/views
'use strict';
var PagedXBlockContainerPage = XBlockContainerPage.extend({
events: {"click .toggle-preview-button": "toggleChildrenPreviews"},
events: _.extend({}, XBlockContainerPage.prototype.events, {
'click .toggle-preview-button': 'toggleChildrenPreviews'
}),
defaultViewClass: PagedContainerView,
components_on_init: false,
initialize: function (options){
this.events = _.extend({}, XBlockContainerPage.prototype.events, this.events);
initialize: function (options) {
this.page_size = options.page_size || 10;
this.showChildrenPreviews = options.showChildrenPreviews || true;
XBlockContainerPage.prototype.initialize.call(this, options);
......
define(["js/views/validation", "jquery", "underscore", "gettext", "codemirror", "js/views/modals/validation_error_modal"],
function(ValidatingView, $, _, gettext, CodeMirror, ValidationErrorModal) {
define(["js/views/validation",
"jquery",
"underscore",
"gettext",
"codemirror",
"js/views/modals/validation_error_modal",
'edx-ui-toolkit/js/utils/html-utils'],
function(ValidatingView, $, _, gettext, CodeMirror, ValidationErrorModal, HtmlUtils) {
var AdvancedView = ValidatingView.extend({
error_saving : "error_saving",
......@@ -13,7 +19,9 @@ var AdvancedView = ValidatingView.extend({
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
},
initialize : function() {
this.template = _.template($("#advanced_entry-tpl").text());
this.template = HtmlUtils.template(
$("#advanced_entry-tpl").text()
);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.render();
},
......@@ -33,7 +41,7 @@ var AdvancedView = ValidatingView.extend({
_.each(_.sortBy(_.keys(this.model.attributes), function(key) { return self.model.get(key).display_name; }),
function(key) {
if (self.render_deprecated || !self.model.get(key).deprecated) {
listEle$.append(self.renderTemplate(key, self.model.get(key)));
HtmlUtils.append(listEle$, self.renderTemplate(key, self.model.get(key)));
}
});
......
define(["js/views/validation", "underscore", "jquery"], function(ValidatingView, _, $) {
define(["js/views/validation",
'gettext',
'edx-ui-toolkit/js/utils/string-utils',
"edx-ui-toolkit/js/utils/html-utils",
"underscore",
"jquery"],
function(ValidatingView, gettext, StringUtils, HtmlUtils, _, $) {
var GraderView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGrader
......@@ -49,9 +55,14 @@ var GraderView = ValidatingView.extend({
if (this.setField(event) != this.oldName && !_.isEmpty(this.oldName)) {
// overload the error display logic
this._cacheValidationErrors.push(event.currentTarget);
$(event.currentTarget).parent().append(
this.errorTemplate({message : 'For grading to work, you must change all "' + this.oldName +
'" subsections to "' + this.model.get('type') + '".'}));
var message = StringUtils.interpolate(
gettext('For grading to work, you must change all {oldName} subsections to {newName}.'),
{
oldName: this.oldName,
newName: this.model.get('type')
}
);
HtmlUtils.append($(event.currentTarget).parent(), this.errorTemplate({message : message}));
}
break;
default:
......
define(["js/views/validation", "underscore", "jquery", "jquery.ui", "js/views/settings/grader"],
function(ValidatingView, _, $, ui, GraderView) {
define(["js/views/validation",
"underscore",
"jquery",
"jquery.ui",
"js/views/settings/grader",
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils',
],
function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) {
var GradingView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy
......@@ -21,13 +28,12 @@ var GradingView = ValidatingView.extend({
initialize : function() {
// load template for grading view
var self = this;
this.template = _.template($("#course_grade_policy-tpl").text());
this.gradeCutoffTemplate = _.template('<li class="grade-specific-bar" style="width:<%= width %>%"><span class="letter-grade" contenteditable="true">' +
'<%= descriptor %>' +
'</span><span class="range"></span>' +
'<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>' +
'</li>');
this.template = HtmlUtils.template(
$("#course_grade_policy-tpl").text()
);
this.gradeCutoffTemplate = HtmlUtils.template(
$("#course_grade_cutoff-tpl").text()
);
this.setupCutoffs();
this.listenTo(this.model, 'invalid', this.handleValidationError);
......@@ -68,7 +74,7 @@ var GradingView = ValidatingView.extend({
},
this);
gradeCollection.each(function(gradeModel) {
$(gradelist).append(self.template({model : gradeModel }));
HtmlUtils.append(gradelist, self.template({model : gradeModel }));
var newEle = gradelist.children().last();
var newView = new GraderView({el: newEle,
model : gradeModel, collection : gradeCollection });
......@@ -147,7 +153,7 @@ var GradingView = ValidatingView.extend({
gradeBarWidth : null, // cache of value since it won't change (more certain)
renderCutoffBar: function() {
var gradeBar =this.$el.find('.grade-bar');
var gradeBar = this.$el.find('.grade-bar');
this.gradeBarWidth = gradeBar.width();
var gradelist = gradeBar.children('.grades');
// HACK fixing a duplicate call issue by undoing previous call effect. Need to figure out why called 2x
......@@ -156,15 +162,15 @@ var GradingView = ValidatingView.extend({
// Can probably be simplified to one variable now.
var removable = false;
var draggable = false; // first and last are not removable, first is not draggable
_.each(this.descendingCutoffs,
function(cutoff, index) {
var newBar = this.gradeCutoffTemplate({
descriptor : cutoff['designation'] ,
_.each(this.descendingCutoffs, function(cutoff) {
HtmlUtils.append(gradelist, this.gradeCutoffTemplate({
descriptor : cutoff.designation,
width : nextWidth,
removable : removable });
gradelist.append(newBar);
contenteditable: true,
removable : removable})
);
if (draggable) {
newBar = gradelist.children().last(); // get the dom object not the unparsed string
var newBar = gradelist.children().last(); // get the dom object not the unparsed string
newBar.resizable({
handles: "e",
containment : "parent",
......@@ -174,19 +180,18 @@ var GradingView = ValidatingView.extend({
});
}
// prepare for next
nextWidth = cutoff['cutoff'];
nextWidth = cutoff.cutoff;
removable = true; // first is not removable, all others are
draggable = true;
},
this);
// add fail which is not in data
var failBar = $(this.gradeCutoffTemplate({
// Add fail which is not in data
HtmlUtils.append(gradelist, this.gradeCutoffTemplate({
descriptor : this.failLabel(),
width : nextWidth,
contenteditable: false,
removable : false
}));
failBar.find("span[contenteditable=true]").attr("contenteditable", false);
gradelist.append(failBar);
gradelist.children().last().resizable({
handles: "e",
containment : "parent",
......@@ -298,10 +303,13 @@ var GradingView = ValidatingView.extend({
this.descendingCutoffs.push({designation: this.GRADES[gradeLength], cutoff: failBarWidth});
this.descendingCutoffs[gradeLength - 1]['cutoff'] = Math.round(targetWidth);
var $newGradeBar = this.gradeCutoffTemplate({ descriptor : this.GRADES[gradeLength],
width : targetWidth, removable : true });
var newGradeHtml = this.gradeCutoffTemplate({
descriptor : this.GRADES[gradeLength],
width : targetWidth,
contenteditable: true,
removable : true });
var gradeDom = this.$el.find('.grades');
gradeDom.children().last().before($newGradeBar);
gradeDom.children().last().before(HtmlUtils.ensureHtml(newGradeHtml).toString());
var newEle = gradeDom.children()[gradeLength];
$(newEle).resizable({
handles: "e",
......@@ -313,8 +321,8 @@ var GradingView = ValidatingView.extend({
// Munge existing grade labels?
// If going from Pass/Fail to 3 levels, change to Pass to A
if (gradeLength === 1 && this.descendingCutoffs[0]['designation'] === 'Pass') {
this.descendingCutoffs[0]['designation'] = this.GRADES[0];
if (gradeLength === 1 && this.descendingCutoffs[0].designation === 'Pass') {
this.descendingCutoffs[0].designation = this.GRADES[0];
this.setTopGradeLabel();
}
this.setFailLabel();
......@@ -349,10 +357,10 @@ var GradingView = ValidatingView.extend({
else return 'F';
},
setFailLabel: function() {
this.$el.find('.grades .letter-grade').last().html(this.failLabel());
this.$el.find('.grades .letter-grade').last().text(this.failLabel());
},
setTopGradeLabel: function() {
this.$el.find('.grades .letter-grade').first().html(this.descendingCutoffs[0]['designation']);
this.$el.find('.grades .letter-grade').first().text(this.descendingCutoffs[0].designation);
},
setupCutoffs: function() {
// Instrument grading scale
......
define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "jquery.form"],
function($, _, gettext, BaseModal) {
var UploadDialog = BaseModal.extend({
events: {
events: _.extend({}, BaseModal.prototype.events, {
"change input[type=file]": "selectFile",
"click .action-upload": "upload"
},
}),
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'assetupload',
......@@ -15,7 +15,6 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "jquery
initialize: function() {
BaseModal.prototype.initialize.call(this);
this.events = _.extend({}, BaseModal.prototype.events, this.events);
this.template = this.loadTemplate("upload-dialog");
this.listenTo(this.model, "change", this.renderContents);
this.options.title = this.model.get('title');
......
define(["js/views/baseview", "underscore", "jquery", "gettext", "common/js/components/views/feedback_notification", "common/js/components/views/feedback_alert", "js/views/baseview", "jquery.smoothScroll"],
function(BaseView, _, $, gettext, NotificationView, AlertView) {
define(["edx-ui-toolkit/js/utils/html-utils",
"js/views/baseview",
"underscore",
"jquery",
"gettext",
"common/js/components/views/feedback_notification",
"common/js/components/views/feedback_alert",
"js/views/baseview",
"jquery.smoothScroll"],
function(HtmlUtils, BaseView, _, $, gettext, NotificationView, AlertView) {
var ValidatingView = BaseView.extend({
// Intended as an abstract class which catches validation errors on the model and
......@@ -10,7 +18,7 @@ var ValidatingView = BaseView.extend({
this.selectorToField = _.invert(this.fieldToSelectorMap);
},
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
errorTemplate : HtmlUtils.template('<span class="message-error"><%- message %></span>'),
save_title: gettext("You've made some changes"),
save_message: gettext("Your changes will not take effect until you save your progress."),
......@@ -34,7 +42,7 @@ var ValidatingView = BaseView.extend({
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
this._cacheValidationErrors.push(ele);
this.getInputElements(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
HtmlUtils.append($(ele).parent(), this.errorTemplate({message : error[field]}));
}
$('.wrapper-notification-warning').addClass('wrapper-notification-warning-w-errors');
$('.action-save').addClass('is-disabled');
......@@ -60,7 +68,7 @@ var ValidatingView = BaseView.extend({
// Set model field and return the new value.
this.clearValidationErrors();
var field = this.selectorToField[event.currentTarget.id];
var newVal = ''
var newVal = '';
if(event.currentTarget.type == 'checkbox'){
newVal = $(event.currentTarget).is(":checked").toString();
}else{
......
......@@ -17,11 +17,11 @@ function($, Backbone, _, Utils) {
uploadTpl: '#file-upload',
initialize: function () {
initialize: function (options) {
_.bindAll(this,
'changeHandler', 'clickHandler', 'xhrResetProgressBar', 'xhrProgressHandler', 'xhrCompleteHandler'
);
this.options = _.extend({}, options);
this.file = false;
this.render();
},
......
......@@ -28,11 +28,13 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
choose: '#transcripts-choose'
},
initialize: function () {
initialize: function (options) {
_.bindAll(this,
'importHandler', 'replaceHandler', 'chooseHandler', 'useExistingHandler', 'showError', 'hideError'
);
this.options = _.extend({}, options);
this.component_locator = this.$el.closest('[data-locator]').data('locator');
this.fileUploader = new FileUploader({
......
......@@ -25,9 +25,10 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager) {
'youtube': 'http://youtube.com/'
},
initialize: function () {
initialize: function (options) {
// Initialize MessageManager that is responsible for
// status messages and errors.
this.options = _.extend({}, options);
var Messenger = this.options.MessageManager || MessageManager;
this.messenger = new Messenger({
......
......@@ -37,7 +37,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jquery.simulate.js
- xmodule_js/common_static/common/js/vendor/underscore.js
- xmodule_js/common_static/common/js/vendor/underscore.string.js
- xmodule_js/common_static/js/vendor/backbone-min.js
- xmodule_js/common_static/common/js/vendor/backbone.js
- xmodule_js/common_static/js/vendor/backbone-associations-min.js
- xmodule_js/common_static/js/vendor/backbone.paginator.min.js
- xmodule_js/common_static/js/vendor/backbone-relational.min.js
......
......@@ -36,7 +36,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jquery.cookie.js
- xmodule_js/common_static/common/js/vendor/underscore.js
- xmodule_js/common_static/common/js/vendor/underscore.string.js
- xmodule_js/common_static/js/vendor/backbone-min.js
- xmodule_js/common_static/common/js/vendor/backbone.js
- xmodule_js/common_static/js/vendor/backbone-associations-min.js
- xmodule_js/common_static/js/vendor/backbone.paginator.min.js
- xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js
......
......@@ -35,7 +35,7 @@ var libraryFiles = [
{pattern: 'xmodule_js/common_static/js/vendor/jquery.simulate.js', included: false},
{pattern: 'xmodule_js/common_static/common/js/vendor/underscore.js', included: false},
{pattern: 'xmodule_js/common_static/common/js/vendor/underscore.string.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone-min.js', included: false},
{pattern: 'xmodule_js/common_static/common/js/vendor/backbone.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone-associations-min.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone.paginator.min.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone-relational.min.js', included: false},
......@@ -82,7 +82,9 @@ var libraryFiles = [
{pattern: 'edx-pattern-library/js/afontgarde.js', included: false},
{pattern: 'edx-pattern-library/js/edx-icons.js', included: false},
{pattern: 'edx-pattern-library/js/**/*.js', included: false},
{pattern: 'edx-ui-toolkit/js/**/*.js', included: false}
{pattern: 'edx-ui-toolkit/js/**/*.js', included: false},
{pattern: 'common/js/utils/require-serial.js', included: true}
];
// Paths to source JavaScript files
......
......@@ -35,7 +35,7 @@ var libraryFiles = [
{pattern: 'xmodule_js/common_static/js/vendor/jquery.cookie.js', included: false},
{pattern: 'xmodule_js/common_static/common/js/vendor/underscore.js', included: false},
{pattern: 'xmodule_js/common_static/common/js/vendor/underscore.string.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone-min.js', included: false},
{pattern: 'xmodule_js/common_static/common/js/vendor/backbone.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone-associations-min.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/backbone.paginator.min.js', included: false},
{pattern: 'xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js', included: false},
......@@ -68,7 +68,8 @@ var libraryFiles = [
pattern: 'xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate.js',
included: false
},
{pattern: 'xmodule_js/common_static/js/vendor/requirejs/text.js', included: false}
{pattern: 'xmodule_js/common_static/js/vendor/requirejs/text.js', included: false},
{pattern: 'common/js/utils/require-serial.js', included: true}
];
// Paths to source JavaScript files
......
......@@ -564,7 +564,7 @@
}
.list-fields {
.field-message {
color: $gray;
color: $gray-d1;
font-size: ($baseline/2);
}
.field {
......
......@@ -303,7 +303,7 @@ $outline-indent-width: $baseline;
%outline-item-status {
@extend %t-copy-sub2;
@extend %t-strong;
color: $color-copy-base;
color: $gray-d1;
.icon {
@extend %t-icon5;
......@@ -576,12 +576,16 @@ $outline-indent-width: $baseline;
> .subsection-status .status-timed-proctored-exam {
opacity: 1.0;
}
> .subsection-status .status-hide-after-due {
opacity: 1.0;
}
}
// status - grading
.status-grading, .status-timed-proctored-exam {
.status-grading, .status-timed-proctored-exam, .status-hide-after-due {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
opacity: 0.75;
}
.status-grading-value, .status-proctored-exam-value {
......
......@@ -54,7 +54,7 @@ from openedx.core.djangolib.js_utils import (
<body class="${static.dir_rtl()} <%block name='bodyclass'></%block> lang_${LANGUAGE_CODE}">
<%block name="view_notes"></%block>
<a class="nav-skip" href="#content">${_("Skip to main content")}</a>
<a class="nav-skip" href="#main">${_("Skip to main content")}</a>
<script type="text/javascript">
window.baseUrl = "${settings.STATIC_URL | n, js_escaped_string}";
......@@ -72,9 +72,11 @@ from openedx.core.djangolib.js_utils import (
<%block name="page_alert"></%block>
</div>
<div id="content" tabindex="-1">
<%block name="content"></%block>
</div>
<main id="main" aria-label="Content" tabindex="-1">
<div id="content">
<%block name="content"></%block>
</div>
</main>
% if user.is_authenticated():
<%include file="widgets/sock.html" args="online_help_token=online_help_token" />
......
......@@ -22,7 +22,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
</%block>
<%block name="content">
<div id="content" tabindex="-1">
<div id="content">
<div class="wrapper-mast wrapper">
<header class="mast mast-wizard has-actions">
<h1 class="page-header">
......
<li class="grade-specific-bar" style="width:<%- width %>%">
<span class="letter-grade" contenteditable="<%- contenteditable %>"><%- descriptor %></span>
<span class="range"></span>
<% if (removable) {%><a href="#" class="remove-button">remove</a><% ;} %>
</li>
<form>
<h3 class="modal-section-title"><%= gettext('Set as a Special Exam') %></h3>
<div class="modal-section-content has-actions">
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<ul class="list-fields list-input exam-types" role="group" aria-label="<%- gettext('Exam Types') %>">
<li class="field-radio">
<label class="label">
<input type="radio" name="exam_type" class="input input-radio no_special_exam" checked="checked"/>
<%- gettext('None') %>
</label>
</li>
<li class="field-radio">
<label class="label">
<input type="radio" name="exam_type" class="input input-radio timed_exam"
aria-describedby="timed-exam-description" />
<%- gettext('Timed') %>
</label>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
</li>
<% if (enable_proctored_exam) { %>
<li class="field-radio">
<input type="radio" id="id_not_timed" name="proctored" class="input input-radio" checked="checked"/>
<label for="id_not_timed" class="label">
<%- gettext('None') %>
<label class="label">
<input type="radio" name="exam_type" class="input input-radio proctored_exam"
aria-describedby="proctored-exam-description" />
<%- gettext('Proctored') %>
</label>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
</li>
</ul>
</div>
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
<input type="radio" id="id_timed_exam" name="proctored" class="input input-radio" />
<label for="id_timed_exam" class="label" aria-describedby="timed-exam-description">
<%- gettext('Timed') %>
<label class="label">
<input type="radio" name="exam_type" class="input input-radio practice_exam"
aria-describedby="practice-exam-description"/>
<%- gettext('Practice Proctored') %>
</label>
</li>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
</ul>
</div>
<% if (enable_proctored_exam) { %>
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
<input type="radio" id="id_proctored_exam" name="proctored" class="input input-radio" />
<label for="id_proctored_exam" class="label" aria-describedby="proctored-exam-description">
<%- gettext('Proctored') %>
</label>
</li>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
</ul>
</div>
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
<input type="radio" id="id_practice_exam" name="proctored" class="input input-radio" />
<label for="id_practice_exam" class="label" aria-describedby="practice-exam-description">
<%- gettext('Practice Proctored') %>
</label>
</li>
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner's grade.") %> </p>
</ul>
</div>
<% } %>
<div class='exam-time-list-fields is-hidden' id='id_time_limit_div'>
<ul class="list-fields list-input time-limit">
<li class="field field-text field-time-limit">
<label for="id_time_limit" class="label"><%- gettext('Time Allotted (HH:MM):') %> </label>
<input type="text" id="id_time_limit" name="time_limit"
value="" aria-describedby="time-limit-description"
placeholder="HH:MM" class="time_limit release-time time input input-text" autocomplete="off" />
</li>
<% } %>
</ul>
<ul class="list-fields list-input exam-options">
<li class="field field-text field-time-limit">
<label class="label">
<%- gettext('Time Allotted (HH:MM):') %>
<input type="text" value="" aria-describedby="time-limit-description" placeholder="HH:MM"
class="time_limit release-time time input input-text" autocomplete="off" />
</label>
<p class='field-message' id='time-limit-description'><%- gettext('Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.') %></p>
</ul>
</div>
<div class='exam-review-rules-list-fields is-hidden'>
<ul class="list-fields list-input exam-review-rules">
<li class="field field-text field-exam-review-rules">
<label for="id_exam_review_rules" class="label"><%- gettext('Review Rules') %> </label>
<textarea id="id_exam_review_rules" cols="50" maxlength="255" name="review_rules" aria-describedby="review-rules-description"
</li>
<li class="field field-text field-exam-review-rules">
<label class="label">
<%- gettext('Review Rules') %>
<textarea cols="50" maxlength="255" aria-describedby="review-rules-description"
class="review-rules input input-text" autocomplete="off" />
</li>
</label>
<p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p>
</ul>
</div>
</li>
<li class="field-checkbox field-hide-after-due">
<label class="label">
<input type="checkbox" class="input input-checkbox"
aria-describedby="hide-after-due-description"/>
<%- gettext('Hide Exam After Due Date') %>
</label>
<p class='field-message' id='hide-after-due-description'><%- gettext('By default, submitted exams are available for review after the due date has passed. Select this option to keep exams hidden after that date.') %></p>
</li>
</ul>
</div>
</form>
......@@ -15,7 +15,7 @@
%>
<%block name="header_extras">
% for template_name in ["course_grade_policy"]:
% for template_name in ["course_grade_policy", "course_grade_cutoff"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
......
## Override the default styles_version to the Pattern Library version (version 2)
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/>
<%inherit file="../../base.html" />
......@@ -22,8 +25,6 @@
</div>
<section class="xblock xblock-student_view xmodule_display xmodule_HtmlModule">
<ul>
<li><a href="container.html">Container page</a></li>
<li><a href="unit.html">Unit page</a></li>
<li><a href="pattern-library-test.html">Pattern Library test page</a></li>
</ul>
</section>
......
<%page expression_filter="h"/>
<div class="wrapper wrapper-modal-window wrapper-modal-window-edit-xblock" aria-labelledby="modal-window-title" role="dialog">
<div class="modal-window-overlay"></div>
<div class="modal-window confirm modal-med modal-type-html modal-editor" style="top: 50px; left: 400px;" tabindex="-1" aria-labelledby="modal-window-title">
<div class="edit-xblock-modal">
<div class="modal-header">
<h2 id="modal-window-title" class="title modal-window-title">Editing visibility for: [Component Name]</h2>
</div>
<div class="modal-content">
<div class="xblock-editor" data-locator="i4x://TestU/cohorts001/chapter/748152225449412a846bc24811a5621c" data-course-key="">
<div class="xblock xblock-visibility_view">
<div class="modal-section visibility-summary">
<div class="summary-message summary-message-warning visibility-summary-message">
<i class="icon fa fa-exclamation-triangle" aria-hidden="true"></i>
<p class="copy"><span class="sr">Warning: </span>This component is contained in a unit that is hidden from students. Component visibility settings are overridden by the unit visibility settings.</p>
</div>
<!-- NOTE: use when no group configuration has been set -->
<div class="is-not-configured has-actions">
<h4 class="title">You have not set up any groups</h4>
<div class="copy">
<p>Groups are a way for you to organize content in your course with a particular student experience in mind. They are commonly used to facilitate content and pedagogical experiments as well as to provide different tracks of content.</p>
</div>
<div class="actions">
<a href="" class="action action-primary action-settings">Manage groups in this course</a>
</div>
</div>
</div>
<form class="visibility-controls-form" method="post" action="">
<div class="modal-section visibility-controls">
<h3 class="modal-section-title">Set visibility to:</h3>
<div class="modal-section-content">
<section class="visibility-controls-primary">
<ul class="list-fields list-radio">
<li class="field field-radio field-visibility-level">
<input type="radio" id="visibility-level-all" name="visibility-level" value="" class="input input-radio visibility-level-all" />
<label for="visibility-level-all" class="label">All Students and Staff</label>
</li>
<li class="field field-radio field-visibility-level">
<input type="radio" id="visibility-level-specific" name="visibility-level" value="" class="input input-radio visibility-level-specific" checked="checked" />
<label for="visibility-level-specific" class="label">Specific Groups</label>
</li>
</ul>
</section>
<!-- NOTE: @andyarmstrong, if you need this wrapper to show and hide, great. If not, please remove it from the DOM -->
<div class="wrapper-visibility-specific">
<section class="visibility-controls-secondary">
<div class="visibility-controls-group">
<h4 class="visibility-controls-title modal-subsection-title sr">Content Groups</h4>
<ul class="list-fields list-checkbox">
<li class="field field-checkbox field-visibility-content-group">
<input type="checkbox" id="visibility-content-group-NAME1" name="visibility-content-group" value="" class="input input-checkbox visibility-content-group-NAME1" />
<label for="visibility-content-group-NAME1" class="label">Content Group NAME 1</label>
</li>
<li class="field field-checkbox field-visibility-content-group">
<input type="checkbox" id="visibility-content-group-NAME2" name="visibility-content-group" value="" class="input input-checkbox visibility-content-group-NAME2" />
<label for="visibility-content-group-NAME2" class="label">Content Group NAME 2</label>
</li>
<li class="field field-checkbox field-visibility-content-group">
<input type="checkbox" id="visibility-content-group-NAME3" name="visibility-content-group" value="" class="input input-checkbox visibility-content-group-NAME3" />
<label for="visibility-content-group-NAME3" class="label">Content Group NAME 3</label>
</li>
<li class="field field-checkbox field-visibility-content-group">
<input type="checkbox" id="visibility-content-group-NAME4" name="visibility-content-group" value="" class="input input-checkbox visibility-content-group-NAME4" />
<label for="visibility-content-group-NAME4" class="label">Content Group NAME 4</label>
</li>
<!-- NOTE: @andyarmstrong, here's an example of how we would treat a group that was deleted/removed - we need a .was removed class and an additional UI element called a .note -->
<li class="field field-checkbox field-visibility-content-group was-removed">
<input type="checkbox" id="visibility-content-group-deleted1" name="visibility-content-group" value="" class="input input-checkbox visibility-content-group-deleted" checked="checked" />
<label for="visibility-content-group-deleted1" class="label">
Deleted Content Group
</label>
<span class="note">The selected group no longer exists. Choose another group or make the component visible to All Students and Staff</span>
</li>
<!-- NOTE: @andyarmstrong, here's an example of how we would treat a group that was deleted/removed - we need a .was removed class and an additional UI element called a .note -->
<li class="field field-checkbox field-visibility-content-group was-removed">
<input type="checkbox" id="visibility-content-group-deleted1" name="visibility-content-group" value="" class="input input-checkbox visibility-content-group-deleted" checked="checked" />
<label for="visibility-content-group-deleted1" class="label">
Deleted Content Group
</label>
<span class="note">The selected group no longer exists. Choose another group or make the component visible to All Students and Staff</span>
</li>
</ul>
</div>
</section>
</div>
</div>
</div>
</form>
</div><!-- .xblock -->
</div><!-- .xblock-editor -->
</div><!-- .modal-content -->
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-save">Save</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div><!-- .xblock-visibility-modal -->
</div><!-- .modal-window -->
</div><!-- .wrapper-modal-window -->
<%page expression_filter="h"/>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-section" aria-labelledby="modal-window-title" role="dialog">
<div class="modal-window-overlay"></div>
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
<div class="bulkpublish-section-modal">
<div class="modal-header">
<h2 class="title modal-window-title">Publish [section name]</h2>
</div>
<div class="modal-content">
<div class="message modal-introduction">
<p>Publish all unpublished changes for this section?</p>
</div>
<div class="modal-section bulkpublish-included">
<h3 class="modal-section-title">The following will be published:</h3>
<div class="modal-section-content">
<div class="outline outline-simple outline-bulkpublish">
<ol class="list-subsections">
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title">Subsection Title</h4>
<div class="subsection-content">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</div>
</li>
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title">Subsection Title</h4>
<div class="subsection-content">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</div>
</li>
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title">Subsection Title</h4>
<div class="subsection-content">
<ol class="list-units">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</ol>
</div>
</li>
</ol>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-publish">Publish</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<%page expression_filter="h"/>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-subsection" aria-labelledby="modal-window-title" role="dialog">
<div class="modal-window-overlay"></div>
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
<div class="bulkpublish-subsection-modal">
<div class="modal-header">
<h2 class="title modal-window-title">Publish [subsection name]</h2>
</div>
<div class="modal-content">
<div class="message modal-introduction">
<p>Publish all unpublished changes for this subsection?</p>
</div>
<div class="modal-section bulkpublish-included">
<h3 class="modal-section-title">The following will be published:</h3>
<div class="modal-section-content">
<div class="outline outline-simple outline-bulkpublish">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-publish">Publish</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<%page expression_filter="h"/>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-unit" aria-labelledby="modal-window-title" role="dialog">
<div class="modal-window-overlay"></div>
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
<div class="bulkpublish-unit-modal">
<div class="modal-header">
<h2 class="title modal-window-title">Publish [unit name]</h2>
</div>
<div class="modal-content">
<div class="message modal-introduction">
<p>Publish all unpublished changes for this unit?</p>
</div>
</div>
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-publish">Publish</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<%page expression_filter="h"/>
<div class="add-section add-item">
<a href="#" class="button button-new" data-category="" data-parent="" data-default-name="New Section">
<i class="icon fa fa-plus"></i> New Section
</a>
</div>
<!-- /add-section -->
<%page expression_filter="h"/>
<div class="add-subsection add-item">
<a href="#" class="button button-new" data-category="" data-parent="" data-default-name="New Subsection">
<i class="icon fa fa-plus"></i> New Subsection
</a>
</div>
<!-- /add-subsection -->
<%page expression_filter="h"/>
<div class="add-unit add-item">
<a href="#" class="button button-new" data-category="" data-parent="" data-default-name="New Unit">
<i class="icon fa fa-plus"></i> New Unit
</a>
</div>
<!-- /add-unit -->
<%page expression_filter="h"/>
<div class="section-header">
<h3 class="section-header-details ui-toggle-expansion" title="Collapse/Expand this Section">
<i class="icon fa fa-caret-down icon"></i>
<span class="wrapper-section-title is-editable wrapper-xblock-field">
<span class="section-title item-title xblock-field-value">Section Title</span>
</span>
</h3>
<div class="section-header-actions">
<ul class="actions-list">
<li class="action-item action-publish">
<a href="#" data-tooltip="Publish all content" class="publish-button action-button">
<i class="icon fa fa-share-square-o"></i>
<span class="action-button-text sr">Publish all content</span>
</a>
</li>
<li class="action-item action-settings">
<a href="#" data-tooltip="Settings" class="settings-button action-button">
<i class="icon fa fa-cog"></i>
<span class="action-button-text sr">Settings</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="Delete" class="delete-button action-button">
<i class="icon fa fa-trash-o"></i>
<span class="action-button-text sr">Delete</span>
</a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
<!-- /section-header -->
<%page expression_filter="h"/>
<div class="section-header">
<h3 class="section-header-details ui-toggle-expansion" title="Collapse/Expand this Section">
<i class="icon fa fa-caret-down icon"></i>
<span class="wrapper-section-title is-editable wrapper-xblock-field">
<span class="section-title item-title xblock-field-value">Section Title</span>
</span>
</h3>
<div class="section-header-actions">
<ul class="actions-list">
<li class="action-item action-publish">
<a href="#" data-tooltip="Publish all content" class="publish-button action-button">
<i class="icon fa fa-share-square-o"></i>
<span class="action-button-text sr">Publish all content</span>
</a>
</li>
<li class="action-item action-settings">
<a href="#" data-tooltip="Settings" class="settings-button action-button">
<i class="icon fa fa-cog"></i>
<span class="action-button-text sr">Settings</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="Delete" class="delete-button action-button">
<i class="icon fa fa-trash-o"></i>
<span class="action-button-text sr">Delete</span>
</a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
<!-- /section-header -->
<%page expression_filter="h"/>
<div class="status-grading">
<p>
<span class="sr status-grading-label">Graded as:</span>
<i class="icon fa fa-check"></i>
<span class="status-grading-value">Homework</span>
<span class="status-grading-date">Due: December 31, 2014</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="status-message">
<i class="icon fa fa-warning"></i>
<p class="status-message-copy">Critical error</p>
</div>
<%page expression_filter="h"/>
<div class="status-message">
<i class="icon fa fa-lock"></i>
<p class="status-message-copy">Contains Staff only content</p>
</div>
<%page expression_filter="h"/>
<div class="status-message">
<i class="icon fa fa-file-o"></i>
<p class="status-message-copy">Unpublished change(s) to live content</p>
</div>
<%page expression_filter="h"/>
<div class="status-message">
<i class="icon fa fa-file-o"></i>
<p class="status-message-copy">Unpublished unit(s) will not be released</p>
</div>
<%page expression_filter="h"/>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<i class="icon fa fa-file-o"></i>
<span class="sr">This item is</span>
</span>
<span class="status-release-date">Unscheduled</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<i class="icon fa fa-lock"></i>
Will never release - Contains Staff only content
</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<i class="icon fa fa-check-square"></i>
Released <span class="sr">on</span>:
</span>
<span class="status-release-date">March 25, 2014</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<i class="icon fa fa-check-square"></i>
Released with Section
</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<i class="icon fa fa-clock-o"></i>
Scheduled: October 31, 2014
</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<i class="icon fa fa-clock-o"></i>
Scheduled: with Section
</span>
</p>
</div>
<%page expression_filter="h"/>
<div class="subsection-header">
<h3 class="subsection-header-details ui-toggle-expansion" title="Collapse/Expand this Subsection">
<i class="icon fa fa-caret-down icon"></i>
<span class="wrapper-subsection-title is-editable wrapper-xblock-field">
<span class="subsection-title item-title xblock-field-value">Subsection Title</span>
<!-- editor-title -->
<span class="editor">
<label class="sr" for="subsection-title_ID">Rename Subsection Name</label>
<input class="item-edit-title is-hidden" id="section-title_ID" type="text" value="" data-metadata-name="" />
</span>
<!-- /editor-title -->
</span>
</h3>
<div class="subsection-header-actions">
<ul class="actions-list">
<li class="action-item action-publish">
<a href="#" data-tooltip="Publish" class="publish-button action-button">
<i class="icon fa fa-share-square-o"></i>
<span class="action-button-text sr">Publish</span>
</a>
</li>
<li class="action-item action-settings">
<a href="#" data-tooltip="Settings" class="settings-button action-button">
<i class="icon fa fa-cog"></i>
<span class="action-button-text sr">Settings</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="Delete" class="delete-button action-button">
<i class="icon fa fa-trash-o"></i>
<span class="action-button-text sr">Delete</span>
</a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
<!-- /subsection-header -->
<%page expression_filter="h"/>
<div class="subsection-header">
<h3 class="subsection-header-details ui-toggle-expansion" title="Collapse/Expand this Subsection">
<i class="icon fa fa-caret-down icon"></i>
<span class="wrapper-subsection-title is-editable wrapper-xblock-field">
<span class="subsection-title item-title xblock-field-value">Subsection Title</span>
<!-- editor-title -->
<span class="editor">
<label class="sr" for="subsection-title_ID">Rename Subsection Name</label>
<input class="item-edit-title is-hidden" id="section-title_ID" type="text" value="" data-metadata-name="" />
</span>
<!-- /editor-title -->
</span>
</h3>
<div class="subsection-header-actions">
<ul class="actions-list">
<li class="action-item action-publish">
<a href="#" data-tooltip="Publish" class="publish-button action-button">
<i class="icon fa fa-share-square-o"></i>
<span class="action-button-text sr">Publish</span>
</a>
</li>
<li class="action-item action-settings">
<a href="#" data-tooltip="Settings" class="settings-button action-button">
<i class="icon fa fa-cog"></i>
<span class="action-button-text sr">Settings</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="Delete" class="delete-button action-button">
<i class="icon fa fa-trash-o"></i>
<span class="action-button-text sr">Delete</span>
</a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
<!-- /subsection-header -->
<%page expression_filter="h"/>
<div class="unit-header">
<h3 class="unit-header-details">
<span class="unit-title item-title"><a class="unit-url" href="">Unit Name</a></span>
</h3>
<div class="unit-header-actions">
<ul class="actions-list">
<li class="action-item action-publish">
<a href="#" data-tooltip="Publish" class="publish-button action-button">
<i class="icon fa fa-share-square-o"></i>
<span class="action-button-text sr">Publish</span>
</a>
</li>
<li class="action-item action-settings">
<a href="#" data-tooltip="Settings" class="settings-button action-button">
<i class="icon fa fa-cog"></i>
<span class="action-button-text sr">Settings</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="Delete" class="delete-button action-button">
<i class="icon fa fa-trash-o"></i>
<span class="action-button-text sr">Delete</span>
</a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
<!-- /unit-header -->
......@@ -7,17 +7,16 @@
<%block name="bodyclass">is-signedin pattern-library</%block>
<%block name="content">
<h3 class="hd-6 example-set-hd">Warning Alert with Message Only</h3>
<div class="example-set">
<div class="alert alert-warning" role="alert" aria-labelledby="alert-warning-publish-title" tabindex="-1">
<span class="icon alert-icon icon-warning" aria-hidden="true"></span>
<h1>Pattern Library test page</h1>
<div class="alert-message">
<p class="alert-copy">
Interesting pattern library content to come...
</p>
</div>
<div class="alert alert-warning" role="alert" tabindex="-1">
<span class="icon alert-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<div class="alert-message">
<p class="alert-copy">
Interesting pattern library content to come...
</p>
</div>
</div>
</%block>
......@@ -7,5 +7,5 @@ from django.conf.urls import url
urlpatterns = (
url(r'^dev_mode$', 'contentstore.views.dev.dev_mode', name='dev_mode'),
url(r'^template/(?P<template>.+)$', 'contentstore.views.dev.dev_show_template'),
url(r'^template/(?P<template>.+)$', 'openedx.core.djangoapps.debug.views.show_reference_template'),
)
......@@ -8,6 +8,7 @@ import decimal
import ddt
import freezegun
from mock import patch
from nose.plugins.attrib import attr
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -25,9 +26,13 @@ from course_modes.models import CourseMode, Mode
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
@attr('shard_3')
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
"""
Course Mode View tests
"""
@patch.dict(settings.FEATURES, {'MODE_CREATION_FOR_TESTING': True})
def setUp(self):
super(CourseModeViewTest, self).setUp('course_modes.urls')
......
......@@ -4,6 +4,7 @@ Tests for EmbargoMiddleware
from contextlib import contextmanager
import mock
from nose.plugins.attrib import attr
import unittest
import pygeoip
import ddt
......@@ -34,11 +35,10 @@ from embargo.exceptions import InvalidAccessPoint
from mock import patch
# Since we don't need any XML course fixtures, use a modulestore configuration
# that disables the XML modulestore.
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
@attr('shard_3')
@ddt.ddt
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
......
......@@ -4,6 +4,7 @@ Tests for EmbargoMiddleware with CountryAccessRules
import unittest
from mock import patch
from nose.plugins.attrib import attr
import ddt
from django.core.urlresolvers import reverse
......@@ -20,6 +21,7 @@ from embargo.models import RestrictedCourse, IPFilter
from embargo.test_utils import restrict_course
@attr('shard_3')
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EmbargoMiddlewareAccessTests(UrlResetMixin, ModuleStoreTestCase):
......
......@@ -9,6 +9,7 @@ import datetime
import ddt
from django.core.cache import cache
from mock import patch
from nose.plugins.attrib import attr
from django.test import Client
from django.core.handlers.wsgi import WSGIRequest
from django.core.urlresolvers import reverse
......@@ -125,6 +126,7 @@ class EnrollmentTestMixin(object):
self.assertEqual(actual_mode, expected_mode)
@attr('shard_3')
@override_settings(EDX_API_KEY="i am a key")
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
......
......@@ -20,6 +20,7 @@ from external_auth.views import (
shib_login, course_specific_login, course_specific_register, _flatten_to_ascii
)
from mock import patch
from nose.plugins.attrib import attr
from urllib import urlencode
from student.views import create_account, change_enrollment
......@@ -72,6 +73,7 @@ def gen_all_identities():
yield _build_identity_dict(mail, display_name, given_name, surname)
@attr('shard_3')
@ddt
@override_settings(SESSION_ENGINE='django.contrib.sessions.backends.cache')
class ShibSPTest(SharedModuleStoreTestCase):
......
......@@ -15,7 +15,7 @@ class RequireJSPathOverridesTest(TestCase):
OVERRIDES = {
'jquery': 'js/vendor/jquery.min.js',
'backbone': 'js/vendor/backbone-min.js',
'backbone': 'common/js/vendor/backbone.js',
'text': 'js/vendor/text.js'
}
......@@ -26,7 +26,7 @@ class RequireJSPathOverridesTest(TestCase):
"paths: {",
"'jquery': 'js/vendor/jquery.min',",
"'text': 'js/vendor/text',",
"'backbone': 'js/vendor/backbone-min'",
"'backbone': 'common/js/vendor/backbone'",
"}",
"});",
"}).call(this, require || RequireJS.require);",
......
......@@ -163,9 +163,7 @@ def replace_static_urls(text, data_directory=None, course_id=None, static_asset_
if settings.DEBUG and finders.find(rest, True):
return original
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls
elif (not static_asset_path) \
and course_id \
and modulestore().get_modulestore_type(course_id) != ModuleStoreEnum.Type.xml:
elif (not static_asset_path) and course_id:
# first look in the static file pipeline and see if we are trying to reference
# a piece of static content which is in the edx-platform repo (e.g. JS associated with an xmodule)
......
......@@ -13,7 +13,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory
# This import is for an lms djangoapp.
......@@ -90,7 +90,7 @@ class TestStudentDashboardEmailViewXMLBacked(SharedModuleStoreTestCase):
"""
Check for email view on student dashboard, with XML backed course.
"""
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
MODULESTORE = TEST_DATA_MIXED_MODULESTORE
def setUp(self):
super(TestStudentDashboardEmailViewXMLBacked, self).setUp()
......
......@@ -4,6 +4,7 @@ Tests for student enrollment.
import ddt
import unittest
from mock import patch
from nose.plugins.attrib import attr
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -20,6 +21,7 @@ from student.roles import (
)
@attr('shard_3')
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
......
......@@ -6,6 +6,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from opaque_keys.edx import locator
from pytz import UTC
from nose.plugins.attrib import attr
import unittest
import ddt
from shoppingcart.models import DonationConfiguration
......@@ -19,6 +20,7 @@ from student.views import get_course_enrollments, _get_recently_enrolled_courses
from common.test.utils import XssTestMixin
@attr('shard_3')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class TestRecentEnrollments(ModuleStoreTestCase, XssTestMixin):
......
......@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
import unittest
import ddt
from mock import patch
from nose.plugins.attrib import attr
from pytz import UTC
from django.core.urlresolvers import reverse
from django.conf import settings
......@@ -24,6 +25,7 @@ from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareS
from util.testing import UrlResetMixin
@attr('shard_3')
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
......
......@@ -11,6 +11,7 @@ from urlparse import urljoin
import pytz
from markupsafe import escape
from mock import Mock, patch
from nose.plugins.attrib import attr
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pyquery import PyQuery as pq
......@@ -888,6 +889,7 @@ class AnonymousLookupTable(ModuleStoreTestCase):
# TODO: Clean up these tests so that they use the ProgramsDataMixin.
@attr('shard_3')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
......@@ -925,7 +927,6 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
programs[unicode(course)] = [{
'id': _id,
'category': self.category,
'display_category': self.display_category,
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'status': program_status,
......@@ -968,7 +969,6 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
u'edx/demox/Run_1': [{
'id': 0,
'category': self.category,
'display_category': self.display_category,
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': marketing_slug,
'status': program_status,
......
......@@ -126,7 +126,7 @@ from notification_prefs.views import enable_notifications
from openedx.core.djangoapps.credentials.utils import get_user_program_credentials
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names, make_providers_strings
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard, get_display_category
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
......@@ -649,7 +649,6 @@ def dashboard(request):
show_email_settings_for = frozenset(
enrollment.course_id for enrollment in course_enrollments if (
settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and
modulestore().get_modulestore_type(enrollment.course_id) != ModuleStoreEnum.Type.xml and
CourseAuthorization.instructor_email_enabled(enrollment.course_id)
)
)
......@@ -2452,8 +2451,8 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
'xseries' + '/{}'
).format(program['marketing_slug'])
})
programs_for_course['display_category'] = program.get('display_category')
programs_for_course['category'] = program.get('category')
programs_for_course['display_category'] = get_display_category(program)
except KeyError:
log.warning('Program structure is invalid, skipping display: %r', program)
......
"""Map new event context values to old top-level field values. Ensures events can be parsed by legacy parsers."""
import json
import logging
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from .transformers import EventTransformerRegistry
log = logging.getLogger(__name__)
CONTEXT_FIELDS_TO_INCLUDE = [
'username',
'session',
......@@ -63,6 +59,9 @@ class LegacyFieldMappingProcessor(object):
def remove_shim_context(event):
"""
Remove obsolete fields from event context.
"""
if 'context' in event:
context = event['context']
# These fields are present elsewhere in the event at this point
......@@ -74,100 +73,6 @@ def remove_shim_context(event):
del context[field]
NAME_TO_EVENT_TYPE_MAP = {
'edx.video.played': 'play_video',
'edx.video.paused': 'pause_video',
'edx.video.stopped': 'stop_video',
'edx.video.loaded': 'load_video',
'edx.video.position.changed': 'seek_video',
'edx.video.seeked': 'seek_video',
'edx.video.transcript.shown': 'show_transcript',
'edx.video.transcript.hidden': 'hide_transcript',
}
class VideoEventProcessor(object):
"""
Converts new format video events into the legacy video event format.
Mobile devices cannot actually emit events that exactly match their counterparts emitted by the LMS javascript
video player. Instead of attempting to get them to do that, we instead insert a shim here that converts the events
they *can* easily emit and converts them into the legacy format.
TODO: Remove this shim and perform the conversion as part of some batch canonicalization process.
"""
def __call__(self, event):
name = event.get('name')
if not name:
return
if name not in NAME_TO_EVENT_TYPE_MAP:
return
# Convert edx.video.seeked to edx.video.position.changed because edx.video.seeked was not intended to actually
# ever be emitted.
if name == "edx.video.seeked":
event['name'] = "edx.video.position.changed"
event['event_type'] = NAME_TO_EVENT_TYPE_MAP[name]
if 'event' not in event:
return
payload = event['event']
if 'module_id' in payload:
module_id = payload['module_id']
try:
usage_key = UsageKey.from_string(module_id)
except InvalidKeyError:
log.warning('Unable to parse module_id "%s"', module_id, exc_info=True)
else:
payload['id'] = usage_key.html_id()
del payload['module_id']
if 'current_time' in payload:
payload['currentTime'] = payload.pop('current_time')
if 'context' in event:
context = event['context']
# Converts seek_type to seek and skip|slide to onSlideSeek|onSkipSeek
if 'seek_type' in payload:
seek_type = payload['seek_type']
if seek_type == 'slide':
payload['type'] = "onSlideSeek"
elif seek_type == 'skip':
payload['type'] = "onSkipSeek"
del payload['seek_type']
# For the iOS build that is returning a +30 for back skip 30
if (
context['application']['version'] == "1.0.02" and
context['application']['name'] == "edx.mobileapp.iOS"
):
if 'requested_skip_interval' in payload and 'type' in payload:
if (
payload['requested_skip_interval'] == 30 and
payload['type'] == "onSkipSeek"
):
payload['requested_skip_interval'] = -30
# For the Android build that isn't distinguishing between skip/seek
if 'requested_skip_interval' in payload:
if abs(payload['requested_skip_interval']) != 30:
if 'type' in payload:
payload['type'] = 'onSlideSeek'
if 'open_in_browser_url' in context:
page, _sep, _tail = context.pop('open_in_browser_url').rpartition('/')
event['page'] = page
event['event'] = json.dumps(payload)
class GoogleAnalyticsProcessor(object):
"""Adds course_id as label, and sets nonInteraction property"""
......@@ -184,3 +89,22 @@ class GoogleAnalyticsProcessor(object):
copied_event['nonInteraction'] = 1
return copied_event
class PrefixedEventProcessor(object):
"""
Process any events whose name or prefix (ending with a '.') is registered
as an EventTransformer.
"""
def __call__(self, event):
"""
If the event is registered with the EventTransformerRegistry, transform
it. Otherwise do nothing to it, and continue processing.
"""
try:
event = EventTransformerRegistry.create_transformer(event)
except KeyError:
return
event.transform()
return event
"""Ensure emitted events contain the fields legacy processors expect to find."""
from collections import namedtuple
import ddt
from mock import sentinel
from django.test.utils import override_settings
from openedx.core.lib.tests.assertions.events import assert_events_equal
from track.tests import EventTrackingTestCase, FROZEN_TIME
from . import EventTrackingTestCase, FROZEN_TIME
from ..shim import PrefixedEventProcessor
from .. import transformers
LEGACY_SHIM_PROCESSOR = [
......@@ -216,3 +222,100 @@ class MultipleShimGoogleAnalyticsProcessorTestCase(EventTrackingTestCase):
'timestamp': FROZEN_TIME,
}
assert_events_equal(expected_event, log_emitted_event)
SequenceDDT = namedtuple('SequenceDDT', ['action', 'tab_count', 'current_tab', 'legacy_event_type'])
@ddt.ddt
class EventTransformerRegistryTestCase(EventTrackingTestCase):
"""
Test the behavior of the event registry
"""
def setUp(self):
super(EventTransformerRegistryTestCase, self).setUp()
self.registry = transformers.EventTransformerRegistry()
@ddt.data(
('edx.ui.lms.sequence.next_selected', transformers.NextSelectedEventTransformer),
('edx.ui.lms.sequence.previous_selected', transformers.PreviousSelectedEventTransformer),
('edx.ui.lms.sequence.tab_selected', transformers.SequenceTabSelectedEventTransformer),
('edx.video.foo.bar', transformers.VideoEventTransformer),
)
@ddt.unpack
def test_event_registry_dispatch(self, event_name, expected_transformer):
event = {'name': event_name}
transformer = self.registry.create_transformer(event)
self.assertIsInstance(transformer, expected_transformer)
@ddt.data(
'edx.ui.lms.sequence.next_selected.what',
'edx',
'unregistered_event',
)
def test_dispatch_to_nonexistent_events(self, event_name):
event = {'name': event_name}
with self.assertRaises(KeyError):
self.registry.create_transformer(event)
@ddt.ddt
class PrefixedEventProcessorTestCase(EventTrackingTestCase):
"""
Test PrefixedEventProcessor
"""
@ddt.data(
SequenceDDT(action=u'next', tab_count=5, current_tab=3, legacy_event_type=u'seq_next'),
SequenceDDT(action=u'next', tab_count=5, current_tab=5, legacy_event_type=None),
SequenceDDT(action=u'previous', tab_count=5, current_tab=3, legacy_event_type=u'seq_prev'),
SequenceDDT(action=u'previous', tab_count=5, current_tab=1, legacy_event_type=None),
)
def test_sequence_linear_navigation(self, sequence_ddt):
event_name = u'edx.ui.lms.sequence.{}_selected'.format(sequence_ddt.action)
event = {
u'name': event_name,
u'event': {
u'current_tab': sequence_ddt.current_tab,
u'tab_count': sequence_ddt.tab_count,
u'id': u'ABCDEFG',
}
}
process_event_shim = PrefixedEventProcessor()
result = process_event_shim(event)
# Legacy fields get added when needed
if sequence_ddt.action == u'next':
offset = 1
else:
offset = -1
if sequence_ddt.legacy_event_type:
self.assertEqual(result[u'event_type'], sequence_ddt.legacy_event_type)
self.assertEqual(result[u'event'][u'old'], sequence_ddt.current_tab)
self.assertEqual(result[u'event'][u'new'], sequence_ddt.current_tab + offset)
else:
self.assertNotIn(u'event_type', result)
self.assertNotIn(u'old', result[u'event'])
self.assertNotIn(u'new', result[u'event'])
def test_sequence_tab_navigation(self):
event_name = u'edx.ui.lms.sequence.tab_selected'
event = {
u'name': event_name,
u'event': {
u'current_tab': 2,
u'target_tab': 5,
u'tab_count': 9,
u'id': u'block-v1:abc',
u'widget_placement': u'top',
}
}
process_event_shim = PrefixedEventProcessor()
result = process_event_shim(event)
self.assertEqual(result[u'event_type'], u'seq_goto')
self.assertEqual(result[u'event'][u'old'], 2)
self.assertEqual(result[u'event'][u'new'], 5)
......@@ -5,6 +5,7 @@ import json
from ddt import ddt, data, unpack
from mock import sentinel
from nose.plugins.attrib import attr
from django.contrib.auth.models import User
from django.test.client import RequestFactory
......@@ -21,12 +22,8 @@ ENDPOINT = '/segmentio/test/event'
USER_ID = 10
MOBILE_SHIM_PROCESSOR = [
{
'ENGINE': 'track.shim.LegacyFieldMappingProcessor'
},
{
'ENGINE': 'track.shim.VideoEventProcessor'
}
{'ENGINE': 'track.shim.LegacyFieldMappingProcessor'},
{'ENGINE': 'track.shim.PrefixedEventProcessor'},
]
......@@ -40,6 +37,7 @@ def expect_failure_with_message(message):
return test_decorator
@attr('shard_3')
@ddt
@override_settings(
TRACKING_SEGMENTIO_WEBHOOK_SECRET=SECRET,
......@@ -411,19 +409,29 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
assert_event_matches(expected_event, actual_event)
@data(
# Verify positive slide case. Verify slide to onSlideSeek. Verify edx.video.seeked emitted from iOS v1.0.02 is changed to edx.video.position.changed.
# Verify positive slide case. Verify slide to onSlideSeek. Verify
# edx.video.seeked emitted from iOS v1.0.02 is changed to
# edx.video.position.changed.
(1, 1, "seek_type", "slide", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'),
# Verify negative slide case. Verify slide to onSlideSeek. Verify edx.video.seeked to edx.video.position.changed.
# Verify negative slide case. Verify slide to onSlideSeek. Verify
# edx.video.seeked to edx.video.position.changed.
(-2, -2, "seek_type", "slide", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'),
# Verify +30 is changed to -30 which is incorrectly emitted in iOS v1.0.02. Verify skip to onSkipSeek
# Verify +30 is changed to -30 which is incorrectly emitted in iOS
# v1.0.02. Verify skip to onSkipSeek
(30, -30, "seek_type", "skip", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'),
# Verify the correct case of -30 is also handled as well. Verify skip to onSkipSeek
# Verify the correct case of -30 is also handled as well. Verify skip
# to onSkipSeek
(-30, -30, "seek_type", "skip", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.iOS', '1.0.02'),
# Verify positive slide case where onSkipSeek is changed to onSlideSkip. Verify edx.video.seeked emitted from Android v1.0.02 is changed to edx.video.position.changed.
# Verify positive slide case where onSkipSeek is changed to
# onSlideSkip. Verify edx.video.seeked emitted from Android v1.0.02 is
# changed to edx.video.position.changed.
(1, 1, "type", "onSkipSeek", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02'),
# Verify positive slide case where onSkipSeek is changed to onSlideSkip. Verify edx.video.seeked emitted from Android v1.0.02 is changed to edx.video.position.changed.
# Verify positive slide case where onSkipSeek is changed to
# onSlideSkip. Verify edx.video.seeked emitted from Android v1.0.02 is
# changed to edx.video.position.changed.
(-2, -2, "type", "onSkipSeek", "onSlideSeek", "edx.video.seeked", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02'),
# Verify positive skip case where onSkipSeek is not changed and does not become negative.
# Verify positive skip case where onSkipSeek is not changed and does
# not become negative.
(30, 30, "type", "onSkipSeek", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02'),
# Verify positive skip case where onSkipSeek is not changed.
(-30, -30, "type", "onSkipSeek", "onSkipSeek", "edx.video.position.changed", "edx.video.position.changed", 'edx.mobileapp.android', '1.0.02')
......
......@@ -6,15 +6,14 @@ from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
def course_key_from_string_or_404(course_key_string, message=None):
def from_string_or_404(course_key_string):
"""
Gets CourseKey from the string passed as parameter.
Parses course key from string(containing course key) or raises 404 if the string's format is invalid.
Arguments:
course_key_string(str): It contains the course key
message(str): It contains the exception message
course_key_string(str): It is string containing the course key
Returns:
CourseKey: A key that uniquely identifies a course
......@@ -26,6 +25,6 @@ def course_key_from_string_or_404(course_key_string, message=None):
try:
course_key = CourseKey.from_string(course_key_string)
except InvalidKeyError:
raise Http404(message)
raise Http404
return course_key
"""
Tests for util.course_key_utils
"""
import ddt
import unittest
from util.course_key_utils import course_key_from_string_or_404
from nose.tools import assert_equals, assert_raises # pylint: disable=no-name-in-module
from util.course_key_utils import from_string_or_404
from opaque_keys.edx.keys import CourseKey
from django.http import Http404
@ddt.ddt
class TestFromStringOr404(unittest.TestCase):
"""
Base Test class for course_key_from_string_or_404 utility tests
"""
@ddt.data(
"course-v1:TTT+CS01+2015_T0", # split style course keys
"TTT/CS01/2015_T0" # mongo style course keys
)
def test_from_string_or_404_for_valid_course_key(self, valid_course_key):
"""
Tests course_key_from_string_or_404 for valid split style course keys and mongo style course keys.
"""
self.assertEquals(
CourseKey.from_string(valid_course_key),
course_key_from_string_or_404(valid_course_key)
)
def test_from_string_or_404():
@ddt.data(
"/some.invalid.key/course-v1:TTT+CS01+2015_T0", # split style course keys
"/some.invalid.key/TTT/CS01/2015_T0" # mongo style course keys
#testing with split style course keys
assert_raises(
Http404,
from_string_or_404,
"/some.invalid.key/course-v1:TTT+CS01+2015_T0"
)
assert_equals(
CourseKey.from_string("course-v1:TTT+CS01+2015_T0"),
from_string_or_404("course-v1:TTT+CS01+2015_T0")
)
def test_from_string_or_404_for_invalid_course_key(self, invalid_course_key):
"""
Tests course_key_from_string_or_404 for valid split style course keys and mongo style course keys.
"""
self.assertRaises(
Http404,
course_key_from_string_or_404,
invalid_course_key,
)
@ddt.data(
"/some.invalid.key/course-v1:TTT+CS01+2015_T0", # split style invalid course key
"/some.invalid.key/TTT/CS01/2015_T0" # mongo style invalid course key
#testing with mongo style course keys
assert_raises(
Http404,
from_string_or_404,
"/some.invalid.key/TTT/CS01/2015_T0"
)
assert_equals(
CourseKey.from_string("TTT/CS01/2015_T0"),
from_string_or_404("TTT/CS01/2015_T0")
)
def test_from_string_or_404_with_message(self, course_string):
"""
Tests course_key_from_string_or_404 with exception message for split style and monog style invalid course keys.
:return:
"""
with self.assertRaises(Http404) as context:
course_key_from_string_or_404(course_string, message="Invalid Keys")
self.assertEquals(str(context.exception), "Invalid Keys")
......@@ -175,7 +175,7 @@ class CourseFields(object):
scope=Scope.settings
)
textbooks = TextbookList(
help=_("List of pairs of (title, url) for textbooks used in this course"),
help=_("List of Textbook objects with (title, url) for textbooks used in this course"),
default=[],
scope=Scope.content
)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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