Files
codeocean/app/assets/javascripts/editor/editor.js.erb
Sebastian Serth 336519b21d Refactor CodeOcean::Config class
The new architecture memorizes settings (which we mostly did after reading the config so far) and also exposes the resulting file path as well as further settings.

This change is a prerequisite to define a dependency with Sprockets.
2024-05-21 21:56:31 +02:00

1102 lines
43 KiB
Plaintext

var CodeOceanEditor = {
THEME: window.getCurrentTheme() === 'dark' ? 'ace/theme/tomorrow_night' : 'ace/theme/tomorrow',
//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,
<% @config ||= CodeOcean::Config.new(:code_ocean, erb: false).read %>
// 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: Routes.events_path(),
fileTypeURL: Routes.file_types_path(),
confirmDestroy: function (event) {
event.preventDefault();
if (confirm(I18n.t('shared.confirm_destroy'))) {
this.destroyFile();
}
},
confirmReset: function (event) {
event.preventDefault();
const initiator = $(event.target.closest("button"));
if (confirm(initiator.data('message-confirm'))) {
this.resetCode(initiator);
}
},
confirmResetActiveFile: function (event) {
event.preventDefault();
const initiator = $(event.target.closest("button"));
let message = initiator.data('message-confirm');
message = message.replace('%{filename}', CodeOceanEditor.active_file.filename)
if (confirm(message)) {
this.resetCode(initiator, 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 'info'
} else if (result.stderr && !result.score) {
return 'danger';
} else if (result.score < 1) {
return 'warning';
} else {
return 'success';
}
},
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(true).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');
},
startSentryTransaction: function (initiator) {
const cause = initiator.data('cause') || initiator.prop('id');
this.sentryTransaction = window.SentryUtils.startIdleTransaction(
Sentry.getCurrentHub(),
{ name: cause, op: "transaction" },
0, // Idle Timeout
window.SentryUtils.TRACING_DEFAULTS.finalTimeout,
true); // onContext
Sentry.getCurrentHub().configureScope(scope => scope.setSpan(this.sentryTransaction));
},
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);
var document = editor.getSession().getDocument();
// insert pre-existing code into editor. we have to use insertFullLines, 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.insertFullLines(0, content.text().split(/\n/));
// remove last (empty) that is there by default line
document.removeFullLines(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).data('file-id'), editor);
var session = editor.getSession();
var mode = $(element).data('mode')
session.setMode(mode);
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 (editor, deltaObject, session) {
// editor.curOp.command is empty for changes that are not caused by user input.
// With that we can differentiate between changes caused by user input and
// changes caused by changed text because of WebSocket notifications from a pair programming partner.
if(_.isEmpty(editor.curOp.command)) {
return;
}
App.synchronized_editor?.editor_change(deltaObject, this.active_file);
this.resetSaveTimer();
}.bind(this, editor));
}.bind(this));
},
handleAceThemeChangeEvent: function (event) {
this.editors.forEach(function (editor) {
editor.setTheme(this.THEME);
}.bind(this));
},
initializeEventHandlers: function () {
$(document).on('click', '#results a', this.showOutput.bind(this));
$(document).on('keydown', this.handleKeyPress.bind(this));
$(document).on('theme:change:ace', this.handleAceThemeChangeEvent.bind(this));
$('#start_chat').on('click', function(event) {
this.createEventHandler('pp_start_chat', null)(event)
// Allow to open the new tab even in Safari.
// See: https://stackoverflow.com/a/70463940
setTimeout(() => {
var pop_up_window = window.open($('#start_chat').data('url'), '_blank');
if (pop_up_window) {
pop_up_window.onerror = function (message) {
$.flash.danger({text: message});
this.sendError(message, null);
};
}
})
}.bind(this));
this.initializeFileTreeButtons();
this.initializeWorkspaceButtons();
this.initializeRequestForComments()
},
teardownEventHandlers: function () {
$(document).unbind('click');
$(document).unbind('keydown');
this.teardownWorkspaceButtons();
this.teardownRequestForComments();
const rfcModal = $('#comment-modal');
if (rfcModal.isPresent()) {
bootstrap.Modal.getInstance(rfcModal)?.hide();
}
this.teardownFileTreeButtons();
},
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);
});
},
initializeFileTree: function (own_solution = false) {
let filesInstance;
if (own_solution) {
filesInstance = $('#own-files');
} else {
filesInstance = $('#files');
}
const jsTreeConfig = filesInstance.data('entries') || {core: {}};
jsTreeConfig.core.themes = {...jsTreeConfig.core.themes, name: window.getCurrentTheme() === "dark" ? "default-dark" : "default"}
filesInstance.jstree(jsTreeConfig);
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));
$(document).on('theme:change', function(event) {
const jsTree = filesInstance?.jstree(true);
if (jsTree) {
const newColorScheme = event.detail.currentTheme;
// Update the JStree theme
jsTree?.set_theme(newColorScheme === "dark" ? "default-dark" : "default");
}
});
},
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));
},
teardownFileTreeButtons: function () {
$('#create-file').unbind('click');
$('#destroy-file').unbind('click');
$('#destroy-file-collapsed').unbind('click');
$('#download').unbind('click');
},
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 &quot;(.+?)&quot;, line (\d+)/g);
this.regex_for_language.set("ace/mode/java", /(?:\.\/)?(.*\.java):(\d+):/g);
},
initializeWorkspaceButtons: function () {
$('#assess').one('click', this.scoreCode.bind(this));
$('#render').on('click', this.renderCode.bind(this));
$('#run').on('click', this.runCode.bind(this));
$('#stop').on('click', this.stopCode.bind(this));
$('#test').on('click', this.testCode.bind(this));
$('#start-over').on('click', this.confirmReset.bind(this));
$('#start-over-active-file').on('click', this.confirmResetActiveFile.bind(this));
},
teardownWorkspaceButtons: function () {
$('#assess').unbind('click');
$('#render').unbind('click');
$('#run').unbind('click');
$('#stop').unbind('click');
$('#test').unbind('click');
$('#start-over').unbind('click');
$('#start-over-active-file').unbind('click');
},
initializeRequestForComments: function () {
var button = $('#requestComments');
button.prop('disabled', true);
button.on('click', function () {
button.tooltip('hide');
$('#rfc_intervention_text').hide()
new bootstrap.Modal($('#comment-modal')).show();
});
$('#askForCommentsButton').one('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);
},
teardownRequestForComments: function () {
$('#requestComments').unbind('click');
$('#askForCommentsButton').unbind('click');
$('#closeAskForCommentsButton').unbind('click');
},
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;
},
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(`card border-${this.getCardClass(result)}`);
card.find('.card-header').addClass(`bg-${this.getCardClass(result)} text-white`);
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 (const [severity, linter_results] of Object.entries(severity_groups)) {
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 (const 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());
const runStopGroup = $('#run-stop-button-group');
if (typeof runStopGroup.tooltip === 'function') {
runStopGroup.tooltip('hide');
}
runStopGroup.toggleClass('flex-grow-1', this.isActiveFileRunnable() || this.isActiveFileStoppable());
$('#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_id);
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) {
const element = $(initiator);
if (initiator && element) {
$(initiator).closest('[data-bs-toggle="tooltip"]').tooltip('hide');
$(initiator).find('i.fa-solid, i.fa-regular').hide();
$(initiator).find('i.fa-spin').addClass('d-inline-block').removeClass('d-none');
}
},
showStatus: function (output) {
switch (output.status) {
case 'timeout':
case 'buffer_overflow':
this.showTimeoutMessage();
break;
case 'container_depleted':
this.showContainerDepletedMessage();
break;
case 'out_of_memory':
this.showOutOfMemoryMessage();
break;
case 'runner_in_use':
this.showRunnerInUseMessage();
break;
case 'scoring_failure':
this.showScoringFailureMessage();
break;
case 'not_for_all_users_submitted':
this.showNotForAllUsersSubmittedMessage(output.failed_users);
break;
case 'scoring_too_late':
this.showScoringTooLateMessage(output.score_sent);
break;
case 'exercise_finished':
this.showExerciseFinishedMessage(output.url);
break;
}
},
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')
});
},
showRunnerInUseMessage: function () {
$.flash.warning({
icon: ['fa-solid', 'fa-triangle-exclamation'],
text: I18n.t('exercises.editor.runner_in_use')
});
},
showScoringFailureMessage: function () {
$.flash.danger({
icon: ['fa-solid', 'fa-exclamation-circle'],
text: I18n.t('exercises.editor.submit_failure_all')
});
},
showNotForAllUsersSubmittedMessage: function (failed_users) {
$.flash.warning({
icon: ['fa-solid', 'fa-triangle-exclamation'],
text: I18n.t('exercises.editor.submit_failure_other_users', {user: failed_users})
});
},
showScoringTooLateMessage: function (score_sent) {
$.flash.info({
icon: ['fa-solid', 'fa-circle-info'],
text: I18n.t('exercises.editor.submit_too_late', {score_sent: score_sent})
});
},
showExerciseFinishedMessage: function (url) {
$.flash.success({
showPermanent: true,
icon: ['fa-solid', 'fa-graduation-cap'],
text: I18n.t('exercises.editor.exercise_finished', {url: url})
});
},
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({
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');
if (!modal.isPresent()) {
// The modal is not present (e.g., because the site was navigated), so we don't continue here.
return;
}
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');
if (!modal.isPresent()) {
// The modal is not present (e.g., because the site was navigated), so we don't continue here.
return;
}
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();
const modal = $('#comment-modal');
if (!modal.isPresent()) {
// The modal is not present (e.g., because the site was navigated), so we don't continue here.
return;
}
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);
}
});
}
},
applyChanges: function (delta, active_file) {
const editor = this.editor_for_file?.get(active_file.id);
if (editor === undefined) {
return;
}
editor.session.doc.applyDeltas([delta]);
},
showPartnersConnectionStatus: function (status, username) {
switch(status) {
case 'connected':
$('#pg_session').text(I18n.t('exercises.editor.is_online', {name: username}));
break;
case 'disconnected':
$('#pg_session').text(I18n.t('exercises.editor.is_offline', {name: username}));
break;
}
},
initializeEverything: function () {
CodeOceanEditor.editors = [];
this.initializeRegexes();
this.initializeEditors();
this.initializeEventHandlers();
this.initializeFileTree();
this.initializeSideBarCollapse();
this.initializeOutputBarToggle();
this.initializeDescriptionToggle();
this.initializeSideBarTooltips();
this.initializeInterventionTimer();
this.initPrompt();
this.renderScore();
this.showFirstFile();
this.resizeAceEditors();
this.resizeSidebars();
this.initializeDeadlines();
CodeOceanEditorTips.initializeEventHandlers();
window.addEventListener("turbolinks:before-render", App.synchronized_editor?.disconnect.bind(App.synchronized_editor));
window.addEventListener("beforeunload", App.synchronized_editor?.disconnect.bind(App.synchronized_editor));
window.addEventListener("turbolinks:before-render", this.autosaveIfChanged.bind(this));
window.addEventListener("beforeunload", this.autosaveIfChanged.bind(this));
// create autosave when the editor is opened the first time
this.autosave();
}
};