Files
codeocean/app/assets/javascripts/editor/submissions.js
Sebastian Serth 17dd8b1267 Change syntax for routes with filename
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.
2024-01-19 11:06:40 +01:00

228 lines
7.6 KiB
JavaScript

CodeOceanEditorSubmissions = {
AUTOSAVE_INTERVAL: 15 * 1000,
autosaveTimer: null,
autosaveLabel: "#statusbar #autosave",
/**
* Submission-Creation
*/
createSubmission: function (initiator, filter, callback) {
const editor = $('#editor');
this.showSpinner(initiator);
var url = $(initiator).data('url') || editor.data('submissions-url');
if (url === undefined) {
const data = {
initiator: initiator,
filter: filter,
}
Sentry.captureException(JSON.stringify(data));
return;
}
var jqxhr = this.ajax({
data: {
submission: {
cause: $(initiator).data('cause') || $(initiator).prop('id'),
exercise_id: editor.data('exercise-id') || $(initiator).data('exercise-id'),
files_attributes: (filter || _.identity)(this.collectFiles())
}
},
dataType: 'json',
method: $(initiator).data('http-method') || 'POST',
url: url,
});
jqxhr.always(this.hideSpinner.bind(this));
jqxhr.done(this.createSubmissionCallback.bind(this));
if(callback != null){
jqxhr.done(callback.bind(this));
}
jqxhr.fail(this.ajaxError.bind(this));
},
collectFiles: function() {
var editable_editors = _.filter(this.editors, function(editor) {
return !editor.getReadOnly();
});
return _.map(editable_editors, function(editor) {
return {
content: editor.getValue(),
file_id: $(editor.container).data('file-id')
};
});
},
createSubmissionCallback: function(submission){
// set all frames context types to submission
$('.frame').each(function(index, element) {
$(element).data('context-type', 'Submission');
});
// update the ids of the editors and reload the annotations
for (var i = 0; i < this.editors.length; i++) {
const file_id_old = $(editor.container).data('file-id');
// file_id_old is always set. Either it is a reference to a teacher supplied given file, or it is the actual id of a new user created file.
// This is the case, since it is set via a call to ancestor_id on the model, which returns either file_id if set, or id if it is not set.
// therefore the else part is not needed any longer...
// if we have an file_id set (the file is a copy of a teacher supplied given file) and the new file-ids are present in the response
if (file_id_old != null && data.files){
// if we find file_id_old (this is the reference to the base file) in the submission, this is the match
for(var j = 0; j< data.files.length; j++){
if(data.files[j].file_id === file_id_old){
//$(editors[i].container).data('id') = data.files[j].id;
$(this.editors[i].container).data('id', data.files[j].id );
}
}
}
}
// toggle button states (it might be the case that the request for comments button has to be enabled
this.toggleButtonStates();
this.updateSaveStateLabel();
},
/**
* File-Management
*/
destroyFile: function() {
this.createSubmission($('#destroy-file'), function(files) {
return _.reject(files, function(file) {
return file.file_id === CodeOceanEditor.active_file.id;
});
}, window.CodeOcean.refresh);
},
downloadCode: function(event) {
event.preventDefault();
this.createSubmission('#download', null,function(submission) {
// to download just a single file, use the following url
// window.location = Routes.download_file_submission_url(submission.id, CodeOceanEditor.active_file.filename);
window.location = Routes.download_submission_url(submission.id);
});
},
resetCode: function(initiator, onlyActiveFile = false) {
this.startSentryTransaction(initiator);
this.showSpinner(initiator);
this.ajax({
method: 'GET',
url: $('#start-over').data('url') || $('#start-over-active-file').data('url')
}).done(function(response) {
this.hideSpinner();
App.synchronized_editor?.reset_content(response);
this.setEditorContent(response, onlyActiveFile);
}.bind(this));
},
setEditorContent: function(new_content, onlyActiveFile = false) {
_.each(this.editors, function(editor) {
const editor_file_id = $(editor.container).data('file-id');
const found_file = _.find(new_content.files, function(file) {
// File.id is used to reload the exercise and file.file_id is used to update the editor content for pair programming group members
return (file.id || file.file_id) === editor_file_id;
});
if(found_file && !onlyActiveFile || found_file && found_file.id === CodeOceanEditor.active_file.id){
editor.setValue(found_file.content);
editor.clearSelection();
}
}.bind(this));
},
renderCode: function(event) {
const cause = $('#render');
this.startSentryTransaction(cause);
event.preventDefault();
if (cause.is(':visible')) {
this.createSubmission(cause, null, function (submission) {
if (submission.render_url === undefined) return;
const active_file = CodeOceanEditor.active_file.filename;
const desired_file = submission.render_url.filter(hash => hash.filepath === active_file);
const url = desired_file[0].url;
// Allow to open the new tab even in Safari.
// See: https://stackoverflow.com/a/70463940
setTimeout(() => {
var pop_up_window = window.open(url, '_blank');
if (pop_up_window) {
pop_up_window.onerror = function (message) {
this.clearOutput();
this.printOutput({
stderr: message
}, true, 0);
this.sendError(message, submission.id);
this.showOutputBar();
};
}
})
});
}
},
/**
* Execution-Logic
*/
runCode: function(event) {
const cause = $('#run');
this.startSentryTransaction(cause);
event.preventDefault();
this.stopCode(event);
if ($('#run').is(':visible')) {
this.createSubmission(cause, null, this.runSubmission.bind(this));
}
},
runSubmission: function (submission) {
//Run part starts here
this.running = true;
this.showSpinner($('#run'));
$('#score_div').addClass('d-none');
this.toggleButtonStates();
this.initializeSocketForRunning(submission.id, CodeOceanEditor.active_file.filename);
},
testCode: function(event) {
const cause = $('#test');
this.startSentryTransaction(cause);
event.preventDefault();
if (cause.is(':visible')) {
this.createSubmission(cause, null, function(submission) {
this.showSpinner($('#test'));
$('#score_div').addClass('d-none');
this.initializeSocketForTesting(submission.id, CodeOceanEditor.active_file.filename);
}.bind(this));
}
},
/**
* Autosave-Logic
*/
resetSaveTimer: function () {
clearTimeout(this.autosaveTimer);
this.autosaveTimer = setTimeout(this.autosave.bind(this), this.AUTOSAVE_INTERVAL);
},
updateSaveStateLabel: function() {
var date = new Date();
var autosaveLabel = $(this.autosaveLabel);
autosaveLabel.parent().css("visibility", "visible");
autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds());
autosaveLabel.text(date.toLocaleTimeString());
},
autosaveIfChanged: function() {
// Only save if the user has changed the code in the meantime (represented by an active timer)
if(this.autosaveTimer != null){
this.autosave();
}
},
autosave: function () {
clearTimeout(this.autosaveTimer);
this.autosaveTimer = null;
this.createSubmission($('#autosave'), null);
}
};