From 80edffaa39fa3dcd9d836b55507df3e67360f6ce Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 15 Nov 2017 13:15:31 +0100 Subject: [PATCH 01/38] Add anomaly detection flag to exercise collections --- ...1125_add_anomaly_detection_flag_to_exercise_collection.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb diff --git a/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb b/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb new file mode 100644 index 00000000..2aad7e28 --- /dev/null +++ b/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb @@ -0,0 +1,5 @@ +class AddAnomalyDetectionFlagToExerciseCollection < ActiveRecord::Migration + def change + add_column :exercise_collections, :use_anomaly_detection, :boolean, :default => false + end +end diff --git a/db/schema.rb b/db/schema.rb index 465ca604..9acdffb4 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: 20170920145852) do +ActiveRecord::Schema.define(version: 20171115121125) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -104,6 +104,7 @@ ActiveRecord::Schema.define(version: 20170920145852) do t.string "name" t.datetime "created_at" t.datetime "updated_at" + t.boolean "use_anomaly_detection", default: false end create_table "exercise_collections_exercises", id: false, force: :cascade do |t| From 13b3b3edc798719e34b6efdc26e426f4216ea47e Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 22 Nov 2017 13:47:23 +0100 Subject: [PATCH 02/38] Add index to exercises --- db/migrate/20171122124222_add_index_to_exercises.rb | 5 +++++ db/schema.rb | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20171122124222_add_index_to_exercises.rb diff --git a/db/migrate/20171122124222_add_index_to_exercises.rb b/db/migrate/20171122124222_add_index_to_exercises.rb new file mode 100644 index 00000000..cf1f4674 --- /dev/null +++ b/db/migrate/20171122124222_add_index_to_exercises.rb @@ -0,0 +1,5 @@ +class AddIndexToExercises < ActiveRecord::Migration + def change + add_index :exercises, :id + end +end diff --git a/db/schema.rb b/db/schema.rb index 9acdffb4..e3f55f7c 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: 20171115121125) do +ActiveRecord::Schema.define(version: 20171122124222) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -139,6 +139,8 @@ ActiveRecord::Schema.define(version: 20171115121125) do t.integer "expected_difficulty", default: 1 end + add_index "exercises", ["id"], name: "index_exercises_on_id", using: :btree + create_table "exercises_proxy_exercises", id: false, force: :cascade do |t| t.integer "proxy_exercise_id" t.integer "exercise_id" From 8b5a05ba06a285a4ea0accc2cf64e2f87ca4e1eb Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 22 Nov 2017 17:40:14 +0100 Subject: [PATCH 03/38] Detect exercises with too high or too low working time average --- lib/tasks/detect_exercise_anomalies.rake | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 lib/tasks/detect_exercise_anomalies.rake diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake new file mode 100644 index 00000000..d0be6795 --- /dev/null +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -0,0 +1,44 @@ +namespace :detect_exercise_anomalies do + + task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args| + number_of_exercises = args.number_of_exercises + number_of_solutions = args.number_of_solutions + + # These factors determine if an exercise is an anomaly, given the average working time (avg): + # (avg * MIN_TIME_FACTOR) <= working_time <= (avg * MAX_TIME_FACTOR) + MIN_TIME_FACTOR = 0.1 + MAX_TIME_FACTOR = 2 + + # 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 = ExerciseCollection + .where(:use_anomaly_detection => true) + .joins("join exercise_collections_exercises ece on exercise_collections.id = ece.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.id) > #{ExerciseCollection.sanitize(number_of_solutions)} + ) as exercises_with_submissions on exercises_with_submissions.id = ece.exercise_id") + .group('exercise_collections.id') + .having('count(exercises_with_submissions.id) > ?', number_of_exercises) + + collections.each do |collection| + puts "\t- #{collection}" + working_times = {} + collection.exercises.each do |exercise| + puts "\t\t> #{exercise.title}" + avgwt = exercise.average_working_time.split(':') + seconds = avgwt[0].to_i * 60 * 60 + avgwt[1].to_i * 60 + avgwt[2].to_f + working_times[exercise.id] = seconds + end + average = working_times.values.reduce(:+) / working_times.size + anomalies = working_times.select do |exercise_id, working_time| + working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR + end + puts anomalies + end + end + +end From 00141830cc7da78a350de431d1abab66f0628c5e Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 6 Dec 2017 11:55:40 +0100 Subject: [PATCH 04/38] Count users instead of submissions --- lib/tasks/detect_exercise_anomalies.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index d0be6795..7cdf6480 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -19,7 +19,7 @@ namespace :detect_exercise_anomalies do from exercises e join submissions s on s.exercise_id = e.id group by e.id - having count(s.id) > #{ExerciseCollection.sanitize(number_of_solutions)} + having count(s.user_id) > #{ExerciseCollection.sanitize(number_of_solutions)} ) as exercises_with_submissions on exercises_with_submissions.id = ece.exercise_id") .group('exercise_collections.id') .having('count(exercises_with_submissions.id) > ?', number_of_exercises) From 339a89107fdec23741e90076321c35120e7600b3 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 10 Dec 2017 18:35:49 +0100 Subject: [PATCH 05/38] Add user to exercise_collection --- app/models/exercise_collection.rb | 1 + .../20171210172208_add_user_to_exercise_collection.rb | 5 +++++ db/schema.rb | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20171210172208_add_user_to_exercise_collection.rb diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index 10946962..f9f09269 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -1,6 +1,7 @@ class ExerciseCollection < ActiveRecord::Base has_and_belongs_to_many :exercises + belongs_to :user, polymorphic: true def to_s "#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})" diff --git a/db/migrate/20171210172208_add_user_to_exercise_collection.rb b/db/migrate/20171210172208_add_user_to_exercise_collection.rb new file mode 100644 index 00000000..6dee1adf --- /dev/null +++ b/db/migrate/20171210172208_add_user_to_exercise_collection.rb @@ -0,0 +1,5 @@ +class AddUserToExerciseCollection < ActiveRecord::Migration + def change + add_reference :exercise_collections, :user, polymorphic: true, index: true + end +end diff --git a/db/schema.rb b/db/schema.rb index e3f55f7c..474aa6f1 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: 20171122124222) do +ActiveRecord::Schema.define(version: 20171210172208) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -105,8 +105,12 @@ ActiveRecord::Schema.define(version: 20171122124222) do t.datetime "created_at" t.datetime "updated_at" t.boolean "use_anomaly_detection", default: false + t.integer "user_id" + t.string "user_type" end + 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" From 351f553c60fd9d5a48d1b80305c66156943f254b Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 10 Dec 2017 18:36:24 +0100 Subject: [PATCH 06/38] Send email to user associated with exercise collection when anomalies are detected --- app/mailers/user_mailer.rb | 7 +++++ .../exercise_anomaly_detected.html.slim | 13 +++++++++ config/locales/de.yml | 29 +++++++++++++++++++ config/locales/en.yml | 29 +++++++++++++++++++ lib/tasks/detect_exercise_anomalies.rake | 3 +- 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 app/views/user_mailer/exercise_anomaly_detected.html.slim diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 8022ee84..2df10be5 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -37,4 +37,11 @@ class UserMailer < ActionMailer::Base @rfc_link = request_for_comment_url(request_for_comments) mail(subject: t('mailers.user_mailer.send_thank_you_note.subject', author: @author), to: receiver.email) end + + def exercise_anomaly_detected(exercise_collection, anomalies) + @receiver_displayname = exercise_collection.user.displayname + @collection = exercise_collection + @anomalies = anomalies + mail(subject: t('mailers.user_mailer.exercise_anomaly_detected.subject', to: exercise_collection.user.email)) + end end diff --git a/app/views/user_mailer/exercise_anomaly_detected.html.slim b/app/views/user_mailer/exercise_anomaly_detected.html.slim new file mode 100644 index 00000000..8c008287 --- /dev/null +++ b/app/views/user_mailer/exercise_anomaly_detected.html.slim @@ -0,0 +1,13 @@ +== t('mailers.user_mailer.exercise_anomaly_detected.body1', + receiver_displayname: @receiver_displayname, + collection_name: @collection.name) + +- @anomalies.keys.each do | key | + =key + =@anomalies[key] + +== t('mailers.user_mailer.exercise_anomaly_detected.body2', + receiver_displayname: @receiver_displayname, + collection_name: @collection.name) + +== t('mailers.user_mailer.exercise_anomaly_detected.body3') diff --git a/config/locales/de.yml b/config/locales/de.yml index a7a2b436..73544027 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -515,6 +515,35 @@ de:
This mail was automatically sent by CodeOcean.
subject: "%{author_displayname} hat einen neuen Kommentar in einer Diskussion veröffentlicht, die Sie abonniert haben." + exercise_anomaly_detected: + subject: "Unregelmäßigkeiten in Aufgaben Ihrer Aufgabensammlung" + body1: | + English version below
+ _________________________
+
+ Hallo %{receiver_displayname},
+
+ eine oder mehrere Aufgaben Ihrer Aufgabensammlung "%{collection_name}" zeigen Unregelmäßigkeiten in der Bearbeitungszeit. Möglicherweise sind sie zu schwer oder zu leicht. +
+ Die Aufgaben sind: + body2: | +
+ Falls Sie beim Klick auf einen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.
+
+ Diese Mail wurde automatisch von CodeOcean verschickt.
+
+ _________________________
+
+ Dear %{receiver_displayname},
+
+ at least one exercise in your exercise collection "%{collection_name}" has a much longer or much shorter average working time than the average. Perhaps they are too difficult or too easy. +
+ The exercises are: + body3: | +
+ If you receive an error that you are not authorized to perform this action when clicking a link, please log-in through any course exercise beforehand and click the link again.
+
+ This mail was automatically sent by CodeOcean.
request_for_comments: click_here: Zum Kommentieren auf die Seitenleiste klicken! comments: Kommentare diff --git a/config/locales/en.yml b/config/locales/en.yml index 7eba1527..410e6759 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -515,6 +515,35 @@ en:
This mail was automatically sent by CodeOcean.
subject: "%{author_displayname} has posted a new comment to a discussion you subscribed to on CodeOcean." + exercise_anomaly_detected: + subject: "Anomalies in exercises of your exercise collection" + body1: | + English version below
+ _________________________
+
+ Hallo %{receiver_displayname},
+
+ eine oder mehrere Aufgaben Ihrer Aufgabensammlung "%{collection_name}" zeigen Unregelmäßigkeiten in der Bearbeitungszeit. Möglicherweise sind sie zu schwer oder zu leicht. +
+ Die Aufgaben sind: + body2: | +
+ Falls Sie beim Klick auf einen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.
+
+ Diese Mail wurde automatisch von CodeOcean verschickt.
+
+ _________________________
+
+ Dear %{receiver_displayname},
+
+ at least one exercise in your exercise collection "%{collection_name}" has a much longer or much shorter average working time than the average. Perhaps they are too difficult or too easy. +
+ The exercises are: + body3: | +
+ If you receive an error that you are not authorized to perform this action when clicking a link, please log-in through any course exercise beforehand and click the link again.
+
+ This mail was automatically sent by CodeOcean.
request_for_comments: click_here: Click on this sidebar to comment! comments: Comments diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 7cdf6480..e8b216cf 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -37,7 +37,8 @@ namespace :detect_exercise_anomalies do anomalies = working_times.select do |exercise_id, working_time| working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR end - puts anomalies + + UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now end end From 3e704c260c08d56724c2d492be2344557314d256 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 10 Dec 2017 18:45:48 +0100 Subject: [PATCH 07/38] Add user to exercise collection UI --- app/views/exercise_collections/show.html.slim | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index 65c28283..23a68cb5 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -3,6 +3,7 @@ h1 = render('shared/edit_button', object: @exercise_collection) = row(label: 'exercise_collections.name', value: @exercise_collection.name) += row(label: 'exercise_collections.user', value: link_to(@exercise_collection.user.name, @exercise_collection.user)) = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) h4 = t('activerecord.attributes.exercise_collections.exercises') diff --git a/config/locales/de.yml b/config/locales/de.yml index 73544027..320e3df4 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -124,6 +124,7 @@ de: exercise_collections: id: "ID" name: "Name" + user: "Verantwortlicher" updated_at: "Letzte Änderung" exercises: "Aufgaben" models: diff --git a/config/locales/en.yml b/config/locales/en.yml index 410e6759..0427403d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -124,6 +124,7 @@ en: exercise_collections: id: "ID" name: "Name" + user: "Associated User" updated_at: "Last Update" exercises: "Exercises" models: From fafa55f85ce0f194b5d0b6558657f2d187bfb0f5 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 10 Dec 2017 19:03:35 +0100 Subject: [PATCH 08/38] Add user to exercise collection form --- app/controllers/exercise_collections_controller.rb | 2 +- app/views/exercise_collections/_form.html.slim | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 4861a062..1f6da6c2 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -46,6 +46,6 @@ class ExerciseCollectionsController < ApplicationController end def exercise_collection_params - params[:exercise_collection].permit(:name, :exercise_ids => []) + params[:exercise_collection].permit(:name, :user_id, :user_type, :exercise_ids => []) end end diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 3c336a62..f13174a7 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -1,11 +1,15 @@ - exercises = Exercise.order(:title) +- users = InternalUser.order(:name) -= form_for(@exercise_collection, data: {exercises: exercises}, multipart: true) do |f| += form_for(@exercise_collection, data: {exercises: exercises, users: users}, multipart: true) do |f| = render('shared/form_errors', object: @exercise_collection) .form-group - = f.label(:name) + = f.label(t('activerecord.attributes.exercise_collections.name')) = f.text_field(:name, class: 'form-control', required: true) .form-group - = f.label(:exercises) + = 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}) .actions = render('shared/submit_button', f: f, object: @exercise_collection) From a6744c20e6213af882bb47459c5f615f96650b51 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 10 Dec 2017 19:17:02 +0100 Subject: [PATCH 09/38] Improve exercise list and link to statistics --- .../exercise_collections_controller.rb | 1 + app/views/exercise_collections/show.html.slim | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 1f6da6c2..415c77f8 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -9,6 +9,7 @@ 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 23a68cb5..fd643f3f 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -7,6 +7,20 @@ h1 = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) h4 = t('activerecord.attributes.exercise_collections.exercises') -ul.list-unstyled - - @exercise_collection.exercises.sort_by{|c| c.title}.each do |exercise| - li = link_to(exercise, exercise) +.table-responsive + table.table + thead + tr + 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| + tr + 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 + td = link_to(t('shared.statistics'), statistics_exercise_path(exercise)) + += render('shared/pagination', collection: @exercises) From 041f080191daa75eb092b0d6530061a62a6f9b6f Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 10 Dec 2017 19:25:05 +0100 Subject: [PATCH 10/38] Reset anomaly flag after sending emails --- lib/tasks/detect_exercise_anomalies.rake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index e8b216cf..63477fb5 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -38,7 +38,14 @@ namespace :detect_exercise_anomalies do working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR end + puts "\t\tSending E-Mail..." UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now + + puts "\t\tResetting flag..." + collection.use_anomaly_detection = false + collection.save! + + puts "\t\tDone." end end From 7ed78a2cfd909255d0b74987b90097c790fb2b34 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 13 Dec 2017 07:37:49 +0100 Subject: [PATCH 11/38] Add UI for anomaly detection flag --- app/controllers/exercise_collections_controller.rb | 2 +- app/views/exercise_collections/_form.html.slim | 3 +++ app/views/exercise_collections/show.html.slim | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 415c77f8..6747480d 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -47,6 +47,6 @@ class ExerciseCollectionsController < ApplicationController end def exercise_collection_params - params[:exercise_collection].permit(:name, :user_id, :user_type, :exercise_ids => []) + params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []) end end diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index f13174a7..c204db47 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -6,6 +6,9 @@ .form-group = f.label(t('activerecord.attributes.exercise_collections.name')) = f.text_field(:name, class: 'form-control', required: true) + .form-group + = f.label(t('activerecord.attributes.exercise_collections.use_anomaly_detection')) + = 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'}) diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index fd643f3f..6de1695f 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -4,6 +4,7 @@ h1 = row(label: 'exercise_collections.name', value: @exercise_collection.name) = row(label: 'exercise_collections.user', value: link_to(@exercise_collection.user.name, @exercise_collection.user)) += row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection) = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) h4 = t('activerecord.attributes.exercise_collections.exercises') diff --git a/config/locales/de.yml b/config/locales/de.yml index 320e3df4..e372481b 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -125,6 +125,7 @@ de: id: "ID" name: "Name" user: "Verantwortlicher" + use_anomaly_detection: "Abweichungen in der Arbeitszeit erkennen" updated_at: "Letzte Änderung" exercises: "Aufgaben" models: diff --git a/config/locales/en.yml b/config/locales/en.yml index 0427403d..647eed3f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -125,6 +125,7 @@ en: id: "ID" name: "Name" user: "Associated User" + use_anomaly_detection: "Enable Worktime Anomaly Detection" updated_at: "Last Update" exercises: "Exercises" models: From 9d3e232b4dc23ba5d317eb3d154ef5a603cbdcaa Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 13 Dec 2017 08:02:46 +0100 Subject: [PATCH 12/38] Only send mail if there are anomalies detected --- lib/tasks/detect_exercise_anomalies.rake | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 63477fb5..63567dae 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -4,6 +4,8 @@ namespace :detect_exercise_anomalies do number_of_exercises = args.number_of_exercises number_of_solutions = args.number_of_solutions + puts "\tSearching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_solutions} users." + # These factors determine if an exercise is an anomaly, given the average working time (avg): # (avg * MIN_TIME_FACTOR) <= working_time <= (avg * MAX_TIME_FACTOR) MIN_TIME_FACTOR = 0.1 @@ -24,6 +26,8 @@ namespace :detect_exercise_anomalies do .group('exercise_collections.id') .having('count(exercises_with_submissions.id) > ?', number_of_exercises) + puts "\tFound #{collections.length}." + collections.each do |collection| puts "\t- #{collection}" working_times = {} @@ -38,15 +42,16 @@ namespace :detect_exercise_anomalies do working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR end - puts "\t\tSending E-Mail..." - UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now + if anomalies.length > 0 + puts "\t\tSending E-Mail..." + UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now - puts "\t\tResetting flag..." - collection.use_anomaly_detection = false - collection.save! - - puts "\t\tDone." + puts "\t\tResetting flag..." + collection.use_anomaly_detection = false + collection.save! + end end + puts "\tDone." end end From 98d52170445b06f5e39a8a5753ff2eb9bedec275 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 13 Dec 2017 12:56:49 +0100 Subject: [PATCH 13/38] Fix parenthesis --- app/mailers/user_mailer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 2df10be5..73367d10 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -42,6 +42,6 @@ class UserMailer < ActionMailer::Base @receiver_displayname = exercise_collection.user.displayname @collection = exercise_collection @anomalies = anomalies - mail(subject: t('mailers.user_mailer.exercise_anomaly_detected.subject', to: exercise_collection.user.email)) + mail(subject: t('mailers.user_mailer.exercise_anomaly_detected.subject'), to: exercise_collection.user.email) end end From 5f5c266ffc3431361071ab303e36078ba6e7d610 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 13 Dec 2017 12:57:45 +0100 Subject: [PATCH 14/38] Format anomaly data in mails --- .../exercise_anomaly_detected.html.slim | 31 +++++++++++++++++-- config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ test/mailers/previews/user_mailer_preview.rb | 7 +++++ 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 test/mailers/previews/user_mailer_preview.rb diff --git a/app/views/user_mailer/exercise_anomaly_detected.html.slim b/app/views/user_mailer/exercise_anomaly_detected.html.slim index 8c008287..56233ce0 100644 --- a/app/views/user_mailer/exercise_anomaly_detected.html.slim +++ b/app/views/user_mailer/exercise_anomaly_detected.html.slim @@ -2,12 +2,37 @@ receiver_displayname: @receiver_displayname, collection_name: @collection.name) -- @anomalies.keys.each do | key | - =key - =@anomalies[key] +table(border=1) + thead + tr + td = t('activerecord.attributes.exercise.title', locale: :de) + td = t('exercises.statistics.average_worktime', locale: :de) + td = t('shared.actions', locale: :de) + tbody + - @anomalies.keys.each do | id | + - exercise = Exercise.find(id) + tr + td = link_to(exercise.title, exercise_path(exercise)) + td = @anomalies[id] + td = link_to(t('shared.statistics', locale: :de), statistics_exercise_path(exercise)) + == t('mailers.user_mailer.exercise_anomaly_detected.body2', receiver_displayname: @receiver_displayname, collection_name: @collection.name) +table(border=1) + thead + tr + td = t('activerecord.attributes.exercise.title', locale: :en) + td = t('exercises.statistics.average_worktime', locale: :en) + td = t('shared.actions', locale: :en) + tbody + - @anomalies.keys.each do | id | + - exercise = Exercise.find(id) + tr + td = link_to(exercise.title, exercise_path(exercise)) + td = @anomalies[id] + td = link_to(t('shared.statistics', locale: :en), statistics_exercise_path(exercise)) + == t('mailers.user_mailer.exercise_anomaly_detected.body3') diff --git a/config/locales/de.yml b/config/locales/de.yml index e372481b..9754caac 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -528,6 +528,7 @@ de: eine oder mehrere Aufgaben Ihrer Aufgabensammlung "%{collection_name}" zeigen Unregelmäßigkeiten in der Bearbeitungszeit. Möglicherweise sind sie zu schwer oder zu leicht.
Die Aufgaben sind: +
body2: |
Falls Sie beim Klick auf einen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.
@@ -541,6 +542,7 @@ de: at least one exercise in your exercise collection "%{collection_name}" has a much longer or much shorter average working time than the average. Perhaps they are too difficult or too easy.
The exercises are: +
body3: |
If you receive an error that you are not authorized to perform this action when clicking a link, please log-in through any course exercise beforehand and click the link again.
diff --git a/config/locales/en.yml b/config/locales/en.yml index 647eed3f..777ee9b7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -528,6 +528,7 @@ en: eine oder mehrere Aufgaben Ihrer Aufgabensammlung "%{collection_name}" zeigen Unregelmäßigkeiten in der Bearbeitungszeit. Möglicherweise sind sie zu schwer oder zu leicht.
Die Aufgaben sind: +
body2: |
Falls Sie beim Klick auf einen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.
@@ -541,6 +542,7 @@ en: at least one exercise in your exercise collection "%{collection_name}" has a much longer or much shorter average working time than the average. Perhaps they are too difficult or too easy.
The exercises are: +
body3: |
If you receive an error that you are not authorized to perform this action when clicking a link, please log-in through any course exercise beforehand and click the link again.
diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb new file mode 100644 index 00000000..d8deead9 --- /dev/null +++ b/test/mailers/previews/user_mailer_preview.rb @@ -0,0 +1,7 @@ +class UserMailerPreview < ActionMailer::Preview + def exercise_anomaly_detected() + collection = ExerciseCollection.new(name: 'Hello World', user: FactoryGirl.create(:admin)) + anomalies = {49 => 879.325828, 51 => 924.870057, 31 => 1031.21233, 69 => 2159.182116} + UserMailer.exercise_anomaly_detected(collection, anomalies) + end +end From 21c1089be7af37707ffdec6491c0ade3c0e47acc Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 20 Dec 2017 10:27:42 +0100 Subject: [PATCH 15/38] Check if user exists --- lib/tasks/detect_exercise_anomalies.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 63567dae..755d2c14 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -42,7 +42,7 @@ namespace :detect_exercise_anomalies do working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR end - if anomalies.length > 0 + if anomalies.length > 0 and not collection.user.nil? puts "\t\tSending E-Mail..." UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now From 9bb85e2968fb42bfe499abc368ebc12bf3c0d341 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 20 Dec 2017 10:27:52 +0100 Subject: [PATCH 16/38] Rename FactoryGirl --- test/mailers/previews/user_mailer_preview.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb index d8deead9..a3770fda 100644 --- a/test/mailers/previews/user_mailer_preview.rb +++ b/test/mailers/previews/user_mailer_preview.rb @@ -1,6 +1,6 @@ class UserMailerPreview < ActionMailer::Preview def exercise_anomaly_detected() - collection = ExerciseCollection.new(name: 'Hello World', user: FactoryGirl.create(:admin)) + collection = ExerciseCollection.new(name: 'Hello World', user: FactoryBot.create(:admin)) anomalies = {49 => 879.325828, 51 => 924.870057, 31 => 1031.21233, 69 => 2159.182116} UserMailer.exercise_anomaly_detected(collection, anomalies) end From 0c5f88d7486c1b522c6b2194bd5237a7f787b961 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 30 Jan 2018 16:57:03 +0100 Subject: [PATCH 17/38] Configure whenever schedule and logging for rake task --- Capfile | 1 + Gemfile | 1 + Gemfile.lock | 3 +++ config/deploy.rb | 2 ++ config/schedule.rb | 26 ++++++++++++++++++++++++++ 5 files changed, 33 insertions(+) create mode 100644 config/schedule.rb diff --git a/Capfile b/Capfile index 86e5243c..f29a788c 100644 --- a/Capfile +++ b/Capfile @@ -5,3 +5,4 @@ require 'capistrano/puma/nginx' require 'capistrano/rails' require 'capistrano/rvm' require 'capistrano/upload-config' +require "whenever/capistrano" diff --git a/Gemfile b/Gemfile index 8595fdf9..c5b3f2ac 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,7 @@ end group :development, :test, :staging do gem 'byebug', platform: :ruby gem 'spring' + gem 'whenever', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 901aa706..073a2d11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -370,6 +370,8 @@ GEM websocket-driver (0.6.3-java) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) + whenever (0.10.0) + chronic (>= 0.6.3) will_paginate (3.1.0) xpath (2.0.0) nokogiri (~> 1.3) @@ -442,4 +444,5 @@ DEPENDENCIES turbolinks uglifier (>= 1.3.0) web-console (~> 2.0) + whenever will_paginate (~> 3.0) diff --git a/config/deploy.rb b/config/deploy.rb index f4b10182..f7ba38f7 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -9,6 +9,8 @@ set :log_level, :info set :puma_threads, [0, 16] set :repo_url, 'git@github.com:openHPI/codeocean.git' +set :whenever_identifier, ->{ "#{fetch(:application)}_#{fetch(:stage)}" } + namespace :deploy do before 'check:linked_files', 'config:push' diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 00000000..b8f8b30e --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,26 @@ +# Use this file to easily define all of your cron jobs. +# +# It's helpful, but not entirely necessary to understand cron before proceeding. +# http://en.wikipedia.org/wiki/Cron + +# Example: +# +# set :output, "/path/to/my/cron_log.log" +# +# every 2.hours do +# command "/usr/bin/some_great_command" +# runner "MyModel.some_method" +# rake "some:great:rake:task" +# end +# +# every 4.days do +# runner "AnotherModel.prune_old_records" +# end + +# Learn more: http://github.com/javan/whenever + +set :output, Whenever.path + '/log/whenever.log' + +every 1.day, at: '3:00 am' do + rake 'detect_exercise_anomalies:with_at_least[50,50]' +end From 842b5c5f23d9a992951faf31e43482612151fa5a Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 31 Jan 2018 11:06:00 +0100 Subject: [PATCH 18/38] Add timestamp to log files --- config/schedule.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schedule.rb b/config/schedule.rb index b8f8b30e..82b1b31d 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -19,7 +19,7 @@ # Learn more: http://github.com/javan/whenever -set :output, Whenever.path + '/log/whenever.log' +set :output, Whenever.path + '/log/whenever_$(date +%Y%m%d%H%M%S).log' every 1.day, at: '3:00 am' do rake 'detect_exercise_anomalies:with_at_least[50,50]' From b5f282b20634ecebacdfbbc5768d784142d1da4b Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 31 Jan 2018 11:16:41 +0100 Subject: [PATCH 19/38] Correctly set environment --- config/schedule.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/schedule.rb b/config/schedule.rb index 82b1b31d..4132ba28 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -20,6 +20,7 @@ # Learn more: http://github.com/javan/whenever set :output, Whenever.path + '/log/whenever_$(date +%Y%m%d%H%M%S).log' +set :environment, ENV['RAILS_ENV'] if ENV['RAILS_ENV'] every 1.day, at: '3:00 am' do rake 'detect_exercise_anomalies:with_at_least[50,50]' From d5f123ad7d09edf9d7cfe86f65bb0c5a39a572b1 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 31 Jan 2018 14:33:42 +0100 Subject: [PATCH 20/38] Improve task output --- lib/tasks/detect_exercise_anomalies.rake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 755d2c14..9299a248 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -43,7 +43,9 @@ namespace :detect_exercise_anomalies do end if anomalies.length > 0 and not collection.user.nil? - puts "\t\tSending E-Mail..." + puts "\t\tAnomalies: #{anomalies}\n" + + puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..." UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now puts "\t\tResetting flag..." From ae7a065bd9aaeac61fb1047a517696a856a66382 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 31 Jan 2018 15:23:29 +0100 Subject: [PATCH 21/38] Lookup best and worst performers --- lib/tasks/detect_exercise_anomalies.rake | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 9299a248..e82c88b2 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -48,6 +48,32 @@ namespace :detect_exercise_anomalies do puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..." UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now + puts "\t\tSending E-Mails to best and worst performing users of each anomaly..." + anomalies.each do |exercise_id, average_working_time| + submissions = Submission.find_by_sql([' + select distinct s.* + from + ( + select + user_id, + first_value(id) over (partition by user_id order by created_at desc) as fv + from submissions + where exercise_id = ? + ) as t + join submissions s on s.id = t.fv + where score is not null + order by score', exercise_id]) + best_performers = submissions.first(10).to_a.map do |item| + item.user_id + end + worst_performers = submissions.last(10).to_a.map do |item| + item.user_id + end + puts "\t\tAnomaly in exercise #{exercise_id}:" + puts "\t\t\tbest performers: #{best_performers}" + puts "\t\t\tworst performers: #{worst_performers}" + end + puts "\t\tResetting flag..." collection.use_anomaly_detection = false collection.save! From 16337ba9affb442f6c86d3797e461ba656fccdbf Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 31 Jan 2018 15:55:07 +0100 Subject: [PATCH 22/38] Fix gemfile for production --- Gemfile | 2 +- Gemfile.lock | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index c5b3f2ac..c3a21ae4 100644 --- a/Gemfile +++ b/Gemfile @@ -41,6 +41,7 @@ gem 'nokogiri' gem 'd3-rails' gem 'rest-client' gem 'rubyzip' +gem 'whenever', require: false group :development, :staging do gem 'better_errors', platform: :ruby @@ -59,7 +60,6 @@ end group :development, :test, :staging do gem 'byebug', platform: :ruby gem 'spring' - gem 'whenever', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 073a2d11..df99106f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,6 +95,7 @@ GEM mime-types (>= 1.16) childprocess (0.5.9) ffi (~> 1.0, >= 1.0.11) + chronic (0.10.2) codeclimate-test-reporter (0.4.8) simplecov (>= 0.7.1, < 1.0.0) coderay (1.1.0) @@ -202,11 +203,6 @@ GEM coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry (0.10.3-java) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - spoon (~> 0.0) pry-byebug (3.3.0) byebug (~> 8.0) pry (~> 0.10) @@ -323,8 +319,6 @@ GEM bcrypt (~> 3.1) oauth (~> 0.4, >= 0.4.4) oauth2 (>= 0.8.0) - spoon (0.0.4) - ffi spring (1.6.3) sprockets (2.12.4) hike (~> 1.2) From 6fd00a66507b506d93d72a7e39edfc41ff5ccb96 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 4 Feb 2018 14:02:49 +0100 Subject: [PATCH 23/38] Update crontab after deploy --- config/deploy.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/deploy.rb b/config/deploy.rb index f7ba38f7..75d61a05 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -23,3 +23,11 @@ namespace :deploy do end end end + +namespace :whenever do + task :update_crontab do + run 'bundle exec whenever --update-crontab' + end +end + +after 'deploy', 'whenever:update_crontab' From 3ee993e9658b4ee6db70da8df0338cf90c25ae06 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 4 Feb 2018 14:37:48 +0100 Subject: [PATCH 24/38] Fix saving and showing exercise collections --- app/controllers/exercise_collections_controller.rb | 2 +- app/views/exercise_collections/show.html.slim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 6747480d..7980d11a 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -47,6 +47,6 @@ class ExerciseCollectionsController < ApplicationController end def exercise_collection_params - params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []) + params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name) end end diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index 6de1695f..ab85a3ba 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -3,7 +3,7 @@ h1 = render('shared/edit_button', object: @exercise_collection) = row(label: 'exercise_collections.name', value: @exercise_collection.name) -= row(label: 'exercise_collections.user', value: link_to(@exercise_collection.user.name, @exercise_collection.user)) += row(label: 'exercise_collections.user', value: link_to(@exercise_collection.user.name, @exercise_collection.user)) unless @exercise_collection.user.nil? = row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection) = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) From 509335a1af96554d9a82b3ef02247a9ca874b404 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 4 Feb 2018 15:14:07 +0100 Subject: [PATCH 25/38] Refactor anomaly detection task --- lib/tasks/detect_exercise_anomalies.rake | 123 +++++++++++++---------- 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index e82c88b2..f2ca9a8f 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -1,56 +1,72 @@ namespace :detect_exercise_anomalies do + # These factors determine if an exercise is an anomaly, given the average working time (avg): + # (avg * MIN_TIME_FACTOR) <= working_time <= (avg * MAX_TIME_FACTOR) + MIN_TIME_FACTOR = 0.1 + MAX_TIME_FACTOR = 2 + task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args| - number_of_exercises = args.number_of_exercises - number_of_solutions = args.number_of_solutions - - puts "\tSearching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_solutions} users." - - # These factors determine if an exercise is an anomaly, given the average working time (avg): - # (avg * MIN_TIME_FACTOR) <= working_time <= (avg * MAX_TIME_FACTOR) - MIN_TIME_FACTOR = 0.1 - MAX_TIME_FACTOR = 2 + number_of_exercises = args[:number_of_exercises] + number_of_solutions = args[:number_of_solutions] + puts "Searching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_solutions} 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 = ExerciseCollection - .where(:use_anomaly_detection => true) - .joins("join exercise_collections_exercises ece on exercise_collections.id = ece.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") - .group('exercise_collections.id') - .having('count(exercises_with_submissions.id) > ?', number_of_exercises) - - puts "\tFound #{collections.length}." + collections = get_collections(number_of_exercises, number_of_solutions) + puts "Found #{collections.length}." collections.each do |collection| puts "\t- #{collection}" - working_times = {} - collection.exercises.each do |exercise| - puts "\t\t> #{exercise.title}" - avgwt = exercise.average_working_time.split(':') - seconds = avgwt[0].to_i * 60 * 60 + avgwt[1].to_i * 60 + avgwt[2].to_f - working_times[exercise.id] = seconds - end - average = working_times.values.reduce(:+) / working_times.size - anomalies = working_times.select do |exercise_id, working_time| - working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR - end + anomalies = find_anomalies(collection) if anomalies.length > 0 and not collection.user.nil? puts "\t\tAnomalies: #{anomalies}\n" + notify_collection_author(collection, anomalies) + notify_users(collection, anomalies) + reset_anomaly_detection_flag(collection) + end + end + puts 'Done.' + end - puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..." - UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now + 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 + 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") + .group('exercise_collections.id') + .having('count(exercises_with_submissions.id) > ?', number_of_exercises) + end - puts "\t\tSending E-Mails to best and worst performing users of each anomaly..." - anomalies.each do |exercise_id, average_working_time| - submissions = Submission.find_by_sql([' + def find_anomalies(collection) + working_times = {} + collection.exercises.each do |exercise| + puts "\t\t> #{exercise.title}" + avgwt = exercise.average_working_time.split(':') + seconds = avgwt[0].to_i * 60 * 60 + avgwt[1].to_i * 60 + avgwt[2].to_f + working_times[exercise.id] = seconds + end + average = working_times.values.reduce(:+) / working_times.size + working_times.select do |exercise_id, working_time| + working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR + end + end + + def notify_collection_author(collection, anomalies) + puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..." + UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now + end + + def notify_users(collection, anomalies) + puts "\t\tSending E-Mails to best and worst performing users of each anomaly..." + anomalies.each do |exercise_id, average_working_time| + submissions = Submission.find_by_sql([' select distinct s.* from ( @@ -63,23 +79,22 @@ namespace :detect_exercise_anomalies do join submissions s on s.id = t.fv where score is not null order by score', exercise_id]) - best_performers = submissions.first(10).to_a.map do |item| - item.user_id - end - worst_performers = submissions.last(10).to_a.map do |item| - item.user_id - end - puts "\t\tAnomaly in exercise #{exercise_id}:" - puts "\t\t\tbest performers: #{best_performers}" - puts "\t\t\tworst performers: #{worst_performers}" - end - - puts "\t\tResetting flag..." - collection.use_anomaly_detection = false - collection.save! + best_performers = submissions.first(10).to_a.map do |item| + item.user_id end + worst_performers = submissions.last(10).to_a.map do |item| + item.user_id + end + puts "\t\tAnomaly in exercise #{exercise_id}:" + puts "\t\t\tbest performers: #{best_performers}" + puts "\t\t\tworst performers: #{worst_performers}" end - puts "\tDone." + end + + def reset_anomaly_detection_flag(collection) + puts "\t\tResetting flag..." + collection.use_anomaly_detection = false + collection.save! end end From 08f16447f3553f1e7a9e9a1156b2d5f75173148c Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 4 Feb 2018 15:41:40 +0100 Subject: [PATCH 26/38] Cache working time query results --- lib/tasks/detect_exercise_anomalies.rake | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index f2ca9a8f..f8e5bc2a 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -5,6 +5,9 @@ namespace :detect_exercise_anomalies do MIN_TIME_FACTOR = 0.1 MAX_TIME_FACTOR = 2 + # Cache exercise working times, because queries are expensive and values do not change between collections + WORKING_TIME_CACHE = {} + task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args| number_of_exercises = args[:number_of_exercises] number_of_solutions = args[:number_of_solutions] @@ -48,9 +51,7 @@ namespace :detect_exercise_anomalies do working_times = {} collection.exercises.each do |exercise| puts "\t\t> #{exercise.title}" - avgwt = exercise.average_working_time.split(':') - seconds = avgwt[0].to_i * 60 * 60 + avgwt[1].to_i * 60 + avgwt[2].to_f - working_times[exercise.id] = seconds + working_times[exercise.id] = get_working_time(exercise) end average = working_times.values.reduce(:+) / working_times.size working_times.select do |exercise_id, working_time| @@ -58,6 +59,15 @@ namespace :detect_exercise_anomalies do end end + def get_working_time(exercise) + unless WORKING_TIME_CACHE.key?(exercise.id) + avgwt = exercise.average_working_time.split(':') + seconds = avgwt[0].to_i * 60 * 60 + avgwt[1].to_i * 60 + avgwt[2].to_f + WORKING_TIME_CACHE[exercise.id] = seconds + end + WORKING_TIME_CACHE[exercise.id] + end + def notify_collection_author(collection, anomalies) puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..." UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now From 06928340c9950bec60220fc2041d2410212a8e20 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Sun, 4 Feb 2018 16:27:11 +0100 Subject: [PATCH 27/38] Extract last submission per user to Exercise model --- app/models/exercise.rb | 10 ++++++++++ lib/tasks/detect_exercise_anomalies.rake | 17 ++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index f03d6641..7902b088 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -368,4 +368,14 @@ class Exercise < ActiveRecord::Base user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS end + def last_submission_per_user + Submission.joins("JOIN ( + SELECT + user_id, + first_value(id) OVER (PARTITION BY user_id ORDER BY created_at DESC) AS fv + FROM submissions + WHERE exercise_id = #{id} + ) AS t ON t.fv = submissions.id").distinct + end + end diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index f8e5bc2a..e9532bcc 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -76,19 +76,10 @@ namespace :detect_exercise_anomalies do def notify_users(collection, anomalies) puts "\t\tSending E-Mails to best and worst performing users of each anomaly..." anomalies.each do |exercise_id, average_working_time| - submissions = Submission.find_by_sql([' - select distinct s.* - from - ( - select - user_id, - first_value(id) over (partition by user_id order by created_at desc) as fv - from submissions - where exercise_id = ? - ) as t - join submissions s on s.id = t.fv - where score is not null - order by score', exercise_id]) + submissions = Exercise.find(exercise_id) + .last_submission_per_user + .where('score is not null') + .order(:score) best_performers = submissions.first(10).to_a.map do |item| item.user_id end From 4c97faeec9f8aad5bb7531fa90e00a8187a515f2 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 20 Feb 2018 21:25:15 +0100 Subject: [PATCH 28/38] Find best and worst performers w.r.t. working time --- app/models/exercise.rb | 1 + lib/tasks/detect_exercise_anomalies.rake | 74 ++++++++++++++++++------ 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 7902b088..9d4cdf79 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -36,6 +36,7 @@ class Exercise < ActiveRecord::Base validates :token, presence: true, uniqueness: true @working_time_statistics = nil + attr_reader :working_time_statistics MAX_EXERCISE_FEEDBACKS = 20 diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index e9532bcc..9f8eb78b 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -5,8 +5,15 @@ namespace :detect_exercise_anomalies do MIN_TIME_FACTOR = 0.1 MAX_TIME_FACTOR = 2 + # Determines how many users are picked from the best/average/worst performers of each anomaly for feedback + NUMBER_OF_USERS_PER_CLASS = 10 + + # Determines margin below which user working times will be considered data errors (e.g. copy/paste solutions) + MIN_USER_WORKING_TIME = 0.0 + # Cache exercise working times, because queries are expensive and values do not change between collections WORKING_TIME_CACHE = {} + AVERAGE_WORKING_TIME_CACHE = {} task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args| number_of_exercises = args[:number_of_exercises] @@ -23,7 +30,6 @@ namespace :detect_exercise_anomalies do anomalies = find_anomalies(collection) if anomalies.length > 0 and not collection.user.nil? - puts "\t\tAnomalies: #{anomalies}\n" notify_collection_author(collection, anomalies) notify_users(collection, anomalies) reset_anomaly_detection_flag(collection) @@ -51,7 +57,7 @@ namespace :detect_exercise_anomalies do working_times = {} collection.exercises.each do |exercise| puts "\t\t> #{exercise.title}" - working_times[exercise.id] = get_working_time(exercise) + working_times[exercise.id] = get_average_working_time(exercise) end average = working_times.values.reduce(:+) / working_times.size working_times.select do |exercise_id, working_time| @@ -59,11 +65,26 @@ namespace :detect_exercise_anomalies do end end - def get_working_time(exercise) + def time_to_f(timestamp) + unless timestamp.nil? + timestamp = timestamp.split(':') + return timestamp[0].to_i * 60 * 60 + timestamp[1].to_i * 60 + timestamp[2].to_f + end + nil + end + + def get_average_working_time(exercise) + unless AVERAGE_WORKING_TIME_CACHE.key?(exercise.id) + seconds = time_to_f exercise.average_working_time + AVERAGE_WORKING_TIME_CACHE[exercise.id] = seconds + end + AVERAGE_WORKING_TIME_CACHE[exercise.id] + end + + def get_user_working_times(exercise) unless WORKING_TIME_CACHE.key?(exercise.id) - avgwt = exercise.average_working_time.split(':') - seconds = avgwt[0].to_i * 60 * 60 + avgwt[1].to_i * 60 + avgwt[2].to_f - WORKING_TIME_CACHE[exercise.id] = seconds + exercise.retrieve_working_time_statistics + WORKING_TIME_CACHE[exercise.id] = exercise.working_time_statistics end WORKING_TIME_CACHE[exercise.id] end @@ -76,22 +97,39 @@ namespace :detect_exercise_anomalies do def notify_users(collection, anomalies) puts "\t\tSending E-Mails to best and worst performing users of each anomaly..." anomalies.each do |exercise_id, average_working_time| - submissions = Exercise.find(exercise_id) - .last_submission_per_user - .where('score is not null') - .order(:score) - best_performers = submissions.first(10).to_a.map do |item| - item.user_id + puts "\t\tAnomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):" + exercise = Exercise.find(exercise_id) + submissions = exercise.last_submission_per_user + + users = performers_by_score(submissions, NUMBER_OF_USERS_PER_CLASS) + users = users.merge(performers_by_time(exercise, NUMBER_OF_USERS_PER_CLASS)) {|key, this, other| this + other} + + [:best, :average, :worst].each do |sym| + segment = users[sym].uniq + puts "\t\t\t#{sym.to_s} performers: #{segment}" end - worst_performers = submissions.last(10).to_a.map do |item| - item.user_id - end - puts "\t\tAnomaly in exercise #{exercise_id}:" - puts "\t\t\tbest performers: #{best_performers}" - puts "\t\t\tworst performers: #{worst_performers}" end end + def performers_by_score(submissions, n) + submissions = submissions.where('score is not null').order(:score) + best_performers = submissions.first(n).to_a.map {|item| item.user_id} + worst_performers = submissions.last(n).to_a.map {|item| item.user_id} + + return {:best => best_performers, :average => [], :worst => worst_performers} + end + + def performers_by_time(exercise, n) + working_times = get_user_working_times(exercise).values.map do |item| + {user_id: item['user_id'], time: time_to_f(item['working_time'])} + end + working_times.reject! {|item| item[:time].nil? or item[:time] <= MIN_USER_WORKING_TIME} + working_times.sort_by! {|item| item[:time]} + + working_times.map! {|item| item[:user_id].to_i} + return {:best => working_times.first(n), :average => [], :worst => working_times.last(n)} + end + def reset_anomaly_detection_flag(collection) puts "\t\tResetting flag..." collection.use_anomaly_detection = false From cce6b5532dc0a8b36ca0c35764504208ef6908cb Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 26 Feb 2018 14:12:16 +0100 Subject: [PATCH 29/38] Refactor and prepare sending e-mails --- lib/tasks/detect_exercise_anomalies.rake | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 9f8eb78b..8c0c37d3 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -99,24 +99,31 @@ namespace :detect_exercise_anomalies do anomalies.each do |exercise_id, average_working_time| puts "\t\tAnomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):" exercise = Exercise.find(exercise_id) - submissions = exercise.last_submission_per_user + users_to_notify = [] - users = performers_by_score(submissions, NUMBER_OF_USERS_PER_CLASS) - users = users.merge(performers_by_time(exercise, NUMBER_OF_USERS_PER_CLASS)) {|key, this, other| this + other} - - [:best, :average, :worst].each do |sym| - segment = users[sym].uniq - puts "\t\t\t#{sym.to_s} performers: #{segment}" + users = {} + [:performers_by_time, :performers_by_score].each do |method| + # merge users found by multiple methods returning a hash {best: [], worst: []} + users = users.merge(send(method, exercise, NUMBER_OF_USERS_PER_CLASS)) {|key, this, other| this + other} end + + users.keys.each do |key| + segment = users[key].uniq + puts "\t\t\t#{key.to_s} performers: #{segment}" + users_to_notify += segment + end + + users_to_notify.uniq! + # todo: send emails end end - def performers_by_score(submissions, n) - submissions = submissions.where('score is not null').order(:score) + def performers_by_score(exercise, n) + submissions = exercise.last_submission_per_user.where('score is not null').order(:score) best_performers = submissions.first(n).to_a.map {|item| item.user_id} worst_performers = submissions.last(n).to_a.map {|item| item.user_id} - return {:best => best_performers, :average => [], :worst => worst_performers} + return {:best => best_performers, :worst => worst_performers} end def performers_by_time(exercise, n) @@ -127,7 +134,7 @@ namespace :detect_exercise_anomalies do working_times.sort_by! {|item| item[:time]} working_times.map! {|item| item[:user_id].to_i} - return {:best => working_times.first(n), :average => [], :worst => working_times.last(n)} + return {:best => working_times.first(n), :worst => working_times.last(n)} end def reset_anomaly_detection_flag(collection) From 357712eac76600ff7130d16a722182656474e965 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 26 Feb 2018 15:26:48 +0100 Subject: [PATCH 30/38] Persist reasons for notifications to db --- app/models/anomaly_notification.rb | 5 ++++ app/models/exercise.rb | 12 +++++--- ...0226131340_create_anomaly_notifications.rb | 11 +++++++ db/schema.rb | 16 +++++++++- lib/tasks/detect_exercise_anomalies.rake | 30 +++++++++++-------- 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 app/models/anomaly_notification.rb create mode 100644 db/migrate/20180226131340_create_anomaly_notifications.rb diff --git a/app/models/anomaly_notification.rb b/app/models/anomaly_notification.rb new file mode 100644 index 00000000..8c9b90cd --- /dev/null +++ b/app/models/anomaly_notification.rb @@ -0,0 +1,5 @@ +class AnomalyNotification < ActiveRecord::Base + belongs_to :user, polymorphic: true + belongs_to :exercise + belongs_to :exercise_collection +end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 9d4cdf79..151e5437 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -66,21 +66,24 @@ class Exercise < ActiveRecord::Base end def user_working_time_query - """ + " SELECT user_id, + user_type, sum(working_time_new) AS working_time FROM (SELECT user_id, + user_type, CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new FROM (SELECT user_id, + user_type, id, (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id ORDER BY created_at)) AS working_time FROM submissions WHERE exercise_id=#{id}) AS foo) AS bar - GROUP BY user_id - """ + GROUP BY user_id, user_type + " end def get_quantiles(quantiles) @@ -203,7 +206,7 @@ class Exercise < ActiveRecord::Base def retrieve_working_time_statistics @working_time_statistics = {} self.class.connection.execute(user_working_time_query).each do |tuple| - @working_time_statistics[tuple["user_id"].to_i] = tuple + @working_time_statistics[tuple['user_id'].to_i] = tuple end end @@ -373,6 +376,7 @@ class Exercise < ActiveRecord::Base Submission.joins("JOIN ( SELECT user_id, + user_type, first_value(id) OVER (PARTITION BY user_id ORDER BY created_at DESC) AS fv FROM submissions WHERE exercise_id = #{id} diff --git a/db/migrate/20180226131340_create_anomaly_notifications.rb b/db/migrate/20180226131340_create_anomaly_notifications.rb new file mode 100644 index 00000000..5de3dbe4 --- /dev/null +++ b/db/migrate/20180226131340_create_anomaly_notifications.rb @@ -0,0 +1,11 @@ +class CreateAnomalyNotifications < ActiveRecord::Migration + def change + create_table :anomaly_notifications do |t| + t.belongs_to :user, polymorphic: true, index: true + t.belongs_to :exercise, index: true + t.belongs_to :exercise_collection, index: true + t.string :reason + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 828e8e41..a40d3e7f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,25 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171210172208) do +ActiveRecord::Schema.define(version: 20180226131340) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "anomaly_notifications", force: :cascade do |t| + t.integer "user_id" + t.string "user_type" + t.integer "exercise_id" + t.integer "exercise_collection_id" + t.string "reason" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "anomaly_notifications", ["exercise_collection_id"], name: "index_anomaly_notifications_on_exercise_collection_id", using: :btree + add_index "anomaly_notifications", ["exercise_id"], name: "index_anomaly_notifications_on_exercise_id", using: :btree + add_index "anomaly_notifications", ["user_type", "user_id"], name: "index_anomaly_notifications_on_user_type_and_user_id", using: :btree + create_table "code_harbor_links", force: :cascade do |t| t.string "oauth2token", limit: 255 t.datetime "created_at" diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 8c0c37d3..e271387c 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -95,6 +95,8 @@ namespace :detect_exercise_anomalies do 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..." anomalies.each do |exercise_id, average_working_time| puts "\t\tAnomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):" @@ -107,33 +109,37 @@ namespace :detect_exercise_anomalies do users = users.merge(send(method, exercise, NUMBER_OF_USERS_PER_CLASS)) {|key, this, other| this + other} end + # write reasons for feedback emails to db users.keys.each do |key| - segment = users[key].uniq - puts "\t\t\t#{key.to_s} performers: #{segment}" + segment = users[key].uniq &by_id_and_type users_to_notify += segment + segment.each do |user| + reason = "{\"segment\": \"#{key.to_s}\", \"feature\": \"#{user[:reason]}\", value: \"#{user[:value]}\"}" + AnomalyNotification.create(user_id: user[:user_id], user_type: user[:user_type], + exercise: exercise, exercise_collection: collection, reason: reason) + end end - users_to_notify.uniq! + users_to_notify.uniq! &by_id_and_type + puts "\t\tAsked #{users_to_notify.size} users for feedback." # todo: send emails end end def performers_by_score(exercise, n) - submissions = exercise.last_submission_per_user.where('score is not null').order(:score) - best_performers = submissions.first(n).to_a.map {|item| item.user_id} - worst_performers = submissions.last(n).to_a.map {|item| item.user_id} - + submissions = exercise.last_submission_per_user.where('score is not null').order(score: :desc) + map_block = proc {|item| {user_id: item.user_id, user_type: item.user_type, value: item.score, reason: 'score'}} + best_performers = submissions.first(n).to_a.map &map_block + worst_performers = submissions.last(n).to_a.map &map_block return {:best => best_performers, :worst => worst_performers} end def performers_by_time(exercise, n) working_times = get_user_working_times(exercise).values.map do |item| - {user_id: item['user_id'], time: time_to_f(item['working_time'])} + {user_id: item['user_id'], user_type: item['user_type'], value: time_to_f(item['working_time']), reason: 'time'} end - working_times.reject! {|item| item[:time].nil? or item[:time] <= MIN_USER_WORKING_TIME} - working_times.sort_by! {|item| item[:time]} - - working_times.map! {|item| item[:user_id].to_i} + working_times.reject! {|item| item[:value].nil? or item[:value] <= MIN_USER_WORKING_TIME} + working_times.sort_by! {|item| item[:value]} return {:best => working_times.first(n), :worst => working_times.last(n)} end From 30fd465780e6493c59711f3c4998a83cffb5ae20 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 26 Feb 2018 17:55:18 +0100 Subject: [PATCH 31/38] Send emails --- app/mailers/user_mailer.rb | 7 +++++ .../exercise_anomaly_needs_feedback.html.slim | 1 + config/locales/de.yml | 27 +++++++++++++++++++ config/locales/en.yml | 27 +++++++++++++++++++ lib/tasks/detect_exercise_anomalies.rake | 6 ++++- 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 app/views/user_mailer/exercise_anomaly_needs_feedback.html.slim diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 73367d10..497c2a31 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -44,4 +44,11 @@ class UserMailer < ActionMailer::Base @anomalies = anomalies mail(subject: t('mailers.user_mailer.exercise_anomaly_detected.subject'), to: exercise_collection.user.email) end + + def exercise_anomaly_needs_feedback(user, exercise, link) + @receiver_displayname = user.displayname + @exercise_title = exercise.title + @link = link + mail(subject: t('mailers.user_mailer.exercise_anomaly_needs_feedback.subject'), to: user.email) + end end diff --git a/app/views/user_mailer/exercise_anomaly_needs_feedback.html.slim b/app/views/user_mailer/exercise_anomaly_needs_feedback.html.slim new file mode 100644 index 00000000..336f3c29 --- /dev/null +++ b/app/views/user_mailer/exercise_anomaly_needs_feedback.html.slim @@ -0,0 +1 @@ +== t('mailers.user_mailer.exercise_anomaly_needs_feedback.body', receiver_displayname: @receiver_displayname, exercise: @exercise_title, link: link_to(@link, @link)) diff --git a/config/locales/de.yml b/config/locales/de.yml index 1e7d1d00..ff353b9a 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -551,6 +551,33 @@ de: If you receive an error that you are not authorized to perform this action when clicking a link, please log-in through any course exercise beforehand and click the link again.

