Commit 1e74f942 by attiyaIshaque

Merge branch 'master' into ai/tnl3964-forum-vote-button

parents 269a8e9d 88aa4a90
@shard_1
Feature: Course export
I want to export my course to a tar.gz file to share with others or check into source control
# Disabling due to failure on master. 05/21/2014 TODO: fix
# Scenario: User is directed to problem with & in it when export fails
# Given I am in Studio editing a new unit
# When I add a "Blank Advanced Problem" "Advanced Problem" component
# And I edit and enter an ampersand
# And I export the course
# Then I get an error dialog
# And I can click to go to the unit with the error
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument
from lettuce import world, step
from component_settings_editor_helpers import enter_xml_in_advanced_problem
from nose.tools import assert_true, assert_equal
from contentstore.utils import reverse_usage_url
@step('I go to the export page$')
def i_go_to_the_export_page(step):
world.click_tools()
link_css = 'li.nav-course-tools-export a'
world.css_click(link_css)
@step('I export the course$')
def i_export_the_course(step):
step.given('I go to the export page')
world.css_click('a.action-export')
@step('I edit and enter bad XML$')
def i_enter_bad_xml(step):
enter_xml_in_advanced_problem(
step,
"""<problem><h1>Smallest Canvas</h1>
<p>You want to make the smallest canvas you can.</p>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false"><verbatim><canvas id="myCanvas" width = 10 height = 100> </canvas></verbatim></choice>
<choice correct="true"><code><canvas id="myCanvas" width = 10 height = 10> </canvas></code></choice>
</choicegroup>
</multiplechoiceresponse>
</problem>"""
)
@step('I edit and enter an ampersand$')
def i_enter_an_ampersand(step):
enter_xml_in_advanced_problem(step, "<problem>&</problem>")
@step('I get an error dialog$')
def get_an_error_dialog(step):
assert_true(world.is_css_present("div.prompt.error"))
@step('I can click to go to the unit with the error$')
def i_click_on_error_dialog(step):
world.css_click("button.action-primary")
problem_string = unicode(world.scenario_dict['COURSE'].id.make_usage_key("problem", 'ignore'))
problem_string = u"Problem {}".format(problem_string[:problem_string.rfind('ignore')])
css_selector = "span.inline-error"
world.wait_for_visible(css_selector)
assert_true(
world.css_html(css_selector).startswith(problem_string),
u"{} does not start with {}".format(
world.css_html(css_selector), problem_string
))
# we don't know the actual ID of the vertical. So just check that we did go to a
# vertical page in the course (there should only be one).
vertical_usage_key = world.scenario_dict['COURSE'].id.make_usage_key("vertical", "test")
vertical_url = reverse_usage_url('container_handler', vertical_usage_key)
# Remove the trailing "/None" from the URL - we don't know the course ID, so we just want to
# check that we visited a vertical URL.
if vertical_url.endswith("/test") or vertical_url.endswith("@test"):
vertical_url = vertical_url[:-5]
assert_equal(1, world.browser.url.count(vertical_url))
...@@ -101,27 +101,3 @@ Feature: CMS.Problem Editor ...@@ -101,27 +101,3 @@ Feature: CMS.Problem Editor
Then I can see Reply to Annotation link Then I can see Reply to Annotation link
And I see that page has scrolled "down" when I click on "annotatable-reply" link And I see that page has scrolled "down" when I click on "annotatable-reply" link
And I see that page has scrolled "up" when I click on "annotation-return" link And I see that page has scrolled "up" when I click on "annotation-return" link
# Disabled 11/13/2013 after failing in master
# The screenshot showed that the LaTeX editor had the text "hi",
# but Selenium timed out waiting for the text to appear.
# It also caused later tests to fail with "UnexpectedAlertPresent"
#
# This feature will work in Firefox only when Firefox is the active window
# IE will not interact with the high level source in sauce labs
#@skip_internetexplorer
#Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
# Given I have created a LaTeX Problem
# When I edit and compile the High Level Source
# Then my change to the High Level Source is persisted
# And when I view the High Level Source I see my changes
# Disabled 10/28/13 due to flakiness observed in master
# Scenario: Exceptions don't cause problem to be uneditable (bug STUD-786)
#Given I have an empty course
#And I go to the import page
#And I import the file "get_html_exception_test.tar.gz"
#When I go to the unit "Probability and BMI"
#And I click on "edit a draft"
#Then I see a message that says "We're having trouble rendering your component"
#And I can edit the problem
@shard_2 @requires_stub_youtube
Feature: CMS Video Component
As a course author, I want to be able to view my created videos in Studio
# 2
# Disabled 2/19/14 after intermittent failures in master
#Scenario: Check that position is stored on page refresh, position within start-end range
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# And I edit the component
# And I open tab "Advanced"
# And I set value "00:00:12" to the field "Video Start Time"
# And I set value "00:00:24" to the field "Video Stop Time"
# And I save changes
# And I click video button "play"
# Then I see a range on slider
# Then I seek video to "16" seconds
# And I click video button "pause"
# And I reload the page
# And I click video button "play"
# Then I see video starts playing from "0:16" position
# 3
# Disabled 2/18/14 after intermittent failures in master
# Scenario: Check that position is stored on page refresh, position before start-end range
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# And I edit the component
# And I open tab "Advanced"
# And I set value "00:00:12" to the field "Video Start Time"
# And I set value "00:00:24" to the field "Video Stop Time"
# And I save changes
# And I click video button "play"
# Then I see a range on slider
# Then I seek video to "5" seconds
# And I click video button "pause"
# And I reload the page
# And I click video button "play"
# Then I see video starts playing from "0:12" position
# 4
# Disabled 2/18/14 after intermittent failures in master
# Scenario: Check that position is stored on page refresh, position after start-end range
# Given I have created a Video component with subtitles
# And Make sure captions are closed
# And I edit the component
# And I open tab "Advanced"
# And I set value "00:00:12" to the field "Video Start Time"
# And I set value "00:00:24" to the field "Video Stop Time"
# And I save changes
# And I click video button "play"
# Then I see a range on slider
# Then I seek video to "30" seconds
# And I click video button "pause"
# And I reload the page
# And I click video button "play"
# Then I see video starts playing from "0:12" position
# -*- coding: utf-8 -*-
# disable missing docstring
# pylint: disable=missing-docstring
from lettuce import world, step
from nose.tools import assert_true
from video_editor import RequestHandlerWithSessionId, success_upload_file
@step('I (?:upload|replace) handout file(?: by)? "([^"]*)"$')
def upload_handout(step, filename):
world.css_click('.wrapper-comp-setting.file-uploader .upload-action')
success_upload_file(filename)
@step('I can download handout file( in editor)? with mime type "([^"]*)"$')
def i_can_download_handout_with_mime_type(_step, is_editor, mime_type):
if is_editor:
selector = '.wrapper-comp-setting.file-uploader .download-action'
else:
selector = '.video-handout.video-download-button a'
button = world.css_find(selector).first
url = button['href']
request = RequestHandlerWithSessionId()
assert_true(request.get(url).is_success())
assert_true(request.check_header('content-type', mime_type))
@step('I clear handout$')
def clear_handout(_step):
world.css_click('.wrapper-comp-setting.file-uploader .setting-clear')
@step('I have created a Video component with handout file "([^"]*)"')
def create_video_with_handout(_step, filename):
_step.given('I have created a Video component')
_step.given('I edit the component')
_step.given('I open tab "Advanced"')
_step.given('I upload handout file "{0}"'.format(filename))
...@@ -283,6 +283,17 @@ else: ...@@ -283,6 +283,17 @@ else:
DATABASES = AUTH_TOKENS['DATABASES'] DATABASES = AUTH_TOKENS['DATABASES']
MODULESTORE = convert_module_store_setting_if_needed(AUTH_TOKENS.get('MODULESTORE', MODULESTORE)) MODULESTORE = convert_module_store_setting_if_needed(AUTH_TOKENS.get('MODULESTORE', MODULESTORE))
MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ENV_TOKENS.get(
'MODULESTORE_FIELD_OVERRIDE_PROVIDERS',
MODULESTORE_FIELD_OVERRIDE_PROVIDERS
)
XBLOCK_FIELD_DATA_WRAPPERS = ENV_TOKENS.get(
'XBLOCK_FIELD_DATA_WRAPPERS',
XBLOCK_FIELD_DATA_WRAPPERS
)
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE'] CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
DOC_STORE_CONFIG = AUTH_TOKENS['DOC_STORE_CONFIG'] DOC_STORE_CONFIG = AUTH_TOKENS['DOC_STORE_CONFIG']
# Datadog for events! # Datadog for events!
......
...@@ -383,6 +383,9 @@ XBLOCK_MIXINS = ( ...@@ -383,6 +383,9 @@ XBLOCK_MIXINS = (
XBLOCK_SELECT_FUNCTION = prefer_xmodules XBLOCK_SELECT_FUNCTION = prefer_xmodules
# Paths to wrapper methods which should be applied to every XBlock's FieldData.
XBLOCK_FIELD_DATA_WRAPPERS = ()
############################ Modulestore Configuration ################################ ############################ Modulestore Configuration ################################
MODULESTORE_BRANCH = 'draft-preferred' MODULESTORE_BRANCH = 'draft-preferred'
...@@ -417,6 +420,10 @@ MODULESTORE = { ...@@ -417,6 +420,10 @@ MODULESTORE = {
} }
} }
# Modulestore-level field override providers. These field override providers don't
# require student context.
MODULESTORE_FIELD_OVERRIDE_PROVIDERS = ()
#################### Python sandbox ############################################ #################### Python sandbox ############################################
CODE_JAIL = { CODE_JAIL = {
......
../../common/static/edx-pattern-library
\ No newline at end of file
../../common/static/edx-ui-toolkit
\ No newline at end of file
...@@ -129,9 +129,9 @@ function ($, _, Backbone, gettext, ...@@ -129,9 +129,9 @@ function ($, _, Backbone, gettext,
if (event && event.preventDefault) { event.preventDefault(); } if (event && event.preventDefault) { event.preventDefault(); }
var model = this.model; var model = this.model;
var self = this; var self = this;
var titleText = gettext('Delete "<%= signatoryName %>" from the list of signatories?'); var titleTextTemplate = _.template(gettext('Delete "<%= signatoryName %>" from the list of signatories?'));
var confirm = new PromptView.Warning({ var confirm = new PromptView.Warning({
title: _.template(titleText, {signatoryName: model.get('name')}), title: titleTextTemplate({signatoryName: model.get('name')}),
message: gettext('This action cannot be undone.'), message: gettext('This action cannot be undone.'),
actions: { actions: {
primary: { primary: {
......
...@@ -66,9 +66,10 @@ var CourseGrader = Backbone.Model.extend({ ...@@ -66,9 +66,10 @@ var CourseGrader = Backbone.Model.extend({
else attrs.drop_count = intDropCount; else attrs.drop_count = intDropCount;
} }
if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && !_.has(errors, 'min_count') && !_.has(errors, 'drop_count') && attrs.drop_count > attrs.min_count) { if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && !_.has(errors, 'min_count') && !_.has(errors, 'drop_count') && attrs.drop_count > attrs.min_count) {
errors.drop_count = _.template( var template = _.template(
gettext("Cannot drop more <% attrs.types %> than will assigned."), gettext("Cannot drop more <%= types %> assignments than are assigned.")
attrs, {variable: 'attrs'}); );
errors.drop_count = template({types: attrs.type});
} }
if (!_.isEmpty(errors)) return errors; if (!_.isEmpty(errors)) return errors;
} }
......
...@@ -15,8 +15,7 @@ var FileUpload = Backbone.Model.extend({ ...@@ -15,8 +15,7 @@ var FileUpload = Backbone.Model.extend({
validate: function(attrs, options) { validate: function(attrs, options) {
if(attrs.selectedFile && !this.checkTypeValidity(attrs.selectedFile)) { if(attrs.selectedFile && !this.checkTypeValidity(attrs.selectedFile)) {
return { return {
message: _.template( message: _.template(gettext("Only <%= fileTypes %> files can be uploaded. Please select a file ending in <%= fileExtensions %> to upload."))( // jshint ignore:line
gettext("Only <%= fileTypes %> files can be uploaded. Please select a file ending in <%= fileExtensions %> to upload."),
this.formatValidTypes() this.formatValidTypes()
), ),
attributes: {selectedFile: true} attributes: {selectedFile: true}
...@@ -64,7 +63,7 @@ var FileUpload = Backbone.Model.extend({ ...@@ -64,7 +63,7 @@ var FileUpload = Backbone.Model.extend({
} }
var or = gettext('or'); var or = gettext('or');
var formatTypes = function(types) { var formatTypes = function(types) {
return _.template('<%= initial %> <%= or %> <%= last %>', { return _.template('<%= initial %> <%= or %> <%= last %>')({
initial: _.initial(types).join(', '), initial: _.initial(types).join(', '),
or: or, or: or,
last: _.last(types) last: _.last(types)
......
...@@ -359,12 +359,12 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "draggabilly", ...@@ -359,12 +359,12 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "draggabilly",
makeDraggable: function (element, options) { makeDraggable: function (element, options) {
var draggable; var draggable;
options = _.defaults({ options = _.defaults({
type: null, type: undefined,
handleClass: null, handleClass: undefined,
droppableClass: null, droppableClass: undefined,
parentLocationSelector: null, parentLocationSelector: undefined,
refresh: null, refresh: undefined,
ensureChildrenRendered: null ensureChildrenRendered: undefined
}, options); }, options);
if ($(element).data('droppable-class') !== options.droppableClass) { if ($(element).data('droppable-class') !== options.droppableClass) {
......
...@@ -67,7 +67,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/models/asset ...@@ -67,7 +67,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/models/asset
ViewUtils.hideLoadingIndicator(); ViewUtils.hideLoadingIndicator();
// Create the table // Create the table
this.$el.html(_.template(asset_library_template, {typeData: this.typeData})); this.$el.html(_.template(asset_library_template)({typeData: this.typeData}));
tableBody = this.$('#asset-table-body'); tableBody = this.$('#asset-table-body');
this.tableBody = tableBody; this.tableBody = tableBody;
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')}); this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')});
......
...@@ -75,18 +75,18 @@ define([ ...@@ -75,18 +75,18 @@ define([
}, },
getOutlineAnchorMessage: function () { getOutlineAnchorMessage: function () {
var message = gettext( var message = _.escape(gettext(
/* /*
Translators: 'outlineAnchor' is an anchor pointing to Translators: 'outlineAnchor' is an anchor pointing to
the course outline page. the course outline page.
*/ */
'This content group is not in use. Add a content group to any unit from the %(outlineAnchor)s.' 'This content group is not in use. Add a content group to any unit from the %(outlineAnchor)s.'
), )),
anchor = str.sprintf( anchor = str.sprintf(
'<a href="%(url)s" title="%(text)s">%(text)s</a>', '<a href="%(url)s" title="%(text)s">%(text)s</a>',
{ {
url: this.model.collection.parents[0].outlineUrl, url: this.model.collection.parents[0].outlineUrl,
text: gettext('Course Outline') text: _.escape(gettext('Course Outline'))
} }
); );
......
/*global course */
define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gettext", "js/models/uploads", "js/views/uploads"], define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gettext", "js/models/uploads", "js/views/uploads"],
function(BaseView, _, str, $, gettext, FileUploadModel, UploadDialogView) { function(BaseView, _, str, $, gettext, FileUploadModel, UploadDialogView) {
_.str = str; // used in template _.str = str; // used in template
...@@ -52,10 +54,8 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gette ...@@ -52,10 +54,8 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gette
asset_path: this.$("input.chapter-asset-path").val() asset_path: this.$("input.chapter-asset-path").val()
}); });
var msg = new FileUploadModel({ var msg = new FileUploadModel({
title: _.template( title: _.template(gettext("Upload a new PDF to “<%= name %>”"))(
gettext("Upload a new PDF to “<%= name %>”"), {name: course.escape('name')}),
{name: window.course.escape('name')}
),
message: gettext("Please select a PDF file to upload."), message: gettext("Please select a PDF file to upload."),
mimeTypes: ['application/pdf'] mimeTypes: ['application/pdf']
}); });
......
...@@ -54,8 +54,8 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview", ...@@ -54,8 +54,8 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview",
title: messages.alreadyMember.title, title: messages.alreadyMember.title,
message: _.template( message: _.template(
messages.alreadyMember.messageTpl, messages.alreadyMember.messageTpl,
{email: email, container: containerName}, {interpolate: /\{(.+?)}/g})(
{interpolate: /\{(.+?)}/g} {email: email, container: containerName}
), ),
actions: { actions: {
primary: { primary: {
...@@ -140,7 +140,9 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview", ...@@ -140,7 +140,9 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview",
roles = _.object(_.pluck(this.roles, 'key'), _.pluck(this.roles, "name")), roles = _.object(_.pluck(this.roles, 'key'), _.pluck(this.roles, "name")),
adminRoleCount = this.getAdminRoleCount(), adminRoleCount = this.getAdminRoleCount(),
viewHelpers = { viewHelpers = {
format: function (template, data) { return _.template(template, data, {interpolate: /\{(.+?)}/g}); } format: function (template, data) {
return _.template(template, {interpolate: /\{(.+?)}/g})(data);
}
}; };
for (var i = 0; i < this.users.length; i++) { for (var i = 0; i < this.users.length; i++) {
var user = this.users[i], var user = this.users[i],
...@@ -284,8 +286,8 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview", ...@@ -284,8 +286,8 @@ define(['jquery', 'underscore', 'gettext', "js/views/baseview",
title: self.messages.deleteUser.title, title: self.messages.deleteUser.title,
message: _.template( message: _.template(
self.messages.deleteUser.messageTpl, self.messages.deleteUser.messageTpl,
{email: email, container: self.containerName}, {interpolate: /\{(.+?)}/g})(
{interpolate: /\{(.+?)}/g} {email: email, container: self.containerName}
), ),
actions: { actions: {
primary: { primary: {
......
...@@ -22,9 +22,7 @@ define(["underscore", "backbone", "gettext", "text!templates/paging-header.under ...@@ -22,9 +22,7 @@ define(["underscore", "backbone", "gettext", "text!templates/paging-header.under
currentPage = collection.currentPage, currentPage = collection.currentPage,
lastPage = collection.totalPages - 1, lastPage = collection.totalPages - 1,
messageHtml = this.messageHtml(); messageHtml = this.messageHtml();
this.$el.html(_.template(paging_header_template, { this.$el.html(_.template(paging_header_template)({ messageHtml: messageHtml}));
messageHtml: messageHtml
}));
this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0); this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0);
this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage); this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage);
return this; return this;
......
...@@ -27,10 +27,9 @@ define(["js/views/baseview", "underscore", "gettext", "common/js/components/view ...@@ -27,10 +27,9 @@ define(["js/views/baseview", "underscore", "gettext", "common/js/components/view
}, },
confirmDelete: function(e) { confirmDelete: function(e) {
if(e && e.preventDefault) { e.preventDefault(); } if(e && e.preventDefault) { e.preventDefault(); }
var textbook = this.model, collection = this.model.collection; var textbook = this.model;
var msg = new PromptView.Warning({ new PromptView.Warning({
title: _.template( title: _.template(gettext("Delete “<%= name %>”?"))(
gettext("Delete “<%= name %>”?"),
{name: textbook.get('name')} {name: textbook.get('name')}
), ),
message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."), message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."),
......
...@@ -18,7 +18,9 @@ function($, Backbone, _, Utils) { ...@@ -18,7 +18,9 @@ function($, Backbone, _, Utils) {
uploadTpl: '#file-upload', uploadTpl: '#file-upload',
initialize: function () { initialize: function () {
_.bindAll(this); _.bindAll(this,
'changeHandler', 'clickHandler', 'xhrResetProgressBar', 'xhrProgressHandler', 'xhrCompleteHandler'
);
this.file = false; this.file = false;
this.render(); this.render();
......
...@@ -29,7 +29,9 @@ function($, Backbone, _, Utils, FileUploader, gettext) { ...@@ -29,7 +29,9 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
}, },
initialize: function () { initialize: function () {
_.bindAll(this); _.bindAll(this,
'importHandler', 'replaceHandler', 'chooseHandler', 'useExistingHandler', 'showError', 'hideError'
);
this.component_locator = this.$el.closest('[data-locator]').data('locator'); this.component_locator = this.$el.closest('[data-locator]').data('locator');
......
...@@ -78,6 +78,8 @@ src_paths: ...@@ -78,6 +78,8 @@ src_paths:
- js/certificates - js/certificates
- js/factories - js/factories
- common/js - common/js
- edx-pattern-library/js
- edx-ui-toolkit/js
# Paths to spec (test) JavaScript files # Paths to spec (test) JavaScript files
# We should define the custom path mapping in /coffee/spec/main.coffee as well e.g. certificates etc. # We should define the custom path mapping in /coffee/spec/main.coffee as well e.g. certificates etc.
......
...@@ -73,6 +73,8 @@ src_paths: ...@@ -73,6 +73,8 @@ src_paths:
- js/utils - js/utils
- js/views - js/views
- common/js - common/js
- edx-pattern-library/js
- edx-ui-toolkit/js
# Paths to spec (test) JavaScript files # Paths to spec (test) JavaScript files
spec_paths: spec_paths:
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<h3 class="title"> <h3 class="title">
<a href="#" class="toggle group-toggle <% if (showContentGroupUsages){ print('hide'); } else { print('show'); } %>-groups"> <a href="#" class="toggle group-toggle <% if (showContentGroupUsages){ print('hide'); } else { print('show'); } %>-groups">
<i class="ui-toggle-expansion icon fa fa-caret-<% if (showContentGroupUsages){ print('down'); } else { print('right'); } %>"></i> <i class="ui-toggle-expansion icon fa fa-caret-<% if (showContentGroupUsages){ print('down'); } else { print('right'); } %>"></i>
<%= name %> <%- name %>
</a> </a>
</h3> </h3>
</header> </header>
...@@ -11,28 +11,28 @@ ...@@ -11,28 +11,28 @@
<ol class="collection-info group-configuration-info group-configuration-info-<% if(showContentGroupUsages){ print('block'); } else { print('inline'); } %>"> <ol class="collection-info group-configuration-info group-configuration-info-<% if(showContentGroupUsages){ print('block'); } else { print('inline'); } %>">
<% if (!_.isUndefined(id)) { %> <% if (!_.isUndefined(id)) { %>
<li class="group-configuration-id" <li class="group-configuration-id"
><span class="group-configuration-label"><%= gettext('ID') %>: </span ><span class="group-configuration-label"><%- gettext('ID') %>: </span
><span class="group-configuration-value"><%= id %></span ><span class="group-configuration-value"><%- id %></span
></li> ></li>
<% } %> <% } %>
<% if (!showContentGroupUsages) { %> <% if (!showContentGroupUsages) { %>
<li class="group-configuration-usage-count"> <li class="group-configuration-usage-count">
<%= usageCountMessage %> <%- usageCountMessage %>
</li> </li>
<% } %> <% } %>
</ol> </ol>
<ul class="actions group-configuration-actions"> <ul class="actions group-configuration-actions">
<li class="action action-edit"> <li class="action action-edit">
<button class="edit"><i class="icon fa fa-pencil"></i> <%= gettext("Edit") %></button> <button class="edit"><i class="icon fa fa-pencil"></i> <%- gettext("Edit") %></button>
</li> </li>
<% if (_.isEmpty(usage)) { %> <% if (_.isEmpty(usage)) { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%= gettext('Delete') %>"> <li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Delete') %>">
<button class="delete action-icon"><i class="icon fa fa-trash-o"></i><span><%= gettext("Delete") %></span></button> <button class="delete action-icon"><i class="icon fa fa-trash-o"></i><span><%- gettext("Delete") %></span></button>
</li> </li>
<% } else { %> <% } else { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%= gettext('Cannot delete when in use by a unit') %>"> <li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by a unit') %>">
<button class="delete action-icon is-disabled" aria-disabled="true" disabled="disabled"><i class="icon fa fa-trash-o"></i><span><%= gettext("Delete") %></span></button> <button class="delete action-icon is-disabled" aria-disabled="true" disabled="disabled"><i class="icon fa fa-trash-o"></i><span><%- gettext("Delete") %></span></button>
</li> </li>
<% } %> <% } %>
</ul> </ul>
...@@ -41,17 +41,18 @@ ...@@ -41,17 +41,18 @@
<% if (showContentGroupUsages) { %> <% if (showContentGroupUsages) { %>
<div class="collection-references wrapper-group-configuration-usages"> <div class="collection-references wrapper-group-configuration-usages">
<% if (!_.isEmpty(usage)) { %> <% if (!_.isEmpty(usage)) { %>
<h4 class="intro group-configuration-usage-text"><%= gettext('This content group is used in:') %></h4> <h4 class="intro group-configuration-usage-text"><%- gettext('This content group is used in:') %></h4>
<ol class="usage group-configuration-usage"> <ol class="usage group-configuration-usage">
<% _.each(usage, function(unit) { %> <% _.each(usage, function(unit) { %>
<li class="usage-unit group-configuration-usage-unit"> <li class="usage-unit group-configuration-usage-unit">
<p><a href=<%= unit.url %> ><%= unit.label %></a></p> <p><a href=<%- unit.url %> ><%- unit.label %></a></p>
</li> </li>
<% }) %> <% }) %>
</ol> </ol>
<% } else { %> <% } else { %>
<p class="group-configuration-usage-text"> <p class="group-configuration-usage-text">
<%= outlineAnchorMessage %> <!-- This contains an anchor link and therefore can't be escaped. -->
<%= outlineAnchorMessage %>
</p> </p>
<% } %> <% } %>
</div> </div>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<h3 class="title group-configuration-title"> <h3 class="title group-configuration-title">
<a href="#" class="toggle group-toggle <% if(showGroups){ print('hide'); } else { print('show'); } %>-groups"> <a href="#" class="toggle group-toggle <% if(showGroups){ print('hide'); } else { print('show'); } %>-groups">
<i class="ui-toggle-expansion icon fa fa-caret-<% if(showGroups){ print('down'); } else { print('right'); } %>"></i> <i class="ui-toggle-expansion icon fa fa-caret-<% if(showGroups){ print('down'); } else { print('right'); } %>"></i>
<%= name %> <%- name %>
</a> </a>
</h3> </h3>
</header> </header>
...@@ -11,20 +11,20 @@ ...@@ -11,20 +11,20 @@
<ol class="collection-info group-configuration-info group-configuration-info-<% if(showGroups){ print('block'); } else { print('inline'); } %>"> <ol class="collection-info group-configuration-info group-configuration-info-<% if(showGroups){ print('block'); } else { print('inline'); } %>">
<% if (!_.isUndefined(id)) { %> <% if (!_.isUndefined(id)) { %>
<li class="group-configuration-id" <li class="group-configuration-id"
><span class="group-configuration-label"><%= gettext('ID') %>: </span ><span class="group-configuration-label"><%- gettext('ID') %>: </span
><span class="group-configuration-value"><%= id %></span ><span class="group-configuration-value"><%- id %></span
></li> ></li>
<% } %> <% } %>
<% if (showGroups) { %> <% if (showGroups) { %>
<li class="collection-description group-configuration-description"> <li class="collection-description group-configuration-description">
<%= description %> <%- description %>
</li> </li>
<% } else { %> <% } else { %>
<li class="group-configuration-groups-count"> <li class="group-configuration-groups-count">
<%= groupsCountMessage %> <%- groupsCountMessage %>
</li> </li>
<li class="group-configuration-usage-count"> <li class="group-configuration-usage-count">
<%= usageCountMessage %> <%- usageCountMessage %>
</li> </li>
<% } %> <% } %>
</ol> </ol>
...@@ -34,23 +34,23 @@ ...@@ -34,23 +34,23 @@
<ol class="collection-items groups groups-<%= index %>"> <ol class="collection-items groups groups-<%= index %>">
<% groups.each(function(group, groupIndex) { %> <% groups.each(function(group, groupIndex) { %>
<li class="item group group-<%= groupIndex %>"> <li class="item group group-<%= groupIndex %>">
<span class="name group-name"><%= group.get('name') %></span> <span class="name group-name"><%- group.get('name') %></span>
<span class="meta group-allocation"><%= allocation %>%</span> <span class="meta group-allocation"><%- allocation %>%</span>
</li> </li>
<% }) %> <% }) %>
</ol> </ol>
<% } %> <% } %>
<ul class="actions group-configuration-actions"> <ul class="actions group-configuration-actions">
<li class="action action-edit"> <li class="action action-edit">
<button class="edit"><i class="icon fa fa-pencil"></i> <%= gettext("Edit") %></button> <button class="edit"><i class="icon fa fa-pencil"></i> <%- gettext("Edit") %></button>
</li> </li>
<% if (_.isEmpty(usage)) { %> <% if (_.isEmpty(usage)) { %>
<li class="action action-delete wrapper-delete-button"> <li class="action action-delete wrapper-delete-button">
<button class="delete action-icon"><i class="icon fa fa-trash-o"></i><span><%= gettext("Delete") %></span></button> <button class="delete action-icon"><i class="icon fa fa-trash-o"></i><span><%- gettext("Delete") %></span></button>
</li> </li>
<% } else { %> <% } else { %>
<li class="action action-delete wrapper-delete-button" data-tooltip="<%= gettext('Cannot delete when in use by an experiment') %>"> <li class="action action-delete wrapper-delete-button" data-tooltip="<%- gettext('Cannot delete when in use by an experiment') %>">
<button class="delete action-icon is-disabled" aria-disabled="true"><i class="icon fa fa-trash-o"></i><span><%= gettext("Delete") %></span></button> <button class="delete action-icon is-disabled" aria-disabled="true"><i class="icon fa fa-trash-o"></i><span><%- gettext("Delete") %></span></button>
</li> </li>
<% } %> <% } %>
</ul> </ul>
...@@ -58,11 +58,11 @@ ...@@ -58,11 +58,11 @@
<% if(showGroups) { %> <% if(showGroups) { %>
<div class="collection-references wrapper-group-configuration-usages"> <div class="collection-references wrapper-group-configuration-usages">
<% if (!_.isEmpty(usage)) { %> <% if (!_.isEmpty(usage)) { %>
<h4 class="intro group-configuration-usage-text"><%= gettext('This Group Configuration is used in:') %></h4> <h4 class="intro group-configuration-usage-text"><%- gettext('This Group Configuration is used in:') %></h4>
<ol class="usage group-configuration-usage"> <ol class="usage group-configuration-usage">
<% _.each(usage, function(unit) { %> <% _.each(usage, function(unit) { %>
<li class="usage-unit group-configuration-usage-unit"> <li class="usage-unit group-configuration-usage-unit">
<p><a href=<%= unit.url %> ><%= unit.label %></a></p> <p><a href=<%- unit.url %> ><%- unit.label %></a></p>
<% if (unit.validation) { %> <% if (unit.validation) { %>
<p> <p>
<% if (unit.validation.type === 'warning') { %> <% if (unit.validation.type === 'warning') { %>
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
<i class="icon fa fa-exclamation-circle"></i> <i class="icon fa fa-exclamation-circle"></i>
<% } %> <% } %>
<span class="usage-validation-message group-configuration-validation-message"> <span class="usage-validation-message group-configuration-validation-message">
<%= unit.validation.text %> <%- unit.validation.text %>
</span> </span>
</p> </p>
<% } %> <% } %>
...@@ -80,6 +80,7 @@ ...@@ -80,6 +80,7 @@
</ol> </ol>
<% } else { %> <% } else { %>
<p class="group-configuration-usage-text"> <p class="group-configuration-usage-text">
<!-- This contains an anchor link and therefore can't be escaped. -->
<%= outlineAnchorMessage %> <%= outlineAnchorMessage %>
</p> </p>
<% } %> <% } %>
......
...@@ -2,17 +2,17 @@ ...@@ -2,17 +2,17 @@
<div class "error-header"> <div class "error-header">
<p> <p>
<%= _.template( <%= _.template(
ngettext( ngettext(
"There was {strong_start}{num_errors} validation error{strong_end} while trying to save the course settings in the database.", "There was {strong_start}{num_errors} validation error{strong_end} while trying to save the course settings in the database.",
"There were {strong_start}{num_errors} validation errors{strong_end} while trying to save the course settings in the database.", "There were {strong_start}{num_errors} validation errors{strong_end} while trying to save the course settings in the database.",
num_errors num_errors
), ),
{ {interpolate: /\{(.+?)\}/g})(
strong_start:'<strong>', {
num_errors: num_errors, strong_start:'<strong>',
strong_end: '</strong>' num_errors: num_errors,
}, strong_end: '</strong>'
{interpolate: /\{(.+?)\}/g})%> })%>
<%= gettext("Please check the following validation feedbacks and reflect them in your course settings:")%></p> <%= gettext("Please check the following validation feedbacks and reflect them in your course settings:")%></p>
</div> </div>
......
...@@ -89,7 +89,7 @@ class ChooseModeView(View): ...@@ -89,7 +89,7 @@ class ChooseModeView(View):
has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active) has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
if CourseMode.has_professional_mode(modes) and not has_enrolled_professional: if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
redirect_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) redirect_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})
if ecommerce_service.is_enabled(request): if ecommerce_service.is_enabled(request.user):
professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL) professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL)
if professional_mode.sku: if professional_mode.sku:
redirect_url = ecommerce_service.checkout_page_url(professional_mode.sku) redirect_url = ecommerce_service.checkout_page_url(professional_mode.sku)
...@@ -158,7 +158,7 @@ class ChooseModeView(View): ...@@ -158,7 +158,7 @@ class ChooseModeView(View):
context["verified_description"] = verified_mode.description context["verified_description"] = verified_mode.description
if verified_mode.sku: if verified_mode.sku:
context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request) context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user)
context["ecommerce_payment_page"] = ecommerce_service.payment_page_url() context["ecommerce_payment_page"] = ecommerce_service.payment_page_url()
context["sku"] = verified_mode.sku context["sku"] = verified_mode.sku
......
...@@ -743,7 +743,7 @@ def dashboard(request): ...@@ -743,7 +743,7 @@ def dashboard(request):
} }
ecommerce_service = EcommerceService() ecommerce_service = EcommerceService()
if ecommerce_service.is_enabled(request): if ecommerce_service.is_enabled(request.user):
context.update({ context.update({
'use_ecommerce_payment_flow': True, 'use_ecommerce_payment_flow': True,
'ecommerce_payment_page': ecommerce_service.payment_page_url(), 'ecommerce_payment_page': ecommerce_service.payment_page_url(),
......
...@@ -361,9 +361,14 @@ function (VideoPlayer) { ...@@ -361,9 +361,14 @@ function (VideoPlayer) {
describe('onSeek', function () { describe('onSeek', function () {
beforeEach(function () { beforeEach(function () {
// jasmine.Clock can't be used to fake out debounce with newer versions of underscore
spyOn(_, 'debounce').andCallFake(function (func) {
return function () {
func.apply(this, arguments);
};
});
state = jasmine.initializePlayer(); state = jasmine.initializePlayer();
state.videoEl = $('video, iframe'); state.videoEl = $('video, iframe');
jasmine.Clock.useMock();
spyOn(state.videoPlayer, 'duration').andReturn(120); spyOn(state.videoPlayer, 'duration').andReturn(120);
}); });
...@@ -384,9 +389,6 @@ function (VideoPlayer) { ...@@ -384,9 +389,6 @@ function (VideoPlayer) {
spyOn(state.videoPlayer, 'stopTimer'); spyOn(state.videoPlayer, 'stopTimer');
spyOn(state.videoPlayer, 'runTimer'); spyOn(state.videoPlayer, 'runTimer');
state.videoPlayer.seekTo(10); state.videoPlayer.seekTo(10);
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
// That's why we have to do this tick(300).
jasmine.Clock.tick(300);
expect(state.videoPlayer.currentTime).toBe(10); expect(state.videoPlayer.currentTime).toBe(10);
expect(state.videoPlayer.stopTimer).toHaveBeenCalled(); expect(state.videoPlayer.stopTimer).toHaveBeenCalled();
expect(state.videoPlayer.runTimer).toHaveBeenCalled(); expect(state.videoPlayer.runTimer).toHaveBeenCalled();
...@@ -399,9 +401,6 @@ function (VideoPlayer) { ...@@ -399,9 +401,6 @@ function (VideoPlayer) {
state.videoProgressSlider.onSlide( state.videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 30 } jQuery.Event('slide'), { value: 30 }
); );
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
// That's why we have to do this tick(300).
jasmine.Clock.tick(300);
expect(state.videoPlayer.currentTime).toBe(30); expect(state.videoPlayer.currentTime).toBe(30);
expect(state.videoPlayer.player.seekTo).toHaveBeenCalledWith(30, true); expect(state.videoPlayer.player.seekTo).toHaveBeenCalledWith(30, true);
}); });
...@@ -413,9 +412,6 @@ function (VideoPlayer) { ...@@ -413,9 +412,6 @@ function (VideoPlayer) {
state.videoProgressSlider.onSlide( state.videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 30 } jQuery.Event('slide'), { value: 30 }
); );
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
// That's why we have to do this tick(300).
jasmine.Clock.tick(300);
expect(state.videoPlayer.currentTime).toBe(30); expect(state.videoPlayer.currentTime).toBe(30);
expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(30, true); expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(30, true);
}); });
...@@ -426,17 +422,11 @@ function (VideoPlayer) { ...@@ -426,17 +422,11 @@ function (VideoPlayer) {
state.videoProgressSlider.onSlide( state.videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 20 } jQuery.Event('slide'), { value: 20 }
); );
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
// That's why we have to do this tick(300).
jasmine.Clock.tick(300);
state.videoPlayer.pause(); state.videoPlayer.pause();
expect(state.videoPlayer.currentTime).toBe(20); expect(state.videoPlayer.currentTime).toBe(20);
state.videoProgressSlider.onSlide( state.videoProgressSlider.onSlide(
jQuery.Event('slide'), { value: 10 } jQuery.Event('slide'), { value: 10 }
); );
// Video player uses _.debounce (with a wait time in 300 ms) for seeking.
// That's why we have to do this tick(300).
jasmine.Clock.tick(300);
expect(state.videoPlayer.currentTime).toBe(10); expect(state.videoPlayer.currentTime).toBe(10);
}); });
......
...@@ -13,7 +13,7 @@ var setupFullScreenModal = function() { ...@@ -13,7 +13,7 @@ var setupFullScreenModal = function() {
"largeALT": smallImageObject.attr('alt'), "largeALT": smallImageObject.attr('alt'),
"largeSRC": largeImageSRC "largeSRC": largeImageSRC
}; };
var html = _.template($("#image-modal-tpl").text(), data); var html = _.template($("#image-modal-tpl").text())(data);
$(this).replaceWith(html); $(this).replaceWith(html);
} }
}); });
......
...@@ -1160,7 +1160,7 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead): ...@@ -1160,7 +1160,7 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead):
contentstore=None, contentstore=None,
doc_store_config=None, # ignore if passed up doc_store_config=None, # ignore if passed up
metadata_inheritance_cache_subsystem=None, request_cache=None, metadata_inheritance_cache_subsystem=None, request_cache=None,
xblock_mixins=(), xblock_select=None, disabled_xblock_types=(), # pylint: disable=bad-continuation xblock_mixins=(), xblock_select=None, xblock_field_data_wrappers=(), disabled_xblock_types=(), # pylint: disable=bad-continuation
# temporary parms to enable backward compatibility. remove once all envs migrated # temporary parms to enable backward compatibility. remove once all envs migrated
db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None, db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None,
# allow lower level init args to pass harmlessly # allow lower level init args to pass harmlessly
...@@ -1177,6 +1177,7 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead): ...@@ -1177,6 +1177,7 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead):
self.request_cache = request_cache self.request_cache = request_cache
self.xblock_mixins = xblock_mixins self.xblock_mixins = xblock_mixins
self.xblock_select = xblock_select self.xblock_select = xblock_select
self.xblock_field_data_wrappers = xblock_field_data_wrappers
self.disabled_xblock_types = disabled_xblock_types self.disabled_xblock_types = disabled_xblock_types
self.contentstore = contentstore self.contentstore = contentstore
......
...@@ -120,11 +120,25 @@ def load_function(path): ...@@ -120,11 +120,25 @@ def load_function(path):
""" """
Load a function by name. Load a function by name.
path is a string of the form "path.to.module.function" Arguments:
returns the imported python object `function` from `path.to.module` path: String of the form 'path.to.module.function'. Strings of the form
'path.to.module:Class.function' are also valid.
Returns:
The imported object 'function'.
""" """
module_path, _, name = path.rpartition('.') if ':' in path:
return getattr(import_module(module_path), name) module_path, _, method_path = path.rpartition(':')
module = import_module(module_path)
class_name, method_name = method_path.split('.')
_class = getattr(module, class_name)
function = getattr(_class, method_name)
else:
module_path, _, name = path.rpartition('.')
function = getattr(import_module(module_path), name)
return function
def create_modulestore_instance( def create_modulestore_instance(
...@@ -179,12 +193,15 @@ def create_modulestore_instance( ...@@ -179,12 +193,15 @@ def create_modulestore_instance(
else: else:
disabled_xblock_types = () disabled_xblock_types = ()
xblock_field_data_wrappers = [load_function(path) for path in settings.XBLOCK_FIELD_DATA_WRAPPERS]
return class_( return class_(
contentstore=content_store, contentstore=content_store,
metadata_inheritance_cache_subsystem=metadata_inheritance_cache, metadata_inheritance_cache_subsystem=metadata_inheritance_cache,
request_cache=request_cache, request_cache=request_cache,
xblock_mixins=getattr(settings, 'XBLOCK_MIXINS', ()), xblock_mixins=getattr(settings, 'XBLOCK_MIXINS', ()),
xblock_select=getattr(settings, 'XBLOCK_SELECT_FUNCTION', None), xblock_select=getattr(settings, 'XBLOCK_SELECT_FUNCTION', None),
xblock_field_data_wrappers=xblock_field_data_wrappers,
disabled_xblock_types=disabled_xblock_types, disabled_xblock_types=disabled_xblock_types,
doc_store_config=doc_store_config, doc_store_config=doc_store_config,
i18n_service=i18n_service or ModuleI18nService(), i18n_service=i18n_service or ModuleI18nService(),
......
...@@ -218,6 +218,17 @@ class InheritanceMixin(XBlockMixin): ...@@ -218,6 +218,17 @@ class InheritanceMixin(XBlockMixin):
default=False default=False
) )
self_paced = Boolean(
display_name=_('Self Paced'),
help=_(
'Set this to "true" to mark this course as self-paced. Self-paced courses do not have '
'due dates for assignments, and students can progress through the course at any rate before '
'the course ends.'
),
default=False,
scope=Scope.settings
)
def compute_inherited_metadata(descriptor): def compute_inherited_metadata(descriptor):
"""Given a descriptor, traverse all of its descendants and do metadata """Given a descriptor, traverse all of its descendants and do metadata
......
...@@ -12,27 +12,24 @@ structure: ...@@ -12,27 +12,24 @@ structure:
} }
""" """
import pymongo
import sys
import logging
import copy import copy
from datetime import datetime
from importlib import import_module
import logging
import pymongo
import re import re
import sys
from uuid import uuid4 from uuid import uuid4
from bson.son import SON from bson.son import SON
from datetime import datetime from contracts import contract, new_contract
from fs.osfs import OSFS from fs.osfs import OSFS
from mongodb_proxy import autoretry_read from mongodb_proxy import autoretry_read
from path import Path as path
from pytz import UTC
from contracts import contract, new_contract
from importlib import import_module
from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey
from opaque_keys.edx.locations import Location, BlockUsageLocator from opaque_keys.edx.locations import Location, BlockUsageLocator, SlashSeparatedCourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocator from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from path import Path as path
from pytz import UTC
from xblock.core import XBlock from xblock.core import XBlock
from xblock.exceptions import InvalidScopeError from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict
...@@ -54,6 +51,7 @@ from xmodule.modulestore.xml import CourseLocationManager ...@@ -54,6 +51,7 @@ from xmodule.modulestore.xml import CourseLocationManager
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
from xmodule.services import SettingsService from xmodule.services import SettingsService
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
new_contract('CourseKey', CourseKey) new_contract('CourseKey', CourseKey)
...@@ -318,6 +316,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -318,6 +316,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
).replace(tzinfo=UTC) ).replace(tzinfo=UTC)
module._edit_info['published_by'] = raw_metadata.get('published_by') module._edit_info['published_by'] = raw_metadata.get('published_by')
for wrapper in self.modulestore.xblock_field_data_wrappers:
module._field_data = wrapper(module, module._field_data) # pylint: disable=protected-access
# decache any computed pending field settings # decache any computed pending field settings
module.save() module.save()
return module return module
......
import sys import sys
import logging import logging
from contracts import contract, new_contract from contracts import contract, new_contract
from fs.osfs import OSFS from fs.osfs import OSFS
from lazy import lazy from lazy import lazy
...@@ -7,6 +8,7 @@ from xblock.runtime import KvsFieldData, KeyValueStore ...@@ -7,6 +8,7 @@ from xblock.runtime import KvsFieldData, KeyValueStore
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xblock.core import XBlock from xblock.core import XBlock
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, LibraryLocator, DefinitionLocator from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, LibraryLocator, DefinitionLocator
from xmodule.library_tools import LibraryToolsService from xmodule.library_tools import LibraryToolsService
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -263,6 +265,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -263,6 +265,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
module.update_version = edit_info.update_version module.update_version = edit_info.update_version
module.source_version = edit_info.source_version module.source_version = edit_info.source_version
module.definition_locator = DefinitionLocator(block_key.type, definition_id) module.definition_locator = DefinitionLocator(block_key.type, definition_id)
for wrapper in self.modulestore.xblock_field_data_wrappers:
module._field_data = wrapper(module, module._field_data) # pylint: disable=protected-access
# decache any pending field settings # decache any pending field settings
module.save() module.save()
......
...@@ -589,6 +589,20 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -589,6 +589,20 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
return xml return xml
def create_youtube_url(self, youtube_id):
"""
Args:
youtube_id: The ID of the video to create a link for
Returns:
A full youtube url to the video whose ID is passed in
"""
if youtube_id:
return 'https://www.youtube.com/watch?v={0}'.format(youtube_id)
else:
return ''
def get_context(self): def get_context(self):
""" """
Extend context by data for transcript basic tab. Extend context by data for transcript basic tab.
...@@ -612,10 +626,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -612,10 +626,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
if val_youtube_id: if val_youtube_id:
video_id = val_youtube_id video_id = val_youtube_id
if video_id: return self.create_youtube_url(video_id)
return 'http://youtu.be/{0}'.format(video_id)
else:
return ''
_ = self.runtime.service(self, "i18n").ugettext _ = self.runtime.service(self, "i18n").ugettext
video_url.update({ video_url.update({
...@@ -848,7 +859,8 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -848,7 +859,8 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
val_video_data = edxval_api.get_video_info(self.edx_video_id) val_video_data = edxval_api.get_video_info(self.edx_video_id)
# Unfortunately, the VAL API is inconsistent in how it returns the encodings, so remap here. # Unfortunately, the VAL API is inconsistent in how it returns the encodings, so remap here.
for enc_vid in val_video_data.pop('encoded_videos'): for enc_vid in val_video_data.pop('encoded_videos'):
encoded_videos[enc_vid['profile']] = {key: enc_vid[key] for key in ["url", "file_size"]} if enc_vid['profile'] in video_profile_names:
encoded_videos[enc_vid['profile']] = {key: enc_vid[key] for key in ["url", "file_size"]}
except edxval_api.ValVideoNotFoundError: except edxval_api.ValVideoNotFoundError:
pass pass
...@@ -861,6 +873,14 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -861,6 +873,14 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
"file_size": 0, # File size is unknown for fallback URLs "file_size": 0, # File size is unknown for fallback URLs
} }
# Include youtube link if there is no encoding for mobile- ie only a fallback URL or no encodings at all
# We are including a fallback URL for older versions of the mobile app that don't handle Youtube urls
if self.youtube_id_1_0:
encoded_videos["youtube"] = {
"url": self.create_youtube_url(self.youtube_id_1_0),
"file_size": 0, # File size is not relevant for external link
}
transcripts_info = self.get_transcripts_info() transcripts_info = self.get_transcripts_info()
transcripts = { transcripts = {
lang: self.runtime.handler_url(self, 'transcript', 'download', query="lang=" + lang, thirdparty=True) lang: self.runtime.handler_url(self, 'transcript', 'download', query="lang=" + lang, thirdparty=True)
......
...@@ -118,7 +118,7 @@ describe "ThreadResponseShowView", -> ...@@ -118,7 +118,7 @@ describe "ThreadResponseShowView", ->
expect(@view.$(".posted-details").text()).not.toMatch("marked as answer") expect(@view.$(".posted-details").text()).not.toMatch("marked as answer")
it "allows a moderator to mark an answer in a question thread", -> it "allows a moderator to mark an answer in a question thread", ->
DiscussionUtil.loadRoles({"Moderator": parseInt(window.user.id)}) DiscussionUtil.loadRoles({"Moderator": [parseInt(window.user.id)]})
@thread.set({ @thread.set({
"thread_type": "question", "thread_type": "question",
"user_id": (parseInt(window.user.id) + 1).toString() "user_id": (parseInt(window.user.id) + 1).toString()
......
class @DiscussionFilter
# TODO: this helper class duplicates functionality in DiscussionThreadListView.filterTopics
# for use with a very similar category dropdown in the New Post form. The two menus' implementations
# should be merged into a single reusable view.
@filterDrop: (e) ->
$drop = $(e.target).parents('.topic-menu-wrapper')
query = $(e.target).val()
$items = $drop.find('.topic-menu-item')
if(query.length == 0)
$items.removeClass('hidden')
return;
$items.addClass('hidden')
$items.each (i) ->
path = $(this).parents(".topic-menu-item").andSelf()
pathTitles = path.children(".topic-title").map((i, elem) -> $(elem).text()).get()
pathText = pathTitles.join(" / ").toLowerCase()
if query.split(" ").every((term) -> pathText.search(term.toLowerCase()) != -1)
$(this).removeClass('hidden')
# show children
$(this).find('.topic-menu-item').removeClass('hidden');
# show parents
$(this).parents('.topic-menu-item').removeClass('hidden');
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
this.threadType = this.model.get('thread_type'); this.threadType = this.model.get('thread_type');
this.topicId = this.model.get('commentable_id'); this.topicId = this.model.get('commentable_id');
this.context = options.context || 'course'; this.context = options.context || 'course';
_.bindAll(this); _.bindAll(this, 'updateHandler', 'cancelHandler');
return this; return this;
}, },
......
...@@ -39,9 +39,9 @@ if Backbone? ...@@ -39,9 +39,9 @@ if Backbone?
@searchAlertCollection.on "add", (searchAlert) => @searchAlertCollection.on "add", (searchAlert) =>
content = _.template( content = _.template(
$("#search-alert-template").html(), $("#search-alert-template").html())(
{'message': searchAlert.attributes.message, 'cid': searchAlert.cid} {'message': searchAlert.attributes.message, 'cid': searchAlert.cid}
) )
@$(".search-alerts").append(content) @$(".search-alerts").append(content)
@$("#search-alert-" + searchAlert.cid + " a.dismiss").bind "click", searchAlert, (event) => @$("#search-alert-" + searchAlert.cid + " a.dismiss").bind "click", searchAlert, (event) =>
@removeSearchAlert(event.data.cid) @removeSearchAlert(event.data.cid)
...@@ -491,7 +491,7 @@ if Backbone? ...@@ -491,7 +491,7 @@ if Backbone?
message = interpolate( message = interpolate(
_.escape(gettext('Show posts by %(username)s.')), _.escape(gettext('Show posts by %(username)s.')),
{"username": {"username":
_.template('<a class="link-jump" href="<%= url %>"><%- username %></a>', { _.template('<a class="link-jump" href="<%= url %>"><%- username %></a>')({
url: DiscussionUtil.urlFor("user_profile", response.users[0].id), url: DiscussionUtil.urlFor("user_profile", response.users[0].id),
username: response.users[0].username username: response.users[0].username
}) })
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
'click .post-topic-button': 'toggleTopicDropdown', 'click .post-topic-button': 'toggleTopicDropdown',
'click .topic-menu-wrapper': 'handleTopicEvent', 'click .topic-menu-wrapper': 'handleTopicEvent',
'click .topic-filter-label': 'ignoreClick', 'click .topic-filter-label': 'ignoreClick',
'keyup .topic-filter-input': this.DiscussionFilter.filterDrop 'keyup .topic-filter-input': 'filterDrop'
}, },
attributes: { attributes: {
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
this.course_settings = options.course_settings; this.course_settings = options.course_settings;
this.currentTopicId = options.topicId; this.currentTopicId = options.topicId;
this.maxNameWidth = 100; this.maxNameWidth = 100;
_.bindAll(this); _.bindAll(this,
'toggleTopicDropdown', 'handleTopicEvent', 'hideTopicDropdown', 'ignoreClick'
);
return this; return this;
}, },
...@@ -34,7 +36,7 @@ ...@@ -34,7 +36,7 @@
render: function() { render: function() {
var context = _.clone(this.course_settings.attributes); var context = _.clone(this.course_settings.attributes);
context.topics_html = this.renderCategoryMap(this.course_settings.get('category_map')); context.topics_html = this.renderCategoryMap(this.course_settings.get('category_map'));
this.$el.html(_.template($('#topic-template').html(), context)); this.$el.html(_.template($('#topic-template').html())(context));
this.dropdownButton = this.$('.post-topic-button'); this.dropdownButton = this.$('.post-topic-button');
this.topicMenu = this.$('.topic-menu-wrapper'); this.topicMenu = this.$('.topic-menu-wrapper');
this.selectedTopic = this.$('.js-selected-topic'); this.selectedTopic = this.$('.js-selected-topic');
...@@ -187,6 +189,38 @@ ...@@ -187,6 +189,38 @@
} }
} }
return name; return name;
},
// TODO: this helper class duplicates functionality in DiscussionThreadListView.filterTopics
// for use with a very similar category dropdown in the New Post form. The two menus' implementations
// should be merged into a single reusable view.
filterDrop: function (e) {
var $drop, $items, query;
$drop = $(e.target).parents('.topic-menu-wrapper');
query = $(e.target).val();
$items = $drop.find('.topic-menu-item');
if (query.length === 0) {
$items.removeClass('hidden');
return;
}
$items.addClass('hidden');
$items.each(function (_index, item) {
var path, pathText, pathTitles;
path = $(item).parents(".topic-menu-item").andSelf();
pathTitles = path.children(".topic-title").map(function (_, elem) {
return $(elem).text();
}).get();
pathText = pathTitles.join(" / ").toLowerCase();
if (query.split(" ").every(function (term) {
return pathText.search(term.toLowerCase()) !== -1;
})) {
$(item).removeClass('hidden');
$(item).find('.topic-menu-item').removeClass('hidden');
$(item).parents('.topic-menu-item').removeClass('hidden');
}
});
} }
}); });
} }
......
...@@ -17,7 +17,7 @@ if Backbone? ...@@ -17,7 +17,7 @@ if Backbone?
mode: @mode, mode: @mode,
form_id: @mode + (if @topicId then "-" + @topicId else "") form_id: @mode + (if @topicId then "-" + @topicId else "")
}) })
@$el.html(_.template($("#new-post-template").html(), context)) @$el.html(_.template($("#new-post-template").html())(context))
threadTypeTemplate = _.template($("#thread-type-template").html()); threadTypeTemplate = _.template($("#thread-type-template").html());
if $('.js-group-select').is(':disabled') if $('.js-group-select').is(':disabled')
$('.group-selector-wrapper').addClass('disabled') $('.group-selector-wrapper').addClass('disabled')
......
...@@ -79,6 +79,9 @@ ...@@ -79,6 +79,9 @@
* underlying server API. * underlying server API.
*/ */
getPage: function () { getPage: function () {
// TODO: this.currentPage is currently returning a function sometimes when it is called.
// It is possible it always did this, but we either need to investigate more, or just wait until
// we replace this code with the pattern library.
return this.currentPage + (this.isZeroIndexed ? 1 : 0); return this.currentPage + (this.isZeroIndexed ? 1 : 0);
}, },
......
...@@ -244,7 +244,7 @@ ...@@ -244,7 +244,7 @@
if (!validateTotalKeyLength(key_field_selectors)) { if (!validateTotalKeyLength(key_field_selectors)) {
$(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding); $(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
$(selectors.errorMessage).html( $(selectors.errorMessage).html(
'<p>' + _.template(message_tpl, {limit: MAX_SUM_KEY_LENGTH}) + '</p>' '<p>' + _.template(message_tpl)({limit: MAX_SUM_KEY_LENGTH}) + '</p>'
); );
$(selectors.save).addClass(classes.disabled); $(selectors.save).addClass(classes.disabled);
} else { } else {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
'use strict'; 'use strict';
define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"], define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"],
function($, _, str, SystemFeedbackView) { function($, _, str, SystemFeedbackView) {
str = str || _.str;
var Alert = SystemFeedbackView.extend({ var Alert = SystemFeedbackView.extend({
options: $.extend({}, SystemFeedbackView.prototype.options, { options: $.extend({}, SystemFeedbackView.prototype.options, {
type: "alert" type: "alert"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
'use strict'; 'use strict';
define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"], define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"],
function($, _, str, SystemFeedbackView) { function($, _, str, SystemFeedbackView) {
str = str || _.str;
var Notification = SystemFeedbackView.extend({ var Notification = SystemFeedbackView.extend({
options: $.extend({}, SystemFeedbackView.prototype.options, { options: $.extend({}, SystemFeedbackView.prototype.options, {
type: "notification", type: "notification",
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
'use strict'; 'use strict';
define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"], define(["jquery", "underscore", "underscore.string", "common/js/components/views/feedback"],
function($, _, str, SystemFeedbackView) { function($, _, str, SystemFeedbackView) {
str = str || _.str;
var Prompt = SystemFeedbackView.extend({ var Prompt = SystemFeedbackView.extend({
options: $.extend({}, SystemFeedbackView.prototype.options, { options: $.extend({}, SystemFeedbackView.prototype.options, {
type: "prompt", type: "prompt",
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
}, },
render: function () { render: function () {
this.$el.html(_.template(paginatedViewTemplate, {type: this.type})); this.$el.html(_.template(paginatedViewTemplate)({type: this.type}));
this.assign(this.listView, '.' + this.type + '-list'); this.assign(this.listView, '.' + this.type + '-list');
if (this.headerView) { if (this.headerView) {
this.assign(this.headerView, '.' + this.type + '-paging-header'); this.assign(this.headerView, '.' + this.type + '-paging-header');
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
this.$el.removeClass('hidden'); this.$el.removeClass('hidden');
} }
} }
this.$el.html(_.template(paging_footer_template, { this.$el.html(_.template(paging_footer_template)({
current_page: this.collection.getPage(), current_page: this.collection.getPage(),
total_pages: this.collection.totalPages total_pages: this.collection.totalPages
})); }));
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
context, true context, true
); );
} }
this.$el.html(_.template(headerTemplate, { this.$el.html(_.template(headerTemplate)({
message: message, message: message,
srInfo: this.srInfo, srInfo: this.srInfo,
sortableFields: this.collection.sortableFields, sortableFields: this.collection.sortableFields,
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
}, },
render: function() { render: function() {
this.$el.html(_.template(searchFieldTemplate, { this.$el.html(_.template(searchFieldTemplate)({
type: this.type, type: this.type,
searchString: this.collection.searchString, searchString: this.collection.searchString,
searchLabel: this.label searchLabel: this.label
......
...@@ -15,9 +15,6 @@ ...@@ -15,9 +15,6 @@
* by the access view, but doing it here helps keep the * by the access view, but doing it here helps keep the
* utility self-contained. * utility self-contained.
*/ */
if (_.isUndefined(_s)) {
_s = _.str;
}
_.mixin( _s.exports() ); _.mixin( _s.exports() );
utils = (function(){ utils = (function(){
......
<% if (!readOnly) { %> <% if (!readOnly) { %>
<ul class="<%= contentType %>-actions-list"> <ul class="<%= contentType %>-actions-list">
<% _.each(primaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %> <% _.each(primaryActions, function(action) { print(_.template($('#forum-action-' + action).html())({})) }) %>
<li class="actions-item is-visible"> <li class="actions-item is-visible">
<div class="more-wrapper"> <div class="more-wrapper">
<a href="javascript:void(0)" class="action-button action-more" role="button" aria-haspopup="true" aria-controls="action-menu-<%= contentId %>"> <a href="javascript:void(0)" class="action-button action-more" role="button" aria-haspopup="true" aria-controls="action-menu-<%= contentId %>">
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
</a> </a>
<div class="actions-dropdown" id="action-menu-<%= contentType %>" aria-expanded="false"> <div class="actions-dropdown" id="action-menu-<%= contentType %>" aria-expanded="false">
<ul class="actions-dropdown-list"> <ul class="actions-dropdown-list">
<% _.each(secondaryActions, function(action) { print(_.template($('#forum-action-' + action).html(), {})) }) %> <% _.each(secondaryActions, function(action) { print(_.template($('#forum-action-' + action).html())({})) }) %>
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="response-body"><%- body %></div> <div class="response-body"><%- body %></div>
<%= <%=
_.template( _.template(
$('#forum-actions').html(), $('#forum-actions').html())(
{ {
contentId: cid, contentId: cid,
contentType: 'comment', contentType: 'comment',
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<div class="response-header-actions"> <div class="response-header-actions">
<%= <%=
_.template( _.template(
$('#forum-actions').html(), $('#forum-actions').html())(
{ {
contentId: cid, contentId: cid,
contentType: 'response', contentType: 'response',
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
<div class="post-header-actions post-extended-content"> <div class="post-header-actions post-extended-content">
<%= <%=
_.template( _.template(
$('#forum-actions').html(), $('#forum-actions').html())(
{ {
contentId: cid, contentId: cid,
contentType: 'post', contentType: 'post',
......
../../../node_modules/edx-pattern-library/pattern-library/fonts
\ No newline at end of file
../../../node_modules/edx-pattern-library/pattern-library/js
\ No newline at end of file
../../../node_modules/edx-ui-toolkit/src/js
\ No newline at end of file
...@@ -115,6 +115,27 @@ describe("Formula Equation Preview", function () { ...@@ -115,6 +115,27 @@ describe("Formula Equation Preview", function () {
]); ]);
}); });
it('does not request again if the initial request has already been made', function () {
// jshint undef:false
expect(Problem.inputAjax.callCount).toEqual(1);
// Reset the spy in order to check calls again.
Problem.inputAjax.reset();
// Enabling the formulaEquationPreview again to see if this will
// reinitialize input request once again.
formulaEquationPreview.enable();
// This part may be asynchronous, so wait.
waitsFor(function () {
return !Problem.inputAjax.wasCalled;
}, "times out in case of AJAX call", 1000);
// Expect Problem.inputAjax was not called as input request was
// initialized before.
expect(Problem.inputAjax).not.toHaveBeenCalled();
});
it('makes a request on user input', function () { it('makes a request on user input', function () {
Problem.inputAjax.reset(); Problem.inputAjax.reset();
$('#input_THE_ID').val('user_input').trigger('input'); $('#input_THE_ID').val('user_input').trigger('input');
......
...@@ -58,9 +58,17 @@ formulaEquationPreview.enable = function () { ...@@ -58,9 +58,17 @@ formulaEquationPreview.enable = function () {
throttledRequest(inputData, this.value); throttledRequest(inputData, this.value);
}; };
$this.on("input", initializeRequest); if (!$this.data("inputInitialized")) {
// Ask for initial preview. // Hack alert: since this javascript file is loaded every time a
initializeRequest.call(this); // problem with mathjax preview is loaded, we wrap this step in this
// condition to make sure we don't attach multiple event listeners
// per math input if multiple such problems are loaded on a page.
$this.on("input", initializeRequest);
// Ask for initial preview.
initializeRequest.call(this);
// indicates that the initial preview is done for current $this!
$this.data("inputInitialized", true);
}
} }
/** /**
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
*/ */
var interpolate_ntext = function (singular, plural, count, values) { var interpolate_ntext = function (singular, plural, count, values) {
var text = count === 1 ? singular : plural; var text = count === 1 ? singular : plural;
return _.template(text, values, {interpolate: /\{(.+?)\}/g}); return _.template(text, {interpolate: /\{(.+?)\}/g})(values);
}; };
this.interpolate_ntext = interpolate_ntext; this.interpolate_ntext = interpolate_ntext;
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
* @returns the text with placeholder values filled in * @returns the text with placeholder values filled in
*/ */
var interpolate_text = function (text, values) { var interpolate_text = function (text, values) {
return _.template(text, values, {interpolate: /\{(.+?)\}/g}); return _.template(text, {interpolate: /\{(.+?)\}/g})(values);
}; };
this.interpolate_text = interpolate_text; this.interpolate_text = interpolate_text;
}).call(this, _); }).call(this, _);
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
} }
this.hide(); this.hide();
_.bindAll(this); _.bindAll(this, 'show', 'hide', 'showTooltip', 'moveTooltip', 'hideTooltip', 'click');
this.bindEvents(); this.bindEvents();
}; };
......
...@@ -50,6 +50,8 @@ lib_paths: ...@@ -50,6 +50,8 @@ lib_paths:
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
- common/js - common/js
- edx-pattern-library/js
- edx-ui-toolkit/js
# Paths to spec (test) JavaScript files # Paths to spec (test) JavaScript files
spec_paths: spec_paths:
......
../../../node_modules/edx-pattern-library/pattern-library/fonts
\ No newline at end of file
../../../node_modules/edx-pattern-library/pattern-library/js
\ No newline at end of file
...@@ -3,50 +3,50 @@ ...@@ -3,50 +3,50 @@
// Official animation shorthand property. // Official animation shorthand property.
@mixin animation ($animations...) { @mixin animation ($animations...) {
@include prefixer(animation, $animations, webkit moz spec); @include prefixer(animation, $animations, spec);
} }
// Individual Animation Properties // Individual Animation Properties
@mixin animation-name ($names...) { @mixin animation-name ($names...) {
@include prefixer(animation-name, $names, webkit moz spec); @include prefixer(animation-name, $names, spec);
} }
@mixin animation-duration ($times...) { @mixin animation-duration ($times...) {
@include prefixer(animation-duration, $times, webkit moz spec); @include prefixer(animation-duration, $times, spec);
} }
@mixin animation-timing-function ($motions...) { @mixin animation-timing-function ($motions...) {
// ease | linear | ease-in | ease-out | ease-in-out // ease | linear | ease-in | ease-out | ease-in-out
@include prefixer(animation-timing-function, $motions, webkit moz spec); @include prefixer(animation-timing-function, $motions, spec);
} }
@mixin animation-iteration-count ($values...) { @mixin animation-iteration-count ($values...) {
// infinite | <number> // infinite | <number>
@include prefixer(animation-iteration-count, $values, webkit moz spec); @include prefixer(animation-iteration-count, $values, spec);
} }
@mixin animation-direction ($directions...) { @mixin animation-direction ($directions...) {
// normal | alternate // normal | alternate
@include prefixer(animation-direction, $directions, webkit moz spec); @include prefixer(animation-direction, $directions, spec);
} }
@mixin animation-play-state ($states...) { @mixin animation-play-state ($states...) {
// running | paused // running | paused
@include prefixer(animation-play-state, $states, webkit moz spec); @include prefixer(animation-play-state, $states, spec);
} }
@mixin animation-delay ($times...) { @mixin animation-delay ($times...) {
@include prefixer(animation-delay, $times, webkit moz spec); @include prefixer(animation-delay, $times, spec);
} }
@mixin animation-fill-mode ($modes...) { @mixin animation-fill-mode ($modes...) {
// none | forwards | backwards | both // none | forwards | backwards | both
@include prefixer(animation-fill-mode, $modes, webkit moz spec); @include prefixer(animation-fill-mode, $modes, spec);
} }
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
// Backface-visibility mixin // Backface-visibility mixin
//************************************************************************// //************************************************************************//
@mixin backface-visibility($visibility) { @mixin backface-visibility($visibility) {
@include prefixer(backface-visibility, $visibility, webkit spec); @include prefixer(backface-visibility, $visibility, spec);
} }
...@@ -4,11 +4,9 @@ ...@@ -4,11 +4,9 @@
//************************************************************************// //************************************************************************//
@mixin background($backgrounds...) { @mixin background($backgrounds...) {
$webkit-backgrounds: ();
$spec-backgrounds: (); $spec-backgrounds: ();
@each $background in $backgrounds { @each $background in $backgrounds {
$webkit-background: ();
$spec-background: (); $spec-background: ();
$background-type: type-of($background); $background-type: type-of($background);
...@@ -19,37 +17,30 @@ ...@@ -19,37 +17,30 @@
$gradient-type: str-slice($background-str, 0, 6); $gradient-type: str-slice($background-str, 0, 6);
@if $url-str == "url" { @if $url-str == "url" {
$webkit-background: $background;
$spec-background: $background; $spec-background: $background;
} }
@else if $gradient-type == "linear" { @else if $gradient-type == "linear" {
$gradients: _linear-gradient-parser("#{$background}"); $gradients: _linear-gradient-parser("#{$background}");
$webkit-background: map-get($gradients, webkit-image); $spec-background: map-get($gradients, spec-image);
$spec-background: map-get($gradients, spec-image);
} }
@else if $gradient-type == "radial" { @else if $gradient-type == "radial" {
$gradients: _radial-gradient-parser("#{$background}"); $gradients: _radial-gradient-parser("#{$background}");
$webkit-background: map-get($gradients, webkit-image); $spec-background: map-get($gradients, spec-image);
$spec-background: map-get($gradients, spec-image);
} }
@else { @else {
$webkit-background: $background;
$spec-background: $background; $spec-background: $background;
} }
} }
@else { @else {
$webkit-background: $background;
$spec-background: $background; $spec-background: $background;
} }
$webkit-backgrounds: append($webkit-backgrounds, $webkit-background, comma); $spec-backgrounds: append($spec-backgrounds, $spec-background, comma);
$spec-backgrounds: append($spec-backgrounds, $spec-background, comma);
} }
background: $webkit-backgrounds;
background: $spec-backgrounds; background: $spec-backgrounds;
} }
@mixin box-sizing ($box) { @mixin box-sizing ($box) {
// content-box | border-box | inherit // content-box | border-box | inherit
@include prefixer(box-sizing, $box, webkit moz spec); @include prefixer(box-sizing, $box, spec);
} }
@mixin perspective($depth: none) { @mixin perspective($depth: none) {
// none | <length> // none | <length>
@include prefixer(perspective, $depth, webkit moz spec); @include prefixer(perspective, $depth, spec);
} }
@mixin perspective-origin($value: 50% 50%) { @mixin perspective-origin($value: 50% 50%) {
@include prefixer(perspective-origin, $value, webkit moz spec); @include prefixer(perspective-origin, $value, spec);
} }
...@@ -34,6 +34,5 @@ ...@@ -34,6 +34,5 @@
$shape-size-spec: if(($shape-size-spec != ' ') and ($pos == null), '#{$shape-size-spec}, ', '#{$shape-size-spec} '); $shape-size-spec: if(($shape-size-spec != ' ') and ($pos == null), '#{$shape-size-spec}, ', '#{$shape-size-spec} ');
background-color: $fallback-color; background-color: $fallback-color;
background-image: -webkit-radial-gradient(unquote(#{$pos}#{$shape-size}#{$full}));
background-image: unquote("radial-gradient(#{$shape-size-spec}#{$pos-spec}#{$full})"); background-image: unquote("radial-gradient(#{$shape-size-spec}#{$pos-spec}#{$full})");
} }
@mixin transform($property: none) { @mixin transform($property: none) {
// none | <transform-function> // none | <transform-function>
@include prefixer(transform, $property, webkit moz ms o spec); @include prefixer(transform, $property, spec);
} }
@mixin transform-origin($axes: 50%) { @mixin transform-origin($axes: 50%) {
// x-axis - left | center | right | length | % // x-axis - left | center | right | length | %
// y-axis - top | center | bottom | length | % // y-axis - top | center | bottom | length | %
// z-axis - length // z-axis - length
@include prefixer(transform-origin, $axes, webkit moz ms o spec); @include prefixer(transform-origin, $axes, spec);
} }
@mixin transform-style ($style: flat) { @mixin transform-style ($style: flat) {
@include prefixer(transform-style, $style, webkit moz ms o spec); @include prefixer(transform-style, $style, spec);
} }
...@@ -4,74 +4,29 @@ ...@@ -4,74 +4,29 @@
// @include transition-property (transform, opacity); // @include transition-property (transform, opacity);
@mixin transition ($properties...) { @mixin transition ($properties...) {
// Fix for vendor-prefix transform property @if length($properties) >= 1 {
$needs-prefixes: false; @include prefixer(transition, $properties, spec);
$webkit: ();
$moz: ();
$spec: ();
// Create lists for vendor-prefixed transform
@each $list in $properties {
@if nth($list, 1) == "transform" {
$needs-prefixes: true;
$list1: -webkit-transform;
$list2: -moz-transform;
$list3: ();
@each $var in $list {
$list3: join($list3, $var);
@if $var != "transform" {
$list1: join($list1, $var);
$list2: join($list2, $var);
}
}
$webkit: append($webkit, $list1);
$moz: append($moz, $list2);
$spec: append($spec, $list3);
}
// Create lists for non-prefixed transition properties
@else {
$webkit: append($webkit, $list, comma);
$moz: append($moz, $list, comma);
$spec: append($spec, $list, comma);
}
} }
@if $needs-prefixes {
-webkit-transition: $webkit;
-moz-transition: $moz;
transition: $spec;
}
@else { @else {
@if length($properties) >= 1 { $properties: all 0.15s ease-out 0s;
@include prefixer(transition, $properties, webkit moz spec); @include prefixer(transition, $properties, spec);
}
@else {
$properties: all 0.15s ease-out 0s;
@include prefixer(transition, $properties, webkit moz spec);
}
} }
} }
@mixin transition-property ($properties...) { @mixin transition-property ($properties...) {
-webkit-transition-property: transition-property-names($properties, 'webkit'); transition-property: transition-property-names($properties, false);
-moz-transition-property: transition-property-names($properties, 'moz');
transition-property: transition-property-names($properties, false);
} }
@mixin transition-duration ($times...) { @mixin transition-duration ($times...) {
@include prefixer(transition-duration, $times, webkit moz spec); @include prefixer(transition-duration, $times, spec);
} }
@mixin transition-timing-function ($motions...) { @mixin transition-timing-function ($motions...) {
// ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() // ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier()
@include prefixer(transition-timing-function, $motions, webkit moz spec); @include prefixer(transition-timing-function, $motions, spec);
} }
@mixin transition-delay ($times...) { @mixin transition-delay ($times...) {
@include prefixer(transition-delay, $times, webkit moz spec); @include prefixer(transition-delay, $times, spec);
} }
@mixin user-select($arg: none) { @mixin user-select($arg: none) {
@include prefixer(user-select, $arg, webkit moz ms spec); @include prefixer(user-select, $arg, spec);
} }
...@@ -4,7 +4,7 @@ Auto-auth page (used to automatically log in during testing). ...@@ -4,7 +4,7 @@ Auto-auth page (used to automatically log in during testing).
import re import re
import urllib import urllib
from bok_choy.page_object import PageObject, unguarded from bok_choy.page_object import PageObject, unguarded, XSS_INJECTION
from . import AUTH_BASE_URL from . import AUTH_BASE_URL
...@@ -17,7 +17,7 @@ class AutoAuthPage(PageObject): ...@@ -17,7 +17,7 @@ class AutoAuthPage(PageObject):
CONTENT_REGEX = r'.+? user (?P<username>\S+) \((?P<email>.+?)\) with password \S+ and user_id (?P<user_id>\d+)$' CONTENT_REGEX = r'.+? user (?P<username>\S+) \((?P<email>.+?)\) with password \S+ and user_id (?P<user_id>\d+)$'
def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, def __init__(self, browser, username=None, email=None, password=None, full_name=None, staff=None, course_id=None,
enrollment_mode=None, roles=None): enrollment_mode=None, roles=None):
""" """
Auto-auth is an end-point for HTTP GET requests. Auto-auth is an end-point for HTTP GET requests.
...@@ -25,6 +25,7 @@ class AutoAuthPage(PageObject): ...@@ -25,6 +25,7 @@ class AutoAuthPage(PageObject):
but you can also specify credentials using querystring parameters. but you can also specify credentials using querystring parameters.
`username`, `email`, and `password` are the user's credentials (strings) `username`, `email`, and `password` are the user's credentials (strings)
'full_name' is the profile full name value
`staff` is a boolean indicating whether the user is global staff. `staff` is a boolean indicating whether the user is global staff.
`course_id` is the ID of the course to enroll the student in. `course_id` is the ID of the course to enroll the student in.
Currently, this has the form "org/number/run" Currently, this has the form "org/number/run"
...@@ -42,6 +43,8 @@ class AutoAuthPage(PageObject): ...@@ -42,6 +43,8 @@ class AutoAuthPage(PageObject):
if username is not None: if username is not None:
self._params['username'] = username self._params['username'] = username
self._params['full_name'] = full_name if full_name is not None else XSS_INJECTION
if email is not None: if email is not None:
self._params['email'] = email self._params['email'] = email
......
...@@ -91,6 +91,7 @@ class ProblemPage(PageObject): ...@@ -91,6 +91,7 @@ class ProblemPage(PageObject):
Fill in the answer to a numerical problem. Fill in the answer to a numerical problem.
""" """
self.q(css='div.problem section.inputtype input').fill(text) self.q(css='div.problem section.inputtype input').fill(text)
self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear')
self.wait_for_ajax() self.wait_for_ajax()
def click_check(self): def click_check(self):
......
...@@ -508,12 +508,6 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -508,12 +508,6 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
self.q(css='{} .section-name .save-button'.format(parent_css)).first.click() self.q(css='{} .section-name .save-button'.format(parent_css)).first.click()
self.wait_for_ajax() self.wait_for_ajax()
def click_release_date(self):
"""
Open release date edit modal of first section in course outline
"""
self.q(css='div.section-published-date a.edit-release-date').first.click()
def sections(self): def sections(self):
""" """
Returns the sections of this course outline page. Returns the sections of this course outline page.
......
...@@ -94,6 +94,7 @@ class UsersPageMixin(PageObject): ...@@ -94,6 +94,7 @@ class UsersPageMixin(PageObject):
self.click_add_button() self.click_add_button()
self.set_new_user_email(email) self.set_new_user_email(email)
self.click_submit_new_user_form() self.click_submit_new_user_form()
self.wait_for_page()
def delete_user_from_course(self, email): def delete_user_from_course(self, email):
""" Deletes user from course/library """ """ Deletes user from course/library """
......
...@@ -53,7 +53,7 @@ DISPLAY_NAME = "Component Display Name" ...@@ -53,7 +53,7 @@ DISPLAY_NAME = "Component Display Name"
DEFAULT_SETTINGS = [ DEFAULT_SETTINGS = [
# basic # basic
[DISPLAY_NAME, 'Video', False], [DISPLAY_NAME, 'Video', False],
['Default Video URL', 'http://youtu.be/3_yD_cEKoCk, , ', False], ['Default Video URL', 'https://www.youtube.com/watch?v=3_yD_cEKoCk, , ', False],
# advanced # advanced
[DISPLAY_NAME, 'Video', False], [DISPLAY_NAME, 'Video', False],
......
...@@ -16,6 +16,7 @@ from path import Path as path ...@@ -16,6 +16,7 @@ from path import Path as path
from bok_choy.javascript import js_defined from bok_choy.javascript import js_defined
from bok_choy.web_app_test import WebAppTest from bok_choy.web_app_test import WebAppTest
from bok_choy.promise import EmptyPromise, Promise from bok_choy.promise import EmptyPromise, Promise
from bok_choy.page_object import XSS_INJECTION
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from pymongo import MongoClient, ASCENDING from pymongo import MongoClient, ASCENDING
from openedx.core.lib.tests.assertions.events import assert_event_matches, is_matching_event, EventMatchTolerates from openedx.core.lib.tests.assertions.events import assert_event_matches, is_matching_event, EventMatchTolerates
...@@ -640,7 +641,7 @@ class UniqueCourseTest(WebAppTest): ...@@ -640,7 +641,7 @@ class UniqueCourseTest(WebAppTest):
'org': 'test_org', 'org': 'test_org',
'number': self.unique_id, 'number': self.unique_id,
'run': 'test_run', 'run': 'test_run',
'display_name': 'Test Course' + self.unique_id 'display_name': 'Test Course' + XSS_INJECTION + self.unique_id
} }
@property @property
......
...@@ -6,6 +6,7 @@ from unittest import skip ...@@ -6,6 +6,7 @@ from unittest import skip
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from bok_choy.web_app_test import WebAppTest from bok_choy.web_app_test import WebAppTest
from bok_choy.page_object import XSS_INJECTION
from ...pages.lms.account_settings import AccountSettingsPage from ...pages.lms.account_settings import AccountSettingsPage
from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.auto_auth import AutoAuthPage
...@@ -33,12 +34,12 @@ class AccountSettingsTestMixin(EventsTestMixin, WebAppTest): ...@@ -33,12 +34,12 @@ class AccountSettingsTestMixin(EventsTestMixin, WebAppTest):
self.account_settings_page.visit() self.account_settings_page.visit()
self.account_settings_page.wait_for_ajax() self.account_settings_page.wait_for_ajax()
def log_in_as_unique_user(self, email=None): def log_in_as_unique_user(self, email=None, full_name=None):
""" """
Create a unique user and return the account's username and id. Create a unique user and return the account's username and id.
""" """
username = "test_{uuid}".format(uuid=self.unique_id[0:6]) username = "test_{uuid}".format(uuid=self.unique_id[0:6])
auto_auth_page = AutoAuthPage(self.browser, username=username, email=email).visit() auto_auth_page = AutoAuthPage(self.browser, username=username, email=email, full_name=full_name).visit()
user_id = auto_auth_page.get_user_id() user_id = auto_auth_page.get_user_id()
return username, user_id return username, user_id
...@@ -122,7 +123,8 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): ...@@ -122,7 +123,8 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
Initialize account and pages. Initialize account and pages.
""" """
super(AccountSettingsPageTest, self).setUp() super(AccountSettingsPageTest, self).setUp()
self.username, self.user_id = self.log_in_as_unique_user() self.full_name = XSS_INJECTION
self.username, self.user_id = self.log_in_as_unique_user(full_name=self.full_name)
self.visit_account_settings_page() self.visit_account_settings_page()
def test_page_view_event(self): def test_page_view_event(self):
...@@ -259,16 +261,16 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): ...@@ -259,16 +261,16 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
self._test_text_field( self._test_text_field(
u'name', u'name',
u'Full Name', u'Full Name',
self.username, self.full_name,
u'@', u'@',
[u'another name', self.username], [u'another name', self.full_name],
) )
actual_events = self.wait_for_events(event_filter=self.settings_changed_event_filter, number_of_matches=2) actual_events = self.wait_for_events(event_filter=self.settings_changed_event_filter, number_of_matches=2)
self.assert_events_match( self.assert_events_match(
[ [
self.expected_settings_changed_event('name', self.username, 'another name'), self.expected_settings_changed_event('name', self.full_name, 'another name'),
self.expected_settings_changed_event('name', 'another name', self.username), self.expected_settings_changed_event('name', 'another name', self.full_name),
], ],
actual_events actual_events
) )
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Base classes used by studio tests. Base classes used by studio tests.
""" """
from bok_choy.web_app_test import WebAppTest from bok_choy.web_app_test import WebAppTest
from bok_choy.page_object import XSS_INJECTION
from ...pages.studio.auto_auth import AutoAuthPage from ...pages.studio.auto_auth import AutoAuthPage
from ...fixtures.course import CourseFixture from ...fixtures.course import CourseFixture
from ...fixtures.library import LibraryFixture from ...fixtures.library import LibraryFixture
...@@ -15,11 +16,12 @@ class StudioCourseTest(UniqueCourseTest): ...@@ -15,11 +16,12 @@ class StudioCourseTest(UniqueCourseTest):
Base class for all Studio course tests. Base class for all Studio course tests.
""" """
def setUp(self, is_staff=False): def setUp(self, is_staff=False, test_xss=True): # pylint: disable=arguments-differ
""" """
Install a course with no content using a fixture. Install a course with no content using a fixture.
""" """
super(StudioCourseTest, self).setUp() super(StudioCourseTest, self).setUp()
self.test_xss = test_xss
self.install_course_fixture(is_staff) self.install_course_fixture(is_staff)
def install_course_fixture(self, is_staff=False): def install_course_fixture(self, is_staff=False):
...@@ -30,8 +32,21 @@ class StudioCourseTest(UniqueCourseTest): ...@@ -30,8 +32,21 @@ class StudioCourseTest(UniqueCourseTest):
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
self.course_info['run'], self.course_info['run'],
self.course_info['display_name'] self.course_info['display_name'],
) )
if self.test_xss:
xss_injected_unique_id = XSS_INJECTION + self.unique_id
test_improper_escaping = {u"value": xss_injected_unique_id}
self.course_fixture.add_advanced_settings({
"advertised_start": test_improper_escaping,
"info_sidebar_name": test_improper_escaping,
"cert_name_short": test_improper_escaping,
"cert_name_long": test_improper_escaping,
"display_organization": test_improper_escaping,
"display_coursenumber": test_improper_escaping,
})
self.course_info['display_organization'] = xss_injected_unique_id
self.course_info['display_coursenumber'] = xss_injected_unique_id
self.populate_course_fixture(self.course_fixture) self.populate_course_fixture(self.course_fixture)
self.course_fixture.install() self.course_fixture.install()
self.user = self.course_fixture.user self.user = self.course_fixture.user
......
...@@ -61,8 +61,8 @@ class CourseTeamPageTest(StudioCourseTest): ...@@ -61,8 +61,8 @@ class CourseTeamPageTest(StudioCourseTest):
def check_course_equality(course1, course2): def check_course_equality(course1, course2):
""" Compares to course dictionaries using org, number and run as keys""" """ Compares to course dictionaries using org, number and run as keys"""
return ( return (
course1['org'] == course2['org'] and course1['org'] == course2['display_organization'] and
course1['number'] == course2['number'] and course1['number'] == course2['display_coursenumber'] and
course1['run'] == course2['run'] course1['run'] == course2['run']
) )
......
...@@ -19,7 +19,7 @@ class CertificatesTest(StudioCourseTest): ...@@ -19,7 +19,7 @@ class CertificatesTest(StudioCourseTest):
Tests for settings/certificates Page. Tests for settings/certificates Page.
""" """
def setUp(self): # pylint: disable=arguments-differ def setUp(self): # pylint: disable=arguments-differ
super(CertificatesTest, self).setUp(is_staff=True) super(CertificatesTest, self).setUp(is_staff=True, test_xss=False)
self.certificates_page = CertificatesPage( self.certificates_page = CertificatesPage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
......
...@@ -18,6 +18,7 @@ from ..pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage ...@@ -18,6 +18,7 @@ from ..pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
from ..tests.lms.test_lms_user_preview import verify_expected_problem_visibility from ..tests.lms.test_lms_user_preview import verify_expected_problem_visibility
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from bok_choy.page_object import XSS_INJECTION
@attr('shard_5') @attr('shard_5')
...@@ -28,8 +29,8 @@ class EndToEndCohortedCoursewareTest(ContainerBase): ...@@ -28,8 +29,8 @@ class EndToEndCohortedCoursewareTest(ContainerBase):
super(EndToEndCohortedCoursewareTest, self).setUp(is_staff=is_staff) super(EndToEndCohortedCoursewareTest, self).setUp(is_staff=is_staff)
self.staff_user = self.user self.staff_user = self.user
self.content_group_a = "Content Group A" self.content_group_a = "Content Group A" + XSS_INJECTION
self.content_group_b = "Content Group B" self.content_group_b = "Content Group B" + XSS_INJECTION
# Create a student who will be in "Cohort A" # Create a student who will be in "Cohort A"
self.cohort_a_student_username = "cohort_a_student" self.cohort_a_student_username = "cohort_a_student"
......
...@@ -399,7 +399,7 @@ common/test/acceptance/tests. This is another example. ...@@ -399,7 +399,7 @@ common/test/acceptance/tests. This is another example.
paver test_bokchoy -t studio/test_studio_bad_data.py paver test_bokchoy -t studio/test_studio_bad_data.py
To run a single test faster by not repeating setup tasks us the ``--fasttest`` option. To run a single test faster by not repeating setup tasks use the ``--fasttest`` option.
:: ::
......
...@@ -17,6 +17,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase ...@@ -17,6 +17,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tabs import get_course_tab_list from courseware.tabs import get_course_tab_list
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse, resolve from django.core.urlresolvers import reverse, resolve
from django.utils.translation import ugettext as _
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.test.utils import override_settings from django.test.utils import override_settings
from django.test import RequestFactory from django.test import RequestFactory
...@@ -264,6 +265,29 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -264,6 +265,29 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
'<form action=".+create_ccx"', '<form action=".+create_ccx"',
response.content)) response.content))
def test_create_ccx_with_ccx_connector_set(self):
"""
Assert that coach cannot create ccx when ``ccx_connector`` url is set.
"""
course = CourseFactory.create()
course.ccx_connector = "http://ccx.com"
course.save()
self.store.update_item(course, 0)
role = CourseCcxCoachRole(course.id)
role.add_users(self.coach)
url = reverse(
'create_ccx',
kwargs={'course_id': unicode(course.id)})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
error_message = _(
"A CCX can only be created on this course through an external service."
" Contact a course admin to give you access."
)
self.assertTrue(re.search(error_message, response.content))
def test_create_ccx(self, ccx_name='New CCX'): def test_create_ccx(self, ccx_name='New CCX'):
""" """
Create CCX. Follow redirect to coach dashboard, confirm we see Create CCX. Follow redirect to coach dashboard, confirm we see
......
...@@ -12,6 +12,7 @@ from django.contrib.auth.models import User ...@@ -12,6 +12,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.urlresolvers import reverse
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
...@@ -34,6 +35,28 @@ from lms.djangoapps.ccx.custom_exception import CCXUserValidationException ...@@ -34,6 +35,28 @@ from lms.djangoapps.ccx.custom_exception import CCXUserValidationException
log = logging.getLogger("edx.ccx") log = logging.getLogger("edx.ccx")
def get_ccx_creation_dict(course):
"""
Return dict of rendering create ccx form.
Arguments:
course (CourseDescriptorWithMixins): An edx course
Returns:
dict: A attribute dict for view rendering
"""
context = {
'course': course,
'create_ccx_url': reverse('create_ccx', kwargs={'course_id': course.id}),
'has_ccx_connector': "true" if hasattr(course, 'ccx_connector') and course.ccx_connector else "false",
'use_ccx_con_error_message': _(
"A CCX can only be created on this course through an external service."
" Contact a course admin to give you access."
)
}
return context
def get_ccx_from_ccx_locator(course_id): def get_ccx_from_ccx_locator(course_id):
""" helper function to allow querying ccx fields from templates """ """ helper function to allow querying ccx fields from templates """
ccx_id = getattr(course_id, 'ccx', None) ccx_id = getattr(course_id, 'ccx', None)
......
...@@ -58,6 +58,7 @@ from lms.djangoapps.ccx.utils import ( ...@@ -58,6 +58,7 @@ from lms.djangoapps.ccx.utils import (
ccx_students_enrolling_center, ccx_students_enrolling_center,
get_ccx_for_coach, get_ccx_for_coach,
get_ccx_by_ccx_id, get_ccx_by_ccx_id,
get_ccx_creation_dict,
get_date, get_date,
parse_date, parse_date,
prep_course_for_grading, prep_course_for_grading,
...@@ -132,6 +133,7 @@ def dashboard(request, course, ccx=None): ...@@ -132,6 +133,7 @@ def dashboard(request, course, ccx=None):
'course': course, 'course': course,
'ccx': ccx, 'ccx': ccx,
} }
context.update(get_ccx_creation_dict(course))
if ccx: if ccx:
ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.id))
...@@ -168,6 +170,13 @@ def create_ccx(request, course, ccx=None): ...@@ -168,6 +170,13 @@ def create_ccx(request, course, ccx=None):
""" """
name = request.POST.get('name') name = request.POST.get('name')
if hasattr(course, 'ccx_connector') and course.ccx_connector:
# if ccx connector url is set in course settings then inform user that he can
# only create ccx by using ccx connector url.
context = get_ccx_creation_dict(course)
messages.error(request, context['use_ccx_con_error_message'])
return render_to_response('ccx/coach_dashboard.html', context)
# prevent CCX objects from being created for deprecated course ids. # prevent CCX objects from being created for deprecated course ids.
if course.id.deprecated: if course.id.deprecated:
messages.error(request, _( messages.error(request, _(
......
...@@ -4,6 +4,7 @@ import datetime ...@@ -4,6 +4,7 @@ import datetime
import json import json
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from freezegun import freeze_time from freezegun import freeze_time
...@@ -99,3 +100,13 @@ class EdxRestApiClientTest(TestCase): ...@@ -99,3 +100,13 @@ class EdxRestApiClientTest(TestCase):
) )
actual_object = ecommerce_api_client(self.user).baskets(1).order.get() actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
self.assertEqual(actual_object, {u"result": u"Préparatoire"}) self.assertEqual(actual_object, {u"result": u"Préparatoire"})
def test_client_with_user_without_profile(self):
"""
Verify client initialize successfully for users having no profile.
"""
worker = User.objects.create_user(username='test_worker', email='test@example.com')
api_client = ecommerce_api_client(worker)
self.assertEqual(api_client._store['session'].auth.__dict__['username'], worker.username) # pylint: disable=protected-access
self.assertIsNone(api_client._store['session'].auth.__dict__['full_name']) # pylint: disable=protected-access
...@@ -38,26 +38,25 @@ class EcommerceServiceTests(TestCase): ...@@ -38,26 +38,25 @@ class EcommerceServiceTests(TestCase):
self.request_factory = RequestFactory() self.request_factory = RequestFactory()
self.user = UserFactory.create() self.user = UserFactory.create()
self.request = self.request_factory.get("foo") self.request = self.request_factory.get("foo")
self.request.user = self.user
update_commerce_config(enabled=True) update_commerce_config(enabled=True)
super(EcommerceServiceTests, self).setUp() super(EcommerceServiceTests, self).setUp()
def test_is_enabled(self): def test_is_enabled(self):
"""Verify that is_enabled() returns True when ecomm checkout is enabled. """ """Verify that is_enabled() returns True when ecomm checkout is enabled. """
is_enabled = EcommerceService().is_enabled(self.request) is_enabled = EcommerceService().is_enabled(self.user)
self.assertTrue(is_enabled) self.assertTrue(is_enabled)
config = CommerceConfiguration.current() config = CommerceConfiguration.current()
config.checkout_on_ecommerce_service = False config.checkout_on_ecommerce_service = False
config.save() config.save()
is_not_enabled = EcommerceService().is_enabled(self.request) is_not_enabled = EcommerceService().is_enabled(self.user)
self.assertFalse(is_not_enabled) self.assertFalse(is_not_enabled)
@patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site') @patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site')
def test_is_enabled_for_microsites(self, is_microsite): def test_is_enabled_for_microsites(self, is_microsite):
"""Verify that is_enabled() returns False if used for a microsite.""" """Verify that is_enabled() returns False if used for a microsite."""
is_microsite.return_value = True is_microsite.return_value = True
is_not_enabled = EcommerceService().is_enabled(self.request) is_not_enabled = EcommerceService().is_enabled(self.user)
self.assertFalse(is_not_enabled) self.assertFalse(is_not_enabled)
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url') @override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
......
...@@ -44,9 +44,9 @@ class EcommerceService(object): ...@@ -44,9 +44,9 @@ class EcommerceService(object):
def __init__(self): def __init__(self):
self.config = CommerceConfiguration.current() self.config = CommerceConfiguration.current()
def is_enabled(self, request): def is_enabled(self, user):
""" Check if the user is activated, if the service is enabled and that the site is not a microsite. """ """ Check if the user is activated, if the service is enabled and that the site is not a microsite. """
return (request.user.is_active and self.config.checkout_on_ecommerce_service and not return (user.is_active and self.config.checkout_on_ecommerce_service and not
helpers.is_request_in_themed_site()) helpers.is_request_in_themed_site())
def payment_page_url(self): def payment_page_url(self):
......
...@@ -14,6 +14,7 @@ from lazy import lazy ...@@ -14,6 +14,7 @@ from lazy import lazy
import pytz import pytz
from course_modes.models import CourseMode from course_modes.models import CourseMode
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -204,6 +205,12 @@ class VerifiedUpgradeDeadlineDate(DateSummary): ...@@ -204,6 +205,12 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
@property @property
def link(self): def link(self):
ecommerce_service = EcommerceService()
if ecommerce_service.is_enabled(self.user):
course_mode = CourseMode.objects.get(
course_id=self.course.id, mode_slug=CourseMode.VERIFIED
)
return ecommerce_service.checkout_page_url(course_mode.sku)
return reverse('verify_student_upgrade_and_verify', args=(self.course.id,)) return reverse('verify_student_upgrade_and_verify', args=(self.course.id,))
@lazy @lazy
......
...@@ -14,17 +14,20 @@ package and is used to wrap the `authored_data` when constructing an ...@@ -14,17 +14,20 @@ package and is used to wrap the `authored_data` when constructing an
`LmsFieldData`. This means overrides will be in effect for all scopes covered `LmsFieldData`. This means overrides will be in effect for all scopes covered
by `authored_data`, e.g. course content and settings stored in Mongo. by `authored_data`, e.g. course content and settings stored in Mongo.
""" """
import threading
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from contextlib import contextmanager from contextlib import contextmanager
import threading
from django.conf import settings from django.conf import settings
from request_cache.middleware import RequestCache
from xblock.field_data import FieldData from xblock.field_data import FieldData
from request_cache.middleware import RequestCache
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
NOTSET = object() NOTSET = object()
ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers.{course_id}" ENABLED_OVERRIDE_PROVIDERS_KEY = u'courseware.field_overrides.enabled_providers.{course_id}'
ENABLED_MODULESTORE_OVERRIDE_PROVIDERS_KEY = u'courseware.modulestore_field_overrides.enabled_providers.{course_id}'
def resolve_dotted(name): def resolve_dotted(name):
...@@ -46,6 +49,88 @@ def resolve_dotted(name): ...@@ -46,6 +49,88 @@ def resolve_dotted(name):
return target return target
def _lineage(block):
"""
Returns an iterator over all ancestors of the given block, starting with
its immediate parent and ending at the root of the block tree.
"""
parent = block.get_parent()
while parent:
yield parent
parent = parent.get_parent()
class _OverridesDisabled(threading.local):
"""
A thread local used to manage state of overrides being disabled or not.
"""
disabled = ()
_OVERRIDES_DISABLED = _OverridesDisabled()
@contextmanager
def disable_overrides():
"""
A context manager which disables field overrides inside the context of a
`with` statement, allowing code to get at the `original` value of a field.
"""
prev = _OVERRIDES_DISABLED.disabled
_OVERRIDES_DISABLED.disabled += (True,)
yield
_OVERRIDES_DISABLED.disabled = prev
def overrides_disabled():
"""
Checks to see whether overrides are disabled in the current context.
Returns a boolean value. See `disable_overrides`.
"""
return bool(_OVERRIDES_DISABLED.disabled)
class FieldOverrideProvider(object):
"""
Abstract class which defines the interface that a `FieldOverrideProvider`
must provide. In general, providers should derive from this class, but
it's not strictly necessary as long as they correctly implement this
interface.
A `FieldOverrideProvider` implementation is only responsible for looking up
field overrides. To set overrides, there will be a domain specific API for
the concrete override implementation being used.
"""
__metaclass__ = ABCMeta
def __init__(self, user):
self.user = user
@abstractmethod
def get(self, block, name, default): # pragma no cover
"""
Look for an override value for the field named `name` in `block`.
Returns the overridden value or `default` if no override is found.
"""
raise NotImplementedError
@abstractmethod
def enabled_for(self, course): # pragma no cover
"""
Return True if this provider should be enabled for a given course,
and False otherwise.
Concrete implementations are responsible for implementing this method.
Arguments:
course (CourseModule or None)
Returns:
bool
"""
return False
class OverrideFieldData(FieldData): class OverrideFieldData(FieldData):
""" """
A :class:`~xblock.field_data.FieldData` which wraps another `FieldData` A :class:`~xblock.field_data.FieldData` which wraps another `FieldData`
...@@ -171,83 +256,54 @@ class OverrideFieldData(FieldData): ...@@ -171,83 +256,54 @@ class OverrideFieldData(FieldData):
return self.fallback.default(block, name) return self.fallback.default(block, name)
class _OverridesDisabled(threading.local): class OverrideModulestoreFieldData(OverrideFieldData):
""" """Apply field data overrides at the modulestore level. No student context required."""
A thread local used to manage state of overrides being disabled or not.
"""
disabled = ()
_OVERRIDES_DISABLED = _OverridesDisabled()
@contextmanager
def disable_overrides():
"""
A context manager which disables field overrides inside the context of a
`with` statement, allowing code to get at the `original` value of a field.
"""
prev = _OVERRIDES_DISABLED.disabled
_OVERRIDES_DISABLED.disabled += (True,)
yield
_OVERRIDES_DISABLED.disabled = prev
def overrides_disabled():
"""
Checks to see whether overrides are disabled in the current context.
Returns a boolean value. See `disable_overrides`.
"""
return bool(_OVERRIDES_DISABLED.disabled)
@classmethod
def wrap(cls, block, field_data): # pylint: disable=arguments-differ
"""
Returns an instance of FieldData wrapped by FieldOverrideProviders which
extend read-only functionality. If no MODULESTORE_FIELD_OVERRIDE_PROVIDERS
are configured, an unwrapped FieldData instance is returned.
class FieldOverrideProvider(object): Arguments:
""" block: An XBlock
Abstract class which defines the interface that a `FieldOverrideProvider` field_data: An instance of FieldData to be wrapped
must provide. In general, providers should derive from this class, but """
it's not strictly necessary as long as they correctly implement this if cls.provider_classes is None:
interface. cls.provider_classes = [
resolve_dotted(name) for name in settings.MODULESTORE_FIELD_OVERRIDE_PROVIDERS
]
A `FieldOverrideProvider` implementation is only responsible for looking up enabled_providers = cls._providers_for_block(block)
field overrides. To set overrides, there will be a domain specific API for if enabled_providers:
the concrete override implementation being used. return cls(field_data, enabled_providers)
"""
__metaclass__ = ABCMeta
def __init__(self, user): return field_data
self.user = user
@abstractmethod @classmethod
def get(self, block, name, default): # pragma no cover def _providers_for_block(cls, block):
"""
Look for an override value for the field named `name` in `block`.
Returns the overridden value or `default` if no override is found.
""" """
raise NotImplementedError Computes a list of enabled providers based on the given XBlock.
The result is cached per request to avoid the overhead incurred
by filtering override providers hundreds of times.
@abstractmethod Arguments:
def enabled_for(self, course): # pragma no cover block: An XBlock
""" """
Return True if this provider should be enabled for a given course, course_id = unicode(block.location.course_key)
and False otherwise. cache_key = ENABLED_MODULESTORE_OVERRIDE_PROVIDERS_KEY.format(course_id=course_id)
Concrete implementations are responsible for implementing this method.
Arguments: request_cache = RequestCache.get_request_cache()
course (CourseModule or None) enabled_providers = request_cache.data.get(cache_key)
Returns: if enabled_providers is None:
bool enabled_providers = [
""" provider_class for provider_class in cls.provider_classes if provider_class.enabled_for(block)
return False ]
request_cache.data[cache_key] = enabled_providers
return enabled_providers
def _lineage(block): def __init__(self, fallback, providers):
""" super(OverrideModulestoreFieldData, self).__init__(None, fallback, providers)
Returns an iterator over all ancestors of the given block, starting with
its immediate parent and ending at the root of the block tree.
"""
parent = block.get_parent()
while parent:
yield parent
parent = parent.get_parent()
...@@ -20,9 +20,10 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider): ...@@ -20,9 +20,10 @@ class SelfPacedDateOverrideProvider(FieldOverrideProvider):
# Remove release dates for course content # Remove release dates for course content
if name == 'start' and block.category != 'course': if name == 'start' and block.category != 'course':
return None return None
return default return default
@classmethod @classmethod
def enabled_for(cls, course): def enabled_for(cls, block):
"""This provider is enabled for self-paced courses only.""" """This provider is enabled for self-paced courses only."""
return course is not None and course.self_paced and SelfPacedConfiguration.current().enabled return block is not None and block.self_paced and SelfPacedConfiguration.current().enabled
...@@ -8,6 +8,7 @@ import freezegun ...@@ -8,6 +8,7 @@ import freezegun
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import pytz import pytz
from commerce.models import CommerceConfiguration
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.courses import _get_course_date_summary_blocks from courseware.courses import _get_course_date_summary_blocks
...@@ -44,6 +45,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -44,6 +45,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
enrollment_mode=CourseMode.VERIFIED, enrollment_mode=CourseMode.VERIFIED,
days_till_verification_deadline=14, days_till_verification_deadline=14,
verification_status=None, verification_status=None,
sku=None
): ):
"""Set up the course and user for this test.""" """Set up the course and user for this test."""
now = datetime.now(pytz.UTC) now = datetime.now(pytz.UTC)
...@@ -61,7 +63,8 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -61,7 +63,8 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
CourseModeFactory.create( CourseModeFactory.create(
course_id=self.course.id, course_id=self.course.id,
mode_slug=enrollment_mode, mode_slug=enrollment_mode,
expiration_datetime=now + timedelta(days=days_till_upgrade_deadline) expiration_datetime=now + timedelta(days=days_till_upgrade_deadline),
sku=sku
) )
CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=enrollment_mode) CourseEnrollmentFactory.create(course_id=self.course.id, user=self.user, mode=enrollment_mode)
else: else:
...@@ -200,6 +203,18 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -200,6 +203,18 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
block = VerifiedUpgradeDeadlineDate(self.course, self.user) block = VerifiedUpgradeDeadlineDate(self.course, self.user)
self.assertIsNone(block.date) self.assertIsNone(block.date)
def test_ecommerce_checkout_redirect(self):
"""Verify the block link redirects to ecommerce checkout if it's enabled."""
sku = 'TESTSKU'
checkout_page = '/test_basket/'
CommerceConfiguration.objects.create(
checkout_on_ecommerce_service=True,
single_course_checkout_page=checkout_page
)
self.setup_course_and_user(sku=sku)
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
self.assertEqual(block.link, '{}?sku={}'.format(checkout_page, sku))
## VerificationDeadlineDate ## VerificationDeadlineDate
def test_no_verification_deadline(self): def test_no_verification_deadline(self):
......
""" """
Tests for `field_overrides` module. Tests for `field_overrides` module.
""" """
# pylint: disable=missing-docstring
import unittest import unittest
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -10,16 +11,39 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -10,16 +11,39 @@ from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from ..field_overrides import ( from ..field_overrides import (
resolve_dotted,
disable_overrides, disable_overrides,
FieldOverrideProvider, FieldOverrideProvider,
OverrideFieldData, OverrideFieldData,
resolve_dotted, OverrideModulestoreFieldData,
) )
TESTUSER = "testuser" TESTUSER = "testuser"
class TestOverrideProvider(FieldOverrideProvider):
"""
A concrete implementation of `FieldOverrideProvider` for testing.
"""
def get(self, block, name, default):
if self.user:
assert self.user is TESTUSER
assert block == 'block'
if name == 'foo':
return 'fu'
elif name == 'oh':
return 'man'
return default
@classmethod
def enabled_for(cls, course):
return True
@attr('shard_1') @attr('shard_1')
@override_settings(FIELD_OVERRIDE_PROVIDERS=( @override_settings(FIELD_OVERRIDE_PROVIDERS=(
'courseware.tests.test_field_overrides.TestOverrideProvider',)) 'courseware.tests.test_field_overrides.TestOverrideProvider',))
...@@ -101,6 +125,31 @@ class OverrideFieldDataTests(SharedModuleStoreTestCase): ...@@ -101,6 +125,31 @@ class OverrideFieldDataTests(SharedModuleStoreTestCase):
@attr('shard_1') @attr('shard_1')
@override_settings(
MODULESTORE_FIELD_OVERRIDE_PROVIDERS=['courseware.tests.test_field_overrides.TestOverrideProvider']
)
class OverrideModulestoreFieldDataTests(OverrideFieldDataTests):
def setUp(self):
super(OverrideModulestoreFieldDataTests, self).setUp()
OverrideModulestoreFieldData.provider_classes = None
def tearDown(self):
super(OverrideModulestoreFieldDataTests, self).tearDown()
OverrideModulestoreFieldData.provider_classes = None
def make_one(self):
return OverrideModulestoreFieldData.wrap(self.course, DictFieldData({
'foo': 'bar',
'bees': 'knees',
}))
@override_settings(MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[])
def test_no_overrides_configured(self):
data = self.make_one()
self.assertIsInstance(data, DictFieldData)
@attr('shard_1')
class ResolveDottedTests(unittest.TestCase): class ResolveDottedTests(unittest.TestCase):
""" """
Tests for `resolve_dotted`. Tests for `resolve_dotted`.
...@@ -121,24 +170,6 @@ class ResolveDottedTests(unittest.TestCase): ...@@ -121,24 +170,6 @@ class ResolveDottedTests(unittest.TestCase):
) )
class TestOverrideProvider(FieldOverrideProvider):
"""
A concrete implementation of `FieldOverrideProvider` for testing.
"""
def get(self, block, name, default):
assert self.user is TESTUSER
assert block == 'block'
if name == 'foo':
return 'fu'
if name == 'oh':
return 'man'
return default
@classmethod
def enabled_for(cls, course):
return True
def inject_field_overrides(blocks, course, user): def inject_field_overrides(blocks, course, user):
""" """
Apparently the test harness doesn't use LmsFieldStorage, and I'm Apparently the test harness doesn't use LmsFieldStorage, and I'm
......
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