Merge branch 'master' into travis-green-docker

took all the newer versions. Only thing I deleted is the part #to pass the test "it executes the run command" ...
We need to shortly discuss this yqbk.

Conflicts:
	lib/docker_client.rb
This commit is contained in:
Ralf Teusner
2016-11-09 18:25:02 +01:00
43 changed files with 1802 additions and 1415 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/config/secrets.yml /config/secrets.yml
/config/sendmail.yml /config/sendmail.yml
/config/smtp.yml /config/smtp.yml
/config/docker.yml.erb
/config/*.production.yml /config/*.production.yml
/config/*.staging.yml /config/*.staging.yml
/coverage /coverage

View File

@ -419,6 +419,3 @@ DEPENDENCIES
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
web-console (~> 2.0) web-console (~> 2.0)
will_paginate (~> 3.0) will_paginate (~> 3.0)
BUNDLED WITH
1.12.4

View File

@ -24,4 +24,5 @@
//= require bootstrap_pagedown //= require bootstrap_pagedown
//= require markdown.converter //= require markdown.converter
//= require markdown.sanitizer //= require markdown.sanitizer
//= require markdown.editor //= require markdown.editor
//= require ../../../vendor/assets/javascripts/ace/ext-language_tools

File diff suppressed because it is too large Load Diff

View 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')
});
}
};

View File

@ -0,0 +1,24 @@
CodeOceanEditorCodePilot = {
qa_api: undefined,
QaApiOutputBuffer: {'stdout': '', 'stderr': ''},
initializeCodePilot: function () {
if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) {
$('#editor-column').addClass('col-md-10').removeClass('col-md-12');
$('#questions-column').addClass('col-md-2');
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': ''};
}
};

View File

@ -0,0 +1,574 @@
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,
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();
},
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);
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 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) {
// 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("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', 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');
},
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));
}
};

View File

@ -0,0 +1,167 @@
CodeOceanEditorEvaluation = {
chunkBuffer: [{streamedResponse: true}],
/**
* Scoring-Functions
*/
scoreCode: function (event) {
event.preventDefault();
this.createSubmission('#assess', null, function (response) {
this.showSpinner($('#assess'));
$('#score_div').removeClass('hidden');
var url = response.score_url;
this.initializeSocketForScoring(url);
}.bind(this));
},
handleScoringResponse: function (results) {
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();
},
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);
}.bind(this));
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, maximum_score);
},
/**
* Testing-Logic
*/
handleTestResponse: function (result) {
this.clearOutput();
this.printOutput(result, false, 0);
if (this.qa_api) {
this.qa_api.executeCommand('syncOutput', [result]);
}
this.showStatus(result);
this.showOutputBar();
},
/**
* Stop-Logic
*/
stopCode: function (event) {
event.preventDefault();
if (this.isActiveFileStoppable()) {
this.websocket.send(JSON.stringify({'cmd': 'client_kill'}));
this.killWebsocket();
this.cleanUpUI();
}
},
killWebsocket: function () {
if (this.websocket != null && this.websocket.getReadyState() != WebSocket.OPEN) {
return;
}
this.websocket.killWebSocket();
this.running = false;
},
cleanUpUI: function() {
this.hideSpinner();
this.toggleButtonStates();
this.hidePrompt();
},
/**
* Output-Logic
*/
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();
},
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'));
}
}
};

View File

@ -0,0 +1,50 @@
CodeOceanEditorWebsocket = {
websocket: null,
createSocketUrl: function(url) {
var rel_url_root = '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>';
return '<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + rel_url_root + window.location.port + url;
},
initializeSocket: function(url) {
this.websocket = new CommandSocket(this.createSocketUrl(url),
function (evt) {
this.resetOutputTab();
}.bind(this)
);
this.websocket.onError(this.showWebsocketError.bind(this));
},
initializeSocketForTesting: function(url) {
this.initializeSocket(url);
this.websocket.on('default',this.handleTestResponse.bind(this));
this.websocket.on('exit', this.handleExitCommand.bind(this));
},
initializeSocketForScoring: function(url) {
this.initializeSocket(url);
this.websocket.on('default',this.handleScoringResponse.bind(this));
this.websocket.on('exit', this.handleExitCommand.bind(this));
},
initializeSocketForRunning: function(url) {
this.initializeSocket(url);
this.websocket.on('input',this.showPrompt.bind(this));
this.websocket.on('write', this.printWebsocketOutput.bind(this));
this.websocket.on('turtle', this.handleTurtleCommand.bind(this));
this.websocket.on('turtlebatch', this.handleTurtlebatchCommand.bind(this));
this.websocket.on('render', this.renderWebsocketOutput.bind(this));
this.websocket.on('exit', this.handleExitCommand.bind(this));
this.websocket.on('timeout', this.showTimeoutMessage.bind(this));
this.websocket.on('status', this.showStatus.bind(this));
},
handleExitCommand: function() {
this.killWebsocket();
this.handleQaApiOutput();
this.handleStderrOutputForFlowr();
this.augmentStacktraceInOutput();
this.cleanUpTurtle();
this.cleanUpUI();
}
};

View File

@ -0,0 +1,35 @@
CodeOceanEditorFlowr = {
isFlowrEnabled: true,
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>',
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 = '';
}
};

View File

@ -0,0 +1,93 @@
CodeOceanEditorFlowr = {
isFlowrEnabled: true,
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>',
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 = '';
}
};
CodeOceanEditorCodePilot = {
qa_api: undefined,
QaApiOutputBuffer: {'stdout': '', 'stderr': ''},
initializeCodePilot: function () {
if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) {
$('#editor-column').addClass('col-md-10').removeClass('col-md-12');
$('#questions-column').addClass('col-md-2');
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': ''};
}
};
CodeOceanEditorRequestForComments = {
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')});
}.bind(this)).error(this.ajaxError.bind(this));
};
this.createSubmission($('.requestCommentsButton'), null, createRequestForComments.bind(this));
$('#comment-modal').modal('hide');
var button = $('#requestComments');
button.prop('disabled', true);
},
};

