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