
Previously, the filename was URL-encoded, thus each / was replaced with %2F. This caused issues with some Apache2 configuration, smartly mingling with the URL to either encode it a second time (resulting in %252F) or decoding it (generating a real /). However, for authenticated file downloads with the JWT, we hardly require a byte-by-byte matching. With these changes, the URL parameter is no longer URL-encoded, so that Apache2 won't break our implementation any longer. Further, we use this opportunity to get rid of the unnecessary .json extension for those filename routes, simplifying the routes generated and doing some further cleanup.
284 lines
9.7 KiB
JavaScript
284 lines
9.7 KiB
JavaScript
CodeOceanEditorEvaluation = {
|
|
// A list of non-printable characters that are not allowed in the code output.
|
|
// Taken from https://stackoverflow.com/a/69024306
|
|
nonPrintableRegEx: /[\u0000-\u0008\u000B\u000C\u000F-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF]/g,
|
|
sentryTransaction: null,
|
|
|
|
/**
|
|
* Scoring-Functions
|
|
*/
|
|
scoreCode: function (event) {
|
|
const cause = $('#assess');
|
|
this.startSentryTransaction(cause);
|
|
event.preventDefault();
|
|
this.stopCode(event);
|
|
this.clearScoringOutput();
|
|
$('#submit').addClass("d-none");
|
|
this.createSubmission(cause, null, function (submission) {
|
|
this.showSpinner($('#assess'));
|
|
$('#score_div').removeClass('d-none');
|
|
this.initializeSocketForScoring(submission.id);
|
|
}.bind(this));
|
|
},
|
|
|
|
handleScoringResponse: function (results) {
|
|
this.printScoringResults(results);
|
|
var score = _.reduce(results, function (sum, result) {
|
|
return sum + result.score * result.weight;
|
|
}, 0).toFixed(2);
|
|
$('#score').data('score', score);
|
|
this.renderScore();
|
|
},
|
|
|
|
printScoringResult: function (result, index) {
|
|
if (result === undefined || result === null) {
|
|
return;
|
|
}
|
|
|
|
$('#results').show();
|
|
let card;
|
|
if (result.file_role === 'teacher_defined_linter') {
|
|
card = $('#linter-dummies').children().first().clone();
|
|
} else {
|
|
card = $('#test-dummies').children().first().clone();
|
|
}
|
|
if (card.isPresent()) {
|
|
// the card won't be present if @embed_options[:hide_test_results] == true
|
|
this.populateCard(card, result, index);
|
|
$('#results ul').first().append(card);
|
|
}
|
|
},
|
|
|
|
printScoringResults: function (response) {
|
|
response = (Array.isArray(response)) ? response : [response]
|
|
const test_results = response.filter(function(x) {
|
|
if (x === undefined || x === null) {
|
|
return false;
|
|
}
|
|
switch (x.file_role) {
|
|
case 'teacher_defined_test':
|
|
return true;
|
|
case 'teacher_defined_linter':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
|
|
$('#results ul').first().html('');
|
|
$('.test-count .number').html(test_results.length);
|
|
this.clearOutput();
|
|
|
|
_.each(test_results, function (result, index) {
|
|
// based on https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
|
|
if (result === Object(result)) {
|
|
this.printOutput(result, false, index);
|
|
this.printScoringResult(result, index);
|
|
}
|
|
}.bind(this));
|
|
|
|
if (_.some(response, function (result) {
|
|
return result.status === 'timeout';
|
|
})) {
|
|
this.showTimeoutMessage();
|
|
}
|
|
if (_.some(response, function (result) {
|
|
return result.status === 'out_of_memory';
|
|
})) {
|
|
this.showOutOfMemoryMessage();
|
|
}
|
|
if (_.some(response, function (result) {
|
|
return result.status === 'runner_in_use';
|
|
})) {
|
|
this.showRunnerInUseMessage();
|
|
}
|
|
if (_.some(response, function (result) {
|
|
return result.status === 'container_depleted';
|
|
})) {
|
|
this.showContainerDepletedMessage();
|
|
}
|
|
},
|
|
|
|
renderScore: function () {
|
|
var score = parseFloat($('#score').data('score'));
|
|
var maximum_score = parseFloat($('#score').data('maximum-score'));
|
|
if (score >= 0 && score <= maximum_score && maximum_score > 0) {
|
|
var percentage_score = (score / maximum_score * 100).toFixed(0);
|
|
$('.score').html(percentage_score + '%');
|
|
} else {
|
|
$('.score').html(0 + '%');
|
|
}
|
|
this.renderProgressBar(score, maximum_score);
|
|
},
|
|
|
|
/**
|
|
* Testing-Logic
|
|
*/
|
|
handleTestResponse: function (result) {
|
|
this.clearOutput();
|
|
this.printOutput(result, false, 0);
|
|
this.showStatus(result);
|
|
this.showOutputBar();
|
|
},
|
|
|
|
/**
|
|
* Stop-Logic
|
|
*/
|
|
stopCode: function (event) {
|
|
event.preventDefault();
|
|
if (this.isActiveFileStoppable() && this.websocket) {
|
|
this.websocket.send(JSON.stringify({'cmd': 'client_kill'}));
|
|
this.killWebsocket();
|
|
this.cleanUpUI();
|
|
}
|
|
},
|
|
|
|
killWebsocket: function () {
|
|
if (this.websocket != null && this.websocket.getReadyState() !== WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
this.websocket.killWebSocket();
|
|
this.websocket.onError(_.noop);
|
|
this.running = false;
|
|
},
|
|
|
|
cleanUpUI: function () {
|
|
this.hideSpinner();
|
|
this.toggleButtonStates();
|
|
this.hidePrompt();
|
|
},
|
|
|
|
/**
|
|
* Output-Logic
|
|
*/
|
|
printWebsocketOutput: function (msg) {
|
|
if (!msg.data || msg.data === "\r") {
|
|
return;
|
|
}
|
|
var stream = {};
|
|
stream[msg.stream] = msg.data;
|
|
this.printOutput(stream, true, 0);
|
|
},
|
|
|
|
clearOutput: function () {
|
|
$('#output > .output-element').remove();
|
|
CodeOceanEditorTurtle.hideCanvas();
|
|
},
|
|
|
|
clearScoringOutput: function () {
|
|
$('#results ul').first().html('');
|
|
$('.test-count .number').html(0);
|
|
$('#score').data('score', 0);
|
|
this.renderScore();
|
|
this.clearOutput();
|
|
},
|
|
|
|
printOutput: function (output, colorize, index) {
|
|
if (output === undefined || output === null || output.stderr === undefined && output.stdout === undefined) {
|
|
// Prevent empty element with no text at all
|
|
return;
|
|
}
|
|
|
|
const sanitizedStdout = this.sanitizeOutput(output.stdout);
|
|
const sanitizedStderr = this.sanitizeOutput(output.stderr);
|
|
|
|
const element = this.findOrCreateOutputElement(index);
|
|
const pre = $('<span>');
|
|
|
|
if (sanitizedStdout !== '') {
|
|
if (colorize) {
|
|
pre.addClass('text-success');
|
|
}
|
|
pre.append(sanitizedStdout)
|
|
}
|
|
|
|
if (sanitizedStderr !== '') {
|
|
if (colorize) {
|
|
pre.addClass('text-warning');
|
|
} else {
|
|
pre.append('StdErr: ');
|
|
}
|
|
pre.append(sanitizedStderr);
|
|
}
|
|
|
|
if (sanitizedStdout === '' && sanitizedStderr === '') {
|
|
if (colorize) {
|
|
pre.addClass('text-body-secondary');
|
|
}
|
|
pre.text($('#output').data('message-no-output'))
|
|
}
|
|
|
|
element.append(pre);
|
|
},
|
|
|
|
sanitizeOutput: function (rawContent) {
|
|
let sanitizedContent = _.escape(rawContent).replace(this.nonPrintableRegEx, "");
|
|
|
|
if (rawContent !== undefined && rawContent.trim().startsWith("<img")) {
|
|
const doc = new DOMParser().parseFromString(rawContent, "text/html");
|
|
// Get the parsed element, it is automatically wrapped in a <html><body> document
|
|
const parsedElement = doc.firstChild.lastChild.firstChild;
|
|
|
|
if (parsedElement.src.startsWith("data:image")) {
|
|
const sanitizedImg = document.createElement('img');
|
|
sanitizedImg.src = parsedElement.src;
|
|
sanitizedContent = sanitizedImg.outerHTML;
|
|
}
|
|
}
|
|
|
|
return sanitizedContent;
|
|
},
|
|
|
|
getDeadlineInformation: function(deadline, translation_key, otherwise) {
|
|
if (deadline !== undefined) {
|
|
let li = document.createElement("li");
|
|
this.submission_deadline = new Date(deadline);
|
|
let deadline_text = I18n.l("time.formats.long", this.submission_deadline);
|
|
deadline_text += ` (${this.getUTCTime(this.submission_deadline, I18n.locale === 'en')})`;
|
|
const bullet_point = I18n.t('exercises.editor.hints.' + translation_key,
|
|
{ deadline: deadline_text, otherwise: otherwise })
|
|
let text = $.parseHTML(bullet_point);
|
|
$(li).append(text);
|
|
return li;
|
|
}
|
|
},
|
|
|
|
getUTCTime: function(d, use_am_pm) {
|
|
let hour = d.getUTCHours();
|
|
const pm = hour >= 12;
|
|
let hour12 = hour % 12;
|
|
if (!hour12) {
|
|
hour12 += 12;
|
|
}
|
|
hour = hour.toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping: false})
|
|
const minute = d.getUTCMinutes().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping: false})
|
|
const second = d.getUTCSeconds().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping: false})
|
|
if (use_am_pm) {
|
|
return `${hour12}:${minute}:${second} ${pm ? 'pm' : 'am'} UTC`;
|
|
} else {
|
|
return `${hour}:${minute}:${second} UTC`;
|
|
}
|
|
},
|
|
|
|
initializeDeadlines: function () {
|
|
const deadline = $('#deadline');
|
|
if (deadline) {
|
|
const submission_deadline = deadline.data('submission-deadline');
|
|
const late_submission_deadline = deadline.data('late-submission-deadline');
|
|
|
|
const ul = document.createElement("ul");
|
|
|
|
if (submission_deadline && late_submission_deadline) {
|
|
ul.append(this.getDeadlineInformation(submission_deadline, 'submission_deadline', ''));
|
|
ul.append(this.getDeadlineInformation(late_submission_deadline, 'late_submission_deadline', ''));
|
|
} else {
|
|
const otherwise_no_points = I18n.t('exercises.editor.hints.otherwise');
|
|
ul.append(this.getDeadlineInformation(submission_deadline, 'submission_deadline', otherwise_no_points));
|
|
}
|
|
|
|
$(ul).insertAfter($(deadline).children()[0]);
|
|
}
|
|
}
|
|
};
|