Persist reasons for notifications to db
This commit is contained in:
5
app/models/anomaly_notification.rb
Normal file
5
app/models/anomaly_notification.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AnomalyNotification < ActiveRecord::Base
|
||||||
|
belongs_to :user, polymorphic: true
|
||||||
|
belongs_to :exercise
|
||||||
|
belongs_to :exercise_collection
|
||||||
|
end
|
@ -66,21 +66,24 @@ class Exercise < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_working_time_query
|
def user_working_time_query
|
||||||
"""
|
"
|
||||||
SELECT user_id,
|
SELECT user_id,
|
||||||
|
user_type,
|
||||||
sum(working_time_new) AS working_time
|
sum(working_time_new) AS working_time
|
||||||
FROM
|
FROM
|
||||||
(SELECT user_id,
|
(SELECT user_id,
|
||||||
|
user_type,
|
||||||
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||||
FROM
|
FROM
|
||||||
(SELECT user_id,
|
(SELECT user_id,
|
||||||
|
user_type,
|
||||||
id,
|
id,
|
||||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||||
ORDER BY created_at)) AS working_time
|
ORDER BY created_at)) AS working_time
|
||||||
FROM submissions
|
FROM submissions
|
||||||
WHERE exercise_id=#{id}) AS foo) AS bar
|
WHERE exercise_id=#{id}) AS foo) AS bar
|
||||||
GROUP BY user_id
|
GROUP BY user_id, user_type
|
||||||
"""
|
"
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_quantiles(quantiles)
|
def get_quantiles(quantiles)
|
||||||
@ -203,7 +206,7 @@ class Exercise < ActiveRecord::Base
|
|||||||
def retrieve_working_time_statistics
|
def retrieve_working_time_statistics
|
||||||
@working_time_statistics = {}
|
@working_time_statistics = {}
|
||||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -373,6 +376,7 @@ class Exercise < ActiveRecord::Base
|
|||||||
Submission.joins("JOIN (
|
Submission.joins("JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
user_id,
|
user_id,
|
||||||
|
user_type,
|
||||||
first_value(id) OVER (PARTITION BY user_id ORDER BY created_at DESC) AS fv
|
first_value(id) OVER (PARTITION BY user_id ORDER BY created_at DESC) AS fv
|
||||||
FROM submissions
|
FROM submissions
|
||||||
WHERE exercise_id = #{id}
|
WHERE exercise_id = #{id}
|
||||||
|
11
db/migrate/20180226131340_create_anomaly_notifications.rb
Normal file
11
db/migrate/20180226131340_create_anomaly_notifications.rb
Normal file
@ -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
|
16
db/schema.rb
16
db/schema.rb
@ -11,11 +11,25 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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|
|
create_table "code_harbor_links", force: :cascade do |t|
|
||||||
t.string "oauth2token", limit: 255
|
t.string "oauth2token", limit: 255
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
|
@ -95,6 +95,8 @@ namespace :detect_exercise_anomalies do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_users(collection, anomalies)
|
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..."
|
puts "\t\tSending E-Mails to best and worst performing users of each anomaly..."
|
||||||
anomalies.each do |exercise_id, average_working_time|
|
anomalies.each do |exercise_id, average_working_time|
|
||||||
puts "\t\tAnomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):"
|
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}
|
users = users.merge(send(method, exercise, NUMBER_OF_USERS_PER_CLASS)) {|key, this, other| this + other}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# write reasons for feedback emails to db
|
||||||
users.keys.each do |key|
|
users.keys.each do |key|
|
||||||
segment = users[key].uniq
|
segment = users[key].uniq &by_id_and_type
|
||||||
puts "\t\t\t#{key.to_s} performers: #{segment}"
|
|
||||||
users_to_notify += segment
|
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
|
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
|
# todo: send emails
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def performers_by_score(exercise, n)
|
def performers_by_score(exercise, n)
|
||||||
submissions = exercise.last_submission_per_user.where('score is not null').order(:score)
|
submissions = exercise.last_submission_per_user.where('score is not null').order(score: :desc)
|
||||||
best_performers = submissions.first(n).to_a.map {|item| item.user_id}
|
map_block = proc {|item| {user_id: item.user_id, user_type: item.user_type, value: item.score, reason: 'score'}}
|
||||||
worst_performers = submissions.last(n).to_a.map {|item| item.user_id}
|
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}
|
return {:best => best_performers, :worst => worst_performers}
|
||||||
end
|
end
|
||||||
|
|
||||||
def performers_by_time(exercise, n)
|
def performers_by_time(exercise, n)
|
||||||
working_times = get_user_working_times(exercise).values.map do |item|
|
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
|
end
|
||||||
working_times.reject! {|item| item[:time].nil? or item[:time] <= MIN_USER_WORKING_TIME}
|
working_times.reject! {|item| item[:value].nil? or item[:value] <= MIN_USER_WORKING_TIME}
|
||||||
working_times.sort_by! {|item| item[:time]}
|
working_times.sort_by! {|item| item[:value]}
|
||||||
|
|
||||||
working_times.map! {|item| item[:user_id].to_i}
|
|
||||||
return {:best => working_times.first(n), :worst => working_times.last(n)}
|
return {:best => working_times.first(n), :worst => working_times.last(n)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user