diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 7ec977a1..8f744e76 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -13,9 +13,10 @@ $(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 qa_api = undefined; var flowrResultHtml = '
' @@ -141,11 +142,20 @@ $(function() { event_source.addEventListener('close', handleStderrOutputForFlowr); } + if (qa_api) { + event_source.addEventListener('close', handleStreamedResponseForCodePilot); + } + event_source.addEventListener('status', function(event) { showStatus(JSON.parse(event.data)); }); }; + var handleStreamedResponseForCodePilot = function(event) { + qa_api.executeCommand('syncOutput', [chunkBuffer]); + chunkBuffer = [{streamedResponse: true}]; + } + var evaluateCodeWithoutStreamedResponse = function(url, callback) { var jqxhr = ajax({ method: 'GET', @@ -264,6 +274,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 +289,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); @@ -498,6 +524,13 @@ $(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(); }; @@ -530,10 +563,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(); @@ -580,6 +620,10 @@ $(function() { })) { showTimeoutMessage(); } + if (qa_api) { + // send test response to QA + qa_api.executeCommand('syncOutput', [response]); + } }; var renderCode = function(event) { @@ -703,10 +747,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(); @@ -865,19 +906,32 @@ $(function() { }) } - if ($('#editor').isPresent()) { - if (isBrowserSupported()) { - $('.score, #development-environment').show(); - configureEditors(); - initializeEditors(); - initializeEventHandlers(); - initializeFileTree(); - initializeTooltips(); - renderScore(); - showFirstFile(); - showRequestedTab(); - } else { - $('#alert').show(); + var initializeCodePilot = function() { + 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'); + + qa_api = new QaApi(node, url); + } } + + if ($('#editor').isPresent()) { + if (isBrowserSupported()) { + initializeCodePilot(); + $('.score, #development-environment').show(); + configureEditors(); + initializeEditors(); + initializeEventHandlers(); + initializeFileTree(); + initializeTooltips(); + renderScore(); + showFirstFile(); + showRequestedTab(); + } else { + $('#alert').show(); + } } }); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index d35c88cd..87fcec56 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -86,6 +86,12 @@ class ExercisesController < ApplicationController @submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first @files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension) @paths = collect_paths(@files) + + if current_user.respond_to? :external_id + @user_id = current_user.external_id + else + @user_id = '00000001-3100-4444-9999-000000000001' + end end def index 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/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index 9f72aa91..73421ff5 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -11,4 +11,5 @@ - else = link_to(file.native_file.file.name_with_extension, file.native_file.url) - else - .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 = file.content + .editor-content.hidden data-file-id=file.ancestor_id = file.content + .editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only 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/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index ee780de7..609579cf 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -34,6 +34,12 @@ html lang='en' .container data-controller=controller_name = render('breadcrumbs') = render('flash') - = yield + - if (controller_name == "exercises" && action_name == "implement") + .container-fluid + = yield + - else + .container + = yield + - template_variables = {execution_environment: @exercise.execution_environment} if action_name == 'implement' = render('shared/modal', classes: 'modal-lg', id: 'modal-help', template: 'application/help', template_variables: template_variables, title: t('shared.help.headline')) 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