From 097938aa6b42d011614d56855fae77cf83d17101 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 18 Sep 2018 10:34:38 +0200 Subject: [PATCH 01/16] Implement server side query building for flowr --- app/controllers/flowr_controller.rb | 42 +++++++++++++++++++++++++++++ app/policies/submission_policy.rb | 2 +- config/routes.rb | 2 ++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/controllers/flowr_controller.rb diff --git a/app/controllers/flowr_controller.rb b/app/controllers/flowr_controller.rb new file mode 100644 index 00000000..b56baf88 --- /dev/null +++ b/app/controllers/flowr_controller.rb @@ -0,0 +1,42 @@ +class FlowrController < ApplicationController + + def insights + unless current_user + skip_authorization + respond_to do |format| + format.html { render_not_authorized } + format.json { render json: {}, status: :unauthorized } + end + else + # get the latest submission for this user that also has a test run (i.e. structured_errors if applicable) + submission = Submission.joins(:testruns) + .where(submissions: {user_id: current_user.id, user_type: current_user.class.name}) + .order('testruns.created_at DESC').first + # verify authorization for the submission, as all queried errors are generated by this submission anyway + # and structured_errors don't have a policy yet + authorize(submission) + errors = StructuredError.where(submission_id: submission.id) + + # for each error get all attributes, filter out uninteresting ones, and build a query + insights = errors.map do |error| + attributes = error.structured_error_attributes.select do |attribute| + is_interesting(attribute) and attribute.match + end + # once the programming language model becomes available, the language name can be added to the query to + # produce more relevant results + query = attributes.map{|att| att.value}.join(' ') + { submission: submission, error: error, attributes: attributes, query: query } + end + + respond_to do |format| + format.html { render json: insights, status: :ok } + format.json { render json: insights, status: :ok } + end + end + end + + def is_interesting(attribute) + attribute.error_template_attribute.key.index(/error message|error type/i) != nil + end + private :is_interesting +end diff --git a/app/policies/submission_policy.rb b/app/policies/submission_policy.rb index 861f5695..ffb0c804 100644 --- a/app/policies/submission_policy.rb +++ b/app/policies/submission_policy.rb @@ -8,7 +8,7 @@ class SubmissionPolicy < ApplicationPolicy everyone end - [:download?, :download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action| + [:download?, :download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?, :insights?].each do |action| define_method(action) { admin? || author? } end diff --git a/config/routes.rb b/config/routes.rb index 7c1f317b..bf07d7f8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,8 @@ Rails.application.routes.draw do get '/help', to: 'application#help' + get '/insights', to: 'flowr#insights' + get 'statistics/', to: 'statistics#show' get 'statistics/graphs', to: 'statistics#graphs' get 'statistics/graphs/user-activity', to: 'statistics#user_activity' From 890b73fe6e06301d0b340b08e551fe56e1149aae Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 18 Sep 2018 11:01:33 +0200 Subject: [PATCH 02/16] Remove old flowr url from example configuration --- config/code_ocean.yml.example | 1 - 1 file changed, 1 deletion(-) diff --git a/config/code_ocean.yml.example b/config/code_ocean.yml.example index e8cb10d0..b21c190e 100644 --- a/config/code_ocean.yml.example +++ b/config/code_ocean.yml.example @@ -7,7 +7,6 @@ default: &default development: flowr: enabled: true - url: http://example.org:3000/api/exceptioninfo?id=&lang=auto code_pilot: enabled: false url: //localhost:3000 From b9054bbcba8a979b428f66271cac6aa199605645 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 18 Sep 2018 15:13:44 +0200 Subject: [PATCH 03/16] Localize flowr heading --- app/views/exercises/_editor_output.html.slim | 2 +- config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index 2c760ec5..f79cc3d1 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -54,5 +54,5 @@ div id='output_sidebar_uncollapsed' class='hidden col-sm-12 enforce-bottom-margi pre = t('exercises.implement.no_output_yet') - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] #flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' - .panel-heading = 'Gain more insights here' + .panel-heading = t('exercises.implement.flowr.heading') .panel-body diff --git a/config/locales/de.yml b/config/locales/de.yml index ae535eb9..a8a1c53b 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -338,6 +338,8 @@ de: text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht später weiter machen um erstmal auf neue Gedanken zu kommen?" error_hints: heading: "Hinweise" + flowr: + heading: "Weitere Hinweise | Unterstützt von StackOverflow" index: clone: Duplizieren implement: Implementieren diff --git a/config/locales/en.yml b/config/locales/en.yml index 8501572f..9ac9b0ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -338,6 +338,8 @@ en: text: "We recognized that you are already working quite a while on this exercise. We would like to encourage you to take a break and come back later." error_hints: heading: "Hints" + flowr: + heading: "Gain more insights here | Powered by StackOverflow" index: clone: Duplicate implement: Implement From b19402ba99c9322a174b2289163f37a8da37e28f Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 18 Sep 2018 15:14:17 +0200 Subject: [PATCH 04/16] Implement client-side querying of stackoverflow --- .../javascripts/editor/evaluation.js.erb | 2 - .../editor/participantsupport.js.erb | 96 +++++++++++++++---- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 19f57ce3..806e3a91 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -164,12 +164,10 @@ CodeOceanEditorEvaluation = { } else if (output.stderr) { element.addClass('text-warning').append(output.stderr); //element.addClass('text-warning').text(element.text() + output.stderr); - this.flowrOutputBuffer += output.stderr; this.QaApiOutputBuffer.stderr += output.stderr; } else if (output.stdout) { element.addClass('text-success').append(output.stdout); //element.addClass('text-success').text(element.text() + output.stdout); - this.flowrOutputBuffer += output.stdout; this.QaApiOutputBuffer.stdout += output.stdout; } else { element.addClass('text-muted').text($('#output').data('message-no-output')); diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index a826cbf0..5e6fd4a4 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -1,36 +1,96 @@ CodeOceanEditorFlowr = { - isFlowrEnabled: true, - flowrResultHtml: '
', + isFlowrEnabled: <%= CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] %>, + flowrResultHtml: + '
' + + '' + + '
' + + '
' + + '
' + + '
', + getInsights: function () { + <% self.class.include Rails.application.routes.url_helpers %> + var flowrUrl = '<%= insights_path %>'; + var stackOverflowUrl = 'http://api.stackexchange.com/2.2/search/advanced'; + + return jQuery.ajax({ + dataType: "json", + url: flowrUrl, + data: {} + }).then(function (queries) { + var stackoverflowRequests = _.map(queries, function (index, query) { + var queryParams = { + accepted: true, + pagesize: 5, + order: 'desc', + sort: 'relevance', + site: 'stackoverflow', + answers: 1, + filter: '!23qca9v**HCO.ESF)dHfT', + q: query + } + + return jQuery.ajax({ + dataType: "json", + url: stackOverflowUrl, + data: queryParams + }).promise(); + }); + return jQuery.when.apply(jQuery, stackoverflowRequests); + }); + }, + collectResults: function(response) { + var results = []; + var addToResultsIfSuccessful = function (data, textStatus, jqXHR) { + if (jqXHR && jqXHR.status === 200) { + _.each(data.items, function (item) { + if (!_.contains(results, item)) { + results.push(item); + } + }); + } + } + + if (_.isArray(response[0])) { + // multiple queries + _.each(response, function (args) { + addToResultsIfSuccessful.apply(this, args) + }); + } else { + // single query + addToResultsIfSuccessful.apply(this, response); + } + return results; + }, handleStderrOutputForFlowr: function () { - if (!this.isFlowrEnabled) return; + if (! this.isFlowrEnabled) return; - var flowrUrl = $('#flowrHint').data('url'); var flowrHintBody = $('#flowrHint .panel-body'); - var queryParameters = { - query: this.flowrOutputBuffer - }; - flowrHintBody.empty(); + var self = this; - 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); + this.getInsights().then(function () { + var results = self.collectResults(arguments); + _.each(results, function (result, index) { + var collapsibleTileHtml = self.flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + index).replace(/{{headingId}}/g, 'heading-' + index); 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'); + resultTile.find('h4 > a').text(result.title); + resultTile.find('.panel-body').html(result.body); + resultTile.find('.panel-body').append('Open this question'); flowrHintBody.append(resultTile); }); - if (data.queryResults.length !== 0) { + if (results.length > 0) { $('#flowrHint').fadeIn(); } }); - - this.flowrOutputBuffer = ''; } }; @@ -93,4 +153,4 @@ CodeOceanEditorRequestForComments = { //var button = $('#requestComments'); //button.prop('disabled', true); }, -}; \ No newline at end of file +}; From dd48cc6d10b217e94e8fa8857fc92384ed302b9e Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 18 Sep 2018 17:22:01 +0200 Subject: [PATCH 05/16] Create events on user interaction --- .../editor/participantsupport.js.erb | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index 5e6fd4a4..eb02176e 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -67,6 +67,16 @@ CodeOceanEditorFlowr = { } return results; }, + createEventHandler: function (eventType, data) { + return function () { + CodeOceanEditor.publishCodeOceanEvent({ + category: eventType, + data: data, + exercise_id: $('#editor').data('exercise-id'), + file_id: null + }); + }; + }, handleStderrOutputForFlowr: function () { if (! this.isFlowrEnabled) return; @@ -80,9 +90,16 @@ CodeOceanEditorFlowr = { var collapsibleTileHtml = self.flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + index).replace(/{{headingId}}/g, 'heading-' + index); var resultTile = $(collapsibleTileHtml); - resultTile.find('h4 > a').text(result.title); - resultTile.find('.panel-body').html(result.body); - resultTile.find('.panel-body').append('Open this question'); + var questionUrl = 'https://stackoverflow.com/questions/' + result.question_id; + + var header = resultTile.find('h4 > a'); + header.text(result.title); + header.on('click', self.createEventHandler('editor_flowr_expand_question', questionUrl)); + + var body = resultTile.find('.panel-body'); + body.html(result.body); + body.append('Open this question'); + body.find('.btn').on('click', self.createEventHandler('editor_flowr_click_question', questionUrl)); flowrHintBody.append(resultTile); }); From fad97e36c1b1ea6ceeeee93b7c44d6b71762a117 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 5 Oct 2018 14:48:09 +0200 Subject: [PATCH 06/16] Fix building stackoverflow query --- app/assets/javascripts/editor/participantsupport.js.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index eb02176e..8bc68fdd 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -22,17 +22,17 @@ CodeOceanEditorFlowr = { dataType: "json", url: flowrUrl, data: {} - }).then(function (queries) { - var stackoverflowRequests = _.map(queries, function (index, query) { + }).then(function (insights) { + var stackoverflowRequests = _.map(insights, function (insight) { var queryParams = { accepted: true, - pagesize: 5, + pagesize: 3, order: 'desc', sort: 'relevance', site: 'stackoverflow', answers: 1, filter: '!23qca9v**HCO.ESF)dHfT', - q: query + q: insight.query } return jQuery.ajax({ From 174db96081844db3303f3a2d9cc0c2e6d4f5438a Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 5 Oct 2018 14:55:22 +0200 Subject: [PATCH 07/16] Move magic number to config instead --- app/assets/javascripts/editor/participantsupport.js.erb | 7 ++++--- config/code_ocean.yml.example | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index 8bc68fdd..e3f81aad 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -26,12 +26,12 @@ CodeOceanEditorFlowr = { var stackoverflowRequests = _.map(insights, function (insight) { var queryParams = { accepted: true, - pagesize: 3, + pagesize: <%= CodeOcean::Config.new(:code_ocean).read[:flowr][:answers_per_query] %>, order: 'desc', sort: 'relevance', site: 'stackoverflow', answers: 1, - filter: '!23qca9v**HCO.ESF)dHfT', + filter: '!23qca9v**HCO.ESF)dHfT', // title, body, accepted answer q: insight.query } @@ -87,7 +87,8 @@ CodeOceanEditorFlowr = { this.getInsights().then(function () { var results = self.collectResults(arguments); _.each(results, function (result, index) { - var collapsibleTileHtml = self.flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + index).replace(/{{headingId}}/g, 'heading-' + index); + var collapsibleTileHtml = self.flowrResultHtml + .replace(/{{collapseId}}/g, 'collapse-' + index).replace(/{{headingId}}/g, 'heading-' + index); var resultTile = $(collapsibleTileHtml); var questionUrl = 'https://stackoverflow.com/questions/' + result.question_id; diff --git a/config/code_ocean.yml.example b/config/code_ocean.yml.example index b21c190e..f931bc68 100644 --- a/config/code_ocean.yml.example +++ b/config/code_ocean.yml.example @@ -1,12 +1,14 @@ default: &default flowr: enabled: false + answers_per_query: 3 code_pilot: enabled: false development: flowr: enabled: true + answers_per_query: 3 code_pilot: enabled: false url: //localhost:3000 From a07d440e026d33b9e179c4b933ffaec00e66cc08 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 5 Oct 2018 15:03:27 +0200 Subject: [PATCH 08/16] Translate button --- app/assets/javascripts/editor/participantsupport.js.erb | 3 ++- config/locales/de.yml | 1 + config/locales/en.yml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index e3f81aad..040ee387 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -99,7 +99,8 @@ CodeOceanEditorFlowr = { var body = resultTile.find('.panel-body'); body.html(result.body); - body.append('Open this question'); + body.append('' + + '<%= I18n.t('exercises.implement.flowr.go_to_question') %>'); body.find('.btn').on('click', self.createEventHandler('editor_flowr_click_question', questionUrl)); flowrHintBody.append(resultTile); diff --git a/config/locales/de.yml b/config/locales/de.yml index a8a1c53b..fdaa2a18 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -340,6 +340,7 @@ de: heading: "Hinweise" flowr: heading: "Weitere Hinweise | Unterstützt von StackOverflow" + go_to_question: "Lösung ansehen" index: clone: Duplizieren implement: Implementieren diff --git a/config/locales/en.yml b/config/locales/en.yml index 9ac9b0ce..5073d525 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -340,6 +340,7 @@ en: heading: "Hints" flowr: heading: "Gain more insights here | Powered by StackOverflow" + go_to_question: "Go to answer" index: clone: Duplicate implement: Implement From 8c201ee34cdf3ca02b24c3ecde7a41e942b18079 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 5 Oct 2018 15:11:35 +0200 Subject: [PATCH 09/16] Add margin between stacktrace and flowr output --- app/assets/stylesheets/flowrdata.css.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/flowrdata.css.scss b/app/assets/stylesheets/flowrdata.css.scss index c71da7ef..e4706b21 100644 --- a/app/assets/stylesheets/flowrdata.css.scss +++ b/app/assets/stylesheets/flowrdata.css.scss @@ -1,3 +1,4 @@ #flowrHint { display: none; + margin-top: 10px; } From 46fd76f1aa4b5109dfc2cc156c8d639808294de1 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 10 Oct 2018 10:58:34 +0200 Subject: [PATCH 10/16] Set default value for answers_per_query config --- app/assets/javascripts/editor/participantsupport.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index 040ee387..54965bfd 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -26,7 +26,7 @@ CodeOceanEditorFlowr = { var stackoverflowRequests = _.map(insights, function (insight) { var queryParams = { accepted: true, - pagesize: <%= CodeOcean::Config.new(:code_ocean).read[:flowr][:answers_per_query] %>, + pagesize: <%= CodeOcean::Config.new(:code_ocean).read[:flowr][:answers_per_query] or 3 %>, order: 'desc', sort: 'relevance', site: 'stackoverflow', From ebea8da3415e1dd3c636ec54f6a6bc05588694b8 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 10 Oct 2018 10:58:54 +0200 Subject: [PATCH 11/16] Open StackOverflow links in a new tab --- app/assets/javascripts/editor/participantsupport.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index 54965bfd..7f9d586d 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -99,7 +99,7 @@ CodeOceanEditorFlowr = { var body = resultTile.find('.panel-body'); body.html(result.body); - body.append('' + + body.append('' + '<%= I18n.t('exercises.implement.flowr.go_to_question') %>'); body.find('.btn').on('click', self.createEventHandler('editor_flowr_click_question', questionUrl)); From 212867f300ee5817ec9707a27f5ab80c477baa75 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Wed, 28 Nov 2018 15:22:21 +0100 Subject: [PATCH 12/16] Fix flowr output to work with Bootstrap 4 --- .../editor/participantsupport.js.erb | 23 +++++++++++-------- app/views/exercises/_editor_output.html.slim | 2 +- config/routes.rb | 2 -- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index d417ee9e..b220e2d0 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -1,15 +1,20 @@ CodeOceanEditorFlowr = { isFlowrEnabled: <%= CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] %>, flowrResultHtml: - '
' + - '