594 lines
19 KiB
Plaintext
594 lines
19 KiB
Plaintext
var CodeOceanEditor = {
|
|
//ACE-Editor-Path
|
|
// ruby part adds the relative_url_root, if it is set.
|
|
ACE_FILES_PATH: '<%= (defined? Rails.application.config.relative_url_root) && Rails.application.config.relative_url_root != nil && Rails.application.config.relative_url_root != "" ? Rails.application.config.relative_url_root : "" %>' + '/assets/ace/',
|
|
THEME: 'ace/theme/textmate',
|
|
|
|
//Color-Encoding for Percentages in Progress Bars (For submissions)
|
|
ADEQUATE_PERCENTAGE: 50,
|
|
SUCCESSFULL_PERCENTAGE: 90,
|
|
|
|
//Key-Codes (for Hotkeys)
|
|
ALT_R_KEY_CODE: 174,
|
|
ALT_S_KEY_CODE: 8218,
|
|
ALT_T_KEY_CODE: 8224,
|
|
ENTER_KEY_CODE: 13,
|
|
|
|
//Request-For-Comments-Configuration
|
|
REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000,
|
|
REQUEST_TOOLTIP_TIME: 5000,
|
|
|
|
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).read(erb: false) %>
|
|
sendLearningAnalyticEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>,
|
|
learningAnalyticsURL: "<%= @config['codeocean_events'] ? @config['codeocean_events']['url'] : "" %>",
|
|
|
|
|
|
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($(this).data('message-confirm'))) {
|
|
this.destroyFile();
|
|
}
|
|
},
|
|
|
|
confirmReset: function (event) {
|
|
event.preventDefault();
|
|
if (confirm($('#start-over').data('message-confirm'))) {
|
|
this.resetCode();
|
|
}
|
|
},
|
|
|
|
fileActionsAvailable: function () {
|
|
return this.isActiveFileRenderable() || this.isActiveFileRunnable() || this.isActiveFileStoppable() || this.isActiveFileTestable();
|
|
},
|
|
|
|
findOrCreateOutputElement: function (index) {
|
|
if ($('#output-' + index).isPresent()) {
|
|
return $('#output-' + index);
|
|
} else {
|
|
var element = $('<pre>').attr('id', 'output-' + index);
|
|
$('#output').append(element);
|
|
return element;
|
|
}
|
|
},
|
|
|
|
findOrCreateRenderElement: function (index) {
|
|
if ($('#render-' + index).isPresent()) {
|
|
return $('#render-' + index);
|
|
} else {
|
|
var element = $('<div>').attr('id', 'render-' + index);
|
|
$('#render').append(element);
|
|
return element;
|
|
}
|
|
},
|
|
|
|
getPanelClass: function (result) {
|
|
if (result.stderr && !result.score) {
|
|
return 'panel-danger';
|
|
} else if (result.score < 1) {
|
|
return 'panel-warning';
|
|
} else {
|
|
return 'panel-success';
|
|
}
|
|
},
|
|
|
|
showOutput: function(event) {
|
|
event.preventDefault();
|
|
this.showOutputBar();
|
|
$('#output').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 + '%');
|
|
},
|
|
|
|
showFirstFile: function() {
|
|
var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
|
|
var file_id = frame.find('.editor').data('file-id');
|
|
this.setActiveFile(frame.data('filename'), file_id);
|
|
$('#files').jstree().select_node(file_id);
|
|
this.showFrame(frame);
|
|
this.toggleButtonStates();
|
|
},
|
|
|
|
showFrame: function(frame) {
|
|
this.active_frame = frame;
|
|
$('.frame').hide();
|
|
frame.show();
|
|
},
|
|
|
|
getProgressBarClass: function (percentage) {
|
|
if (percentage < this.ADEQUATE_PERCENTAGE) {
|
|
return 'progress-bar progress-bar-striped progress-bar-danger';
|
|
} else if (percentage < this.SUCCESSFULL_PERCENTAGE) {
|
|
return 'progress-bar progress-bar-striped progress-bar-warning';
|
|
} else {
|
|
return 'progress-bar progress-bar-striped progress-bar-success';
|
|
}
|
|
},
|
|
|
|
handleKeyPress: function (event) {
|
|
if (event.which === this.ALT_R_KEY_CODE) {
|
|
$('#run').trigger('click');
|
|
} else if (event.which === this.ALT_S_KEY_CODE) {
|
|
$('#assess').trigger('click');
|
|
} else if (event.which === this.ALT_T_KEY_CODE) {
|
|
$('#test').trigger('click');
|
|
} else {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
},
|
|
|
|
handleCopyEvent: function (text) {
|
|
this.lastCopyText = text;
|
|
},
|
|
|
|
handlePasteEvent: function (pasteObject) {
|
|
var same = (this.lastCopyText === pasteObject.text);
|
|
|
|
// if the text is not copied from within the editor (from any file), send an event to lanalytics
|
|
if (!same) {
|
|
this.publishCodeOceanEvent("codeocean_editor_paste", {
|
|
text: pasteObject.text,
|
|
exercise: $('#editor').data('exercise-id'),
|
|
file_id: "1"
|
|
|
|
});
|
|
}
|
|
},
|
|
|
|
hideSpinner: function () {
|
|
$('button i.fa').show();
|
|
$('button i.fa-spin').hide();
|
|
},
|
|
|
|
|
|
resizeAceEditors: function (){
|
|
$('.editor').each(function (index, element) {
|
|
this.resizeParentOfAceEditor(element);
|
|
}.bind(this));
|
|
},
|
|
|
|
resizeParentOfAceEditor: function (element){
|
|
// calculate needed size: window height - position of top of button-bar - 60 for bar itself and margins
|
|
var windowHeight = window.innerHeight - $('#editor-buttons').offset().top - 60;
|
|
$(element).parent().height(windowHeight);
|
|
},
|
|
|
|
initializeEditors: function () {
|
|
$('.editor').each(function (index, element) {
|
|
|
|
// Resize frame on load
|
|
this.resizeParentOfAceEditor(element);
|
|
|
|
// Resize frame on window size change
|
|
$(window).resize(function(){
|
|
this.resizeParentOfAceEditor(element);
|
|
}.bind(this));
|
|
|
|
var editor = ace.edit(element);
|
|
|
|
if (this.qa_api) {
|
|
editor.getSession().on("change", function (deltaObject) {
|
|
this.qa_api.executeCommand('syncEditor', [this.active_file, deltaObject]);
|
|
}.bind(this));
|
|
}
|
|
|
|
var document = editor.getSession().getDocument();
|
|
// insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added
|
|
var file_id = $(element).data('file-id');
|
|
var content = $('.editor-content[data-file-id=' + file_id + ']');
|
|
this.setActiveFile($(element).parent().data('filename'), file_id);
|
|
|
|
document.insertLines(0, content.text().split(/\n/));
|
|
// remove last (empty) that is there by default line
|
|
document.removeLines(document.getLength() - 1, document.getLength() - 1);
|
|
editor.setReadOnly($(element).data('read-only') !== undefined);
|
|
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();
|
|
session.setMode($(element).data('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'));
|
|
}
|
|
|
|
var file_id = $(element).data('id');
|
|
|
|
/*
|
|
* Register event handlers
|
|
*/
|
|
|
|
// editor itself
|
|
editor.on("paste", this.handlePasteEvent.bind(this));
|
|
editor.on("copy", this.handleCopyEvent.bind(this));
|
|
|
|
// listener for autosave
|
|
session.on("change", function (deltaObject) {
|
|
this.resetSaveTimer();
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
initializeEventHandlers: function () {
|
|
$(document).on('click', '#results a', this.showOutput.bind(this));
|
|
$(document).on('keypress', this.handleKeyPress.bind(this));
|
|
this.initializeFileTreeButtons();
|
|
this.initializeWorkspaceButtons();
|
|
this.initializeRequestForComments()
|
|
},
|
|
|
|
initializeFileTree: function () {
|
|
$('#files').jstree($('#files').data('entries'));
|
|
$('#files').on('click', 'li.jstree-leaf', function (event) {
|
|
active_file = {
|
|
filename: $(event.target).parent().text(),
|
|
id: parseInt($(event.target).parent().attr('id'))
|
|
};
|
|
var frame = $('[data-file-id="' + active_file.id + '"]').parent();
|
|
this.showFrame(frame);
|
|
this.toggleButtonStates();
|
|
}.bind(this));
|
|
},
|
|
|
|
initializeFileTreeButtons: function () {
|
|
$('#create-file').on('click', this.showFileDialog.bind(this));
|
|
$('#create-file-collapsed').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));
|
|
$('#download-collapsed').on('click', this.downloadCode.bind(this));
|
|
$('#request-for-comments').on('click', this.requestComments.bind(this));
|
|
},
|
|
|
|
initializeSideBarCollapse: function() {
|
|
$('#sidebar-collapse-collapsed').on('click',this.handleSideBarToggle.bind(this));
|
|
$('#sidebar-collapse').on('click',this.handleSideBarToggle.bind(this))
|
|
},
|
|
|
|
handleSideBarToggle: function() {
|
|
$('#sidebar').toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed');
|
|
$('#sidebar-collapsed').toggleClass('hidden');
|
|
$('#sidebar-uncollapsed').toggleClass('hidden');
|
|
},
|
|
|
|
initializeRegexes: function () {
|
|
this.regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g);
|
|
this.regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g);
|
|
},
|
|
|
|
initializeTooltips: function () {
|
|
$('[data-tooltip]').tooltip();
|
|
},
|
|
|
|
|
|
initializeWorkspaceButtons: function () {
|
|
$('#submit').on('click', this.submitCode.bind(this));
|
|
$('#assess').on('click', this.scoreCode.bind(this));
|
|
$('#dropdown-render, #render').on('click', this.renderCode.bind(this));
|
|
$('#dropdown-run, #run').on('click', this.runCode.bind(this));
|
|
$('#dropdown-stop, #stop').on('click', this.stopCode.bind(this));
|
|
$('#dropdown-test, #test').on('click', this.testCode.bind(this));
|
|
$('#save').on('click', this.saveCode.bind(this));
|
|
$('#start-over').on('click', this.confirmReset.bind(this));
|
|
$('#start-over-collapsed').on('click', this.confirmReset.bind(this));
|
|
},
|
|
|
|
initializeRequestForComments: function () {
|
|
var button = $('#requestComments');
|
|
button.prop('disabled', true);
|
|
button.on('click', function () {
|
|
$('#comment-modal').modal('show');
|
|
});
|
|
|
|
$('#askForCommentsButton').on('click', this.requestComments.bind(this));
|
|
|
|
setTimeout(function () {
|
|
button.prop('disabled', false);
|
|
button.tooltip('show');
|
|
setTimeout(function() {
|
|
button.tooltip('hide');
|
|
}, this.REQUEST_TOOLTIP_TIME);
|
|
}.bind(this), this.REQUEST_FOR_COMMENTS_DELAY);
|
|
},
|
|
|
|
isActiveFileRenderable: function () {
|
|
return 'renderable' in this.active_frame.data();
|
|
},
|
|
|
|
isActiveFileRunnable: function () {
|
|
return this.isActiveFileExecutable() && ['main_file', 'user_defined_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'].includes(this.active_frame.data('role'));
|
|
},
|
|
|
|
isBrowserSupported: function () {
|
|
// websockets are used for run, score and test
|
|
return Modernizr.websockets;
|
|
},
|
|
|
|
populatePanel: function (panel, result, index) {
|
|
panel.removeClass('panel-default').addClass(this.getPanelClass(result));
|
|
panel.find('.panel-title .filename').text(result.filename);
|
|
panel.find('.panel-title .number').text(index + 1);
|
|
panel.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed);
|
|
panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
|
|
panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
|
|
panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
|
|
panel.find('.row .col-sm-9').eq(2).html(result.message);
|
|
if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(', '));
|
|
panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
|
|
},
|
|
|
|
// move URL to config file, only fire event if desired.
|
|
publishCodeOceanEvent: function (eventName, contextData) {
|
|
if(this.sendLearningAnalyticEvents){
|
|
|
|
// enhance contextData hash with the user agent
|
|
contextData['user_agent'] = navigator.userAgent;
|
|
|
|
var payload = {
|
|
user: {
|
|
type: 'User',
|
|
uuid: $('#editor').data('user-id'),
|
|
external_id: $('#editor').data('user-external-id')
|
|
},
|
|
verb: {
|
|
type: eventName
|
|
},
|
|
resource: {
|
|
type: 'page',
|
|
uuid: document.location.href
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
with_result: {},
|
|
in_context: contextData
|
|
};
|
|
|
|
$.ajax(this.learningAnalyticsURL, {
|
|
type: 'POST',
|
|
cache: false,
|
|
dataType: 'JSON',
|
|
data: payload,
|
|
success: {},
|
|
error: {}
|
|
})
|
|
|
|
}
|
|
},
|
|
|
|
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);
|
|
jqxhr.success(this.renderHint);
|
|
},
|
|
|
|
toggleButtonStates: function () {
|
|
$('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file');
|
|
$('#dummy').toggle(!this.fileActionsAvailable());
|
|
$('#render').toggle(this.isActiveFileRenderable());
|
|
$('#run').toggle(this.isActiveFileRunnable() && !this.running);
|
|
$('#stop').toggle(this.isActiveFileStoppable());
|
|
$('#test').toggle(this.isActiveFileTestable());
|
|
},
|
|
|
|
jumpToSourceLine: function (event) {
|
|
var file = $(event.target).data('file');
|
|
var line = $(event.target).data('line');
|
|
|
|
// set active file, only needed for codepilot, so skipped for now
|
|
|
|
var frame = $('div.frame[data-filename="' + file + '"]');
|
|
this.showFrame(frame);
|
|
|
|
var editor = this.editor_for_file.get(file);
|
|
editor.gotoLine(line, 0);
|
|
|
|
},
|
|
|
|
augmentStacktraceInOutput: function () {
|
|
if (this.tracepositions_regex) {
|
|
var element = $('#output>pre');
|
|
var text = element.text();
|
|
element.on("click", "a", this.jumpToSourceLine.bind(this));
|
|
|
|
var matches;
|
|
|
|
while (matches = this.tracepositions_regex.exec(text)) {
|
|
var frame = $('div.frame[data-filename="' + matches[1] + '"]')
|
|
|
|
if (frame.length > 0) {
|
|
element.html(text.replace(matches[0], "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>"));
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
resetOutputTab: function () {
|
|
this.clearOutput();
|
|
$('#hint').fadeOut();
|
|
$('#flowrHint').fadeOut();
|
|
this.showOutputBar();
|
|
},
|
|
|
|
isActiveFileBinary: function () {
|
|
return 'binary' in this.active_frame.data();
|
|
},
|
|
|
|
isActiveFileExecutable: function () {
|
|
return 'executable' in this.active_frame.data();
|
|
},
|
|
|
|
setActiveFile: function (filename, fileId) {
|
|
this.active_file = {
|
|
filename: filename,
|
|
id: fileId
|
|
};
|
|
},
|
|
|
|
showSpinner: function(initiator) {
|
|
$(initiator).find('i.fa').hide();
|
|
$(initiator).find('i.fa-spin').show();
|
|
},
|
|
|
|
showStatus: function(output) {
|
|
if (output.status === 'timeout') {
|
|
this.showTimeoutMessage();
|
|
} else if (output.status === 'container_depleted') {
|
|
this.showContainerDepletedMessage();
|
|
} else if (output.stderr) {
|
|
$.flash.danger({
|
|
icon: ['fa', 'fa-bug'],
|
|
text: $('#run').data('message-failure')
|
|
});
|
|
}
|
|
},
|
|
|
|
showContainerDepletedMessage: function() {
|
|
$.flash.danger({
|
|
icon: ['fa', 'fa-clock-o'],
|
|
text: $('#editor').data('message-depleted')
|
|
});
|
|
},
|
|
|
|
showTimeoutMessage: function() {
|
|
$.flash.info({
|
|
icon: ['fa', 'fa-clock-o'],
|
|
text: $('#editor').data('message-timeout')
|
|
});
|
|
},
|
|
|
|
showWebsocketError: function() {
|
|
$.flash.danger({
|
|
text: $('#flash').data('message-failure')
|
|
});
|
|
},
|
|
|
|
showFileDialog: function(event) {
|
|
event.preventDefault();
|
|
this.createSubmission('#create-file', null, function(response) {
|
|
$('#code_ocean_file_context_id').val(response.id);
|
|
$('#modal-file').modal('show');
|
|
}.bind(this));
|
|
},
|
|
|
|
initializeOutputBarToggle: function() {
|
|
$('#toggle-sidebar-output').on('click',this.hideOutputBar.bind(this));
|
|
$('#toggle-sidebar-output-collapsed').on('click',this.showOutputBar.bind(this));
|
|
},
|
|
|
|
showOutputBar: function() {
|
|
$('#output_sidebar_collapsed').addClass('hidden');
|
|
$('#output_sidebar_uncollapsed').removeClass('hidden');
|
|
$('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col');
|
|
},
|
|
|
|
hideOutputBar: function() {
|
|
$('#output_sidebar_collapsed').removeClass('hidden');
|
|
$('#output_sidebar_uncollapsed').addClass('hidden');
|
|
$('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed');
|
|
},
|
|
|
|
initializeSideBarTooltips: function() {
|
|
$('[data-toggle="tooltip"]').tooltip()
|
|
},
|
|
|
|
initializeDescriptionToggle: function() {
|
|
$('#exercise-headline').on('click',this.toggleDescriptionPanel.bind(this))
|
|
},
|
|
|
|
toggleDescriptionPanel: function() {
|
|
$('#description-panel').toggleClass('description-panel-collapsed');
|
|
$('#description-panel').toggleClass('description-panel');
|
|
$('#description-symbol').toggleClass('fa-chevron-down');
|
|
$('#description-symbol').toggleClass('fa-chevron-right');
|
|
// resize ace editors
|
|
this.resizeAceEditors();
|
|
|
|
},
|
|
|
|
|
|
initializeEverything: function() {
|
|
this.initializeRegexes();
|
|
this.initializeCodePilot();
|
|
$('.score, #development-environment').show();
|
|
this.configureEditors();
|
|
this.initializeEditors();
|
|
this.initializeEventHandlers();
|
|
this.initializeFileTree();
|
|
this.initializeSideBarCollapse();
|
|
this.initializeOutputBarToggle();
|
|
this.initializeDescriptionToggle();
|
|
this.initializeSideBarTooltips();
|
|
this.initializeTooltips();
|
|
this.initPrompt();
|
|
this.renderScore();
|
|
this.showFirstFile();
|
|
|
|
$(window).on("beforeunload", this.unloadAutoSave.bind(this));
|
|
}
|
|
}; |