From 818012fa911a573177f3bbdce2901bde04d4edbb Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 21 Jun 2018 05:27:11 +0000 Subject: [PATCH 01/30] fix: Gemfile.lock & Gemfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-SPROCKETS-22032 --- Gemfile | 4 ++-- Gemfile.lock | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 08f5461e..b853aa15 100644 --- a/Gemfile +++ b/Gemfile @@ -25,10 +25,10 @@ gem 'rails', '4.2.10' gem 'rails-i18n' gem 'ransack' gem 'rubytree' -gem 'sass-rails' +gem 'sass-rails', '>= 5.0.7' gem 'sdoc', group: :doc gem 'slim-rails' -gem 'bootstrap_pagedown' +gem 'bootstrap_pagedown', '>= 1.1.0' gem 'pagedown-rails' gem 'sorcery' gem 'thread_safe' diff --git a/Gemfile.lock b/Gemfile.lock index d167b88c..1d6e7f06 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,7 +108,7 @@ GEM concurrent-ruby (1.0.5) concurrent-ruby-ext (1.0.5) concurrent-ruby (= 1.0.5) - crass (1.0.3) + crass (1.0.4) d3-rails (4.13.0) railties (>= 3.1) database_cleaner (1.6.2) @@ -135,7 +135,7 @@ GEM faye-websocket (0.10.7) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.23) + ffi (1.9.25) forgery (0.7.0) globalid (0.4.1) activesupport (>= 4.2.0) @@ -161,7 +161,7 @@ GEM json (2.1.0) jwt (1.5.6) kramdown (1.16.2) - loofah (2.2.0) + loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) @@ -181,7 +181,7 @@ GEM net-ssh (4.2.0) netrc (0.11.0) newrelic_rpm (4.8.0.341) - nokogiri (1.8.2) + nokogiri (1.8.3) mini_portile2 (~> 2.3.0) nyan-cat-formatter (0.12.0) rspec (>= 2.99, >= 2.14.2, < 4) @@ -211,7 +211,7 @@ GEM puma (3.11.3) pundit (1.1.0) activesupport (>= 3.0.0) - rack (1.6.9) + rack (1.6.10) rack-mini-profiler (0.10.7) rack (>= 1.2.0) rack-test (0.6.3) @@ -233,8 +233,8 @@ GEM activesupport (>= 4.2.0, < 5.0) nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails-i18n (4.0.9) i18n (~> 0.7) railties (~> 4.0) @@ -244,7 +244,7 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (3.0.0) - rake (12.3.0) + rake (12.3.1) ransack (1.8.7) actionpack (>= 3.0) activerecord (>= 3.0) @@ -296,7 +296,7 @@ GEM json (~> 2.1) structured_warnings (~> 0.3) rubyzip (1.2.1) - sass (3.5.5) + sass (3.5.6) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -330,7 +330,7 @@ GEM oauth2 (~> 1.0, >= 0.8.0) spring (2.0.2) activesupport (>= 4.2) - sprockets (3.7.1) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) @@ -383,7 +383,7 @@ DEPENDENCIES better_errors binding_of_caller bootstrap-will_paginate - bootstrap_pagedown + bootstrap_pagedown (>= 1.1.0) byebug capistrano capistrano-rails @@ -430,7 +430,7 @@ DEPENDENCIES rubocop-rspec rubytree rubyzip - sass-rails + sass-rails (>= 5.0.7) sdoc simplecov slim-rails From 6864cef4df7f16836bdcdc2a0164613d15e0ec0e Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 29 Jun 2018 05:23:20 +0000 Subject: [PATCH 02/30] fix: Gemfile.lock & Gemfile to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-FFI-22037 --- Gemfile | 4 ++-- Gemfile.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index 08f5461e..2edd4b99 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,7 @@ gem 'rails', '4.2.10' gem 'rails-i18n' gem 'ransack' gem 'rubytree' -gem 'sass-rails' +gem 'sass-rails', '>= 5.0.7' gem 'sdoc', group: :doc gem 'slim-rails' gem 'bootstrap_pagedown' @@ -66,7 +66,7 @@ end group :test do gem 'autotest-rails' gem 'capybara' - gem 'capybara-selenium' + gem 'capybara-selenium', '>= 0.0.6' gem 'headless' gem 'codeclimate-test-reporter', require: false gem 'database_cleaner' diff --git a/Gemfile.lock b/Gemfile.lock index d167b88c..d286d5eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,13 +78,13 @@ GEM capistrano (~> 3.7) capistrano-bundler puma (~> 3.4) - capybara (2.18.0) + capybara (3.3.1) addressable mini_mime (>= 0.1.3) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (>= 2.0, < 4.0) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + xpath (~> 3.1) capybara-selenium (0.0.6) capybara selenium-webdriver @@ -92,7 +92,7 @@ GEM activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) - childprocess (0.8.0) + childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) codeclimate-test-reporter (1.0.7) @@ -108,7 +108,7 @@ GEM concurrent-ruby (1.0.5) concurrent-ruby-ext (1.0.5) concurrent-ruby (= 1.0.5) - crass (1.0.3) + crass (1.0.4) d3-rails (4.13.0) railties (>= 3.1) database_cleaner (1.6.2) @@ -135,7 +135,7 @@ GEM faye-websocket (0.10.7) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.23) + ffi (1.9.25) forgery (0.7.0) globalid (0.4.1) activesupport (>= 4.2.0) @@ -161,7 +161,7 @@ GEM json (2.1.0) jwt (1.5.6) kramdown (1.16.2) - loofah (2.2.0) + loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) @@ -181,7 +181,7 @@ GEM net-ssh (4.2.0) netrc (0.11.0) newrelic_rpm (4.8.0.341) - nokogiri (1.8.2) + nokogiri (1.8.3) mini_portile2 (~> 2.3.0) nyan-cat-formatter (0.12.0) rspec (>= 2.99, >= 2.14.2, < 4) @@ -211,7 +211,7 @@ GEM puma (3.11.3) pundit (1.1.0) activesupport (>= 3.0.0) - rack (1.6.9) + rack (1.6.10) rack-mini-profiler (0.10.7) rack (>= 1.2.0) rack-test (0.6.3) @@ -233,8 +233,8 @@ GEM activesupport (>= 4.2.0, < 5.0) nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails-i18n (4.0.9) i18n (~> 0.7) railties (~> 4.0) @@ -244,7 +244,7 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (3.0.0) - rake (12.3.0) + rake (12.3.1) ransack (1.8.7) actionpack (>= 3.0) activerecord (>= 3.0) @@ -296,7 +296,7 @@ GEM json (~> 2.1) structured_warnings (~> 0.3) rubyzip (1.2.1) - sass (3.5.5) + sass (3.5.6) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -309,7 +309,7 @@ GEM tilt (>= 1.1, < 3) sdoc (1.0.0) rdoc (>= 5.0) - selenium-webdriver (3.10.0) + selenium-webdriver (3.13.0) childprocess (~> 0.5) rubyzip (~> 1.2) simplecov (0.15.1) @@ -330,7 +330,7 @@ GEM oauth2 (~> 1.0, >= 0.8.0) spring (2.0.2) activesupport (>= 4.2) - sprockets (3.7.1) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) @@ -369,7 +369,7 @@ GEM whenever (0.10.0) chronic (>= 0.6.3) will_paginate (3.1.6) - xpath (3.0.0) + xpath (3.1.0) nokogiri (~> 1.8) PLATFORMS @@ -391,7 +391,7 @@ DEPENDENCIES capistrano-upload-config capistrano3-puma capybara - capybara-selenium + capybara-selenium (>= 0.0.6) carrierwave codeclimate-test-reporter concurrent-ruby @@ -430,7 +430,7 @@ DEPENDENCIES rubocop-rspec rubytree rubyzip - sass-rails + sass-rails (>= 5.0.7) sdoc simplecov slim-rails From b68b3bc2b00ea30b961f72c374022eaea88e51b8 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 3 Jul 2018 15:23:00 +0200 Subject: [PATCH 03/30] Add position attribute to relation between exercise collection and exercises --- app/models/exercise.rb | 3 ++- app/models/exercise_collection.rb | 3 ++- app/models/exercise_collection_item.rb | 4 ++++ ...125302_create_exercise_collection_items.rb | 13 +++++++++++++ db/schema.rb | 19 ++++++++++--------- 5 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 app/models/exercise_collection_item.rb create mode 100644 db/migrate/20180703125302_create_exercise_collection_items.rb diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 3ff49ad8..b2b27af4 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -14,7 +14,8 @@ class Exercise < ActiveRecord::Base has_and_belongs_to_many :proxy_exercises has_many :user_proxy_exercise_exercises - has_and_belongs_to_many :exercise_collections + has_many :exercise_collection_items + has_many :exercise_collections, through: :exercise_collection_items has_many :user_exercise_interventions has_many :interventions, through: :user_exercise_interventions has_many :exercise_tags diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index 5c159dda..fa00e521 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -1,7 +1,8 @@ class ExerciseCollection < ActiveRecord::Base include TimeHelper - has_and_belongs_to_many :exercises + has_many :exercise_collection_items + has_many :exercises, through: :exercise_collection_items belongs_to :user, polymorphic: true def exercise_working_times diff --git a/app/models/exercise_collection_item.rb b/app/models/exercise_collection_item.rb new file mode 100644 index 00000000..c7b01f20 --- /dev/null +++ b/app/models/exercise_collection_item.rb @@ -0,0 +1,4 @@ +class ExerciseCollectionItem < ActiveRecord::Base + belongs_to :exercise_collection + belongs_to :exercise +end diff --git a/db/migrate/20180703125302_create_exercise_collection_items.rb b/db/migrate/20180703125302_create_exercise_collection_items.rb new file mode 100644 index 00000000..c881c8a5 --- /dev/null +++ b/db/migrate/20180703125302_create_exercise_collection_items.rb @@ -0,0 +1,13 @@ +class CreateExerciseCollectionItems < ActiveRecord::Migration + def up + rename_table :exercise_collections_exercises, :exercise_collection_items + add_column :exercise_collection_items, :position, :integer, default: 0, null: false + add_column :exercise_collection_items, :id, :primary_key + end + + def down + remove_column :exercise_collection_items, :position + remove_column :exercise_collection_items, :id + rename_table :exercise_collection_items, :exercise_collections_exercises + end +end diff --git a/db/schema.rb b/db/schema.rb index 73a2c228..ae1398a6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180515110030) do +ActiveRecord::Schema.define(version: 20180703125302) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -114,6 +114,15 @@ ActiveRecord::Schema.define(version: 20180515110030) do t.boolean "network_enabled" end + create_table "exercise_collection_items", force: :cascade do |t| + t.integer "exercise_collection_id" + t.integer "exercise_id" + t.integer "position", default: 0, null: false + end + + add_index "exercise_collection_items", ["exercise_collection_id"], name: "index_exercise_collection_items_on_exercise_collection_id", using: :btree + add_index "exercise_collection_items", ["exercise_id"], name: "index_exercise_collection_items_on_exercise_id", using: :btree + create_table "exercise_collections", force: :cascade do |t| t.string "name" t.datetime "created_at" @@ -125,14 +134,6 @@ ActiveRecord::Schema.define(version: 20180515110030) do add_index "exercise_collections", ["user_type", "user_id"], name: "index_exercise_collections_on_user_type_and_user_id", using: :btree - create_table "exercise_collections_exercises", id: false, force: :cascade do |t| - t.integer "exercise_collection_id" - t.integer "exercise_id" - end - - add_index "exercise_collections_exercises", ["exercise_collection_id"], name: "index_exercise_collections_exercises_on_exercise_collection_id", using: :btree - add_index "exercise_collections_exercises", ["exercise_id"], name: "index_exercise_collections_exercises_on_exercise_id", using: :btree - create_table "exercise_tags", force: :cascade do |t| t.integer "exercise_id" t.integer "tag_id" From 089bf578d3c4b3847f89478ec5ba25b96ae9c1e9 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 3 Jul 2018 15:32:37 +0200 Subject: [PATCH 04/30] Sort exercise collection items by position --- app/models/exercise_collection.rb | 1 + app/views/exercise_collections/show.html.slim | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index fa00e521..0836b404 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -2,6 +2,7 @@ class ExerciseCollection < ActiveRecord::Base include TimeHelper has_many :exercise_collection_items + alias_method :items, :exercise_collection_items has_many :exercises, through: :exercise_collection_items belongs_to :user, polymorphic: true diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index ab85a3ba..b2b04cb5 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -12,13 +12,16 @@ h4 = t('activerecord.attributes.exercise_collections.exercises') table.table thead tr + th = '#' th = t('activerecord.attributes.exercise.title') th = t('activerecord.attributes.exercise.execution_environment') th = t('activerecord.attributes.exercise.user') th = t('shared.actions') tbody - - @exercises.sort_by{|c| c.title}.each do |exercise| + - @exercise_collection.items.sort_by{|item| item.position}.each do |exercise_collection_item| + - exercise = exercise_collection_item.exercise tr + td = exercise_collection_item.position td = link_to(exercise.title, exercise) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = exercise.user.name From e7b38df0eb47281ebab3a454e090219cca1459c7 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 3 Jul 2018 16:14:19 +0200 Subject: [PATCH 05/30] Add sortable exercise list to exercise collection new/edit page --- .../javascripts/exercise_collections.js.erb | 199 ++++++++++-------- .../exercise_collections/_form.html.slim | 20 ++ config/locales/de.yml | 2 + config/locales/en.yml | 2 + 4 files changed, 140 insertions(+), 83 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index 3530b636..0784f9ac 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -1,102 +1,135 @@ $(function() { if ($.isController('exercise_collections')) { - var data = $('#data').data('working-times'); - var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time')); + var data = $('#data'); + var exerciseList = $('#exercise-list'); - var margin = { top: 30, right: 40, bottom: 30, left: 50 }, - width = 720 - margin.left - margin.right, - height = 500 - margin.top - margin.bottom; + if (data.isPresent()) { + data.data('working-times'); + var averageWorkingTimeValue = parseFloat(data.data('average-working-time')); - var x = d3.scaleBand().range([0, width]); - var y = d3.scaleLinear().range([height, 0]); + var margin = {top: 30, right: 40, bottom: 30, left: 50}, + width = 720 - margin.left - margin.right, + height = 500 - margin.top - margin.bottom; - var xAxis = d3.axisBottom(x); - var yAxisLeft = d3.axisLeft(y); + var x = d3.scaleBand().range([0, width]); + var y = d3.scaleLinear().range([height, 0]); - var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip"); + var xAxis = d3.axisBottom(x); + var yAxisLeft = d3.axisLeft(y); - var averageWorkingTime = d3.line() - .x(function (d) { return x(d.index) + x.bandwidth()/2; }) - .y(function () { return y(averageWorkingTimeValue); }); + var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip"); - var minWorkingTime = d3.line() - .x(function (d) { return x(d.index) + x.bandwidth()/2; }) - .y(function () { return y(0.1*averageWorkingTimeValue); }); + var averageWorkingTime = d3.line() + .x(function (d) { + return x(d.index) + x.bandwidth() / 2; + }) + .y(function () { + return y(averageWorkingTimeValue); + }); - var maxWorkingTime = d3.line() - .x(function (d) { return x(d.index) + x.bandwidth()/2; }) - .y(function () { return y(2*averageWorkingTimeValue); }); + var minWorkingTime = d3.line() + .x(function (d) { + return x(d.index) + x.bandwidth() / 2; + }) + .y(function () { + return y(0.1 * averageWorkingTimeValue); + }); - var svg = d3.select('#graph') - .append("svg") - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .append("g") - .attr("transform", - "translate(" + margin.left + "," + margin.top + ")"); + var maxWorkingTime = d3.line() + .x(function (d) { + return x(d.index) + x.bandwidth() / 2; + }) + .y(function () { + return y(2 * averageWorkingTimeValue); + }); - // Get the data - data = Object.keys(data).map(function (key, index) { - return { - index: index, - exercise_id: parseInt(key), - working_time: parseFloat(data[key]) - }; - }); + var svg = d3.select('#graph') + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", + "translate(" + margin.left + "," + margin.top + ")"); - // Scale the range of the data - x.domain(data.map(function (d) { return d.index; })); - y.domain([0, d3.max(data, function (d) { return d.working_time; })]); + // Get the data + data = Object.keys(data).map(function (key, index) { + return { + index: index, + exercise_id: parseInt(key), + working_time: parseFloat(data[key]) + }; + }); - // Add the X Axis - svg.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0," + height + ")") - .call(xAxis); + // Scale the range of the data + x.domain(data.map(function (d) { + return d.index; + })); + y.domain([0, d3.max(data, function (d) { + return d.working_time; + })]); - // Add the Y Axis - svg.append("g") - .attr("class", "y axis") - .style("fill", "steelblue") - .call(yAxisLeft); + // Add the X Axis + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); - // Draw the bars - svg.selectAll("bar") - .data(data) - .enter() - .append("rect") - .attr("class", "value-bar") - .on("mousemove", function (d){ - tooltip - .style("left", d3.event.pageX - 50 + "px") - .style("top", d3.event.pageY + 50 + "px") - .style("display", "inline-block") - .html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "
" + - "<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s"); - }) - .on("mouseout", function (){ tooltip.style("display", "none");}) - .on("click", function (d) { - window.location.href = "/exercises/" + d.exercise_id + "/statistics"; - }) - .attr("x", function (d) { return x(d.index); }) - .attr("width", x.bandwidth()) - .attr("y", function (d) { return y(d.working_time); }) - .attr("height", function (d) { return height - y(d.working_time); }); + // Add the Y Axis + svg.append("g") + .attr("class", "y axis") + .style("fill", "steelblue") + .call(yAxisLeft); - // Add the average working time path - svg.append("path") - .datum(data) - .attr("class", "line average-working-time") - .attr("d", averageWorkingTime); + // Draw the bars + svg.selectAll("bar") + .data(data) + .enter() + .append("rect") + .attr("class", "value-bar") + .on("mousemove", function (d) { + tooltip + .style("left", d3.event.pageX - 50 + "px") + .style("top", d3.event.pageY + 50 + "px") + .style("display", "inline-block") + .html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "
" + + "<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s"); + }) + .on("mouseout", function () { + tooltip.style("display", "none"); + }) + .on("click", function (d) { + window.location.href = "/exercises/" + d.exercise_id + "/statistics"; + }) + .attr("x", function (d) { + return x(d.index); + }) + .attr("width", x.bandwidth()) + .attr("y", function (d) { + return y(d.working_time); + }) + .attr("height", function (d) { + return height - y(d.working_time); + }); - // Add the anomaly paths (min/max average exercise working time) - svg.append("path") - .datum(data) - .attr("class", "line minimum-working-time") - .attr("d", minWorkingTime); - svg.append("path") - .datum(data) - .attr("class", "line maximum-working-time") - .attr("d", maxWorkingTime); + // Add the average working time path + svg.append("path") + .datum(data) + .attr("class", "line average-working-time") + .attr("d", averageWorkingTime); + + // Add the anomaly paths (min/max average exercise working time) + svg.append("path") + .datum(data) + .attr("class", "line minimum-working-time") + .attr("d", minWorkingTime); + svg.append("path") + .datum(data) + .attr("class", "line maximum-working-time") + .attr("d", maxWorkingTime); + } else if (exerciseList.isPresent()) { + var list = $("#sortable"); + list.sortable(); + list.disableSelection(); + } } }); diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index c204db47..b7249267 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -15,4 +15,24 @@ .form-group = f.label(t('activerecord.attributes.exercise_collections.exercises')) = f.collection_select(:exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true}) + + .table-responsive#exercise-list + table.table + thead + tr + th + th = t('activerecord.attributes.exercise_collection_item.exercise') + th = t('activerecord.attributes.exercise.user') + th colspan=2 = t('shared.actions') + tbody#sortable + - @exercise_collection.items.order(:position).each do |item| + tr + td + span.fa.fa-bars + td = item.exercise.title + td = item.exercise.author + td = link_to(t('shared.show'), item.exercise) + td + a.remove-exercise href="#" = t('shared.destroy') + .actions = render('shared/submit_button', f: f, object: @exercise_collection) diff --git a/config/locales/de.yml b/config/locales/de.yml index c3f7dc15..e62dbd47 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -131,6 +131,8 @@ de: user: "Autor" exercise: "Aufgabe" feedback_text: "Feedback Text" + exercise_collection_item: + exercise: "Aufgabe" models: code_harbor_link: one: CodeHarbor-Link diff --git a/config/locales/en.yml b/config/locales/en.yml index 494dd163..842dd346 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -131,6 +131,8 @@ en: user: "Author" exercise: "Exercise" feedback_text: "Feedback Text" + exercise_collection_item: + exercise: "Exercise" models: code_harbor_link: one: CodeHarbor Link From b4927cdecb9a872e607bf8cd5a463d84cbfc4065 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 10 Jul 2018 12:27:19 +0200 Subject: [PATCH 06/30] Update exercise anomaly detection to work on new schema --- lib/tasks/detect_exercise_anomalies.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 3436b87a..82f7211e 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -49,14 +49,14 @@ namespace :detect_exercise_anomalies do def get_collections(number_of_exercises, number_of_solutions) ExerciseCollection .where(:use_anomaly_detection => true) - .joins("join exercise_collections_exercises ece on exercise_collections.id = ece.exercise_collection_id + .joins("join exercise_collection_items eci on exercise_collections.id = eci.exercise_collection_id join (select e.id from exercises e join submissions s on s.exercise_id = e.id group by e.id having count(s.user_id) > #{ExerciseCollection.sanitize(number_of_solutions)} - ) as exercises_with_submissions on exercises_with_submissions.id = ece.exercise_id") + ) as exercises_with_submissions on exercises_with_submissions.id = eci.exercise_id") .group('exercise_collections.id') .having('count(exercises_with_submissions.id) > ?', number_of_exercises) end From 260ac9f8fb788137f9848a29e612c722c4876c4d Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 10 Jul 2018 13:00:15 +0200 Subject: [PATCH 07/30] Remove exercises and users from form data --- app/views/exercise_collections/_form.html.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index b7249267..617a056c 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -1,7 +1,7 @@ - exercises = Exercise.order(:title) - users = InternalUser.order(:name) -= form_for(@exercise_collection, data: {exercises: exercises, users: users}, multipart: true) do |f| += form_for(@exercise_collection, multipart: true) do |f| = render('shared/form_errors', object: @exercise_collection) .form-group = f.label(t('activerecord.attributes.exercise_collections.name')) From 927eaaeb85989437c919f94536291fbe1911b945 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 13 Jul 2018 11:01:53 +0200 Subject: [PATCH 08/30] fix raw output, prevent angle brackets < and > from beeing interpreted as html tags --- app/assets/javascripts/editor/evaluation.js.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 3aa4e25d..55e106d8 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -152,19 +152,19 @@ CodeOceanEditorEvaluation = { var element = this.findOrCreateOutputElement(index); if (!colorize) { if (output.stdout != undefined && output.stdout != '') { - element.append(output.stdout) + element.text(element.text() + output.stdout) } if (output.stderr != undefined && output.stderr != '') { - element.append('StdErr: ' + output.stderr); + element.text('StdErr: ' + element.text() + output.stderr); } } 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 { From 1093968782247aa7bdab01589d5b0e90c0c2bc3d Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 13 Jul 2018 11:12:07 +0200 Subject: [PATCH 09/30] fix error parsing for result boxes --- lib/junit_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/junit_adapter.rb b/lib/junit_adapter.rb index 6230a785..df0eff5d 100644 --- a/lib/junit_adapter.rb +++ b/lib/junit_adapter.rb @@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter COUNT_REGEXP = /Tests run: (\d+)/ FAILURES_REGEXP = /Failures: (\d+)/ SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/ - ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/ + ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:?\s(.*?)\tat org.junit|org\.junit\.ComparisonFailure:\s(.*?)\tat org.junit/m def self.framework_name 'JUnit' From 7da7bd5f3a5061f96cd76f294c0d9513b79ce9c9 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 13 Jul 2018 12:16:43 +0200 Subject: [PATCH 10/30] Fix data object in exercise collection statistics --- app/assets/javascripts/exercise_collections.js.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index 0784f9ac..b87eec53 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -1,11 +1,11 @@ $(function() { if ($.isController('exercise_collections')) { - var data = $('#data'); + var dataElement = $('#data'); var exerciseList = $('#exercise-list'); - if (data.isPresent()) { - data.data('working-times'); - var averageWorkingTimeValue = parseFloat(data.data('average-working-time')); + if (dataElement.isPresent()) { + var data = dataElement.data('working-times'); + var averageWorkingTimeValue = parseFloat(dataElement.data('average-working-time')); var margin = {top: 30, right: 40, bottom: 30, left: 50}, width = 720 - margin.left - margin.right, From 5ea30b56257583c53833913edef6709e08b22d04 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 13 Jul 2018 15:42:26 +0200 Subject: [PATCH 11/30] Add UI for adding exercises to collection --- .../javascripts/exercise_collections.js.erb | 25 +++++++++++++++++++ .../stylesheets/exercise_collections.scss | 4 +++ .../_add_exercise_modal.slim | 8 ++++++ .../exercise_collections/_form.html.slim | 13 ++++------ config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ 6 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 app/views/exercise_collections/_add_exercise_modal.slim diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index b87eec53..cf5b9369 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -130,6 +130,31 @@ $(function() { var list = $("#sortable"); list.sortable(); list.disableSelection(); + + var addExercisesForm = $('#exercise-selection'); + var addExercisesButton = $('#add-exercises'); + + var collectContainedExercises = function () { + return exerciseList.find('tbody > tr').toArray().map(function (item) {return $(item).data('id')}); + } + + addExercisesButton.on('click', function (e) { + e.preventDefault(); + var collectionExercises = collectContainedExercises(); + var selectedExercises = addExercisesForm.find('select')[0].selectedOptions; + for (var i = 0; i < selectedExercises.length; i++) { + var exercise = {id: selectedExercises[i].value, title: selectedExercises[i].label} + if (collectionExercises.indexOf(exercise.id) === -1) { + // only add exercises that are not already contained in the collection + var template = '' + + '' + + '' + exercise.title + '' + + '<%= I18n.t('shared.show') %>' + + '<%= I18n.t('shared.destroy') %>'; + exerciseList.find('tbody').append(template); + } + } + }); } } }); diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss index 11b6b3a1..98db1ab5 100644 --- a/app/assets/stylesheets/exercise_collections.scss +++ b/app/assets/stylesheets/exercise_collections.scss @@ -67,3 +67,7 @@ rect.value-bar { padding: 14px; text-align: center; } + +#exercise-list { + margin-bottom: 20px; +} diff --git a/app/views/exercise_collections/_add_exercise_modal.slim b/app/views/exercise_collections/_add_exercise_modal.slim new file mode 100644 index 00000000..0faec269 --- /dev/null +++ b/app/views/exercise_collections/_add_exercise_modal.slim @@ -0,0 +1,8 @@ +- exercises = Exercise.order(:title) + +form#exercise-selection + .form-group + span.label = t('activerecord.attributes.exercise_collections.exercises') + = collection_select({}, :exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true}) + +button.btn.btn-primary#add-exercises = t('exercise_collections.form.add_exercises') diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 617a056c..3824ff0e 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -1,4 +1,3 @@ -- exercises = Exercise.order(:title) - users = InternalUser.order(:name) = form_for(@exercise_collection, multipart: true) do |f| @@ -12,9 +11,6 @@ .form-group = f.label(t('activerecord.attributes.exercise_collections.user')) = f.collection_select(:user_id, users, :id, :name, {}, {class: 'form-control'}) - .form-group - = f.label(t('activerecord.attributes.exercise_collections.exercises')) - = f.collection_select(:exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true}) .table-responsive#exercise-list table.table @@ -22,17 +18,18 @@ tr th th = t('activerecord.attributes.exercise_collection_item.exercise') - th = t('activerecord.attributes.exercise.user') th colspan=2 = t('shared.actions') tbody#sortable - @exercise_collection.items.order(:position).each do |item| - tr + tr data-id=item.exercise.id td span.fa.fa-bars td = item.exercise.title - td = item.exercise.author td = link_to(t('shared.show'), item.exercise) td - a.remove-exercise href="#" = t('shared.destroy') + a.remove-exercise href='#' = t('shared.destroy') + button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') .actions = render('shared/submit_button', f: f, object: @exercise_collection) + += render('shared/modal', id: 'add-exercise-modal', title: t('.add_exercises'), template: 'exercise_collections/_add_exercise_modal') diff --git a/config/locales/de.yml b/config/locales/de.yml index 1dda5a12..d227ebb6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -784,3 +784,6 @@ de: files: "Dateien" users: "Benutzer" integrations: "Integrationen" + exercise_collections: + form: + add_exercises: "Add Exercises" diff --git a/config/locales/en.yml b/config/locales/en.yml index 344dc2d5..d7137c34 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -784,3 +784,6 @@ en: files: "Files" users: "Users" integrations: "Integrations" + exercise_collections: + form: + add_exercises: "Aufgaben hinzufügen" From 2d8f016b5e5ea65e75557e42891722f987069097 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 13 Jul 2018 16:41:13 +0200 Subject: [PATCH 12/30] Add hidden form element to save added exercises to collection --- app/assets/javascripts/exercise_collections.js.erb | 1 + app/views/exercise_collections/_form.html.slim | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index cf5b9369..97727a12 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -152,6 +152,7 @@ $(function() { '<%= I18n.t('shared.show') %>' + '<%= I18n.t('shared.destroy') %>'; exerciseList.find('tbody').append(template); + $('#exercise-list').find('option[value="' + exercise.id + '"]').prop('selected', true); } } }); diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 3824ff0e..dcdde074 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -29,6 +29,8 @@ td a.remove-exercise href='#' = t('shared.destroy') button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') + .hidden + = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {class: 'form-control', multiple: true}) .actions = render('shared/submit_button', f: f, object: @exercise_collection) From 620a0841e898da16c0ed6030415ec59efe1a81f2 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 13 Jul 2018 16:54:19 +0200 Subject: [PATCH 13/30] Cleanup --- app/views/exercise_collections/_form.html.slim | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index dcdde074..791edc3a 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -1,5 +1,3 @@ -- users = InternalUser.order(:name) - = form_for(@exercise_collection, multipart: true) do |f| = render('shared/form_errors', object: @exercise_collection) .form-group @@ -10,7 +8,7 @@ = f.check_box(:use_anomaly_detection, {class: 'form-control'}) .form-group = f.label(t('activerecord.attributes.exercise_collections.user')) - = f.collection_select(:user_id, users, :id, :name, {}, {class: 'form-control'}) + = f.collection_select(:user_id, InternalUser.order(:name), :id, :name, {}, {class: 'form-control'}) .table-responsive#exercise-list table.table From 24c5e0e88dd7f09bbb2d99c6353c66f4dbdb3c61 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 16 Jul 2018 10:38:34 +0200 Subject: [PATCH 14/30] Order exercises in request body according to manual sort order in UI --- .../javascripts/exercise_collections.js.erb | 16 +++++++++++++++- app/views/exercise_collections/_form.html.slim | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index 97727a12..c7de9db6 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -127,8 +127,22 @@ $(function() { .attr("class", "line maximum-working-time") .attr("d", maxWorkingTime); } else if (exerciseList.isPresent()) { + var exerciseSelect = $('#exercise-select'); var list = $("#sortable"); - list.sortable(); + + var updateExerciseList = function () { + // remove all options from the hidden select and add all selected exercises in the new order + exerciseSelect.find('option').remove(); + var exerciseIdsInSortedOrder = list.sortable('toArray', {attribute: 'data-id'}); + for (var i = 0; i < exerciseIdsInSortedOrder.length; i += 1) { + exerciseSelect.append('') + } + } + + list.sortable({ + items: 'tr', + update: updateExerciseList + }); list.disableSelection(); var addExercisesForm = $('#exercise-selection'); diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 791edc3a..2035fe85 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -28,7 +28,7 @@ a.remove-exercise href='#' = t('shared.destroy') button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') .hidden - = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {class: 'form-control', multiple: true}) + = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true}) .actions = render('shared/submit_button', f: f, object: @exercise_collection) From 574116cb1d84cdfc64fe85da993afc3a249d7ff9 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 16 Jul 2018 11:18:01 +0200 Subject: [PATCH 15/30] Save exercise collection items according to sort position --- app/controllers/exercise_collections_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index de425dcd..7c4a7d3f 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -50,6 +50,8 @@ class ExerciseCollectionsController < ApplicationController end def exercise_collection_params - params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name) + sanitized_params = params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name) + sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == ''} + sanitized_params.tap {|p| p[:exercise_collection_items] = p[:exercise_ids].map.with_index {|_id, index| ExerciseCollectionItem.find_or_create_by(exercise_id: _id, exercise_collection_id: @exercise_collection.id, position: index)}; p.delete(:exercise_ids)} end end From 1627d10600f6c0b45f90fc941ba45f30daec5286 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 18 Jul 2018 09:33:29 +0200 Subject: [PATCH 16/30] remove disabling of the RFC button after having posted an RFC --- app/assets/javascripts/editor/participantsupport.js.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb index 63787f4a..a826cbf0 100644 --- a/app/assets/javascripts/editor/participantsupport.js.erb +++ b/app/assets/javascripts/editor/participantsupport.js.erb @@ -89,7 +89,8 @@ CodeOceanEditorRequestForComments = { this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this)); $('#comment-modal').modal('hide'); - var button = $('#requestComments'); - button.prop('disabled', true); + // we disabled the button to prevent that the user spams RFCs, but decided against this now. + //var button = $('#requestComments'); + //button.prop('disabled', true); }, }; \ No newline at end of file From d971382b5bb3e6690fc609c47ff518301d73ab4f Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 18 Jul 2018 09:34:25 +0200 Subject: [PATCH 17/30] re-introduce exercise description exercise split --- app/models/proxy_exercise.rb | 32 ++++++++++++++++++++++++-------- lib/user_group_separator.rb | 14 ++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 3d9197fb..c6da3870 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -36,15 +36,31 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" ) assigned_user_proxy_exercise.exercise else - Rails.logger.debug("find new matching exercise for user #{user.id}" ) matching_exercise = - begin - find_matching_exercise(user) - rescue => e #fallback - Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" ) - @reason[:reason] = "fallback because of error" - @reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}" - exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen. + if (token.eql? "e85689d5") + Rails.logger.debug("Proxy exercise with token e85689d5, split user in groups..") + group = UserGroupSeparator.getGroupExerciseDescriptionTesting(user) + Rails.logger.debug("user assigned to group #{group}") + case group + when :group_a + exercises.where(id: 557).first + when :group_b + exercises.where(id: 558).first + when :group_c + exercises.where(id: 559).first + when :group_d + exercises.where(id: 560).first + end + else + Rails.logger.debug("find new matching exercise for user #{user.id}" ) + begin + find_matching_exercise(user) + rescue => e #fallback + Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" ) + @reason[:reason] = "fallback because of error" + @reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}" + exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen. + end end user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json) matching_exercise diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb index 4dcd00e1..2333b686 100644 --- a/lib/user_group_separator.rb +++ b/lib/user_group_separator.rb @@ -31,4 +31,18 @@ class UserGroupSeparator :recommended_assignment end end + + def self.getGroupExerciseDescriptionTesting(user) + groupById = user.id % 4 + if groupById == 0 + :group_a + elsif groupById == 1 + :group_b + elsif groupById == 2 + :group_c + else # 3 + :group_d + end + end + end \ No newline at end of file From 35243a544c6495c75c43a7c6b6f1185928be14b4 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 18 Jul 2018 14:51:35 +0200 Subject: [PATCH 18/30] dont call text for the moment to allow pictures being rendered. Quickfix for LED exercise. --- app/assets/javascripts/editor/evaluation.js.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 55e106d8..2267f460 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -152,7 +152,8 @@ CodeOceanEditorEvaluation = { var element = this.findOrCreateOutputElement(index); if (!colorize) { if (output.stdout != undefined && output.stdout != '') { - element.text(element.text() + output.stdout) + element.append(output.stdout) + //element.text(element.text() + output.stdout) } if (output.stderr != undefined && output.stderr != '') { From 39f1d3ec3a37525b303d92b633c49387ae47588c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 18 Jul 2018 15:10:34 +0200 Subject: [PATCH 19/30] fix LED exercise part 2 --- app/assets/javascripts/editor/evaluation.js.erb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb index 2267f460..19f57ce3 100644 --- a/app/assets/javascripts/editor/evaluation.js.erb +++ b/app/assets/javascripts/editor/evaluation.js.erb @@ -157,15 +157,18 @@ CodeOceanEditorEvaluation = { } if (output.stderr != undefined && output.stderr != '') { - element.text('StdErr: ' + element.text() + output.stderr); + element.append('StdErr: ' + output.stderr); + //element.text('StdErr: ' + element.text() + output.stderr); } } else if (output.stderr) { - element.addClass('text-warning').text(element.text() + 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').text(element.text() + 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 { From d80e3ecd3bfdfcacb96bb7c96f67deafd9330e15 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 10:36:10 +0200 Subject: [PATCH 20/30] Sort exercise collection items by position in statistics view --- app/assets/javascripts/exercise_collections.js.erb | 8 ++++---- app/models/exercise_collection.rb | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index c7de9db6..c1995c7e 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -52,11 +52,11 @@ $(function() { "translate(" + margin.left + "," + margin.top + ")"); // Get the data - data = Object.keys(data).map(function (key, index) { + data = Object.keys(data).map(function (key) { return { - index: index, - exercise_id: parseInt(key), - working_time: parseFloat(data[key]) + index: parseInt(key), + exercise_id: parseInt(data[key]['exercise_id']), + working_time: parseFloat(data[key]['working_time']) }; }); diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index 0836b404..df7082d5 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -8,8 +8,8 @@ class ExerciseCollection < ActiveRecord::Base def exercise_working_times working_times = {} - exercises.each do |exercise| - working_times[exercise.id] = time_to_f exercise.average_working_time + exercise_collection_items.each do |item| + working_times[item.position] = {exercise_id: item.exercise.id, working_time: time_to_f(item.exercise.average_working_time)} end working_times end @@ -18,8 +18,9 @@ class ExerciseCollection < ActiveRecord::Base if exercises.empty? 0 else - values = exercise_working_times.values.reject { |v| v.nil?} - values.reduce(:+) / exercises.size + values = exercise_working_times.values.reject { |o| o[:working_time].nil?} + sum = values.reduce(0) {|sum, item| sum + item[:working_time]} + sum / values.size end end From ed11004c2b7708294bc0944375e3f75719cbbaeb Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 10:58:35 +0200 Subject: [PATCH 21/30] Refactor exercise anomaly detection task --- lib/tasks/detect_exercise_anomalies.rake | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 55b7a716..4671cfde 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -21,29 +21,35 @@ namespace :detect_exercise_anomalies do WORKING_TIME_CACHE = {} AVERAGE_WORKING_TIME_CACHE = {} - task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args| + task :with_at_least, [:number_of_exercises, :number_of_users] => :environment do |task, args| include TimeHelper number_of_exercises = args[:number_of_exercises] - number_of_solutions = args[:number_of_solutions] + number_of_users = args[:number_of_users] - puts "Searching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_solutions} users." + log "Searching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_users} users." # Get all exercise collections that have at least the specified amount of exercises and at least the specified - # number of submissions AND are flagged for anomaly detection - collections = get_collections(number_of_exercises, number_of_solutions) - puts "Found #{collections.length}." + # number of users AND are flagged for anomaly detection + collections = get_collections(number_of_exercises, number_of_users) + log "Found #{collections.length}." collections.each do |collection| - puts "\t- #{collection}" + log(collection, 1, '- ') anomalies = find_anomalies(collection) - if anomalies.length > 0 and not collection.user.nil? - notify_collection_author(collection, anomalies) + if anomalies.length > 0 + unless collection.user.nil? + notify_collection_author(collection, anomalies) + end notify_users(collection, anomalies) reset_anomaly_detection_flag(collection) end end - puts 'Done.' + log 'Done.' + end + + def log(message='', indent_level=0, prefix='') + puts("\t" * indent_level + "#{prefix}#{message}") end def get_collections(number_of_exercises, number_of_solutions) @@ -64,7 +70,7 @@ namespace :detect_exercise_anomalies do def collect_working_times(collection) working_times = {} collection.exercises.each do |exercise| - puts "\t\t> #{exercise.title}" + log(exercise.title, 2, '> ') working_times[exercise.id] = get_average_working_time(exercise) end working_times @@ -99,16 +105,16 @@ namespace :detect_exercise_anomalies do end def notify_collection_author(collection, anomalies) - puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..." + log("Sending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)...", 2) UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now end def notify_users(collection, anomalies) by_id_and_type = proc { |u| {user_id: u[:user_id], user_type: u[:user_type]} } - puts "\t\tSending E-Mails to best and worst performing users of each anomaly..." + log("Sending E-Mails to best and worst performing users of each anomaly...", 2) anomalies.each do |exercise_id, average_working_time| - puts "\t\tAnomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):" + log("Anomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):", 2) exercise = Exercise.find(exercise_id) users_to_notify = [] @@ -136,7 +142,7 @@ namespace :detect_exercise_anomalies do feedback_link = url_for(action: :new, controller: :user_exercise_feedbacks, exercise_id: exercise.id, host: host) UserMailer.exercise_anomaly_needs_feedback(user, exercise, feedback_link).deliver end - puts "\t\tAsked #{users_to_notify.size} users for feedback." + log("Asked #{users_to_notify.size} users for feedback.", 2) end end @@ -160,7 +166,7 @@ namespace :detect_exercise_anomalies do end def reset_anomaly_detection_flag(collection) - puts "\t\tResetting flag..." + log("Resetting flag...", 2) collection.use_anomaly_detection = false collection.save! end From 97fe900f526814c6a41c24a02858d1626c52db96 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:02:41 +0200 Subject: [PATCH 22/30] Sort exercises in anomaly detection task according to position in exercise collection --- lib/tasks/detect_exercise_anomalies.rake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 4671cfde..4c630557 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -69,9 +69,9 @@ namespace :detect_exercise_anomalies do def collect_working_times(collection) working_times = {} - collection.exercises.each do |exercise| - log(exercise.title, 2, '> ') - working_times[exercise.id] = get_average_working_time(exercise) + collection.exercise_collection_items.order(:position).each do |eci| + log(eci.exercise.title, 2, '> ') + working_times[eci.exercise.id] = get_average_working_time(eci.exercise) end working_times end From 365a6e2c7487c73b873a41cc7bb497885165643d Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:07:48 +0200 Subject: [PATCH 23/30] Make exercise list larger --- app/assets/stylesheets/exercise_collections.scss | 4 ++++ app/views/exercise_collections/_add_exercise_modal.slim | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss index 98db1ab5..737dccbc 100644 --- a/app/assets/stylesheets/exercise_collections.scss +++ b/app/assets/stylesheets/exercise_collections.scss @@ -71,3 +71,7 @@ rect.value-bar { #exercise-list { margin-bottom: 20px; } + +#add-exercise-list { + min-height: 450px; +} diff --git a/app/views/exercise_collections/_add_exercise_modal.slim b/app/views/exercise_collections/_add_exercise_modal.slim index 0faec269..62080f4f 100644 --- a/app/views/exercise_collections/_add_exercise_modal.slim +++ b/app/views/exercise_collections/_add_exercise_modal.slim @@ -3,6 +3,6 @@ form#exercise-selection .form-group span.label = t('activerecord.attributes.exercise_collections.exercises') - = collection_select({}, :exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true}) + = collection_select({}, :exercise_ids, exercises, :id, :title, {}, {id: 'add-exercise-list', class: 'form-control', multiple: true}) button.btn.btn-primary#add-exercises = t('exercise_collections.form.add_exercises') From c09e12ca9a0999b87f7e18aa4dd450a5c6884b66 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:18:30 +0200 Subject: [PATCH 24/30] Allow removing exercises from collections --- app/assets/javascripts/exercise_collections.js.erb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index c1995c7e..d1c4aae8 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -170,6 +170,15 @@ $(function() { } } }); + + $('.remove-exercise').on('click', function (e) { + e.preventDefault(); + + var row = $(this).parent().parent(); + var exerciseId = row.data('id'); + $('#exercise-list').find('option[value="' + exerciseId + '"]').prop('selected', false); + row.remove() + }); } } }); From 43a220ddc857d1575fb444639ae5b3c5642afd2d Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:20:31 +0200 Subject: [PATCH 25/30] Fix translations --- config/locales/de.yml | 2 +- config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index d227ebb6..163afc40 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -786,4 +786,4 @@ de: integrations: "Integrationen" exercise_collections: form: - add_exercises: "Add Exercises" + add_exercises: "Aufgaben hinzufügen" diff --git a/config/locales/en.yml b/config/locales/en.yml index d7137c34..5a144428 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -786,4 +786,4 @@ en: integrations: "Integrations" exercise_collections: form: - add_exercises: "Aufgaben hinzufügen" + add_exercises: "Add exercises" From 7f18d844db646c9a6bbea13a64a7bf7fdc5ea1e2 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:39:00 +0200 Subject: [PATCH 26/30] Allow to order exercises in collection by title --- .../javascripts/exercise_collections.js.erb | 47 ++++++++++++++----- .../stylesheets/exercise_collections.scss | 4 ++ .../exercise_collections/_form.html.slim | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index d1c4aae8..03ee56d7 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -147,31 +147,47 @@ $(function() { var addExercisesForm = $('#exercise-selection'); var addExercisesButton = $('#add-exercises'); + var removeExerciseButtons = $('.remove-exercise'); + var sortButton = $('#sort-button'); var collectContainedExercises = function () { return exerciseList.find('tbody > tr').toArray().map(function (item) {return $(item).data('id')}); } + var sortExercises = function() { + var listitems = $('tr', list); + listitems.sort(function (a, b) { + return ($(a).find('td:nth-child(2)').text().toUpperCase() > $(b).find('td:nth-child(2)').text().toUpperCase()) ? 1 : -1; + }); + list.append(listitems); + list.sortable('refresh'); + updateExerciseList(); + } + + var addExercise = function (id, title) { + var exercise = {id: id, title: title} + var collectionExercises = collectContainedExercises(); + if (collectionExercises.indexOf(exercise.id) === -1) { + // only add exercises that are not already contained in the collection + var template = '' + + '' + + '' + exercise.title + '' + + '<%= I18n.t('shared.show') %>' + + '<%= I18n.t('shared.destroy') %>'; + exerciseList.find('tbody').append(template); + $('#exercise-list').find('option[value="' + exercise.id + '"]').prop('selected', true); + } + } + addExercisesButton.on('click', function (e) { e.preventDefault(); - var collectionExercises = collectContainedExercises(); var selectedExercises = addExercisesForm.find('select')[0].selectedOptions; for (var i = 0; i < selectedExercises.length; i++) { - var exercise = {id: selectedExercises[i].value, title: selectedExercises[i].label} - if (collectionExercises.indexOf(exercise.id) === -1) { - // only add exercises that are not already contained in the collection - var template = '' + - '' + - '' + exercise.title + '' + - '<%= I18n.t('shared.show') %>' + - '<%= I18n.t('shared.destroy') %>'; - exerciseList.find('tbody').append(template); - $('#exercise-list').find('option[value="' + exercise.id + '"]').prop('selected', true); - } + addExercise(selectedExercises[i].value, selectedExercises[i].label); } }); - $('.remove-exercise').on('click', function (e) { + removeExerciseButtons.on('click', function (e) { e.preventDefault(); var row = $(this).parent().parent(); @@ -179,6 +195,11 @@ $(function() { $('#exercise-list').find('option[value="' + exerciseId + '"]').prop('selected', false); row.remove() }); + + sortButton.on('click', function (e) { + e.preventDefault(); + sortExercises(); + }); } } }); diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss index 737dccbc..527ebd07 100644 --- a/app/assets/stylesheets/exercise_collections.scss +++ b/app/assets/stylesheets/exercise_collections.scss @@ -75,3 +75,7 @@ rect.value-bar { #add-exercise-list { min-height: 450px; } + +button { + margin-right: 10px; +} diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 2035fe85..05b07d93 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -27,6 +27,7 @@ td a.remove-exercise href='#' = t('shared.destroy') button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') + button.btn.btn-secondary#sort-button type='button' = t('exercise_collections.form.sort_by_title') .hidden = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true}) diff --git a/config/locales/de.yml b/config/locales/de.yml index 163afc40..be078620 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -787,3 +787,4 @@ de: exercise_collections: form: add_exercises: "Aufgaben hinzufügen" + sort_by_title: "Nach Titel sortieren" diff --git a/config/locales/en.yml b/config/locales/en.yml index 5a144428..863b0a64 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -787,3 +787,4 @@ en: exercise_collections: form: add_exercises: "Add exercises" + sort_by_title: "Sort by title" From e7f293ac144c33d1b6b89af987cbd60dd93796a7 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:54:00 +0200 Subject: [PATCH 27/30] Group exercise actions --- app/views/exercise_collections/_form.html.slim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 05b07d93..9c765071 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -26,10 +26,11 @@ td = link_to(t('shared.show'), item.exercise) td a.remove-exercise href='#' = t('shared.destroy') + .hidden + = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true}) + .exercise-actions button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') button.btn.btn-secondary#sort-button type='button' = t('exercise_collections.form.sort_by_title') - .hidden - = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true}) .actions = render('shared/submit_button', f: f, object: @exercise_collection) From 16cd93d5a4cb1d987f6e037329a7b91aee0a9864 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:54:27 +0200 Subject: [PATCH 28/30] Remove now unnecessary pagination --- app/controllers/exercise_collections_controller.rb | 1 - app/views/exercise_collections/show.html.slim | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 7c4a7d3f..1d7c1782 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -9,7 +9,6 @@ class ExerciseCollectionsController < ApplicationController end def show - @exercises = @exercise_collection.exercises.paginate(:page => params[:page]) end def new diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index b2b04cb5..c55d1d6b 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -8,7 +8,7 @@ h1 = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) h4 = t('activerecord.attributes.exercise_collections.exercises') -.table-responsive +.table-responsive#exercise-list table.table thead tr @@ -26,5 +26,3 @@ h4 = t('activerecord.attributes.exercise_collections.exercises') td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = exercise.user.name td = link_to(t('shared.statistics'), statistics_exercise_path(exercise)) - -= render('shared/pagination', collection: @exercises) From 7c292a0ce1e94eed4ad40eb2d6fa0607817041b0 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 11:54:52 +0200 Subject: [PATCH 29/30] Improve exercise collection UI --- app/assets/stylesheets/exercise_collections.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss index 527ebd07..79141988 100644 --- a/app/assets/stylesheets/exercise_collections.scss +++ b/app/assets/stylesheets/exercise_collections.scss @@ -57,6 +57,10 @@ rect.value-bar { } } +.table-responsive#exercise-list { + max-height: 512px; +} + .exercise-id-tooltip { position: absolute; display: none; @@ -76,6 +80,10 @@ rect.value-bar { min-height: 450px; } -button { - margin-right: 10px; +.exercise-actions { + margin-bottom: 20px; + + button { + margin-right: 10px; + } } From 6a1dbe4853193d8ebfe01269cfb9ff5265f9dca0 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 13:28:03 +0200 Subject: [PATCH 30/30] Add exercise title to statistics tooltip --- app/assets/javascripts/exercise_collections.js.erb | 2 ++ app/models/exercise_collection.rb | 10 +++++----- app/views/exercise_collections/statistics.html.slim | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index 03ee56d7..8ac76c1a 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -56,6 +56,7 @@ $(function() { return { index: parseInt(key), exercise_id: parseInt(data[key]['exercise_id']), + exercise_title: data[key]['exercise_title'], working_time: parseFloat(data[key]['working_time']) }; }); @@ -92,6 +93,7 @@ $(function() { .style("top", d3.event.pageY + 50 + "px") .style("display", "inline-block") .html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "
" + + "<%= I18n.t('activerecord.attributes.exercise.title') %>: " + d.exercise_title + "
" + "<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s"); }) .on("mouseout", function () { diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index df7082d5..a4a19f94 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -6,19 +6,19 @@ class ExerciseCollection < ActiveRecord::Base has_many :exercises, through: :exercise_collection_items belongs_to :user, polymorphic: true - def exercise_working_times - working_times = {} + def collection_statistics + statistics = {} exercise_collection_items.each do |item| - working_times[item.position] = {exercise_id: item.exercise.id, working_time: time_to_f(item.exercise.average_working_time)} + statistics[item.position] = {exercise_id: item.exercise.id, exercise_title: item.exercise.title, working_time: time_to_f(item.exercise.average_working_time)} end - working_times + statistics end def average_working_time if exercises.empty? 0 else - values = exercise_working_times.values.reject { |o| o[:working_time].nil?} + values = collection_statistics.values.reject { |o| o[:working_time].nil?} sum = values.reduce(0) {|sum, item| sum + item[:working_time]} sum / values.size end diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 486a0dbd..60c49a01 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -6,7 +6,7 @@ h1 = @exercise_collection = row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's') #graph - #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time) + #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time) #legend - {time: t('exercises.statistics.average_worktime'), min: 'min. anomaly threshold',