View File

@ -0,0 +1,45 @@
CodeOceanEditorPrompt = {
prompt: '#prompt',
showPrompt: function(msg) {
var label = $('#prompt .input-group-addon');
var prompt = $(this.prompt);
label.text(msg.data || label.data('prompt'));
if (prompt.isPresent() && prompt.hasClass('hidden')) {
prompt.removeClass('hidden');
}
$('#prompt input').focus();
},
hidePrompt: function() {
var prompt = $(this.prompt);
if (prompt.isPresent() && !prompt.hasClass('hidden')) {
prompt.addClass('hidden');
}
},
initPrompt: function() {
if ($('#run').isPresent()) {
$('#run').bind('click', this.hidePrompt.bind(this));
}
if ($('#prompt').isPresent()) {
$('#prompt').on('keypress', this.handlePromptKeyPress.bind(this));
$('#prompt-submit').on('click', this.submitPromptInput.bind(this));
}
},
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();
}
}
};

View File

@ -0,0 +1,211 @@
CodeOceanEditorSubmissions = {
FILENAME_URL_PLACEHOLDER: '{filename}',
AUTOSAVE_INTERVAL: 15 * 1000,
autosaveTimer: null,
autosaveLabel: "#autosave-label span",
/**
* Submission-Creation
*/
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.bind(this));
jqxhr.done(this.createSubmissionCallback.bind(this));
if(callback != null){
jqxhr.done(callback.bind(this));
}
jqxhr.fail(this.ajaxError.bind(this));
},
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')
};
});
},
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) and the new file-ids are present in the response
if (file_id_old != null && data.files){
// 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();
},
/**
* File-Management
*/
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('#download', 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);
}.bind(this));
}.bind(this));
},
renderCode: function(event) {
event.preventDefault();
if ($('#render').is(':visible')) {
this.createSubmission('#render', 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.showOutputBar();
};
}
});
}
},
/**
* Execution-Logic
*/
runCode: function(event) {
event.preventDefault();
if ($('#run').is(':visible')) {
this.createSubmission('#run', null, function(response) {
//Run part starts here
$('#stop').data('url', response.stop_url);
this.running = true;
this.showSpinner($('#run'));
$('#score_div').addClass('hidden');
this.toggleButtonStates();
var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename);
this.initializeSocketForRunning(url);
}.bind(this));
}
},
saveCode: function(event) {
event.preventDefault();
this.createSubmission('#save', null, function() {
$.flash.success({
text: $('#save').data('message-success')
});
});
},
testCode: function(event) {
event.preventDefault();
if ($('#test').is(':visible')) {
this.createSubmission('#test', null, function(response) {
this.showSpinner($('#test'));
$('#score_div').addClass('hidden');
var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename);
this.initializeSocketForTesting(url);
}.bind(this));
}
},
submitCode: function() {
this.createSubmission($('#submit'), null, function (response) {
if (response.redirect) {
localStorage.removeItem('tab');
window.location = response.redirect;
}
})
},
/**
* Autosave-Logic
*/
resetSaveTimer: function () {
clearTimeout(this.autosaveTimer);
this.autosaveTimer = setTimeout(this.autosave.bind(this), this.AUTOSAVE_INTERVAL);
},
unloadAutoSave: function() {
if(this.autosaveTimer != null){
this.autosave();
}
},
autosave: function () {
var date = new Date();
var autosaveLabel = $(this.autosaveLabel);
autosaveLabel.parent().css("visibility", "visible");
autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds());
autosaveLabel.text(date.toLocaleTimeString());
this.autosaveTimer = null;
this.createSubmission($('#autosave'), null);
}
};

View File

@ -0,0 +1,48 @@
CodeOceanEditorTurtle = {
turtlecanvas: null,
turtlescreen: null,
resetTurtle: true,
initTurtle: function () {
if (this.resetTurtle) {
this.resetTurtle = false;
this.turtlecanvas = $('#turtlecanvas');
this.turtlescreen = new Turtle(this.websocket, this.turtlecanvas);
}
},
cleanUpTurtle: function() {
this.resetTurtle = true;
},
handleTurtleCommand: function (msg) {
this.initTurtle();
this.showCanvas();
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) {
this.initTurtle();
this.showCanvas();
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');
}
}
};

View File

@ -0,0 +1,112 @@
CommandSocket = function(url, onOpen) {
this.handlers = {};
this.websocket = new WebSocket(url);
this.websocket.onopen = onOpen;
this.websocket.onmessage = this.onMessage.bind(this);
this.websocket.flush = function () {
this.send('\n');
}
};
CommandSocket.prototype.onError = function(callback){
this.websocket.onerror = callback
};
/**
* Allows it to register an event-handler on the given cmd.
* The handler needs to accept one argument, the message.
* There is only handler per command at the moment.
* @param command
* @param handler
*/
CommandSocket.prototype.on = function(command, handler) {
this.handlers[command] = handler;
};
/**
* Used to initialize the recursive message parser.
* @param event
*/
CommandSocket.prototype.onMessage = function(event) {
//Parses the message (serches for linebreaks) and executes every contained cmd.
this.parseMessage(event.data, true)
};
/**
* Parses a message, checks wether it contains multiple commands (seperated by linebreaks)
* This needs to be done because of the behavior of the docker-socket connection.
* Because of this, sometimes multiple commands might be executed in one message.
* @param message
* @param recursive
* @returns {boolean}
*/
CommandSocket.prototype.parseMessage = function(message, recursive) {
var msg;
var message_string = message.replace(/^\s+|\s+$/g, "");
try {
// todo validate json instead of catching
msg = JSON.parse(message_string);
} catch (e) {
if (!recursive) {
return false;
}
// why does docker sometimes send multiple commands at once?
message_string = message_string.replace(/^\s+|\s+$/g, "");
var messages = message_string.split("\n");
for (var i = 0; i < messages.length; i++) {
if (!messages[i]) {
continue;
}
this.parseMessage(messages[i], false);
}
return;
}
this.executeCommand(msg);
};
/**
* Executes the handler that is registered for a certain command.
* Does nothing if the command was not specified yet.
* If there is a null-handler (defined with on('default',func)) this gets
* executed if the command was not registered or the message has no cmd prop.
* @param cmd
*/
CommandSocket.prototype.executeCommand = function(cmd) {
if ('cmd' in cmd && cmd.cmd in this.handlers) {
this.handlers[cmd.cmd](cmd);
} else if ('default' in this.handlers) {
this.handlers['default'](cmd);
}
};
/**
* Used to send a message through the socket.
* If data is not a string we'll try use jsonify to make it a string.
* @param data
*/
CommandSocket.prototype.send = function(data) {
this.websocket.send(data);
};
/**
* Returns the ready state of the socket.
*/
CommandSocket.prototype.getReadyState = function() {
return this.websocket.readyState;
};
/**
* Flush the websocket.
*/
CommandSocket.prototype.flush = function() {
this.websocket.flush();
};
/**
* Closes the websocket.
*/
CommandSocket.prototype.killWebSocket = function() {
this.websocket.flush();
this.websocket.close();
};

