From bad98cca1d74b5f68985c6e13d36055dbbce0a6a Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Fri, 27 Mar 2015 21:27:40 +0100 Subject: [PATCH] merged qa into codeocean --- app/assets/javascripts/editor.js | 126 ++++++++++++++++----- app/helpers/exercise_helper.rb | 13 +++ app/views/exercises/implement.html.slim | 140 ++++++++++++------------ config/code_ocean.yml.example | 5 + config/code_ocean.yml.travis | 2 + 5 files changed, 194 insertions(+), 92 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index c5b0f0ad..fba0fe51 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -13,8 +13,8 @@ $(function() { var THEME = 'ace/theme/textmate'; var editors = []; - var active_file; - var active_frame; + var active_file = undefined; + var active_frame = undefined; var running = false; var flowrResultHtml = '
' @@ -128,9 +128,22 @@ $(function() { var evaluateCodeWithStreamedResponse = function(url, callback) { var event_source = new EventSource(url); + event_source.addEventListener('close', function(event) { + event_source.close(); + hideSpinner(); + running = false; + toggleButtonStates(); + if (JSON.parse(event.data).code !== 200) { + ajaxError(); + showTab(1); + } - event_source.addEventListener('close', closeEventSource); - event_source.addEventListener('error', closeEventSource); + if (qa_api) { + qa_api.executeCommand('syncOutput', [chunkBuffer]); + chunkBuffer = [{streamedResponse: true}]; + } + }); + event_source.addEventListener('error', ajaxError); event_source.addEventListener('hint', renderHint); event_source.addEventListener('info', storeContainerInformation); event_source.addEventListener('output', callback); @@ -264,6 +277,9 @@ $(function() { var handleTestResponse = function(response) { clearOutput(); printOutput(response[0], false, 0); + if (qa_api) { + qa_api.executeCommand('syncOutput', [response]); + } showStatus(response[0]); showTab(2); }; @@ -276,6 +292,19 @@ $(function() { 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/)); editor.setReadOnly($(element).data('read-only') !== undefined); editor.setShowPrintMargin(false); editor.setTheme(THEME); @@ -500,10 +529,15 @@ $(function() { return 'executable' in active_frame.data(); }; - var isActiveFileRenderable = function() { - return 'renderable' 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')); }; @@ -532,10 +566,17 @@ $(function() { panel.find('.row .col-sm-9').eq(3).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); + printOutput(output, true, 0); + // send test response to QA + // we are expecting an array of outputs: + if (qa_api) { + chunkBuffer.push(output); + } } else { clearOutput(); $('#hint').fadeOut(); @@ -575,6 +616,10 @@ $(function() { printOutput(result, false, index); printScoringResult(result, index); }); + if (qa_api) { + // send test response to QA + qa_api.executeCommand('syncOutput', [response]); + } }; var renderCode = function(event) { @@ -698,10 +743,7 @@ $(function() { 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'); - active_file = { - filename: frame.data('filename'), - id: file_id - }; + setActiveFile(frame.data('filename'), file_id); $('#files').jstree().select_node(file_id); showFrame(frame); toggleButtonStates(); @@ -834,18 +876,52 @@ $(function() { }; if ($('#editor').isPresent()) { - if (isBrowserSupported()) { - $('.score, #development-environment').show(); - configureEditors(); - initializeEditors(); - initializeEventHandlers(); - initializeFileTree(); - initializeTooltips(); - renderScore(); - showFirstFile(); - showRequestedTab(); - } else { - $('#alert').show(); + var qa_api; + if ($('#questions-column').isPresent() && 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'); + + var qa_api = new QaApi(node, url); } + configureEditors(); + initializeEditors(qa_api); + initializeEventHandlers(); + initializeFileTree(); + initializeTooltips(); + renderScore(); + showMainFile(); + showRequestedTab(); } + + var stderrOutput = '' + var handleStderrOutputForFlowr = function(event) { + var json = JSON.parse(event.data); + + if (json.stderr) { + stderrOutput += json.stderr; + } else if (json.code) { + var flowrHintBody = $('#flowrHint .panel-body') + + jQuery.getJSON(flowrUrl + '&query=' + escape(stderrOutput), function(data) { + for (var question in data.queryResults) { + // replace everything, not only one occurence + var collapsibleTileHtml = flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + question).replace(/{{headingId}}/g, 'heading-' + question) + var resultTile = $(collapsibleTileHtml) + + resultTile.find('h4 > a').text(data.queryResults[question].title) + resultTile.find('.panel-body').append($(data.queryResults[question].body)) + + flowrHintBody.append(resultTile) + } + + $('#flowrHint').fadeIn() + }) + + + stderrOutput = '' + } + }; }); diff --git a/app/helpers/exercise_helper.rb b/app/helpers/exercise_helper.rb index 5520b75d..cab90c21 100644 --- a/app/helpers/exercise_helper.rb +++ b/app/helpers/exercise_helper.rb @@ -2,4 +2,17 @@ module ExerciseHelper def embedding_parameters(exercise) "locale=#{I18n.locale}&token=#{exercise.token}" end + + def qa_js_tag + javascript_include_tag qa_url + "/assets/qa_api.js" + end + + def qa_url + config = CodeOcean::Config.new(:code_ocean) + enabled = config.read[:code_pilot][:enabled] + + if enabled + config.read[:code_pilot][:url] + end + end end diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index ea14acdb..47cac7c1 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -1,73 +1,79 @@ -h1 = @exercise +.row + #editor-column.col-md-10.col-md-offset-1 + h1 = @exercise -span.badge.pull-right.score + span.badge.pull-right.score -p.lead = @exercise.description + p.lead = @exercise.description -#alert.alert.alert-danger role='alert' - h4 = t('.alert.title') - p = t('.alert.text', application_name: application_name) + #alert.alert.alert-danger role='alert' + h4 = t('.alert.title') + p = t('.alert.text', application_name: application_name) -#development-environment - ul.nav.nav-justified.nav-tabs role='tablist' - li.active - a data-placement='top' data-toggle='tab' data-tooltip=true href='#instructions' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1') - i.fa.fa-question - = t('activerecord.attributes.exercise.instructions') - li - a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2') - 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 + 3') - 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 + 4') - i.fa.fa-line-chart - = t('.progress') + #development-environment + ul.nav.nav-justified.nav-tabs role='tablist' + li.active + a data-placement='top' data-toggle='tab' data-tooltip=true href='#instructions' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1') + i.fa.fa-question + = t('activerecord.attributes.exercise.instructions') + li + a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2') + 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 + 3') + 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 + 4') + i.fa.fa-line-chart + = t('.progress') - hr + hr - .tab-content - #instructions.tab-pane.active - p = render_markdown(@exercise.instructions) - br - p.text-center - a#start.btn.btn-lg.btn-success - i.fa.fa-code - = t('.start') - #workspace.tab-pane = 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 - #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 - #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: '.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 - p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-message-confirm' => t('exercises.editor.confirm_submit'), :'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) + .tab-content + #instructions.tab-pane.active + p = render_markdown(@exercise.instructions) + br + p.text-center + a#start.btn.btn-lg.btn-success + i.fa.fa-code + = t('.start') + #workspace.tab-pane = 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 + #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 + #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: '.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 + p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-message-confirm' => t('exercises.editor.confirm_submit'), :'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) + - if qa_url + #questions-column + #questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}" + = qa_js_tag \ No newline at end of file diff --git a/config/code_ocean.yml.example b/config/code_ocean.yml.example index d71d4d06..86bd7971 100644 --- a/config/code_ocean.yml.example +++ b/config/code_ocean.yml.example @@ -1,11 +1,16 @@ default: &default flowr: enabled: false + code_pilot: + enabled: false development: flowr: enabled: true url: http://example.org:3000/api/exceptioninfo?id=&lang=auto + code_pilot: + enabled: true + url: //localhost:3000 production: <<: *default diff --git a/config/code_ocean.yml.travis b/config/code_ocean.yml.travis index e093289f..28485afc 100644 --- a/config/code_ocean.yml.travis +++ b/config/code_ocean.yml.travis @@ -1,3 +1,5 @@ test: flowr: enabled: false + code_pilot: + enabled: false \ No newline at end of file