This mail was automatically sent by CodeOcean.
+ exercise_anomaly_needs_feedback: + body: | + English version below
+ _________________________
+
+ Hallo %{receiver_displayname},
+
+ um die Aufgaben auf CodeOcean weiter zu verbessern, benötigen wir Ihre Mithilfe. Bitte nehmen Sie sich ein paar Minuten Zeit um ein kurzes Feedback zu folgender Aufgabe zu geben: +
+ %{exercise} - %{link} +
+ Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.
+
+ Diese Mail wurde automatisch von CodeOcean verschickt.
+
+ _________________________
+
+ Dear %{receiver_displayname},
+
+ we need your help to improve the quality of the exercises on CodeOcean. Please take a few minutes to give us feedback for the following exercise: +
+ %{exercise} - %{link} +
+ If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again.
+
+ This mail was automatically sent by CodeOcean.
+ subject: "Eine Aufgabe auf CodeOcean benötigt Ihr Feedback" request_for_comments: click_here: Zum Kommentieren auf die Seitenleiste klicken! comments: Kommentare diff --git a/config/locales/en.yml b/config/locales/en.yml index d5e8cd4d..7d475c88 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -551,6 +551,33 @@ en: If you receive an error that you are not authorized to perform this action when clicking a link, please log-in through any course exercise beforehand and click the link again.