View File

@ -1,55 +0,0 @@
$(function() {
var ACE_FILES_PATH = '/assets/ace/';
var THEME = 'ace/theme/textmate';
var configureEditors = function() {
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
};
var initializeEditors = function() {
$('.editor').each(function(index, element) {
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 + ']');
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(THEME);
var textarea = $('textarea[id="exercise_files_attributes_'+index+'_content"]');
var content = textarea.val();
if (content != undefined)
{
editor.getSession().setValue(content);
editor.getSession().on('change', function(){
textarea.val(editor.getSession().getValue());
});
}
editor.commands.bindKey("ctrl+alt+0", null);
var session = editor.getSession();
session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
var file_id = $(element).data('id');
}
)};
if ($('#editor-edit').isPresent()) {
configureEditors();
initializeEditors();
$('.frame').show();
}
});

View File

@ -1,13 +1,68 @@
$(function() { $(function() {
// ruby part adds the relative_url_root, if it is set.
var 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/';
var THEME = 'ace/theme/textmate';
var TAB_KEY_CODE = 9; var TAB_KEY_CODE = 9;
var execution_environments; var execution_environments;
var file_types; var file_types;
var configureEditors = function() {
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
};
var initializeEditor = function(index, element) {
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 + ']');
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(THEME);
var textarea = $('textarea[id="exercise_files_attributes_'+index+'_content"]');
var content = textarea.val();
if (content != undefined)
{
editor.getSession().setValue(content);
editor.getSession().on('change', function(){
textarea.val(editor.getSession().getValue());
});
}
editor.commands.bindKey("ctrl+alt+0", null);
var session = editor.getSession();
session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
}
var initializeEditors = function() {
// initialize ace editors for all code textareas in the dom except the last one. The last one is the dummy area for new files, which is cloned when needed.
// this one must NOT! be initialized.
$('.editor:not(:last)').each(initializeEditor)
};
var addFileForm = function(event) { var addFileForm = function(event) {
event.preventDefault(); event.preventDefault();
var element = $('#dummies').children().first().clone(); var element = $('#dummies').children().first().clone();
var html = $('<div>').append(element).html().replace(/index/g, new Date().getTime());
// the timestamp is used here, since it is most probably unique. This is strange, but was originally designed that way.
var latestTextAreaIndex = new Date().getTime();
var html = $('<div>').append(element).html().replace(/index/g, latestTextAreaIndex);
$('#files').append(html); $('#files').append(html);
$('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id); $('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id);
$('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS); $('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS);
@ -15,6 +70,10 @@ $(function() {
// if we collapse the file forms by default, we need to click on the new element in order to open it. // if we collapse the file forms by default, we need to click on the new element in order to open it.
// however, this crashes for more files (if we add several ones by clicking the add button more often), since the elements are probably not correctly added to the files list. // however, this crashes for more files (if we add several ones by clicking the add button more often), since the elements are probably not correctly added to the files list.
//$('#files li:last>div:first>a>div').click(); //$('#files li:last>div:first>a>div').click();
// initialize the ace editor for the new textarea.
// pass the correct index and the last ace editor under the node files. this is the last one, since we just added it.
initializeEditor(latestTextAreaIndex, $('#files .editor').last()[0]);
}; };
var ajaxError = function() { var ajaxError = function() {
@ -152,8 +211,9 @@ $(function() {
}; };
var updateFileTemplates = function(fileType) { var updateFileTemplates = function(fileType) {
var rel_url_root = '<%= (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 : "" %>';
var jqxhr = $.ajax({ var jqxhr = $.ajax({
url: '/file_templates/by_file_type/' + fileType + '.json', url: rel_url_root + '/file_templates/by_file_type/' + fileType + '.json',
dataType: 'json' dataType: 'json'
}); });
jqxhr.done(function(response) { jqxhr.done(function(response) {
@ -192,4 +252,13 @@ $(function() {
highlightCode(); highlightCode();
} }
} }
if ($('#editor-edit').isPresent()) {
configureEditors();
initializeEditors();
$('.frame').show();
}
}); });

View File

@ -7,9 +7,18 @@ button i.fa-spin {
width: 100%; width: 100%;
} }
/* this class is used for the edit view of an exercise. It needs the height set, as it does not automatically resize */
.edit-frame {
height: 400px;
audio, img, video {
max-width: 100%;
}
}
.frame { .frame {
display: none; display: none;
height: 400px;
audio, img, video { audio, img, video {
max-width: 100%; max-width: 100%;
@ -18,6 +27,7 @@ button i.fa-spin {
.score { .score {
display: none; display: none;
vertical-align: bottom;
} }
#alert, #development-environment { #alert, #development-environment {
@ -26,7 +36,6 @@ button i.fa-spin {
#dummy { #dummy {
display: none; display: none;
width: 100% !important;
} }
#editor-buttons { #editor-buttons {
@ -64,6 +73,7 @@ button i.fa-spin {
#outputInformation { #outputInformation {
#output { #output {
max-height: 500px; max-height: 500px;
width: 100%;
overflow: auto; overflow: auto;
margin: 2em 0; margin: 2em 0;
@ -89,9 +99,96 @@ button i.fa-spin {
font-size: 0.8em; font-size: 0.8em;
} }
.requestCommentsButton { #turtlecanvas{
border-style:solid;
border-width:thin;
display: block;
margin: auto;
}
/* .requestCommentsButton {
position: relative; position: relative;
margin-top: -50px; margin-top: -50px;
margin-right: 25px; margin-right: 25px;
float: right; float: right;
} */
.sidebar-col-collapsed {
-webkit-transition: width 2s;
transition: width 2s;
width:67px;
float:left;
min-height: 1px;
padding-left: 15px;
padding-right: 15px;
} }
.sidebar-col {
-webkit-transition: width 2s;
transition: width 2s;
width:20%;
float:left;
min-height: 1px;
padding-left: 15px;
padding-right: 15px;
}
.editor-col {
min-height: 1px;
width:auto;
height:100%;
overflow:hidden;
}
.output-col {
-webkit-transition: width 2s;
transition: width 2s;
width:40%;
float:right;
min-height: 1px;
padding-left: 15px;
padding-right: 15px;
box-sizing: border-box
}
.output-col-collapsed {
-webkit-transition: width 2s;
transition: width 2s;
width:67px;
float:right;
min-height: 1px;
padding-left: 15px;
padding-right: 15px;
box-sizing: border-box
}
.enforce-top-margin {
margin-top: 5px !important;
}
.enforce-right-margin {
margin-right: 10px !important;
}
.description-panel-collapsed {
-webkit-transition: width 2s;
transition: width 2s;
height: 0px;
visibility: hidden;
}
.description-panel {
height: auto;
-webkit-transition: height 2s;
transition: height 2s;
visibility: visible;
}
.enforce-big-top-margin {
margin-top: 15px !important;
}
.enforce-bottom-margin {
margin-bottom: 5px !important;
}

View File

@ -118,7 +118,7 @@ class ExercisesController < ApplicationController
private :user_by_code_harbor_token private :user_by_code_harbor_token
def exercise_params def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
private :exercise_params private :exercise_params
@ -247,23 +247,34 @@ class ExercisesController < ApplicationController
end end
def redirect_after_submit def redirect_after_submit
Rails.logger.debug('Score ' + @submission.normalized_score.to_s) Rails.logger.debug('Redirecting user with score:s ' + @submission.normalized_score.to_s)
if @submission.normalized_score == 1.0 if @submission.normalized_score == 1.0
# if user has an own rfc, redirect to it and message him to clean up and accept the answer. # if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external,
# otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.)
if current_user.respond_to? :external_id
if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first
# set a message that informs the user that his own RFC should be closed.
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc')
flash.keep(:notice)
# else: show open rfc for same exercise respond_to do |format|
if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).order("RANDOM()").first format.html { redirect_to(rfc) }
format.json { render(json: {redirect: url_for(rfc)}) }
end
return
# set a message that informs the user that his score was perfect and help in RFC is greatly appreciated. # else: show open rfc for same exercise if available
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc') elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).order("RANDOM()").first
flash.keep(:notice) # set a message that informs the user that his score was perfect and help in RFC is greatly appreciated.
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc')
flash.keep(:notice)
respond_to do |format| respond_to do |format|
format.html { redirect_to(rfc) } format.html { redirect_to(rfc) }
format.json { render(json: {redirect: url_for(rfc)}) } format.json { render(json: {redirect: url_for(rfc)}) }
end
return
end end
return
end end
end end
redirect_to_lti_return_path redirect_to_lti_return_path

View File

@ -82,6 +82,7 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params def request_for_comment_params
# we are using the current_user.id here, since internal users are not able to create comments. The external_user.id is a primary key and does not require the consumer_id to be unique.
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name) params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
end end

View File

@ -129,7 +129,7 @@ class SubmissionsController < ApplicationController
socket.on :message do |event| socket.on :message do |event|
Rails.logger.info( Time.now.getutc.to_s + ": Docker sending: " + event.data) Rails.logger.info( Time.now.getutc.to_s + ": Docker sending: " + event.data)
handle_message(event.data, tubesock) handle_message(event.data, tubesock, result[:container])
end end
socket.on :close do |event| socket.on :close do |event|
@ -139,12 +139,12 @@ class SubmissionsController < ApplicationController
tubesock.onmessage do |data| tubesock.onmessage do |data|
Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data) Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data)
# Check whether the client send a JSON command and kill container # Check whether the client send a JSON command and kill container
# if the command is 'exit', send it to docker otherwise. # if the command is 'client_exit', send it to docker otherwise.
begin begin
parsed = JSON.parse(data) parsed = JSON.parse(data)
if parsed['cmd'] == 'exit' if parsed['cmd'] == 'client_kill'
Rails.logger.debug("Client exited container.") Rails.logger.debug("Client exited container.")
@docker_client.exit_container(result[:container]) @docker_client.kill_container(result[:container])
else else
socket.send data socket.send data
Rails.logger.debug('Sent the received client data to docker:' + data) Rails.logger.debug('Sent the received client data to docker:' + data)
@ -171,10 +171,11 @@ class SubmissionsController < ApplicationController
tubesock.close tubesock.close
end end
def handle_message(message, tubesock) def handle_message(message, tubesock, container)
# Handle special commands first # Handle special commands first
if (/^exit/.match(message)) if (/^exit/.match(message))
kill_socket(tubesock) kill_socket(tubesock)
@docker_client.exit_container(container)
else else
# Filter out information about run_command, test_command, user or working directory # Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
@ -231,7 +232,13 @@ class SubmissionsController < ApplicationController
hijack do |tubesock| hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
# tubesock is the socket to the client # tubesock is the socket to the client
tubesock.send_data JSON.dump(score_submission(@submission))
# the score_submission call will end up calling docker exec, which is blocking.
# to ensure responsiveness, we therefore open a thread here.
Thread.new {
tubesock.send_data JSON.dump(score_submission(@submission))
tubesock.send_data JSON.dump({'cmd' => 'exit'})
}
end end
end end
@ -291,6 +298,7 @@ class SubmissionsController < ApplicationController
# tubesock is the socket to the client # tubesock is the socket to the client
tubesock.send_data JSON.dump(output) tubesock.send_data JSON.dump(output)
tubesock.send_data JSON.dump('cmd' => 'exit')
end end
end end

View File

@ -2,5 +2,6 @@
= form.label(attribute, label) = form.label(attribute, label)
| &nbsp; | &nbsp;
a.toggle-input data={text_initial: t('shared.upload_file'), text_toggled: t('shared.back')} href='#' = t('shared.upload_file') a.toggle-input data={text_initial: t('shared.upload_file'), text_toggled: t('shared.back')} href='#' = t('shared.upload_file')
= form.text_area(attribute, class: 'code-field form-control original-input', rows: 16, style: "display:none;") = form.text_area(attribute, class: 'code-field form-control', rows: 16, style: "display:none;")
= form.file_field(attribute, class: 'alternative-input form-control', disabled: true) = form.file_field(attribute, class: 'alternative-input form-control', disabled: true)
= render partial: 'editor_edit', locals: { exercise: @exercise }

View File

@ -1,41 +1,24 @@
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
div class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') = render('editor_file_tree', files: @files) #editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_id
div id='frames' class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9') div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output')
div id='frames' class='editor-col'
#editor-buttons.btn-group.enforce-bottom-margin
// = render('editor_button', data: {:'data-message-success' => t('submissions.create.success'), :'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-save', id: 'save', label: t('exercises.editor.save'), title: t('.tooltips.save'))
// .btn-group
= render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy'))
= render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render'))
= render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-network' => t('exercises.editor.network'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))
= render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip'))
- @files.each do |file| - @files.each do |file|
= render('editor_frame', exercise: exercise, file: file) = render('editor_frame', exercise: exercise, file: file)
#autosave-label #autosave-label
= t('exercises.editor.lastsaved') = t('exercises.editor.lastsaved')
span span
#editor-buttons.btn-group
= render('editor_button', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
// = render('editor_button', data: {:'data-message-success' => t('submissions.create.success'), :'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-save', id: 'save', label: t('exercises.editor.save'), title: t('.tooltips.save'))
button style="display:none" id="autosave" button style="display:none" id="autosave"
.btn-group
= render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy'))
= render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render'))
= render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-network' => t('exercises.editor.network'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t'))
button.btn.btn-primary.dropdown-toggle data-toggle='dropdown' type='button'
span.caret
span.sr-only Toggle Dropdown
ul.dropdown-menu role='menu'
li
a#dropdown-render data-cause='render' href='#'
i.fa.fa-desktop
= t('exercises.editor.render')
li
a#dropdown-run data-cause='run' href='#'
i.fa.fa-play
= t('exercises.editor.run')
li
a#dropdown-stop href='#'
i.fa.fa-stop
= t('exercises.editor.stop')
li
a#dropdown-test data-cause='test' href='#'
i.fa.fa-rocket
= t('exercises.editor.test')
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')

View File

@ -1,4 +1,4 @@
button.btn class=local_assigns.fetch(:classes, 'btn-primary') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button' button.btn class=local_assigns.fetch(:classes, 'btn-primary btn-sm') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button'
i.fa.fa-circle-o-notch.fa-spin i.fa.fa-circle-o-notch.fa-spin
i class=icon i class=icon
= label = label

View File

@ -1,5 +1,5 @@
#editor-edit.panel-group.row data-exercise-id=@exercise.id #editor-edit.panel-group.row.original-input data-exercise-id=@exercise.id
#frames #frames
.frame .edit-frame
.editor-content.hidden .editor-content.hidden
.editor .editor

View File

@ -1,10 +1,28 @@
#files data-entries=FileTree.new(files).to_js_tree div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden')
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar'))
hr - if @exercise.allow_file_creation and not @exercise.hide_file_tree?
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-cause' => 'file', :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus', id: 'create-file-collapsed', label:'', title: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over'))
div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '')
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar'))
div class=(@exercise.hide_file_tree ? 'hidden' : '')
hr
#files data-entries=FileTree.new(files).to_js_tree
hr
- if @exercise.allow_file_creation and not @exercise.hide_file_tree?
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
- if @exercise.allow_file_creation? - if @exercise.allow_file_creation?
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file')) = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file'))
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))

