From 818012fa911a573177f3bbdce2901bde04d4edbb Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 21 Jun 2018 05:27:11 +0000 Subject: [PATCH 01/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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 7da7bd5f3a5061f96cd76f294c0d9513b79ce9c9 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 13 Jul 2018 12:16:43 +0200 Subject: [PATCH 08/31] 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 09/31] 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 10/31] 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 11/31] 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 12/31] 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 13/31] 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 d80e3ecd3bfdfcacb96bb7c96f67deafd9330e15 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Fri, 20 Jul 2018 10:36:10 +0200 Subject: [PATCH 14/31] 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 15/31] 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 16/31] 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 17/31] 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 18/31] 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 19/31] 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 20/31] 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 21/31] 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 22/31] 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 23/31] 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 24/31] 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', From c71afc55c9a346ca94d4b1386de918ac82b957a7 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 25 Jul 2018 13:41:00 +0200 Subject: [PATCH 25/31] Fix nil value error --- 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 be4ff161..fd8210da 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -72,8 +72,8 @@ namespace :detect_exercise_anomalies do def find_anomalies(collection) working_times = collect_working_times(collection).reject {|_, value| value.nil?} - if working_times.size > 0 - average = working_times.reduce(:+) / working_times.size + if working_times.values.size > 0 + average = working_times.values.reduce(:+) / working_times.values.size return working_times.select do |_, working_time| working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR end From 75bc0e1411ba24dbf98f67cd6363d4224377c5be Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 25 Jul 2018 14:14:37 +0200 Subject: [PATCH 26/31] Add min height to editor frames to avoid hiding content on small screens --- app/assets/stylesheets/editor.css.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss index f8e708eb..dad17193 100644 --- a/app/assets/stylesheets/editor.css.scss +++ b/app/assets/stylesheets/editor.css.scss @@ -19,6 +19,7 @@ button i.fa-spin { .frame { display: none; + min-height: 300px; audio, img, video { max-width: 100%; From 7e85fadfa1582d904fe656b7023c9a6a644e3357 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 25 Jul 2018 16:25:30 +0200 Subject: [PATCH 27/31] fix table name in query --- app/models/submission.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/submission.rb b/app/models/submission.rb index ad0767a4..3b2c7d3a 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -70,6 +70,6 @@ class Submission < ActiveRecord::Base # RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) } # experimental query: - RequestForComment.unsolved.joins('JOIN exercise_collections_exercises ece ON ece.exercise_id = request_for_comments.exercise_id').where('ece.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) } + RequestForComment.unsolved.joins('JOIN exercise_collection_items ece ON eci.exercise_id = request_for_comments.exercise_id').where('eci.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) } end end From 7478ef8eff67b5bd4b9a2cad205bc0e58732ae8c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 25 Jul 2018 16:29:57 +0200 Subject: [PATCH 28/31] and forgot to change it to i when doing a quickfix... --- app/models/submission.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/submission.rb b/app/models/submission.rb index 3b2c7d3a..763e1366 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -70,6 +70,6 @@ class Submission < ActiveRecord::Base # RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) } # experimental query: - RequestForComment.unsolved.joins('JOIN exercise_collection_items ece ON eci.exercise_id = request_for_comments.exercise_id').where('eci.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) } + RequestForComment.unsolved.joins('JOIN exercise_collection_items eci ON eci.exercise_id = request_for_comments.exercise_id').where('eci.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) } end end From 2c5308d4cdb198aa1edcaa2ce88e9f729900e7c1 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Mon, 30 Jul 2018 14:35:11 +0200 Subject: [PATCH 29/31] fix parsing problem of windows script --- app/assets/remote_scripts/windows.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/remote_scripts/windows.ps1 b/app/assets/remote_scripts/windows.ps1 index 9647d2e5..f548caea 100644 --- a/app/assets/remote_scripts/windows.ps1 +++ b/app/assets/remote_scripts/windows.ps1 @@ -62,6 +62,7 @@ function get_escaped_file_content ($file){ $content = $content.replace('\', '\\') $content = $content -replace "`r`n", '\n' $content = $content -replace "`n", '\n' + $content = $content -replace "`t", '\t' $content = $content.replace('"', '\"') return $content } From 331d12be3c3c6c7f9fede773362829a4ee5ef1cf Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 2 Aug 2018 08:08:52 +0000 Subject: [PATCH 30/31] Fix the spelling mistake "wether" with "whether" --- app/assets/javascripts/editor/websocket.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js.erb index 1d0e2b25..1bc26648 100644 --- a/app/assets/javascripts/editor/websocket.js.erb +++ b/app/assets/javascripts/editor/websocket.js.erb @@ -34,7 +34,7 @@ CommandSocket.prototype.onMessage = function(event) { }; /** - * Parses a message, checks wether it contains multiple commands (seperated by linebreaks) + * Parses a message, checks whether it contains multiple commands (seperated by linebreaks) * This needs to be done because of the behavior of the docker-socket connection. * Because of this, sometimes multiple commands might be executed in one message. * @param message From 3162764c81946bfbde6e8aa2902de86cb60b626d Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 2 Aug 2018 08:08:52 +0000 Subject: [PATCH 31/31] Fix the spelling mistake "occured" with "occurred" --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 863b0a64..26722361 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -358,7 +358,7 @@ en: external_users: External Users finishing_rate: Finishing Rate submit: - failure: An error occured while transmitting your score. Please try again later. + failure: An error occurred while transmitting your score. Please try again later. full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated! full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window! external_users: