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:
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
@ -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
|
|
||||||
|
@ -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
16
app/assets/javascripts/editor/ajax.js.erb
Normal file
16
app/assets/javascripts/editor/ajax.js.erb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
CodeOceanEditorAJAX = {
|
||||||
|
ajax: function(options) {
|
||||||
|
return $.ajax(_.extend({
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'POST',
|
||||||
|
}, options));
|
||||||
|
},
|
||||||
|
|
||||||
|
ajaxError: function(response) {
|
||||||
|
var message = ((response || {}).responseJSON || {}).message || '';
|
||||||
|
|
||||||
|
$.flash.danger({
|
||||||
|
text: message.length > 0 ? message : $('#flash').data('message-failure')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
24
app/assets/javascripts/editor/codepilot.js.erb
Normal file
24
app/assets/javascripts/editor/codepilot.js.erb
Normal 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': ''};
|
||||||
|
}
|
||||||
|
};
|
574
app/assets/javascripts/editor/editor.js.erb
Normal file
574
app/assets/javascripts/editor/editor.js.erb
Normal 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));
|
||||||
|
}
|
||||||
|
};
|
167
app/assets/javascripts/editor/evaluation.js.erb
Normal file
167
app/assets/javascripts/editor/evaluation.js.erb
Normal 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
50
app/assets/javascripts/editor/execution.js.erb
Normal file
50
app/assets/javascripts/editor/execution.js.erb
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
35
app/assets/javascripts/editor/flowr.js.erb
Normal file
35
app/assets/javascripts/editor/flowr.js.erb
Normal 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 = '';
|
||||||
|
}
|
||||||
|
};
|
93
app/assets/javascripts/editor/participantsupport.js.erb
Normal file
93
app/assets/javascripts/editor/participantsupport.js.erb
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
45
app/assets/javascripts/editor/prompt.js.erb
Normal file
45
app/assets/javascripts/editor/prompt.js.erb
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
211
app/assets/javascripts/editor/submissions.js.erb
Normal file
211
app/assets/javascripts/editor/submissions.js.erb
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
48
app/assets/javascripts/editor/turtle.js.erb
Normal file
48
app/assets/javascripts/editor/turtle.js.erb
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
112
app/assets/javascripts/editor/websocket.js.erb
Normal file
112
app/assets/javascripts/editor/websocket.js.erb
Normal 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();
|
||||||
|
};
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
0
app/assets/javascripts/editor_edit.js.erb
Normal file
0
app/assets/javascripts/editor_edit.js.erb
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
@ -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;
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
= form.label(attribute, label)
|
= form.label(attribute, label)
|
||||||
|
|
|
|
||||||
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 }
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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'))
|
|
@ -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')
|
|
55
app/views/exercises/_editor_output.html.slim
Normal file
55
app/views/exercises/_editor_output.html.slim
Normal 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')}: "
|
||||||
|
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
|
@ -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 }
|
|
@ -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|
|
||||||
|
@ -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')}: "
|
|
||||||
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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -43,3 +43,5 @@ module CodeOcean
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Rails.application.config.assets.precompile += %w( markdown-buttons.png )
|
@ -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]
|
||||||
|
@ -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:
|
||||||
|
40
config/docker.yml.erb.example
Normal file
40
config/docker.yml.erb.example
Normal 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) %>
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddAllowAutoCompletionToExercises < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :exercises, :allow_auto_completion, :boolean, default: false
|
||||||
|
end
|
||||||
|
end
|
@ -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|
|
||||||
|
@ -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.
|
||||||
|
Reference in New Issue
Block a user