View File

@ -12,8 +12,4 @@
= link_to(file.native_file.file.name_with_extension, file.native_file.url) = link_to(file.native_file.file.name_with_extension, file.native_file.url)
- else - else
.editor-content.hidden data-file-id=file.ancestor_id = file.content .editor-content.hidden data-file-id=file.ancestor_id = file.content
.editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-id=file.id .editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id
button.btn.btn-primary.requestCommentsButton type='button' id="requestComments"
i.fa.fa-comment
= t('exercises.editor.requestComments')

View File

@ -0,0 +1,55 @@
div id='output_sidebar_collapsed'
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '')
div id='output_sidebar_uncollapsed' class='hidden col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output')
.row
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar'))
div.enforce-big-top-margin.hidden id='score_div'
#results
h2 = t('exercises.implement.results')
p.test-count == t('exercises.implement.test_count', count: 0)
ul.list-unstyled
ul#dummies.hidden.list-unstyled
li.panel.panel-default
.panel-heading
h3.panel-title == t('exercises.implement.file', filename: '', number: 0)
.panel-body
= row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: 'exercises.implement.feedback')
= row(label: 'exercises.implement.error_messages')
= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#'))
#score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score)
h4
span == "#{t('activerecord.attributes.submission.score')}:&nbsp;"
span.score
.progress
.progress-bar role='progressbar'
br
- if session[:lti_parameters].try(:has_key?, 'lis_outcome_service_url')
p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit'))
- else
p.text-center = render('editor_button', classes: 'btn-lg btn-warning-outline', data: {:'data-placement' => 'bottom', :'data-tooltip' => true}, icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed'))
hr
div.enforce-big-top-margin
#turtlediv
canvas#turtlecanvas.hidden width=400 height=400
div.enforce-big-top-margin
#hint
.panel.panel-warning
.panel-heading = t('exercises.implement.hint')
.panel-body
div.enforce-big-top-margin
#prompt.input-group.hidden
span.input-group-addon data-prompt=t('exercises.editor.input') = t('exercises.editor.input')
input#prompt-input.form-control type='text'
span.input-group-btn
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
#output
pre = t('exercises.implement.no_output_yet')
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
#flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
.panel-heading = 'Gain more insights here'
.panel-body

View File

@ -37,5 +37,4 @@ li.panel.panel-default
.form-group .form-group
= f.label(:role, t('activerecord.attributes.file.weight')) = f.label(:role, t('activerecord.attributes.file.weight'))
= f.number_field(:weight, class: 'form-control', min: 1, step: 'any') = f.number_field(:weight, class: 'form-control', min: 1, step: 'any')
= render('code_field', attribute: :content, form: f, label: t('activerecord.attributes.file.content')) = render('code_field', attribute: :content, form: f, label: t('activerecord.attributes.file.content'))
= render partial: 'editor_edit', locals: { exercise: @exercise }

View File

@ -28,6 +28,10 @@
label label
= f.check_box(:allow_file_creation) = f.check_box(:allow_file_creation)
= t('activerecord.attributes.exercise.allow_file_creation') = t('activerecord.attributes.exercise.allow_file_creation')
.checkbox
label
= f.check_box(:allow_auto_completion)
= t('activerecord.attributes.exercise.allow_auto_completion')
h2 = t('activerecord.attributes.exercise.files') h2 = t('activerecord.attributes.exercise.files')
ul#files.list-unstyled.panel-group ul#files.list-unstyled.panel-group
= f.fields_for :files do |files_form| = f.fields_for :files do |files_form|

View File

@ -1,85 +1,22 @@
.row .row
#editor-column.col-md-10.col-md-offset-1 #editor-column.col-md-12
h1 = @exercise div
span.badge.pull-right.score
span.badge.pull-right.score h1 id="exercise-headline"
i class="fa fa-chevron-down" id="description-symbol"
= @exercise
p.lead = render_markdown(@exercise.description) #description-panel.lead.description-panel
= render_markdown(@exercise.description)
#alert.alert.alert-danger role='alert' #alert.alert.alert-danger role='alert'
h4 = t('.alert.title') h4 = t('.alert.title')
p = t('.alert.text', application_name: application_name) p = t('.alert.text', application_name: application_name)
#development-environment #development-environment
ul.nav.nav-justified.nav-tabs role='tablist'
li.active
a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1')
i.fa.fa-code
= t('.workspace')
li
a data-placement='top' data-toggle='tab' data-tooltip=true href='#outputInformation' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2')
i.fa.fa-terminal
= t('.output')
li
a data-placement='top' data-toggle='tab' data-tooltip=true href='#progress' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 3')
i.fa.fa-line-chart
= t('.progress')
hr
.tab-content .tab-content
#workspace.tab-pane.active = render('editor', exercise: @exercise, files: @files, submission: @submission) #workspace.tab-pane.active = render('editor', exercise: @exercise, files: @files, submission: @submission)
#outputInformation.tab-pane data-message-no-output=t('.no_output')
#hint
.panel.panel-warning
.panel-heading = t('.hint')
.panel-body
.row
/ #output-col1.col-sm-12
#output-col1
// todo set to full width if turtle isnt used
#prompt.input-group.hidden.col-lg-7.col-md-7.two-column
span.input-group-addon data-prompt=t('exercises.editor.input') = t('exercises.editor.input')
input#prompt-input.form-control type='text'
span.input-group-btn
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
#output
pre = t('.no_output_yet')
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
#flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
.panel-heading = 'Gain more insights here'
.panel-body
#output-col2.col-lg-5.col-md-5
#turtlediv
// todo what should the canvas default size be?
canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin'
#progress.tab-pane
#results
h2 = t('.results')
p.test-count == t('.test_count', count: 0)
ul.list-unstyled
ul#dummies.hidden.list-unstyled
li.panel.panel-default
.panel-heading
h3.panel-title == t('.file', filename: '', number: 0)
.panel-body
= row(label: '.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe)
= row(label: '.feedback')
= row(label: '.error_messages')
= row(label: '.output', value: link_to(t('shared.show'), '#'))
#score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score)
h4
span == "#{t('activerecord.attributes.submission.score')}:&nbsp;"
span.score
.progress
.progress-bar role='progressbar'
br
- if session[:lti_parameters].try(:has_key?, 'lis_outcome_service_url')
p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit'))
- else
p.text-center = render('editor_button', classes: 'btn-lg btn-warning-outline', data: {:'data-placement' => 'bottom', :'data-tooltip' => true} , icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed'))
- if qa_url - if qa_url
#questions-column #questions-column

View File

@ -16,6 +16,7 @@ h1
= row(label: 'exercise.public', value: @exercise.public?) = row(label: 'exercise.public', value: @exercise.public?)
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) = row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
= row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?) = row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?)
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
= row(label: 'exercise.embedding_parameters') do = row(label: 'exercise.embedding_parameters') do
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))