This mail was automatically sent by CodeOcean.
+ exercise_anomaly_needs_feedback: + body: | + English version below
+ _________________________
+
+ Hallo %{receiver_displayname},
+
+ um die Aufgaben auf CodeOcean weiter zu verbessern, benötigen wir Ihre Mithilfe. Bitte nehmen Sie sich ein paar Minuten Zeit um ein kurzes Feedback zu folgender Aufgabe zu geben: +
+ %{exercise} - %{link} +
+ Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.
+
+ Diese Mail wurde automatisch von CodeOcean verschickt.
+
+ _________________________
+
+ Dear %{receiver_displayname},
+
+ we need your help to improve the quality of the exercises on CodeOcean. Please take a few minutes to give us feedback for the following exercise: +
+ %{exercise} - %{link} +
+ If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again.
+
+ This mail was automatically sent by CodeOcean.
+ subject: "An exercise on CodeOcean needs your feedback" request_for_comments: click_here: Click on this sidebar to comment! comments: Comments diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index e271387c..0a9b9b7e 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -121,8 +121,12 @@ namespace :detect_exercise_anomalies do end users_to_notify.uniq! &by_id_and_type + users_to_notify.each do |u| + user = u[:user_type] == InternalUser.name ? InternalUser.find(u[:user_id]) : ExternalUser.find(u[:user_id]) + feedback_link = 'http://google.com' + UserMailer.exercise_anomaly_needs_feedback(user, exercise, feedback_link).deliver + end puts "\t\tAsked #{users_to_notify.size} users for feedback." - # todo: send emails end end From 3fe7a2b0c1122842938c3eee27c8bbad19c9795b Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 26 Feb 2018 19:33:02 +0100 Subject: [PATCH 32/38] Adapt new route to accept simple exercise_id parameter --- app/controllers/user_exercise_feedbacks_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb index 8abffd66..4c350a81 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -69,7 +69,8 @@ class UserExerciseFeedbacksController < ApplicationController @texts = comment_presets.to_a @times = time_presets.to_a @uef = UserExerciseFeedback.new - @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) + exercise_id = if params[:user_exercise_feedback].nil? then params[:exercise_id] else params[:user_exercise_feedback][:exercise_id] end + @exercise = Exercise.find(exercise_id) authorize! end From 0ba94574b51fad115aa48ce9b0781a654ebe1610 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 26 Feb 2018 19:33:34 +0100 Subject: [PATCH 33/38] Use correct link for feedback emails --- lib/tasks/detect_exercise_anomalies.rake | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index 0a9b9b7e..c36f5dbe 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -1,4 +1,10 @@ +include Rails.application.routes.url_helpers + namespace :detect_exercise_anomalies do + # uncomment for debug logging: + # logger = Logger.new(STDOUT) + # logger.level = Logger::DEBUG + # Rails.logger = logger # These factors determine if an exercise is an anomaly, given the average working time (avg): # (avg * MIN_TIME_FACTOR) <= working_time <= (avg * MAX_TIME_FACTOR) @@ -123,7 +129,8 @@ namespace :detect_exercise_anomalies do users_to_notify.uniq! &by_id_and_type users_to_notify.each do |u| user = u[:user_type] == InternalUser.name ? InternalUser.find(u[:user_id]) : ExternalUser.find(u[:user_id]) - feedback_link = 'http://google.com' + host = CodeOcean::Application.config.action_mailer.default_url_options[:host] + 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." From 73929512c67a9c03f11a22bcf054a20ea1cc4076 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 26 Feb 2018 19:54:11 +0100 Subject: [PATCH 34/38] Only ask for feedback from fast users if they achieved an above-average score --- app/models/exercise.rb | 5 ++++- lib/tasks/detect_exercise_anomalies.rake | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 151e5437..efdb00c7 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -69,14 +69,17 @@ class Exercise < ActiveRecord::Base " SELECT user_id, user_type, - sum(working_time_new) AS working_time + SUM(working_time_new) AS working_time, + MAX(score) AS score FROM (SELECT user_id, user_type, + score, CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new FROM (SELECT user_id, user_type, + score, id, (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id ORDER BY created_at)) AS working_time diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index c36f5dbe..e22550b2 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -147,9 +147,11 @@ namespace :detect_exercise_anomalies do def performers_by_time(exercise, n) working_times = get_user_working_times(exercise).values.map do |item| - {user_id: item['user_id'], user_type: item['user_type'], value: time_to_f(item['working_time']), reason: 'time'} + {user_id: item['user_id'], user_type: item['user_type'], score: item['score'].to_f, + value: time_to_f(item['working_time']), reason: 'time'} end - working_times.reject! {|item| item[:value].nil? or item[:value] <= MIN_USER_WORKING_TIME} + avg_score = exercise.average_score + working_times.reject! {|item| item[:value].nil? or item[:value] <= MIN_USER_WORKING_TIME or item[:score] < avg_score} working_times.sort_by! {|item| item[:value]} return {:best => working_times.first(n), :worst => working_times.last(n)} end From 782f9eea73e1b9ed6d705d593f40e90f3cbc2014 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 28 Feb 2018 16:51:13 +0100 Subject: [PATCH 35/38] Update schema --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 6f00ada8..55339422 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: 20180222145909) do +ActiveRecord::Schema.define(version: 20180226131340) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 8b259e44fee8b163b988ed2e8e3fc9267b1f100b Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 8 Mar 2018 12:31:56 +0100 Subject: [PATCH 36/38] Log task output to subdirectory of the log directory --- .gitignore | 2 +- config/schedule.rb | 2 +- log/whenever/.gitignore | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 log/whenever/.gitignore diff --git a/.gitignore b/.gitignore index af16e87b..c1030982 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ /config/*.staging-epic.yml /config/deploy/staging-epic.rb /coverage -/log +/log/*.* /public/assets /public/uploads /rubocop.html diff --git a/config/schedule.rb b/config/schedule.rb index 4132ba28..f7ad2f74 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -19,7 +19,7 @@ # Learn more: http://github.com/javan/whenever -set :output, Whenever.path + '/log/whenever_$(date +%Y%m%d%H%M%S).log' +set :output, Whenever.path + '/log/whenever/whenever_$(date +%Y%m%d%H%M%S).log' set :environment, ENV['RAILS_ENV'] if ENV['RAILS_ENV'] every 1.day, at: '3:00 am' do diff --git a/log/whenever/.gitignore b/log/whenever/.gitignore new file mode 100644 index 00000000..397b4a76 --- /dev/null +++ b/log/whenever/.gitignore @@ -0,0 +1 @@ +*.log From b55704e5231501833c5f56f8d38a2655edf22878 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 8 Mar 2018 12:52:20 +0100 Subject: [PATCH 37/38] Fix gemfile.lock --- Gemfile.lock | 390 ++++++++++++++++++++++++--------------------------- 1 file changed, 186 insertions(+), 204 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 14913288..7edb56f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - ZenTest (4.11.0) + ZenTest (4.11.1) actionmailer (4.2.10) actionpack (= 4.2.10) actionview (= 4.2.10) @@ -32,195 +32,183 @@ GEM activesupport (= 4.2.10) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) - activerecord-jdbc-adapter (1.3.19) - activerecord (>= 2.2) - activerecord-jdbcpostgresql-adapter (1.3.19) - activerecord-jdbc-adapter (~> 1.3.19) - jdbc-postgres (>= 9.1) activesupport (4.2.10) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.4.0) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + airbrussh (1.3.0) + sshkit (>= 1.6.1, != 1.7.0) arel (6.0.4) - ast (2.3.0) + ast (2.4.0) autotest-rails (4.2.1) ZenTest (~> 4.5) - bcrypt (3.1.10) - bcrypt (3.1.10-java) - better_errors (2.1.1) + bcrypt (3.1.11) + better_errors (2.4.0) coderay (>= 1.0.0) - erubis (>= 2.6.6) + erubi (>= 1.0.0) rack (>= 0.9.0) - binding_of_caller (0.7.2) + binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootstrap-will_paginate (0.0.10) + bootstrap-will_paginate (1.0.0) will_paginate bootstrap_pagedown (1.1.0) rails (>= 3.2) builder (3.2.3) - byebug (8.2.2) - capistrano (3.3.5) - capistrano-stats (~> 1.1.0) + byebug (10.0.0) + capistrano (3.10.1) + airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) - sshkit (~> 1.3) - capistrano-bundler (1.1.4) + sshkit (>= 1.9.0) + capistrano-bundler (1.3.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.1.6) + capistrano-rails (1.3.1) capistrano (~> 3.1) capistrano-bundler (~> 1.1) capistrano-rvm (0.1.2) capistrano (~> 3.0) sshkit (~> 1.2) - capistrano-stats (1.1.1) - capistrano-upload-config (0.7.0) + capistrano-upload-config (0.8.2) capistrano (>= 3.0) - capistrano3-puma (1.2.1) - capistrano (~> 3.0) - puma (>= 2.6) - capybara (2.6.2) + capistrano3-puma (3.1.1) + capistrano (~> 3.7) + capistrano-bundler + puma (~> 3.4) + capybara (2.18.0) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - xpath (~> 2.0) - carrierwave (0.10.0) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) - json (>= 1.7) + xpath (>= 2.0, < 4.0) + carrierwave (1.2.2) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) mime-types (>= 1.16) - childprocess (0.5.9) + childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) - codeclimate-test-reporter (0.4.8) - simplecov (>= 0.7.1, < 1.0.0) - coderay (1.1.0) - coffee-rails (4.0.1) + codeclimate-test-reporter (1.0.7) + simplecov + coderay (1.1.2) + coffee-rails (4.2.2) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.10.0) - concurrent-ruby (1.0.2) - concurrent-ruby (1.0.2-java) - concurrent-ruby-ext (1.0.2) - concurrent-ruby (~> 1.0.2) + coffee-script-source (1.12.2) + concurrent-ruby (1.0.5) + concurrent-ruby-ext (1.0.5) + concurrent-ruby (= 1.0.5) crass (1.0.3) - d3-rails (3.5.11) + d3-rails (4.13.0) railties (>= 3.1) - database_cleaner (1.5.1) - debug_inspector (0.0.2) - diff-lcs (1.2.5) + database_cleaner (1.6.2) + debug_inspector (0.0.3) + diff-lcs (1.3) docile (1.1.5) - docker-api (1.25.0) - excon (>= 0.38.0) - json - domain_name (0.5.25) + docker-api (1.34.1) + excon (>= 0.47.0) + multi_json + domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) + erubi (1.7.1) erubis (2.7.0) eventmachine (1.0.9.1) - eventmachine (1.0.9.1-java) - excon (0.54.0) - execjs (2.6.0) + excon (0.60.0) + execjs (2.7.0) factory_bot (4.8.2) activesupport (>= 3.0.0) factory_bot_rails (4.8.2) factory_bot (~> 4.8.2) railties (>= 3.0.0) - faraday (0.9.2) + faraday (0.12.2) multipart-post (>= 1.2, < 3) - faye-websocket (0.10.2) + faye-websocket (0.10.7) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.10) - ffi (1.9.10-java) - forgery (0.6.0) + ffi (1.9.23) + forgery (0.7.0) globalid (0.4.1) activesupport (>= 4.2.0) - highline (1.7.8) - hike (1.2.3) - http-cookie (1.0.2) + highline (1.7.10) + http-cookie (1.0.3) domain_name (~> 0.5) i18n (0.9.5) concurrent-ruby (~> 1.0) ims-lti (1.1.10) builder oauth (~> 0.4.5) - jbuilder (2.4.1) - activesupport (>= 3.0.0, < 5.1) - multi_json (~> 1.2) - jdbc-postgres (9.4.1206) - jquery-rails (3.1.4) - railties (>= 3.0, < 5.0) + jbuilder (2.7.0) + activesupport (>= 4.2.0) + multi_json (>= 1.2) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks - json (1.8.6) - json (1.8.6-java) - jwt (1.5.1) - kramdown (1.9.0) + json (2.1.0) + jwt (1.5.6) + kramdown (1.16.2) loofah (2.2.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) mini_mime (>= 0.1.1) - method_source (0.8.2) - mime-types (2.99.3) + method_source (0.9.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.11.3) multi_json (1.13.1) - multi_xml (0.5.5) + multi_xml (0.6.0) multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (3.0.2) - netrc (0.10.3) - newrelic_rpm (3.14.3.313) + net-ssh (4.2.0) + netrc (0.11.0) + newrelic_rpm (4.8.0.341) nokogiri (1.8.2) mini_portile2 (~> 2.3.0) - nokogiri (1.8.2-java) - nyan-cat-formatter (0.11) + nyan-cat-formatter (0.12.0) rspec (>= 2.99, >= 2.14.2, < 4) oauth (0.4.7) - oauth2 (1.1.0) - faraday (>= 0.8, < 0.10) - jwt (~> 1.0, < 1.5.2) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) + jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) pagedown-rails (1.1.4) railties (> 3.1) - parser (2.3.3.1) - ast (~> 2.2) - pg (0.18.4) - polyamorous (1.3.0) + parallel (1.12.1) + parser (2.5.0.3) + ast (~> 2.4.0) + pg (0.21.0) + polyamorous (1.3.3) activerecord (>= 3.0) powerpack (0.1.1) - pry (0.10.3) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry (0.10.3-java) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - spoon (~> 0.0) - pry-byebug (3.3.0) - byebug (~> 8.0) + method_source (~> 0.9.0) + pry-byebug (3.6.0) + byebug (~> 10.0) pry (~> 0.10) - puma (2.15.3) - puma (2.15.3-java) + public_suffix (3.0.2) + puma (3.11.3) pundit (1.1.0) activesupport (>= 3.0.0) rack (1.6.9) - rack-mini-profiler (0.10.1) + rack-mini-profiler (0.10.7) rack (>= 1.2.0) rack-test (0.6.3) rack (>= 1.0) @@ -243,7 +231,7 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails-i18n (4.0.8) + rails-i18n (4.0.9) i18n (~> 0.7) railties (~> 4.0) railties (4.2.10) @@ -251,138 +239,132 @@ GEM activesupport (= 4.2.10) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.2.1) + rainbow (3.0.0) rake (12.3.0) - ransack (1.7.0) + ransack (1.8.7) actionpack (>= 3.0) activerecord (>= 3.0) activesupport (>= 3.0) i18n - polyamorous (~> 1.2) - rdoc (4.2.2) - json (~> 1.4) - rest-client (1.8.0) + polyamorous (~> 1.3.2) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + rdoc (6.0.1) + rest-client (2.0.2) http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) - rspec (3.4.0) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) rspec-autotest (1.0.0) rspec-core (>= 2.99.0.beta1, < 4.0.0) - rspec-core (3.4.4) - rspec-support (~> 3.4.0) - rspec-expectations (3.4.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-mocks (3.4.1) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-rails (3.4.2) - actionpack (>= 3.0, < 4.3) - activesupport (>= 3.0, < 4.3) - railties (>= 3.0, < 4.3) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) - rspec-support (~> 3.4.0) - rspec-support (3.4.1) - rubocop (0.46.0) - parser (>= 2.3.1.1, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.9.0) - rubocop (>= 0.42.0) - ruby-progressbar (1.8.1) - rubytree (0.9.7) - json (~> 1.8) - structured_warnings (~> 0.2) - rubyzip (1.1.7) - sass (3.2.19) - sass-rails (4.0.5) - railties (>= 4.0.0, < 5.0) - sass (~> 3.2.2) - sprockets (~> 2.8, < 3.0) - sprockets-rails (~> 2.0) - sdoc (0.4.1) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) - selenium-webdriver (2.52.0) - childprocess (~> 0.5) - multi_json (~> 1.0) - rubyzip (~> 1.0) - websocket (~> 1.0) - simplecov (0.11.2) - docile (~> 1.1.0) - json (~> 1.8) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.0) - slim (3.0.6) - temple (~> 0.7.3) - tilt (>= 1.3.3, < 2.1) - slop (3.6.0) - sorcery (0.9.1) - bcrypt (~> 3.1) - oauth (~> 0.4, >= 0.4.4) - oauth2 (>= 0.8.0) - spoon (0.0.6) - ffi - spring (1.6.3) - sprockets (2.12.4) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.3) + rspec-support (~> 3.7.0) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) + rubocop (0.53.0) + parallel (~> 1.10) + parser (>= 2.5) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-rspec (1.24.0) + rubocop (>= 0.53.0) + ruby-progressbar (1.9.0) + rubytree (1.0.0) + json (~> 2.1) + structured_warnings (~> 0.3) + rubyzip (1.2.1) + sass (3.5.5) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sass-rails (5.0.7) + railties (>= 4.0.0, < 6) + sass (~> 3.1) sprockets (>= 2.8, < 4.0) - sshkit (1.8.1) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + sdoc (1.0.0) + rdoc (>= 5.0) + selenium-webdriver (3.10.0) + childprocess (~> 0.5) + rubyzip (~> 1.2) + simplecov (0.15.1) + docile (~> 1.1.0) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) + slim (3.0.9) + temple (>= 0.7.6, < 0.9) + tilt (>= 1.3.3, < 2.1) + sorcery (0.11.0) + bcrypt (~> 3.1) + oauth (~> 0.4, >= 0.4.4) + oauth2 (~> 1.0, >= 0.8.0) + spring (2.0.2) + activesupport (>= 4.2) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sshkit (1.16.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - structured_warnings (0.2.0) - temple (0.7.6) + structured_warnings (0.3.0) + temple (0.8.0) thor (0.20.0) thread_safe (0.3.6) - thread_safe (0.3.6-java) - tilt (1.4.1) - tubesock (0.2.5) + tilt (2.0.8) + tubesock (0.2.7) rack (>= 1.5.0) websocket (>= 1.1.0) - turbolinks (2.5.3) + turbolinks (2.5.4) coffee-rails tzinfo (1.2.5) thread_safe (~> 0.1) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) + uglifier (4.1.6) + execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext - unf (0.1.4-java) - unf_ext (0.0.7.1) - unicode-display_width (1.1.2) - web-console (2.3.0) - activemodel (>= 4.0) - binding_of_caller (>= 0.7.2) - railties (>= 4.0) - sprockets-rails (>= 2.0, < 4.0) - websocket (1.2.2) - websocket-driver (0.6.3) + unf_ext (0.0.7.5) + unicode-display_width (1.3.0) + web-console (3.3.0) + activemodel (>= 4.2) + debug_inspector + railties (>= 4.2) + websocket (1.2.5) + websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) - websocket-driver (0.6.3-java) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) + websocket-extensions (0.1.3) whenever (0.10.0) chronic (>= 0.6.3) - will_paginate (3.1.0) - xpath (2.0.0) - nokogiri (~> 1.3) + will_paginate (3.1.6) + xpath (3.0.0) + nokogiri (~> 1.8) PLATFORMS - java ruby DEPENDENCIES From 534fd651e9f6544a935fcc16d62a175a26a17de7 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Sun, 25 Feb 2018 23:46:49 +0100 Subject: [PATCH 38/38] Update Vagrant provision.sh to execute command as non-root user Also, some commands were only available in an interactive shell, those were changed. Signed-off-by: Sebastian Serth --- Vagrantfile | 2 +- provision.sh | 102 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 37 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 5ec9182e..cc6acf2c 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -8,5 +8,5 @@ Vagrant.configure(2) do |config| end config.vm.network "private_network", ip: "192.168.59.104" # config.vm.synced_folder "../data", "/vagrant_data" - config.vm.provision "shell", path: "provision.sh" + config.vm.provision "shell", path: "provision.sh", privileged: false end diff --git a/provision.sh b/provision.sh index 60c9c04b..a6128d1d 100644 --- a/provision.sh +++ b/provision.sh @@ -2,71 +2,87 @@ # rvm/rails installation from https://gorails.com/setup/ubuntu/14.04 # passenger installation from https://www.phusionpassenger.com/library/install/nginx/install/oss/trusty/ +######## VERSION INFORMATION ######## + +postgres_version=10 +ruby_version=2.3.6 +rails_version=4.2.10 + +########## INSTALL SCRIPT ########### + +# PostgreSQL +sudo add-apt-repository "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -sc)-pgdg main" +wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + # passenger -apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7 -apt-get install -y apt-transport-https ca-certificates -sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main > /etc/apt/sources.list.d/passenger.list' +sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7 +sudo apt-get -qq -y install apt-transport-https ca-certificates +sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main > /etc/apt/sources.list.d/passenger.list' # rails -add-apt-repository ppa:chris-lea/node.js +sudo add-apt-repository -y ppa:chris-lea/node.js -apt-get update +sudo apt-get -qq update # code_ocean -apt-get install -y postgresql-client postgresql-10 postgresql-server-dev-10 vagrant +sudo apt-get -qq -y install postgresql-client postgresql-$postgres_version postgresql-server-dev-$postgres_version vagrant # Docker if [ ! -f /etc/default/docker ] then - curl -sSL https://get.docker.com/ | sh + curl -sSL https://get.docker.com/ | sudo sh fi if ! grep code_ocean /etc/default/docker then - cat >>/etc/default/docker </etc/postgresql/10/main/pg_hba.conf < /etc/nginx/sites-available/code_ocean <