From 63f62a8efc78c565ff0d2a129715c0e66bdd7621 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 11 Aug 2016 19:01:44 +0200 Subject: [PATCH 01/57] Reconfigured docker-yml and added missing (something) to application.rb --- Gemfile.lock | 3 --- config/application.rb | 2 ++ config/docker.yml.erb | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 527a91fe..eef6f7f0 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/config/application.rb b/config/application.rb index 4b2a543a..f180783b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,3 +41,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 1124e063..9f55d126 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: From 842a38c13ad472a7e0313e95e5b3eaf4de4e36ed Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 11 Aug 2016 23:24:38 +0200 Subject: [PATCH 02/57] Moved everything into new files. Made editor.js.erb really small. --- app/assets/javascripts/editor.js.erb | 1305 +---------------- app/assets/javascripts/editor/ajax.js.erb | 16 + app/assets/javascripts/editor/editor.js.erb | 585 ++++++++ .../javascripts/editor/evaluation.js.erb | 266 ++++ app/assets/javascripts/editor/flowr.js.erb | 91 ++ .../javascripts/editor/submissions.js.erb | 171 +++ app/assets/javascripts/editor/turtle.js.erb | 45 + .../javascripts/editor/websocket.js.erb | 86 ++ 8 files changed, 1274 insertions(+), 1291 deletions(-) create mode 100644 app/assets/javascripts/editor/ajax.js.erb create mode 100644 app/assets/javascripts/editor/editor.js.erb create mode 100644 app/assets/javascripts/editor/evaluation.js.erb create mode 100644 app/assets/javascripts/editor/flowr.js.erb create mode 100644 app/assets/javascripts/editor/submissions.js.erb create mode 100644 app/assets/javascripts/editor/turtle.js.erb create mode 100644 app/assets/javascripts/editor/websocket.js.erb diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index 2d73bbf0..0247841b 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -1,1298 +1,21 @@ $(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 NONE = 0; - var WEBSOCKET = 1; - var SERVER_SEND_EVENT = 2; - var editors = []; - var editor_for_file = new Map(); - var regex_for_language = new Map(); - var tracepositions_regex; + //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); + $.extend(CodeOceanEditor, CodeOceanEditorEvaluation); + $.extend(CodeOceanEditor, CodeOceanEditorFlowr); + $.extend(CodeOceanEditor, CodeOceanEditorSubmissions); + $.extend(CodeOceanEditor, CodeOceanEditorTurtle); + $.extend(CodeOceanEditor, CodeOceanEditorWebsocket); - var active_file = undefined; - var active_frame = undefined; - var running = false; - var qa_api = undefined; - var output_mode_is_streaming = true; - var runmode = NONE; - - 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 closeEventSource = function(event) { - event.target.close(); - hideSpinner(); - running = false; - toggleButtonStates(); - - if (event.type === 'error' || JSON.parse(event.data).code !== 200) { - ajaxError(); - showTab(0); - } - }; - - 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) - if (file_id_old != null){ - // if we find file_id_old (this is the reference to the base file) in the submission, this is the match - for(var j = 0; j< data.files.length; j++){ - if(data.files[j].file_id == file_id_old){ - //$(editors[i].container).data('id') = data.files[j].id; - $(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, streamed, callback) { - (streamed ? evaluateCodeWithStreamedResponse : evaluateCodeWithoutStreamedResponse)(url, callback); - }; - - var evaluateCodeWithStreamedResponse = function(url, onmessageFunction) { - initWebsocketConnection(url, onmessageFunction); - - // TODO only init turtle when required - initTurtle(); - - // TODO reimplement via websocket messsages - /*var event_source = new EventSource(url); - event_source.addEventListener('hint', renderHint); - event_source.addEventListener('info', storeContainerInformation); - - if ($('#flowrHint').isPresent()) { - event_source.addEventListener('output', handleStderrOutputForFlowr); - event_source.addEventListener('close', handleStderrOutputForFlowr); - } - - if (qa_api) { - event_source.addEventListener('close', handleStreamedResponseForCodePilot); - }*/ - }; - - var handleStreamedResponseForCodePilot = function(event) { - qa_api.executeCommand('syncOutput', [chunkBuffer]); - chunkBuffer = [{streamedResponse: true}]; - } - - var evaluateCodeWithoutStreamedResponse = function(url, callback) { - var jqxhr = ajax({ - method: 'GET', - url: url - }); - jqxhr.always(hideSpinner); - jqxhr.done(callback); - jqxhr.fail(ajaxError); - }; - - 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 isActiveFileBinary = function() { - return 'binary' in active_frame.data(); - }; - - 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 chunkBuffer = [{streamedResponse: true}]; - - var printChunk = function(event) { - var output = JSON.parse(event.data); - if (output) { - printOutput(output, true, 0); - // send test response to QA - // we are expecting an array of outputs: - if (qa_api) { - chunkBuffer.push(output); - } - } else { - resetOutputTab(); - } - }; - - 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')) { - runmode = WEBSOCKET; - 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, true, 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(); - runmode = SERVER_SEND_EVENT; - createSubmission(this, null, function(response) { - showSpinner($('#assess')); - var url = response.score_url; - evaluateCode(url, true, 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 ($('#stop').is(':visible')) { - if(runmode == WEBSOCKET){ - killWebsocketAndContainer(); - } else if (runmode == SERVER_SEND_EVENT) { - stopCodeServerSendEvent(event); - } - runmode = NONE; - } - }; - - var stopCodeServerSendEvent = function(event){ - var jqxhr = ajax({ - data: { - container_id: $('#stop').data('container').id - }, - url: $('#stop').data('url') - }); - jqxhr.always(function() { - hideSpinner(); - running = false; - toggleButtonStates(); - }); - jqxhr.fail(ajaxError); - }; - - var killWebsocketAndContainer = function() { - if (websocket.readyState != WebSocket.OPEN) { - return; - } - websocket.send(JSON.stringify({cmd: 'exit'})); - websocket.flush(); - websocket.close(); - hideSpinner(); - running = false; - toggleButtonStates(); - hidePrompt(); - } - - // todo set this from websocket command, required to e.g. stop container - var storeContainerInformation = function(event) { - var container_information = JSON.parse(event.data); - $('#stop').data('container', container_information); - - if (_.size(container_information.port_bindings) > 0) { - $.flash.info({ - icon: ['fa', 'fa-exchange'], - text: _.map(container_information.port_bindings, function(key, value) { - var url = window.location.protocol + '//' + window.location.hostname + ':' + key; - return $('#run').data('message-network').replace('%{port}', value).replace(/%{address}/g, url); - }).join('\n') - }); - } - }; - - 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, true, 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); - - turtlescreen = new Turtle(websocket, turtlecanvas); - if ($('#run').isPresent()) { - $('#run').bind('click', hideCanvas); - } - }; - - 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': - showCanvas(); - handleTurtleCommand(msg); - break; - case 'turtlebatch': - 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/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb new file mode 100644 index 00000000..62b40f51 --- /dev/null +++ b/app/assets/javascripts/editor/editor.js.erb @@ -0,0 +1,585 @@ +var CodeOceanEditor = { + ACE_FILES_PATH: '/assets/ace/', + ADEQUATE_PERCENTAGE: 50, + ALT_1_KEY_CODE: 161, + ALT_2_KEY_CODE: 8220, + ALT_3_KEY_CODE: 182, + ALT_4_KEY_CODE: 162, + ALT_R_KEY_CODE: 174, + ALT_S_KEY_CODE: 8218, + ALT_T_KEY_CODE: 8224, + FILENAME_URL_PLACEHOLDER: '{filename}', + SUCCESSFULL_PERCENTAGE: 90, + THEME: 'ace/theme/textmate', + REMEMBER_TAB: false, + AUTOSAVE_INTERVAL: 15 * 1000, + REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000, + NONE: 0, + WEBSOCKET: 1, + SERVER_SEND_EVENT: 2, + + editors: [], + editor_for_file: new Map(), + regex_for_language: new Map(), + tracepositions_regex: undefined, + + active_file: undefined, + active_frame: undefined, + running: false, + qa_api: undefined, + output_mode_is_streaming: true, + runmode: this.NONE, + + websocket: null, + turtlescreen: null, + numMessages: 0, + turtlecanvas: $('#turtlecanvas'), + prompt: $('#prompt'), + commands: ['input', 'write', 'turtle', 'turtlebatch', 'render', 'exit', 'timeout', 'status'], + streams: ['stdin', 'stdout', 'stderr'], + lastCopyText: null, + + autosaveTimer: null, + autosaveLabel: $("#autosave-label span"), + + ENTER_KEY_CODE: 13, + + flowrOutputBuffer: "", + QaApiOutputBuffer: {'stdout': '', 'stderr': ''}, + flowrResultHtml: '
', + + + configureEditors: function () { + _.each(['modePath', 'themePath', 'workerPath'], function (attribute) { + this.ace.config.set(attribute, this.ACE_FILES_PATH); + }.bind(this)); + }, + + confirmDestroy: function (event) { + event.preventDefault(); + if (confirm($(this).data('message-confirm'))) { + this.destroyFile(); + } + }, + + + confirmReset: function (event) { + event.preventDefault(); + if (confirm($(this).data('message-confirm'))) { + this.resetCode(); + } + }, + + fileActionsAvailable: function () { + return this.isActiveFileRenderable() || this.isActiveFileRunnable() || this.isActiveFileStoppable() || this.isActiveFileTestable(); + }, + + findOrCreateOutputElement: function (index) { + if ($('#output-' + index).isPresent()) { + return $('#output-' + index); + } else { + var element = $('
').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.showTab(1); + $('#output').scrollTo($(this).attr('href')); + }, + + renderProgressBar: function(score, maximum_score) { + var percentage = score / maximum_score * 100; + var progress_bar = $('#score .progress-bar'); + progress_bar.removeClass().addClass(this.getProgressBarClass(percentage)); + progress_bar.attr({ + 'aria-valuemax': maximum_score, + 'aria-valuemin': 0, + 'aria-valuenow': score + }); + progress_bar.css('width', percentage + '%'); + }, + + showFirstFile: function() { + var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first(); + var file_id = frame.find('.editor').data('file-id'); + this.setActiveFile(frame.data('filename'), file_id); + $('#files').jstree().select_node(file_id); + this.showFrame(frame); + this.toggleButtonStates(); + }, + + showFrame: function(frame) { + this.active_frame = frame; + $('.frame').hide(); + frame.show(); + }, + + getProgressBarClass: function (percentage) { + if (percentage < this.ADEQUATE_PERCENTAGE) { + return 'progress-bar progress-bar-striped progress-bar-danger'; + } else if (percentage < this.SUCCESSFULL_PERCENTAGE) { + return 'progress-bar progress-bar-striped progress-bar-warning'; + } else { + return 'progress-bar progress-bar-striped progress-bar-success'; + } + }, + + handleKeyPress: function (event) { + if (event.which === this.ALT_1_KEY_CODE) { + this.showWorkspaceTab(event); + } else if (event.which === this.ALT_2_KEY_CODE) { + this.showTab(1); + } else if (event.which === this.ALT_3_KEY_CODE) { + this.showTab(2); + } else if (event.which === this.ALT_R_KEY_CODE) { + $('#run').trigger('click'); + } else if (event.which === this.ALT_S_KEY_CODE) { + $('#assess').trigger('click'); + } else if (event.which === this.ALT_T_KEY_CODE) { + $('#test').trigger('click'); + } else { + return; + } + event.preventDefault(); + }, + + handleCopyEvent: function (text) { + this.lastCopyText = text; + }, + + handlePasteEvent: function (pasteObject) { + var same = (this.lastCopyText === pasteObject.text); + + // if the text is not copied from within the editor (from any file), send an event to lanalytics + if (!same) { + this.publishCodeOceanEvent("codeocean_editor_paste", { + text: pasteObject.text, + exercise: $('#editor').data('exercise-id'), + file_id: "1" + + }); + } + }, + + hideSpinner: function () { + $('button i.fa').show(); + $('button i.fa-spin').hide(); + }, + + resetSaveTimer: function () { + this.clearTimeout(this.autosaveTimer); + this.autosaveTimer = setTimeout(this.autosave, this.AUTOSAVE_INTERVAL); + }, + + autosave: function () { + var date = new Date(); + this.autosaveLabel.parent().css("visibility", "visible"); + this.autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds()); + this.autosaveLabel.text(date.toLocaleTimeString()); + this.autosaveTimer = null; + this.createSubmission($('#autosave'), null); + }, + + initializeEditors: function () { + $('.editor').each(function (index, element) { + var editor = ace.edit(element); + + if (this.qa_api) { + editor.getSession().on("change", function (deltaObject) { + this.qa_api.executeCommand('syncEditor', [this.active_file, deltaObject]); + }); + } + var document = editor.getSession().getDocument(); + // insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added + var file_id = $(element).data('file-id'); + var content = $('.editor-content[data-file-id=' + file_id + ']'); + this.setActiveFile($(element).parent().data('filename'), file_id); + + document.insertLines(0, content.text().split(/\n/)); + // remove last (empty) that is there by default line + document.removeLines(document.getLength() - 1, document.getLength() - 1); + editor.setReadOnly($(element).data('read-only') !== undefined); + editor.setShowPrintMargin(false); + editor.setTheme(this.THEME); + editor.commands.bindKey("ctrl+alt+0", null); + this.editors.push(editor); + this.editor_for_file.set($(element).parent().data('filename'), editor); + var session = editor.getSession(); + session.setMode($(element).data('mode')); + session.setTabSize($(element).data('indent-size')); + session.setUseSoftTabs(true); + session.setUseWrapMode(true); + + // set regex for parsing error traces based on the mode of the main file. + if ($(element).parent().data('role') == "main_file") { + this.tracepositions_regex = this.regex_for_language.get($(element).data('mode')); + } + + var file_id = $(element).data('id'); + + /* + * Register event handlers + */ + + // editor itself + editor.on("paste", this.handlePasteEvent); + editor.on("copy", this.handleCopyEvent); + + // listener for autosave + session.on("change", function (deltaObject) { + this.resetSaveTimer(); + }.bind(this)); + }); + }, + + initializeEventHandlers: function () { + $(document).on('click', '#results a', this.showOutput); + $(document).on('keypress', this.handleKeyPress); + $('a[data-toggle="tab"]').on('show.bs.tab', this.storeTab); + this.initializeFileTreeButtons(); + this.initializeWorkflowButtons(); + this.initializeWorkspaceButtons(); + this.initializeRequestForComments() + }, + + initializeFileTree: function () { + $('#files').jstree($('#files').data('entries')); + $('#files').on('click', 'li.jstree-leaf', function () { + active_file = { + filename: $(this).text(), + id: parseInt($(this).attr('id')) + }; + var frame = $('[data-file-id="' + active_file.id + '"]').parent(); + this.showFrame(frame); + this.toggleButtonStates(); + }); + }, + + initializeFileTreeButtons: function () { + $('#create-file').on('click', this.showFileDialog); + $('#destroy-file').on('click', this.confirmDestroy); + $('#download').on('click', this.downloadCode); + $('#request-for-comments').on('click', this.requestComments); + }, + + initializeRegexes: function () { + this.regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g); + this.regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g); + }, + + initializeTooltips: function () { + $('[data-tooltip]').tooltip(); + }, + + initializeWorkflowButtons: function () { + $('#start').on('click', this.showWorkspaceTab); + //$('#submit').on('click', confirmSubmission); + $('#submit').on('click', this.submitCode); + }, + + initializeWorkspaceButtons: function () { + $('#assess').on('click', this.scoreCode); // todo + $('#dropdown-render, #render').on('click', this.renderCode); + $('#dropdown-run, #run').on('click', this.runCode); + $('#dropdown-stop, #stop').on('click', this.stopCode); // todo + $('#dropdown-test, #test').on('click', this.testCode); // todo + $('#save').on('click', this.saveCode); + $('#start-over').on('click', this.confirmReset); + }, + + initializeRequestForComments: function () { + var button = $('.requestCommentsButton'); + button.hide(); + button.on('click', function () { + $('#comment-modal').modal('show'); + }); + + $('#askForCommentsButton').on('click', this.requestComments); + + setTimeout(function () { + button.fadeIn(); + }, this.REQUEST_FOR_COMMENTS_DELAY); + }, + + isActiveFileRenderable: function () { + return 'renderable' in this.active_frame.data(); + }, + + isActiveFileRunnable: function () { + return this.isActiveFileExecutable() && ['main_file', 'user_defined_file'].includes(this.active_frame.data('role')); + }, + + isActiveFileStoppable: function () { + return this.isActiveFileRunnable() && this.running; + }, + + isActiveFileSubmission: function () { + return ['Submission'].includes(this.active_frame.data('contextType')); + }, + + isActiveFileTestable: function () { + return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test'].includes(this.active_frame.data('role')); + }, + + isBrowserSupported: function () { + // websockets is used for run, score and test + return Modernizr.websockets; + }, + + populatePanel: function (panel, result, index) { + panel.removeClass('panel-default').addClass(this.getPanelClass(result)); + panel.find('.panel-title .filename').text(result.filename); + panel.find('.panel-title .number').text(index + 1); + panel.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed); + panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count); + panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text((result.score * result.weight).toFixed(2)); + panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight); + panel.find('.row .col-sm-9').eq(2).text(result.message); + if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(', ')); + panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index); + }, + + publishCodeOceanEvent: function (eventName, contextData) { + var payload = { + user: { + type: 'User', + uuid: $('#editor').data('user-id') + }, + verb: { + type: eventName + }, + resource: { + type: 'page', + uuid: document.location.href + }, + timestamp: new Date().toISOString(), + with_result: {}, + in_context: contextData + }; + + $.ajax("https://open.hpi.de/lanalytics/log", { + type: 'POST', + cache: false, + dataType: 'JSON', + data: payload, + success: {}, + error: {} + }) + }, + + sendError: function (message, submission_id) { + this.showSpinner($('#render')); + var jqxhr = this.ajax({ + data: { + error: { + message: message, + submission_id: submission_id + } + }, + url: $('#editor').data('errors-url') + }); + jqxhr.always(this.hideSpinner); + jqxhr.success(this.renderHint); + }, + + toggleButtonStates: function () { + $('#destroy-file').prop('disabled', active_frame.data('role') !== 'user_defined_file'); + $('#dropdown-render').toggleClass('disabled', !this.isActiveFileRenderable()); + $('#dropdown-run').toggleClass('disabled', !this.isActiveFileRunnable() || this.running); + $('#dropdown-stop').toggleClass('disabled', !this.isActiveFileStoppable()); + $('#dropdown-test').toggleClass('disabled', !this.isActiveFileTestable()); + $('#dummy').toggle(!this.fileActionsAvailable()); + $('#editor-buttons .dropdown-toggle').toggle(this.fileActionsAvailable()); + $('#render').toggle(this.isActiveFileRenderable()); + $('#run').toggle(this.isActiveFileRunnable() && !this.running); + $('#stop').toggle(this.isActiveFileStoppable()); + $('#test').toggle(this.isActiveFileTestable()); + }, + + jumpToSourceLine: function (event) { + var file = $(event.target).data('file'); + var line = $(event.target).data('line'); + + this.showWorkspaceTab(null); + // set active file ?!?! + + var frame = $('div.frame[data-filename="' + file + '"]'); + this.showFrame(frame); + + var editor = editor_for_file.get(file); + editor.gotoLine(line, 0); + + }, + + augmentStacktraceInOutput: function () { + if (this.tracepositions_regex) { + var element = $('#output>pre'); + var text = element.text(); + element.on("click", "a", this.jumpToSourceLine); + + var matches; + + while (matches = this.tracepositions_regex.exec(text)) { + var frame = $('div.frame[data-filename="' + matches[1] + '"]') + + if (frame.length > 0) { + element.html(text.replace(matches[0], "" + matches[0] + "")); + } + } + } + }, + + storeTab: function (event) { + localStorage.tab = $(event.target).parent().index(); + }, + + resetOutputTab: function () { + this.clearOutput(); + $('#hint').fadeOut(); + $('#flowrHint').fadeOut(); + this.showTab(1); + }, + + isActiveFileBinary: function () { + return 'binary' in this.active_frame.data(); + }, + + isActiveFileExecutable: function () { + return 'executable' in this.active_frame.data(); + }, + + setActiveFile: function (filename, fileId) { + this.active_file = { + filename: filename, + id: fileId + }; + }, + + closeEventSource: function (event) { + event.target.close(); + this.hideSpinner(); + this.running = false; + this.toggleButtonStates(); + + if (event.type === 'error' || JSON.parse(event.data).code !== 200) { + this.ajaxError(); + this.showTab(0); + } + }, + + showRequestedTab: function() { + if(this.REMEMBER_TAB){ + var regexp = /tab=(\d+)/; + if (regexp.test(window.location.search)) { + var index = regexp.exec(window.location.search)[1] - 1; + } else { + var index = this.localStorage.tab; + } + } else { + // else start with first tab. + var index = 0; + } + this.showTab(index); + }, + + showSpinner: function(initiator) { + $(initiator).find('i.fa').hide(); + $(initiator).find('i.fa-spin').show(); + }, + + showStatus: function(output) { + if (output.status === 'timeout') { + this.showTimeoutMessage(); + } else if (output.status === 'container_depleted') { + this.showContainerDepletedMessage(); + } else if (output.stderr) { + $.flash.danger({ + icon: ['fa', 'fa-bug'], + text: $('#run').data('message-failure') + }); + } + }, + + showContainerDepletedMessage: function() { + $.flash.danger({ + icon: ['fa', 'fa-clock-o'], + text: $('#editor').data('message-depleted') + }); + }, + + showTab: function(index) { + $('a[data-toggle="tab"]').eq(index || 0).tab('show'); + }, + + showTimeoutMessage: function() { + $.flash.info({ + icon: ['fa', 'fa-clock-o'], + text: $('#editor').data('message-timeout') + }); + }, + + showWebsocketError: function() { + $.flash.danger({ + text: $('#flash').data('message-failure') + }); + }, + + showWorkspaceTab: function(event) { + if(event){ + event.preventDefault(); + } + this.showTab(0); + }, + + showFileDialog: function(event) { + event.preventDefault(); + this.createSubmission(this, null, function(response) { + $('#code_ocean_file_context_id').val(response.id); + $('#modal-file').modal('show'); + }); + }, + + initializeEverything: function() { + this.initializeRegexes(); + this.initializeCodePilot(); + $('.score, #development-environment').show(); + this.configureEditors(); + this.initializeEditors(); + this.initializeEventHandlers(); + this.initializeFileTree(); + this.initializeTooltips(); + this.initPrompt(); + this.renderScore(); + this.showFirstFile(); + this.showRequestedTab(); + + $(window).on("beforeunload", function() { + if(this.autosaveTimer != null){ + this.autosave(); + } + }.bind(this)); + } +}; \ 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..aa1933e4 --- /dev/null +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -0,0 +1,266 @@ +CodeOceanEditorEvaluation = { + chunkBuffer: [{streamedResponse: true}], + + evaluateCode: function (url, streamed, callback) { + (streamed ? this.evaluateCodeWithStreamedResponse : this.evaluateCodeWithoutStreamedResponse)(url, callback); + }, + + evaluateCodeWithStreamedResponse: function (url, onmessageFunction) { + this.initWebsocketConnection(url, onmessageFunction); + + // TODO only init turtle when required + this.initTurtle(); + }, + + evaluateCodeWithoutStreamedResponse: function (url, callback) { + var jqxhr = this.ajax({ + method: 'GET', + url: url + }); + jqxhr.always(this.hideSpinner); + jqxhr.done(callback); + jqxhr.fail(this.ajaxError); + }, + + handleScoringResponse: function (websocket_event) { + var results = JSON.parse(websocket_event.data); + this.printScoringResults(results); + var score = _.reduce(results, function (sum, result) { + return sum + result.score * result.weight; + }, 0).toFixed(2); + $('#score').data('score', score); + this.renderScore(); + this.showTab(2); + }, + + handleTestResponse: function (websocket_event) { + var result = JSON.parse(websocket_event.data); + this.clearOutput(); + this.printOutput(result, false, 0); + if (this.qa_api) { + this.qa_api.executeCommand('syncOutput', [result]); + } + this.showStatus(result); + this.showTab(1); + }, + + printOutput: function (output, colorize, index) { + var element = this.findOrCreateOutputElement(index); + if (!colorize) { + if (output.stdout != undefined && output.stdout != '') { + element.append(output.stdout) + } + + if (output.stderr != undefined && output.stderr != '') { + element.append('There was an error: StdErr: ' + output.stderr); + } + + } else if (output.stderr) { + element.addClass('text-warning').append(output.stderr); + this.flowrOutputBuffer += output.stderr; + this.QaApiOutputBuffer.stderr += output.stderr; + } else if (output.stdout) { + element.addClass('text-success').append(output.stdout); + this.flowrOutputBuffer += output.stdout; + this.QaApiOutputBuffer.stdout += output.stdout; + } else { + element.addClass('text-muted').text($('#output').data('message-no-output')); + } + }, + + printScoringResult: function (result, index) { + $('#results').show(); + var panel = $('#dummies').children().first().clone(); + this.populatePanel(panel, result, index); + $('#results ul').first().append(panel); + }, + + printScoringResults: function (response) { + $('#results ul').first().html(''); + $('.test-count .number').html(response.length); + this.clearOutput(); + + _.each(response, function (result, index) { + this.printOutput(result, false, index); + this.printScoringResult(result, index); + }); + + if (_.some(response, function (result) { + return result.status === 'timeout'; + })) { + this.showTimeoutMessage(); + } + if (_.some(response, function (result) { + return result.status === 'container_depleted'; + })) { + this.showContainerDepletedMessage(); + } + if (this.qa_api) { + // send test response to QA + this.qa_api.executeCommand('syncOutput', [response]); + } + }, + + renderHint: function (object) { + var hint = object.data || object.hint; + if (hint) { + $('#hint .panel-body').text(hint); + $('#hint').fadeIn(); + } + }, + + renderScore: function () { + var score = parseFloat($('#score').data('score')); + var maximum_score = parseFloat($('#score').data('maximum-score')); + if (score >= 0 && score <= maximum_score && maximum_score > 0) { + var percentage_score = (score / maximum_score * 100 ).toFixed(0); + $('.score').html(percentage_score + '%'); + } + else { + $('.score').html(0 + '%'); + } + this.renderProgressBar(score, maxium_score); + }, + + scoreCode: function (event) { + event.preventDefault(); + runmode = this.SERVER_SEND_EVENT; + this.createSubmission(this, null, function (response) { + showSpinner($('#assess')); + var url = response.score_url; + this.evaluateCode(url, true, this.handleScoringResponse); + }); + }, + + stopCode: function (event) { + event.preventDefault(); + if ($('#stop').is(':visible')) { + if (this.runmode == this.WEBSOCKET) { + killWebsocketAndContainer(); + } else if (this.runmode == this.SERVER_SEND_EVENT) { + stopCodeServerSendEvent(event); + } + this.runmode = this.NONE; + } + }, + + stopCodeServerSendEvent: function (event) { + var jqxhr = this.ajax({ + data: { + container_id: $('#stop').data('container').id + }, + url: $('#stop').data('url') + }); + jqxhr.always(function () { + this.hideSpinner(); + this.running = false; + this.toggleButtonStates(); + }); + jqxhr.fail(ajaxError); + }, + + killWebsocketAndContainer: function () { + if (this.websocket.readyState != WebSocket.OPEN) { + return; + } + + this.websocket.send(JSON.stringify({cmd: 'exit'})); + this.websocket.flush(); + this.websocket.close(); + this.hideSpinner(); + this.running = false; + this.toggleButtonStates(); + this.hidePrompt(); + }, + + // todo set this from websocket command, required to e.g. stop container + storeContainerInformation: function (event) { + var container_information = JSON.parse(event.data); + $('#stop').data('container', container_information); + + if (_.size(container_information.port_bindings) > 0) { + $.flash.info({ + icon: ['fa', 'fa-exchange'], + text: _.map(container_information.port_bindings, function (key, value) { + var url = window.location.protocol + '//' + window.location.hostname + ':' + key; + return $('#run').data('message-network').replace('%{port}', value).replace(/%{address}/g, url); + }).join('\n') + }); + } + }, + + //TODO: Move Prompt Part in own component + showPrompt: function(msg) { + var label = $('#prompt .input-group-addon'); + label.text(msg.data || label.data('prompt')); + if (this.prompt.isPresent() && this.prompt.hasClass('hidden')) { + this.prompt.removeClass('hidden'); + } + $('#prompt input').focus(); + }, + + hidePrompt: function() { + if (this.prompt.isPresent() && !this.prompt.hasClass('hidden')) { + this.prompt.addClass('hidden'); + } + }, + + initPrompt: function() { + if ($('#run').isPresent()) { + $('#run').bind('click', this.hidePrompt); + } + if ($('#prompt').isPresent()) { + $('#prompt').on('keypress', this.handlePromptKeyPress); + $('#prompt-submit').on('click', this.submitPromptInput); + } + }, + + submitPromptInput: function() { + var input = $('#prompt-input'); + var message = input.val(); + this.websocket.send(JSON.stringify({cmd: 'result', 'data': message})); + this.websocket.flush(); + input.val(''); + this.hidePrompt(); + }, + + handlePromptKeyPress: function(evt) { + if (evt.which === this.ENTER_KEY_CODE) { + this.submitPromptInput(); + } + }, + + renderWebsocketOutput: function(msg){ + var element = this.findOrCreateRenderElement(0); + element.append(msg.data); + }, + + printWebsocketOutput: function(msg) { + if (!msg.data) { + return; + } + msg.data = msg.data.replace(/(\r)/gm, "\n"); + var stream = {}; + stream[msg.stream] = msg.data; + this.printOutput(stream, true, 0); + }, + + clearOutput: function() { + $('#output pre').remove(); + }, + + printChunk: function(event) { + var output = JSON.parse(event.data); + if (output) { + this.printOutput(output, true, 0); + // send test response to QA + // we are expecting an array of outputs: + if (this.qa_api) { + this.chunkBuffer.push(output); + } + } else { + this.resetOutputTab(); + } + }, + +}; diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb new file mode 100644 index 00000000..66b8fcda --- /dev/null +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -0,0 +1,91 @@ +CodeOceanEditorFlowr = { + isFlowrEnabled: true, + + handleStderrOutputForFlowr: function () { + if (!this.isFlowrEnabled) return; + + var flowrUrl = $('#flowrHint').data('url'); + var flowrHintBody = $('#flowrHint .panel-body'); + var queryParameters = { + query: this.flowrOutputBuffer + } + + flowrHintBody.empty(); + + jQuery.getJSON(flowrUrl, queryParameters, function (data) { + jQuery.each(data.queryResults, function (index, question) { + var collapsibleTileHtml = this.flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + question).replace(/{{headingId}}/g, 'heading-' + question); + var resultTile = $(collapsibleTileHtml); + + resultTile.find('h4 > a').text(question.title + ' | Found via ' + question.source); + resultTile.find('.panel-body').html(question.body); + resultTile.find('.panel-body').append('Open this question'); + + flowrHintBody.append(resultTile); + }); + + if (data.queryResults.length !== 0) { + $('#flowrHint').fadeIn(); + } + }); + + this.flowrOutputBuffer = ''; + }, + + requestComments: function () { + var user_id = $('#editor').data('user-id'); + var exercise_id = $('#editor').data('exercise-id'); + var file_id = $('.editor').data('id'); + var question = $('#question').val(); + + var createRequestForComments = function (submission) { + $.ajax({ + method: 'POST', + url: '/request_for_comments', + data: { + request_for_comment: { + exercise_id: exercise_id, + file_id: file_id, + submission_id: submission.id, + question: question + } + } + }).done(function () { + this.hideSpinner(); + $.flash.success({text: $('#askForCommentsButton').data('message-success')}); + }).error(this.ajaxError); + } + + this.createSubmission($('.requestCommentsButton'), null, createRequestForComments); + + $('#comment-modal').modal('hide'); + var button = $('.requestCommentsButton'); + button.fadeOut(); + }, + + //tODO move codepilot out of here. + initializeCodePilot: function () { + if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) { + $('#editor-column').addClass('col-md-8').removeClass('col-md-10'); + $('#questions-column').addClass('col-md-3'); + + var node = document.getElementById('questions-holder'); + var url = $('#questions-holder').data('url'); + + this.qa_api = new QaApi(node, url); + } + }, + + handleQaApiOutput: function () { + if (this.qa_api) { + this.qa_api.executeCommand('syncOutput', [[this.QaApiOutputBuffer]]); + // reset the object + } + this.QaApiOutputBuffer = {'stdout': '', 'stderr': ''}; + }, + + handleStreamedResponseForCodePilot: function (event) { + this.qa_api.executeCommand('syncOutput', [this.chunkBuffer]); + this.chunkBuffer = [{streamedResponse: true}]; + } +}; \ 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..eb1d3ab9 --- /dev/null +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -0,0 +1,171 @@ + +CodeOceanEditorSubmissions = { + collectFiles: function() { + var editable_editors = _.filter(this.editors, function(editor) { + return !editor.getReadOnly(); + }); + return _.map(editable_editors, function(editor) { + return { + content: editor.getValue(), + file_id: $(editor.container).data('file-id') + }; + }); + }, + + createSubmission: function (initiator, filter, callback) { + this.showSpinner(initiator); + var jqxhr = this.ajax({ + data: { + submission: { + cause: $(initiator).data('cause') || $(initiator).prop('id'), + exercise_id: $('#editor').data('exercise-id'), + files_attributes: (filter || _.identity)(this.collectFiles()) + }, + annotations_arr: [] + }, + dataType: 'json', + method: 'POST', + url: $(initiator).data('url') || $('#editor').data('submissions-url') + }); + jqxhr.always(this.hideSpinner); + jqxhr.done(this.createSubmissionCallback); + jqxhr.done(callback); + jqxhr.fail(this.ajaxError); + }, + + createSubmissionCallback: function(data){ + // set all frames context types to submission + $('.frame').each(function(index, element) { + $(element).data('context-type', 'Submission'); + }); + + // update the ids of the editors and reload the annotations + for (var i = 0; i < this.editors.length; i++) { + + // set the data attribute to submission + //$(editors[i].container).data('context-type', 'Submission'); + + var file_id_old = $(this.editors[i].container).data('file-id'); + + // file_id_old is always set. Either it is a reference to a teacher supplied given file, or it is the actual id of a new user created file. + // This is the case, since it is set via a call to ancestor_id on the model, which returns either file_id if set, or id if it is not set. + // therefore the else part is not needed any longer... + + // if we have an file_id set (the file is a copy of a teacher supplied given file) + if (file_id_old != null){ + // if we find file_id_old (this is the reference to the base file) in the submission, this is the match + for(var j = 0; j< data.files.length; j++){ + if(data.files[j].file_id == file_id_old){ + //$(editors[i].container).data('id') = data.files[j].id; + $(this.editors[i].container).data('id', data.files[j].id ); + } + } + } + } + // toggle button states (it might be the case that the request for comments button has to be enabled + this.toggleButtonStates(); + }, + + destroyFile: function() { + this.createSubmission($('#destroy-file'), function(files) { + return _.reject(files, function(file) { + return file.file_id === active_file.id; + }); + }, window.CodeOcean.refresh); + }, + + downloadCode: function(event) { + event.preventDefault(); + this.createSubmission(this, null,function(response) { + var url = response.download_url; + + // to download just a single file, use the following url + //var url = response.download_file_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); + window.location = url; + }); + }, + + resetCode: function() { + this.showSpinner(this); + this.ajax({ + method: 'GET', + url: $('#start-over').data('url') + }).success(function(response) { + this.hideSpinner(); + _.each(this.editors, function(editor) { + var file_id = $(editor.container).data('file-id'); + var file = _.find(response.files, function(file) { + return file.id === file_id; + }); + editor.setValue(file.content); + }); + }); + }, + + renderCode: function(event) { + event.preventDefault(); + if ($('#render').is(':visible')) { + this.createSubmission(this, null, function (response) { + var url = response.render_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); + var pop_up_window = window.open(url); + if (pop_up_window) { + pop_up_window.onerror = function (message) { + this.clearOutput(); + this.printOutput({ + stderr: message + }, true, 0); + this.sendError(message, response.id); + this.showTab(1); + }; + } + }); + } + }, + + //Todo Split up in submitpart and run part + runCode: function(event) { + event.preventDefault(); + if ($('#run').is(':visible')) { + this.runmode = this.WEBSOCKET; + this.createSubmission(this, null, function(response) { + + //Run part starts here + $('#stop').data('url', response.stop_url); + this.running = true; + this.showSpinner($('#run')); + this.toggleButtonStates(); + var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); + this.evaluateCode(url, true, function(evt) { this.parseCanvasMessage(evt.data, true); }); + }); + } + }, + + saveCode: function(event) { + event.preventDefault(); + this.createSubmission(this, null, function() { + $.flash.success({ + text: $('#save').data('message-success') + }); + }); + }, + + testCode: function(event) { + event.preventDefault(); + if ($('#test').is(':visible')) { + this.createSubmission(this, null, function(response) { + this.showSpinner($('#test')); + var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); + this.evaluateCode(url, true, this.handleTestResponse); + }); + } + }, + + submitCode: function() { + this.createSubmission($('#submit'), null, function (response) { + if (response.redirect) { + localStorage.removeItem('tab'); + window.location = response.redirect; + } + }) + } +}; diff --git a/app/assets/javascripts/editor/turtle.js.erb b/app/assets/javascripts/editor/turtle.js.erb new file mode 100644 index 00000000..a6c06ed7 --- /dev/null +++ b/app/assets/javascripts/editor/turtle.js.erb @@ -0,0 +1,45 @@ +CodeOceanEditorTurtle = { + initTurtle: function () { + this.turtlescreen = new Turtle(this.websocket, this.turtlecanvas); + if ($('#run').isPresent()) { + $('#run').bind('click', this.hideCanvas); + } + }, + + handleTurtleCommand: function (msg) { + if (msg.action in this.turtlescreen) { + var result = this.turtlescreen[msg.action].apply(this.turtlescreen, msg.args); + this.websocket.send(JSON.stringify({cmd: 'result', 'result': result})); + } else { + this.websocket.send(JSON.stringify({cmd: 'exception', exception: 'AttributeError', message: msg.action})); + } + this.websocket.flush(); + }, + + handleTurtlebatchCommand: function (msg) { + for (var i = 0; i < msg.batch.length; i++) { + var cmd = msg.batch[i]; + this.turtlescreen[cmd[0]].apply(this.turtlescreen, cmd[1]); + } + }, + + showCanvas: function () { + if ($('#turtlediv').isPresent() + && this.turtlecanvas.hasClass('hidden')) { + // initialize two-column layout + $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); + this.turtlecanvas.removeClass('hidden'); + } + }, + + hideCanvas: function () { + if ($('#turtlediv').isPresent() + && !(this.turtlecanvas.hasClass('hidden'))) { + var output = $('#output-col1'); + if (output.hasClass('two-column')) { + output.removeClass('col-lg-7 col-md-7 two-column'); + } + this.turtlecanvas.addClass('hidden'); + } + }, +}; \ 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..6024549b --- /dev/null +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -0,0 +1,86 @@ +CodeOceanEditorWebsocket = { + initWebsocketConnection: function (url, onmessageFunction) { + //TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss) + //causes: Puma::HttpParserError: Invalid HTTP format, parsing fails. + //TODO: make sure that this gets cached. + this.websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url); + this.websocket.onopen = function (evt) { + this.resetOutputTab(); + }; // todo show some kind of indicator for established connection + this.websocket.onclose = function (evt) { /* expected at some point */ + }; + this.websocket.onmessage = onmessageFunction; + this.websocket.onerror = function (evt) { + this.showWebsocketError(); + }; + this.websocket.flush = function () { + this.send('\n'); + } + }, + + //ToDo: Move websocket and commands variable in here + executeWebsocketCommand: function (msg) { + if ($.inArray(msg.cmd, this.commands) == -1) { + console.log("Unknown command: " + msg.cmd); + // skipping unregistered commands is required + // as we may receive mirrored response due to internal behaviour + return; + } + switch (msg.cmd) { + case 'input': + this.showPrompt(msg); + break; + case 'write': + this.printWebsocketOutput(msg); + break; + case 'turtle': + this.showCanvas(); + this.handleTurtleCommand(msg); + break; + case 'turtlebatch': + this.showCanvas(); + this.handleTurtlebatchCommand(msg); + break; + case 'render': + this.renderWebsocketOutput(msg); + break; + case 'exit': + this.killWebsocketAndContainer(); + this.handleQaApiOutput(); + this.handleStderrOutputForFlowr(); + this.augmentStacktraceInOutput(); + break; + case 'timeout': + // just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes. + this.showTimeoutMessage(); + break; + case 'status': + this.showStatus(msg); + break; + } + }, + + parseCanvasMessage: function (message, recursive) { + var msg; + message = message.replace(/^\s+|\s+$/g, ""); + try { + // todo validate json instead of catching + msg = JSON.parse(message); + } catch (e) { + if (!recursive) { + return false; + } + // why does docker sometimes send multiple commands at once? + message = message.replace(/^\s+|\s+$/g, ""); + var messages = message.split("\n"); + for (var i = 0; i < messages.length; i++) { + if (!messages[i]) { + continue; + } + this.parseCanvasMessage(messages[i], false); + } + return; + } + this.executeWebsocketCommand(msg); + } +}; \ No newline at end of file From fbb1cfb67b02792a3b96d744f8ea1d9ed9a65721 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 12 Aug 2016 13:22:52 +0200 Subject: [PATCH 03/57] Cleaned up code. --- app/assets/javascripts/editor.js.erb | 15 ++--- app/assets/javascripts/editor/editor.js.erb | 44 +++++++-------- .../javascripts/editor/evaluation.js.erb | 56 +++++-------------- app/assets/javascripts/editor/flowr.js.erb | 2 +- .../javascripts/editor/submissions.js.erb | 31 +++++----- .../javascripts/editor/websocket.js.erb | 6 +- 6 files changed, 63 insertions(+), 91 deletions(-) diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index 0247841b..cff00332 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -1,15 +1,16 @@ $(function() { //Merge all editor components. OOP for the win. O rly. - //TODO CHange this. Otherwise it will fuck people up, + //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); - $.extend(CodeOceanEditor, CodeOceanEditorEvaluation); - $.extend(CodeOceanEditor, CodeOceanEditorFlowr); - $.extend(CodeOceanEditor, CodeOceanEditorSubmissions); - $.extend(CodeOceanEditor, CodeOceanEditorTurtle); - $.extend(CodeOceanEditor, CodeOceanEditorWebsocket); + $.extend(CodeOceanEditor, + CodeOceanEditorAJAX, + CodeOceanEditorEvaluation, + CodeOceanEditorFlowr, + CodeOceanEditorSubmissions, + CodeOceanEditorTurtle, + CodeOceanEditorWebsocket); if ($('#editor').isPresent() && CodeOceanEditor) { if (CodeOceanEditor.isBrowserSupported()) { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 62b40f51..825b1f88 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -51,7 +51,7 @@ var CodeOceanEditor = { configureEditors: function () { _.each(['modePath', 'themePath', 'workerPath'], function (attribute) { - this.ace.config.set(attribute, this.ACE_FILES_PATH); + ace.config.set(attribute, this.ACE_FILES_PATH); }.bind(this)); }, @@ -65,7 +65,7 @@ var CodeOceanEditor = { confirmReset: function (event) { event.preventDefault(); - if (confirm($(this).data('message-confirm'))) { + if (confirm($('#start-over').data('message-confirm'))) { this.resetCode(); } }, @@ -190,7 +190,7 @@ var CodeOceanEditor = { }, resetSaveTimer: function () { - this.clearTimeout(this.autosaveTimer); + clearTimeout(this.autosaveTimer); this.autosaveTimer = setTimeout(this.autosave, this.AUTOSAVE_INTERVAL); }, @@ -212,6 +212,7 @@ var CodeOceanEditor = { this.qa_api.executeCommand('syncEditor', [this.active_file, deltaObject]); }); } + var document = editor.getSession().getDocument(); // insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added var file_id = $(element).data('file-id'); @@ -245,14 +246,14 @@ var CodeOceanEditor = { */ // editor itself - editor.on("paste", this.handlePasteEvent); - editor.on("copy", this.handleCopyEvent); + 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 () { @@ -279,10 +280,10 @@ var CodeOceanEditor = { }, initializeFileTreeButtons: function () { - $('#create-file').on('click', this.showFileDialog); - $('#destroy-file').on('click', this.confirmDestroy); - $('#download').on('click', this.downloadCode); - $('#request-for-comments').on('click', this.requestComments); + $('#create-file').on('click', this.showFileDialog.bind(this)); + $('#destroy-file').on('click', this.confirmDestroy.bind(this)); + $('#download').on('click', this.downloadCode.bind(this)); + $('#request-for-comments').on('click', this.requestComments.bind(this)); }, initializeRegexes: function () { @@ -295,19 +296,18 @@ var CodeOceanEditor = { }, initializeWorkflowButtons: function () { - $('#start').on('click', this.showWorkspaceTab); - //$('#submit').on('click', confirmSubmission); - $('#submit').on('click', this.submitCode); + $('#start').on('click', this.showWorkspaceTab.bind(this)); + $('#submit').on('click', this.submitCode.bind(this)); }, initializeWorkspaceButtons: function () { - $('#assess').on('click', this.scoreCode); // todo - $('#dropdown-render, #render').on('click', this.renderCode); - $('#dropdown-run, #run').on('click', this.runCode); - $('#dropdown-stop, #stop').on('click', this.stopCode); // todo - $('#dropdown-test, #test').on('click', this.testCode); // todo - $('#save').on('click', this.saveCode); - $('#start-over').on('click', this.confirmReset); + $('#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)); }, initializeRequestForComments: function () { @@ -406,7 +406,7 @@ var CodeOceanEditor = { }, toggleButtonStates: function () { - $('#destroy-file').prop('disabled', active_frame.data('role') !== 'user_defined_file'); + $('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file'); $('#dropdown-render').toggleClass('disabled', !this.isActiveFileRenderable()); $('#dropdown-run').toggleClass('disabled', !this.isActiveFileRunnable() || this.running); $('#dropdown-stop').toggleClass('disabled', !this.isActiveFileStoppable()); @@ -556,7 +556,7 @@ var CodeOceanEditor = { showFileDialog: function(event) { event.preventDefault(); - this.createSubmission(this, null, function(response) { + this.createSubmission('#create-file', null, function(response) { $('#code_ocean_file_context_id').val(response.id); $('#modal-file').modal('show'); }); diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index aa1933e4..8d877a77 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -1,27 +1,13 @@ CodeOceanEditorEvaluation = { chunkBuffer: [{streamedResponse: true}], - evaluateCode: function (url, streamed, callback) { - (streamed ? this.evaluateCodeWithStreamedResponse : this.evaluateCodeWithoutStreamedResponse)(url, callback); - }, - - evaluateCodeWithStreamedResponse: function (url, onmessageFunction) { + evaluateCode: function (url, onmessageFunction) { this.initWebsocketConnection(url, onmessageFunction); // TODO only init turtle when required this.initTurtle(); }, - evaluateCodeWithoutStreamedResponse: function (url, callback) { - var jqxhr = this.ajax({ - method: 'GET', - url: url - }); - jqxhr.always(this.hideSpinner); - jqxhr.done(callback); - jqxhr.fail(this.ajaxError); - }, - handleScoringResponse: function (websocket_event) { var results = JSON.parse(websocket_event.data); this.printScoringResults(results); @@ -83,7 +69,7 @@ CodeOceanEditorEvaluation = { _.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'; @@ -119,26 +105,26 @@ CodeOceanEditorEvaluation = { else { $('.score').html(0 + '%'); } - this.renderProgressBar(score, maxium_score); + this.renderProgressBar(score, maximum_score); }, scoreCode: function (event) { event.preventDefault(); - runmode = this.SERVER_SEND_EVENT; - this.createSubmission(this, null, function (response) { - showSpinner($('#assess')); + this.runmode = this.SERVER_SEND_EVENT; + this.createSubmission('#assess', null, function (response) { + this.showSpinner($('#assess')); var url = response.score_url; - this.evaluateCode(url, true, this.handleScoringResponse); - }); + this.evaluateCode(url, this.handleScoringResponse.bind(this)); + }.bind(this)); }, stopCode: function (event) { event.preventDefault(); if ($('#stop').is(':visible')) { if (this.runmode == this.WEBSOCKET) { - killWebsocketAndContainer(); + this.killWebsocketAndContainer(); } else if (this.runmode == this.SERVER_SEND_EVENT) { - stopCodeServerSendEvent(event); + this.stopCodeServerSendEvent(event); } this.runmode = this.NONE; } @@ -207,11 +193,11 @@ CodeOceanEditorEvaluation = { initPrompt: function() { if ($('#run').isPresent()) { - $('#run').bind('click', this.hidePrompt); + $('#run').bind('click', this.hidePrompt.bind(this)); } if ($('#prompt').isPresent()) { - $('#prompt').on('keypress', this.handlePromptKeyPress); - $('#prompt-submit').on('click', this.submitPromptInput); + $('#prompt').on('keypress', this.handlePromptKeyPress.bind(this)); + $('#prompt-submit').on('click', this.submitPromptInput.bind(this)); } }, @@ -247,20 +233,6 @@ CodeOceanEditorEvaluation = { clearOutput: function() { $('#output pre').remove(); - }, - - printChunk: function(event) { - var output = JSON.parse(event.data); - if (output) { - this.printOutput(output, true, 0); - // send test response to QA - // we are expecting an array of outputs: - if (this.qa_api) { - this.chunkBuffer.push(output); - } - } else { - this.resetOutputTab(); - } - }, + } }; diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index 66b8fcda..57cc742b 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -54,7 +54,7 @@ CodeOceanEditorFlowr = { this.hideSpinner(); $.flash.success({text: $('#askForCommentsButton').data('message-success')}); }).error(this.ajaxError); - } + }; this.createSubmission($('.requestCommentsButton'), null, createRequestForComments); diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index eb1d3ab9..f2712f02 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -27,10 +27,10 @@ CodeOceanEditorSubmissions = { method: 'POST', url: $(initiator).data('url') || $('#editor').data('submissions-url') }); - jqxhr.always(this.hideSpinner); - jqxhr.done(this.createSubmissionCallback); - jqxhr.done(callback); - jqxhr.fail(this.ajaxError); + jqxhr.always(this.hideSpinner.bind(this)); + jqxhr.done(this.createSubmissionCallback.bind(this)); + jqxhr.done(callback.bind(this)); + jqxhr.fail(this.ajaxError.bind(this)); }, createSubmissionCallback: function(data){ @@ -76,7 +76,7 @@ CodeOceanEditorSubmissions = { downloadCode: function(event) { event.preventDefault(); - this.createSubmission(this, null,function(response) { + this.createSubmission('#download', null,function(response) { var url = response.download_url; // to download just a single file, use the following url @@ -98,14 +98,14 @@ CodeOceanEditorSubmissions = { return file.id === file_id; }); editor.setValue(file.content); - }); - }); + }.bind(this)); + }.bind(this)); }, renderCode: function(event) { event.preventDefault(); if ($('#render').is(':visible')) { - this.createSubmission(this, null, function (response) { + 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) { @@ -127,22 +127,21 @@ CodeOceanEditorSubmissions = { event.preventDefault(); if ($('#run').is(':visible')) { this.runmode = this.WEBSOCKET; - this.createSubmission(this, null, function(response) { - + this.createSubmission('#run', null, function(response) { //Run part starts here $('#stop').data('url', response.stop_url); this.running = true; this.showSpinner($('#run')); this.toggleButtonStates(); var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); - this.evaluateCode(url, true, function(evt) { this.parseCanvasMessage(evt.data, true); }); - }); + this.evaluateCode(url, function(evt) { this.parseCanvasMessage(evt.data, true); }.bind(this)); + }.bind(this)); } }, saveCode: function(event) { event.preventDefault(); - this.createSubmission(this, null, function() { + this.createSubmission('#save', null, function() { $.flash.success({ text: $('#save').data('message-success') }); @@ -152,11 +151,11 @@ CodeOceanEditorSubmissions = { testCode: function(event) { event.preventDefault(); if ($('#test').is(':visible')) { - this.createSubmission(this, null, function(response) { + this.createSubmission('#test', null, function(response) { this.showSpinner($('#test')); var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); - this.evaluateCode(url, true, this.handleTestResponse); - }); + this.evaluateCode(url, this.handleTestResponse.bind(this)); + }.bind(this)); } }, diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js.erb index 6024549b..ee6928d7 100644 --- a/app/assets/javascripts/editor/websocket.js.erb +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -6,13 +6,13 @@ CodeOceanEditorWebsocket = { this.websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url); this.websocket.onopen = function (evt) { this.resetOutputTab(); - }; // todo show some kind of indicator for established connection + }.bind(this); // todo show some kind of indicator for established connection this.websocket.onclose = function (evt) { /* expected at some point */ - }; + }.bind(this); this.websocket.onmessage = onmessageFunction; this.websocket.onerror = function (evt) { this.showWebsocketError(); - }; + }.bind(this); this.websocket.flush = function () { this.send('\n'); } From 0ca52a9b8f2434a951c857a38fd4eb38a1ceef67 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 12 Aug 2016 14:42:35 +0200 Subject: [PATCH 04/57] Fixed turtle. --- app/assets/javascripts/editor/editor.js.erb | 16 +------------ .../javascripts/editor/evaluation.js.erb | 23 ++----------------- app/assets/javascripts/editor/flowr.js.erb | 5 ---- app/assets/javascripts/editor/turtle.js.erb | 20 ++++++++++++---- .../javascripts/editor/websocket.js.erb | 3 +++ 5 files changed, 22 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 825b1f88..eb5562c0 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -31,9 +31,7 @@ var CodeOceanEditor = { runmode: this.NONE, websocket: null, - turtlescreen: null, numMessages: 0, - turtlecanvas: $('#turtlecanvas'), prompt: $('#prompt'), commands: ['input', 'write', 'turtle', 'turtlebatch', 'render', 'exit', 'timeout', 'status'], streams: ['stdin', 'stdout', 'stderr'], @@ -191,7 +189,7 @@ var CodeOceanEditor = { resetSaveTimer: function () { clearTimeout(this.autosaveTimer); - this.autosaveTimer = setTimeout(this.autosave, this.AUTOSAVE_INTERVAL); + this.autosaveTimer = setTimeout(this.autosave.bind(this), this.AUTOSAVE_INTERVAL); }, autosave: function () { @@ -478,18 +476,6 @@ var CodeOceanEditor = { }; }, - closeEventSource: function (event) { - event.target.close(); - this.hideSpinner(); - this.running = false; - this.toggleButtonStates(); - - if (event.type === 'error' || JSON.parse(event.data).code !== 200) { - this.ajaxError(); - this.showTab(0); - } - }, - showRequestedTab: function() { if(this.REMEMBER_TAB){ var regexp = /tab=(\d+)/; diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 8d877a77..a062e4e5 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -2,10 +2,7 @@ CodeOceanEditorEvaluation = { chunkBuffer: [{streamedResponse: true}], evaluateCode: function (url, onmessageFunction) { - this.initWebsocketConnection(url, onmessageFunction); - - // TODO only init turtle when required - this.initTurtle(); + this.initWebsocketConnection(url, onmessageFunction);; }, handleScoringResponse: function (websocket_event) { @@ -120,7 +117,7 @@ CodeOceanEditorEvaluation = { stopCode: function (event) { event.preventDefault(); - if ($('#stop').is(':visible')) { + if (this.isActiveFileStoppable()) { if (this.runmode == this.WEBSOCKET) { this.killWebsocketAndContainer(); } else if (this.runmode == this.SERVER_SEND_EVENT) { @@ -159,22 +156,6 @@ CodeOceanEditorEvaluation = { this.hidePrompt(); }, - // todo set this from websocket command, required to e.g. stop container - storeContainerInformation: function (event) { - var container_information = JSON.parse(event.data); - $('#stop').data('container', container_information); - - if (_.size(container_information.port_bindings) > 0) { - $.flash.info({ - icon: ['fa', 'fa-exchange'], - text: _.map(container_information.port_bindings, function (key, value) { - var url = window.location.protocol + '//' + window.location.hostname + ':' + key; - return $('#run').data('message-network').replace('%{port}', value).replace(/%{address}/g, url); - }).join('\n') - }); - } - }, - //TODO: Move Prompt Part in own component showPrompt: function(msg) { var label = $('#prompt .input-group-addon'); diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index 57cc742b..6440a0b5 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -83,9 +83,4 @@ CodeOceanEditorFlowr = { } this.QaApiOutputBuffer = {'stdout': '', 'stderr': ''}; }, - - handleStreamedResponseForCodePilot: function (event) { - this.qa_api.executeCommand('syncOutput', [this.chunkBuffer]); - this.chunkBuffer = [{streamedResponse: true}]; - } }; \ No newline at end of file diff --git a/app/assets/javascripts/editor/turtle.js.erb b/app/assets/javascripts/editor/turtle.js.erb index a6c06ed7..87e5bdc8 100644 --- a/app/assets/javascripts/editor/turtle.js.erb +++ b/app/assets/javascripts/editor/turtle.js.erb @@ -1,11 +1,20 @@ CodeOceanEditorTurtle = { + turtlecanvas: null, + turtlescreen: null, + resetTurtle: true, + initTurtle: function () { - this.turtlescreen = new Turtle(this.websocket, this.turtlecanvas); - if ($('#run').isPresent()) { - $('#run').bind('click', this.hideCanvas); + 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) { if (msg.action in this.turtlescreen) { var result = this.turtlescreen[msg.action].apply(this.turtlescreen, msg.args); @@ -29,6 +38,7 @@ CodeOceanEditorTurtle = { // initialize two-column layout $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); this.turtlecanvas.removeClass('hidden'); + //this.turtlecanvas.parent().parent().removeClass('hidden'); } }, @@ -40,6 +50,8 @@ CodeOceanEditorTurtle = { output.removeClass('col-lg-7 col-md-7 two-column'); } this.turtlecanvas.addClass('hidden'); + //this.turtlecanvas.parent().parent().addClass('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 index ee6928d7..f9d5466a 100644 --- a/app/assets/javascripts/editor/websocket.js.erb +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -34,10 +34,12 @@ CodeOceanEditorWebsocket = { this.printWebsocketOutput(msg); break; case 'turtle': + this.initTurtle(); this.showCanvas(); this.handleTurtleCommand(msg); break; case 'turtlebatch': + this.initTurtle(); this.showCanvas(); this.handleTurtlebatchCommand(msg); break; @@ -49,6 +51,7 @@ CodeOceanEditorWebsocket = { this.handleQaApiOutput(); this.handleStderrOutputForFlowr(); this.augmentStacktraceInOutput(); + this.cleanUpTurtle(); 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. From c04fc85c3ba2903f2ac35a0ce71b6bb4dd2f296c Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 12 Aug 2016 14:45:08 +0200 Subject: [PATCH 05/57] Added docker.yml.erb to gitignore. Added docker.yml.erb.example --- .gitignore | 1 + config/docker.yml.erb.example | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 config/docker.yml.erb.example diff --git a/.gitignore b/.gitignore index d39c25e5..4e7f1527 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 /coverage /log 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) %> From 97c1c5e455a68f59fc45468fbd5168bfe9f5fbdf Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 12 Aug 2016 14:50:49 +0200 Subject: [PATCH 06/57] Fixed context-error in jump to source line. --- app/assets/javascripts/editor/editor.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index eb5562c0..e0c64faa 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -427,7 +427,7 @@ var CodeOceanEditor = { var frame = $('div.frame[data-filename="' + file + '"]'); this.showFrame(frame); - var editor = editor_for_file.get(file); + var editor = this.editor_for_file.get(file); editor.gotoLine(line, 0); }, @@ -436,7 +436,7 @@ var CodeOceanEditor = { if (this.tracepositions_regex) { var element = $('#output>pre'); var text = element.text(); - element.on("click", "a", this.jumpToSourceLine); + element.on("click", "a", this.jumpToSourceLine.bind(this)); var matches; From aec7c593d84e3b4631ce1280f2b3d787030afa88 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Wed, 17 Aug 2016 17:25:27 +0200 Subject: [PATCH 07/57] Moved sockets. Fixed sockets. --- app/assets/javascripts/editor/editor.js.erb | 10 +- .../javascripts/editor/evaluation.js.erb | 12 +- .../javascripts/editor/execution.js.erb | 46 +++++ .../javascripts/editor/submissions.js.erb | 4 +- app/assets/javascripts/editor/turtle.js.erb | 4 + .../javascripts/editor/websocket.js.erb | 177 +++++++++--------- 6 files changed, 147 insertions(+), 106 deletions(-) create mode 100644 app/assets/javascripts/editor/execution.js.erb diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index e0c64faa..b16892ff 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -30,11 +30,8 @@ var CodeOceanEditor = { output_mode_is_streaming: true, runmode: this.NONE, - websocket: null, numMessages: 0, prompt: $('#prompt'), - commands: ['input', 'write', 'turtle', 'turtlebatch', 'render', 'exit', 'timeout', 'status'], - streams: ['stdin', 'stdout', 'stderr'], lastCopyText: null, autosaveTimer: null, @@ -42,7 +39,6 @@ var CodeOceanEditor = { ENTER_KEY_CODE: 13, - flowrOutputBuffer: "", QaApiOutputBuffer: {'stdout': '', 'stderr': ''}, flowrResultHtml: '
', @@ -255,9 +251,9 @@ var CodeOceanEditor = { }, initializeEventHandlers: function () { - $(document).on('click', '#results a', this.showOutput); - $(document).on('keypress', this.handleKeyPress); - $('a[data-toggle="tab"]').on('show.bs.tab', this.storeTab); + $(document).on('click', '#results a', this.showOutput.bind(this)); + $(document).on('keypress', this.handleKeyPress.bind(this)); + $('a[data-toggle="tab"]').on('show.bs.tab', this.storeTab.bind(this)); this.initializeFileTreeButtons(); this.initializeWorkflowButtons(); this.initializeWorkspaceButtons(); diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index a062e4e5..0f944239 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -1,12 +1,7 @@ CodeOceanEditorEvaluation = { chunkBuffer: [{streamedResponse: true}], - evaluateCode: function (url, onmessageFunction) { - this.initWebsocketConnection(url, onmessageFunction);; - }, - - handleScoringResponse: function (websocket_event) { - var results = JSON.parse(websocket_event.data); + handleScoringResponse: function (results) { this.printScoringResults(results); var score = _.reduce(results, function (sum, result) { return sum + result.score * result.weight; @@ -16,8 +11,7 @@ CodeOceanEditorEvaluation = { this.showTab(2); }, - handleTestResponse: function (websocket_event) { - var result = JSON.parse(websocket_event.data); + handleTestResponse: function (result) { this.clearOutput(); this.printOutput(result, false, 0); if (this.qa_api) { @@ -111,7 +105,7 @@ CodeOceanEditorEvaluation = { this.createSubmission('#assess', null, function (response) { this.showSpinner($('#assess')); var url = response.score_url; - this.evaluateCode(url, this.handleScoringResponse.bind(this)); + this.initializeSocketForScoring(url); }.bind(this)); }, diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb new file mode 100644 index 00000000..53b53ff7 --- /dev/null +++ b/app/assets/javascripts/editor/execution.js.erb @@ -0,0 +1,46 @@ +CodeOceanEditorWebsocket = { + websocket: null, + + createSocketUrl: function(url) { + return '<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + 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)); + }, + + initializeSocketForScoring: function(url) { + this.initializeSocket(url); + this.websocket.on('default',this.handleScoringResponse.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.killWebsocketAndContainer(); + this.handleQaApiOutput(); + this.handleStderrOutputForFlowr(); + this.augmentStacktraceInOutput(); + this.cleanUpTurtle(); + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index f2712f02..f752dde2 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -134,7 +134,7 @@ CodeOceanEditorSubmissions = { this.showSpinner($('#run')); this.toggleButtonStates(); var url = response.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); - this.evaluateCode(url, function(evt) { this.parseCanvasMessage(evt.data, true); }.bind(this)); + this.initializeSocketForRunning(url); }.bind(this)); } }, @@ -154,7 +154,7 @@ CodeOceanEditorSubmissions = { this.createSubmission('#test', null, function(response) { this.showSpinner($('#test')); var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); - this.evaluateCode(url, this.handleTestResponse.bind(this)); + this.initializeSocketForTesting(); }.bind(this)); } }, diff --git a/app/assets/javascripts/editor/turtle.js.erb b/app/assets/javascripts/editor/turtle.js.erb index 87e5bdc8..daebd59c 100644 --- a/app/assets/javascripts/editor/turtle.js.erb +++ b/app/assets/javascripts/editor/turtle.js.erb @@ -16,6 +16,8 @@ CodeOceanEditorTurtle = { }, 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})); @@ -26,6 +28,8 @@ CodeOceanEditorTurtle = { }, 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]); diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js.erb index f9d5466a..4c3f9787 100644 --- a/app/assets/javascripts/editor/websocket.js.erb +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -1,89 +1,90 @@ -CodeOceanEditorWebsocket = { - initWebsocketConnection: function (url, onmessageFunction) { - //TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss) - //causes: Puma::HttpParserError: Invalid HTTP format, parsing fails. - //TODO: make sure that this gets cached. - this.websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url); - this.websocket.onopen = function (evt) { - this.resetOutputTab(); - }.bind(this); // todo show some kind of indicator for established connection - this.websocket.onclose = function (evt) { /* expected at some point */ - }.bind(this); - this.websocket.onmessage = onmessageFunction; - this.websocket.onerror = function (evt) { - this.showWebsocketError(); - }.bind(this); - this.websocket.flush = function () { - this.send('\n'); - } - }, - - //ToDo: Move websocket and commands variable in here - executeWebsocketCommand: function (msg) { - if ($.inArray(msg.cmd, this.commands) == -1) { - console.log("Unknown command: " + msg.cmd); - // skipping unregistered commands is required - // as we may receive mirrored response due to internal behaviour - return; - } - switch (msg.cmd) { - case 'input': - this.showPrompt(msg); - break; - case 'write': - this.printWebsocketOutput(msg); - break; - case 'turtle': - this.initTurtle(); - this.showCanvas(); - this.handleTurtleCommand(msg); - break; - case 'turtlebatch': - this.initTurtle(); - this.showCanvas(); - this.handleTurtlebatchCommand(msg); - break; - case 'render': - this.renderWebsocketOutput(msg); - break; - case 'exit': - this.killWebsocketAndContainer(); - this.handleQaApiOutput(); - this.handleStderrOutputForFlowr(); - this.augmentStacktraceInOutput(); - this.cleanUpTurtle(); - break; - case 'timeout': - // just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes. - this.showTimeoutMessage(); - break; - case 'status': - this.showStatus(msg); - break; - } - }, - - parseCanvasMessage: function (message, recursive) { - var msg; - message = message.replace(/^\s+|\s+$/g, ""); - try { - // todo validate json instead of catching - msg = JSON.parse(message); - } catch (e) { - if (!recursive) { - return false; - } - // why does docker sometimes send multiple commands at once? - message = message.replace(/^\s+|\s+$/g, ""); - var messages = message.split("\n"); - for (var i = 0; i < messages.length; i++) { - if (!messages[i]) { - continue; - } - this.parseCanvasMessage(messages[i], false); - } - return; - } - this.executeWebsocketCommand(msg); +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'); } -}; \ No newline at end of file +}; + +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); +}; From dca26cf237fcff99d6133c1f7cffc68bba44ba4c Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 10:49:55 +0200 Subject: [PATCH 08/57] Refactored frontend. Moved output to editor tab. Added collapse function to sidebar. --- app/assets/javascripts/editor/editor.js.erb | 55 +++++++++++++++++-- .../javascripts/editor/evaluation.js.erb | 7 ++- .../javascripts/editor/submissions.js.erb | 3 +- app/assets/stylesheets/editor.css.scss | 51 +++++++++++++++++ app/views/exercises/_editor.html.slim | 5 +- .../exercises/_editor_file_tree.html.slim | 27 ++++++--- app/views/exercises/_editor_output.html.slim | 29 ++++++++++ app/views/exercises/implement.html.slim | 30 +--------- 8 files changed, 159 insertions(+), 48 deletions(-) create mode 100644 app/views/exercises/_editor_output.html.slim diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index b16892ff..026aa2a9 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -100,7 +100,8 @@ var CodeOceanEditor = { showOutput: function(event) { event.preventDefault(); - this.showTab(1); + this.showTab(0); + this.showOutputBar(); $('#output').scrollTo($(this).attr('href')); }, @@ -144,10 +145,8 @@ var CodeOceanEditor = { handleKeyPress: function (event) { if (event.which === this.ALT_1_KEY_CODE) { this.showWorkspaceTab(event); - } else if (event.which === this.ALT_2_KEY_CODE) { - this.showTab(1); } else if (event.which === this.ALT_3_KEY_CODE) { - this.showTab(2); + this.showTab(1); } else if (event.which === this.ALT_R_KEY_CODE) { $('#run').trigger('click'); } else if (event.which === this.ALT_S_KEY_CODE) { @@ -270,16 +269,37 @@ var CodeOceanEditor = { 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-collapsed').addClass('hidden'); + $('#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');/** + $('#frames').toggleClass('editor-col') + if (main_area.hasClass('col-sm-10')) { + main_area.removeClass('col-sm-10').addClass('col-sm-11'); + } else if (main_area.hasClass('col-sm-11')) { + main_area.removeClass('col-sm-11').addClass('col-sm-10'); + }*/ + $('#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); @@ -454,7 +474,8 @@ var CodeOceanEditor = { this.clearOutput(); $('#hint').fadeOut(); $('#flowrHint').fadeOut(); - this.showTab(1); + this.showTab(0); + this.showOutputBar(); }, isActiveFileBinary: function () { @@ -544,6 +565,26 @@ var CodeOceanEditor = { }); }, + initializeOutputBarToggle: function() { + $('#toggle-sidebar-output').on('click',this.hideOutputBar.bind(this)); + $('#toggle-sidebar-output-collapsed').on('click',this.showOutputBar.bind(this)); + }, + + showOutputBar: function() { + if ($('#sidebar').hasClass('sidebar-col')) { + this.handleSideBarToggle(); + } + $('#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'); + }, + initializeEverything: function() { this.initializeRegexes(); this.initializeCodePilot(); @@ -552,6 +593,8 @@ var CodeOceanEditor = { this.initializeEditors(); this.initializeEventHandlers(); this.initializeFileTree(); + this.initializeSideBarCollapse(); + this.initializeOutputBarToggle(); this.initializeTooltips(); this.initPrompt(); this.renderScore(); diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 0f944239..4e19bc9a 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -8,7 +8,7 @@ CodeOceanEditorEvaluation = { }, 0).toFixed(2); $('#score').data('score', score); this.renderScore(); - this.showTab(2); + this.showTab(1); }, handleTestResponse: function (result) { @@ -18,7 +18,8 @@ CodeOceanEditorEvaluation = { this.qa_api.executeCommand('syncOutput', [result]); } this.showStatus(result); - this.showTab(1); + this.showTab(0); + this.showOutputBar(); }, printOutput: function (output, colorize, index) { @@ -132,7 +133,7 @@ CodeOceanEditorEvaluation = { this.hideSpinner(); this.running = false; this.toggleButtonStates(); - }); + }.bind(this)); jqxhr.fail(ajaxError); }, diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index f752dde2..32557d8d 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -115,7 +115,8 @@ CodeOceanEditorSubmissions = { stderr: message }, true, 0); this.sendError(message, response.id); - this.showTab(1); + this.showTab(0); + this.showOutputBar(); }; } }); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index e41153e2..9f1b4784 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -64,6 +64,7 @@ button i.fa-spin { #outputInformation { #output { max-height: 500px; + width: 100%; overflow: auto; margin: 2em 0; @@ -95,3 +96,53 @@ button i.fa-spin { margin-right: 25px; float: right; } + +.sidebar-col-collapsed { + -webkit-transition: width 2s; + transition: width 2s; + width:52px; + float:left; + min-height: 1px; + padding-left: 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; + padding-left: 15px; + padding-right: 15px; + 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 +} \ No newline at end of file diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 0ce5b73f..9c04b7ef 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,6 +1,7 @@ #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') + div id="sidebar" class=(@exercise.hide_file_tree ? 'hidden sidebar-col' : 'sidebar-col') = render('editor_file_tree', files: @files) + div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output') + div id='frames' class=(@exercise.hide_file_tree ? 'editor-col' : 'editor-col') - @files.each do |file| = render('editor_frame', exercise: exercise, file: file) #autosave-label diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index c23158b9..26f14112 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,10 +1,23 @@ -#files data-entries=FileTree.new(files).to_js_tree +div id='sidebar-collapsed' + = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'') + - 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-collapsed', label:'') + = render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, label:'', icon: 'fa fa-times', id: 'destroy-file-collapsed') -hr + = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download-collapsed', label:'') -- 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')) +div id='sidebar-uncollapsed' + = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: 'Collapse') -= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) + hr + + #files data-entries=FileTree.new(files).to_js_tree + + hr + + - 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')) diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim new file mode 100644 index 00000000..e96911ac --- /dev/null +++ b/app/views/exercises/_editor_output.html.slim @@ -0,0 +1,29 @@ +div id='output_sidebar_collapsed' + = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') +div id='output_sidebar_uncollapsed' class='hidden col-sm-12' 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: 'Collapse') + hr + div + #hint + .panel.panel-warning + .panel-heading = t('exercises.implement.hint') + .panel-body + div + // todo set to full width if turtle isnt used + #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 + hr + div + #turtlediv + // todo what should the canvas default size be? + canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin' \ No newline at end of file diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 0c5e109b..19924a1a 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -1,5 +1,5 @@ .row - #editor-column.col-md-10.col-md-offset-1 + #editor-column.col-md-12 h1 = @exercise span.badge.pull-right.score @@ -16,10 +16,6 @@ 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 @@ -29,30 +25,6 @@ .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 - 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') From 81fca2b8c739bcfed2845e051bd14a1916507d40 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 11:01:44 +0200 Subject: [PATCH 09/57] fixed qa_api binding, changed formatting ... --- app/assets/javascripts/editor.js.erb | 6 ++++-- app/assets/javascripts/editor/editor.js.erb | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index cff00332..e4f66c4c 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -4,13 +4,15 @@ $(function() { //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, + $.extend( + CodeOceanEditor, CodeOceanEditorAJAX, CodeOceanEditorEvaluation, CodeOceanEditorFlowr, CodeOceanEditorSubmissions, CodeOceanEditorTurtle, - CodeOceanEditorWebsocket); + CodeOceanEditorWebsocket + ); if ($('#editor').isPresent() && CodeOceanEditor) { if (CodeOceanEditor.isBrowserSupported()) { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index b16892ff..ab673983 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -204,7 +204,7 @@ var CodeOceanEditor = { 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(); From 5f61c24dc632c82764431139eca6db1d129e93fc Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 11:18:28 +0200 Subject: [PATCH 10/57] Fixed socket calls. --- app/assets/javascripts/editor/evaluation.js.erb | 6 +++--- app/assets/javascripts/editor/websocket.js.erb | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 0f944239..373ed197 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -137,13 +137,13 @@ CodeOceanEditorEvaluation = { }, killWebsocketAndContainer: function () { - if (this.websocket.readyState != WebSocket.OPEN) { + if (this.websocket.getReadyState() != WebSocket.OPEN) { return; } this.websocket.send(JSON.stringify({cmd: 'exit'})); - this.websocket.flush(); - this.websocket.close(); + this.websocket.killWebSocket(); + this.hideSpinner(); this.running = false; this.toggleButtonStates(); diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js.erb index 4c3f9787..e4c45e24 100644 --- a/app/assets/javascripts/editor/websocket.js.erb +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -88,3 +88,18 @@ CommandSocket.prototype.executeCommand = function(cmd) { CommandSocket.prototype.send = function(data) { this.websocket.send(data); }; + +/** + * Returns the ready state of the socket. + */ +CommandSocket.prototype.getReadyState = function() { + return this.websocket.readyState; +}; + +/** + * Closes the websocket. + */ +CommandSocket.prototype.killWebSocket = function() { + this.websocket.flush(); + this.websocket.close(); +}; \ No newline at end of file From 7903447a31833df1f08d545323d880f919406ec8 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 11:47:49 +0200 Subject: [PATCH 11/57] Fixed autosavelabel. --- app/assets/javascripts/editor/editor.js.erb | 64 +++++++++++++++++---- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index ab673983..2a0a8b35 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -35,7 +35,7 @@ var CodeOceanEditor = { lastCopyText: null, autosaveTimer: null, - autosaveLabel: $("#autosave-label span"), + autosaveLabel: "#autosave-label span", ENTER_KEY_CODE: 13, @@ -100,7 +100,8 @@ var CodeOceanEditor = { showOutput: function(event) { event.preventDefault(); - this.showTab(1); + this.showTab(0); + this.showOutputBar(); $('#output').scrollTo($(this).attr('href')); }, @@ -144,10 +145,8 @@ var CodeOceanEditor = { handleKeyPress: function (event) { if (event.which === this.ALT_1_KEY_CODE) { this.showWorkspaceTab(event); - } else if (event.which === this.ALT_2_KEY_CODE) { - this.showTab(1); } else if (event.which === this.ALT_3_KEY_CODE) { - this.showTab(2); + this.showTab(1); } else if (event.which === this.ALT_R_KEY_CODE) { $('#run').trigger('click'); } else if (event.which === this.ALT_S_KEY_CODE) { @@ -190,9 +189,10 @@ var CodeOceanEditor = { autosave: function () { var date = new Date(); - this.autosaveLabel.parent().css("visibility", "visible"); - this.autosaveLabel.text(date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds()); - this.autosaveLabel.text(date.toLocaleTimeString()); + 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); }, @@ -270,16 +270,37 @@ var CodeOceanEditor = { 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-collapsed').addClass('hidden'); + $('#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');/** + $('#frames').toggleClass('editor-col') + if (main_area.hasClass('col-sm-10')) { + main_area.removeClass('col-sm-10').addClass('col-sm-11'); + } else if (main_area.hasClass('col-sm-11')) { + main_area.removeClass('col-sm-11').addClass('col-sm-10'); + }*/ + $('#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); @@ -454,7 +475,8 @@ var CodeOceanEditor = { this.clearOutput(); $('#hint').fadeOut(); $('#flowrHint').fadeOut(); - this.showTab(1); + this.showTab(0); + this.showOutputBar(); }, isActiveFileBinary: function () { @@ -544,6 +566,26 @@ var CodeOceanEditor = { }); }, + initializeOutputBarToggle: function() { + $('#toggle-sidebar-output').on('click',this.hideOutputBar.bind(this)); + $('#toggle-sidebar-output-collapsed').on('click',this.showOutputBar.bind(this)); + }, + + showOutputBar: function() { + if ($('#sidebar').hasClass('sidebar-col')) { + this.handleSideBarToggle(); + } + $('#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'); + }, + initializeEverything: function() { this.initializeRegexes(); this.initializeCodePilot(); @@ -552,6 +594,8 @@ var CodeOceanEditor = { this.initializeEditors(); this.initializeEventHandlers(); this.initializeFileTree(); + this.initializeSideBarCollapse(); + this.initializeOutputBarToggle(); this.initializeTooltips(); this.initPrompt(); this.renderScore(); From 6e516ca31e10d51d4c4931620ccc706e6d43f085 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 11:54:05 +0200 Subject: [PATCH 12/57] some minor fixes --- app/assets/javascripts/editor/flowr.js.erb | 4 ++-- app/assets/javascripts/editor/submissions.js.erb | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index 6440a0b5..30b4b298 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -53,10 +53,10 @@ CodeOceanEditorFlowr = { }).done(function () { this.hideSpinner(); $.flash.success({text: $('#askForCommentsButton').data('message-success')}); - }).error(this.ajaxError); + }.bind(this)).error(this.ajaxError.bind(this)); }; - this.createSubmission($('.requestCommentsButton'), null, createRequestForComments); + this.createSubmission($('.requestCommentsButton'), null, createRequestForComments.bind(this)); $('#comment-modal').modal('hide'); var button = $('.requestCommentsButton'); diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index f752dde2..4b38f85d 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -29,7 +29,10 @@ CodeOceanEditorSubmissions = { }); jqxhr.always(this.hideSpinner.bind(this)); jqxhr.done(this.createSubmissionCallback.bind(this)); - jqxhr.done(callback.bind(this)); + if(callback != null){ + jqxhr.done(callback.bind(this)); + } + jqxhr.fail(this.ajaxError.bind(this)); }, From 7bf0200094a779e634a19c6712cb6678dc000112 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 12:01:20 +0200 Subject: [PATCH 13/57] Fixed filetree selection. --- app/assets/javascripts/editor/editor.js.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 2a0a8b35..a252041d 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -262,10 +262,10 @@ var CodeOceanEditor = { initializeFileTree: function () { $('#files').jstree($('#files').data('entries')); - $('#files').on('click', 'li.jstree-leaf', function () { + $('#files').on('click', 'li.jstree-leaf', function (event) { active_file = { - filename: $(this).text(), - id: parseInt($(this).attr('id')) + filename: $(event.target).parent().text(), + id: parseInt($(event.target).parent().attr('id')) }; var frame = $('[data-file-id="' + active_file.id + '"]').parent(); this.showFrame(frame); From fc1266d0d139f5db0dfc7973cd4a375c586cab93 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 14:35:08 +0200 Subject: [PATCH 14/57] Added tooltips to sidebar buttons. --- app/assets/javascripts/editor/editor.js.erb | 5 +++++ app/assets/stylesheets/editor.css.scss | 4 ++++ app/views/exercises/_editor_file_tree.html.slim | 12 ++++++------ app/views/exercises/_editor_output.html.slim | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index a252041d..65e303e9 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -586,6 +586,10 @@ var CodeOceanEditor = { $('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed'); }, + initializeSideBarTooltips: function() { + $('[data-toggle="tooltip"]').tooltip() + }, + initializeEverything: function() { this.initializeRegexes(); this.initializeCodePilot(); @@ -596,6 +600,7 @@ var CodeOceanEditor = { this.initializeFileTree(); this.initializeSideBarCollapse(); this.initializeOutputBarToggle(); + this.initializeSideBarTooltips(); this.initializeTooltips(); this.initPrompt(); this.renderScore(); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 9f1b4784..85dd4f05 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -145,4 +145,8 @@ button i.fa-spin { padding-left: 15px; padding-right: 15px; box-sizing: border-box +} + +.enforce-top-margin { + margin-top: 5px !important; } \ 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 26f14112..7bb3da68 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,13 +1,13 @@ div id='sidebar-collapsed' - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'') - - 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-collapsed', label:'') - = render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, label:'', icon: 'fa fa-times', id: 'destroy-file-collapsed') + = 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:'Expand sidebar') - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download-collapsed', label:'') + - if @exercise.allow_file_creation? + = 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')) div id='sidebar-uncollapsed' - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: 'Collapse') + = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: 'Collapse') hr diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index e96911ac..2f32432f 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,5 +1,5 @@ div id='output_sidebar_collapsed' - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') + = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: 'Expand sidebar', icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') div id='output_sidebar_uncollapsed' class='hidden col-sm-12' 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: 'Collapse') From b24db1dca0cbe67c90dd28a8ecb39b24d2d4783a Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 15:23:58 +0200 Subject: [PATCH 15/57] Toggle description Tooltips fixed Buttons moved --- app/assets/javascripts/editor/editor.js.erb | 18 ++++++++++++++---- app/assets/stylesheets/editor.css.scss | 19 +++++++++++++++++++ app/views/exercises/_editor.html.slim | 6 ++++-- .../exercises/_editor_file_tree.html.slim | 2 ++ app/views/exercises/_editor_frame.html.slim | 6 +----- app/views/exercises/implement.html.slim | 10 +++++++--- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 65e303e9..430bff15 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -323,10 +323,11 @@ var CodeOceanEditor = { $('#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 = $('.requestCommentsButton'); + var button = $('#requestComments'); button.hide(); button.on('click', function () { $('#comment-modal').modal('show'); @@ -572,9 +573,6 @@ var CodeOceanEditor = { }, showOutputBar: function() { - if ($('#sidebar').hasClass('sidebar-col')) { - this.handleSideBarToggle(); - } $('#output_sidebar_collapsed').addClass('hidden'); $('#output_sidebar_uncollapsed').removeClass('hidden'); $('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col'); @@ -590,6 +588,17 @@ var CodeOceanEditor = { $('[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(); @@ -600,6 +609,7 @@ var CodeOceanEditor = { this.initializeFileTree(); this.initializeSideBarCollapse(); this.initializeOutputBarToggle(); + this.initializeDescriptionToggle(); this.initializeSideBarTooltips(); this.initializeTooltips(); this.initPrompt(); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 85dd4f05..be607e29 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -18,6 +18,7 @@ button i.fa-spin { .score { display: none; + vertical-align: bottom; } #alert, #development-environment { @@ -149,4 +150,22 @@ button i.fa-spin { .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; } \ No newline at end of file diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 9c04b7ef..3781b035 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,5 +1,5 @@ #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 id="sidebar" class=(@exercise.hide_file_tree ? 'hidden sidebar-col' : 'sidebar-col') = render('editor_file_tree', files: @files) + div id="sidebar" class=(@exercise.hide_file_tree ? 'hidden sidebar-col' : 'sidebar-col') = render('editor_file_tree', exercisee: @exercise, files: @files) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output') div id='frames' class=(@exercise.hide_file_tree ? 'editor-col' : 'editor-col') - @files.each do |file| @@ -8,7 +8,6 @@ = 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 @@ -38,5 +37,8 @@ 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')) + button.btn.btn-primary.requestCommentsButton type='button' id="requestComments" + i.fa.fa-comment + = t('exercises.editor.requestComments') = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 7bb3da68..77ffa024 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -5,6 +5,7 @@ div id='sidebar-collapsed' = 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' = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: 'Collapse') @@ -21,3 +22,4 @@ div id='sidebar-uncollapsed' = 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('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')) diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index 01640fa8..94b43ab0 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-id=file.id \ No newline at end of file diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 19924a1a..75cdbec0 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -1,10 +1,14 @@ .row #editor-column.col-md-12 - h1 = @exercise + div + span.badge.pull-right.score - span.badge.pull-right.score + h1 id="exercise-headline" + i class="fa fa-chevron-down" id="description-symbol" + = @exercise - p.lead = render_markdown(@exercise.description) + #description-panel.lead.description-panel + = render_markdown(@exercise.description) #alert.alert.alert-danger role='alert' h4 = t('.alert.title') From 858bbdab957e810540321a9fb957a971ba547fd2 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 15:34:54 +0200 Subject: [PATCH 16/57] Fixed button. --- app/views/exercises/_editor.html.slim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 3781b035..ed338d14 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -36,9 +36,9 @@ 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')) - button.btn.btn-primary.requestCommentsButton type='button' id="requestComments" - i.fa.fa-comment - = t('exercises.editor.requestComments') + = 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')) + button.btn.btn-primary.requestCommentsButton type='button' id="requestComments" + i.fa.fa-comment + = t('exercises.editor.requestComments') = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') From 0ca1cbaa141f2507ff492b6d256043db5d8bb092 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 15:41:56 +0200 Subject: [PATCH 17/57] Fixed file-creation-modal --- app/assets/javascripts/editor/editor.js.erb | 2 +- app/views/exercises/_editor_file_tree.html.slim | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 430bff15..3aa5a2f9 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -564,7 +564,7 @@ var CodeOceanEditor = { this.createSubmission('#create-file', null, function(response) { $('#code_ocean_file_context_id').val(response.id); $('#modal-file').modal('show'); - }); + }.bind(this)); }, initializeOutputBarToggle: function() { diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 77ffa024..64250ad7 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -19,7 +19,9 @@ div id='sidebar-uncollapsed' - 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('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('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) \ No newline at end of file From e42330cf1b6048e01bb090062d45960160128bf6 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 16:27:40 +0200 Subject: [PATCH 18/57] Added tooltip to request-comments button. Added locale for all new texts. --- app/assets/javascripts/editor/editor.js.erb | 12 ++++-- app/assets/javascripts/editor/flowr.js.erb | 4 +- app/assets/stylesheets/editor.css.scss | 4 +- app/views/exercises/_editor.html.slim | 38 ++++--------------- .../exercises/_editor_file_tree.html.slim | 4 +- app/views/exercises/_editor_output.html.slim | 4 +- config/locales/en.yml | 3 ++ 7 files changed, 28 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 3aa5a2f9..336def3b 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -14,6 +14,8 @@ var CodeOceanEditor = { REMEMBER_TAB: false, AUTOSAVE_INTERVAL: 15 * 1000, REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000, + REQUEST_TOOLTIP_TIME: 5000, + NONE: 0, WEBSOCKET: 1, SERVER_SEND_EVENT: 2, @@ -328,7 +330,7 @@ var CodeOceanEditor = { initializeRequestForComments: function () { var button = $('#requestComments'); - button.hide(); + button.prop('disabled', true); button.on('click', function () { $('#comment-modal').modal('show'); }); @@ -336,8 +338,12 @@ var CodeOceanEditor = { $('#askForCommentsButton').on('click', this.requestComments); setTimeout(function () { - button.fadeIn(); - }, this.REQUEST_FOR_COMMENTS_DELAY); + 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 () { diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index 30b4b298..d8d19e91 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -59,8 +59,8 @@ CodeOceanEditorFlowr = { this.createSubmission($('.requestCommentsButton'), null, createRequestForComments.bind(this)); $('#comment-modal').modal('hide'); - var button = $('.requestCommentsButton'); - button.fadeOut(); + var button = $('#requestComments'); + button.prop('disabled', true); }, //tODO move codepilot out of here. diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index be607e29..79a1797b 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -91,12 +91,12 @@ button i.fa-spin { font-size: 0.8em; } -.requestCommentsButton { +/* .requestCommentsButton { position: relative; margin-top: -50px; margin-right: 25px; float: right; -} +} */ .sidebar-col-collapsed { -webkit-transition: width 2s; diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index ed338d14..488ebe17 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -10,35 +10,13 @@ #editor-buttons.btn-group // = 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')) - button.btn.btn-primary.requestCommentsButton type='button' id="requestComments" - i.fa.fa-comment - = t('exercises.editor.requestComments') + // .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')) = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 64250ad7..c783ecec 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,5 +1,5 @@ div id='sidebar-collapsed' - = 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:'Expand sidebar') + = 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_sidebar')) - if @exercise.allow_file_creation? = 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')) @@ -8,7 +8,7 @@ div id='sidebar-collapsed' = 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' - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: 'Collapse') + = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_sidebar')) hr diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index 2f32432f..d7deabd8 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,8 +1,8 @@ div id='output_sidebar_collapsed' - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: 'Expand sidebar', icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') + = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') div id='output_sidebar_uncollapsed' class='hidden col-sm-12' 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: 'Collapse') + = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_sidebar')) hr div #hint diff --git a/config/locales/en.yml b/config/locales/en.yml index eaf88018..85266025 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -203,6 +203,9 @@ 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! + collapse_sidebar: Collapse sidebar + expand_sidebar: Expand sidebar save: Save score: Score send: Send From 1c7b4c931bdb6c5731a7c26ed56680f879e2f130 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 16:29:45 +0200 Subject: [PATCH 19/57] Added german locale. --- config/locales/de.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/locales/de.yml b/config/locales/de.yml index 4905f929..76935b93 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -203,6 +203,9 @@ 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: Wenn du Hilfe mit deinem Code benötigst, kannst dir hier Kommentare erbitten + collapse_sidebar: Sidebar einklappen + expand_sidebar: Sidebar ausklappen save: Speichern score: Bewerten send: Senden From 1f159ce0bdd340294d37b552ed6ac7f1e602681f Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 16:48:55 +0200 Subject: [PATCH 20/57] some cleanup, reducing size of questions sidebar --- app/assets/javascripts/editor/editor.js.erb | 5 ----- app/assets/javascripts/editor/flowr.js.erb | 4 ++-- app/assets/stylesheets/editor.css.scss | 1 - app/views/exercises/_editor.html.slim | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 336def3b..297ea1a9 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -429,12 +429,7 @@ var CodeOceanEditor = { toggleButtonStates: function () { $('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file'); - $('#dropdown-render').toggleClass('disabled', !this.isActiveFileRenderable()); - $('#dropdown-run').toggleClass('disabled', !this.isActiveFileRunnable() || this.running); - $('#dropdown-stop').toggleClass('disabled', !this.isActiveFileStoppable()); - $('#dropdown-test').toggleClass('disabled', !this.isActiveFileTestable()); $('#dummy').toggle(!this.fileActionsAvailable()); - $('#editor-buttons .dropdown-toggle').toggle(this.fileActionsAvailable()); $('#render').toggle(this.isActiveFileRenderable()); $('#run').toggle(this.isActiveFileRunnable() && !this.running); $('#stop').toggle(this.isActiveFileStoppable()); diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index d8d19e91..931b4043 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -66,8 +66,8 @@ CodeOceanEditorFlowr = { //tODO move codepilot out of here. initializeCodePilot: function () { if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) { - $('#editor-column').addClass('col-md-8').removeClass('col-md-10'); - $('#questions-column').addClass('col-md-3'); + $('#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'); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 79a1797b..9f31fcbc 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -27,7 +27,6 @@ button i.fa-spin { #dummy { display: none; - width: 100% !important; } #editor-buttons { diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 488ebe17..77418945 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -7,9 +7,9 @@ #autosave-label = t('exercises.editor.lastsaved') span + button style="display:none" id="autosave" #editor-buttons.btn-group // = 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')) From 333a72c9969bdec3ae530c1d6c5cd6169aa0b65e Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 16:50:10 +0200 Subject: [PATCH 21/57] Added url param. --- app/assets/javascripts/editor/submissions.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index 2c97cb5b..08e62dba 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -158,7 +158,7 @@ CodeOceanEditorSubmissions = { this.createSubmission('#test', null, function(response) { this.showSpinner($('#test')); var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename); - this.initializeSocketForTesting(); + this.initializeSocketForTesting(url); }.bind(this)); } }, From 9ec9f012576a3d134ba13d7f9aca0eeaeba38596 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 17:03:28 +0200 Subject: [PATCH 22/57] Added handler for exit command. --- app/assets/javascripts/editor/execution.js.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb index 53b53ff7..c28c47a4 100644 --- a/app/assets/javascripts/editor/execution.js.erb +++ b/app/assets/javascripts/editor/execution.js.erb @@ -17,11 +17,13 @@ CodeOceanEditorWebsocket = { 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('default',this.handleScoringResponse.bind(this)); + this.websocket.on('exit', this.handleExitCommand.bind(this)); }, initializeSocketForRunning: function(url) { From 0d550a44af42ef5c030697519b1078936ecf0138 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 17:15:44 +0200 Subject: [PATCH 23/57] send exit commands from rails server when score or test have finished - this is needed to reset the buttons in the frontend correctly. --- app/controllers/submissions_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 1b6f9421..865ff747 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -232,6 +232,7 @@ class SubmissionsController < ApplicationController 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)) + tubesock.send_data JSON.dump({'cmd' => 'exit'}) end end @@ -291,6 +292,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 From 11d284b1665d4df13dd7bfdcdd619c46a34ec01a Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 17:31:18 +0200 Subject: [PATCH 24/57] Disabled file tree and collapsed sidebar when filetree should be disabled. --- app/assets/javascripts/editor/editor.js.erb | 9 +-------- app/views/exercises/_editor.html.slim | 2 +- app/views/exercises/_editor_file_tree.html.slim | 17 +++++++++-------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 297ea1a9..34fbe19f 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -286,19 +286,12 @@ var CodeOceanEditor = { }, initializeSideBarCollapse: function() { - $('#sidebar-collapsed').addClass('hidden'); $('#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');/** - $('#frames').toggleClass('editor-col') - if (main_area.hasClass('col-sm-10')) { - main_area.removeClass('col-sm-10').addClass('col-sm-11'); - } else if (main_area.hasClass('col-sm-11')) { - main_area.removeClass('col-sm-11').addClass('col-sm-10'); - }*/ + $('#sidebar').toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed'); $('#sidebar-collapsed').toggleClass('hidden'); $('#sidebar-uncollapsed').toggleClass('hidden'); }, diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 77418945..83cad405 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,5 +1,5 @@ #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 id="sidebar" class=(@exercise.hide_file_tree ? 'hidden sidebar-col' : 'sidebar-col') = render('editor_file_tree', exercisee: @exercise, files: @files) + 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=(@exercise.hide_file_tree ? 'editor-col' : 'editor-col') - @files.each do |file| diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index c783ecec..306342e6 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,26 +1,27 @@ -div id='sidebar-collapsed' +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_sidebar')) - - if @exercise.allow_file_creation? + - 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' +div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_sidebar')) - hr + div class=(@exercise.hide_file_tree ? 'hidden' : '') + hr - #files data-entries=FileTree.new(files).to_js_tree + #files data-entries=FileTree.new(files).to_js_tree - hr + hr - - if @exercise.allow_file_creation? + - 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', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) + = 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? From eacaa665f6ba00515cd02cc801c475f6f73c0eea Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 17:31:46 +0200 Subject: [PATCH 25/57] make websocket flush accessible. --- app/assets/javascripts/editor/websocket.js.erb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js.erb index e4c45e24..baba623d 100644 --- a/app/assets/javascripts/editor/websocket.js.erb +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -96,6 +96,13 @@ CommandSocket.prototype.getReadyState = function() { return this.websocket.readyState; }; +/** + * Flush the websocket. + */ +CommandSocket.prototype.flush = function() { + this.websocket.flush(); +}; + /** * Closes the websocket. */ From 1670839ecf03edd53e4eefb2ebfd9ba84c0345da Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 17:41:00 +0200 Subject: [PATCH 26/57] Fixed part of prompt. --- app/assets/javascripts/editor/editor.js.erb | 2 +- app/assets/javascripts/editor/evaluation.js.erb | 10 ++++++---- app/views/exercises/_editor_output.html.slim | 12 +++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 34fbe19f..3d89bada 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -33,7 +33,7 @@ var CodeOceanEditor = { runmode: this.NONE, numMessages: 0, - prompt: $('#prompt'), + prompt: '#prompt', lastCopyText: null, autosaveTimer: null, diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 2235e137..b4099811 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -154,16 +154,18 @@ CodeOceanEditorEvaluation = { //TODO: Move Prompt Part in own component showPrompt: function(msg) { var label = $('#prompt .input-group-addon'); + var prompt = $(this.prompt) label.text(msg.data || label.data('prompt')); - if (this.prompt.isPresent() && this.prompt.hasClass('hidden')) { - this.prompt.removeClass('hidden'); + if (prompt.isPresent() && prompt.hasClass('hidden')) { + prompt.removeClass('hidden'); } $('#prompt input').focus(); }, hidePrompt: function() { - if (this.prompt.isPresent() && !this.prompt.hasClass('hidden')) { - this.prompt.addClass('hidden'); + var prompt = $(this.prompt) + if (prompt.isPresent() && !prompt.hasClass('hidden')) { + prompt.addClass('hidden'); } }, diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index d7deabd8..f2f5b4af 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -4,13 +4,16 @@ div id='output_sidebar_uncollapsed' class='hidden col-sm-12' data-message-no-out .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_sidebar')) hr + div + #turtlediv + canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin' + hr div #hint .panel.panel-warning .panel-heading = t('exercises.implement.hint') .panel-body div - // todo set to full width if turtle isnt used #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' @@ -21,9 +24,4 @@ div id='output_sidebar_uncollapsed' class='hidden col-sm-12' data-message-no-out - 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 - hr - div - #turtlediv - // todo what should the canvas default size be? - canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin' \ No newline at end of file + .panel-body \ No newline at end of file From f170fee6a9cb5bae1429b3b0b44183e972064564 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 17:49:59 +0200 Subject: [PATCH 27/57] Replaced hr with enforced margin. --- app/assets/javascripts/editor/evaluation.js.erb | 4 ++-- app/assets/stylesheets/editor.css.scss | 4 ++++ app/views/exercises/_editor_output.html.slim | 8 +++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index b4099811..7de3795a 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -154,7 +154,7 @@ CodeOceanEditorEvaluation = { //TODO: Move Prompt Part in own component showPrompt: function(msg) { var label = $('#prompt .input-group-addon'); - var prompt = $(this.prompt) + var prompt = $(this.prompt); label.text(msg.data || label.data('prompt')); if (prompt.isPresent() && prompt.hasClass('hidden')) { prompt.removeClass('hidden'); @@ -163,7 +163,7 @@ CodeOceanEditorEvaluation = { }, hidePrompt: function() { - var prompt = $(this.prompt) + var prompt = $(this.prompt); if (prompt.isPresent() && !prompt.hasClass('hidden')) { prompt.addClass('hidden'); } diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 9f31fcbc..fce2bfa2 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -167,4 +167,8 @@ button i.fa-spin { -webkit-transition: height 2s; transition: height 2s; visibility: visible; +} + +.enforce-big-top-margin { + margin-top: 15px !important; } \ No newline at end of file diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index f2f5b4af..d084fb63 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -3,17 +3,15 @@ div id='output_sidebar_collapsed' div id='output_sidebar_uncollapsed' class='hidden col-sm-12' 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_sidebar')) - hr - div + div.enforce-big-top-margin #turtlediv canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin' - hr - div + div.enforce-big-top-margin #hint .panel.panel-warning .panel-heading = t('exercises.implement.hint') .panel-body - div + 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' From c2bdcc791536f84847bb5b97b4183153453bfaba Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 1 Sep 2016 17:54:04 +0200 Subject: [PATCH 28/57] Added more height! --- app/assets/stylesheets/editor.css.scss | 2 +- app/views/exercises/_editor.html.slim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index fce2bfa2..80eee2dd 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -9,7 +9,7 @@ button i.fa-spin { .frame { display: none; - height: 400px; + height: 100%; audio, img, video { max-width: 100%; diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 83cad405..b108d0df 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,7 +1,7 @@ #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 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=(@exercise.hide_file_tree ? 'editor-col' : 'editor-col') + div id='frames' class='editor-col' - @files.each do |file| = render('editor_frame', exercise: exercise, file: file) #autosave-label From ddaed5929b5773d4026280f6a07d58c0b86d14f9 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 18:47:16 +0200 Subject: [PATCH 29/57] Naming of sidebars --- app/views/exercises/_editor_file_tree.html.slim | 4 ++-- app/views/exercises/_editor_output.html.slim | 4 ++-- config/locales/de.yml | 6 ++++-- config/locales/en.yml | 6 ++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 306342e6..aa2eb35d 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,5 +1,5 @@ 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_sidebar')) + = 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')) - 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')) @@ -8,7 +8,7 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') = 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 enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_sidebar')) + = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) div class=(@exercise.hide_file_tree ? 'hidden' : '') hr diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index d084fb63..f470c870 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,8 +1,8 @@ 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_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') + = 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' 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_sidebar')) + = 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 #turtlediv canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin' diff --git a/config/locales/de.yml b/config/locales/de.yml index 76935b93..b12db0ad 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -188,6 +188,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 +197,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}.' @@ -204,8 +208,6 @@ de: run_success: Ihr Code wurde auf der Plattform ausgeführt. requestComments: Kommentare erbitten requestCommentsTooltip: Wenn du Hilfe mit deinem Code benötigst, kannst dir hier Kommentare erbitten - collapse_sidebar: Sidebar einklappen - expand_sidebar: Sidebar ausklappen save: Speichern score: Bewerten send: Senden diff --git a/config/locales/en.yml b/config/locales/en.yml index 85266025..b70997e3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -188,6 +188,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 +197,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}.' @@ -204,8 +208,6 @@ en: 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! - collapse_sidebar: Collapse sidebar - expand_sidebar: Expand sidebar save: Save score: Score send: Send From 18c8927e86fe52f6a0ad1db68221b921cdfc4ff8 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 1 Sep 2016 18:47:32 +0200 Subject: [PATCH 30/57] set height of ace editor to 600px. --- app/assets/stylesheets/editor.css.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 80eee2dd..ad3da965 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -9,7 +9,7 @@ button i.fa-spin { .frame { display: none; - height: 100%; + height: 600px; audio, img, video { max-width: 100%; From 8f499b72da2ed0d0789ad67755477a69f7bef33c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 7 Sep 2016 17:34:20 +0200 Subject: [PATCH 31/57] Feature: Auto-Completion in Source Code + allow flags for exercises --- app/assets/javascripts/application.js | 3 ++- app/assets/javascripts/editor/editor.js.erb | 11 +++++++++++ app/controllers/exercises_controller.rb | 2 +- app/views/exercises/_editor_frame.html.slim | 2 +- app/views/exercises/_form.html.slim | 4 ++++ app/views/exercises/show.html.slim | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + ...07123009_add_allow_auto_completion_to_exercises.rb | 5 +++++ db/schema.rb | 4 ++-- 10 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb 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/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 3d89bada..7a0a3a7e 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -221,6 +221,17 @@ var CodeOceanEditor = { 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); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 45fd04d9..1e97d625 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/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index 94b43ab0..eff1541c 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -12,4 +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 \ 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/_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/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/locales/de.yml b/config/locales/de.yml index b12db0ad..8404ff3a 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 diff --git a/config/locales/en.yml b/config/locales/en.yml index b70997e3..2fe2379a 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 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| From 952cc5ec2446296891083d6e0d52de92e42d565f Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 13:21:51 +0200 Subject: [PATCH 32/57] Moved buttons to the top. --- app/assets/stylesheets/editor.css.scss | 4 ++++ app/views/exercises/_editor.html.slim | 15 ++++++++------- app/views/exercises/_editor_file_tree.html.slim | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index ad3da965..a331572a 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -171,4 +171,8 @@ button i.fa-spin { .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/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index b108d0df..0b9d33c1 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -2,13 +2,7 @@ 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' - - @files.each do |file| - = render('editor_frame', exercise: exercise, file: file) - #autosave-label - = t('exercises.editor.lastsaved') - span - button style="display:none" id="autosave" - #editor-buttons.btn-group + #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')) @@ -18,5 +12,12 @@ = 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 + button style="display:none" id="autosave" + = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index aa2eb35d..16cc705b 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -8,7 +8,7 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') = 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 enforce-top-margin', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) + = 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 From bac86cd4661eb80da8f6fe786642ab1369ea89ff Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 13:42:51 +0200 Subject: [PATCH 33/57] Made editor-resizing possible. --- app/assets/javascripts/editor/editor.js.erb | 12 ++++++++++++ app/assets/stylesheets/editor.css.scss | 1 - 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 7a0a3a7e..eafe6f44 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -15,6 +15,7 @@ var CodeOceanEditor = { AUTOSAVE_INTERVAL: 15 * 1000, REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000, REQUEST_TOOLTIP_TIME: 5000, + EDITOR_HEIGHT_SUBST: 100, NONE: 0, WEBSOCKET: 1, @@ -201,6 +202,15 @@ var CodeOceanEditor = { initializeEditors: function () { $('.editor').each(function (index, element) { + // Resize frame. + var windowHeight = window.innerHeight - this.EDITOR_HEIGHT_SUBST; + $(element).parent().height(windowHeight); + + $(window).resize(function(){ + var windowHeight = window.innerHeight - this.EDITOR_HEIGHT_SUBST; + $(element).parent().height(windowHeight); + }.bind(this)); + var editor = ace.edit(element); if (this.qa_api) { @@ -222,6 +232,8 @@ var CodeOceanEditor = { editor.setShowPrintMargin(false); editor.setTheme(this.THEME); + + // set options for autocompletion if($(element).data('allow-auto-completion')){ editor.setOptions({ diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index a331572a..807f4435 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -9,7 +9,6 @@ button i.fa-spin { .frame { display: none; - height: 600px; audio, img, video { max-width: 100%; From d2dab21292a68eb8d72583ce6785d24d527b918d Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 9 Sep 2016 14:04:51 +0200 Subject: [PATCH 34/57] some changes to resizing --- app/assets/javascripts/editor/editor.js.erb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index eafe6f44..ceb24a6a 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -15,7 +15,6 @@ var CodeOceanEditor = { AUTOSAVE_INTERVAL: 15 * 1000, REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000, REQUEST_TOOLTIP_TIME: 5000, - EDITOR_HEIGHT_SUBST: 100, NONE: 0, WEBSOCKET: 1, @@ -200,15 +199,21 @@ var CodeOceanEditor = { this.createSubmission($('#autosave'), null); }, + 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. - var windowHeight = window.innerHeight - this.EDITOR_HEIGHT_SUBST; - $(element).parent().height(windowHeight); + // Resize frame on load + this.resizeParentOfAceEditor(element); + + // Resize frame on window size change $(window).resize(function(){ - var windowHeight = window.innerHeight - this.EDITOR_HEIGHT_SUBST; - $(element).parent().height(windowHeight); + this.resizeParentOfAceEditor(element); }.bind(this)); var editor = ace.edit(element); From 3cd224c57ee0e48a9fb22682353811d266c0b5ab Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 9 Sep 2016 14:19:47 +0200 Subject: [PATCH 35/57] re-applied changes for redirect to request for comments --- app/assets/javascripts/editor/submissions.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index 08e62dba..e860c7a6 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -54,8 +54,8 @@ CodeOceanEditorSubmissions = { // This is the case, since it is set via a call to ancestor_id on the model, which returns either file_id if set, or id if it is not set. // therefore the else part is not needed any longer... - // if we have an file_id set (the file is a copy of a teacher supplied given file) - if (file_id_old != null){ + // if we 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){ From 77452e6c2b0d67e8217761e45387d298f7ac4b7f Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 9 Sep 2016 15:40:09 +0200 Subject: [PATCH 36/57] move progress into sidebar --- .../javascripts/editor/evaluation.js.erb | 1 + .../javascripts/editor/submissions.js.erb | 1 + app/assets/stylesheets/editor.css.scss | 8 ++++ app/views/exercises/_editor_output.html.slim | 32 ++++++++++++++- app/views/exercises/implement.html.slim | 39 ------------------- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 7de3795a..2011de6b 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -105,6 +105,7 @@ CodeOceanEditorEvaluation = { this.runmode = this.SERVER_SEND_EVENT; this.createSubmission('#assess', null, function (response) { this.showSpinner($('#assess')); + $('#score_div').removeClass('hidden'); var url = response.score_url; this.initializeSocketForScoring(url); }.bind(this)); diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index e860c7a6..213a9540 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -136,6 +136,7 @@ CodeOceanEditorSubmissions = { $('#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); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 807f4435..189dcb7f 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -89,6 +89,14 @@ button i.fa-spin { font-size: 0.8em; } +#turtlecanvas{ + border-style:solid; + border-width:thin; + display: block; + margin: auto; + +} + /* .requestCommentsButton { position: relative; margin-top: -50px; diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index f470c870..90d72d22 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -3,9 +3,39 @@ div id='output_sidebar_collapsed' div id='output_sidebar_uncollapsed' class='hidden col-sm-12' 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 style='border-style:solid;border-width:thin' + canvas#turtlecanvas.hidden width=400 height=400 div.enforce-big-top-margin #hint .panel.panel-warning diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 75cdbec0..87ff4e1f 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -15,47 +15,8 @@ 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='#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) - #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 From 46cb0715030bad82fff68bf1203e4dbedacad505 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 9 Sep 2016 15:40:53 +0200 Subject: [PATCH 37/57] remove code that showed tabs and leftovers of serversend events (runmode) --- app/assets/javascripts/editor/editor.js.erb | 36 ------------------- .../javascripts/editor/evaluation.js.erb | 10 +----- .../javascripts/editor/submissions.js.erb | 2 -- 3 files changed, 1 insertion(+), 47 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index ceb24a6a..5385319b 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -16,10 +16,6 @@ var CodeOceanEditor = { REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000, REQUEST_TOOLTIP_TIME: 5000, - NONE: 0, - WEBSOCKET: 1, - SERVER_SEND_EVENT: 2, - editors: [], editor_for_file: new Map(), regex_for_language: new Map(), @@ -30,7 +26,6 @@ var CodeOceanEditor = { running: false, qa_api: undefined, output_mode_is_streaming: true, - runmode: this.NONE, numMessages: 0, prompt: '#prompt', @@ -102,7 +97,6 @@ var CodeOceanEditor = { showOutput: function(event) { event.preventDefault(); - this.showTab(0); this.showOutputBar(); $('#output').scrollTo($(this).attr('href')); }, @@ -147,8 +141,6 @@ var CodeOceanEditor = { handleKeyPress: function (event) { if (event.which === this.ALT_1_KEY_CODE) { this.showWorkspaceTab(event); - } else if (event.which === this.ALT_3_KEY_CODE) { - this.showTab(1); } else if (event.which === this.ALT_R_KEY_CODE) { $('#run').trigger('click'); } else if (event.which === this.ALT_S_KEY_CODE) { @@ -498,7 +490,6 @@ var CodeOceanEditor = { this.clearOutput(); $('#hint').fadeOut(); $('#flowrHint').fadeOut(); - this.showTab(0); this.showOutputBar(); }, @@ -517,21 +508,6 @@ var CodeOceanEditor = { }; }, - showRequestedTab: function() { - if(this.REMEMBER_TAB){ - var regexp = /tab=(\d+)/; - if (regexp.test(window.location.search)) { - var index = regexp.exec(window.location.search)[1] - 1; - } else { - var index = this.localStorage.tab; - } - } else { - // else start with first tab. - var index = 0; - } - this.showTab(index); - }, - showSpinner: function(initiator) { $(initiator).find('i.fa').hide(); $(initiator).find('i.fa-spin').show(); @@ -557,10 +533,6 @@ var CodeOceanEditor = { }); }, - showTab: function(index) { - $('a[data-toggle="tab"]').eq(index || 0).tab('show'); - }, - showTimeoutMessage: function() { $.flash.info({ icon: ['fa', 'fa-clock-o'], @@ -574,13 +546,6 @@ var CodeOceanEditor = { }); }, - showWorkspaceTab: function(event) { - if(event){ - event.preventDefault(); - } - this.showTab(0); - }, - showFileDialog: function(event) { event.preventDefault(); this.createSubmission('#create-file', null, function(response) { @@ -637,7 +602,6 @@ var CodeOceanEditor = { this.initPrompt(); this.renderScore(); this.showFirstFile(); - this.showRequestedTab(); $(window).on("beforeunload", function() { if(this.autosaveTimer != null){ diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 2011de6b..fdc3d552 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -8,7 +8,6 @@ CodeOceanEditorEvaluation = { }, 0).toFixed(2); $('#score').data('score', score); this.renderScore(); - this.showTab(1); }, handleTestResponse: function (result) { @@ -18,7 +17,6 @@ CodeOceanEditorEvaluation = { this.qa_api.executeCommand('syncOutput', [result]); } this.showStatus(result); - this.showTab(0); this.showOutputBar(); }, @@ -102,7 +100,6 @@ CodeOceanEditorEvaluation = { scoreCode: function (event) { event.preventDefault(); - this.runmode = this.SERVER_SEND_EVENT; this.createSubmission('#assess', null, function (response) { this.showSpinner($('#assess')); $('#score_div').removeClass('hidden'); @@ -114,12 +111,7 @@ CodeOceanEditorEvaluation = { stopCode: function (event) { event.preventDefault(); if (this.isActiveFileStoppable()) { - if (this.runmode == this.WEBSOCKET) { - this.killWebsocketAndContainer(); - } else if (this.runmode == this.SERVER_SEND_EVENT) { - this.stopCodeServerSendEvent(event); - } - this.runmode = this.NONE; + this.killWebsocketAndContainer(); } }, diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index 213a9540..7ed7ad45 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -118,7 +118,6 @@ CodeOceanEditorSubmissions = { stderr: message }, true, 0); this.sendError(message, response.id); - this.showTab(0); this.showOutputBar(); }; } @@ -130,7 +129,6 @@ CodeOceanEditorSubmissions = { runCode: function(event) { event.preventDefault(); if ($('#run').is(':visible')) { - this.runmode = this.WEBSOCKET; this.createSubmission('#run', null, function(response) { //Run part starts here $('#stop').data('url', response.stop_url); From 542ec78076feaa3214543b1ed1df32878385a1b4 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 9 Sep 2016 15:43:50 +0200 Subject: [PATCH 38/57] remove some more tab code not used any longer. --- app/assets/javascripts/editor/editor.js.erb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 5385319b..09c640eb 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -275,7 +275,6 @@ var CodeOceanEditor = { initializeEventHandlers: function () { $(document).on('click', '#results a', this.showOutput.bind(this)); $(document).on('keypress', this.handleKeyPress.bind(this)); - $('a[data-toggle="tab"]').on('show.bs.tab', this.storeTab.bind(this)); this.initializeFileTreeButtons(); this.initializeWorkflowButtons(); this.initializeWorkspaceButtons(); @@ -482,10 +481,6 @@ var CodeOceanEditor = { } }, - storeTab: function (event) { - localStorage.tab = $(event.target).parent().index(); - }, - resetOutputTab: function () { this.clearOutput(); $('#hint').fadeOut(); From 2b621e2de6bd63f3b3dd1c5dee0a6e6b8436ed3c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 9 Sep 2016 16:10:43 +0200 Subject: [PATCH 39/57] some more code cleanup and UI optimizations --- app/assets/javascripts/editor/editor.js.erb | 13 +++---------- app/assets/javascripts/editor/submissions.js.erb | 1 + app/assets/stylesheets/editor.css.scss | 5 ++--- app/views/exercises/_editor_button.html.slim | 2 +- app/views/exercises/_editor_output.html.slim | 2 +- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 09c640eb..3d1c7257 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -139,9 +139,7 @@ var CodeOceanEditor = { }, handleKeyPress: function (event) { - if (event.which === this.ALT_1_KEY_CODE) { - this.showWorkspaceTab(event); - } else if (event.which === this.ALT_R_KEY_CODE) { + if (event.which === this.ALT_R_KEY_CODE) { $('#run').trigger('click'); } else if (event.which === this.ALT_S_KEY_CODE) { $('#assess').trigger('click'); @@ -276,7 +274,6 @@ var CodeOceanEditor = { $(document).on('click', '#results a', this.showOutput.bind(this)); $(document).on('keypress', this.handleKeyPress.bind(this)); this.initializeFileTreeButtons(); - this.initializeWorkflowButtons(); this.initializeWorkspaceButtons(); this.initializeRequestForComments() }, @@ -324,12 +321,9 @@ var CodeOceanEditor = { $('[data-tooltip]').tooltip(); }, - initializeWorkflowButtons: function () { - $('#start').on('click', this.showWorkspaceTab.bind(this)); - $('#submit').on('click', this.submitCode.bind(this)); - }, 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)); @@ -452,8 +446,7 @@ var CodeOceanEditor = { var file = $(event.target).data('file'); var line = $(event.target).data('line'); - this.showWorkspaceTab(null); - // set active file ?!?! + // set active file, only needed for codepilot, so skipped for now var frame = $('div.frame[data-filename="' + file + '"]'); this.showFrame(frame); diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index 7ed7ad45..fec496bd 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -156,6 +156,7 @@ CodeOceanEditorSubmissions = { 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)); diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index 189dcb7f..e62c4f89 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -107,10 +107,11 @@ button i.fa-spin { .sidebar-col-collapsed { -webkit-transition: width 2s; transition: width 2s; - width:52px; + width:67px; float:left; min-height: 1px; padding-left: 15px; + padding-right: 15px; } .sidebar-col { @@ -125,8 +126,6 @@ button i.fa-spin { .editor-col { min-height: 1px; - padding-left: 15px; - padding-right: 15px; width:auto; height:100%; overflow:hidden; 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_output.html.slim b/app/views/exercises/_editor_output.html.slim index 90d72d22..ab09adac 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,6 +1,6 @@ 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' data-message-no-output=t('exercises.implement.no_output') +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')) From 3fd43fdee2f56da6ba2d96550c6be35ea5c05af6 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 16:41:20 +0200 Subject: [PATCH 40/57] Sending command 'client_kill' on stop button click now. Killing a container only if command client_kill was send. Moved propmpt to prompt.js --- app/assets/javascripts/editor.js.erb | 3 +- app/assets/javascripts/editor/editor.js.erb | 1 - .../javascripts/editor/evaluation.js.erb | 73 +++---------------- .../javascripts/editor/execution.js.erb | 3 +- app/assets/javascripts/editor/prompt.js | 45 ++++++++++++ app/controllers/submissions_controller.rb | 6 +- 6 files changed, 61 insertions(+), 70 deletions(-) create mode 100644 app/assets/javascripts/editor/prompt.js diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index e4f66c4c..87a39fb2 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -11,7 +11,8 @@ $(function() { CodeOceanEditorFlowr, CodeOceanEditorSubmissions, CodeOceanEditorTurtle, - CodeOceanEditorWebsocket + CodeOceanEditorWebsocket, + CodeOceanEditorPrompt ); if ($('#editor').isPresent() && CodeOceanEditor) { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 3d1c7257..c6ed46c2 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -28,7 +28,6 @@ var CodeOceanEditor = { output_mode_is_streaming: true, numMessages: 0, - prompt: '#prompt', lastCopyText: null, autosaveTimer: null, diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index fdc3d552..1c457b36 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -111,82 +111,27 @@ CodeOceanEditorEvaluation = { stopCode: function (event) { event.preventDefault(); if (this.isActiveFileStoppable()) { - this.killWebsocketAndContainer(); + this.websocket.send(JSON.stringify({'cmd': 'client_kill'})); + this.killWebsocket(); + this.cleanUpUI(); } }, - stopCodeServerSendEvent: function (event) { - var jqxhr = this.ajax({ - data: { - container_id: $('#stop').data('container').id - }, - url: $('#stop').data('url') - }); - jqxhr.always(function () { - this.hideSpinner(); - this.running = false; - this.toggleButtonStates(); - }.bind(this)); - jqxhr.fail(ajaxError); - }, - - killWebsocketAndContainer: function () { - if (this.websocket.getReadyState() != WebSocket.OPEN) { + killWebsocket: function () { + if (this.websocket != null && this.websocket.getReadyState() != WebSocket.OPEN) { return; } - this.websocket.send(JSON.stringify({cmd: 'exit'})); this.websocket.killWebSocket(); - - this.hideSpinner(); this.running = false; + }, + + cleanUpUI: function() { + this.hideSpinner(); this.toggleButtonStates(); this.hidePrompt(); }, - //TODO: Move Prompt Part in own component - 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(); - } - }, - renderWebsocketOutput: function(msg){ var element = this.findOrCreateRenderElement(0); element.append(msg.data); diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb index c28c47a4..acc7eb55 100644 --- a/app/assets/javascripts/editor/execution.js.erb +++ b/app/assets/javascripts/editor/execution.js.erb @@ -39,10 +39,11 @@ CodeOceanEditorWebsocket = { }, handleExitCommand: function() { - this.killWebsocketAndContainer(); + 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/prompt.js b/app/assets/javascripts/editor/prompt.js new file mode 100644 index 00000000..1cbdfc8f --- /dev/null +++ b/app/assets/javascripts/editor/prompt.js @@ -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/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 865ff747..7ff99437 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -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) From 6ff3d3680957d233e9de45bc5a3226a58898c2fc Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 17:06:30 +0200 Subject: [PATCH 41/57] Fixed container-ending on exit. --- app/controllers/submissions_controller.rb | 5 +++-- lib/docker_client.rb | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 7ff99437..c272b177 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| @@ -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]) diff --git a/lib/docker_client.rb b/lib/docker_client.rb index f993c37c..6bcf7392 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -261,16 +261,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 +291,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) From 493bf98850990b03e5be1f6cef9acebb7f7c65ba Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 17:17:54 +0200 Subject: [PATCH 42/57] Removed unused functions. --- app/assets/javascripts/editor/turtle.js.erb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/assets/javascripts/editor/turtle.js.erb b/app/assets/javascripts/editor/turtle.js.erb index daebd59c..bc5552eb 100644 --- a/app/assets/javascripts/editor/turtle.js.erb +++ b/app/assets/javascripts/editor/turtle.js.erb @@ -42,19 +42,6 @@ CodeOceanEditorTurtle = { // initialize two-column layout $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); this.turtlecanvas.removeClass('hidden'); - //this.turtlecanvas.parent().parent().removeClass('hidden'); - } - }, - - hideCanvas: function () { - if ($('#turtlediv').isPresent() - && !(this.turtlecanvas.hasClass('hidden'))) { - var output = $('#output-col1'); - if (output.hasClass('two-column')) { - output.removeClass('col-lg-7 col-md-7 two-column'); - } - this.turtlecanvas.addClass('hidden'); - //this.turtlecanvas.parent().parent().addClass('hidden'); } } From 87d1b2388df73f1f13c67e394ebe38385d44b46a Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 17:24:04 +0200 Subject: [PATCH 43/57] Moved codepilot into dedicated file. Renamed prompt.js Moved requestComments out of flowr.js --- .../javascripts/editor/codepilot.js.erb | 24 ++++++++ app/assets/javascripts/editor/editor.js.erb | 2 - app/assets/javascripts/editor/flowr.js.erb | 56 +------------------ .../editor/{prompt.js => prompt.js.erb} | 0 .../javascripts/editor/submissions.js.erb | 31 ++++++++++ 5 files changed, 57 insertions(+), 56 deletions(-) create mode 100644 app/assets/javascripts/editor/codepilot.js.erb rename app/assets/javascripts/editor/{prompt.js => prompt.js.erb} (100%) 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 index c6ed46c2..94ce0654 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -24,7 +24,6 @@ var CodeOceanEditor = { active_file: undefined, active_frame: undefined, running: false, - qa_api: undefined, output_mode_is_streaming: true, numMessages: 0, @@ -35,7 +34,6 @@ var CodeOceanEditor = { ENTER_KEY_CODE: 13, - QaApiOutputBuffer: {'stdout': '', 'stderr': ''}, flowrResultHtml: '
', diff --git a/app/assets/javascripts/editor/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index 931b4043..a4e699f9 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -8,7 +8,7 @@ CodeOceanEditorFlowr = { var flowrHintBody = $('#flowrHint .panel-body'); var queryParameters = { query: this.flowrOutputBuffer - } + }; flowrHintBody.empty(); @@ -30,57 +30,5 @@ CodeOceanEditorFlowr = { }); this.flowrOutputBuffer = ''; - }, - - requestComments: function () { - var user_id = $('#editor').data('user-id'); - var exercise_id = $('#editor').data('exercise-id'); - var file_id = $('.editor').data('id'); - var question = $('#question').val(); - - var createRequestForComments = function (submission) { - $.ajax({ - method: 'POST', - url: '/request_for_comments', - data: { - request_for_comment: { - exercise_id: exercise_id, - file_id: file_id, - submission_id: submission.id, - question: question - } - } - }).done(function () { - this.hideSpinner(); - $.flash.success({text: $('#askForCommentsButton').data('message-success')}); - }.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); - }, - - //tODO move codepilot out of here. - 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/prompt.js b/app/assets/javascripts/editor/prompt.js.erb similarity index 100% rename from app/assets/javascripts/editor/prompt.js rename to app/assets/javascripts/editor/prompt.js.erb diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index fec496bd..510b813a 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -170,5 +170,36 @@ CodeOceanEditorSubmissions = { window.location = response.redirect; } }) + }, + + 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); } }; From 70040b3c6b1c922583fc5f238369a6988e97c5a9 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 18:08:41 +0200 Subject: [PATCH 44/57] Reordered everything again. --- app/assets/javascripts/editor.js.erb | 4 +- app/assets/javascripts/editor/editor.js.erb | 51 +++------- .../javascripts/editor/evaluation.js.erb | 97 +++++++++++-------- app/assets/javascripts/editor/flowr.js.erb | 1 + .../editor/paiticipantsupport.js.erb | 93 ++++++++++++++++++ .../javascripts/editor/submissions.js.erb | 86 ++++++++-------- 6 files changed, 209 insertions(+), 123 deletions(-) create mode 100644 app/assets/javascripts/editor/paiticipantsupport.js.erb diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb index 87a39fb2..f402e982 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js.erb @@ -12,7 +12,9 @@ $(function() { CodeOceanEditorSubmissions, CodeOceanEditorTurtle, CodeOceanEditorWebsocket, - CodeOceanEditorPrompt + CodeOceanEditorPrompt, + CodeOceanEditorCodePilot, + CodeOceanEditorRequestForComments ); if ($('#editor').isPresent() && CodeOceanEditor) { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 94ce0654..7f2672f5 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -1,18 +1,19 @@ var CodeOceanEditor = { + //ACE-Editor-Path ACE_FILES_PATH: '/assets/ace/', + THEME: 'ace/theme/textmate', + + //Color-Encoding for Percentages in Progress Bars (For submissions) ADEQUATE_PERCENTAGE: 50, - ALT_1_KEY_CODE: 161, - ALT_2_KEY_CODE: 8220, - ALT_3_KEY_CODE: 182, - ALT_4_KEY_CODE: 162, + SUCCESSFULL_PERCENTAGE: 90, + + //Key-Codes (for Hotkeys) ALT_R_KEY_CODE: 174, ALT_S_KEY_CODE: 8218, ALT_T_KEY_CODE: 8224, - FILENAME_URL_PLACEHOLDER: '{filename}', - SUCCESSFULL_PERCENTAGE: 90, - THEME: 'ace/theme/textmate', - REMEMBER_TAB: false, - AUTOSAVE_INTERVAL: 15 * 1000, + ENTER_KEY_CODE: 13, + + //Request-For-Comments-Configuration REQUEST_FOR_COMMENTS_DELAY: 3 * 60 * 1000, REQUEST_TOOLTIP_TIME: 5000, @@ -24,19 +25,9 @@ var CodeOceanEditor = { active_file: undefined, active_frame: undefined, running: false, - output_mode_is_streaming: true, - numMessages: 0, lastCopyText: null, - autosaveTimer: null, - autosaveLabel: "#autosave-label span", - - ENTER_KEY_CODE: 13, - - flowrResultHtml: '
', - - configureEditors: function () { _.each(['modePath', 'themePath', 'workerPath'], function (attribute) { ace.config.set(attribute, this.ACE_FILES_PATH); @@ -50,7 +41,6 @@ var CodeOceanEditor = { } }, - confirmReset: function (event) { event.preventDefault(); if (confirm($('#start-over').data('message-confirm'))) { @@ -171,21 +161,6 @@ var CodeOceanEditor = { $('button i.fa-spin').hide(); }, - resetSaveTimer: function () { - clearTimeout(this.autosaveTimer); - this.autosaveTimer = setTimeout(this.autosave.bind(this), this.AUTOSAVE_INTERVAL); - }, - - 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); - }, - 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; @@ -588,10 +563,6 @@ var CodeOceanEditor = { this.renderScore(); this.showFirstFile(); - $(window).on("beforeunload", function() { - if(this.autosaveTimer != null){ - this.autosave(); - } - }.bind(this)); + $(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 index 1c457b36..f7b51f30 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -1,6 +1,19 @@ 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) { @@ -10,40 +23,6 @@ CodeOceanEditorEvaluation = { this.renderScore(); }, - 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(); - }, - - printOutput: function (output, colorize, index) { - var element = this.findOrCreateOutputElement(index); - if (!colorize) { - if (output.stdout != undefined && output.stdout != '') { - element.append(output.stdout) - } - - if (output.stderr != undefined && output.stderr != '') { - element.append('There was an error: StdErr: ' + output.stderr); - } - - } else if (output.stderr) { - element.addClass('text-warning').append(output.stderr); - this.flowrOutputBuffer += output.stderr; - this.QaApiOutputBuffer.stderr += output.stderr; - } else if (output.stdout) { - element.addClass('text-success').append(output.stdout); - this.flowrOutputBuffer += output.stdout; - this.QaApiOutputBuffer.stdout += output.stdout; - } else { - element.addClass('text-muted').text($('#output').data('message-no-output')); - } - }, - printScoringResult: function (result, index) { $('#results').show(); var panel = $('#dummies').children().first().clone(); @@ -98,16 +77,22 @@ CodeOceanEditorEvaluation = { this.renderProgressBar(score, maximum_score); }, - 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)); + /** + * 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()) { @@ -132,6 +117,9 @@ CodeOceanEditorEvaluation = { this.hidePrompt(); }, + /** + * Output-Logic + */ renderWebsocketOutput: function(msg){ var element = this.findOrCreateRenderElement(0); element.append(msg.data); @@ -149,6 +137,31 @@ CodeOceanEditorEvaluation = { 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/flowr.js.erb b/app/assets/javascripts/editor/flowr.js.erb index a4e699f9..4dc0de45 100644 --- a/app/assets/javascripts/editor/flowr.js.erb +++ b/app/assets/javascripts/editor/flowr.js.erb @@ -1,5 +1,6 @@ CodeOceanEditorFlowr = { isFlowrEnabled: true, + flowrResultHtml: '
', handleStderrOutputForFlowr: function () { if (!this.isFlowrEnabled) return; diff --git a/app/assets/javascripts/editor/paiticipantsupport.js.erb b/app/assets/javascripts/editor/paiticipantsupport.js.erb new file mode 100644 index 00000000..1329cef2 --- /dev/null +++ b/app/assets/javascripts/editor/paiticipantsupport.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/submissions.js.erb b/app/assets/javascripts/editor/submissions.js.erb index 510b813a..4b8678a7 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js.erb @@ -1,17 +1,13 @@ - CodeOceanEditorSubmissions = { - collectFiles: function() { - var editable_editors = _.filter(this.editors, function(editor) { - return !editor.getReadOnly(); - }); - return _.map(editable_editors, function(editor) { - return { - content: editor.getValue(), - file_id: $(editor.container).data('file-id') - }; - }); - }, + 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({ @@ -36,6 +32,18 @@ CodeOceanEditorSubmissions = { 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) { @@ -69,6 +77,9 @@ CodeOceanEditorSubmissions = { this.toggleButtonStates(); }, + /** + * File-Management + */ destroyFile: function() { this.createSubmission($('#destroy-file'), function(files) { return _.reject(files, function(file) { @@ -125,7 +136,9 @@ CodeOceanEditorSubmissions = { } }, - //Todo Split up in submitpart and run part + /** + * Execution-Logic + */ runCode: function(event) { event.preventDefault(); if ($('#run').is(':visible')) { @@ -172,34 +185,27 @@ CodeOceanEditorSubmissions = { }) }, - 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(); + /** + * Autosave-Logic + */ + resetSaveTimer: function () { + clearTimeout(this.autosaveTimer); + this.autosaveTimer = setTimeout(this.autosave.bind(this), this.AUTOSAVE_INTERVAL); + }, - 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)); - }; + unloadAutoSave: function() { + if(this.autosaveTimer != null){ + this.autosave(); + } + }, - this.createSubmission($('.requestCommentsButton'), null, createRequestForComments.bind(this)); - - $('#comment-modal').modal('hide'); - var button = $('#requestComments'); - button.prop('disabled', true); + 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); } }; From 31c4f4b608aa204748fa18b377a91c55c672af96 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Fri, 9 Sep 2016 18:10:08 +0200 Subject: [PATCH 45/57] FIxed typo. --- .../{paiticipantsupport.js.erb => participantsupport.js.erb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/editor/{paiticipantsupport.js.erb => participantsupport.js.erb} (100%) diff --git a/app/assets/javascripts/editor/paiticipantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb similarity index 100% rename from app/assets/javascripts/editor/paiticipantsupport.js.erb rename to app/assets/javascripts/editor/participantsupport.js.erb From d26a0fa6a4741eeb360aca784f3e3559bdf5938b Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 27 Sep 2016 17:56:16 +0200 Subject: [PATCH 46/57] Enhance codeocean events with user agent and external-id if present --- app/assets/javascripts/editor/editor.js.erb | 7 ++++++- app/views/exercises/_editor.html.slim | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 7f2672f5..a9dc6a98 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -363,10 +363,15 @@ var CodeOceanEditor = { }, 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') + uuid: $('#editor').data('user-id'), + external_id: $('#editor').data('user-external-id') }, verb: { type: eventName diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 0b9d33c1..6b14d3c8 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,4 +1,5 @@ -#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id +- external_user_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') +#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' From 58dac37d77f777952e289d77e7e844d3f29df6cd Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 6 Oct 2016 16:14:51 +0200 Subject: [PATCH 47/57] added relative_url_root to URL for websocket call, needed for staging server --- app/assets/javascripts/editor/execution.js.erb | 3 ++- app/assets/javascripts/{exercises.js => exercises.js.erb} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename app/assets/javascripts/{exercises.js => exercises.js.erb} (100%) diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb index acc7eb55..3cc9b262 100644 --- a/app/assets/javascripts/editor/execution.js.erb +++ b/app/assets/javascripts/editor/execution.js.erb @@ -2,7 +2,8 @@ CodeOceanEditorWebsocket = { websocket: null, createSocketUrl: function(url) { - return '<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + 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) { diff --git a/app/assets/javascripts/exercises.js b/app/assets/javascripts/exercises.js.erb similarity index 100% rename from app/assets/javascripts/exercises.js rename to app/assets/javascripts/exercises.js.erb From 59119e43d5420bb1b794743d02fe8a2ccd9ea1be Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 6 Oct 2016 16:32:20 +0200 Subject: [PATCH 48/57] added relative_url_root to some more javascript calls (ace file path, file template path). --- app/assets/javascripts/editor/editor.js.erb | 3 ++- app/assets/javascripts/exercises.js.erb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index a9dc6a98..7d6f4556 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -1,6 +1,7 @@ var CodeOceanEditor = { //ACE-Editor-Path - ACE_FILES_PATH: '/assets/ace/', + // ruby part adds the relative_url_root, if it is set. + ACE_FILES_PATH: '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>' + '/assets/ace/', THEME: 'ace/theme/textmate', //Color-Encoding for Percentages in Progress Bars (For submissions) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 9d85b4f1..26e76218 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -152,8 +152,9 @@ $(function() { }; var updateFileTemplates = function(fileType) { + var rel_url_root = '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? 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) { From 1dfd6f61dc909bb63dda0a999e3447e9e298138e Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 7 Oct 2016 14:16:24 +0200 Subject: [PATCH 49/57] some more relative url roots in editor_edit, removed javascript uglifying for staging, added debug message for docker websocket connection url. added doku for setting up docker daemon for codeocean --- .../javascripts/{editor_edit.js => editor_edit.js.erb} | 3 ++- codeocean-dockerconfig.md | 9 +++++++++ config/environments/staging.rb | 2 +- lib/docker_client.rb | 5 ++++- 4 files changed, 16 insertions(+), 3 deletions(-) rename app/assets/javascripts/{editor_edit.js => editor_edit.js.erb} (89%) create mode 100644 codeocean-dockerconfig.md diff --git a/app/assets/javascripts/editor_edit.js b/app/assets/javascripts/editor_edit.js.erb similarity index 89% rename from app/assets/javascripts/editor_edit.js rename to app/assets/javascripts/editor_edit.js.erb index b1251cf9..fa3d40fc 100644 --- a/app/assets/javascripts/editor_edit.js +++ b/app/assets/javascripts/editor_edit.js.erb @@ -1,5 +1,6 @@ $(function() { - var ACE_FILES_PATH = '/assets/ace/'; + // ruby part adds the relative_url_root, if it is set. + var ACE_FILES_PATH = '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>' + '/assets/ace/'; var THEME = 'ace/theme/textmate'; var configureEditors = function() { diff --git a/codeocean-dockerconfig.md b/codeocean-dockerconfig.md new file mode 100644 index 00000000..4facaee4 --- /dev/null +++ b/codeocean-dockerconfig.md @@ -0,0 +1,9 @@ +In order to make containers accessible for codeocean, they need to be reachable via tcp. +For this, the docker daemon has to be started with the following options: + +DOCKER_OPTS='-H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --iptables=false' + +This binds the daemon to the specified socket (for access via the command line on the machine) as well as the specified tcp url. +Either pass these options to the starting call, or specify them in the docker config file. + +In Ubuntu, this file is located under: /ect/default/docker \ No newline at end of file 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/lib/docker_client.rb b/lib/docker_client.rb index 6bcf7392..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 From 4cf192d0f3d9e59292baa2023ba41fb8fff229d2 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 12 Oct 2016 14:37:09 +0200 Subject: [PATCH 50/57] fix wrong link in refactor, since "this" is no longer available in javascript calls. --- app/assets/javascripts/editor/editor.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 7d6f4556..5633ce4e 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -86,7 +86,7 @@ var CodeOceanEditor = { showOutput: function(event) { event.preventDefault(); this.showOutputBar(); - $('#output').scrollTo($(this).attr('href')); + $('#output').scrollTo($(event.target).attr('href')); }, renderProgressBar: function(score, maximum_score) { From accf0550d7505229fa36a5aaa00e3a9129777c41 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 12 Oct 2016 16:35:21 +0200 Subject: [PATCH 51/57] reworked the exercise edit dialog. Moved javascript part from editor_edit.js.erb to exercises.js.erb. Manipulated some further javascript as necessary. It is not super elegant, but this is due to the former structure of the code which uses cloned dummy forms. Integrating the ace editor made some strange calls necessary. Also fixed toggling the input area and the file upload dialog --- app/assets/javascripts/editor_edit.js.erb | 56 ----------------- app/assets/javascripts/exercises.js.erb | 70 +++++++++++++++++++++- app/assets/stylesheets/editor.css.scss | 10 ++++ app/views/exercises/_code_field.html.slim | 3 +- app/views/exercises/_editor_edit.html.slim | 4 +- app/views/exercises/_file_form.html.slim | 3 +- 6 files changed, 84 insertions(+), 62 deletions(-) diff --git a/app/assets/javascripts/editor_edit.js.erb b/app/assets/javascripts/editor_edit.js.erb index fa3d40fc..e69de29b 100644 --- a/app/assets/javascripts/editor_edit.js.erb +++ b/app/assets/javascripts/editor_edit.js.erb @@ -1,56 +0,0 @@ -$(function() { - // ruby part adds the relative_url_root, if it is set. - var ACE_FILES_PATH = '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>' + '/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/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 26e76218..73799bec 100644 --- a/app/assets/javascripts/exercises.js.erb +++ 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? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? 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() { @@ -193,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 e62c4f89..755e3409 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -7,6 +7,16 @@ 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; 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_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/_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 From ac88956d271c71489f2f9fa250c79ab45ff48da1 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 12 Oct 2016 18:15:16 +0200 Subject: [PATCH 52/57] Put blocking score submission call into a thread to keep the puma server responsive --- app/controllers/submissions_controller.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index c272b177..63507ede 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -232,8 +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)) - tubesock.send_data JSON.dump({'cmd' => 'exit'}) + + # 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 From 181578ca87eb8d8020c53c337b9a4233e91b8853 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 28 Oct 2016 17:13:21 +0200 Subject: [PATCH 53/57] change call to retrieve relative_url_root --- app/assets/javascripts/editor/editor.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 5633ce4e..c5f8a56b 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -1,7 +1,7 @@ var CodeOceanEditor = { //ACE-Editor-Path // ruby part adds the relative_url_root, if it is set. - ACE_FILES_PATH: '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>' + '/assets/ace/', + 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) From b0ceeea6503c00c7a5d47e4c446a123e2cb53860 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 28 Oct 2016 17:22:31 +0200 Subject: [PATCH 54/57] fix retrieval of relative_url_root also for other calls. --- app/assets/javascripts/exercises.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 73799bec..2a17b405 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -1,6 +1,6 @@ $(function() { // ruby part adds the relative_url_root, if it is set. - var ACE_FILES_PATH = '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>' + '/assets/ace/'; + 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; @@ -211,7 +211,7 @@ $(function() { }; var updateFileTemplates = function(fileType) { - var rel_url_root = '<%= (defined? config.relative_url_root) && config.relative_url_root != nil && config.relative_url_root != "" ? config.relative_url_root : "" %>'; + 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: rel_url_root + '/file_templates/by_file_type/' + fileType + '.json', dataType: 'json' From e8f93cb870e3dbec7dea3cc4d27a5cf2c990e543 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 9 Nov 2016 17:56:56 +0100 Subject: [PATCH 55/57] Removed bin folder from linked_dirs in deploy, otherwise no rails console can be started on the servers, because the directory will be empty (as stated on several stackoverflow questions, for example: http://stackoverflow.com/questions/29039927/rails-4-doesnt-detect-application-after-capistrano-deployment ) --- config/deploy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deploy.rb b/config/deploy.rb index a1567cb7..173f2b56 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -3,7 +3,7 @@ set :config_example_suffix, '.example' set :default_env, 'PATH' => '/usr/java/jdk1.8.0_40/bin:$PATH' set :deploy_to, '/var/www/app' set :keep_releases, 3 -set :linked_dirs, %w(bin log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets) +set :linked_dirs, %w(log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets) set :linked_files, %w(config/action_mailer.yml config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml) set :log_level, :info set :puma_threads, [0, 16] From ff7446fde6972acd28e4612246350efc77965597 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 9 Nov 2016 17:58:10 +0100 Subject: [PATCH 56/57] redirect to RFCs on max score: redirect users to their own RFCs if they are open, fixed wrong usage of user_id (external_id is not used in RFCs, normal id has to be used!). --- app/controllers/exercises_controller.rb | 35 ++++++++++++------- .../request_for_comments_controller.rb | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 75451eb3..cb6e9c9d 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -247,23 +247,34 @@ class ExercisesController < ApplicationController end def redirect_after_submit - Rails.logger.debug('Score ' + @submission.normalized_score.to_s) + Rails.logger.debug('Redirecting user with score:s ' + @submission.normalized_score.to_s) if @submission.normalized_score == 1.0 - # if user has an own rfc, redirect to it and message him to clean up and accept the answer. + # if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external, + # otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.) + if current_user.respond_to? :external_id + if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first + # set a message that informs the user that his own RFC should be closed. + flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc') + flash.keep(:notice) - # else: show open rfc for same exercise - if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).order("RANDOM()").first + respond_to do |format| + format.html { redirect_to(rfc) } + format.json { render(json: {redirect: url_for(rfc)}) } + end + return - # set a message that informs the user that his score was perfect and help in RFC is greatly appreciated. - flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc') - flash.keep(:notice) + # else: show open rfc for same exercise if available + elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).order("RANDOM()").first + # set a message that informs the user that his score was perfect and help in RFC is greatly appreciated. + flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc') + flash.keep(:notice) - respond_to do |format| - format.html { redirect_to(rfc) } - format.json { render(json: {redirect: url_for(rfc)}) } + respond_to do |format| + format.html { redirect_to(rfc) } + format.json { render(json: {redirect: url_for(rfc)}) } + end + return end - - return end end redirect_to_lti_return_path diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb index 37d8bef9..f9e7137e 100644 --- a/app/controllers/request_for_comments_controller.rb +++ b/app/controllers/request_for_comments_controller.rb @@ -82,6 +82,7 @@ class RequestForCommentsController < ApplicationController # Never trust parameters from the scary internet, only allow the white list through. def request_for_comment_params + # we are using the current_user.id here, since internal users are not able to create comments. The external_user.id is a primary key and does not require the consumer_id to be unique. params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name) end end diff --git a/config/locales/de.yml b/config/locales/de.yml index a57b2c04..b34d6a7a 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -274,6 +274,7 @@ de: submit: failure: Beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut. full_score_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über ihre Hilfe und Kommentare freuen. + full_score_redirect_to_own_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ihre Frage ist damit wahrscheinlich gelöst? Falls ja, fügen Sie doch den entscheidenden Kniff als Antwort hinzu und markieren die Frage als gelöst, bevor sie das Fenster schließen. external_users: statistics: no_data_available: Keine Daten verfügbar. diff --git a/config/locales/en.yml b/config/locales/en.yml index 691dfe72..e1663e5d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -274,6 +274,7 @@ en: submit: failure: An error occured while transmitting your score. Please try again later. full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated! + full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window! external_users: statistics: no_data_available: No data available. From bfa12899a5f687b4ecdb236fb6ec17b2b8d47483 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 9 Nov 2016 18:09:24 +0100 Subject: [PATCH 57/57] adjust german locales to comply with the standard form of address --- config/locales/de.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 8428edcf..0695eda6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -208,7 +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: Wenn du Hilfe mit deinem Code benötigst, kannst dir hier Kommentare erbitten + requestCommentsTooltip: Falls Sie Hilfe mit Ihrem Code benötigen, können Sie hier Kommentare erbitten save: Speichern score: Bewerten send: Senden