View File

@ -43,3 +43,5 @@ module CodeOcean
end end
end end
end end
Rails.application.config.assets.precompile += %w( markdown-buttons.png )

View File

@ -3,7 +3,7 @@ set :config_example_suffix, '.example'
set :default_env, 'PATH' => '/usr/java/jdk1.8.0_40/bin:$PATH' set :default_env, 'PATH' => '/usr/java/jdk1.8.0_40/bin:$PATH'
set :deploy_to, '/var/www/app' set :deploy_to, '/var/www/app'
set :keep_releases, 3 set :keep_releases, 3
set :linked_dirs, %w(bin log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets) set :linked_dirs, %w(log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets)
set :linked_files, %w(config/action_mailer.yml config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml) set :linked_files, %w(config/action_mailer.yml config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml)
set :log_level, :info set :log_level, :info
set :puma_threads, [0, 16] set :puma_threads, [0, 16]

View File

@ -7,8 +7,8 @@ default: &default
development: development:
<<: *default <<: *default
host: tcp://192.168.59.104:2376 host: tcp://127.0.0.1:2376
ws_host: ws://192.168.59.104:2376 #url to connect rails server to docker host ws_host: ws://127.0.0.1:2376 #url to connect rails server to docker host
ws_client_protocol: ws:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) ws_client_protocol: ws:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
pool: pool:

