Merge pull request #81 from openHPI/editor-frontend-refactor
Editor frontend refactor
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
|
||||||
|
@ -417,6 +417,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
|
||||||
|
|
||||||
|
@ -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 )
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,7 +72,10 @@ class DockerClient
|
|||||||
# Headers are required by Docker
|
# Headers are required by Docker
|
||||||
headers = {'Origin' => 'http://localhost'}
|
headers = {'Origin' => 'http://localhost'}
|
||||||
|
|
||||||
socket = Faye::WebSocket::Client.new(DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params, [], :headers => headers)
|
socket_url = DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params
|
||||||
|
socket = Faye::WebSocket::Client.new(socket_url, [], :headers => headers)
|
||||||
|
|
||||||
|
Rails.logger.debug "Opening Websocket on URL " + socket_url
|
||||||
|
|
||||||
socket.on :error do |event|
|
socket.on :error do |event|
|
||||||
Rails.logger.info "Websocket error: " + event.message
|
Rails.logger.info "Websocket error: " + event.message
|
||||||
@ -261,16 +264,21 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def kill_container(container)
|
def kill_container(container)
|
||||||
Rails.logger.info('killing container ' + container.to_s)
|
Rails.logger.info('killing container ' + container.to_s)
|
||||||
# remove container from pool, then destroy it
|
# remove container from pool, then destroy it
|
||||||
@ -286,6 +294,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)
|
||||||
|
Reference in New Issue
Block a user