From 842a38c13ad472a7e0313e95e5b3eaf4de4e36ed Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Thu, 11 Aug 2016 23:24:38 +0200 Subject: [PATCH] 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