View File

@ -0,0 +1,40 @@
#Why erb?
default: &default
connection_timeout: 3
pool:
active: false
ports: !ruby/range 4500..4600
development:
<<: *default
host: tcp://127.0.0.1:2376
ws_host: ws://127.0.0.1:2376 #url to connect rails server to docker host
ws_client_protocol: ws:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
pool:
active: true
refill:
async: false
batch_size: 8
interval: 15
timeout: 60
#workspace_root: <%= File.join('/', 'shared', Rails.env) %>
production:
<<: *default
host: unix:///var/run/docker.sock
pool:
active: true
refill:
async: false
batch_size: 8
interval: 15
timeout: 60
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
ws_host: ws://localhost:4243 #url to connect rails server to docker host
ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
test:
<<: *default
host: tcp://192.168.59.104:2376
workspace_root: <%= File.join('/', 'shared', Rails.env) %>

View File

@ -23,7 +23,7 @@ Rails.application.configure do
config.serve_static_assets = false config.serve_static_assets = false
# Compress JavaScripts and CSS. # Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier # config.assets.js_compressor = :uglifier
# config.assets.css_compressor = :sass # config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed. # Do not fallback to assets pipeline if a precompiled asset is missed.

View File

@ -36,6 +36,7 @@ de:
public: Öffentlich public: Öffentlich
title: Titel title: Titel
user: Autor user: Autor
allow_auto_completion: "Autovervollständigung aktivieren"
allow_file_creation: "Dateierstellung erlauben" allow_file_creation: "Dateierstellung erlauben"
external_user: external_user:
consumer: Konsument consumer: Konsument
@ -188,6 +189,8 @@ de:
worktime: Durchschnittliche Arbeitszeit worktime: Durchschnittliche Arbeitszeit
exercises: exercises:
editor: editor:
collapse_action_sidebar: Aktions-Leiste Einklappen
collapse_output_sidebar: Ausgabe-Leiste Einklappen
confirm_start_over: Wollen Sie wirklich von vorne anfangen? confirm_start_over: Wollen Sie wirklich von vorne anfangen?
confirm_submit: Wollen Sie Ihren Code wirklich zur Bewertung abgeben? confirm_submit: Wollen Sie Ihren Code wirklich zur Bewertung abgeben?
create_file: Neue Datei create_file: Neue Datei
@ -195,6 +198,8 @@ de:
destroy_file: Datei löschen destroy_file: Datei löschen
download: Herunterladen download: Herunterladen
dummy: Keine Aktion dummy: Keine Aktion
expand_action_sidebar: Aktions-Leiste Ausklappen
expand_output_sidebar: Ausgabe-Leiste Ausklappen
input: Ihre Eingabe input: Ihre Eingabe
lastsaved: 'Zuletzt gespeichert: ' lastsaved: 'Zuletzt gespeichert: '
network: 'Während Ihr Code läuft, ist Port %{port} unter folgender Adresse erreichbar: <a href="%{address}" target="_blank">%{address}</a>.' network: 'Während Ihr Code läuft, ist Port %{port} unter folgender Adresse erreichbar: <a href="%{address}" target="_blank">%{address}</a>.'
@ -203,6 +208,7 @@ de:
run_failure: Ihr Code konnte nicht auf der Plattform ausgeführt werden. run_failure: Ihr Code konnte nicht auf der Plattform ausgeführt werden.
run_success: Ihr Code wurde auf der Plattform ausgeführt. run_success: Ihr Code wurde auf der Plattform ausgeführt.
requestComments: Kommentare erbitten requestComments: Kommentare erbitten
requestCommentsTooltip: Falls Sie Hilfe mit Ihrem Code benötigen, können Sie hier Kommentare erbitten
save: Speichern save: Speichern
score: Bewerten score: Bewerten
send: Senden send: Senden
@ -274,6 +280,7 @@ de:
submit: submit:
failure: Beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut. failure: Beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.
full_score_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über ihre Hilfe und Kommentare freuen. full_score_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über ihre Hilfe und Kommentare freuen.
full_score_redirect_to_own_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ihre Frage ist damit wahrscheinlich gelöst? Falls ja, fügen Sie doch den entscheidenden Kniff als Antwort hinzu und markieren die Frage als gelöst, bevor sie das Fenster schließen.
external_users: external_users:
statistics: statistics:
no_data_available: Keine Daten verfügbar. no_data_available: Keine Daten verfügbar.

