diff --git a/.gitignore b/.gitignore index 4a862a70..46060552 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /config/secrets.yml /config/sendmail.yml /config/smtp.yml +/config/docker.yml.erb /config/*.production.yml /config/*.staging.yml /coverage diff --git a/Gemfile.lock b/Gemfile.lock index 18067c27..ced57d7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -417,6 +417,3 @@ DEPENDENCIES uglifier (>= 1.3.0) web-console (~> 2.0) will_paginate (~> 3.0) - -BUNDLED WITH - 1.12.4 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ebdcc08d..ccf8ce15 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -24,4 +24,5 @@ //= require bootstrap_pagedown //= require markdown.converter //= require markdown.sanitizer -//= require markdown.editor \ No newline at end of file +//= require markdown.editor +//= require ../../../vendor/assets/javascripts/ace/ext-language_tools \ No newline at end of file diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index b2f69fcf..f402e982 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -1,1198 +1,27 @@ $(function() { - var ACE_FILES_PATH = '/assets/ace/'; - var ADEQUATE_PERCENTAGE = 50; - var ALT_1_KEY_CODE = 161; - var ALT_2_KEY_CODE = 8220; - var ALT_3_KEY_CODE = 182; - var ALT_4_KEY_CODE = 162; - var ALT_R_KEY_CODE = 174; - var ALT_S_KEY_CODE = 8218; - var ALT_T_KEY_CODE = 8224; - var FILENAME_URL_PLACEHOLDER = '{filename}'; - var SUCCESSFULL_PERCENTAGE = 90; - var THEME = 'ace/theme/textmate'; - var REMEMBER_TAB = false; - var AUTOSAVE_INTERVAL = 15 * 1000; - var REQUEST_FOR_COMMENTS_DELAY = 3 * 60 * 1000; - var editors = []; - var editor_for_file = new Map(); - var regex_for_language = new Map(); - var tracepositions_regex; - var resetTurtle = true; + //Merge all editor components. OOP for the win. O rly. + //TODO Change this. Otherwise it will fuck people up, + //because it's really confusing if the variables and the code are + //split over 6 files. + $.extend( + CodeOceanEditor, + CodeOceanEditorAJAX, + CodeOceanEditorEvaluation, + CodeOceanEditorFlowr, + CodeOceanEditorSubmissions, + CodeOceanEditorTurtle, + CodeOceanEditorWebsocket, + CodeOceanEditorPrompt, + CodeOceanEditorCodePilot, + CodeOceanEditorRequestForComments + ); - var active_file = undefined; - var active_frame = undefined; - var running = false; - var qa_api = undefined; - var output_mode_is_streaming = true; - - var websocket, - turtlescreen, - numMessages = 0, - turtlecanvas = $('#turtlecanvas'), - prompt = $('#prompt'), - commands = ['input', 'write', 'turtle', 'turtlebatch', 'render', 'exit', 'timeout', 'status'], - streams = ['stdin', 'stdout', 'stderr']; - - var ENTER_KEY_CODE = 13; - - var flowrOutputBuffer = ""; - var QaApiOutputBuffer = {'stdout': '', 'stderr': ''}; - var flowrResultHtml = '
' - - var ajax = function(options) { - return $.ajax(_.extend({ - dataType: 'json', - method: 'POST', - }, options)); - }; - - var ajaxError = function(response) { - var message = ((response || {}).responseJSON || {}).message || ''; - - $.flash.danger({ - text: message.length > 0 ? message : $('#flash').data('message-failure') - }); - }; - - var clearOutput = function() { - $('#output pre').remove(); - }; - - var collectFiles = function() { - var editable_editors = _.filter(editors, function(editor) { - return !editor.getReadOnly(); - }); - return _.map(editable_editors, function(editor) { - return { - content: editor.getValue(), - file_id: $(editor.container).data('file-id') - }; - }); - }; - - var configureEditors = function() { - _.each(['modePath', 'themePath', 'workerPath'], function(attribute) { - ace.config.set(attribute, ACE_FILES_PATH); - }); - }; - - var confirmDestroy = function(event) { - event.preventDefault(); - if (confirm($(this).data('message-confirm'))) { - destroyFile(); - } - }; - - var confirmReset = function(event) { - event.preventDefault(); - if (confirm($(this).data('message-confirm'))) { - resetCode(); - } - }; - - //var confirmSubmission = function(event) { - // event.preventDefault(); - // if (confirm($(this).data('message-confirm'))) { - // submitCode(); - // } - //}; - - var createSubmission = function(initiator, filter, callback) { - showSpinner(initiator); - var jqxhr = ajax({ - data: { - submission: { - cause: $(initiator).data('cause') || $(initiator).prop('id'), - exercise_id: $('#editor').data('exercise-id'), - files_attributes: (filter || _.identity)(collectFiles()) - }, - annotations_arr: [] - }, - dataType: 'json', - method: 'POST', - url: $(initiator).data('url') || $('#editor').data('submissions-url') - }); - jqxhr.always(hideSpinner); - jqxhr.done(createSubmissionCallback); - jqxhr.done(callback); - jqxhr.fail(ajaxError); - }; - - var 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 < editors.length; i++) { - - // set the data attribute to submission - //$(editors[i].container).data('context-type', 'Submission'); - - var file_id_old = $(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; - $(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 - toggleButtonStates(); - - }; - - var destroyFile = function() { - createSubmission($('#destroy-file'), function(files) { - return _.reject(files, function(file) { - return file.file_id === active_file.id; - }); - }, window.CodeOcean.refresh); - }; - - var downloadCode = function(event) { - event.preventDefault(); - createSubmission(this, null,function(response) { - var url = response.download_url; - - // to download just a single file, use the following url - //var url = response.download_file_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); - window.location = url; - }); - }; - - var evaluateCode = function(url, callback) { - initWebsocketConnection(url, callback); - }; - - var fileActionsAvailable = function() { - return isActiveFileRenderable() || isActiveFileRunnable() || isActiveFileStoppable() || isActiveFileTestable(); - }; - - var findOrCreateOutputElement = function(index) { - if ($('#output-' + index).isPresent()) { - return $('#output-' + index); + if ($('#editor').isPresent() && CodeOceanEditor) { + if (CodeOceanEditor.isBrowserSupported()) { + CodeOceanEditor.initializeEverything(); } else { - var element = $('
').attr('id', 'output-' + index);
-      $('#output').append(element);
-      return element;
+      $('#alert').show();
     }
-  };
-
-  var findOrCreateRenderElement = function(index) {
-    if ($('#render-' + index).isPresent()) {
-      return $('#render-' + index);
-    } else {
-      var element = $('
').attr('id', 'render-' + index); - $('#render').append(element); - return element; - } - }; - - var getPanelClass = function(result) { - if (result.stderr && !result.score) { - return 'panel-danger'; - } else if (result.score < 1) { - return 'panel-warning'; - } else { - return 'panel-success'; - } - }; - - var getProgressBarClass = function(percentage) { - if (percentage < ADEQUATE_PERCENTAGE) { - return 'progress-bar progress-bar-striped progress-bar-danger'; - } else if (percentage < SUCCESSFULL_PERCENTAGE) { - return 'progress-bar progress-bar-striped progress-bar-warning'; - } else { - return 'progress-bar progress-bar-striped progress-bar-success'; - } - }; - - var handleKeyPress = function(event) { - if (event.which === ALT_1_KEY_CODE) { - showWorkspaceTab(event); - } else if (event.which === ALT_2_KEY_CODE) { - showTab(1); - } else if (event.which === ALT_3_KEY_CODE) { - showTab(2); - } else if (event.which === ALT_R_KEY_CODE) { - $('#run').trigger('click'); - } else if (event.which === ALT_S_KEY_CODE) { - $('#assess').trigger('click'); - } else if (event.which === ALT_T_KEY_CODE) { - $('#test').trigger('click'); - } else { - return; - } - event.preventDefault(); - }; - - var lastCopyText; - var handleCopyEvent = function(text){ - lastCopyText = text; - }; - - var handlePasteEvent = function (pasteObject) { - //console.log("Handling paste event. this is ", this ); - //console.log("Text: " + pasteObject.text); - - var same = (lastCopyText === pasteObject.text); - //console.log("Text is the same: " + same); - - // if the text is not copied from within the editor (from any file), send an event to lanalytics - if(!same){ - publishCodeOceanEvent("codeocean_editor_paste", { - text: pasteObject.text, - exercise: $('#editor').data('exercise-id'), - file_id: "1" - - }); - } - }; - - var handleScoringResponse = function(websocket_event) { - results = JSON.parse(websocket_event.data); - printScoringResults(results); - var score = _.reduce(results, function(sum, result) { - return sum + result.score * result.weight; - }, 0).toFixed(2); - $('#score').data('score', score); - renderScore(); - showTab(2); - }; - - var handleQaApiOutput = function() { - if (qa_api) { - qa_api.executeCommand('syncOutput', [[QaApiOutputBuffer]]); - // reset the object - } - QaApiOutputBuffer = {'stdout': '', 'stderr': ''}; - } - - // activate flowr only for half of the audience - var isFlowrEnabled = true;//parseInt($('#editor').data('user-id'))%2 == 0; - var handleStderrOutputForFlowr = function() { - if (!isFlowrEnabled) return - - var flowrUrl = $('#flowrHint').data('url'); - var flowrHintBody = $('#flowrHint .panel-body'); - var queryParameters = { - query: flowrOutputBuffer - } - - flowrHintBody.empty(); - - jQuery.getJSON(flowrUrl, queryParameters, function(data) { - jQuery.each(data.queryResults, function(index, question) { - var collapsibleTileHtml = 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('Open this question'); - - flowrHintBody.append(resultTile); - }); - - if (data.queryResults.length !== 0) { - $('#flowrHint').fadeIn(); - } - }) - - flowrOutputBuffer = ''; - }; - - var handleTestResponse = function(websocket_event) { - result = JSON.parse(websocket_event.data); - clearOutput(); - printOutput(result, false, 0); - if (qa_api) { - qa_api.executeCommand('syncOutput', [result]); - } - showStatus(result); - showTab(1); - }; - - var hideSpinner = function() { - $('button i.fa').show(); - $('button i.fa-spin').hide(); - }; - - var autosaveTimer; - var autosaveLabel = $("#autosave-label span"); - - var resetSaveTimer = function(){ - clearTimeout(autosaveTimer); - autosaveTimer = setTimeout(autosave, AUTOSAVE_INTERVAL); - }; - - var autosave = function(){ - var date = new Date(); - autosaveLabel.parent().css("visibility", "visible"); - autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds()); - autosaveLabel.text(date.toLocaleTimeString()); - autosaveTimer = null; - createSubmission($('#autosave'), null); - } - - var initializeEditors = function() { - $('.editor').each(function(index, element) { - var editor = ace.edit(element); - - if (qa_api) { - editor.getSession().on("change", function (deltaObject) { - qa_api.executeCommand('syncEditor', [active_file, deltaObject]); - }); - } - var document = editor.getSession().getDocument(); - // insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added - var file_id = $(element).data('file-id'); - var content = $('.editor-content[data-file-id=' + file_id + ']'); - 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(THEME); - editor.commands.bindKey("ctrl+alt+0", null); - editors.push(editor); - 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"){ - tracepositions_regex = regex_for_language.get($(element).data('mode')); - } - - var file_id = $(element).data('id'); - - /* - * Register event handlers - */ - - // editor itself - editor.on("paste", handlePasteEvent); - editor.on("copy", handleCopyEvent); - - // listener for autosave - session.on("change", function (deltaObject) { - resetSaveTimer(); - }); - }); - }; - - var initializeEventHandlers = function() { - $(document).on('click', '#results a', showOutput); - $(document).on('keypress', handleKeyPress); - $('a[data-toggle="tab"]').on('show.bs.tab', storeTab); - initializeFileTreeButtons(); - initializeWorkflowButtons(); - initializeWorkspaceButtons(); - initializeRequestForComments() - }; - - var initializeFileTree = function() { - $('#files').jstree($('#files').data('entries')); - $('#files').on('click', 'li.jstree-leaf', function() { - active_file = { - filename: $(this).text(), - id: parseInt($(this).attr('id')) - }; - var frame = $('[data-file-id="' + active_file.id + '"]').parent(); - showFrame(frame); - toggleButtonStates(); - }); - }; - - var initializeFileTreeButtons = function() { - $('#create-file').on('click', showFileDialog); - $('#destroy-file').on('click', confirmDestroy); - $('#download').on('click', downloadCode); - $('#request-for-comments').on('click', requestComments); - }; - - - var initializeRegexes = function(){ - regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g); - regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g); - } - - var initializeTooltips = function() { - $('[data-tooltip]').tooltip(); - }; - - var initializeWorkflowButtons = function() { - $('#start').on('click', showWorkspaceTab); - //$('#submit').on('click', confirmSubmission); - $('#submit').on('click', submitCode); - }; - - var initializeWorkspaceButtons = function() { - $('#assess').on('click', scoreCode); // todo - $('#dropdown-render, #render').on('click', renderCode); - $('#dropdown-run, #run').on('click', runCode); - $('#dropdown-stop, #stop').on('click', stopCode); // todo - $('#dropdown-test, #test').on('click', testCode); // todo - $('#save').on('click', saveCode); - $('#start-over').on('click', confirmReset); - }; - - var initializeRequestForComments = function () { - var button = $('.requestCommentsButton'); - button.hide(); - button.on('click', function() { - $('#comment-modal').modal('show'); - }); - - $('#askForCommentsButton').on('click', requestComments); - - setTimeout(function() { - button.fadeIn(); - }, REQUEST_FOR_COMMENTS_DELAY); - }; - - var isActiveFileExecutable = function() { - return 'executable' in active_frame.data(); - }; - - var setActiveFile = function (filename, fileId) { - active_file = { - filename: filename, - id: fileId - }; - }; - - var isActiveFileRenderable = function() { - return 'renderable' in active_frame.data(); - }; - - var isActiveFileRunnable = function() { - return isActiveFileExecutable() && ['main_file', 'user_defined_file'].includes(active_frame.data('role')); - }; - - var isActiveFileStoppable = function() { - return isActiveFileRunnable() && running; - }; - - var isActiveFileSubmission = function() { - return ['Submission'].includes(active_frame.data('contextType')); - }; - - var isActiveFileTestable = function() { - return isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test'].includes(active_frame.data('role')); - }; - - var isBrowserSupported = function() { - // websockets is used for run, score and test - return Modernizr.websockets; - }; - - var populatePanel = function(panel, result, index) { - panel.removeClass('panel-default').addClass(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); - }; - - - var resetOutputTab = function() { - clearOutput(); - $('#hint').fadeOut(); - $('#flowrHint').fadeOut(); - showTab(1); - } - - var printOutput = function(output, colorize, index) { - var element = findOrCreateOutputElement(index); - // disable streaming if desired - //if (output.stdout && output.stdout.length >= 20 && output.stdout.substr(0,20) == "##DISABLESTREAMING##"){ - // output_mode_is_streaming = false; - //} - 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); - flowrOutputBuffer += output.stderr; - QaApiOutputBuffer.stderr += output.stderr; - } else if (output.stdout) { - //if (output_mode_is_streaming){ - element.addClass('text-success').append(output.stdout); - flowrOutputBuffer += output.stdout; - QaApiOutputBuffer.stdout += output.stdout; - //}else{ - // element.addClass('text-success'); - // element.data('content_buffer' , element.data('content_buffer') + output.stdout); - //} - //} else if (output.code && output.code == '200'){ - // element.append( element.data('content_buffer')); - } else { - element.addClass('text-muted').text($('#output').data('message-no-output')); - } - }; - - var printScoringResult = function(result, index) { - $('#results').show(); - var panel = $('#dummies').children().first().clone(); - populatePanel(panel, result, index); - $('#results ul').first().append(panel); - }; - - var printScoringResults = function(response) { - $('#results ul').first().html(''); - $('.test-count .number').html(response.length); - clearOutput(); - - _.each(response, function(result, index) { - printOutput(result, false, index); - printScoringResult(result, index); - }); - - if (_.some(response, function(result) { - return result.status === 'timeout'; - })) { - showTimeoutMessage(); - } - if (_.some(response, function(result) { - return result.status === 'container_depleted'; - })) { - showContainerDepletedMessage(); - } - if (qa_api) { - // send test response to QA - qa_api.executeCommand('syncOutput', [response]); - } - }; - - // Publishing events for other (JS) components to react to codeocean events - var publishCodeOceanEvent = function (eventName, contextData) { - - var payload = { - user: { - type: 'User', - uuid: $('#editor').data('user-id') - }, - verb: { - type: eventName - }, - resource: { - type: 'page', - uuid: document.location.href - }, - timestamp: new Date().toISOString(), - with_result: {}, - in_context: contextData - }; - - $.ajax("https://open.hpi.de/lanalytics/log", { - type: 'POST', - cache: false, - dataType: 'JSON', - data: payload, - success: {}, - error: {} - }) - - }; - - var renderCode = function(event) { - event.preventDefault(); - if ($('#render').is(':visible')) { - createSubmission(this, null, function(response) { - var url = response.render_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); - var pop_up_window = window.open(url); - if (pop_up_window) { - pop_up_window.onerror = function(message) { - clearOutput(); - printOutput({ - stderr: message - }, true, 0); - sendError(message, response.id); - showTab(1); - }; - } - }); - } - }; - - var renderHint = function(object) { - var hint = object.data || object.hint; - if (hint) { - $('#hint .panel-body').text(hint); - $('#hint').fadeIn(); - } - }; - - var renderProgressBar = function(score, maximum_score) { - var percentage = score / maximum_score * 100; - var progress_bar = $('#score .progress-bar'); - progress_bar.removeClass().addClass(getProgressBarClass(percentage)); - progress_bar.attr({ - 'aria-valuemax': maximum_score, - 'aria-valuemin': 0, - 'aria-valuenow': score - }); - progress_bar.css('width', percentage + '%'); - }; - - var renderScore = function() { - var score = parseFloat($('#score').data('score')); - var maxium_score = parseFloat($('#score').data('maximum-score')); - if (score >= 0 && score <= maxium_score && maxium_score >0 ) { - var percentage_score = (score / maxium_score * 100 ).toFixed(0); - $('.score').html(percentage_score + '%'); - } - else { - $('.score').html( 0 + '%'); - } - renderProgressBar(score, maxium_score); - }; - - var resetCode = function() { - showSpinner(this); - ajax({ - method: 'GET', - url: $('#start-over').data('url') - }).success(function(response) { - hideSpinner(); - _.each(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); - }); - }); - }; - - var runCode = function(event) { - event.preventDefault(); - if ($('#run').is(':visible')) { - createSubmission(this, null, function(response) { - $('#stop').data('url', response.stop_url); - running = true; - showSpinner($('#run')); - toggleButtonStates(); - var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); - evaluateCode(url, function(evt) { parseCanvasMessage(evt.data, true); }); - }); - } - }; - - var saveCode = function(event) { - event.preventDefault(); - createSubmission(this, null, function() { - $.flash.success({ - text: $('#save').data('message-success') - }); - }); - }; - - var sendError = function(message, submission_id) { - showSpinner($('#render')); - var jqxhr = ajax({ - data: { - error: { - message: message, - submission_id: submission_id - } - }, - url: $('#editor').data('errors-url') - }); - jqxhr.always(hideSpinner); - jqxhr.success(renderHint); - }; - - var scoreCode = function(event) { - event.preventDefault(); - createSubmission(this, null, function(response) { - showSpinner($('#assess')); - var url = response.score_url; - evaluateCode(url, handleScoringResponse); - }); - }; - - var showFileDialog = function(event) { - event.preventDefault(); - createSubmission(this, null, function(response) { - $('#code_ocean_file_context_id').val(response.id); - $('#modal-file').modal('show'); - }); - }; - - var 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'); - setActiveFile(frame.data('filename'), file_id); - $('#files').jstree().select_node(file_id); - showFrame(frame); - toggleButtonStates(); - }; - - var showFrame = function(frame) { - active_frame = frame; - $('.frame').hide(); - frame.show(); - }; - - var showOutput = function(event) { - event.preventDefault(); - showTab(1); - $('#output').scrollTo($(this).attr('href')); - }; - - var showRequestedTab = function() { - if(REMEMBER_TAB){ - var regexp = /tab=(\d+)/; - if (regexp.test(window.location.search)) { - var index = regexp.exec(window.location.search)[1] - 1; - } else { - var index = localStorage.tab; - } - } else { - // else start with first tab. - var index = 0; - } - showTab(index); - }; - - var showSpinner = function(initiator) { - $(initiator).find('i.fa').hide(); - $(initiator).find('i.fa-spin').show(); - }; - - var showStatus = function(output) { - if (output.status === 'timeout') { - showTimeoutMessage(); - } else if (output.status === 'container_depleted') { - showContainerDepletedMessage(); - } else if (output.stderr) { - $.flash.danger({ - icon: ['fa', 'fa-bug'], - text: $('#run').data('message-failure') - }); - } - /* do not show the success message any longer, puzzles and distracts users. - else { - $.flash.success({ - icon: ['fa', 'fa-check'], - text: $('#run').data('message-success') - }); - } */ - }; - - var showContainerDepletedMessage = function() { - $.flash.danger({ - icon: ['fa', 'fa-clock-o'], - text: $('#editor').data('message-depleted') - }); - }; - - var showTab = function(index) { - $('a[data-toggle="tab"]').eq(index || 0).tab('show'); - }; - - var showTimeoutMessage = function() { - $.flash.info({ - icon: ['fa', 'fa-clock-o'], - text: $('#editor').data('message-timeout') - }); - }; - - var showWebsocketError = function() { - $.flash.danger({ - text: $('#flash').data('message-failure') - }); - } - - var showWorkspaceTab = function(event) { - if(event){ - event.preventDefault(); - } - showTab(0); - }; - - var stopCode = function(event) { - event.preventDefault(); - if (isActiveFileStoppable()) { - killWebsocketAndContainer(); - } - }; - - var killWebsocketAndContainer = function() { - if (websocket.readyState != WebSocket.OPEN) { - return; - } - websocket.send(JSON.stringify({cmd: 'exit'})); - websocket.flush(); - websocket.close(); - - if(turtlescreen != null){ - resetTurtle = true; - } - - hideSpinner(); - running = false; - toggleButtonStates(); - hidePrompt(); - } - - var storeTab = function(event) { - localStorage.tab = $(event.target).parent().index(); - }; - - var submitCode = function() { - createSubmission($('#submit'), null, function(response) { - if (response.redirect) { - localStorage.removeItem('tab'); - window.location = response.redirect; - } - }); - }; - - var testCode = function(event) { - event.preventDefault(); - if ($('#test').is(':visible')) { - createSubmission(this, null, function(response) { - showSpinner($('#test')); - var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); - evaluateCode(url, handleTestResponse); - }); - } - }; - - var toggleButtonStates = function() { - $('#destroy-file').prop('disabled', active_frame.data('role') !== 'user_defined_file'); - $('#dropdown-render').toggleClass('disabled', !isActiveFileRenderable()); - $('#dropdown-run').toggleClass('disabled', !isActiveFileRunnable() || running); - $('#dropdown-stop').toggleClass('disabled', !isActiveFileStoppable()); - $('#dropdown-test').toggleClass('disabled', !isActiveFileTestable()); - $('#dummy').toggle(!fileActionsAvailable()); - $('#editor-buttons .dropdown-toggle').toggle(fileActionsAvailable()); - $('#render').toggle(isActiveFileRenderable()); - $('#run').toggle(isActiveFileRunnable() && !running); - $('#stop').toggle(isActiveFileStoppable()); - $('#test').toggle(isActiveFileTestable()); - }; - - var initWebsocketConnection = function(url, onmessageFunction) { - //TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss) - //causes: Puma::HttpParserError: Invalid HTTP format, parsing fails. - //TODO: make sure that this gets cached. - websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url); - websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection - websocket.onclose = function(evt) { /* expected at some point */ }; - websocket.onmessage = onmessageFunction; - websocket.onerror = function(evt) { showWebsocketError(); }; - websocket.flush = function() { this.send('\n'); } - }; - - var initTurtle = function() { - // todo guard clause if turtle is not required for the current exercise - - // clear canvas - // turtlecanvas.getContext("2d").clearRect(0, 0, turtlecanvas.width, turtlecanvas.height); - - if(resetTurtle) { - turtlescreen = new Turtle(websocket, turtlecanvas); - showCanvas(); - resetTurtle = false; - } - }; - - var initPrompt = function() { - if ($('#run').isPresent()) { - $('#run').bind('click', hidePrompt); - } - if ($('#prompt').isPresent()) { - $('#prompt').on('keypress', handlePromptKeyPress); - $('#prompt-submit').on('click', submitPromptInput); - } - } - - var executeWebsocketCommand = function(msg) { - if ($.inArray(msg.cmd, commands) == -1) { - console.log("Unknown command: " + msg.cmd); - // skipping unregistered commands is required - // as we may receive mirrored response due to internal behaviour - return; - } - switch(msg.cmd) { - case 'input': - showPrompt(msg); - break; - case 'write': - printWebsocketOutput(msg); - break; - case 'turtle': - initTurtle(); - showCanvas(); - handleTurtleCommand(msg); - break; - case 'turtlebatch': - initTurtle(); - showCanvas(); - handleTurtlebatchCommand(msg); - break; - case 'render': - renderWebsocketOutput(msg); - break; - case 'exit': - killWebsocketAndContainer(); - handleQaApiOutput(); - handleStderrOutputForFlowr(); - augmentStacktraceInOutput(); - break; - case 'timeout': - // just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes. - showTimeoutMessage(); - break; - case 'status': - showStatus(msg) - break; - } - }; - - - var jumpToSourceLine = function(event){ - var file = $(event.target).data('file'); - var line = $(event.target).data('line'); - - showWorkspaceTab(null); - // set active file ?!?! - - var frame = $('div.frame[data-filename="' + file + '"]'); - showFrame(frame); - - var editor = editor_for_file.get(file); - editor.gotoLine(line, 0); - - }; - - var augmentStacktraceInOutput = function() { - if(tracepositions_regex){ - var element = $('#output>pre'); - var text = element.text(); - element.on( "click", "a", jumpToSourceLine); - - var matches; - - while(matches = tracepositions_regex.exec(text)){ - var frame = $('div.frame[data-filename="' + matches[1] + '"]') - - if(frame.length > 0){ - element.html(text.replace(matches[0], "" + matches[0] + "")); - } - } - } - - }; - - var renderWebsocketOutput = function(msg){ - var element = findOrCreateRenderElement(0); - element.append(msg.data); - }; - - var printWebsocketOutput = function(msg) { - if (!msg.data) { - return; - } - //msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "
"); - msg.data = msg.data.replace(/(\r)/gm, "\n"); - var stream = {}; - stream[msg.stream] = msg.data; - printOutput(stream, true, 0); - }; - - var handleTurtleCommand = function(msg) { - if (msg.action in turtlescreen) { - result = turtlescreen[msg.action].apply(turtlescreen, msg.args); - websocket.send(JSON.stringify({cmd: 'result', 'result': result})); - } else { - websocket.send(JSON.stringify({cmd: 'exception', exception: 'AttributeError', message: msg.action})); - } - websocket.flush(); - }; - - var handleTurtlebatchCommand = function(msg) { - for (i = 0; i < msg.batch.length; i++) { - cmd = msg.batch[i]; - turtlescreen[cmd[0]].apply(turtlescreen, cmd[1]); - } - }; - - var handlePromptKeyPress = function(evt) { - if (evt.which === ENTER_KEY_CODE) { - submitPromptInput(); - } - } - - var submitPromptInput = function() { - var input = $('#prompt-input'); - var message = input.val(); - websocket.send(JSON.stringify({cmd: 'result', 'data': message})); - websocket.flush(); - input.val(''); - hidePrompt(); - } - - var parseCanvasMessage = function(message, recursive) { - var msg; - message = message.replace(/^\s+|\s+$/g, ""); - try { - // todo validate json instead of catching - msg = JSON.parse(message); - } catch (e) { - if (!recursive) { - return false; - } - // why does docker sometimes send multiple commands at once? - message = message.replace(/^\s+|\s+$/g, ""); - messages = message.split("\n"); - for (var i = 0; i < messages.length; i++) { - if (!messages[i]) { - continue; - } - parseCanvasMessage(messages[i], false); - } - return; - } - executeWebsocketCommand(msg); - }; - - var showPrompt = function(msg) { - var label = $('#prompt .input-group-addon'); - label.text(msg.data || label.data('prompt')); - if (prompt.isPresent() && prompt.hasClass('hidden')) { - prompt.removeClass('hidden'); - } - $('#prompt input').focus(); - } - - var hidePrompt = function() { - if (prompt.isPresent() && !prompt.hasClass('hidden')) { - prompt.addClass('hidden'); - } - } - - var showCanvas = function() { - if ($('#turtlediv').isPresent() - && turtlecanvas.hasClass('hidden')) { - // initialize two-column layout - $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); - turtlecanvas.removeClass('hidden'); - } - }; - - var hideCanvas = function() { - if ($('#turtlediv').isPresent() - && !(turtlecanvas.hasClass('hidden'))) { - output = $('#output-col1'); - if (output.hasClass('two-column')) { - output.removeClass('col-lg-7 col-md-7 two-column'); - } - turtlecanvas.addClass('hidden'); - } - }; - - var 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() { - hideSpinner(); - $.flash.success({ text: $('#askForCommentsButton').data('message-success') }); - }).error(ajaxError); - } - - createSubmission($('.requestCommentsButton'), null, createRequestForComments); - - $('#comment-modal').modal('hide'); - var button = $('.requestCommentsButton'); - button.fadeOut(); - } - - var initializeCodePilot = function() { - if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) { - $('#editor-column').addClass('col-md-8').removeClass('col-md-10'); - $('#questions-column').addClass('col-md-3'); - - var node = document.getElementById('questions-holder'); - var url = $('#questions-holder').data('url'); - - qa_api = new QaApi(node, url); - } - } - - // save on quit - $(window).on("beforeunload", function() { - if(autosaveTimer){ - autosave(); - } - - }); - - if ($('#editor').isPresent()) { - if (isBrowserSupported()) { - initializeRegexes(); - initializeCodePilot(); - $('.score, #development-environment').show(); - configureEditors(); - initializeEditors(); - initializeEventHandlers(); - initializeFileTree(); - initializeTooltips(); - initPrompt(); - renderScore(); - showFirstFile(); - showRequestedTab(); - } else { - $('#alert').show(); - } } }); diff --git a/app/assets/javascripts/editor/ajax.js.erb b/app/assets/javascripts/editor/ajax.js.erb new file mode 100644 index 00000000..a6caa904 --- /dev/null +++ b/app/assets/javascripts/editor/ajax.js.erb @@ -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') + }); + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/codepilot.js.erb b/app/assets/javascripts/editor/codepilot.js.erb new file mode 100644 index 00000000..aa1513be --- /dev/null +++ b/app/assets/javascripts/editor/codepilot.js.erb @@ -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': ''}; + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb new file mode 100644 index 00000000..c5f8a56b --- /dev/null +++ b/app/assets/javascripts/editor/editor.js.erb @@ -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 = $('
').attr('id', 'output-' + index);
+      $('#output').append(element);
+      return element;
+    }
+  },
+
+  findOrCreateRenderElement: function (index) {
+    if ($('#render-' + index).isPresent()) {
+      return $('#render-' + index);
+    } else {
+      var element = $('
').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], "" + matches[0] + "")); + } + } + } + }, + + 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)); + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb new file mode 100644 index 00000000..f7b51f30 --- /dev/null +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -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')); + } + } + + +}; diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb new file mode 100644 index 00000000..3cc9b262 --- /dev/null +++ b/app/assets/javascripts/editor/execution.js.erb @@ -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(); + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb new file mode 100644 index 00000000..4dc0de45 --- /dev/null +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -0,0 +1,35 @@ +CodeOceanEditorFlowr = { + isFlowrEnabled: true, + flowrResultHtml: '
', + + 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('Open this question'); + + flowrHintBody.append(resultTile); + }); + + if (data.queryResults.length !== 0) { + $('#flowrHint').fadeIn(); + } + }); + + this.flowrOutputBuffer = ''; + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb new file mode 100644 index 00000000..1329cef2 --- /dev/null +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -0,0 +1,93 @@ +CodeOceanEditorFlowr = { + isFlowrEnabled: true, + flowrResultHtml: '
', + + 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('Open this question'); + + 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); + }, +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/prompt.js.erb b/app/assets/javascripts/editor/prompt.js.erb new file mode 100644 index 00000000..1cbdfc8f --- /dev/null +++ b/app/assets/javascripts/editor/prompt.js.erb @@ -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(); + } + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb new file mode 100644 index 00000000..4b8678a7 --- /dev/null +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -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); + } +}; diff --git a/app/assets/javascripts/editor/turtle.js.erb b/app/assets/javascripts/editor/turtle.js.erb new file mode 100644 index 00000000..bc5552eb --- /dev/null +++ b/app/assets/javascripts/editor/turtle.js.erb @@ -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'); + } + } + +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js.erb new file mode 100644 index 00000000..baba623d --- /dev/null +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -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(); +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor_edit.js b/app/assets/javascripts/editor_edit.js deleted file mode 100644 index b1251cf9..00000000 --- a/app/assets/javascripts/editor_edit.js +++ /dev/null @@ -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(); - } -}); - diff --git a/app/assets/javascripts/editor_edit.js.erb b/app/assets/javascripts/editor_edit.js.erb new file mode 100644 index 00000000..e69de29b diff --git a/app/assets/javascripts/exercises.js b/app/assets/javascripts/exercises.js.erb similarity index 68% rename from app/assets/javascripts/exercises.js rename to app/assets/javascripts/exercises.js.erb index 9d85b4f1..2a17b405 100644 --- a/app/assets/javascripts/exercises.js +++ b/app/assets/javascripts/exercises.js.erb @@ -1,13 +1,68 @@ $(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 execution_environments; 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) { event.preventDefault(); var element = $('#dummies').children().first().clone(); - var html = $('
').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 = $('
').append(element).html().replace(/index/g, latestTextAreaIndex); $('#files').append(html); $('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id); $('#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. // 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(); + + // 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() { @@ -152,8 +211,9 @@ $(function() { }; 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({ - url: '/file_templates/by_file_type/' + fileType + '.json', + url: rel_url_root + '/file_templates/by_file_type/' + fileType + '.json', dataType: 'json' }); jqxhr.done(function(response) { @@ -192,4 +252,13 @@ $(function() { highlightCode(); } } + + + if ($('#editor-edit').isPresent()) { + configureEditors(); + initializeEditors(); + $('.frame').show(); + } + + }); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index e41153e2..755e3409 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -7,9 +7,18 @@ button i.fa-spin { 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 { display: none; - height: 400px; audio, img, video { max-width: 100%; @@ -18,6 +27,7 @@ button i.fa-spin { .score { display: none; + vertical-align: bottom; } #alert, #development-environment { @@ -26,7 +36,6 @@ button i.fa-spin { #dummy { display: none; - width: 100% !important; } #editor-buttons { @@ -64,6 +73,7 @@ button i.fa-spin { #outputInformation { #output { max-height: 500px; + width: 100%; overflow: auto; margin: 2em 0; @@ -89,9 +99,96 @@ button i.fa-spin { font-size: 0.8em; } -.requestCommentsButton { +#turtlecanvas{ + border-style:solid; + border-width:thin; + display: block; + margin: auto; + +} + +/* .requestCommentsButton { position: relative; margin-top: -50px; margin-right: 25px; 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; +} \ No newline at end of file diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index cb6e9c9d..31e970f3 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -118,7 +118,7 @@ class ExercisesController < ApplicationController private :user_by_code_harbor_token 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 private :exercise_params diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 1b6f9421..63507ede 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -129,7 +129,7 @@ class SubmissionsController < ApplicationController socket.on :message do |event| 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 socket.on :close do |event| @@ -139,12 +139,12 @@ class SubmissionsController < ApplicationController tubesock.onmessage do |data| Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data) # 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 parsed = JSON.parse(data) - if parsed['cmd'] == 'exit' + if parsed['cmd'] == 'client_kill' Rails.logger.debug("Client exited container.") - @docker_client.exit_container(result[:container]) + @docker_client.kill_container(result[:container]) else socket.send data Rails.logger.debug('Sent the received client data to docker:' + data) @@ -171,10 +171,11 @@ class SubmissionsController < ApplicationController tubesock.close end - def handle_message(message, tubesock) + def handle_message(message, tubesock, container) # Handle special commands first if (/^exit/.match(message)) kill_socket(tubesock) + @docker_client.exit_container(container) else # Filter out information about run_command, test_command, user or working directory run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) @@ -231,7 +232,13 @@ class SubmissionsController < ApplicationController hijack do |tubesock| Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? # 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 @@ -291,6 +298,7 @@ class SubmissionsController < ApplicationController # tubesock is the socket to the client tubesock.send_data JSON.dump(output) + tubesock.send_data JSON.dump('cmd' => 'exit') end end diff --git a/app/views/exercises/_code_field.html.slim b/app/views/exercises/_code_field.html.slim index 5273a693..bb2a806f 100644 --- a/app/views/exercises/_code_field.html.slim +++ b/app/views/exercises/_code_field.html.slim @@ -2,5 +2,6 @@ = form.label(attribute, label) |   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) + = render partial: 'editor_edit', locals: { exercise: @exercise } diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 0ce5b73f..6b14d3c8 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -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 - div class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') = render('editor_file_tree', files: @files) - div id='frames' class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9') +- external_user_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') +#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="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| = render('editor_frame', exercise: exercise, file: file) #autosave-label = t('exercises.editor.lastsaved') 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" - .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') diff --git a/app/views/exercises/_editor_button.html.slim b/app/views/exercises/_editor_button.html.slim index cc03c1a9..ae69529e 100644 --- a/app/views/exercises/_editor_button.html.slim +++ b/app/views/exercises/_editor_button.html.slim @@ -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 class=icon = label diff --git a/app/views/exercises/_editor_edit.html.slim b/app/views/exercises/_editor_edit.html.slim index 810653d2..83f27d68 100644 --- a/app/views/exercises/_editor_edit.html.slim +++ b/app/views/exercises/_editor_edit.html.slim @@ -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 - .frame + .edit-frame .editor-content.hidden .editor \ No newline at end of file diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index c23158b9..16cc705b 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -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? - = 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('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')) + = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) \ No newline at end of file diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index 01640fa8..eff1541c 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -12,8 +12,4 @@ = link_to(file.native_file.file.name_with_extension, file.native_file.url) - else .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 - - button.btn.btn-primary.requestCommentsButton type='button' id="requestComments" - i.fa.fa-comment - = t('exercises.editor.requestComments') \ No newline at end of file + .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 \ No newline at end of file diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim new file mode 100644 index 00000000..ab09adac --- /dev/null +++ b/app/views/exercises/_editor_output.html.slim @@ -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 \ No newline at end of file diff --git a/app/views/exercises/_file_form.html.slim b/app/views/exercises/_file_form.html.slim index 796c6722..065eca66 100644 --- a/app/views/exercises/_file_form.html.slim +++ b/app/views/exercises/_file_form.html.slim @@ -37,5 +37,4 @@ li.panel.panel-default .form-group = f.label(:role, t('activerecord.attributes.file.weight')) = 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 partial: 'editor_edit', locals: { exercise: @exercise } \ No newline at end of file + = render('code_field', attribute: :content, form: f, label: t('activerecord.attributes.file.content')) \ No newline at end of file diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index a640f0c2..c4159254 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -28,6 +28,10 @@ label = f.check_box(: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') ul#files.list-unstyled.panel-group = f.fields_for :files do |files_form| diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 1feab189..87ff4e1f 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -1,85 +1,22 @@ .row - #editor-column.col-md-10.col-md-offset-1 - h1 = @exercise + #editor-column.col-md-12 + 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' h4 = t('.alert.title') p = t('.alert.text', application_name: application_name) #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 #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 #questions-column diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index 5c554da8..902f8135 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -16,6 +16,7 @@ h1 = row(label: 'exercise.public', value: @exercise.public?) = 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_auto_completion', value: @exercise.allow_auto_completion?) = row(label: 'exercise.embedding_parameters') do = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) diff --git a/config/application.rb b/config/application.rb index 68350af1..10a8537d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -43,3 +43,5 @@ module CodeOcean end end end + +Rails.application.config.assets.precompile += %w( markdown-buttons.png ) \ No newline at end of file diff --git a/config/docker.yml.erb b/config/docker.yml.erb index 0a018241..8fac75d0 100644 --- a/config/docker.yml.erb +++ b/config/docker.yml.erb @@ -7,8 +7,8 @@ default: &default development: <<: *default - host: tcp://192.168.59.104:2376 - ws_host: ws://192.168.59.104:2376 #url to connect rails server to docker host + 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: diff --git a/config/docker.yml.erb.example b/config/docker.yml.erb.example new file mode 100644 index 00000000..9f55d126 --- /dev/null +++ b/config/docker.yml.erb.example @@ -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) %> diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 1cb098cb..5dfdae13 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -23,7 +23,7 @@ Rails.application.configure do config.serve_static_assets = false # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. diff --git a/config/locales/de.yml b/config/locales/de.yml index b34d6a7a..0695eda6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -36,6 +36,7 @@ de: public: Öffentlich title: Titel user: Autor + allow_auto_completion: "Autovervollständigung aktivieren" allow_file_creation: "Dateierstellung erlauben" external_user: consumer: Konsument @@ -188,6 +189,8 @@ de: worktime: Durchschnittliche Arbeitszeit exercises: editor: + collapse_action_sidebar: Aktions-Leiste Einklappen + collapse_output_sidebar: Ausgabe-Leiste Einklappen confirm_start_over: Wollen Sie wirklich von vorne anfangen? confirm_submit: Wollen Sie Ihren Code wirklich zur Bewertung abgeben? create_file: Neue Datei @@ -195,6 +198,8 @@ de: destroy_file: Datei löschen download: Herunterladen dummy: Keine Aktion + expand_action_sidebar: Aktions-Leiste Ausklappen + expand_output_sidebar: Ausgabe-Leiste Ausklappen input: Ihre Eingabe lastsaved: 'Zuletzt gespeichert: ' network: 'Während Ihr Code läuft, ist Port %{port} unter folgender Adresse erreichbar: %{address}.' @@ -203,6 +208,7 @@ de: run_failure: Ihr Code konnte nicht auf der Plattform ausgeführt werden. run_success: Ihr Code wurde auf der Plattform ausgeführt. requestComments: Kommentare erbitten + requestCommentsTooltip: Falls Sie Hilfe mit Ihrem Code benötigen, können Sie hier Kommentare erbitten save: Speichern score: Bewerten send: Senden diff --git a/config/locales/en.yml b/config/locales/en.yml index e1663e5d..6d30f612 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -36,6 +36,7 @@ en: public: Public title: Title user: Author + allow_auto_completion: "Allow auto completion" allow_file_creation: "Allow file creation" external_user: consumer: Consumer @@ -188,6 +189,8 @@ en: worktime: Average Working Time exercises: editor: + collapse_action_sidebar: Collapse Action Sidebar + collapse_output_sidebar: Collapse Output Sidebar confirm_start_over: Do you really want to start over? confirm_submit: Do you really want to submit your code for grading? create_file: New File @@ -195,6 +198,8 @@ en: destroy_file: Delete File download: Download dummy: No Action + expand_action_sidebar: Expand Action Sidebar + expand_output_sidebar: Expand Output Sidebar input: Your input lastsaved: 'Last saved: ' network: 'While your code is running, port %{port} is accessible using the following address: %{address}.' @@ -203,6 +208,7 @@ en: run_failure: Your code could not be run. run_success: Your code was run on our servers. requestComments: Request comments + requestCommentsTooltip: If you need help with your code, you can now request comments here! save: Save score: Score send: Send diff --git a/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb b/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb new file mode 100644 index 00000000..99941e36 --- /dev/null +++ b/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb @@ -0,0 +1,5 @@ +class AddAllowAutoCompletionToExercises < ActiveRecord::Migration + def change + add_column :exercises, :allow_auto_completion, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index b2c3b8a4..330f99d6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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 enable_extension "plpgsql" @@ -74,7 +74,6 @@ ActiveRecord::Schema.define(version: 20160704143402) do t.integer "file_type_id" t.integer "memory_limit" t.boolean "network_enabled" - end create_table "exercises", force: true do |t| @@ -90,6 +89,7 @@ ActiveRecord::Schema.define(version: 20160704143402) do t.string "token" t.boolean "hide_file_tree" t.boolean "allow_file_creation" + t.boolean "allow_auto_completion", default: false end create_table "external_users", force: true do |t| diff --git a/lib/docker_client.rb b/lib/docker_client.rb index f993c37c..7fe02d02 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -72,7 +72,10 @@ class DockerClient # Headers are required by Docker 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| Rails.logger.info "Websocket error: " + event.message @@ -261,16 +264,21 @@ class DockerClient end end - def exit_container(container) - Rails.logger.debug('exiting container ' + container.to_s) - # exit the timeout thread if it is still alive + def exit_thread_if_alive if(@thread && @thread.alive?) @thread.exit 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. (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container) end + def kill_container(container) Rails.logger.info('killing container ' + container.to_s) # remove container from pool, then destroy it @@ -286,6 +294,7 @@ class DockerClient container = self.class.create_container(@execution_environment) DockerContainerPool.add_to_all_containers(container, @execution_environment) end + exit_thread_if_alive end def execute_run_command(submission, filename, &block)