1352 lines
64 KiB
JavaScript
1352 lines
64 KiB
JavaScript
/* global MathJax, Collapsible, interpolate, JavascriptLoader, Logger, CodeMirror */
|
|
// Note: this code was originally converted from CoffeeScript, and thus follows some
|
|
// coding conventions that are discouraged by eslint. Some warnings have been suppressed
|
|
// to avoid substantial rewriting of the code. Allow the eslint suppressions to exceed
|
|
// the max line length of 120.
|
|
/* eslint max-len: ["error", 120, { "ignoreComments": true }] */
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
var indexOfHelper = [].indexOf
|
|
|| function(item) {
|
|
var i, len;
|
|
for (i = 0, len = this.length; i < len; i++) {
|
|
if (i in this && this[i] === item) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
this.Problem = (function() {
|
|
function Problem(element) {
|
|
var that = this;
|
|
this.hint_button = function() {
|
|
return Problem.prototype.hint_button.apply(that, arguments);
|
|
};
|
|
this.enableSubmitButtonAfterTimeout = function() {
|
|
return Problem.prototype.enableSubmitButtonAfterTimeout.apply(that, arguments);
|
|
};
|
|
this.enableSubmitButtonAfterResponse = function() {
|
|
return Problem.prototype.enableSubmitButtonAfterResponse.apply(that, arguments);
|
|
};
|
|
this.enableSubmitButton = function(enable, changeText) {
|
|
if (changeText === null || changeText === undefined) {
|
|
changeText = true; // eslint-disable-line no-param-reassign
|
|
}
|
|
return Problem.prototype.enableSubmitButton.apply(that, arguments);
|
|
};
|
|
this.disableAllButtonsWhileRunning = function(
|
|
operationCallback, isFromCheckOperation // eslint-disable-line no-unused-vars
|
|
) {
|
|
return Problem.prototype.disableAllButtonsWhileRunning.apply(that, arguments);
|
|
};
|
|
this.submitAnswersAndSubmitButton = function(bind) {
|
|
if (bind === null || bind === undefined) {
|
|
bind = false; // eslint-disable-line no-param-reassign
|
|
}
|
|
return Problem.prototype.submitAnswersAndSubmitButton.apply(that, arguments);
|
|
};
|
|
this.refreshAnswers = function() {
|
|
return Problem.prototype.refreshAnswers.apply(that, arguments);
|
|
};
|
|
this.updateMathML = function(jax, el) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.updateMathML.apply(that, arguments);
|
|
};
|
|
this.refreshMath = function(event, el) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.refreshMath.apply(that, arguments);
|
|
};
|
|
this.save_internal = function() {
|
|
return Problem.prototype.save_internal.apply(that, arguments);
|
|
};
|
|
this.save = function() {
|
|
return Problem.prototype.save.apply(that, arguments);
|
|
};
|
|
this.gentle_alert = function(msg) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.gentle_alert.apply(that, arguments);
|
|
};
|
|
this.clear_all_notifications = function() {
|
|
return Problem.prototype.clear_all_notifications.apply(that, arguments);
|
|
};
|
|
this.show = function() {
|
|
return Problem.prototype.show.apply(that, arguments);
|
|
};
|
|
this.reset_internal = function() {
|
|
return Problem.prototype.reset_internal.apply(that, arguments);
|
|
};
|
|
this.reset = function() {
|
|
return Problem.prototype.reset.apply(that, arguments);
|
|
};
|
|
this.get_sr_status = function(contents) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.get_sr_status.apply(that, arguments);
|
|
};
|
|
this.submit_internal = function() {
|
|
return Problem.prototype.submit_internal.apply(that, arguments);
|
|
};
|
|
this.submit = function() {
|
|
return Problem.prototype.submit.apply(that, arguments);
|
|
};
|
|
this.submit_fd = function() {
|
|
return Problem.prototype.submit_fd.apply(that, arguments);
|
|
};
|
|
this.focus_on_save_notification = function() {
|
|
return Problem.prototype.focus_on_save_notification.apply(that, arguments);
|
|
};
|
|
this.focus_on_hint_notification = function() {
|
|
return Problem.prototype.focus_on_hint_notification.apply(that, arguments);
|
|
};
|
|
this.focus_on_submit_notification = function() {
|
|
return Problem.prototype.focus_on_submit_notification.apply(that, arguments);
|
|
};
|
|
this.focus_on_notification = function(type) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.focus_on_notification.apply(that, arguments);
|
|
};
|
|
this.scroll_to_problem_meta = function() {
|
|
return Problem.prototype.scroll_to_problem_meta.apply(that, arguments);
|
|
};
|
|
this.submit_save_waitfor = function(callback) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.submit_save_waitfor.apply(that, arguments);
|
|
};
|
|
this.setupInputTypes = function() {
|
|
return Problem.prototype.setupInputTypes.apply(that, arguments);
|
|
};
|
|
this.poll = function(prevTimeout, focusCallback // eslint-disable-line no-unused-vars
|
|
) {
|
|
return Problem.prototype.poll.apply(that, arguments);
|
|
};
|
|
this.queueing = function(focusCallback) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.queueing.apply(that, arguments);
|
|
};
|
|
this.forceUpdate = function(response) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.forceUpdate.apply(that, arguments);
|
|
};
|
|
this.updateProgress = function(response) { // eslint-disable-line no-unused-vars
|
|
return Problem.prototype.updateProgress.apply(that, arguments);
|
|
};
|
|
this.renderProgressState = function() {
|
|
return Problem.prototype.renderProgressState.apply(that, arguments);
|
|
};
|
|
this.bind = function() {
|
|
return Problem.prototype.bind.apply(that, arguments);
|
|
};
|
|
this.el = $(element).find('.problems-wrapper');
|
|
this.id = this.el.data('problem-id');
|
|
this.element_id = this.el.attr('id');
|
|
this.url = this.el.data('url');
|
|
this.content = this.el.data('content');
|
|
|
|
// has_timed_out and has_response are used to ensure that
|
|
// we wait a minimum of ~ 1s before transitioning the submit
|
|
// button from disabled to enabled
|
|
this.has_timed_out = false;
|
|
this.has_response = false;
|
|
this.render(this.content);
|
|
}
|
|
|
|
Problem.prototype.$ = function(selector) {
|
|
return $(selector, this.el);
|
|
};
|
|
|
|
Problem.prototype.bind = function() {
|
|
var problemPrefix,
|
|
that = this;
|
|
if (typeof MathJax !== 'undefined' && MathJax !== null) {
|
|
this.el.find('.problem > div').each(function(index, element) {
|
|
return MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]);
|
|
});
|
|
}
|
|
if (window.hasOwnProperty('update_schematics')) {
|
|
window.update_schematics();
|
|
}
|
|
problemPrefix = this.element_id.replace(/problem_/, '');
|
|
this.inputs = this.$('[id^="input_' + problemPrefix + '_"]');
|
|
this.$('div.action button').click(this.refreshAnswers);
|
|
this.reviewButton = this.$('.notification-btn.review-btn');
|
|
this.reviewButton.click(this.scroll_to_problem_meta);
|
|
this.submitButton = this.$('.action .submit');
|
|
this.submitButtonLabel = this.$('.action .submit .submit-label');
|
|
this.submitButtonSubmitText = this.submitButtonLabel.text();
|
|
this.submitButtonSubmittingText = this.submitButton.data('submitting');
|
|
this.submitButton.click(this.submit_fd);
|
|
this.hintButton = this.$('.action .hint-button');
|
|
this.hintButton.click(this.hint_button);
|
|
this.resetButton = this.$('.action .reset');
|
|
this.resetButton.click(this.reset);
|
|
this.showButton = this.$('.action .show');
|
|
this.showButton.click(this.show);
|
|
this.saveButton = this.$('.action .save');
|
|
this.saveNotification = this.$('.notification-save');
|
|
this.showAnswerNotification = this.$('.notification-show-answer');
|
|
this.saveButton.click(this.save);
|
|
this.gentleAlertNotification = this.$('.notification-gentle-alert');
|
|
this.submitNotification = this.$('.notification-submit');
|
|
|
|
// Accessibility helper for sighted keyboard users to show <clarification> tooltips on focus:
|
|
this.$('.clarification').focus(function(ev) {
|
|
var icon;
|
|
icon = $(ev.target).children('i');
|
|
return window.globalTooltipManager.openTooltip(icon);
|
|
});
|
|
this.$('.clarification').blur(function() {
|
|
return window.globalTooltipManager.hide();
|
|
});
|
|
this.$('.review-btn').focus(function(ev) {
|
|
return $(ev.target).removeClass('sr');
|
|
});
|
|
this.$('.review-btn').blur(function(ev) {
|
|
return $(ev.target).addClass('sr');
|
|
});
|
|
this.bindResetCorrectness();
|
|
if (this.submitButton.length) {
|
|
this.submitAnswersAndSubmitButton(true);
|
|
}
|
|
Collapsible.setCollapsibles(this.el);
|
|
this.$('input.math').keyup(this.refreshMath);
|
|
if (typeof MathJax !== 'undefined' && MathJax !== null) {
|
|
this.$('input.math').each(function(index, element) {
|
|
return MathJax.Hub.Queue([that.refreshMath, null, element]);
|
|
});
|
|
}
|
|
};
|
|
|
|
Problem.prototype.renderProgressState = function() {
|
|
var graded, progress, progressTemplate, curScore, totalScore, attemptsUsed;
|
|
curScore = this.el.data('problem-score');
|
|
totalScore = this.el.data('problem-total-possible');
|
|
attemptsUsed = this.el.data('attempts-used');
|
|
graded = this.el.data('graded');
|
|
|
|
// The problem is ungraded if it's explicitly marked as such, or if the total possible score is 0
|
|
if (graded === 'True' && totalScore !== 0) {
|
|
graded = true;
|
|
} else {
|
|
graded = false;
|
|
}
|
|
|
|
if (curScore === undefined || totalScore === undefined) {
|
|
// Render an empty string.
|
|
progressTemplate = '';
|
|
} else if (curScore === null || curScore === 'None') {
|
|
// Render 'x point(s) possible (un/graded, results hidden)' if no current score provided.
|
|
if (graded) {
|
|
progressTemplate = ngettext(
|
|
// Translators: {num_points} is the number of points possible (examples: 1, 3, 10).;
|
|
'{num_points} point possible (graded, results hidden)',
|
|
'{num_points} points possible (graded, results hidden)',
|
|
totalScore
|
|
);
|
|
} else {
|
|
progressTemplate = ngettext(
|
|
// Translators: {num_points} is the number of points possible (examples: 1, 3, 10).;
|
|
'{num_points} point possible (ungraded, results hidden)',
|
|
'{num_points} points possible (ungraded, results hidden)',
|
|
totalScore
|
|
);
|
|
}
|
|
} else if ((attemptsUsed === 0 || totalScore === 0) && curScore === 0) {
|
|
// Render 'x point(s) possible' if student has not yet attempted question
|
|
// But if staff has overridden score to a non-zero number, show it
|
|
if (graded) {
|
|
progressTemplate = ngettext(
|
|
// Translators: {num_points} is the number of points possible (examples: 1, 3, 10).;
|
|
'{num_points} point possible (graded)', '{num_points} points possible (graded)',
|
|
totalScore
|
|
);
|
|
} else {
|
|
progressTemplate = ngettext(
|
|
// Translators: {num_points} is the number of points possible (examples: 1, 3, 10).;
|
|
'{num_points} point possible (ungraded)', '{num_points} points possible (ungraded)',
|
|
totalScore
|
|
);
|
|
}
|
|
} else {
|
|
// Render 'x/y point(s)' if student has attempted question
|
|
if (graded) {
|
|
progressTemplate = ngettext(
|
|
// This comment needs to be on one line to be properly scraped for the translators.
|
|
// Translators: {earned} is the number of points earned. {possible} 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);
|
|
'{earned}/{possible} point (graded)', '{earned}/{possible} points (graded)',
|
|
totalScore
|
|
);
|
|
} else {
|
|
progressTemplate = ngettext(
|
|
// This comment needs to be on one line to be properly scraped for the translators.
|
|
// Translators: {earned} is the number of points earned. {possible} 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);
|
|
'{earned}/{possible} point (ungraded)', '{earned}/{possible} points (ungraded)',
|
|
totalScore
|
|
);
|
|
}
|
|
}
|
|
progress = edx.StringUtils.interpolate(
|
|
progressTemplate, {
|
|
earned: curScore,
|
|
num_points: totalScore,
|
|
possible: totalScore
|
|
}
|
|
);
|
|
return this.$('.problem-progress').text(progress);
|
|
};
|
|
|
|
Problem.prototype.updateProgress = function(response) {
|
|
if (response.progress_changed) {
|
|
this.el.data('problem-score', response.current_score);
|
|
this.el.data('problem-total-possible', response.total_possible);
|
|
this.el.data('attempts-used', response.attempts_used);
|
|
this.el.trigger('progressChanged');
|
|
}
|
|
return this.renderProgressState();
|
|
};
|
|
|
|
Problem.prototype.forceUpdate = function(response) {
|
|
this.el.data('problem-score', response.current_score);
|
|
this.el.data('problem-total-possible', response.total_possible);
|
|
this.el.data('attempts-used', response.attempts_used);
|
|
this.el.trigger('progressChanged');
|
|
return this.renderProgressState();
|
|
};
|
|
|
|
Problem.prototype.queueing = function(focusCallback) {
|
|
var that = this;
|
|
this.queued_items = this.$('.xqueue');
|
|
this.num_queued_items = this.queued_items.length;
|
|
if (this.num_queued_items > 0) {
|
|
if (window.queuePollerID) { // Only one poller 'thread' per Problem
|
|
window.clearTimeout(window.queuePollerID);
|
|
}
|
|
window.queuePollerID = window.setTimeout(function() {
|
|
return that.poll(1000, focusCallback);
|
|
}, 1000);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.poll = function(previousTimeout, focusCallback) {
|
|
var that = this;
|
|
return $.postWithPrefix('' + this.url + '/problem_get', function(response) {
|
|
var newTimeout;
|
|
// If queueing status changed, then render
|
|
that.new_queued_items = $(response.html).find('.xqueue');
|
|
if (that.new_queued_items.length !== that.num_queued_items) {
|
|
edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)).promise().done(function() {
|
|
// eslint-disable-next-line no-void
|
|
return typeof focusCallback === 'function' ? focusCallback() : void 0;
|
|
});
|
|
JavascriptLoader.executeModuleScripts(that.el, function() {
|
|
that.setupInputTypes();
|
|
that.bind();
|
|
});
|
|
}
|
|
that.num_queued_items = that.new_queued_items.length;
|
|
if (that.num_queued_items === 0) {
|
|
that.forceUpdate(response);
|
|
delete window.queuePollerID;
|
|
} else {
|
|
newTimeout = previousTimeout * 2;
|
|
// if the timeout is greather than 1 minute
|
|
if (newTimeout >= 60000) {
|
|
delete window.queuePollerID;
|
|
that.gentle_alert(
|
|
gettext('The grading process is still running. Refresh the page to see updates.')
|
|
);
|
|
} else {
|
|
window.queuePollerID = window.setTimeout(function() {
|
|
return that.poll(newTimeout, focusCallback);
|
|
}, newTimeout);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Use this if you want to make an ajax call on the input type object
|
|
* static method so you don't have to instantiate a Problem in order to use it
|
|
*
|
|
* Input:
|
|
* url: the AJAX url of the problem
|
|
* inputId: the inputId of the input you would like to make the call on
|
|
* 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
|
|
* finding the correct input
|
|
* dispatch: string that indicates how this data should be handled by the inputtype
|
|
* data: dictionary of data to send to the server
|
|
* callback: the function that will be called once the AJAX call has been completed.
|
|
* It will be passed a response object
|
|
*/
|
|
Problem.inputAjax = function(url, inputId, dispatch, data, callback) {
|
|
data.dispatch = dispatch; // eslint-disable-line no-param-reassign
|
|
data.input_id = inputId; // eslint-disable-line no-param-reassign
|
|
return $.postWithPrefix('' + url + '/input_ajax', data, callback);
|
|
};
|
|
|
|
Problem.prototype.render = function(content, focusCallback) {
|
|
var that = this;
|
|
if (content) {
|
|
edx.HtmlUtils.setHtml(this.el, edx.HtmlUtils.HTML(content));
|
|
return JavascriptLoader.executeModuleScripts(this.el, function() {
|
|
that.setupInputTypes();
|
|
that.bind();
|
|
that.queueing(focusCallback);
|
|
that.renderProgressState();
|
|
// eslint-disable-next-line no-void
|
|
return typeof focusCallback === 'function' ? focusCallback() : void 0;
|
|
});
|
|
} else {
|
|
return $.postWithPrefix('' + this.url + '/problem_get', function(response) {
|
|
edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html));
|
|
return JavascriptLoader.executeModuleScripts(that.el, function() {
|
|
that.setupInputTypes();
|
|
that.bind();
|
|
that.queueing();
|
|
return that.forceUpdate(response);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
Problem.prototype.setupInputTypes = function() {
|
|
var that = this;
|
|
this.inputtypeDisplays = {};
|
|
return this.el.find('.capa_inputtype').each(function(index, inputtype) {
|
|
var classes, cls, id, setupMethod, i, len, results;
|
|
classes = $(inputtype).attr('class').split(' ');
|
|
id = $(inputtype).attr('id');
|
|
results = [];
|
|
for (i = 0, len = classes.length; i < len; i++) {
|
|
cls = classes[i];
|
|
setupMethod = that.inputtypeSetupMethods[cls];
|
|
if (setupMethod != null) {
|
|
results.push(that.inputtypeDisplays[id] = setupMethod(inputtype));
|
|
} else {
|
|
// eslint-disable-next-line no-void
|
|
results.push(void 0);
|
|
}
|
|
}
|
|
return results;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* If some function wants to be called before sending the answer to the
|
|
* server, give it a chance to do so.
|
|
*
|
|
* submit_save_waitfor allows the callee to send alerts if the user's input is
|
|
* invalid. To do so, the callee must throw an exception named "WaitforException".
|
|
* This and any other errors or exceptions that arise from the callee are rethrown
|
|
* and abort the submission.
|
|
*
|
|
* In order to use this feature, add a 'data-waitfor' attribute to the input,
|
|
* and specify the function to be called by the submit button before sending off @answers
|
|
*/
|
|
Problem.prototype.submit_save_waitfor = function(callback) {
|
|
var flag, inp, i, len, ref,
|
|
that = this;
|
|
flag = false;
|
|
ref = this.inputs;
|
|
for (i = 0, len = ref.length; i < len; i++) {
|
|
inp = ref[i];
|
|
if ($(inp).is('input[waitfor]')) {
|
|
try {
|
|
$(inp).data('waitfor')(function() {
|
|
that.refreshAnswers();
|
|
return callback();
|
|
});
|
|
} catch (e) {
|
|
if (e.name === 'Waitfor Exception') {
|
|
alert(e.message); // eslint-disable-line no-alert
|
|
} else {
|
|
alert( // eslint-disable-line no-alert
|
|
gettext('Could not grade your answer. The submission was aborted.')
|
|
);
|
|
}
|
|
throw e;
|
|
}
|
|
flag = true;
|
|
} else {
|
|
flag = false;
|
|
}
|
|
}
|
|
return flag;
|
|
};
|
|
|
|
// Scroll to problem metadata and next focus is problem input
|
|
Problem.prototype.scroll_to_problem_meta = function() {
|
|
var questionTitle;
|
|
questionTitle = this.$('.problem-header');
|
|
if (questionTitle.length > 0) {
|
|
$('html, body').animate({
|
|
scrollTop: questionTitle.offset().top
|
|
}, 500);
|
|
questionTitle.focus();
|
|
}
|
|
};
|
|
|
|
Problem.prototype.focus_on_notification = function(type) {
|
|
var notification;
|
|
notification = this.$('.notification-' + type);
|
|
if (notification.length > 0) {
|
|
notification.focus();
|
|
}
|
|
};
|
|
|
|
Problem.prototype.focus_on_submit_notification = function() {
|
|
this.focus_on_notification('submit');
|
|
};
|
|
|
|
Problem.prototype.focus_on_hint_notification = function(hintIndex) {
|
|
this.$('.notification-hint .notification-message > ol > li.hint-index-' + hintIndex).focus();
|
|
};
|
|
|
|
Problem.prototype.focus_on_save_notification = function() {
|
|
this.focus_on_notification('save');
|
|
};
|
|
|
|
/**
|
|
* 'submit_fd' uses FormData to allow file submissions in the 'problem_check' dispatch,
|
|
* in addition to simple querystring-based answers
|
|
*
|
|
* NOTE: The dispatch 'problem_check' is being singled out for the use of FormData;
|
|
* maybe preferable to consolidate all dispatches to use FormData
|
|
*/
|
|
Problem.prototype.submit_fd = function() {
|
|
var abortSubmission, error, errorHtml, errors, fd, fileNotSelected, fileTooLarge, maxFileSize,
|
|
requiredFilesNotSubmitted, settings, timeoutId, unallowedFileSubmitted, i, len,
|
|
that = this;
|
|
|
|
// If there are no file inputs in the problem, we can fall back on submit.
|
|
if (this.el.find('input:file').length === 0) {
|
|
this.submit();
|
|
return;
|
|
}
|
|
this.enableSubmitButton(false);
|
|
if (!window.FormData) {
|
|
alert(gettext('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.')); // eslint-disable-line max-len, no-alert
|
|
this.enableSubmitButton(true);
|
|
return;
|
|
}
|
|
timeoutId = this.enableSubmitButtonAfterTimeout();
|
|
fd = new FormData();
|
|
|
|
// Sanity checks on submission
|
|
maxFileSize = 4 * 1000 * 1000;
|
|
fileTooLarge = false;
|
|
fileNotSelected = false;
|
|
requiredFilesNotSubmitted = false;
|
|
unallowedFileSubmitted = false;
|
|
|
|
errors = [];
|
|
this.inputs.each(function(index, element) {
|
|
var allowedFiles, file, maxSize, requiredFiles, loopI, loopLen, ref;
|
|
if (element.type === 'file') {
|
|
requiredFiles = $(element).data('required_files');
|
|
allowedFiles = $(element).data('allowed_files');
|
|
ref = element.files;
|
|
for (loopI = 0, loopLen = ref.length; loopI < loopLen; loopI++) {
|
|
file = ref[loopI];
|
|
if (allowedFiles.length !== 0 && indexOfHelper.call(allowedFiles, file.name) < 0) {
|
|
unallowedFileSubmitted = true;
|
|
errors.push(edx.StringUtils.interpolate(
|
|
gettext('You submitted {filename}; only {allowedFiles} are allowed.'), {
|
|
filename: file.name,
|
|
allowedFiles: allowedFiles
|
|
}
|
|
));
|
|
}
|
|
if (indexOfHelper.call(requiredFiles, file.name) >= 0) {
|
|
requiredFiles.splice(requiredFiles.indexOf(file.name), 1);
|
|
}
|
|
if (file.size > maxFileSize) {
|
|
fileTooLarge = true;
|
|
maxSize = maxFileSize / (1000 * 1000);
|
|
errors.push(edx.StringUtils.interpolate(
|
|
gettext('Your file {filename} is too large (max size: {maxSize}MB).'), {
|
|
filename: file.name,
|
|
maxSize: maxSize
|
|
}
|
|
));
|
|
}
|
|
fd.append(element.id, file); // xss-lint: disable=javascript-jquery-append
|
|
}
|
|
if (element.files.length === 0) {
|
|
fileNotSelected = true;
|
|
// In case we want to allow submissions with no file
|
|
fd.append(element.id, ''); // xss-lint: disable=javascript-jquery-append
|
|
}
|
|
if (requiredFiles.length !== 0) {
|
|
requiredFilesNotSubmitted = true;
|
|
errors.push(edx.StringUtils.interpolate(
|
|
gettext('You did not submit the required files: {requiredFiles}.'), {
|
|
requiredFiles: requiredFiles
|
|
}
|
|
));
|
|
}
|
|
} else {
|
|
fd.append(element.id, element.value); // xss-lint: disable=javascript-jquery-append
|
|
}
|
|
});
|
|
if (fileNotSelected) {
|
|
errors.push(gettext('You did not select any files to submit.'));
|
|
}
|
|
errorHtml = '';
|
|
for (i = 0, len = errors.length; i < len; i++) {
|
|
error = errors[i];
|
|
errorHtml = edx.HtmlUtils.joinHtml(
|
|
errorHtml,
|
|
edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML('<li>{error}</li>'), {error: error})
|
|
);
|
|
}
|
|
errorHtml = edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML('<ul>{errors}</ul>'), {errors: errorHtml});
|
|
this.gentle_alert(errorHtml.toString());
|
|
abortSubmission = fileTooLarge || fileNotSelected || unallowedFileSubmitted || requiredFilesNotSubmitted;
|
|
if (abortSubmission) {
|
|
window.clearTimeout(timeoutId);
|
|
this.enableSubmitButton(true);
|
|
} else {
|
|
settings = {
|
|
type: 'POST',
|
|
data: fd,
|
|
processData: false,
|
|
contentType: false,
|
|
complete: this.enableSubmitButtonAfterResponse,
|
|
success: function(response) {
|
|
switch (response.success) {
|
|
case 'submitted':
|
|
case 'incorrect':
|
|
case 'correct':
|
|
that.render(response.contents);
|
|
that.updateProgress(response);
|
|
break;
|
|
default:
|
|
that.gentle_alert(response.success);
|
|
}
|
|
return Logger.log('problem_graded', [that.answers, response.contents], that.id);
|
|
},
|
|
error: function(response) {
|
|
that.gentle_alert(response.responseJSON.success);
|
|
}
|
|
};
|
|
$.ajaxWithPrefix('' + this.url + '/problem_check', settings);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.submit = function() {
|
|
if (!this.submit_save_waitfor(this.submit_internal)) {
|
|
this.disableAllButtonsWhileRunning(this.submit_internal, true);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.submit_internal = function() {
|
|
var that = this;
|
|
Logger.log('problem_check', this.answers);
|
|
return $.postWithPrefix('' + this.url + '/problem_check', this.answers, function(response) {
|
|
switch (response.success) {
|
|
case 'submitted':
|
|
case 'incorrect':
|
|
case 'correct':
|
|
window.SR.readTexts(that.get_sr_status(response.contents));
|
|
that.el.trigger('contentChanged', [that.id, response.contents, response]);
|
|
that.render(response.contents, that.focus_on_submit_notification);
|
|
that.updateProgress(response);
|
|
// This is used by the Learning MFE to know when the Entrance Exam has been passed
|
|
// for a user. The MFE is then able to respond appropriately.
|
|
if (response.entrance_exam_passed) {
|
|
window.parent.postMessage({type: 'entranceExam.passed'}, '*');
|
|
}
|
|
break;
|
|
default:
|
|
that.saveNotification.hide();
|
|
that.gentle_alert(response.success);
|
|
}
|
|
return Logger.log('problem_graded', [that.answers, response.contents], that.id);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* in sections with aria-labels. If so, labels are prepended to the status element text.
|
|
* If not, just the text of the status elements are returned.
|
|
*/
|
|
Problem.prototype.get_sr_status = function(contents) {
|
|
var addedStatus, ariaLabel, element, labeledStatus, parentSection, statusElement, template, i, len;
|
|
statusElement = $(contents).find('.status');
|
|
labeledStatus = [];
|
|
for (i = 0, len = statusElement.length; i < len; i++) {
|
|
element = statusElement[i];
|
|
parentSection = $(element).closest('.wrapper-problem-response');
|
|
addedStatus = false;
|
|
if (parentSection) {
|
|
ariaLabel = parentSection.attr('aria-label');
|
|
if (ariaLabel) {
|
|
// Translators: This is only translated to allow for reordering of label and associated status.;
|
|
template = gettext('{label}: {status}');
|
|
labeledStatus.push(edx.StringUtils.interpolate(
|
|
template, {
|
|
label: ariaLabel,
|
|
status: $(element).text()
|
|
}
|
|
));
|
|
addedStatus = true;
|
|
}
|
|
}
|
|
if (!addedStatus) {
|
|
labeledStatus.push($(element).text());
|
|
}
|
|
}
|
|
return labeledStatus;
|
|
};
|
|
|
|
Problem.prototype.reset = function() {
|
|
return this.disableAllButtonsWhileRunning(this.reset_internal, false);
|
|
};
|
|
|
|
Problem.prototype.reset_internal = function() {
|
|
var that = this;
|
|
Logger.log('problem_reset', this.answers);
|
|
return $.postWithPrefix('' + this.url + '/problem_reset', {
|
|
id: this.id
|
|
}, function(response) {
|
|
if (response.success) {
|
|
that.el.trigger('contentChanged', [that.id, response.html, response]);
|
|
that.render(response.html, that.scroll_to_problem_meta);
|
|
that.updateProgress(response);
|
|
return window.SR.readText(gettext('This problem has been reset.'));
|
|
} else {
|
|
return that.gentle_alert(response.msg);
|
|
}
|
|
});
|
|
};
|
|
|
|
// TODO this needs modification to deal with javascript responses; perhaps we
|
|
// need something where responsetypes can define their own behavior when show
|
|
// is called.
|
|
Problem.prototype.show = function() {
|
|
var that = this;
|
|
Logger.log('problem_show', {
|
|
problem: this.id
|
|
});
|
|
return $.postWithPrefix('' + this.url + '/problem_show', function(response) {
|
|
var answers;
|
|
answers = response.answers;
|
|
$.each(answers, function(key, value) {
|
|
var safeKey = key.replace(':', '\\:'); // fix for courses which use url_names with colons, e.g. problem:question1
|
|
safeKey = safeKey.replace(/\./g, '\\.'); // fix for courses which use url_names with periods. e.g. question1.1
|
|
var answer;
|
|
if (!$.isArray(value)) {
|
|
answer = that.$('#answer_' + safeKey + ', #solution_' + safeKey);
|
|
edx.HtmlUtils.setHtml(answer, edx.HtmlUtils.HTML(value));
|
|
Collapsible.setCollapsibles(answer);
|
|
|
|
// Sometimes, `value` is just a string containing a MathJax formula.
|
|
// 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
|
|
// the script doesn't break in such cases.
|
|
//
|
|
// We will fallback to the second `if statement` below, if an
|
|
// error is thrown by jQuery.
|
|
try {
|
|
return $(value).find('.detailed-solution');
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
|
|
// TODO remove the above once everything is extracted into its own
|
|
// inputtype functions.
|
|
}
|
|
});
|
|
that.el.find('.capa_inputtype').each(function(index, inputtype) {
|
|
var classes, cls, display, showMethod, i, len, results;
|
|
classes = $(inputtype).attr('class').split(' ');
|
|
results = [];
|
|
for (i = 0, len = classes.length; i < len; i++) {
|
|
cls = classes[i];
|
|
display = that.inputtypeDisplays[$(inputtype).attr('id')];
|
|
showMethod = that.inputtypeShowAnswerMethods[cls];
|
|
if (showMethod != null) {
|
|
results.push(showMethod(inputtype, display, answers, response.correct_status_html));
|
|
} else {
|
|
// eslint-disable-next-line no-void
|
|
results.push(void 0);
|
|
}
|
|
}
|
|
return results;
|
|
});
|
|
if (typeof MathJax !== 'undefined' && MathJax !== null) {
|
|
that.el.find('.problem > div').each(function(index, element) {
|
|
return MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]);
|
|
});
|
|
}
|
|
that.el.find('.show').attr('disabled', 'disabled');
|
|
that.updateProgress(response);
|
|
that.clear_all_notifications();
|
|
that.showAnswerNotification.show();
|
|
that.focus_on_notification('show-answer');
|
|
});
|
|
};
|
|
|
|
Problem.prototype.clear_all_notifications = function() {
|
|
this.submitNotification.remove();
|
|
this.gentleAlertNotification.hide();
|
|
this.saveNotification.hide();
|
|
this.showAnswerNotification.hide();
|
|
};
|
|
|
|
Problem.prototype.gentle_alert = function(msg) {
|
|
edx.HtmlUtils.setHtml(
|
|
this.el.find('.notification-gentle-alert .notification-message'),
|
|
edx.HtmlUtils.HTML(msg)
|
|
);
|
|
this.clear_all_notifications();
|
|
this.gentleAlertNotification.show();
|
|
this.gentleAlertNotification.focus();
|
|
};
|
|
|
|
Problem.prototype.save = function() {
|
|
if (!this.submit_save_waitfor(this.save_internal)) {
|
|
this.disableAllButtonsWhileRunning(this.save_internal, false);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.save_internal = function() {
|
|
var that = this;
|
|
Logger.log('problem_save', this.answers);
|
|
return $.postWithPrefix('' + this.url + '/problem_save', this.answers, function(response) {
|
|
var saveMessage;
|
|
saveMessage = response.msg;
|
|
if (response.success) {
|
|
that.el.trigger('contentChanged', [that.id, response.html, response]);
|
|
edx.HtmlUtils.setHtml(
|
|
that.el.find('.notification-save .notification-message'),
|
|
edx.HtmlUtils.HTML(saveMessage)
|
|
);
|
|
that.clear_all_notifications();
|
|
that.el.find('.wrapper-problem-response .message').hide();
|
|
that.saveNotification.show();
|
|
that.focus_on_save_notification();
|
|
} else {
|
|
that.gentle_alert(saveMessage);
|
|
}
|
|
});
|
|
};
|
|
|
|
Problem.prototype.refreshMath = function(event, element) {
|
|
var elid, eqn, jax, mathjaxPreprocessor, preprocessorTag, target;
|
|
if (!element) {
|
|
element = event.target; // eslint-disable-line no-param-reassign
|
|
}
|
|
elid = element.id.replace(/^input_/, '');
|
|
target = 'display_' + elid;
|
|
|
|
// MathJax preprocessor is loaded by 'setupInputTypes'
|
|
preprocessorTag = 'inputtype_' + elid;
|
|
mathjaxPreprocessor = this.inputtypeDisplays[preprocessorTag];
|
|
if (typeof MathJax !== 'undefined' && MathJax !== null && MathJax.Hub.getAllJax(target)[0]) {
|
|
jax = MathJax.Hub.getAllJax(target)[0];
|
|
eqn = $(element).val();
|
|
if (mathjaxPreprocessor) {
|
|
eqn = mathjaxPreprocessor(eqn);
|
|
}
|
|
MathJax.Hub.Queue(['Text', jax, eqn], [this.updateMathML, jax, element]);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.updateMathML = function(jax, element) {
|
|
try {
|
|
$('#' + element.id + '_dynamath').val(jax.root.toMathML(''));
|
|
} catch (exception) {
|
|
if (!exception.restart) {
|
|
throw exception;
|
|
}
|
|
if (typeof MathJax !== 'undefined' && MathJax !== null) {
|
|
MathJax.Callback.After([this.refreshMath, jax], exception.restart);
|
|
}
|
|
}
|
|
};
|
|
|
|
Problem.prototype.refreshAnswers = function() {
|
|
this.$('input.schematic').each(function(index, element) {
|
|
return element.schematic.update_value();
|
|
});
|
|
this.$('.CodeMirror').each(function(index, element) {
|
|
if (element.CodeMirror.save) {
|
|
element.CodeMirror.save();
|
|
}
|
|
});
|
|
this.answers = this.inputs.serialize();
|
|
};
|
|
|
|
/**
|
|
* Used to check available answers and if something is checked (or the answer is set in some textbox),
|
|
* the "Submit" button becomes enabled. Otherwise it is disabled by default.
|
|
*
|
|
* Arguments:
|
|
* bind (boolean): used on the first check to attach event handlers to input fields
|
|
* to change "Submit" enable status in case of some manipulations with answers
|
|
*/
|
|
Problem.prototype.submitAnswersAndSubmitButton = function(bind) {
|
|
var answered, atLeastOneTextInputFound, oneTextInputFilled,
|
|
that = this;
|
|
if (bind === null || bind === undefined) {
|
|
bind = false; // eslint-disable-line no-param-reassign
|
|
}
|
|
answered = true;
|
|
atLeastOneTextInputFound = false;
|
|
oneTextInputFilled = false;
|
|
this.el.find('input:text').each(function(i, textField) {
|
|
if ($(textField).is(':visible')) {
|
|
atLeastOneTextInputFound = true;
|
|
if ($(textField).val() !== '') {
|
|
oneTextInputFilled = true;
|
|
}
|
|
if (bind) {
|
|
$(textField).on('input', function() {
|
|
that.saveNotification.hide();
|
|
that.showAnswerNotification.hide();
|
|
that.submitAnswersAndSubmitButton();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
if (atLeastOneTextInputFound && !oneTextInputFilled) {
|
|
answered = false;
|
|
}
|
|
this.el.find('.choicegroup').each(function(i, choicegroupBlock) {
|
|
var checked;
|
|
checked = false;
|
|
$(choicegroupBlock).find('input[type=checkbox], input[type=radio]')
|
|
.each(function(j, checkboxOrRadio) {
|
|
if ($(checkboxOrRadio).is(':checked')) {
|
|
checked = true;
|
|
}
|
|
if (bind) {
|
|
$(checkboxOrRadio).on('click', function() {
|
|
that.saveNotification.hide();
|
|
that.el.find('.show').removeAttr('disabled');
|
|
that.showAnswerNotification.hide();
|
|
that.submitAnswersAndSubmitButton();
|
|
});
|
|
}
|
|
});
|
|
if (!checked) {
|
|
answered = false;
|
|
}
|
|
});
|
|
this.el.find('select').each(function(i, selectField) {
|
|
var selectedOption = $(selectField).find('option:selected').text()
|
|
.trim();
|
|
if (selectedOption === 'Select an option') {
|
|
answered = false;
|
|
}
|
|
if (bind) {
|
|
$(selectField).on('change', function() {
|
|
that.saveNotification.hide();
|
|
that.showAnswerNotification.hide();
|
|
that.submitAnswersAndSubmitButton();
|
|
});
|
|
}
|
|
});
|
|
if (answered) {
|
|
return this.enableSubmitButton(true);
|
|
} else {
|
|
return this.enableSubmitButton(false, false);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.bindResetCorrectness = function() {
|
|
// Loop through all input types.
|
|
// Bind the reset functions at that scope.
|
|
var $inputtypes,
|
|
that = this;
|
|
$inputtypes = this.el.find('.capa_inputtype').add(this.el.find('.inputtype'));
|
|
return $inputtypes.each(function(index, inputtype) {
|
|
var bindMethod, classes, cls, i, len, results;
|
|
classes = $(inputtype).attr('class').split(' ');
|
|
results = [];
|
|
for (i = 0, len = classes.length; i < len; i++) {
|
|
cls = classes[i];
|
|
bindMethod = that.bindResetCorrectnessByInputtype[cls];
|
|
if (bindMethod != null) {
|
|
results.push(bindMethod(inputtype));
|
|
} else {
|
|
// eslint-disable-next-line no-void
|
|
results.push(void 0);
|
|
}
|
|
}
|
|
return results;
|
|
});
|
|
};
|
|
|
|
// Find all places where each input type displays its correct-ness
|
|
// Replace them with their original state--'unanswered'.
|
|
Problem.prototype.bindResetCorrectnessByInputtype = {
|
|
// These are run at the scope of the capa inputtype
|
|
// They should set handlers on each <input> to reset the whole.
|
|
formulaequationinput: function(element) {
|
|
return $(element).find('input').on('input', function() {
|
|
var $p;
|
|
$p = $(element).find('span.status');
|
|
$p.removeClass('correct incorrect submitted');
|
|
return $p.parent().removeAttr('class').addClass('unsubmitted');
|
|
});
|
|
},
|
|
choicegroup: function(element) {
|
|
var $element, id;
|
|
$element = $(element);
|
|
id = ($element.attr('id').match(/^inputtype_(.*)$/))[1];
|
|
return $element.find('input').on('change', function() {
|
|
var $status;
|
|
$status = $('#status_' + id);
|
|
if ($status[0]) {
|
|
$status.removeAttr('class').addClass('status unanswered');
|
|
} else {
|
|
$('<span>', {
|
|
class: 'status unanswered',
|
|
style: 'display: inline-block;',
|
|
id: 'status_' + id
|
|
});
|
|
}
|
|
$element.find('label').find('span.status.correct').remove();
|
|
return $element.find('label').removeAttr('class');
|
|
});
|
|
},
|
|
'option-input': function(element) {
|
|
var $select, id;
|
|
$select = $(element).find('select');
|
|
id = ($select.attr('id').match(/^input_(.*)$/))[1];
|
|
return $select.on('change', function() {
|
|
return $('#status_' + id).removeAttr('class').addClass('unanswered')
|
|
.find('.sr')
|
|
.text(gettext('unsubmitted'));
|
|
});
|
|
},
|
|
textline: function(element) {
|
|
return $(element).find('input').on('input', function() {
|
|
var $p;
|
|
$p = $(element).find('span.status');
|
|
$p.removeClass('correct incorrect submitted');
|
|
return $p.parent().removeClass('correct incorrect').addClass('unsubmitted');
|
|
});
|
|
}
|
|
};
|
|
|
|
Problem.prototype.inputtypeSetupMethods = {
|
|
'text-input-dynamath': function(element) {
|
|
/*
|
|
Return: function (eqn) -> eqn that preprocesses the user formula input before
|
|
it is fed into MathJax. Return 'false' if no preprocessor specified
|
|
*/
|
|
var data, preprocessor, preprocessorClass, preprocessorClassName;
|
|
data = $(element).find('.text-input-dynamath_data');
|
|
preprocessorClassName = data.data('preprocessor');
|
|
preprocessorClass = window[preprocessorClassName];
|
|
if (preprocessorClass == null) {
|
|
return false;
|
|
} else {
|
|
preprocessor = new preprocessorClass();
|
|
return preprocessor.fn;
|
|
}
|
|
},
|
|
cminput: function(container) {
|
|
var CodeMirrorEditor, CodeMirrorTextArea, element, id, linenumbers, mode, spaces, tabsize;
|
|
element = $(container).find('textarea');
|
|
tabsize = element.data('tabsize');
|
|
mode = element.data('mode');
|
|
linenumbers = element.data('linenums');
|
|
spaces = Array(parseInt(tabsize, 10) + 1).join(' ');
|
|
CodeMirrorEditor = CodeMirror.fromTextArea(element[0], {
|
|
lineNumbers: linenumbers,
|
|
indentUnit: tabsize,
|
|
tabSize: tabsize,
|
|
mode: mode,
|
|
matchBrackets: true,
|
|
lineWrapping: true,
|
|
indentWithTabs: false,
|
|
smartIndent: false,
|
|
extraKeys: {
|
|
Esc: function() {
|
|
$('.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, correctStatusHtml) {
|
|
var answer, choice, inputId, i, len, results, $element, $inputLabel, $inputStatus;
|
|
$element = $(element);
|
|
inputId = $element.attr('id').replace(/inputtype_/, '');
|
|
inputId = inputId.replace(':', '\\:'); // fix for courses which use url_names with colons, e.g. problem:question1
|
|
var safeId = inputId.replace(/\./g, '\\.'); // fix for courses which use url_names with periods. e.g. question1.1
|
|
answer = answers[inputId];
|
|
results = [];
|
|
for (i = 0, len = answer.length; i < len; i++) {
|
|
choice = answer[i];
|
|
$inputLabel = $element.find('#input_' + safeId + '_' + choice + ' + label');
|
|
$inputStatus = $element.find('#status_' + safeId);
|
|
// If the correct answer was already Submitted before "Show Answer" was selected,
|
|
// the status HTML will already be present. Otherwise, inject the status HTML.
|
|
|
|
// If the learner clicked a different answer after Submit, their submitted answers
|
|
// will be marked as "unanswered". In that case, for correct answers update the
|
|
// classes accordingly.
|
|
if ($inputStatus.hasClass('unanswered')) {
|
|
edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml));
|
|
$inputLabel.addClass('choicegroup_correct');
|
|
} else if (!$inputLabel.hasClass('choicegroup_correct')) {
|
|
// If the status HTML is not already present (due to clicking Submit), append
|
|
// the status HTML for correct answers.
|
|
edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml));
|
|
$inputLabel.removeClass('choicegroup_incorrect');
|
|
results.push($inputLabel.addClass('choicegroup_correct'));
|
|
}
|
|
}
|
|
return results;
|
|
},
|
|
choicetextgroup: function(element, display, answers) {
|
|
var answer, choice, inputId, i, len, results, $element;
|
|
$element = $(element);
|
|
inputId = $element.attr('id').replace(/inputtype_/, '');
|
|
answer = answers[inputId];
|
|
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) {
|
|
// answers is a dict of (answer_id, answer_text) for each answer for this question.
|
|
//
|
|
// @Examples:
|
|
// {'anwser_id': {
|
|
// 'rectangle': '(10,10)-(20,30);(12,12)-(40,60)',
|
|
// 'regions': '[[10,10], [30,30], [10, 30], [30, 10]]'
|
|
// } }
|
|
var canvas, container, id, types, context, $element;
|
|
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]);
|
|
ctx.rect(points[1], points[2], width, height);
|
|
}
|
|
});
|
|
ctx.stroke();
|
|
return ctx.fill();
|
|
},
|
|
regions: function(ctx, coords) {
|
|
var parseCoords;
|
|
parseCoords = function(coordinates) {
|
|
var reg;
|
|
reg = JSON.parse(coordinates);
|
|
|
|
// Regions is list of lists [region1, region2, region3, ...] where regionN
|
|
// is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]].
|
|
// 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
|
|
// setting outer list)
|
|
if (typeof reg[0][0][0] === 'undefined') {
|
|
// we have [[1,2],[3,4],[5,6]] - single region
|
|
// instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]]
|
|
// or [[[1,2],[3,4],[5,6]]] - multiple regions syntax
|
|
reg = [reg];
|
|
}
|
|
return reg;
|
|
};
|
|
return $.each(parseCoords(coords), function(index, region) {
|
|
ctx.beginPath();
|
|
$.each(region, function(idx, point) {
|
|
if (idx === 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) {
|
|
context = canvas.getContext('2d');
|
|
} else {
|
|
console.log('Canvas is not supported.'); // eslint-disable-line no-console
|
|
}
|
|
context.fillStyle = 'rgba(255,255,255,.3)';
|
|
context.strokeStyle = '#FF0000';
|
|
context.lineWidth = '2';
|
|
if (answers[id]) {
|
|
$.each(answers[id], function(key, value) {
|
|
if ((types[key] !== null && types[key] !== undefined) && value) {
|
|
types[key](context, value);
|
|
}
|
|
});
|
|
edx.HtmlUtils.setHtml(container, edx.HtmlUtils.HTML(canvas));
|
|
} else {
|
|
console.log('Answer is absent for image input with id=' + id); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Used to keep the buttons disabled while operationCallback is running.
|
|
*
|
|
* params:
|
|
* 'operationCallback' is an operation to be run.
|
|
* isFromCheckOperation' is a boolean to keep track if 'operationCallback' was
|
|
* from submit, if so then text of submit button will be changed as well.
|
|
*
|
|
*/
|
|
Problem.prototype.disableAllButtonsWhileRunning = function(operationCallback, isFromCheckOperation) {
|
|
var that = this;
|
|
var allButtons = [this.resetButton, this.saveButton, this.showButton, this.hintButton, this.submitButton];
|
|
var initiallyEnabledButtons = allButtons.filter(function(button) {
|
|
return !button.attr('disabled');
|
|
});
|
|
this.enableButtons(initiallyEnabledButtons, false, isFromCheckOperation);
|
|
return operationCallback().always(function() {
|
|
return that.enableButtons(initiallyEnabledButtons, true, isFromCheckOperation);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Enables/disables buttons by removing/adding the disabled attribute. The submit button is checked
|
|
* separately due to the changing text it contains.
|
|
*
|
|
* params:
|
|
* 'buttons' is an array of buttons that will have their 'disabled' attribute modified
|
|
* 'enable' a boolean to either enable or disable the buttons passed in the first parameter
|
|
* 'changeSubmitButtonText' is a boolean to keep track if operation was initiated
|
|
* from submit so that text of submit button will also be changed while disabling/enabling
|
|
* the submit button.
|
|
*/
|
|
Problem.prototype.enableButtons = function(buttons, enable, changeSubmitButtonText) {
|
|
var that = this;
|
|
buttons.forEach(function(button) {
|
|
if (button.hasClass('submit')) {
|
|
that.enableSubmitButton(enable, changeSubmitButtonText);
|
|
} else if (enable) {
|
|
button.removeAttr('disabled');
|
|
} else {
|
|
button.attr({disabled: 'disabled'});
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Used to disable submit button to reduce chance of accidental double-submissions.
|
|
*
|
|
* params:
|
|
* 'enable' is a boolean to determine enabling/disabling of submit button.
|
|
* 'changeText' is a boolean to determine if there is need to change the
|
|
* text of submit button as well.
|
|
*/
|
|
Problem.prototype.enableSubmitButton = function(enable, changeText) {
|
|
var submitCanBeEnabled;
|
|
if (changeText === null || changeText === undefined) {
|
|
changeText = true; // eslint-disable-line no-param-reassign
|
|
}
|
|
if (enable) {
|
|
submitCanBeEnabled = this.submitButton.data('should-enable-submit-button') === 'True';
|
|
if (submitCanBeEnabled) {
|
|
this.submitButton.removeAttr('disabled');
|
|
}
|
|
if (changeText) {
|
|
this.submitButtonLabel.text(this.submitButtonSubmitText);
|
|
}
|
|
} else {
|
|
this.submitButton.attr({disabled: 'disabled'});
|
|
if (changeText) {
|
|
this.submitButtonLabel.text(this.submitButtonSubmittingText);
|
|
}
|
|
}
|
|
};
|
|
|
|
Problem.prototype.enableSubmitButtonAfterResponse = function() {
|
|
this.has_response = true;
|
|
if (!this.has_timed_out) {
|
|
// Server has returned response before our timeout.
|
|
return this.enableSubmitButton(false);
|
|
} else {
|
|
return this.enableSubmitButton(true);
|
|
}
|
|
};
|
|
|
|
Problem.prototype.enableSubmitButtonAfterTimeout = function() {
|
|
var enableSubmitButton,
|
|
that = this;
|
|
this.has_timed_out = false;
|
|
this.has_response = false;
|
|
enableSubmitButton = function() {
|
|
that.has_timed_out = true;
|
|
if (that.has_response) {
|
|
that.enableSubmitButton(true);
|
|
}
|
|
};
|
|
return window.setTimeout(enableSubmitButton, 750);
|
|
};
|
|
|
|
Problem.prototype.hint_button = function() {
|
|
// Store the index of the currently shown hint as an attribute.
|
|
// Use that to compute the next hint number when the button is clicked.
|
|
var hintContainer, hintIndex, nextIndex,
|
|
that = this;
|
|
hintContainer = this.$('.problem-hint');
|
|
hintIndex = hintContainer.attr('hint_index');
|
|
// eslint-disable-next-line no-void
|
|
if (hintIndex === void 0) {
|
|
nextIndex = 0;
|
|
} else {
|
|
nextIndex = parseInt(hintIndex, 10) + 1;
|
|
}
|
|
return $.postWithPrefix('' + this.url + '/hint_button', {
|
|
hint_index: nextIndex,
|
|
input_id: this.id
|
|
}, function(response) {
|
|
var hintMsgContainer;
|
|
if (response.success) {
|
|
hintMsgContainer = that.$('.problem-hint .notification-message');
|
|
hintContainer.attr('hint_index', response.hint_index);
|
|
edx.HtmlUtils.setHtml(hintMsgContainer, edx.HtmlUtils.HTML(response.msg));
|
|
MathJax.Hub.Queue(['Typeset', MathJax.Hub, hintContainer[0]]);
|
|
if (response.should_enable_next_hint) {
|
|
that.hintButton.removeAttr('disabled');
|
|
} else {
|
|
that.hintButton.attr({disabled: 'disabled'});
|
|
}
|
|
that.el.find('.notification-hint').show();
|
|
that.focus_on_hint_notification(nextIndex);
|
|
} else {
|
|
that.gentle_alert(response.msg);
|
|
}
|
|
});
|
|
};
|
|
|
|
return Problem;
|
|
}).call(this);
|
|
}).call(this);
|