View File

@ -36,6 +36,7 @@ en:
public: Public public: Public
title: Title title: Title
user: Author user: Author
allow_auto_completion: "Allow auto completion"
allow_file_creation: "Allow file creation" allow_file_creation: "Allow file creation"
external_user: external_user:
consumer: Consumer consumer: Consumer
@ -188,6 +189,8 @@ en:
worktime: Average Working Time worktime: Average Working Time
exercises: exercises:
editor: editor:
collapse_action_sidebar: Collapse Action Sidebar
collapse_output_sidebar: Collapse Output Sidebar
confirm_start_over: Do you really want to start over? confirm_start_over: Do you really want to start over?
confirm_submit: Do you really want to submit your code for grading? confirm_submit: Do you really want to submit your code for grading?
create_file: New File create_file: New File
@ -195,6 +198,8 @@ en:
destroy_file: Delete File destroy_file: Delete File
download: Download download: Download
dummy: No Action dummy: No Action
expand_action_sidebar: Expand Action Sidebar
expand_output_sidebar: Expand Output Sidebar
input: Your input input: Your input
lastsaved: 'Last saved: ' lastsaved: 'Last saved: '
network: 'While your code is running, port %{port} is accessible using the following address: <a href="%{address}" target="_blank">%{address}</a>.' network: 'While your code is running, port %{port} is accessible using the following address: <a href="%{address}" target="_blank">%{address}</a>.'
@ -203,6 +208,7 @@ en:
run_failure: Your code could not be run. run_failure: Your code could not be run.
run_success: Your code was run on our servers. run_success: Your code was run on our servers.
requestComments: Request comments requestComments: Request comments
requestCommentsTooltip: If you need help with your code, you can now request comments here!
save: Save save: Save
score: Score score: Score
send: Send send: Send
@ -274,6 +280,7 @@ en:
submit: submit:
failure: An error occured while transmitting your score. Please try again later. failure: An error occured while transmitting your score. Please try again later.
full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated! full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated!
full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window!
external_users: external_users:
statistics: statistics:
no_data_available: No data available. no_data_available: No data available.

