Files
codeocean/app/assets/javascripts/editor/editor.js.erb
kiragrammel 175c8933f3 Automatically submit LTI grade on each score run
With this commit, we refactor the overall score handling of CodeOcean. Previously, "Score" and "Submit" were two distinct actions, requiring users to confirm the LTI transmission of their score (after assessing their submission). This yielded many questions and was unnecessary, since LTI parameters are no longer expiring after each use. Therefore, we can now transmit the current grade on each score run with the very same LTI parameters. As a consequence, the LTI consumer gets a more detailed history of the scores, enabling further analytical insights.

For users, the previous "Submit" button got replaced with a notification that is shown as soon as the full score got reached. Then, learners can decide to "finalize" their work on the given exercise, which will initiate a redirect to a follow-up action (as defined in the RedirectBehavior). This RedirectBehavior has also been unified and simplified for better readability.

As part of this refactoring, we rephrased the notifications and UX workflow of a) the LTI transmission, b) the finalization of an exercise (measured by reaching the full score) and c) the deadline handling (on time, within grace period, too late). Those information are now separately shown, potentially resulting in multiple notifications. As a side effect, they are much better maintainable, and the LTI transmission is more decoupled from this notification handling.
2023-11-23 14:42:10 +01:00

1123 lines
44 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: 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,
<% 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();
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.replace(/#$/, ''))
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 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).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;
},
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(`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 (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());
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.configureEditors();
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();
}
};