
Previously, a user-defined test (those run with "Test" instead of "Run") would cause a flash message and a Sentry error if anything was printed to StdErr. As this might happen during user code execution (and therefore is no error), we remove this warning. All existing errors are already caught (compare with enum status in testrun.rb), so it's fine for now. Fixes CODEOCEAN-BT
984 lines
38 KiB
Plaintext
984 lines
38 KiB
Plaintext
var CodeOceanEditor = {
|
|
//ACE-Editor-Path
|
|
// ruby part adds the relative_url_root, if it is set.
|
|
ACE_FILES_PATH: '<%= "#{Rails.application.config.relative_url_root.chomp('/')}/assets/ace/" %>',
|
|
THEME: 'ace/theme/textmate',
|
|
|
|
//Color-Encoding for Percentages in Progress Bars (For submissions)
|
|
ADEQUATE_PERCENTAGE: 50,
|
|
SUCCESSFULL_PERCENTAGE: 90,
|
|
|
|
//Key-Codes (for Hotkeys)
|
|
R_KEY_CODE: 82,
|
|
S_KEY_CODE: 83,
|
|
T_KEY_CODE: 84,
|
|
ENTER_KEY_CODE: 13,
|
|
|
|
//Request-For-Comments-Configuration
|
|
REQUEST_FOR_COMMENTS_DELAY: 0,
|
|
REQUEST_TOOLTIP_TIME: 5000,
|
|
REQUEST_TOOLTIP_DELAY: 15 * 60 * 1000,
|
|
|
|
editors: [],
|
|
editor_for_file: new Map(),
|
|
regex_for_language: new Map(),
|
|
tracepositions_regex: undefined,
|
|
|
|
active_file: undefined,
|
|
active_frame: undefined,
|
|
running: false,
|
|
|
|
lastCopyText: null,
|
|
|
|
<% self.class.include Rails.application.routes.url_helpers %>
|
|
<% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
|
|
// Important notice: Changing the config values requires any content-wise
|
|
// modification for this file in the development environment. Lacking to do so
|
|
// will result in the old, server-side cached serving of this file even across
|
|
// server restarts!
|
|
sendEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>,
|
|
eventURL: "<%= @config['codeocean_events'] ? events_path : '' %>",
|
|
fileTypeURL: "<%= file_types_path %>",
|
|
|
|
|
|
configureEditors: function () {
|
|
_.each(['modePath', 'themePath', 'workerPath'], function (attribute) {
|
|
ace.config.set(attribute, this.ACE_FILES_PATH);
|
|
}.bind(this));
|
|
},
|
|
|
|
confirmDestroy: function (event) {
|
|
event.preventDefault();
|
|
if (confirm($(event.target).data('message-confirm'))) {
|
|
this.destroyFile();
|
|
}
|
|
},
|
|
|
|
confirmReset: function (event) {
|
|
event.preventDefault();
|
|
if (confirm($('#start-over').data('message-confirm'))) {
|
|
this.resetCode();
|
|
}
|
|
},
|
|
|
|
confirmResetActiveFile: function (event) {
|
|
event.preventDefault();
|
|
let message = $('#start-over-active-file').data('message-confirm');
|
|
message = message.replace('%{filename}', CodeOceanEditor.active_file.filename.replace(/#$/, ''))
|
|
if (confirm(message)) {
|
|
this.resetCode(true); // delete only active file
|
|
}
|
|
},
|
|
|
|
fileActionsAvailable: function () {
|
|
return this.isActiveFileRenderable() || this.isActiveFileRunnable() || this.isActiveFileStoppable() || this.isActiveFileTestable();
|
|
},
|
|
|
|
findOrCreateOutputElement: function (index) {
|
|
if ($('#output-' + index).isPresent()) {
|
|
return $('#output-' + index);
|
|
} else {
|
|
var element = $('<div class="mb-2 output-element">').attr('id', 'output-' + index);
|
|
$('#output').append(element);
|
|
return element;
|
|
}
|
|
},
|
|
|
|
getCardClass: function (result) {
|
|
if (result.file_role === 'teacher_defined_linter') {
|
|
return 'card bg-info text-white'
|
|
} else if (result.stderr && !result.score) {
|
|
return 'card bg-danger text-white';
|
|
} else if (result.score < 1) {
|
|
return 'card bg-warning text-white';
|
|
} else {
|
|
return 'card bg-success text-white';
|
|
}
|
|
},
|
|
|
|
showOutput: function (event) {
|
|
const target = $(event.target).attr('href');
|
|
if (target !== "#") {
|
|
event.preventDefault();
|
|
this.showOutputBar();
|
|
$('body').scrollTo($(event.target).attr('href'));
|
|
}
|
|
},
|
|
|
|
renderProgressBar: function (score, maximum_score) {
|
|
var percentage = score / maximum_score * 100;
|
|
var progress_bar = $('#score .progress-bar');
|
|
progress_bar.removeClass().addClass(this.getProgressBarClass(percentage));
|
|
progress_bar.attr({
|
|
'aria-valuemax': maximum_score,
|
|
'aria-valuemin': 0,
|
|
'aria-valuenow': score
|
|
});
|
|
progress_bar.css('width', percentage + '%');
|
|
},
|
|
|
|
// The event ready.jstree is fired too early and thus doesn't work.
|
|
selectFileInJsTree: function (filetree, file_id) {
|
|
if (!filetree.is(':visible'))
|
|
// The left sidebar is not shown and thus the filetree is not rendered.
|
|
return;
|
|
|
|
if (!filetree.hasClass('jstree-loading')) {
|
|
filetree.jstree("deselect_all");
|
|
filetree.jstree().select_node(file_id);
|
|
} else {
|
|
setTimeout(CodeOceanEditor.selectFileInJsTree.bind(null, filetree, file_id), 250);
|
|
}
|
|
},
|
|
|
|
showFirstFile: function (own_solution = false) {
|
|
let frame;
|
|
let filetree;
|
|
let editorSelector;
|
|
if (own_solution) {
|
|
frame = $('.own-frame[data-role="main_file"]').isPresent() ? $('.own-frame[data-role="main_file"]') : $('.own-frame').first();
|
|
filetree = $('#own-files');
|
|
editorSelector = '.own-editor';
|
|
} else {
|
|
frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
|
|
filetree = $('#files');
|
|
editorSelector = '.editor';
|
|
}
|
|
|
|
var file_id = frame.find(editorSelector).data('file-id');
|
|
this.setActiveFile(frame.data('filename'), file_id);
|
|
this.selectFileInJsTree(filetree, file_id);
|
|
this.showFrame(frame);
|
|
this.toggleButtonStates();
|
|
},
|
|
|
|
showFrame: function (frame) {
|
|
if (frame.hasClass('own-frame')) {
|
|
$('.own-frame').hide();
|
|
} else {
|
|
$('.frame').hide();
|
|
}
|
|
|
|
this.active_frame = frame;
|
|
frame.show();
|
|
this.resizeParentOfAceEditor(frame.find('.ace_editor'));
|
|
},
|
|
|
|
getProgressBarClass: function (percentage) {
|
|
if (percentage < this.ADEQUATE_PERCENTAGE) {
|
|
return 'progress-bar progress-bar-striped bg-danger';
|
|
} else if (percentage < this.SUCCESSFULL_PERCENTAGE) {
|
|
return 'progress-bar progress-bar-striped bg-warning';
|
|
} else {
|
|
return 'progress-bar progress-bar-striped bg-success';
|
|
}
|
|
},
|
|
|
|
handleKeyPress: function (event) {
|
|
if (event.altKey && event.which === this.R_KEY_CODE) {
|
|
$('#run').trigger('click');
|
|
} else if (event.altKey && event.which === this.S_KEY_CODE) {
|
|
$('#assess').trigger('click');
|
|
} else if (event.altKey && event.which === this.T_KEY_CODE) {
|
|
$('#test').trigger('click');
|
|
} else {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
},
|
|
|
|
handleCopyEvent: function (text) {
|
|
CodeOceanEditor.lastCopyText = text;
|
|
},
|
|
|
|
handlePasteEvent: function (pasteObject, event) {
|
|
var same = (CodeOceanEditor.lastCopyText === pasteObject.text);
|
|
|
|
// if the text is not copied from within the editor (from any file), send an event to the backend
|
|
if (!same) {
|
|
CodeOceanEditor.publishCodeOceanEvent({
|
|
category: 'editor_paste',
|
|
data: pasteObject.text,
|
|
exercise_id: $('#editor').data('exercise-id'),
|
|
file_id: $(event.container).data('file-id')
|
|
});
|
|
}
|
|
},
|
|
|
|
hideSpinner: function () {
|
|
$('button i.fa-solid, button i.fa-regular').show();
|
|
$('button i.fa-spin').removeClass('d-inline-block').addClass('d-none');
|
|
},
|
|
|
|
|
|
resizeAceEditors: function (own_solution = false) {
|
|
let editorSelector;
|
|
if (own_solution) {
|
|
editorSelector = $('.own-editor')
|
|
} else {
|
|
editorSelector = $('.editor')
|
|
}
|
|
|
|
editorSelector.each(function (index, element) {
|
|
this.resizeParentOfAceEditor(element);
|
|
}.bind(this));
|
|
window.dispatchEvent(new Event('resize'));
|
|
},
|
|
|
|
resizeSidebars: function () {
|
|
$('#content-left-sidebar').height(this.calculateEditorHeight('#content-left-sidebar', false));
|
|
$('#content-right-sidebar').height(this.calculateEditorHeight('#content-right-sidebar', false));
|
|
},
|
|
|
|
calculateEditorHeight: function (element, considerStatusbar) {
|
|
const jqueryElement = $(element);
|
|
if (jqueryElement.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const bottom = considerStatusbar ? ($('#statusbar').height() || 0) : 0;
|
|
// calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
|
|
return window.innerHeight - jqueryElement.offset().top - bottom - 5;
|
|
},
|
|
|
|
resizeParentOfAceEditor: function (element) {
|
|
const editorHeight = this.calculateEditorHeight(element, true);
|
|
$(element).parent().height(editorHeight);
|
|
},
|
|
|
|
initializeEditors: function (own_solution = false) {
|
|
// Initialize the editors array if not present already. This is mainly required for community solutions
|
|
this.editors = this.editors || [];
|
|
let editorSelector;
|
|
if (own_solution) {
|
|
editorSelector = $('.own-editor')
|
|
} else {
|
|
editorSelector = $('.editor')
|
|
}
|
|
|
|
editorSelector.each(function (index, element) {
|
|
|
|
// Resize frame on load
|
|
this.resizeParentOfAceEditor(element);
|
|
|
|
// Resize frame on window size change
|
|
$(window).resize(function () {
|
|
this.resizeParentOfAceEditor(element);
|
|
this.resizeSidebars();
|
|
}.bind(this));
|
|
|
|
var editor = ace.edit(element);
|
|
|
|
if (this.qa_api) {
|
|
editor.getSession().on("change", function (deltaObject) {
|
|
this.qa_api.executeCommand('syncEditor', [this.active_file, deltaObject]);
|
|
}.bind(this));
|
|
}
|
|
|
|
var document = editor.getSession().getDocument();
|
|
// insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added
|
|
var file_id = $(element).data('file-id');
|
|
var content = $('.editor-content[data-file-id=' + file_id + ']');
|
|
this.setActiveFile($(element).parent().data('filename'), file_id);
|
|
|
|
document.insertLines(0, content.text().split(/\n/));
|
|
// remove last (empty) that is there by default line
|
|
document.removeLines(document.getLength() - 1, document.getLength() - 1);
|
|
editor.setReadOnly($(element).parent().data('read-only') !== undefined);
|
|
if (editor.getReadOnly()) {
|
|
editor.setHighlightActiveLine(false);
|
|
editor.setHighlightGutterLine(false);
|
|
editor.renderer.$cursorLayer.element.style.opacity = 0;
|
|
}
|
|
editor.setShowPrintMargin(false);
|
|
editor.setTheme(this.THEME);
|
|
|
|
|
|
// set options for autocompletion
|
|
if ($(element).data('allow-auto-completion')) {
|
|
editor.setOptions({
|
|
enableBasicAutocompletion: true,
|
|
enableSnippets: false,
|
|
enableLiveAutocompletion: true
|
|
});
|
|
}
|
|
|
|
|
|
editor.commands.bindKey("ctrl+alt+0", null);
|
|
this.editors.push(editor);
|
|
this.editor_for_file.set($(element).parent().data('filename'), editor);
|
|
var session = editor.getSession();
|
|
var mode = $(element).data('mode')
|
|
session.setMode(mode);
|
|
if (mode === 'ace/mode/python') {
|
|
editor.setTheme('ace/theme/tomorrow')
|
|
}
|
|
session.setTabSize($(element).data('indent-size'));
|
|
session.setUseSoftTabs(true);
|
|
session.setUseWrapMode(true);
|
|
|
|
// set regex for parsing error traces based on the mode of the main file.
|
|
if ($(element).parent().data('role') === "main_file") {
|
|
this.tracepositions_regex = this.regex_for_language.get($(element).data('mode'));
|
|
}
|
|
|
|
/*
|
|
* Register event handlers
|
|
*/
|
|
|
|
// editor itself
|
|
editor.on("paste", this.handlePasteEvent.bind(element));
|
|
editor.on("copy", this.handleCopyEvent.bind(element));
|
|
|
|
// listener for autosave
|
|
session.on("change", function (deltaObject) {
|
|
this.resetSaveTimer();
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
initializeEventHandlers: function () {
|
|
$(document).on('click', '#results a', this.showOutput.bind(this));
|
|
$(document).on('keydown', this.handleKeyPress.bind(this));
|
|
this.initializeFileTreeButtons();
|
|
this.initializeWorkspaceButtons();
|
|
this.initializeRequestForComments()
|
|
},
|
|
|
|
updateEditorModeToFileTypeID: function (editor, fileTypeID) {
|
|
var newMode = 'ace/mode/text'
|
|
|
|
$.ajax(this.fileTypeURL + '/' + fileTypeID, {
|
|
dataType: 'json'
|
|
}).done(function (data) {
|
|
if (data['editor_mode'] !== null) {
|
|
newMode = data['editor_mode'];
|
|
}
|
|
}).fail(_.noop)
|
|
.always(function () {
|
|
ace.edit(editor).session.setMode(newMode);
|
|
if (newMode === 'ace/mode/python') {
|
|
ace.edit(editor).setTheme('ace/theme/tomorrow')
|
|
}
|
|
});
|
|
},
|
|
|
|
initializeFileTree: function (own_solution = false) {
|
|
let filesInstance;
|
|
if (own_solution) {
|
|
filesInstance = $('#own-files');
|
|
} else {
|
|
filesInstance = $('#files');
|
|
}
|
|
filesInstance.jstree(filesInstance.data('entries'));
|
|
filesInstance.on('click', 'li.jstree-leaf > a', function (event) {
|
|
const file_id = parseInt($(event.target).parent().attr('id'));
|
|
const frame = $('[data-file-id="' + file_id + '"]').parent();
|
|
this.setActiveFile(frame.data('filename'), file_id);
|
|
this.showFrame(frame);
|
|
this.toggleButtonStates();
|
|
}.bind(this));
|
|
},
|
|
|
|
initializeFileTreeButtons: function () {
|
|
$('#create-file').on('click', this.showFileDialog.bind(this));
|
|
$('#destroy-file').on('click', this.confirmDestroy.bind(this));
|
|
$('#destroy-file-collapsed').on('click', this.confirmDestroy.bind(this));
|
|
$('#download').on('click', this.downloadCode.bind(this));
|
|
$('#request-for-comments').on('click', this.requestComments.bind(this));
|
|
},
|
|
|
|
initializeSideBarCollapse: function () {
|
|
$('#sidebar-collapse-collapsed').on('click', this.handleSideBarToggle.bind(this));
|
|
$('#sidebar-collapse').on('click', this.handleSideBarToggle.bind(this));
|
|
const tipButton = $('#tips-collapsed');
|
|
if (tipButton) {
|
|
tipButton.on('click', this.handleSideBarToggle.bind(this));
|
|
}
|
|
$('#sidebar').on('transitionend', this.resizeAceEditors.bind(this));
|
|
$('#sidebar').on('transitionend', this.resizeSidebars.bind(this));
|
|
},
|
|
|
|
handleSideBarToggle: function () {
|
|
const sidebar = $('#sidebar');
|
|
sidebar.toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed');
|
|
if (sidebar.hasClass('w-25') || sidebar.hasClass('restore-to-w-25')) {
|
|
sidebar.toggleClass('w-25').toggleClass('restore-to-w-25');
|
|
}
|
|
$('#sidebar-collapsed').toggleClass('d-none');
|
|
$('#sidebar-uncollapsed').toggleClass('d-none');
|
|
},
|
|
|
|
initializeRegexes: function () {
|
|
// These RegEx are run on the HTML escaped output!
|
|
this.regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g);
|
|
this.regex_for_language.set("ace/mode/java", /(?:\.\/)?(.*\.java):(\d+):/g);
|
|
},
|
|
|
|
initializeTooltips: function () {
|
|
$('[data-tooltip]').tooltip();
|
|
},
|
|
|
|
|
|
initializeWorkspaceButtons: function () {
|
|
$('#submit').one('click', this.submitCode.bind(this));
|
|
$('#assess').on('click', this.scoreCode.bind(this));
|
|
$('#dropdown-render, #render').on('click', this.renderCode.bind(this));
|
|
$('#dropdown-run, #run').on('click', this.runCode.bind(this));
|
|
$('#dropdown-stop, #stop').on('click', this.stopCode.bind(this));
|
|
$('#dropdown-test, #test').on('click', this.testCode.bind(this));
|
|
$('#save').on('click', this.saveCode.bind(this));
|
|
$('#start-over').on('click', this.confirmReset.bind(this));
|
|
$('#start-over-collapsed').on('click', this.confirmReset.bind(this));
|
|
$('#start-over-active-file').on('click', this.confirmResetActiveFile.bind(this));
|
|
$('#start-over-active-file-collapsed').on('click', this.confirmResetActiveFile.bind(this));
|
|
|
|
},
|
|
|
|
initializeRequestForComments: function () {
|
|
var button = $('#requestComments');
|
|
button.prop('disabled', true);
|
|
button.on('click', function () {
|
|
$('#rfc_intervention_text').hide()
|
|
new bootstrap.Modal($('#comment-modal')).show();
|
|
});
|
|
|
|
$('#askForCommentsButton').on('click', this.requestComments.bind(this));
|
|
$('#closeAskForCommentsButton').on('click', function () {
|
|
bootstrap.Modal.getInstance($('#comment-modal')).hide();
|
|
});
|
|
|
|
setTimeout(function () {
|
|
button.prop('disabled', false);
|
|
setTimeout(function () {
|
|
button.tooltip('show');
|
|
setTimeout(function () {
|
|
button.tooltip('hide');
|
|
}, this.REQUEST_TOOLTIP_TIME);
|
|
}, this.REQUEST_TOOLTIP_DELAY)
|
|
}.bind(this), this.REQUEST_FOR_COMMENTS_DELAY);
|
|
},
|
|
|
|
isActiveFileRenderable: function () {
|
|
if (this.active_frame.data() === undefined) {
|
|
return false;
|
|
}
|
|
return 'renderable' in this.active_frame.data();
|
|
},
|
|
|
|
isActiveFileRunnable: function () {
|
|
return this.isActiveFileExecutable() && ['main_file', 'user_defined_file', 'executable_file'].includes(this.active_frame.data('role'));
|
|
},
|
|
|
|
isActiveFileStoppable: function () {
|
|
return this.isActiveFileRunnable() && this.running;
|
|
},
|
|
|
|
isActiveFileSubmission: function () {
|
|
return ['Submission'].includes(this.active_frame.data('contextType'));
|
|
},
|
|
|
|
isActiveFileTestable: function () {
|
|
return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test', 'teacher_defined_linter'].includes(this.active_frame.data('role'));
|
|
},
|
|
|
|
populateCard: function (card, result, index) {
|
|
card.addClass(this.getCardClass(result));
|
|
card.find('.card-title .filename').text(result.filename);
|
|
card.find('.card-title .number').text(index + 1);
|
|
card.find('.row .col-md-9').eq(0).find('.number').eq(0).text(result.passed);
|
|
card.find('.row .col-md-9').eq(0).find('.number').eq(1).text(result.count);
|
|
if (result.weight !== 0) {
|
|
card.find('.row .col-md-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
|
|
card.find('.row .col-md-9').eq(1).find('.number').eq(1).text(result.weight);
|
|
} else {
|
|
// Hide score row if no score could be achieved
|
|
card.find('.attribute-row.row').eq(1).addClass('d-none');
|
|
}
|
|
card.find('.row .col-md-9').eq(2).html(result.message);
|
|
|
|
// Add error message from code to card
|
|
if (result.error_messages) {
|
|
const targetNode = card.find('.row .col-md-9').eq(3);
|
|
|
|
let errorMessagesToShow = [];
|
|
result.error_messages.forEach(function (item) {
|
|
if (item) {
|
|
errorMessagesToShow.push(item)
|
|
}
|
|
})
|
|
|
|
// delete all current elements
|
|
targetNode.text('');
|
|
// create a new list and append each element
|
|
const ul = document.createElement("ul");
|
|
|
|
// Extract detailed linter results
|
|
if (result.file_role === 'teacher_defined_linter') {
|
|
const detailed_linter_results = result.detailed_linter_results;
|
|
const severity_groups = detailed_linter_results.reduce(function(map, obj) {
|
|
map[obj.severity] = map[obj.severity] || []
|
|
map[obj.severity].push(obj);
|
|
return map;
|
|
}, {});
|
|
|
|
for (let severity in severity_groups) {
|
|
if (!severity_groups.hasOwnProperty(severity)) {
|
|
continue;
|
|
}
|
|
const linter_results = severity_groups[severity]
|
|
|
|
const li = document.createElement("li");
|
|
const text = $.parseHTML(`<u>${severity}:</u>`);
|
|
$(li).append(text);
|
|
ul.append(li);
|
|
|
|
const sub_ul = document.createElement("ul");
|
|
sub_ul.setAttribute('class', 'inline_list');
|
|
for (let check_run of linter_results) {
|
|
const sub_li = document.createElement("li");
|
|
|
|
let scope = '';
|
|
if (check_run.scope) {
|
|
scope = `, ${check_run.scope}()`;
|
|
}
|
|
const context = `${check_run.file_name}: ${check_run.line}${scope}`;
|
|
const line_link = `<a href='#' data-file='${check_run.file_name}' data-line='${check_run.line}'>${context}</a>`;
|
|
const message = `${check_run.name}: ${check_run.result} (${line_link})`;
|
|
const sub_text = $.parseHTML(message);
|
|
$(sub_li).append(sub_text).on("click", "a", this.jumpToSourceLine.bind(this));
|
|
sub_ul.append(sub_li);
|
|
}
|
|
li.append(sub_ul);
|
|
}
|
|
|
|
// Just show standard results for normal test results
|
|
} else {
|
|
errorMessagesToShow.forEach(function (item) {
|
|
var li = document.createElement("li");
|
|
var text = document.createTextNode(item);
|
|
$(li).append(text);
|
|
ul.append(li);
|
|
})
|
|
}
|
|
|
|
// one or more errors?
|
|
if (errorMessagesToShow.length > 1) {
|
|
ul.setAttribute('class', 'inline_list');
|
|
} else {
|
|
ul.setAttribute('class', 'single_entry_inline_list');
|
|
}
|
|
targetNode.append(ul);
|
|
}
|
|
//card.find('.row .col-md-9').eq(4).find('a').attr('href', '#output-' + index);
|
|
},
|
|
|
|
createEventHandler: function (eventType, data) {
|
|
return function (event) {
|
|
CodeOceanEditor.publishCodeOceanEvent({
|
|
category: eventType,
|
|
data: data,
|
|
exercise_id: $('#editor').data('exercise-id'),
|
|
file_id: CodeOceanEditor.active_file.id,
|
|
});
|
|
event.stopPropagation();
|
|
};
|
|
},
|
|
|
|
publishCodeOceanEvent: function (payload) {
|
|
if (this.sendEvents) {
|
|
$.ajax(this.eventURL, {
|
|
type: 'POST',
|
|
cache: false,
|
|
dataType: 'JSON',
|
|
data: {
|
|
event: payload
|
|
},
|
|
success: _.noop,
|
|
error: _.noop
|
|
});
|
|
}
|
|
},
|
|
|
|
sendError: function (message, submission_id) {
|
|
this.showSpinner($('#render'));
|
|
var jqxhr = this.ajax({
|
|
data: {
|
|
error: {
|
|
message: message,
|
|
submission_id: submission_id
|
|
}
|
|
},
|
|
url: $('#editor').data('errors-url')
|
|
});
|
|
jqxhr.always(this.hideSpinner);
|
|
},
|
|
|
|
toggleButtonStates: function () {
|
|
$('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file');
|
|
$('#start-over-active-file').prop('disabled', this.active_frame.data('role') === 'user_defined_file' || this.active_frame.data('read-only') !== undefined);
|
|
$('#dummy').toggle(!this.fileActionsAvailable());
|
|
$('#render').toggle(this.isActiveFileRenderable());
|
|
$('#run').toggle(this.isActiveFileRunnable() && !this.running);
|
|
$('#stop').toggle(this.isActiveFileStoppable());
|
|
$('#test').toggle(this.isActiveFileTestable());
|
|
},
|
|
|
|
jumpToSourceLine: function (event) {
|
|
const file = $(event.target).data('file');
|
|
const line = $(event.target).data('line');
|
|
|
|
const frame = $('div.frame[data-filename="' + file + '"]');
|
|
this.showFrame(frame);
|
|
this.toggleButtonStates();
|
|
|
|
const file_id = frame.find('.editor').data('file-id');
|
|
this.setActiveFile(frame.data('filename'), file_id);
|
|
this.selectFileInJsTree($('#files'), file_id);
|
|
|
|
const editor = this.editor_for_file.get(file);
|
|
editor.gotoLine(line, 0);
|
|
event.preventDefault();
|
|
},
|
|
|
|
augmentStacktraceInOutput: function () {
|
|
if (this.tracepositions_regex) {
|
|
$('#output > .output-element').each($.proxy(function(index, element) {
|
|
element = $(element)
|
|
|
|
const text = _.escape(element.text());
|
|
element.on("click", "a", this.jumpToSourceLine.bind(this));
|
|
|
|
let matches;
|
|
|
|
let augmented_text = element.html();
|
|
while (matches = this.tracepositions_regex.exec(text)) {
|
|
const frame = $('div.frame[data-filename="' + matches[1] + '"]')
|
|
|
|
if (frame.length > 0) {
|
|
augmented_text = augmented_text.replace(new RegExp(_.unescape(matches[0]), 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
|
|
}
|
|
}
|
|
element.html(augmented_text);
|
|
}, this));
|
|
}
|
|
},
|
|
|
|
resetOutputTab: function () {
|
|
this.clearOutput();
|
|
$('#flowrHint').fadeOut();
|
|
this.clearHints();
|
|
this.showOutputBar();
|
|
this.clearFileDownloads();
|
|
},
|
|
|
|
isActiveFileBinary: function () {
|
|
if (this.active_frame.data() === undefined) {
|
|
return false;
|
|
}
|
|
return 'binary' in this.active_frame.data();
|
|
},
|
|
|
|
isActiveFileExecutable: function () {
|
|
if (this.active_frame.data() === undefined) {
|
|
return false;
|
|
}
|
|
return 'executable' in this.active_frame.data();
|
|
},
|
|
|
|
setActiveFile: function (filename, fileId) {
|
|
this.active_file = {
|
|
filename: filename,
|
|
id: fileId
|
|
};
|
|
},
|
|
|
|
showSpinner: function (initiator) {
|
|
$(initiator).find('i.fa-solid, i.fa-regular').hide();
|
|
$(initiator).find('i.fa-spin').addClass('d-inline-block').removeClass('d-none');
|
|
},
|
|
|
|
showStatus: function (output) {
|
|
if (output.status === 'timeout') {
|
|
this.showTimeoutMessage();
|
|
} else if (output.status === 'container_depleted') {
|
|
this.showContainerDepletedMessage();
|
|
} else if (output.status === 'out_of_memory') {
|
|
this.showOutOfMemoryMessage();
|
|
}
|
|
},
|
|
|
|
clearHints: function () {
|
|
var container = $('#error-hints');
|
|
container.find('ul.body > li.hint').remove();
|
|
container.fadeOut();
|
|
},
|
|
|
|
showHint: function (message) {
|
|
var template = function (description, hint) {
|
|
return '\
|
|
<li class="hint">\
|
|
<div class="description">\
|
|
' + description + '\
|
|
</div>\
|
|
<div class="hint">\
|
|
' + hint + '\
|
|
</div>\
|
|
</li>\
|
|
'
|
|
};
|
|
var container = $('#error-hints');
|
|
container.find('ul.body').append(template(message.description, message.hint));
|
|
container.fadeIn();
|
|
},
|
|
|
|
prepareFileDownloads: function(message) {
|
|
const fileTree = $('#download-file-tree');
|
|
fileTree.jstree(message.data);
|
|
fileTree.on('select_node.jstree', function (node, selected, _event) {
|
|
selected.instance.deselect_all();
|
|
const downloadPath = selected.node.original.download_path;
|
|
if (downloadPath) {
|
|
window.location = downloadPath;
|
|
}
|
|
}.bind(this));
|
|
$('#download-files').removeClass('d-none');
|
|
},
|
|
|
|
clearFileDownloads: function() {
|
|
$('#download-files').addClass('d-none');
|
|
$('#download-file-tree').replaceWith($('<div id="download-file-tree">'));
|
|
},
|
|
|
|
showContainerDepletedMessage: function () {
|
|
$.flash.danger({
|
|
icon: ['fa-regular', 'fa-clock'],
|
|
text: $('#editor').data('message-depleted')
|
|
});
|
|
},
|
|
|
|
showOutOfMemoryMessage: function () {
|
|
$.flash.info({
|
|
icon: ['fa-regular', 'fa-clock'],
|
|
text: $('#editor').data('message-out-of-memory')
|
|
});
|
|
},
|
|
|
|
showTimeoutMessage: function () {
|
|
$.flash.info({
|
|
icon: ['fa-regular', 'fa-clock'],
|
|
text: $('#editor').data('message-timeout')
|
|
});
|
|
},
|
|
|
|
showWebsocketError: function (error) {
|
|
if (window.navigator.userAgent.indexOf('Edge') > -1 || window.navigator.userAgent.indexOf('Trident') > -1) {
|
|
// Mute errors in Microsoft Edge and Internet Explorer
|
|
return;
|
|
}
|
|
$.flash.danger({
|
|
text: $('#flash').data('websocket-failure'),
|
|
showPermanent: true
|
|
});
|
|
Sentry.captureException(JSON.stringify(error, ["message", "arguments", "type", "name", "data"]));
|
|
},
|
|
|
|
showFileDialog: function (event) {
|
|
event.preventDefault();
|
|
this.createSubmission('#create-file', null, function (response) {
|
|
$('#code_ocean_file_context_id').val(response.id);
|
|
new bootstrap.Modal($('#modal-file')).show();
|
|
}.bind(this));
|
|
},
|
|
|
|
initializeOutputBarToggle: function () {
|
|
$('#toggle-sidebar-output').on('click', this.hideOutputBar.bind(this));
|
|
$('#toggle-sidebar-output-collapsed').on('click', this.showOutputBar.bind(this));
|
|
$('#output_sidebar').on('transitionend', this.resizeAceEditors.bind(this));
|
|
$('#output_sidebar').on('transitionend', this.resizeSidebars.bind(this));
|
|
},
|
|
|
|
showOutputBar: function () {
|
|
$('#output_sidebar_collapsed').addClass('d-none');
|
|
$('#output_sidebar_uncollapsed').removeClass('d-none');
|
|
$('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col');
|
|
},
|
|
|
|
hideOutputBar: function () {
|
|
$('#output_sidebar_collapsed').removeClass('d-none');
|
|
$('#output_sidebar_uncollapsed').addClass('d-none');
|
|
$('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed');
|
|
},
|
|
|
|
initializeSideBarTooltips: function () {
|
|
$('[data-bs-toggle="tooltip"]').tooltip()
|
|
},
|
|
|
|
initializeDescriptionToggle: function () {
|
|
$('#exercise-headline').on('click', this.toggleDescriptionCard.bind(this));
|
|
$('a#toggle').on('click', this.toggleDescriptionCard.bind(this));
|
|
},
|
|
|
|
toggleDescriptionCard: function (event) {
|
|
$('#description-card').toggleClass('description-card-collapsed').toggleClass('description-card');
|
|
$('#description-symbol').toggleClass('fa-chevron-down').toggleClass('fa-chevron-right');
|
|
var toggle = $('a#toggle');
|
|
toggle.text(toggle.text() == toggle.data('hide') ? toggle.data('show') : toggle.data('hide'));
|
|
this.resizeAceEditors();
|
|
this.resizeSidebars();
|
|
event.preventDefault();
|
|
},
|
|
|
|
/**
|
|
* interventions
|
|
* */
|
|
initializeInterventionTimer: function () {
|
|
const editor = $('#editor');
|
|
|
|
if (editor.data('rfc-interventions') || editor.data('break-interventions') || editor.data('tips-interventions')) { // split in break or rfc intervention
|
|
window.onblur = function () {
|
|
window.blurred = true;
|
|
};
|
|
window.onfocus = function () {
|
|
window.blurred = false;
|
|
};
|
|
|
|
const delta = 100; // time in ms to wait for window event before time gets stopped
|
|
let tid;
|
|
$.ajax({
|
|
data: {
|
|
exercise_id: editor.data('exercise-id'),
|
|
user_id: editor.data('user-id')
|
|
},
|
|
dataType: 'json',
|
|
method: 'GET',
|
|
// get working times for this exercise
|
|
url: editor.data('working-times-url'),
|
|
success: function (data) {
|
|
const percentile75 = data['working_time_75_percentile'];
|
|
const accumulatedWorkTimeUser = data['working_time_accumulated'];
|
|
|
|
let minTimeIntervention = 20 * 60 * 1000;
|
|
|
|
let timeUntilIntervention;
|
|
if ((accumulatedWorkTimeUser - percentile75) > 0) {
|
|
// working time is already over 75 percentile
|
|
timeUntilIntervention = minTimeIntervention;
|
|
} else {
|
|
// working time is less than 75 percentile
|
|
// ensure we give user at least minTimeIntervention before we bother the user
|
|
timeUntilIntervention = Math.max(percentile75 - accumulatedWorkTimeUser, minTimeIntervention);
|
|
}
|
|
|
|
tid = setInterval(function () {
|
|
if (window.blurred) {
|
|
return;
|
|
}
|
|
timeUntilIntervention -= delta;
|
|
if (timeUntilIntervention <= 0) {
|
|
const interventionSaveUrl = editor.data('intervention-save-url');
|
|
clearInterval(tid);
|
|
// timeUntilIntervention passed
|
|
if (editor.data('tips-interventions')) {
|
|
const modal = $('#tips-intervention-modal');
|
|
modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
|
|
new bootstrap.Modal(modal).show();
|
|
$.ajax({
|
|
data: {
|
|
intervention_type: 'TipsIntervention'
|
|
},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
url: interventionSaveUrl
|
|
});
|
|
} else if (editor.data('break-interventions')) {
|
|
const modal = $('#break-intervention-modal');
|
|
modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
|
|
new bootstrap.Modal(modal).show();
|
|
$.ajax({
|
|
data: {
|
|
intervention_type: 'BreakIntervention'
|
|
},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
url: interventionSaveUrl
|
|
});
|
|
} else if (editor.data('rfc-interventions')) {
|
|
const button = $('#requestComments');
|
|
// only show intervention if user did not requested for a comment already
|
|
if (!button.prop('disabled')) {
|
|
$('#rfc_intervention_text').show();
|
|
modal = $('#comment-modal');
|
|
modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
|
|
modal.on('hidden.bs.modal', function () {
|
|
modal.find('.modal-footer').text('');
|
|
});
|
|
new bootstrap.Modal(modal).show();
|
|
$.ajax({
|
|
data: {
|
|
intervention_type: 'QuestionIntervention'
|
|
},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
url: interventionSaveUrl
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}, delta);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
initializeSearchButton: function () {
|
|
$('#btn-search-col').button().click(function () {
|
|
var search = $('#search-input-text').val();
|
|
var course_token = $('#editor').data('course_token')
|
|
var save_search_url = $('#editor').data('search-save-url')
|
|
window.open("https://open.hpi.de/courses/" + course_token + "/pinboard?query=" + search, '_blank');
|
|
// save search
|
|
$.ajax({
|
|
data: {
|
|
search_text: search
|
|
},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
url: save_search_url
|
|
});
|
|
})
|
|
|
|
$('#sidebar-search-collapsed').on('click', this.handleSideBarToggle.bind(this));
|
|
},
|
|
|
|
|
|
initializeEverything: function () {
|
|
CodeOceanEditor.editors = [];
|
|
this.initializeRegexes();
|
|
this.initializeCodePilot();
|
|
this.configureEditors();
|
|
this.initializeEditors();
|
|
this.initializeEventHandlers();
|
|
this.initializeFileTree();
|
|
this.initializeSideBarCollapse();
|
|
this.initializeOutputBarToggle();
|
|
this.initializeDescriptionToggle();
|
|
this.initializeSideBarTooltips();
|
|
this.initializeTooltips();
|
|
this.initializeInterventionTimer();
|
|
this.initializeSearchButton();
|
|
this.initPrompt();
|
|
this.renderScore();
|
|
this.showFirstFile();
|
|
this.resizeAceEditors();
|
|
this.resizeSidebars();
|
|
this.initializeDeadlines();
|
|
CodeOceanEditorTips.initializeEventHandlers();
|
|
|
|
window.addEventListener("turbolinks:before-render", this.unloadAutoSave.bind(this));
|
|
window.addEventListener("beforeunload", this.unloadAutoSave.bind(this));
|
|
// create autosave when the editor is opened the first time
|
|
this.autosave();
|
|
}
|
|
};
|