
* This commit refactors the table used to store events. * We also use a UUID as session identifier in the synchronized editor to support multiple concurrent tabs opened by the same user. * Further, we renamed some methods to make them easier to distinguish.
1101 lines
43 KiB
Plaintext
1101 lines
43 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,
|
|
lastDeltaObject: 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).parent().data('filename'), 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 (deltaObject, session) {
|
|
if (this.compareDeltaObjects(deltaObject, CodeOceanEditor.lastDeltaObject)) {
|
|
CodeOceanEditor.lastDeltaObject = null;
|
|
return;
|
|
}
|
|
App.synchronized_editor?.editor_change(deltaObject, this.active_file);
|
|
|
|
// TODO: This is a workaround for a bug in Ace. Remove when upgrading Ace.
|
|
this.handleUTF16Surrogates(deltaObject, session);
|
|
this.resetSaveTimer();
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
handleAceThemeChangeEvent: function (event) {
|
|
this.editors.forEach(function (editor) {
|
|
editor.setTheme(this.THEME);
|
|
}.bind(this));
|
|
},
|
|
|
|
handleUTF16Surrogates: function (AceDeltaObject, AceSession) {
|
|
if (AceDeltaObject.data === undefined || AceDeltaObject.data.action !== "removeText") {
|
|
return;
|
|
}
|
|
|
|
const codePoint = AceDeltaObject.data.text.codePointAt(0);
|
|
if (0xDC00 <= codePoint && codePoint <= 0xDFFF) {
|
|
// The text contains a UTF-16 surrogate pair, and the only the lower part is removed.
|
|
// We need to remove the high surrogate pair as well.
|
|
const currentCharacter = AceDeltaObject.data.range
|
|
const previousCharacter = {
|
|
start: {
|
|
row: currentCharacter.start.row,
|
|
column: currentCharacter.start.column - 1
|
|
},
|
|
end: {
|
|
row: currentCharacter.start.row,
|
|
column: currentCharacter.start.column
|
|
}
|
|
}
|
|
AceSession.remove(previousCharacter);
|
|
}
|
|
},
|
|
|
|
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 "(.+?)", line (\d+)/g);
|
|
this.regex_for_language.set("ace/mode/java", /(?:\.\/)?(.*\.java):(\d+):/g);
|
|
},
|
|
|
|
initializeWorkspaceButtons: function () {
|
|
$('#submit').one('click', this.submitCode.bind(this));
|
|
$('#assess').on('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 () {
|
|
$('#submit').unbind('click');
|
|
$('#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());
|
|
$('#run-stop-button-group').tooltip('hide').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);
|
|
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).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) {
|
|
if (output.status === 'timeout' || output.status === 'buffer_overflow') {
|
|
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({
|
|
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) {
|
|
this.lastDeltaObject = delta;
|
|
const editor = this.editor_for_file.get(active_file.filename)
|
|
editor.session.doc.applyDeltas([delta]);
|
|
},
|
|
|
|
compareDeltaObjects: function (delta, last_delta) {
|
|
if (delta === null || last_delta === null) {
|
|
return false;
|
|
}
|
|
|
|
const delta_data = delta.data
|
|
// We need this manual comparison because the range uses two different classes
|
|
// and there is no function to compare these two objects.
|
|
return delta_data.action === last_delta.action &&
|
|
delta_data.range.start.row === last_delta.range.start.row &&
|
|
delta_data.range.start.column === last_delta.range.start.column &&
|
|
delta_data.range.end.row === last_delta.range.end.row &&
|
|
delta_data.range.end.column === last_delta.range.end.column &&
|
|
delta_data.text === last_delta.text;
|
|
},
|
|
|
|
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();
|
|
}
|
|
};
|