Moved everything into new files. Made editor.js.erb really small.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
16
app/assets/javascripts/editor/ajax.js.erb
Normal file
16
app/assets/javascripts/editor/ajax.js.erb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
CodeOceanEditorAJAX = {
|
||||||
|
ajax: function(options) {
|
||||||
|
return $.ajax(_.extend({
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'POST',
|
||||||
|
}, options));
|
||||||
|
},
|
||||||
|
|
||||||
|
ajaxError: function(response) {
|
||||||
|
var message = ((response || {}).responseJSON || {}).message || '';
|
||||||
|
|
||||||
|
$.flash.danger({
|
||||||
|
text: message.length > 0 ? message : $('#flash').data('message-failure')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
585
app/assets/javascripts/editor/editor.js.erb
Normal file
585
app/assets/javascripts/editor/editor.js.erb
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
var CodeOceanEditor = {
|
||||||
|
ACE_FILES_PATH: '/assets/ace/',
|
||||||
|
ADEQUATE_PERCENTAGE: 50,
|
||||||
|
ALT_1_KEY_CODE: 161,
|
||||||
|
ALT_2_KEY_CODE: 8220,
|
||||||
|
ALT_3_KEY_CODE: 182,
|
||||||
|
ALT_4_KEY_CODE: 162,
|
||||||
|
ALT_R_KEY_CODE: 174,
|
||||||
|
ALT_S_KEY_CODE: 8218,
|
||||||
|
ALT_T_KEY_CODE: 8224,
|
||||||
|
FILENAME_URL_PLACEHOLDER: '{filename}',
|
||||||
|
SUCCESSFULL_PERCENTAGE: 90,
|
||||||
|
THEME: 'ace/theme/textmate',
|
||||||
|
REMEMBER_TAB: false,
|
||||||
|
AUTOSAVE_INTERVAL: 15 * 1000,
|
||||||
|
REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000,
|
||||||
|
NONE: 0,
|
||||||
|
WEBSOCKET: 1,
|
||||||
|
SERVER_SEND_EVENT: 2,
|
||||||
|
|
||||||
|
editors: [],
|
||||||
|
editor_for_file: new Map(),
|
||||||
|
regex_for_language: new Map(),
|
||||||
|
tracepositions_regex: undefined,
|
||||||
|
|
||||||
|
active_file: undefined,
|
||||||
|
active_frame: undefined,
|
||||||
|
running: false,
|
||||||
|
qa_api: undefined,
|
||||||
|
output_mode_is_streaming: true,
|
||||||
|
runmode: this.NONE,
|
||||||
|
|
||||||
|
websocket: null,
|
||||||
|
turtlescreen: null,
|
||||||
|
numMessages: 0,
|
||||||
|
turtlecanvas: $('#turtlecanvas'),
|
||||||
|
prompt: $('#prompt'),
|
||||||
|
commands: ['input', 'write', 'turtle', 'turtlebatch', 'render', 'exit', 'timeout', 'status'],
|
||||||
|
streams: ['stdin', 'stdout', 'stderr'],
|
||||||
|
lastCopyText: null,
|
||||||
|
|
||||||
|
autosaveTimer: null,
|
||||||
|
autosaveLabel: $("#autosave-label span"),
|
||||||
|
|
||||||
|
ENTER_KEY_CODE: 13,
|
||||||
|
|
||||||
|
flowrOutputBuffer: "",
|
||||||
|
QaApiOutputBuffer: {'stdout': '', 'stderr': ''},
|
||||||
|
flowrResultHtml: '<div class="panel panel-default"><div id="{{headingId}}" role="tab" class="panel-heading"><h4 class="panel-title"><a data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="true" aria-controls="{{collapseId}}"></a></h4></div><div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="panel-collapse collapse"><div class="panel-body"></div></div></div>',
|
||||||
|
|
||||||
|
|
||||||
|
configureEditors: function () {
|
||||||
|
_.each(['modePath', 'themePath', 'workerPath'], function (attribute) {
|
||||||
|
this.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($(this).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.showTab(1);
|
||||||
|
$('#output').scrollTo($(this).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_1_KEY_CODE) {
|
||||||
|
this.showWorkspaceTab(event);
|
||||||
|
} else if (event.which === this.ALT_2_KEY_CODE) {
|
||||||
|
this.showTab(1);
|
||||||
|
} else if (event.which === this.ALT_3_KEY_CODE) {
|
||||||
|
this.showTab(2);
|
||||||
|
} else 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();
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSaveTimer: function () {
|
||||||
|
this.clearTimeout(this.autosaveTimer);
|
||||||
|
this.autosaveTimer = setTimeout(this.autosave, this.AUTOSAVE_INTERVAL);
|
||||||
|
},
|
||||||
|
|
||||||
|
autosave: function () {
|
||||||
|
var date = new Date();
|
||||||
|
this.autosaveLabel.parent().css("visibility", "visible");
|
||||||
|
this.autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds());
|
||||||
|
this.autosaveLabel.text(date.toLocaleTimeString());
|
||||||
|
this.autosaveTimer = null;
|
||||||
|
this.createSubmission($('#autosave'), null);
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeEditors: function () {
|
||||||
|
$('.editor').each(function (index, element) {
|
||||||
|
var editor = ace.edit(element);
|
||||||
|
|
||||||
|
if (this.qa_api) {
|
||||||
|
editor.getSession().on("change", function (deltaObject) {
|
||||||
|
this.qa_api.executeCommand('syncEditor', [this.active_file, deltaObject]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
editor.on("copy", this.handleCopyEvent);
|
||||||
|
|
||||||
|
// listener for autosave
|
||||||
|
session.on("change", function (deltaObject) {
|
||||||
|
this.resetSaveTimer();
|
||||||
|
}.bind(this));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeEventHandlers: function () {
|
||||||
|
$(document).on('click', '#results a', this.showOutput);
|
||||||
|
$(document).on('keypress', this.handleKeyPress);
|
||||||
|
$('a[data-toggle="tab"]').on('show.bs.tab', this.storeTab);
|
||||||
|
this.initializeFileTreeButtons();
|
||||||
|
this.initializeWorkflowButtons();
|
||||||
|
this.initializeWorkspaceButtons();
|
||||||
|
this.initializeRequestForComments()
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeFileTree: function () {
|
||||||
|
$('#files').jstree($('#files').data('entries'));
|
||||||
|
$('#files').on('click', 'li.jstree-leaf', function () {
|
||||||
|
active_file = {
|
||||||
|
filename: $(this).text(),
|
||||||
|
id: parseInt($(this).attr('id'))
|
||||||
|
};
|
||||||
|
var frame = $('[data-file-id="' + active_file.id + '"]').parent();
|
||||||
|
this.showFrame(frame);
|
||||||
|
this.toggleButtonStates();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeFileTreeButtons: function () {
|
||||||
|
$('#create-file').on('click', this.showFileDialog);
|
||||||
|
$('#destroy-file').on('click', this.confirmDestroy);
|
||||||
|
$('#download').on('click', this.downloadCode);
|
||||||
|
$('#request-for-comments').on('click', this.requestComments);
|
||||||
|
},
|
||||||
|
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeWorkflowButtons: function () {
|
||||||
|
$('#start').on('click', this.showWorkspaceTab);
|
||||||
|
//$('#submit').on('click', confirmSubmission);
|
||||||
|
$('#submit').on('click', this.submitCode);
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeWorkspaceButtons: function () {
|
||||||
|
$('#assess').on('click', this.scoreCode); // todo
|
||||||
|
$('#dropdown-render, #render').on('click', this.renderCode);
|
||||||
|
$('#dropdown-run, #run').on('click', this.runCode);
|
||||||
|
$('#dropdown-stop, #stop').on('click', this.stopCode); // todo
|
||||||
|
$('#dropdown-test, #test').on('click', this.testCode); // todo
|
||||||
|
$('#save').on('click', this.saveCode);
|
||||||
|
$('#start-over').on('click', this.confirmReset);
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeRequestForComments: function () {
|
||||||
|
var button = $('.requestCommentsButton');
|
||||||
|
button.hide();
|
||||||
|
button.on('click', function () {
|
||||||
|
$('#comment-modal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#askForCommentsButton').on('click', this.requestComments);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
button.fadeIn();
|
||||||
|
}, 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 is 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((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).text(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);
|
||||||
|
},
|
||||||
|
|
||||||
|
publishCodeOceanEvent: function (eventName, contextData) {
|
||||||
|
var payload = {
|
||||||
|
user: {
|
||||||
|
type: 'User',
|
||||||
|
uuid: $('#editor').data('user-id')
|
||||||
|
},
|
||||||
|
verb: {
|
||||||
|
type: eventName
|
||||||
|
},
|
||||||
|
resource: {
|
||||||
|
type: 'page',
|
||||||
|
uuid: document.location.href
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
with_result: {},
|
||||||
|
in_context: contextData
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax("https://open.hpi.de/lanalytics/log", {
|
||||||
|
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', active_frame.data('role') !== 'user_defined_file');
|
||||||
|
$('#dropdown-render').toggleClass('disabled', !this.isActiveFileRenderable());
|
||||||
|
$('#dropdown-run').toggleClass('disabled', !this.isActiveFileRunnable() || this.running);
|
||||||
|
$('#dropdown-stop').toggleClass('disabled', !this.isActiveFileStoppable());
|
||||||
|
$('#dropdown-test').toggleClass('disabled', !this.isActiveFileTestable());
|
||||||
|
$('#dummy').toggle(!this.fileActionsAvailable());
|
||||||
|
$('#editor-buttons .dropdown-toggle').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');
|
||||||
|
|
||||||
|
this.showWorkspaceTab(null);
|
||||||
|
// set active file ?!?!
|
||||||
|
|
||||||
|
var frame = $('div.frame[data-filename="' + file + '"]');
|
||||||
|
this.showFrame(frame);
|
||||||
|
|
||||||
|
var editor = 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);
|
||||||
|
|
||||||
|
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>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
storeTab: function (event) {
|
||||||
|
localStorage.tab = $(event.target).parent().index();
|
||||||
|
},
|
||||||
|
|
||||||
|
resetOutputTab: function () {
|
||||||
|
this.clearOutput();
|
||||||
|
$('#hint').fadeOut();
|
||||||
|
$('#flowrHint').fadeOut();
|
||||||
|
this.showTab(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
closeEventSource: function (event) {
|
||||||
|
event.target.close();
|
||||||
|
this.hideSpinner();
|
||||||
|
this.running = false;
|
||||||
|
this.toggleButtonStates();
|
||||||
|
|
||||||
|
if (event.type === 'error' || JSON.parse(event.data).code !== 200) {
|
||||||
|
this.ajaxError();
|
||||||
|
this.showTab(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showRequestedTab: function() {
|
||||||
|
if(this.REMEMBER_TAB){
|
||||||
|
var regexp = /tab=(\d+)/;
|
||||||
|
if (regexp.test(window.location.search)) {
|
||||||
|
var index = regexp.exec(window.location.search)[1] - 1;
|
||||||
|
} else {
|
||||||
|
var index = this.localStorage.tab;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// else start with first tab.
|
||||||
|
var index = 0;
|
||||||
|
}
|
||||||
|
this.showTab(index);
|
||||||
|
},
|
||||||
|
|
||||||
|
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')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showTab: function(index) {
|
||||||
|
$('a[data-toggle="tab"]').eq(index || 0).tab('show');
|
||||||
|
},
|
||||||
|
|
||||||
|
showTimeoutMessage: function() {
|
||||||
|
$.flash.info({
|
||||||
|
icon: ['fa', 'fa-clock-o'],
|
||||||
|
text: $('#editor').data('message-timeout')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showWebsocketError: function() {
|
||||||
|
$.flash.danger({
|
||||||
|
text: $('#flash').data('message-failure')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showWorkspaceTab: function(event) {
|
||||||
|
if(event){
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
this.showTab(0);
|
||||||
|
},
|
||||||
|
|
||||||
|
showFileDialog: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.createSubmission(this, null, function(response) {
|
||||||
|
$('#code_ocean_file_context_id').val(response.id);
|
||||||
|
$('#modal-file').modal('show');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeEverything: function() {
|
||||||
|
this.initializeRegexes();
|
||||||
|
this.initializeCodePilot();
|
||||||
|
$('.score, #development-environment').show();
|
||||||
|
this.configureEditors();
|
||||||
|
this.initializeEditors();
|
||||||
|
this.initializeEventHandlers();
|
||||||
|
this.initializeFileTree();
|
||||||
|
this.initializeTooltips();
|
||||||
|
this.initPrompt();
|
||||||
|
this.renderScore();
|
||||||
|
this.showFirstFile();
|
||||||
|
this.showRequestedTab();
|
||||||
|
|
||||||
|
$(window).on("beforeunload", function() {
|
||||||
|
if(this.autosaveTimer != null){
|
||||||
|
this.autosave();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
};
|
266
app/assets/javascripts/editor/evaluation.js.erb
Normal file
266
app/assets/javascripts/editor/evaluation.js.erb
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
CodeOceanEditorEvaluation = {
|
||||||
|
chunkBuffer: [{streamedResponse: true}],
|
||||||
|
|
||||||
|
evaluateCode: function (url, streamed, callback) {
|
||||||
|
(streamed ? this.evaluateCodeWithStreamedResponse : this.evaluateCodeWithoutStreamedResponse)(url, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
evaluateCodeWithStreamedResponse: function (url, onmessageFunction) {
|
||||||
|
this.initWebsocketConnection(url, onmessageFunction);
|
||||||
|
|
||||||
|
// TODO only init turtle when required
|
||||||
|
this.initTurtle();
|
||||||
|
},
|
||||||
|
|
||||||
|
evaluateCodeWithoutStreamedResponse: function (url, callback) {
|
||||||
|
var jqxhr = this.ajax({
|
||||||
|
method: 'GET',
|
||||||
|
url: url
|
||||||
|
});
|
||||||
|
jqxhr.always(this.hideSpinner);
|
||||||
|
jqxhr.done(callback);
|
||||||
|
jqxhr.fail(this.ajaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScoringResponse: function (websocket_event) {
|
||||||
|
var results = JSON.parse(websocket_event.data);
|
||||||
|
this.printScoringResults(results);
|
||||||
|
var score = _.reduce(results, function (sum, result) {
|
||||||
|
return sum + result.score * result.weight;
|
||||||
|
}, 0).toFixed(2);
|
||||||
|
$('#score').data('score', score);
|
||||||
|
this.renderScore();
|
||||||
|
this.showTab(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTestResponse: function (websocket_event) {
|
||||||
|
var result = JSON.parse(websocket_event.data);
|
||||||
|
this.clearOutput();
|
||||||
|
this.printOutput(result, false, 0);
|
||||||
|
if (this.qa_api) {
|
||||||
|
this.qa_api.executeCommand('syncOutput', [result]);
|
||||||
|
}
|
||||||
|
this.showStatus(result);
|
||||||
|
this.showTab(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
printOutput: function (output, colorize, index) {
|
||||||
|
var element = this.findOrCreateOutputElement(index);
|
||||||
|
if (!colorize) {
|
||||||
|
if (output.stdout != undefined && output.stdout != '') {
|
||||||
|
element.append(output.stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.stderr != undefined && output.stderr != '') {
|
||||||
|
element.append('There was an error: StdErr: ' + output.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (output.stderr) {
|
||||||
|
element.addClass('text-warning').append(output.stderr);
|
||||||
|
this.flowrOutputBuffer += output.stderr;
|
||||||
|
this.QaApiOutputBuffer.stderr += output.stderr;
|
||||||
|
} else if (output.stdout) {
|
||||||
|
element.addClass('text-success').append(output.stdout);
|
||||||
|
this.flowrOutputBuffer += output.stdout;
|
||||||
|
this.QaApiOutputBuffer.stdout += output.stdout;
|
||||||
|
} else {
|
||||||
|
element.addClass('text-muted').text($('#output').data('message-no-output'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
printScoringResult: function (result, index) {
|
||||||
|
$('#results').show();
|
||||||
|
var panel = $('#dummies').children().first().clone();
|
||||||
|
this.populatePanel(panel, result, index);
|
||||||
|
$('#results ul').first().append(panel);
|
||||||
|
},
|
||||||
|
|
||||||
|
printScoringResults: function (response) {
|
||||||
|
$('#results ul').first().html('');
|
||||||
|
$('.test-count .number').html(response.length);
|
||||||
|
this.clearOutput();
|
||||||
|
|
||||||
|
_.each(response, function (result, index) {
|
||||||
|
this.printOutput(result, false, index);
|
||||||
|
this.printScoringResult(result, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_.some(response, function (result) {
|
||||||
|
return result.status === 'timeout';
|
||||||
|
})) {
|
||||||
|
this.showTimeoutMessage();
|
||||||
|
}
|
||||||
|
if (_.some(response, function (result) {
|
||||||
|
return result.status === 'container_depleted';
|
||||||
|
})) {
|
||||||
|
this.showContainerDepletedMessage();
|
||||||
|
}
|
||||||
|
if (this.qa_api) {
|
||||||
|
// send test response to QA
|
||||||
|
this.qa_api.executeCommand('syncOutput', [response]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHint: function (object) {
|
||||||
|
var hint = object.data || object.hint;
|
||||||
|
if (hint) {
|
||||||
|
$('#hint .panel-body').text(hint);
|
||||||
|
$('#hint').fadeIn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderScore: function () {
|
||||||
|
var score = parseFloat($('#score').data('score'));
|
||||||
|
var maximum_score = parseFloat($('#score').data('maximum-score'));
|
||||||
|
if (score >= 0 && score <= maximum_score && maximum_score > 0) {
|
||||||
|
var percentage_score = (score / maximum_score * 100 ).toFixed(0);
|
||||||
|
$('.score').html(percentage_score + '%');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.score').html(0 + '%');
|
||||||
|
}
|
||||||
|
this.renderProgressBar(score, maxium_score);
|
||||||
|
},
|
||||||
|
|
||||||
|
scoreCode: function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
runmode = this.SERVER_SEND_EVENT;
|
||||||
|
this.createSubmission(this, null, function (response) {
|
||||||
|
showSpinner($('#assess'));
|
||||||
|
var url = response.score_url;
|
||||||
|
this.evaluateCode(url, true, this.handleScoringResponse);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stopCode: function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#stop').is(':visible')) {
|
||||||
|
if (this.runmode == this.WEBSOCKET) {
|
||||||
|
killWebsocketAndContainer();
|
||||||
|
} else if (this.runmode == this.SERVER_SEND_EVENT) {
|
||||||
|
stopCodeServerSendEvent(event);
|
||||||
|
}
|
||||||
|
this.runmode = this.NONE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopCodeServerSendEvent: function (event) {
|
||||||
|
var jqxhr = this.ajax({
|
||||||
|
data: {
|
||||||
|
container_id: $('#stop').data('container').id
|
||||||
|
},
|
||||||
|
url: $('#stop').data('url')
|
||||||
|
});
|
||||||
|
jqxhr.always(function () {
|
||||||
|
this.hideSpinner();
|
||||||
|
this.running = false;
|
||||||
|
this.toggleButtonStates();
|
||||||
|
});
|
||||||
|
jqxhr.fail(ajaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
killWebsocketAndContainer: function () {
|
||||||
|
if (this.websocket.readyState != WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.websocket.send(JSON.stringify({cmd: 'exit'}));
|
||||||
|
this.websocket.flush();
|
||||||
|
this.websocket.close();
|
||||||
|
this.hideSpinner();
|
||||||
|
this.running = false;
|
||||||
|
this.toggleButtonStates();
|
||||||
|
this.hidePrompt();
|
||||||
|
},
|
||||||
|
|
||||||
|
// todo set this from websocket command, required to e.g. stop container
|
||||||
|
storeContainerInformation: function (event) {
|
||||||
|
var container_information = JSON.parse(event.data);
|
||||||
|
$('#stop').data('container', container_information);
|
||||||
|
|
||||||
|
if (_.size(container_information.port_bindings) > 0) {
|
||||||
|
$.flash.info({
|
||||||
|
icon: ['fa', 'fa-exchange'],
|
||||||
|
text: _.map(container_information.port_bindings, function (key, value) {
|
||||||
|
var url = window.location.protocol + '//' + window.location.hostname + ':' + key;
|
||||||
|
return $('#run').data('message-network').replace('%{port}', value).replace(/%{address}/g, url);
|
||||||
|
}).join('\n')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//TODO: Move Prompt Part in own component
|
||||||
|
showPrompt: function(msg) {
|
||||||
|
var label = $('#prompt .input-group-addon');
|
||||||
|
label.text(msg.data || label.data('prompt'));
|
||||||
|
if (this.prompt.isPresent() && this.prompt.hasClass('hidden')) {
|
||||||
|
this.prompt.removeClass('hidden');
|
||||||
|
}
|
||||||
|
$('#prompt input').focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
hidePrompt: function() {
|
||||||
|
if (this.prompt.isPresent() && !this.prompt.hasClass('hidden')) {
|
||||||
|
this.prompt.addClass('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initPrompt: function() {
|
||||||
|
if ($('#run').isPresent()) {
|
||||||
|
$('#run').bind('click', this.hidePrompt);
|
||||||
|
}
|
||||||
|
if ($('#prompt').isPresent()) {
|
||||||
|
$('#prompt').on('keypress', this.handlePromptKeyPress);
|
||||||
|
$('#prompt-submit').on('click', this.submitPromptInput);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submitPromptInput: function() {
|
||||||
|
var input = $('#prompt-input');
|
||||||
|
var message = input.val();
|
||||||
|
this.websocket.send(JSON.stringify({cmd: 'result', 'data': message}));
|
||||||
|
this.websocket.flush();
|
||||||
|
input.val('');
|
||||||
|
this.hidePrompt();
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePromptKeyPress: function(evt) {
|
||||||
|
if (evt.which === this.ENTER_KEY_CODE) {
|
||||||
|
this.submitPromptInput();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderWebsocketOutput: function(msg){
|
||||||
|
var element = this.findOrCreateRenderElement(0);
|
||||||
|
element.append(msg.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
printWebsocketOutput: function(msg) {
|
||||||
|
if (!msg.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msg.data = msg.data.replace(/(\r)/gm, "\n");
|
||||||
|
var stream = {};
|
||||||
|
stream[msg.stream] = msg.data;
|
||||||
|
this.printOutput(stream, true, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearOutput: function() {
|
||||||
|
$('#output pre').remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
printChunk: function(event) {
|
||||||
|
var output = JSON.parse(event.data);
|
||||||
|
if (output) {
|
||||||
|
this.printOutput(output, true, 0);
|
||||||
|
// send test response to QA
|
||||||
|
// we are expecting an array of outputs:
|
||||||
|
if (this.qa_api) {
|
||||||
|
this.chunkBuffer.push(output);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.resetOutputTab();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
91
app/assets/javascripts/editor/flowr.js.erb
Normal file
91
app/assets/javascripts/editor/flowr.js.erb
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
CodeOceanEditorFlowr = {
|
||||||
|
isFlowrEnabled: true,
|
||||||
|
|
||||||
|
handleStderrOutputForFlowr: function () {
|
||||||
|
if (!this.isFlowrEnabled) return;
|
||||||
|
|
||||||
|
var flowrUrl = $('#flowrHint').data('url');
|
||||||
|
var flowrHintBody = $('#flowrHint .panel-body');
|
||||||
|
var queryParameters = {
|
||||||
|
query: this.flowrOutputBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
flowrHintBody.empty();
|
||||||
|
|
||||||
|
jQuery.getJSON(flowrUrl, queryParameters, function (data) {
|
||||||
|
jQuery.each(data.queryResults, function (index, question) {
|
||||||
|
var collapsibleTileHtml = this.flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + question).replace(/{{headingId}}/g, 'heading-' + question);
|
||||||
|
var resultTile = $(collapsibleTileHtml);
|
||||||
|
|
||||||
|
resultTile.find('h4 > a').text(question.title + ' | Found via ' + question.source);
|
||||||
|
resultTile.find('.panel-body').html(question.body);
|
||||||
|
resultTile.find('.panel-body').append('<a href="' + question.url + '" class="btn btn-primary btn-block">Open this question</a>');
|
||||||
|
|
||||||
|
flowrHintBody.append(resultTile);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.queryResults.length !== 0) {
|
||||||
|
$('#flowrHint').fadeIn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.flowrOutputBuffer = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
requestComments: function () {
|
||||||
|
var user_id = $('#editor').data('user-id');
|
||||||
|
var exercise_id = $('#editor').data('exercise-id');
|
||||||
|
var file_id = $('.editor').data('id');
|
||||||
|
var question = $('#question').val();
|
||||||
|
|
||||||
|
var createRequestForComments = function (submission) {
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/request_for_comments',
|
||||||
|
data: {
|
||||||
|
request_for_comment: {
|
||||||
|
exercise_id: exercise_id,
|
||||||
|
file_id: file_id,
|
||||||
|
submission_id: submission.id,
|
||||||
|
question: question
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).done(function () {
|
||||||
|
this.hideSpinner();
|
||||||
|
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
|
||||||
|
}).error(this.ajaxError);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createSubmission($('.requestCommentsButton'), null, createRequestForComments);
|
||||||
|
|
||||||
|
$('#comment-modal').modal('hide');
|
||||||
|
var button = $('.requestCommentsButton');
|
||||||
|
button.fadeOut();
|
||||||
|
},
|
||||||
|
|
||||||
|
//tODO move codepilot out of here.
|
||||||
|
initializeCodePilot: function () {
|
||||||
|
if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) {
|
||||||
|
$('#editor-column').addClass('col-md-8').removeClass('col-md-10');
|
||||||
|
$('#questions-column').addClass('col-md-3');
|
||||||
|
|
||||||
|
var node = document.getElementById('questions-holder');
|
||||||
|
var url = $('#questions-holder').data('url');
|
||||||
|
|
||||||
|
this.qa_api = new QaApi(node, url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleQaApiOutput: function () {
|
||||||
|
if (this.qa_api) {
|
||||||
|
this.qa_api.executeCommand('syncOutput', [[this.QaApiOutputBuffer]]);
|
||||||
|
// reset the object
|
||||||
|
}
|
||||||
|
this.QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleStreamedResponseForCodePilot: function (event) {
|
||||||
|
this.qa_api.executeCommand('syncOutput', [this.chunkBuffer]);
|
||||||
|
this.chunkBuffer = [{streamedResponse: true}];
|
||||||
|
}
|
||||||
|
};
|
171
app/assets/javascripts/editor/submissions.js.erb
Normal file
171
app/assets/javascripts/editor/submissions.js.erb
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
|
||||||
|
CodeOceanEditorSubmissions = {
|
||||||
|
collectFiles: function() {
|
||||||
|
var editable_editors = _.filter(this.editors, function(editor) {
|
||||||
|
return !editor.getReadOnly();
|
||||||
|
});
|
||||||
|
return _.map(editable_editors, function(editor) {
|
||||||
|
return {
|
||||||
|
content: editor.getValue(),
|
||||||
|
file_id: $(editor.container).data('file-id')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createSubmission: function (initiator, filter, callback) {
|
||||||
|
this.showSpinner(initiator);
|
||||||
|
var jqxhr = this.ajax({
|
||||||
|
data: {
|
||||||
|
submission: {
|
||||||
|
cause: $(initiator).data('cause') || $(initiator).prop('id'),
|
||||||
|
exercise_id: $('#editor').data('exercise-id'),
|
||||||
|
files_attributes: (filter || _.identity)(this.collectFiles())
|
||||||
|
},
|
||||||
|
annotations_arr: []
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'POST',
|
||||||
|
url: $(initiator).data('url') || $('#editor').data('submissions-url')
|
||||||
|
});
|
||||||
|
jqxhr.always(this.hideSpinner);
|
||||||
|
jqxhr.done(this.createSubmissionCallback);
|
||||||
|
jqxhr.done(callback);
|
||||||
|
jqxhr.fail(this.ajaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
createSubmissionCallback: function(data){
|
||||||
|
// set all frames context types to submission
|
||||||
|
$('.frame').each(function(index, element) {
|
||||||
|
$(element).data('context-type', 'Submission');
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the ids of the editors and reload the annotations
|
||||||
|
for (var i = 0; i < this.editors.length; i++) {
|
||||||
|
|
||||||
|
// set the data attribute to submission
|
||||||
|
//$(editors[i].container).data('context-type', 'Submission');
|
||||||
|
|
||||||
|
var file_id_old = $(this.editors[i].container).data('file-id');
|
||||||
|
|
||||||
|
// file_id_old is always set. Either it is a reference to a teacher supplied given file, or it is the actual id of a new user created file.
|
||||||
|
// This is the case, since it is set via a call to ancestor_id on the model, which returns either file_id if set, or id if it is not set.
|
||||||
|
// therefore the else part is not needed any longer...
|
||||||
|
|
||||||
|
// if we have an file_id set (the file is a copy of a teacher supplied given file)
|
||||||
|
if (file_id_old != null){
|
||||||
|
// if we find file_id_old (this is the reference to the base file) in the submission, this is the match
|
||||||
|
for(var j = 0; j< data.files.length; j++){
|
||||||
|
if(data.files[j].file_id == file_id_old){
|
||||||
|
//$(editors[i].container).data('id') = data.files[j].id;
|
||||||
|
$(this.editors[i].container).data('id', data.files[j].id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// toggle button states (it might be the case that the request for comments button has to be enabled
|
||||||
|
this.toggleButtonStates();
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyFile: function() {
|
||||||
|
this.createSubmission($('#destroy-file'), function(files) {
|
||||||
|
return _.reject(files, function(file) {
|
||||||
|
return file.file_id === active_file.id;
|
||||||
|
});
|
||||||
|
}, window.CodeOcean.refresh);
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadCode: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.createSubmission(this, null,function(response) {
|
||||||
|
var url = response.download_url;
|
||||||
|
|
||||||
|
// to download just a single file, use the following url
|
||||||
|
//var url = response.download_file_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
|
||||||
|
window.location = url;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetCode: function() {
|
||||||
|
this.showSpinner(this);
|
||||||
|
this.ajax({
|
||||||
|
method: 'GET',
|
||||||
|
url: $('#start-over').data('url')
|
||||||
|
}).success(function(response) {
|
||||||
|
this.hideSpinner();
|
||||||
|
_.each(this.editors, function(editor) {
|
||||||
|
var file_id = $(editor.container).data('file-id');
|
||||||
|
var file = _.find(response.files, function(file) {
|
||||||
|
return file.id === file_id;
|
||||||
|
});
|
||||||
|
editor.setValue(file.content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCode: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#render').is(':visible')) {
|
||||||
|
this.createSubmission(this, null, function (response) {
|
||||||
|
var url = response.render_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename);
|
||||||
|
var pop_up_window = window.open(url);
|
||||||
|
if (pop_up_window) {
|
||||||
|
pop_up_window.onerror = function (message) {
|
||||||
|
this.clearOutput();
|
||||||
|
this.printOutput({
|
||||||
|
stderr: message
|
||||||
|
}, true, 0);
|
||||||
|
this.sendError(message, response.id);
|
||||||
|
this.showTab(1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//Todo Split up in submitpart and run part
|
||||||
|
runCode: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#run').is(':visible')) {
|
||||||
|
this.runmode = this.WEBSOCKET;
|
||||||
|
this.createSubmission(this, null, function(response) {
|
||||||
|
|
||||||
|
//Run part starts here
|
||||||
|
$('#stop').data('url', response.stop_url);
|
||||||
|
this.running = true;
|
||||||
|
this.showSpinner($('#run'));
|
||||||
|
this.toggleButtonStates();
|
||||||
|
var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename);
|
||||||
|
this.evaluateCode(url, true, function(evt) { this.parseCanvasMessage(evt.data, true); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveCode: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.createSubmission(this, null, function() {
|
||||||
|
$.flash.success({
|
||||||
|
text: $('#save').data('message-success')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
testCode: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#test').is(':visible')) {
|
||||||
|
this.createSubmission(this, null, function(response) {
|
||||||
|
this.showSpinner($('#test'));
|
||||||
|
var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename);
|
||||||
|
this.evaluateCode(url, true, this.handleTestResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submitCode: function() {
|
||||||
|
this.createSubmission($('#submit'), null, function (response) {
|
||||||
|
if (response.redirect) {
|
||||||
|
localStorage.removeItem('tab');
|
||||||
|
window.location = response.redirect;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
45
app/assets/javascripts/editor/turtle.js.erb
Normal file
45
app/assets/javascripts/editor/turtle.js.erb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
CodeOceanEditorTurtle = {
|
||||||
|
initTurtle: function () {
|
||||||
|
this.turtlescreen = new Turtle(this.websocket, this.turtlecanvas);
|
||||||
|
if ($('#run').isPresent()) {
|
||||||
|
$('#run').bind('click', this.hideCanvas);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTurtleCommand: function (msg) {
|
||||||
|
if (msg.action in this.turtlescreen) {
|
||||||
|
var result = this.turtlescreen[msg.action].apply(this.turtlescreen, msg.args);
|
||||||
|
this.websocket.send(JSON.stringify({cmd: 'result', 'result': result}));
|
||||||
|
} else {
|
||||||
|
this.websocket.send(JSON.stringify({cmd: 'exception', exception: 'AttributeError', message: msg.action}));
|
||||||
|
}
|
||||||
|
this.websocket.flush();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTurtlebatchCommand: function (msg) {
|
||||||
|
for (var i = 0; i < msg.batch.length; i++) {
|
||||||
|
var cmd = msg.batch[i];
|
||||||
|
this.turtlescreen[cmd[0]].apply(this.turtlescreen, cmd[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showCanvas: function () {
|
||||||
|
if ($('#turtlediv').isPresent()
|
||||||
|
&& this.turtlecanvas.hasClass('hidden')) {
|
||||||
|
// initialize two-column layout
|
||||||
|
$('#output-col1').addClass('col-lg-7 col-md-7 two-column');
|
||||||
|
this.turtlecanvas.removeClass('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hideCanvas: function () {
|
||||||
|
if ($('#turtlediv').isPresent()
|
||||||
|
&& !(this.turtlecanvas.hasClass('hidden'))) {
|
||||||
|
var output = $('#output-col1');
|
||||||
|
if (output.hasClass('two-column')) {
|
||||||
|
output.removeClass('col-lg-7 col-md-7 two-column');
|
||||||
|
}
|
||||||
|
this.turtlecanvas.addClass('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
86
app/assets/javascripts/editor/websocket.js.erb
Normal file
86
app/assets/javascripts/editor/websocket.js.erb
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
CodeOceanEditorWebsocket = {
|
||||||
|
initWebsocketConnection: function (url, onmessageFunction) {
|
||||||
|
//TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss)
|
||||||
|
//causes: Puma::HttpParserError: Invalid HTTP format, parsing fails.
|
||||||
|
//TODO: make sure that this gets cached.
|
||||||
|
this.websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url);
|
||||||
|
this.websocket.onopen = function (evt) {
|
||||||
|
this.resetOutputTab();
|
||||||
|
}; // todo show some kind of indicator for established connection
|
||||||
|
this.websocket.onclose = function (evt) { /* expected at some point */
|
||||||
|
};
|
||||||
|
this.websocket.onmessage = onmessageFunction;
|
||||||
|
this.websocket.onerror = function (evt) {
|
||||||
|
this.showWebsocketError();
|
||||||
|
};
|
||||||
|
this.websocket.flush = function () {
|
||||||
|
this.send('\n');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//ToDo: Move websocket and commands variable in here
|
||||||
|
executeWebsocketCommand: function (msg) {
|
||||||
|
if ($.inArray(msg.cmd, this.commands) == -1) {
|
||||||
|
console.log("Unknown command: " + msg.cmd);
|
||||||
|
// skipping unregistered commands is required
|
||||||
|
// as we may receive mirrored response due to internal behaviour
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (msg.cmd) {
|
||||||
|
case 'input':
|
||||||
|
this.showPrompt(msg);
|
||||||
|
break;
|
||||||
|
case 'write':
|
||||||
|
this.printWebsocketOutput(msg);
|
||||||
|
break;
|
||||||
|
case 'turtle':
|
||||||
|
this.showCanvas();
|
||||||
|
this.handleTurtleCommand(msg);
|
||||||
|
break;
|
||||||
|
case 'turtlebatch':
|
||||||
|
this.showCanvas();
|
||||||
|
this.handleTurtlebatchCommand(msg);
|
||||||
|
break;
|
||||||
|
case 'render':
|
||||||
|
this.renderWebsocketOutput(msg);
|
||||||
|
break;
|
||||||
|
case 'exit':
|
||||||
|
this.killWebsocketAndContainer();
|
||||||
|
this.handleQaApiOutput();
|
||||||
|
this.handleStderrOutputForFlowr();
|
||||||
|
this.augmentStacktraceInOutput();
|
||||||
|
break;
|
||||||
|
case 'timeout':
|
||||||
|
// just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes.
|
||||||
|
this.showTimeoutMessage();
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
this.showStatus(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseCanvasMessage: function (message, recursive) {
|
||||||
|
var msg;
|
||||||
|
message = message.replace(/^\s+|\s+$/g, "");
|
||||||
|
try {
|
||||||
|
// todo validate json instead of catching
|
||||||
|
msg = JSON.parse(message);
|
||||||
|
} catch (e) {
|
||||||
|
if (!recursive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// why does docker sometimes send multiple commands at once?
|
||||||
|
message = message.replace(/^\s+|\s+$/g, "");
|
||||||
|
var messages = message.split("\n");
|
||||||
|
for (var i = 0; i < messages.length; i++) {
|
||||||
|
if (!messages[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.parseCanvasMessage(messages[i], false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.executeWebsocketCommand(msg);
|
||||||
|
}
|
||||||
|
};
|
Reference in New Issue
Block a user