View File

@ -0,0 +1,5 @@
class AddAllowAutoCompletionToExercises < ActiveRecord::Migration
def change
add_column :exercises, :allow_auto_completion, :boolean, default: false
end
end

View File

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160704143402) do ActiveRecord::Schema.define(version: 20160907123009) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -74,7 +74,6 @@ ActiveRecord::Schema.define(version: 20160704143402) do
t.integer "file_type_id" t.integer "file_type_id"
t.integer "memory_limit" t.integer "memory_limit"
t.boolean "network_enabled" t.boolean "network_enabled"
end end
create_table "exercises", force: true do |t| create_table "exercises", force: true do |t|
@ -90,6 +89,7 @@ ActiveRecord::Schema.define(version: 20160704143402) do
t.string "token" t.string "token"
t.boolean "hide_file_tree" t.boolean "hide_file_tree"
t.boolean "allow_file_creation" t.boolean "allow_file_creation"
t.boolean "allow_auto_completion", default: false
end end
create_table "external_users", force: true do |t| create_table "external_users", force: true do |t|

View File

@ -72,9 +72,10 @@ class DockerClient
# Headers are required by Docker # Headers are required by Docker
headers = {'Origin' => 'http://localhost'} headers = {'Origin' => 'http://localhost'}
socket_url = DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params
socket = Faye::WebSocket::Client.new(socket_url, [], :headers => headers)
# rspec error: undefined method `+' for nil:NilClass. problem with ws_host? Rails.logger.debug "Opening Websocket on URL " + socket_url
socket = Faye::WebSocket::Client.new(DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params, [], :headers => headers)
socket.on :error do |event| socket.on :error do |event|
Rails.logger.info "Websocket error: " + event.message Rails.logger.info "Websocket error: " + event.message
@ -263,12 +264,16 @@ class DockerClient
end end
end end
def exit_container(container) def exit_thread_if_alive
Rails.logger.debug('exiting container ' + container.to_s)
# exit the timeout thread if it is still alive
if(@thread && @thread.alive?) if(@thread && @thread.alive?)
@thread.exit @thread.exit
end end
end
def exit_container(container)
Rails.logger.debug('exiting container ' + container.to_s)
# exit the timeout thread if it is still alive
exit_thread_if_alive
# if we use pooling and recylce the containers, put it back. otherwise, destroy it. # if we use pooling and recylce the containers, put it back. otherwise, destroy it.
(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container) (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
end end
@ -288,6 +293,7 @@ class DockerClient
container = self.class.create_container(@execution_environment) container = self.class.create_container(@execution_environment)
DockerContainerPool.add_to_all_containers(container, @execution_environment) DockerContainerPool.add_to_all_containers(container, @execution_environment)
end end
exit_thread_if_alive
end end
def execute_run_command(submission, filename, &block) def execute_run_command(submission, filename, &block)
@ -297,13 +303,6 @@ class DockerClient
command = submission.execution_environment.run_command % command_substitutions(filename) command = submission.execution_environment.run_command % command_substitutions(filename)
create_workspace_files = proc { create_workspace_files(container, submission) } create_workspace_files = proc { create_workspace_files(container, submission) }
open_websocket_connection(command, create_workspace_files, block) open_websocket_connection(command, create_workspace_files, block)
# to pass the test "it executes the run command" it needs to send a command, not sure if it should be implemented.
if container
container.status = :executing
send_command(command, container, &block)
end
# actual run command is run in the submissions controller, after all listeners are attached. # actual run command is run in the submissions controller, after all listeners are attached.
end end
@ -329,7 +328,6 @@ class DockerClient
Docker::Image.all.map { |image| image.info['RepoTags'] }.flatten.reject { |tag| tag.include?('<none>') } Docker::Image.all.map { |image| image.info['RepoTags'] }.flatten.reject { |tag| tag.include?('<none>') }
end end
# When @image commented test doesn't work -> test set to pending
def initialize(options = {}) def initialize(options = {})
@execution_environment = options[:execution_environment] @execution_environment = options[:execution_environment]
# todo: eventually re-enable this if it is cached. But in the end, we do not need this. # todo: eventually re-enable this if it is cached. But in the end, we do not need this.