Commit 1b03a009 by cahrens

Replace with CoffeeScript generated version of JS.

Includes reformatting to 4 spaces.
parent 7a443737
...@@ -549,7 +549,7 @@ class JavascriptInput(InputTypeBase): ...@@ -549,7 +549,7 @@ class JavascriptInput(InputTypeBase):
TODO (arjun?): document this in detail. Initial notes: TODO (arjun?): document this in detail. Initial notes:
- display_class is a subclass of XProblemClassDisplay (see - display_class is a subclass of XProblemClassDisplay (see
xmodule/xmodule/js/src/capa/display.coffee), xmodule/xmodule/js/src/capa/display.js),
- display_file is the js script to be in /static/js/ where display_class is defined. - display_file is the js script to be in /static/js/ where display_class is defined.
""" """
......
...@@ -29,11 +29,9 @@ class CapaModule(CapaMixin, XModule): ...@@ -29,11 +29,9 @@ class CapaModule(CapaMixin, XModule):
icon_class = 'problem' icon_class = 'problem'
js = { js = {
'coffee': [
resource_string(__name__, 'js/src/capa/display.coffee'),
],
'js': [ 'js': [
resource_string(__name__, 'js/src/javascript_loader.js'), resource_string(__name__, 'js/src/javascript_loader.js'),
resource_string(__name__, 'js/src/capa/display.js'),
resource_string(__name__, 'js/src/collapsible.js'), resource_string(__name__, 'js/src/collapsible.js'),
resource_string(__name__, 'js/src/capa/imageinput.js'), resource_string(__name__, 'js/src/capa/imageinput.js'),
resource_string(__name__, 'js/src/capa/schematic.js'), resource_string(__name__, 'js/src/capa/schematic.js'),
......
class @Problem // Generated by CoffeeScript 1.6.1
(function () {
constructor: (element) -> var _this = this,
@el = $(element).find('.problems-wrapper') __indexOf = [].indexOf || function (item) {
@id = @el.data('problem-id') for (var i = 0, l = this.length; i < l; i++) {
@element_id = @el.attr('id') if (i in this && this[i] === item) return i;
@url = @el.data('url') }
@content = @el.data('content') return -1;
};
# has_timed_out and has_response are used to ensure that are used to
# ensure that we wait a minimum of ~ 1s before transitioning the submit this.Problem = (function () {
# button from disabled to enabled var _this = this;
@has_timed_out = false
@has_response = false function Problem(element) {
var _this = this;
@render(@content) this.hint_button = function () {
return Problem.prototype.hint_button.apply(_this, arguments);
$: (selector) -> };
$(selector, @el) this.enableSubmitButtonAfterTimeout = function () {
return Problem.prototype.enableSubmitButtonAfterTimeout.apply(_this, arguments);
bind: => };
if MathJax? this.enableSubmitButtonAfterResponse = function () {
@el.find('.problem > div').each (index, element) => return Problem.prototype.enableSubmitButtonAfterResponse.apply(_this, arguments);
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] };
this.enableSubmitButton = function (enable, changeText) {
window.update_schematics() if (changeText == null) {
changeText = true;
problem_prefix = @element_id.replace(/problem_/,'') }
@inputs = @$("[id^='input_#{problem_prefix}_']") return Problem.prototype.enableSubmitButton.apply(_this, arguments);
@$('div.action button').click @refreshAnswers };
@reviewButton = @$('.notification-btn.review-btn') this.enableAllButtons = function (enable, isFromCheckOperation) {
@reviewButton.click @scroll_to_problem_meta return Problem.prototype.enableAllButtons.apply(_this, arguments);
@submitButton = @$('.action .submit') };
@submitButtonLabel = @$('.action .submit .submit-label') this.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) {
@submitButtonSubmitText = @submitButtonLabel.text() return Problem.prototype.disableAllButtonsWhileRunning.apply(_this, arguments);
@submitButtonSubmittingText = @submitButton.data('submitting') };
@submitButton.click @submit_fd this.submitAnswersAndSubmitButton = function (bind) {
@hintButton = @$('.action .hint-button') if (bind == null) {
@hintButton.click @hint_button bind = false;
@resetButton = @$('.action .reset') }
@resetButton.click @reset return Problem.prototype.submitAnswersAndSubmitButton.apply(_this, arguments);
@showButton = @$('.action .show') };
@showButton.click @show this.refreshAnswers = function () {
@saveButton = @$('.action .save') return Problem.prototype.refreshAnswers.apply(_this, arguments);
@saveNotification = @$('.notification-save') };
@saveButtonLabel = @$('.action .save .save-label') this.updateMathML = function (jax, element) {
@saveButton.click @save return Problem.prototype.updateMathML.apply(_this, arguments);
@gentleAlertNotification = @$('.notification-gentle-alert') };
@submitNotification = @$('.notification-submit') this.refreshMath = function (event, element) {
return Problem.prototype.refreshMath.apply(_this, arguments);
# Accessibility helper for sighted keyboard users to show <clarification> tooltips on focus: };
@$('.clarification').focus (ev) => this.save_internal = function () {
icon = $(ev.target).children "i" return Problem.prototype.save_internal.apply(_this, arguments);
window.globalTooltipManager.openTooltip icon };
@$('.clarification').blur (ev) => this.save = function () {
window.globalTooltipManager.hide() return Problem.prototype.save.apply(_this, arguments);
};
@$('.review-btn').focus (ev) => this.gentle_alert = function (msg) {
$(ev.target).removeClass('sr'); return Problem.prototype.gentle_alert.apply(_this, arguments);
};
@$('.review-btn').blur (ev) => this.clear_all_notifications = function () {
$(ev.target).addClass('sr'); return Problem.prototype.clear_all_notifications.apply(_this, arguments);
};
@bindResetCorrectness() this.show = function () {
if @submitButton.length return Problem.prototype.show.apply(_this, arguments);
@submitAnswersAndSubmitButton true };
this.reset_internal = function () {
# Collapsibles return Problem.prototype.reset_internal.apply(_this, arguments);
Collapsible.setCollapsibles(@el) };
this.reset = function () {
# Dynamath return Problem.prototype.reset.apply(_this, arguments);
@$('input.math').keyup(@refreshMath) };
if MathJax? this.get_sr_status = function (contents) {
@$('input.math').each (index, element) => return Problem.prototype.get_sr_status.apply(_this, arguments);
MathJax.Hub.Queue [@refreshMath, null, element] };
this.submit_internal = function () {
renderProgressState: => return Problem.prototype.submit_internal.apply(_this, arguments);
detail = @el.data('progress_detail') };
status = @el.data('progress_status') this.submit = function () {
graded = @el.data('graded') return Problem.prototype.submit.apply(_this, arguments);
};
# Render 'x/y point(s)' if student has attempted question this.submit_fd = function () {
if status != 'none' and detail? and (jQuery.type(detail) == "string") and detail.indexOf('/') > 0 return Problem.prototype.submit_fd.apply(_this, arguments);
a = detail.split('/') };
earned = parseFloat(a[0]) this.focus_on_save_notification = function () {
possible = parseFloat(a[1]) return Problem.prototype.focus_on_save_notification.apply(_this, arguments);
};
if graded == "True" and possible != 0 this.focus_on_hint_notification = function () {
# This comment needs to be on one line to be properly scraped for the translators. Sry for length. return Problem.prototype.focus_on_hint_notification.apply(_this, arguments);
`// Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points)` };
progress_template = ngettext('%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', possible) this.focus_on_submit_notification = function () {
else return Problem.prototype.focus_on_submit_notification.apply(_this, arguments);
# This comment needs to be on one line to be properly scraped for the translators. Sry for length. };
`// Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points)` this.focus_on_notification = function (type) {
progress_template = ngettext('%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', possible) return Problem.prototype.focus_on_notification.apply(_this, arguments);
progress = interpolate(progress_template, {'earned': earned, 'possible': possible}, true) };
this.scroll_to_problem_meta = function () {
# Render 'x point(s) possible' if student has not yet attempted question return Problem.prototype.scroll_to_problem_meta.apply(_this, arguments);
# Status is set to none when a user has a score of 0, and 0 when the problem has a weight of 0. };
if status == 'none' or status == 0 this.submit_save_waitfor = function (callback) {
if detail? and (jQuery.type(detail) == "string") and detail.indexOf('/') > 0 return Problem.prototype.submit_save_waitfor.apply(_this, arguments);
a = detail.split('/') };
possible = parseFloat(a[1]) this.setupInputTypes = function () {
else return Problem.prototype.setupInputTypes.apply(_this, arguments);
possible = 0 };
this.poll = function (prev_timeout, focus_callback) {
if graded == "True" and possible != 0 return Problem.prototype.poll.apply(_this, arguments);
`// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).` };
progress_template = ngettext("%(num_points)s point possible (graded)", "%(num_points)s points possible (graded)", possible) this.queueing = function (focus_callback) {
else return Problem.prototype.queueing.apply(_this, arguments);
`// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).` };
progress_template = ngettext("%(num_points)s point possible (ungraded)", "%(num_points)s points possible (ungraded)", possible) this.forceUpdate = function (response) {
progress = interpolate(progress_template, {'num_points': possible}, true) return Problem.prototype.forceUpdate.apply(_this, arguments);
};
@$('.problem-progress').text(progress) this.updateProgress = function (response) {
return Problem.prototype.updateProgress.apply(_this, arguments);
updateProgress: (response) => };
if response.progress_changed this.renderProgressState = function () {
@el.data('progress_status', response.progress_status) return Problem.prototype.renderProgressState.apply(_this, arguments);
@el.data('progress_detail', response.progress_detail) };
@el.trigger('progressChanged') this.bind = function () {
@renderProgressState() return Problem.prototype.bind.apply(_this, arguments);
};
forceUpdate: (response) => this.el = $(element).find('.problems-wrapper');
@el.data('progress_status', response.progress_status) this.id = this.el.data('problem-id');
@el.data('progress_detail', response.progress_detail) this.element_id = this.el.attr('id');
@el.trigger('progressChanged') this.url = this.el.data('url');
@renderProgressState() this.content = this.el.data('content');
this.has_timed_out = false;
queueing: (focus_callback) => this.has_response = false;
@queued_items = @$(".xqueue") this.render(this.content);
@num_queued_items = @queued_items.length }
if @num_queued_items > 0
if window.queuePollerID # Only one poller 'thread' per Problem Problem.prototype.$ = function (selector) {
window.clearTimeout(window.queuePollerID) return $(selector, this.el);
window.queuePollerID = window.setTimeout( };
=> @poll(1000, focus_callback),
1000) Problem.prototype.bind = function () {
var problem_prefix,
poll: (prev_timeout, focus_callback) => _this = this;
$.postWithPrefix "#{@url}/problem_get", (response) => if (typeof MathJax !== "undefined" && MathJax !== null) {
# If queueing status changed, then render this.el.find('.problem > div').each(function (index, element) {
@new_queued_items = $(response.html).find(".xqueue") return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]);
if @new_queued_items.length isnt @num_queued_items });
edx.HtmlUtils.setHtml(@el, edx.HtmlUtils.HTML(response.html)).promise().done => }
focus_callback?() window.update_schematics();
JavascriptLoader.executeModuleScripts @el, () => problem_prefix = this.element_id.replace(/problem_/, '');
@setupInputTypes() this.inputs = this.$("[id^='input_" + problem_prefix + "_']");
@bind() this.$('div.action button').click(this.refreshAnswers);
this.reviewButton = this.$('.notification-btn.review-btn');
@num_queued_items = @new_queued_items.length this.reviewButton.click(this.scroll_to_problem_meta);
if @num_queued_items == 0 this.submitButton = this.$('.action .submit');
@forceUpdate response this.submitButtonLabel = this.$('.action .submit .submit-label');
delete window.queuePollerID this.submitButtonSubmitText = this.submitButtonLabel.text();
else this.submitButtonSubmittingText = this.submitButton.data('submitting');
new_timeout = prev_timeout * 2 this.submitButton.click(this.submit_fd);
# if the timeout is greather than 1 minute this.hintButton = this.$('.action .hint-button');
if new_timeout >= 60000 this.hintButton.click(this.hint_button);
delete window.queuePollerID this.resetButton = this.$('.action .reset');
@gentle_alert gettext("The grading process is still running. Refresh the page to see updates.") this.resetButton.click(this.reset);
else this.showButton = this.$('.action .show');
window.queuePollerID = window.setTimeout( this.showButton.click(this.show);
=> @poll(new_timeout, focus_callback), this.saveButton = this.$('.action .save');
new_timeout this.saveNotification = this.$('.notification-save');
) this.saveButtonLabel = this.$('.action .save .save-label');
this.saveButton.click(this.save);
this.gentleAlertNotification = this.$('.notification-gentle-alert');
# Use this if you want to make an ajax call on the input type object this.submitNotification = this.$('.notification-submit');
# static method so you don't have to instantiate a Problem in order to use it this.$('.clarification').focus(function (ev) {
# Input: var icon;
# url: the AJAX url of the problem icon = $(ev.target).children("i");
# input_id: the input_id of the input you would like to make the call on return window.globalTooltipManager.openTooltip(icon);
# NOTE: the id is the ${id} part of "input_${id}" during rendering });
# If this function is passed the entire prefixed id, the backend may have trouble this.$('.clarification').blur(function (ev) {
# finding the correct input return window.globalTooltipManager.hide();
# dispatch: string that indicates how this data should be handled by the inputtype });
# callback: the function that will be called once the AJAX call has been completed. this.$('.review-btn').focus(function (ev) {
# It will be passed a response object return $(ev.target).removeClass('sr');
@inputAjax: (url, input_id, dispatch, data, callback) -> });
data['dispatch'] = dispatch this.$('.review-btn').blur(function (ev) {
data['input_id'] = input_id return $(ev.target).addClass('sr');
$.postWithPrefix "#{url}/input_ajax", data, callback });
this.bindResetCorrectness();
if (this.submitButton.length) {
render: (content, focus_callback) -> this.submitAnswersAndSubmitButton(true);
if content }
@el.html(content) Collapsible.setCollapsibles(this.el);
JavascriptLoader.executeModuleScripts @el, () => this.$('input.math').keyup(this.refreshMath);
@setupInputTypes() if (typeof MathJax !== "undefined" && MathJax !== null) {
@bind() return this.$('input.math').each(function (index, element) {
@queueing(focus_callback) return MathJax.Hub.Queue([_this.refreshMath, null, element]);
@renderProgressState() });
focus_callback?() }
else };
$.postWithPrefix "#{@url}/problem_get", (response) =>
@el.html(response.html) Problem.prototype.renderProgressState = function () {
JavascriptLoader.executeModuleScripts @el, () => var a, detail, earned, graded, possible, progress, progress_template, status;
@setupInputTypes() detail = this.el.data('progress_detail');
@bind() status = this.el.data('progress_status');
@queueing() graded = this.el.data('graded');
@forceUpdate response if (status !== 'none' && (detail != null) && (jQuery.type(detail) === "string") && detail.indexOf('/') > 0) {
a = detail.split('/');
# TODO add hooks for problem types here by inspecting response.html and doing earned = parseFloat(a[0]);
# stuff if a div w a class is found possible = parseFloat(a[1]);
if (graded === "True" && possible !== 0) {
setupInputTypes: => // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points);
@inputtypeDisplays = {} progress_template = ngettext('%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', possible);
@el.find(".capa_inputtype").each (index, inputtype) => } else {
classes = $(inputtype).attr('class').split(' ') // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points);
id = $(inputtype).attr('id') progress_template = ngettext('%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', possible);
for cls in classes }
setupMethod = @inputtypeSetupMethods[cls] progress = interpolate(progress_template, {
if setupMethod? 'earned': earned,
@inputtypeDisplays[id] = setupMethod(inputtype) 'possible': possible
}, true);
# If some function wants to be called before sending the answer to the }
# server, give it a chance to do so. if (status === 'none' || status === 0) {
# if ((detail != null) && (jQuery.type(detail) === "string") && detail.indexOf('/') > 0) {
# submit_save_waitfor allows the callee to send alerts if the user's input is a = detail.split('/');
# invalid. To do so, the callee must throw an exception named "Waitfor possible = parseFloat(a[1]);
# Exception". This and any other errors or exceptions that arise from the } else {
# callee are rethrown and abort the submission. possible = 0;
# }
# In order to use this feature, add a 'data-waitfor' attribute to the input, if (graded === "True" && possible !== 0) {
# and specify the function to be called by the submit button before sending // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).;
# off @answers progress_template = ngettext("%(num_points)s point possible (graded)", "%(num_points)s points possible (graded)", possible);
submit_save_waitfor: (callback) => } else {
flag = false // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).;
for inp in @inputs progress_template = ngettext("%(num_points)s point possible (ungraded)", "%(num_points)s points possible (ungraded)", possible);
if ($(inp).is("input[waitfor]")) }
try progress = interpolate(progress_template, {
$(inp).data("waitfor")(() => 'num_points': possible
@refreshAnswers() }, true);
callback() }
) return this.$('.problem-progress').text(progress);
catch e };
if e.name == "Waitfor Exception"
alert e.message Problem.prototype.updateProgress = function (response) {
else if (response.progress_changed) {
alert "Could not grade your answer. The submission was aborted." this.el.data('progress_status', response.progress_status);
throw e this.el.data('progress_detail', response.progress_detail);
flag = true this.el.trigger('progressChanged');
else }
flag = false return this.renderProgressState();
return flag };
# Scroll to problem metadata and next focus is problem input Problem.prototype.forceUpdate = function (response) {
scroll_to_problem_meta: => this.el.data('progress_status', response.progress_status);
questionTitle = @$(".problem-header") this.el.data('progress_detail', response.progress_detail);
if questionTitle.length > 0 this.el.trigger('progressChanged');
$('html, body').animate({ return this.renderProgressState();
scrollTop: questionTitle.offset().top };
}, 500);
questionTitle.focus() Problem.prototype.queueing = function (focus_callback) {
var _this = this;
focus_on_notification: (type) => this.queued_items = this.$(".xqueue");
notification = @$('.notification-'+type) this.num_queued_items = this.queued_items.length;
if notification.length > 0 if (this.num_queued_items > 0) {
notification.focus() if (window.queuePollerID) {
window.clearTimeout(window.queuePollerID);
focus_on_submit_notification: => }
@focus_on_notification('submit') return window.queuePollerID = window.setTimeout(function () {
return _this.poll(1000, focus_callback);
focus_on_hint_notification: => }, 1000);
@focus_on_notification('hint') }
};
focus_on_save_notification: =>
@focus_on_notification('save') Problem.prototype.poll = function (prev_timeout, focus_callback) {
var _this = this;
### return $.postWithPrefix("" + this.url + "/problem_get", function (response) {
# 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, var new_timeout;
# in addition to simple querystring-based answers _this.new_queued_items = $(response.html).find(".xqueue");
# if (_this.new_queued_items.length !== _this.num_queued_items) {
# NOTE: The dispatch 'problem_check' is being singled out for the use of FormData; edx.HtmlUtils.setHtml(_this.el, edx.HtmlUtils.HTML(response.html)).promise().done(function () {
# maybe preferable to consolidate all dispatches to use FormData return typeof focus_callback === "function" ? focus_callback() : void 0;
### });
submit_fd: => JavascriptLoader.executeModuleScripts(_this.el, function () {
# If there are no file inputs in the problem, we can fall back on @submit _this.setupInputTypes();
if @el.find('input:file').length == 0 return _this.bind();
@submit() });
return }
_this.num_queued_items = _this.new_queued_items.length;
@enableSubmitButton false if (_this.num_queued_items === 0) {
_this.forceUpdate(response);
if not window.FormData return delete window.queuePollerID;
alert "Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads." } else {
@enableSubmitButton true new_timeout = prev_timeout * 2;
return if (new_timeout >= 60000) {
delete window.queuePollerID;
timeout_id = @enableSubmitButtonAfterTimeout() return _this.gentle_alert(gettext("The grading process is still running. Refresh the page to see updates."));
} else {
fd = new FormData() return window.queuePollerID = window.setTimeout(function () {
return _this.poll(new_timeout, focus_callback);
# Sanity checks on submission }, new_timeout);
max_filesize = 4*1000*1000 # 4 MB }
file_too_large = false }
file_not_selected = false });
required_files_not_submitted = false };
unallowed_file_submitted = false
Problem.inputAjax = function (url, input_id, dispatch, data, callback) {
errors = [] data['dispatch'] = dispatch;
data['input_id'] = input_id;
@inputs.each (index, element) -> return $.postWithPrefix("" + url + "/input_ajax", data, callback);
if element.type is 'file' };
required_files = $(element).data("required_files")
allowed_files = $(element).data("allowed_files") Problem.prototype.render = function (content, focus_callback) {
for file in element.files var _this = this;
if allowed_files.length != 0 and file.name not in allowed_files if (content) {
unallowed_file_submitted = true this.el.html(content);
errors.push "You submitted #{file.name}; only #{allowed_files} are allowed." return JavascriptLoader.executeModuleScripts(this.el, function () {
if file.name in required_files _this.setupInputTypes();
required_files.splice(required_files.indexOf(file.name), 1) _this.bind();
if file.size > max_filesize _this.queueing(focus_callback);
file_too_large = true _this.renderProgressState();
max_size = max_filesize / (1000*1000) return typeof focus_callback === "function" ? focus_callback() : void 0;
errors.push "Your file #{file.name} is too large (max size: {max_size}MB)" });
fd.append(element.id, file) } else {
if element.files.length == 0 return $.postWithPrefix("" + this.url + "/problem_get", function (response) {
file_not_selected = true _this.el.html(response.html);
fd.append(element.id, '') # In case we want to allow submissions with no file return JavascriptLoader.executeModuleScripts(_this.el, function () {
if required_files.length != 0 _this.setupInputTypes();
required_files_not_submitted = true _this.bind();
errors.push "You did not submit the required files: #{required_files}." _this.queueing();
else return _this.forceUpdate(response);
fd.append(element.id, element.value) });
});
}
if file_not_selected };
errors.push 'You did not select any files to submit'
Problem.prototype.setupInputTypes = function () {
error_html = '<ul>\n' var _this = this;
for error in errors this.inputtypeDisplays = {};
error_html += '<li>' + error + '</li>\n' return this.el.find(".capa_inputtype").each(function (index, inputtype) {
error_html += '</ul>' var classes, cls, id, setupMethod, _i, _len, _results;
@gentle_alert error_html classes = $(inputtype).attr('class').split(' ');
id = $(inputtype).attr('id');
abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted _results = [];
if abort_submission for (_i = 0, _len = classes.length; _i < _len; _i++) {
window.clearTimeout(timeout_id) cls = classes[_i];
@enableSubmitButton true setupMethod = _this.inputtypeSetupMethods[cls];
return if (setupMethod != null) {
_results.push(_this.inputtypeDisplays[id] = setupMethod(inputtype));
settings = } else {
type: "POST" _results.push(void 0);
data: fd }
processData: false }
contentType: false return _results;
complete: @enableSubmitButtonAfterResponse });
success: (response) => };
switch response.success
when 'incorrect', 'correct' Problem.prototype.submit_save_waitfor = function (callback) {
@render(response.contents) var flag, inp, _i, _len, _ref,
@updateProgress response _this = this;
else flag = false;
@gentle_alert response.success _ref = this.inputs;
Logger.log 'problem_graded', [@answers, response.contents], @id for (_i = 0, _len = _ref.length; _i < _len; _i++) {
inp = _ref[_i];
$.ajaxWithPrefix("#{@url}/problem_check", settings) if ($(inp).is("input[waitfor]")) {
try {
submit: => $(inp).data("waitfor")(function () {
if not @submit_save_waitfor(@submit_internal) _this.refreshAnswers();
@disableAllButtonsWhileRunning @submit_internal, true return callback();
});
submit_internal: => } catch (e) {
Logger.log 'problem_check', @answers if (e.name === "Waitfor Exception") {
$.postWithPrefix "#{@url}/problem_check", @answers, (response) => alert(e.message);
switch response.success } else {
when 'incorrect', 'correct' alert("Could not grade your answer. The submission was aborted.");
window.SR.readTexts(@get_sr_status(response.contents)) }
@el.trigger('contentChanged', [@id, response.contents]) throw e;
@render(response.contents, @focus_on_submit_notification) }
@updateProgress response flag = true;
else } else {
@saveNotification.hide() flag = false;
@gentle_alert response.success }
Logger.log 'problem_graded', [@answers, response.contents], @id }
return flag;
get_sr_status: (contents) => };
# This method builds up an array of strings to send to the page screen-reader span.
# It first gets all elements with class "status", and then looks to see if they are contained Problem.prototype.scroll_to_problem_meta = function () {
# in sections with aria-labels. If so, labels are prepended to the status element text. var questionTitle;
# If not, just the text of the status elements are returned. questionTitle = this.$(".problem-header");
status_elements = $(contents).find('.status') if (questionTitle.length > 0) {
labeled_status = [] $('html, body').animate({
for element in status_elements scrollTop: questionTitle.offset().top
parent_section = $(element).closest('section') }, 500);
added_status = false return questionTitle.focus();
if parent_section }
aria_label = parent_section.attr('aria-label') };
if aria_label
`// Translators: This is only translated to allow for reording of label and associated status.` Problem.prototype.focus_on_notification = function (type) {
template = gettext("{label}: {status}") var notification;
labeled_status.push(edx.StringUtils.interpolate(template, {label: aria_label, status: $(element).text()})) notification = this.$('.notification-' + type);
added_status = true if (notification.length > 0) {
return notification.focus();
if not added_status }
labeled_status.push($(element).text()) };
return labeled_status Problem.prototype.focus_on_submit_notification = function () {
return this.focus_on_notification('submit');
reset: => };
@disableAllButtonsWhileRunning @reset_internal, false
Problem.prototype.focus_on_hint_notification = function () {
reset_internal: => return this.focus_on_notification('hint');
Logger.log 'problem_reset', @answers };
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>
if response.success Problem.prototype.focus_on_save_notification = function () {
@el.trigger('contentChanged', [@id, response.html]) return this.focus_on_notification('save');
@render(response.html, @scroll_to_problem_meta) };
@updateProgress response
window.SR.readText(gettext('This problem has been reset.')) /*
else # 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch,
@gentle_alert response.msg # in addition to simple querystring-based answers
#
# TODO this needs modification to deal with javascript responses; perhaps we # NOTE: The dispatch 'problem_check' is being singled out for the use of FormData;
# need something where responsetypes can define their own behavior when show # maybe preferable to consolidate all dispatches to use FormData
# is called. */
show: =>
Logger.log 'problem_show', problem: @id
$.postWithPrefix "#{@url}/problem_show", (response) => Problem.prototype.submit_fd = function () {
answers = response.answers var abort_submission, error, error_html, errors, fd, file_not_selected, file_too_large, max_filesize, required_files_not_submitted, settings, timeout_id, unallowed_file_submitted, _i, _len,
$.each answers, (key, value) => _this = this;
if $.isArray(value) if (this.el.find('input:file').length === 0) {
for choice in value this.submit();
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' return;
else }
answer = @$("#answer_#{key}, #solution_#{key}") this.enableSubmitButton(false);
edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value)) if (!window.FormData) {
Collapsible.setCollapsibles(answer) alert("Submission aborted! Sorry, your browser does not support file uploads. If you can, please use Chrome or Safari which have been verified to support file uploads.");
this.enableSubmitButton(true);
# Sometimes, `value` is just a string containing a MathJax formula. return;
# If this is the case, jQuery will throw an error in some corner cases }
# because of an incorrect selector. We setup a try..catch so that timeout_id = this.enableSubmitButtonAfterTimeout();
# the script doesn't break in such cases. fd = new FormData();
# max_filesize = 4 * 1000 * 1000;
# We will fallback to the second `if statement` below, if an file_too_large = false;
# error is thrown by jQuery. file_not_selected = false;
try required_files_not_submitted = false;
solution = $(value).find('.detailed-solution') unallowed_file_submitted = false;
catch e errors = [];
solution = {} this.inputs.each(function (index, element) {
var allowed_files, file, max_size, required_files, _i, _len, _ref, _ref1, _ref2;
# TODO remove the above once everything is extracted into its own if (element.type === 'file') {
# inputtype functions. required_files = $(element).data("required_files");
allowed_files = $(element).data("allowed_files");
@el.find(".capa_inputtype").each (index, inputtype) => _ref = element.files;
classes = $(inputtype).attr('class').split(' ') for (_i = 0, _len = _ref.length; _i < _len; _i++) {
for cls in classes file = _ref[_i];
display = @inputtypeDisplays[$(inputtype).attr('id')] if (allowed_files.length !== 0 && (_ref1 = file.name, __indexOf.call(allowed_files, _ref1) < 0)) {
showMethod = @inputtypeShowAnswerMethods[cls] unallowed_file_submitted = true;
showMethod(inputtype, display, answers) if showMethod? errors.push("You submitted " + file.name + "; only " + allowed_files + " are allowed.");
}
if MathJax? if (_ref2 = file.name, __indexOf.call(required_files, _ref2) >= 0) {
@el.find('.problem > div').each (index, element) => required_files.splice(required_files.indexOf(file.name), 1);
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] }
if (file.size > max_filesize) {
@el.find('.show').attr('disabled', 'disabled') file_too_large = true;
@updateProgress response max_size = max_filesize / (1000 * 1000);
window.SR.readText(gettext('Answers to this problem are now shown. Navigate through the problem to review it with answers inline.')) errors.push("Your file " + file.name + " is too large (max size: {max_size}MB)");
@scroll_to_problem_meta() }
fd.append(element.id, file);
clear_all_notifications: => }
@submitNotification.remove() if (element.files.length === 0) {
@gentleAlertNotification.hide() file_not_selected = true;
@saveNotification.hide() fd.append(element.id, '');
}
gentle_alert: (msg) => if (required_files.length !== 0) {
edx.HtmlUtils.setHtml(@el.find('.notification-gentle-alert .notification-message'), edx.HtmlUtils.HTML(msg)) required_files_not_submitted = true;
@clear_all_notifications() return errors.push("You did not submit the required files: " + required_files + ".");
@gentleAlertNotification.show() }
@gentleAlertNotification.focus() } else {
return fd.append(element.id, element.value);
save: => }
if not @submit_save_waitfor(@save_internal) });
@disableAllButtonsWhileRunning @save_internal, false if (file_not_selected) {
errors.push('You did not select any files to submit');
save_internal: => }
Logger.log 'problem_save', @answers error_html = '<ul>\n';
$.postWithPrefix "#{@url}/problem_save", @answers, (response) => for (_i = 0, _len = errors.length; _i < _len; _i++) {
saveMessage = response.msg error = errors[_i];
if response.success error_html += '<li>' + error + '</li>\n';
@el.trigger('contentChanged', [@id, response.html]) }
edx.HtmlUtils.setHtml(@el.find('.notification-save .notification-message'), edx.HtmlUtils.HTML(saveMessage)) error_html += '</ul>';
@clear_all_notifications() this.gentle_alert(error_html);
@saveNotification.show() abort_submission = file_too_large || file_not_selected || unallowed_file_submitted || required_files_not_submitted;
@focus_on_save_notification() if (abort_submission) {
else window.clearTimeout(timeout_id);
@gentle_alert saveMessage this.enableSubmitButton(true);
return;
refreshMath: (event, element) => }
element = event.target unless element settings = {
elid = element.id.replace(/^input_/,'') type: "POST",
target = "display_" + elid data: fd,
processData: false,
# MathJax preprocessor is loaded by 'setupInputTypes' contentType: false,
preprocessor_tag = "inputtype_" + elid complete: this.enableSubmitButtonAfterResponse,
mathjax_preprocessor = @inputtypeDisplays[preprocessor_tag] success: function (response) {
switch (response.success) {
if MathJax? and jax = MathJax.Hub.getAllJax(target)[0] case 'incorrect':
eqn = $(element).val() case 'correct':
if mathjax_preprocessor _this.render(response.contents);
eqn = mathjax_preprocessor(eqn) _this.updateProgress(response);
MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element]) break;
default:
return # Explicit return for CoffeeScript _this.gentle_alert(response.success);
}
updateMathML: (jax, element) => return Logger.log('problem_graded', [_this.answers, response.contents], _this.id);
try }
$("##{element.id}_dynamath").val(jax.root.toMathML '') };
catch exception return $.ajaxWithPrefix("" + this.url + "/problem_check", settings);
throw exception unless exception.restart };
if MathJax?
MathJax.Callback.After [@refreshMath, jax], exception.restart Problem.prototype.submit = function () {
if (!this.submit_save_waitfor(this.submit_internal)) {
refreshAnswers: => return this.disableAllButtonsWhileRunning(this.submit_internal, true);
@$('input.schematic').each (index, element) -> }
element.schematic.update_value() };
@$(".CodeMirror").each (index, element) ->
element.CodeMirror.save() if element.CodeMirror.save Problem.prototype.submit_internal = function () {
@answers = @inputs.serialize() var _this = this;
Logger.log('problem_check', this.answers);
submitAnswersAndSubmitButton: (bind=false) => return $.postWithPrefix("" + this.url + "/problem_check", this.answers, function (response) {
""" switch (response.success) {
Used to check available answers and if something is checked (or the answer is set in some textbox) case 'incorrect':
"Submit" button becomes enabled. Otherwise it is disabled by default. case 'correct':
window.SR.readTexts(_this.get_sr_status(response.contents));
Arguments: _this.el.trigger('contentChanged', [_this.id, response.contents]);
bind (bool): used on the first check to attach event handlers to input fields _this.render(response.contents, _this.focus_on_submit_notification);
to change "Submit" enable status in case of some manipulations with answers _this.updateProgress(response);
""" break;
answered = true default:
_this.saveNotification.hide();
at_least_one_text_input_found = false _this.gentle_alert(response.success);
one_text_input_filled = false }
@el.find("input:text").each (i, text_field) => return Logger.log('problem_graded', [_this.answers, response.contents], _this.id);
if $(text_field).is(':visible') });
at_least_one_text_input_found = true };
if $(text_field).val() isnt ''
one_text_input_filled = true Problem.prototype.get_sr_status = function (contents) {
if bind var added_status, aria_label, element, labeled_status, parent_section, status_elements, template, _i, _len;
$(text_field).on 'input', (e) => status_elements = $(contents).find('.status');
@saveNotification.hide() labeled_status = [];
@submitAnswersAndSubmitButton() for (_i = 0, _len = status_elements.length; _i < _len; _i++) {
return element = status_elements[_i];
return parent_section = $(element).closest('section');
if at_least_one_text_input_found and not one_text_input_filled added_status = false;
answered = false if (parent_section) {
aria_label = parent_section.attr('aria-label');
@el.find(".choicegroup").each (i, choicegroup_block) => if (aria_label) {
checked = false // Translators: This is only translated to allow for reording of label and associated status.;
$(choicegroup_block).find("input[type=checkbox], input[type=radio]").each (j, checkbox_or_radio) => template = gettext("{label}: {status}");
if $(checkbox_or_radio).is(':checked') labeled_status.push(edx.StringUtils.interpolate(template, {
checked = true label: aria_label,
if bind status: $(element).text()
$(checkbox_or_radio).on 'click', (e) => }));
@saveNotification.hide() added_status = true;
@submitAnswersAndSubmitButton() }
return }
return if (!added_status) {
if not checked labeled_status.push($(element).text());
answered = false }
return }
return labeled_status;
@el.find("select").each (i, select_field) => };
selected_option = $(select_field).find("option:selected").text().trim()
if selected_option is 'Select an option' Problem.prototype.reset = function () {
answered = false return this.disableAllButtonsWhileRunning(this.reset_internal, false);
if bind };
$(select_field).on 'change', (e) =>
@saveNotification.hide() Problem.prototype.reset_internal = function () {
@submitAnswersAndSubmitButton() var _this = this;
return Logger.log('problem_reset', this.answers);
return return $.postWithPrefix("" + this.url + "/problem_reset", {
id: this.id
if answered }, function (response) {
@enableSubmitButton true if (response.success) {
else _this.el.trigger('contentChanged', [_this.id, response.html]);
@enableSubmitButton false, false _this.render(response.html, _this.scroll_to_problem_meta);
_this.updateProgress(response);
bindResetCorrectness: -> return window.SR.readText(gettext('This problem has been reset.'));
# Loop through all input types } else {
# Bind the reset functions at that scope. return _this.gentle_alert(response.msg);
$inputtypes = @el.find(".capa_inputtype").add(@el.find(".inputtype")) }
$inputtypes.each (index, inputtype) => });
classes = $(inputtype).attr('class').split(' ') };
for cls in classes
bindMethod = @bindResetCorrectnessByInputtype[cls] Problem.prototype.show = function () {
if bindMethod? var _this = this;
bindMethod(inputtype) Logger.log('problem_show', {
problem: this.id
# Find all places where each input type displays its correct-ness });
# Replace them with their original state--'unanswered'. return $.postWithPrefix("" + this.url + "/problem_show", function (response) {
bindResetCorrectnessByInputtype: var answers;
# These are run at the scope of the capa inputtype answers = response.answers;
# They should set handlers on each <input> to reset the whole. $.each(answers, function (key, value) {
formulaequationinput: (element) -> var answer, choice, solution, _i, _len, _results;
$(element).find('input').on 'input', -> if ($.isArray(value)) {
$p = $(element).find('span.status') _results = [];
`// Translators: the word unanswered here is about answering a problem the student must solve.` for (_i = 0, _len = value.length; _i < _len; _i++) {
$p.parent().removeClass().addClass "unsubmitted" choice = value[_i];
_results.push(_this.$("label[for='input_" + key + "_" + choice + "']").attr({
choicegroup: (element) -> correct_answer: 'true'
$element = $(element) }));
id = ($element.attr('id').match /^inputtype_(.*)$/)[1] }
$element.find('input').on 'change', -> return _results;
$status = $("#status_#{id}") } else {
if $status[0] # We found a status icon. answer = _this.$("#answer_" + key + ", #solution_" + key);
$status.removeClass().addClass "unanswered" edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value));
$status.empty().css 'display', 'inline-block' Collapsible.setCollapsibles(answer);
else try {
# Recreate the unanswered dot on left. return solution = $(value).find('.detailed-solution');
$("<span>", {"class": "unanswered", "style": "display: inline-block;", "id": "status_#{id}"}) } catch (e) {
return solution = {};
$element.find("label").removeClass() }
}
'option-input': (element) -> });
$select = $(element).find('select') _this.el.find(".capa_inputtype").each(function (index, inputtype) {
id = ($select.attr('id').match /^input_(.*)$/)[1] var classes, cls, display, showMethod, _i, _len, _results;
$select.on 'change', -> classes = $(inputtype).attr('class').split(' ');
$status = $("#status_#{id}") _results = [];
.removeClass().addClass("unanswered") for (_i = 0, _len = classes.length; _i < _len; _i++) {
.find('span').text(gettext('Status: unsubmitted')) cls = classes[_i];
display = _this.inputtypeDisplays[$(inputtype).attr('id')];
textline: (element) -> showMethod = _this.inputtypeShowAnswerMethods[cls];
$(element).find('input').on 'input', -> if (showMethod != null) {
$p = $(element).find('span.status') _results.push(showMethod(inputtype, display, answers));
`// Translators: the word unanswered here is about answering a problem the student must solve.` } else {
$p.parent().removeClass("correct incorrect").addClass "unsubmitted" _results.push(void 0);
}
inputtypeSetupMethods: }
return _results;
'text-input-dynamath': (element) => });
### if (typeof MathJax !== "undefined" && MathJax !== null) {
Return: function (eqn) -> eqn that preprocesses the user formula input before _this.el.find('.problem > div').each(function (index, element) {
it is fed into MathJax. Return 'false' if no preprocessor specified return MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]);
### });
data = $(element).find('.text-input-dynamath_data') }
_this.el.find('.show').attr('disabled', 'disabled');
preprocessorClassName = data.data('preprocessor') _this.updateProgress(response);
preprocessorClass = window[preprocessorClassName] window.SR.readText(gettext('Answers to this problem are now shown. Navigate through the problem to review it with answers inline.'));
if not preprocessorClass? return _this.scroll_to_problem_meta();
return false });
else };
preprocessor = new preprocessorClass()
return preprocessor.fn Problem.prototype.clear_all_notifications = function () {
this.submitNotification.remove();
javascriptinput: (element) => this.gentleAlertNotification.hide();
return this.saveNotification.hide();
data = $(element).find(".javascriptinput_data") };
params = data.data("params") Problem.prototype.gentle_alert = function (msg) {
submission = data.data("submission") edx.HtmlUtils.setHtml(this.el.find('.notification-gentle-alert .notification-message'), edx.HtmlUtils.HTML(msg));
evaluation = data.data("evaluation") this.clear_all_notifications();
problemState = data.data("problem_state") this.gentleAlertNotification.show();
displayClass = window[data.data('display_class')] return this.gentleAlertNotification.focus();
};
if evaluation == ''
evaluation = null Problem.prototype.save = function () {
if (!this.submit_save_waitfor(this.save_internal)) {
container = $(element).find(".javascriptinput_container") return this.disableAllButtonsWhileRunning(this.save_internal, false);
submissionField = $(element).find(".javascriptinput_input") }
};
display = new displayClass(problemState, submission, evaluation, container, submissionField, params)
display.render() Problem.prototype.save_internal = function () {
var _this = this;
return display Logger.log('problem_save', this.answers);
return $.postWithPrefix("" + this.url + "/problem_save", this.answers, function (response) {
cminput: (container) => var saveMessage;
element = $(container).find("textarea") saveMessage = response.msg;
tabsize = element.data("tabsize") if (response.success) {
mode = element.data("mode") _this.el.trigger('contentChanged', [_this.id, response.html]);
linenumbers = element.data("linenums") edx.HtmlUtils.setHtml(_this.el.find('.notification-save .notification-message'), edx.HtmlUtils.HTML(saveMessage));
spaces = Array(parseInt(tabsize) + 1).join(" ") _this.clear_all_notifications();
CodeMirrorEditor = CodeMirror.fromTextArea element[0], { _this.saveNotification.show();
lineNumbers: linenumbers return _this.focus_on_save_notification();
indentUnit: tabsize } else {
tabSize: tabsize return _this.gentle_alert(saveMessage);
mode: mode }
matchBrackets: true });
lineWrapping: true };
indentWithTabs: false
smartIndent: false Problem.prototype.refreshMath = function (event, element) {
extraKeys: { var elid, eqn, jax, mathjax_preprocessor, preprocessor_tag, target;
"Esc": (cm) -> if (!element) {
$(".grader-status").focus() element = event.target;
return false }
"Tab": (cm) -> elid = element.id.replace(/^input_/, '');
cm.replaceSelection(spaces, "end") target = "display_" + elid;
return false preprocessor_tag = "inputtype_" + elid;
} mathjax_preprocessor = this.inputtypeDisplays[preprocessor_tag];
} if ((typeof MathJax !== "undefined" && MathJax !== null) && (jax = MathJax.Hub.getAllJax(target)[0])) {
id = element.attr("id").replace(/^input_/, "") eqn = $(element).val();
CodeMirrorTextArea = CodeMirrorEditor.getInputField() if (mathjax_preprocessor) {
CodeMirrorTextArea.setAttribute("id", "cm-textarea-#{id}") eqn = mathjax_preprocessor(eqn);
CodeMirrorTextArea.setAttribute("aria-describedby", "cm-editor-exit-message-#{id} status_#{id}") }
return CodeMirrorEditor MathJax.Hub.Queue(['Text', jax, eqn], [this.updateMathML, jax, element]);
}
inputtypeShowAnswerMethods: };
choicegroup: (element, display, answers) =>
element = $(element) Problem.prototype.updateMathML = function (jax, element) {
try {
input_id = element.attr('id').replace(/inputtype_/, '') return $("#" + element.id + "_dynamath").val(jax.root.toMathML(''));
answer = answers[input_id] } catch (exception) {
for choice in answer if (!exception.restart) {
element.find("#input_#{input_id}_#{choice}").parent("label").addClass 'choicegroup_correct' throw exception;
}
javascriptinput: (element, display, answers) => if (typeof MathJax !== "undefined" && MathJax !== null) {
answer_id = $(element).attr('id').split("_")[1...].join("_") return MathJax.Callback.After([this.refreshMath, jax], exception.restart);
answer = JSON.parse(answers[answer_id]) }
display.showAnswer(answer) }
};
choicetextgroup: (element, display, answers) =>
element = $(element) Problem.prototype.refreshAnswers = function () {
this.$('input.schematic').each(function (index, element) {
input_id = element.attr('id').replace(/inputtype_/, '') return element.schematic.update_value();
answer = answers[input_id] });
for choice in answer this.$(".CodeMirror").each(function (index, element) {
element.find("section#forinput#{choice}").addClass 'choicetextgroup_show_correct' if (element.CodeMirror.save) {
return element.CodeMirror.save();
imageinput: (element, display, answers) => }
# answers is a dict of (answer_id, answer_text) for each answer for this });
# question. return this.answers = this.inputs.serialize();
# @Examples: };
# {'anwser_id': {
# 'rectangle': '(10,10)-(20,30);(12,12)-(40,60)', Problem.prototype.submitAnswersAndSubmitButton = function (bind) {
# 'regions': '[[10,10], [30,30], [10, 30], [30, 10]]' var answered, at_least_one_text_input_found, one_text_input_filled,
# } } _this = this;
types = if (bind == null) {
rectangle: (ctx, coords) => bind = false;
reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/ }
rects = coords.replace(/\s*/g, '').split(/;/) "Used to check available answers and if something is checked (or the answer is set in some textbox)\n\"Submit\" button becomes enabled. Otherwise it is disabled by default.\n\nArguments:\n bind (bool): used on the first check to attach event handlers to input fields\n to change \"Submit\" enable status in case of some manipulations with answers";
answered = true;
$.each rects, (index, rect) => at_least_one_text_input_found = false;
abs = Math.abs one_text_input_filled = false;
points = reg.exec(rect) this.el.find("input:text").each(function (i, text_field) {
if points if ($(text_field).is(':visible')) {
width = abs(points[3] - points[1]) at_least_one_text_input_found = true;
height = abs(points[4] - points[2]) if ($(text_field).val() !== '') {
one_text_input_filled = true;
ctx.rect(points[1], points[2], width, height) }
if (bind) {
ctx.stroke() $(text_field).on('input', function (e) {
ctx.fill() _this.saveNotification.hide();
_this.submitAnswersAndSubmitButton();
regions: (ctx, coords) => });
parseCoords = (coords) => }
reg = JSON.parse(coords) }
});
# Regions is list of lists [region1, region2, region3, ...] where regionN if (at_least_one_text_input_found && !one_text_input_filled) {
# is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. answered = false;
# If there is only one region in the list, simpler notation can be used: }
# regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly this.el.find(".choicegroup").each(function (i, choicegroup_block) {
# setting outer list) var checked;
if typeof reg[0][0][0] == "undefined" checked = false;
# we have [[1,2],[3,4],[5,6]] - single region $(choicegroup_block).find("input[type=checkbox], input[type=radio]").each(function (j, checkbox_or_radio) {
# instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] if ($(checkbox_or_radio).is(':checked')) {
# or [[[1,2],[3,4],[5,6]]] - multiple regions syntax checked = true;
reg = [reg] }
if (bind) {
return reg $(checkbox_or_radio).on('click', function (e) {
_this.saveNotification.hide();
$.each parseCoords(coords), (index, region) => _this.submitAnswersAndSubmitButton();
ctx.beginPath() });
$.each region, (index, point) => }
if index is 0 });
ctx.moveTo(point[0], point[1]) if (!checked) {
else answered = false;
ctx.lineTo(point[0], point[1]); }
});
ctx.closePath() this.el.find("select").each(function (i, select_field) {
ctx.stroke() var selected_option;
ctx.fill() selected_option = $(select_field).find("option:selected").text().trim();
if (selected_option === 'Select an option') {
element = $(element) answered = false;
id = element.attr('id').replace(/inputtype_/,'') }
container = element.find("#answer_#{id}") if (bind) {
canvas = document.createElement('canvas') $(select_field).on('change', function (e) {
canvas.width = container.data('width') _this.saveNotification.hide();
canvas.height = container.data('height') _this.submitAnswersAndSubmitButton();
});
if canvas.getContext }
ctx = canvas.getContext('2d') });
else if (answered) {
return console.log 'Canvas is not supported.' return this.enableSubmitButton(true);
} else {
ctx.fillStyle = 'rgba(255,255,255,.3)'; return this.enableSubmitButton(false, false);
ctx.strokeStyle = "#FF0000"; }
ctx.lineWidth = "2"; };
if answers[id] Problem.prototype.bindResetCorrectness = function () {
$.each answers[id], (key, value) => var $inputtypes,
types[key](ctx, value) if types[key]? and value _this = this;
container.html(canvas) $inputtypes = this.el.find(".capa_inputtype").add(this.el.find(".inputtype"));
else return $inputtypes.each(function (index, inputtype) {
console.log "Answer is absent for image input with id=#{id}" var bindMethod, classes, cls, _i, _len, _results;
classes = $(inputtype).attr('class').split(' ');
inputtypeHideAnswerMethods: _results = [];
choicegroup: (element, display) => for (_i = 0, _len = classes.length; _i < _len; _i++) {
element = $(element) cls = classes[_i];
element.find('label').removeClass('choicegroup_correct') bindMethod = _this.bindResetCorrectnessByInputtype[cls];
if (bindMethod != null) {
javascriptinput: (element, display) => _results.push(bindMethod(inputtype));
display.hideAnswer() } else {
_results.push(void 0);
choicetextgroup: (element, display) => }
element = $(element) }
element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct') return _results;
});
disableAllButtonsWhileRunning: (operationCallback, isFromCheckOperation) => };
# Used to keep the buttons disabled while operationCallback is running.
# params: Problem.prototype.bindResetCorrectnessByInputtype = {
# 'operationCallback' is an operation to be run. formulaequationinput: function (element) {
# 'isFromCheckOperation' is a boolean to keep track if 'operationCallback' was return $(element).find('input').on('input', function () {
# @submit, if so then text of submit button will be changed as well. var $p;
@enableAllButtons false, isFromCheckOperation $p = $(element).find('span.status');
operationCallback().always => // Translators: the word unanswered here is about answering a problem the student must solve.;
@enableAllButtons true, isFromCheckOperation return $p.parent().removeClass().addClass("unsubmitted");
});
# Called by disableAllButtonsWhileRunning to automatically disable all buttons while check,reset, or },
# save internal are running. Then enable all the buttons again after it is done. choicegroup: function (element) {
enableAllButtons: (enable, isFromCheckOperation) => var $element, id;
# Used to enable/disable all buttons in problem. $element = $(element);
# params: id = ($element.attr('id').match(/^inputtype_(.*)$/))[1];
# 'enable' is a boolean to determine enabling/disabling of buttons. return $element.find('input').on('change', function () {
# 'isFromCheckOperation' is a boolean to keep track if operation was initiated var $status;
# from @submit so that text of submit button will also be changed while disabling/enabling $status = $("#status_" + id);
# the submit button. if ($status[0]) {
if enable $status.removeClass().addClass("unanswered");
@resetButton $status.empty().css('display', 'inline-block');
.add(@saveButton) } else {
.add(@hintButton) $("<span>", {
.add(@showButton) "class": "unanswered",
.removeAttr 'disabled' "style": "display: inline-block;",
else "id": "status_" + id
@resetButton });
.add(@saveButton) }
.add(@hintButton) return $element.find("label").removeClass();
.add(@showButton) });
.attr({'disabled': 'disabled'}) },
'option-input': function (element) {
@enableSubmitButton enable, isFromCheckOperation var $select, id;
$select = $(element).find('select');
enableSubmitButton: (enable, changeText = true) => id = ($select.attr('id').match(/^input_(.*)$/))[1];
# Used to disable submit button to reduce chance of accidental double-submissions. return $select.on('change', function () {
# params: var $status;
# 'enable' is a boolean to determine enabling/disabling of submit button. return $status = $("#status_" + id).removeClass().addClass("unanswered").find('span').text(gettext('Status: unsubmitted'));
# 'changeText' is a boolean to determine if there is need to change the });
# text of submit button as well. },
if enable textline: function (element) {
submitCanBeEnabled = @submitButton.data('should-enable-submit-button') == 'True' return $(element).find('input').on('input', function () {
if submitCanBeEnabled var $p;
@submitButton.removeAttr 'disabled' $p = $(element).find('span.status');
if changeText // Translators: the word unanswered here is about answering a problem the student must solve.;
@submitButtonLabel.text(@submitButtonSubmitText) return $p.parent().removeClass("correct incorrect").addClass("unsubmitted");
else });
@submitButton.attr({'disabled': 'disabled'}) }
if changeText };
@submitButtonLabel.text(@submitButtonSubmittingText)
Problem.prototype.inputtypeSetupMethods = {
enableSubmitButtonAfterResponse: => 'text-input-dynamath': function (element) {
@has_response = true /*
if not @has_timed_out Return: function (eqn) -> eqn that preprocesses the user formula input before
# Server has returned response before our timeout it is fed into MathJax. Return 'false' if no preprocessor specified
@enableSubmitButton false */
else
@enableSubmitButton true var data, preprocessor, preprocessorClass, preprocessorClassName;
data = $(element).find('.text-input-dynamath_data');
enableSubmitButtonAfterTimeout: => preprocessorClassName = data.data('preprocessor');
@has_timed_out = false preprocessorClass = window[preprocessorClassName];
@has_response = false if (preprocessorClass == null) {
enableSubmitButton = () => return false;
@has_timed_out = true } else {
if @has_response preprocessor = new preprocessorClass();
@enableSubmitButton true return preprocessor.fn;
window.setTimeout(enableSubmitButton, 750) }
},
hint_button: => javascriptinput: function (element) {
# Store the index of the currently shown hint as an attribute. var container, data, display, displayClass, evaluation, params, problemState, submission, submissionField;
# Use that to compute the next hint number when the button is clicked. data = $(element).find(".javascriptinput_data");
hint_container = @.$('.problem-hint') params = data.data("params");
hint_index = hint_container.attr('hint_index') submission = data.data("submission");
if hint_index == undefined evaluation = data.data("evaluation");
next_index = 0 problemState = data.data("problem_state");
else displayClass = window[data.data('display_class')];
next_index = parseInt(hint_index) + 1 if (evaluation === '') {
$.postWithPrefix "#{@url}/hint_button", hint_index: next_index, input_id: @id, (response) => evaluation = null;
if response.success }
hint_msg_container = @.$('.problem-hint .notification-message') container = $(element).find(".javascriptinput_container");
hint_container.attr('hint_index', response.hint_index) submissionField = $(element).find(".javascriptinput_input");
edx.HtmlUtils.setHtml(hint_msg_container, edx.HtmlUtils.HTML(response.msg)) display = new displayClass(problemState, submission, evaluation, container, submissionField, params);
# Update any Mathjax entries display.render();
MathJax.Hub.Queue [ return display;
'Typeset' },
MathJax.Hub cminput: function (container) {
hint_container[0] var CodeMirrorEditor, CodeMirrorTextArea, element, id, linenumbers, mode, spaces, tabsize;
] element = $(container).find("textarea");
# Enable/Disable the next hint button tabsize = element.data("tabsize");
if response.should_enable_next_hint mode = element.data("mode");
@hintButton.removeAttr 'disabled' linenumbers = element.data("linenums");
else spaces = Array(parseInt(tabsize) + 1).join(" ");
@hintButton.attr({'disabled': 'disabled'}) CodeMirrorEditor = CodeMirror.fromTextArea(element[0], {
@el.find('.notification-hint').show() lineNumbers: linenumbers,
@focus_on_hint_notification() indentUnit: tabsize,
else tabSize: tabsize,
@gentle_alert response.msg mode: mode,
matchBrackets: true,
lineWrapping: true,
indentWithTabs: false,
smartIndent: false,
extraKeys: {
"Esc": function (cm) {
$(".grader-status").focus();
return false;
},
"Tab": function (cm) {
cm.replaceSelection(spaces, "end");
return false;
}
}
});
id = element.attr("id").replace(/^input_/, "");
CodeMirrorTextArea = CodeMirrorEditor.getInputField();
CodeMirrorTextArea.setAttribute("id", "cm-textarea-" + id);
CodeMirrorTextArea.setAttribute("aria-describedby", "cm-editor-exit-message-" + id + " status_" + id);
return CodeMirrorEditor;
}
};
Problem.prototype.inputtypeShowAnswerMethods = {
choicegroup: function (element, display, answers) {
var answer, choice, input_id, _i, _len, _results;
element = $(element);
input_id = element.attr('id').replace(/inputtype_/, '');
answer = answers[input_id];
_results = [];
for (_i = 0, _len = answer.length; _i < _len; _i++) {
choice = answer[_i];
_results.push(element.find("#input_" + input_id + "_" + choice).parent("label").addClass('choicegroup_correct'));
}
return _results;
},
javascriptinput: function (element, display, answers) {
var answer, answer_id;
answer_id = $(element).attr('id').split("_").slice(1).join("_");
answer = JSON.parse(answers[answer_id]);
return display.showAnswer(answer);
},
choicetextgroup: function (element, display, answers) {
var answer, choice, input_id, _i, _len, _results;
element = $(element);
input_id = element.attr('id').replace(/inputtype_/, '');
answer = answers[input_id];
_results = [];
for (_i = 0, _len = answer.length; _i < _len; _i++) {
choice = answer[_i];
_results.push(element.find("section#forinput" + choice).addClass('choicetextgroup_show_correct'));
}
return _results;
},
imageinput: function (element, display, answers) {
var canvas, container, ctx, id, types;
types = {
rectangle: function (ctx, coords) {
var rects, reg;
reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/;
rects = coords.replace(/\s*/g, '').split(/;/);
$.each(rects, function (index, rect) {
var abs, height, points, width;
abs = Math.abs;
points = reg.exec(rect);
if (points) {
width = abs(points[3] - points[1]);
height = abs(points[4] - points[2]);
return ctx.rect(points[1], points[2], width, height);
}
});
ctx.stroke();
return ctx.fill();
},
regions: function (ctx, coords) {
var parseCoords;
parseCoords = function (coords) {
var reg;
reg = JSON.parse(coords);
if (typeof reg[0][0][0] === "undefined") {
reg = [reg];
}
return reg;
};
return $.each(parseCoords(coords), function (index, region) {
ctx.beginPath();
$.each(region, function (index, point) {
if (index === 0) {
return ctx.moveTo(point[0], point[1]);
} else {
return ctx.lineTo(point[0], point[1]);
}
});
ctx.closePath();
ctx.stroke();
return ctx.fill();
});
}
};
element = $(element);
id = element.attr('id').replace(/inputtype_/, '');
container = element.find("#answer_" + id);
canvas = document.createElement('canvas');
canvas.width = container.data('width');
canvas.height = container.data('height');
if (canvas.getContext) {
ctx = canvas.getContext('2d');
} else {
return console.log('Canvas is not supported.');
}
ctx.fillStyle = 'rgba(255,255,255,.3)';
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = "2";
if (answers[id]) {
$.each(answers[id], function (key, value) {
if ((types[key] != null) && value) {
return types[key](ctx, value);
}
});
return container.html(canvas);
} else {
return console.log("Answer is absent for image input with id=" + id);
}
}
};
Problem.prototype.inputtypeHideAnswerMethods = {
choicegroup: function (element, display) {
element = $(element);
return element.find('label').removeClass('choicegroup_correct');
},
javascriptinput: function (element, display) {
return display.hideAnswer();
},
choicetextgroup: function (element, display) {
element = $(element);
return element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct');
}
};
Problem.prototype.disableAllButtonsWhileRunning = function (operationCallback, isFromCheckOperation) {
var _this = this;
this.enableAllButtons(false, isFromCheckOperation);
return operationCallback().always(function () {
return _this.enableAllButtons(true, isFromCheckOperation);
});
};
Problem.prototype.enableAllButtons = function (enable, isFromCheckOperation) {
if (enable) {
this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton).removeAttr('disabled');
} else {
this.resetButton.add(this.saveButton).add(this.hintButton).add(this.showButton).attr({
'disabled': 'disabled'
});
}
return this.enableSubmitButton(enable, isFromCheckOperation);
};
Problem.prototype.enableSubmitButton = function (enable, changeText) {
var submitCanBeEnabled;
if (changeText == null) {
changeText = true;
}
if (enable) {
submitCanBeEnabled = this.submitButton.data('should-enable-submit-button') === 'True';
if (submitCanBeEnabled) {
this.submitButton.removeAttr('disabled');
}
if (changeText) {
return this.submitButtonLabel.text(this.submitButtonSubmitText);
}
} else {
this.submitButton.attr({
'disabled': 'disabled'
});
if (changeText) {
return this.submitButtonLabel.text(this.submitButtonSubmittingText);
}
}
};
Problem.prototype.enableSubmitButtonAfterResponse = function () {
this.has_response = true;
if (!this.has_timed_out) {
return this.enableSubmitButton(false);
} else {
return this.enableSubmitButton(true);
}
};
Problem.prototype.enableSubmitButtonAfterTimeout = function () {
var enableSubmitButton,
_this = this;
this.has_timed_out = false;
this.has_response = false;
enableSubmitButton = function () {
_this.has_timed_out = true;
if (_this.has_response) {
return _this.enableSubmitButton(true);
}
};
return window.setTimeout(enableSubmitButton, 750);
};
Problem.prototype.hint_button = function () {
var hint_container, hint_index, next_index,
_this = this;
hint_container = this.$('.problem-hint');
hint_index = hint_container.attr('hint_index');
if (hint_index === void 0) {
next_index = 0;
} else {
next_index = parseInt(hint_index) + 1;
}
return $.postWithPrefix("" + this.url + "/hint_button", {
hint_index: next_index,
input_id: this.id
}, function (response) {
var hint_msg_container;
if (response.success) {
hint_msg_container = _this.$('.problem-hint .notification-message');
hint_container.attr('hint_index', response.hint_index);
edx.HtmlUtils.setHtml(hint_msg_container, edx.HtmlUtils.HTML(response.msg));
MathJax.Hub.Queue(['Typeset', MathJax.Hub, hint_container[0]]);
if (response.should_enable_next_hint) {
_this.hintButton.removeAttr('disabled');
} else {
_this.hintButton.attr({
'disabled': 'disabled'
});
}
_this.el.find('.notification-hint').show();
return _this.focus_on_hint_notification();
} else {
return _this.gentle_alert(response.msg);
}
});
};
return Problem;
}).call(this);
}).call(this);
...@@ -216,7 +216,7 @@ class @Sequence ...@@ -216,7 +216,7 @@ class @Sequence
widget_placement: widget_placement widget_placement: widget_placement
# On Sequence change, destroy any existing polling thread # On Sequence change, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee # for queued submissions, see ../capa/display.js
if window.queuePollerID if window.queuePollerID
window.clearTimeout(window.queuePollerID) window.clearTimeout(window.queuePollerID)
delete window.queuePollerID delete window.queuePollerID
......
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