Merge branch 'master' into client-routesv2
This commit is contained in:
@@ -8,6 +8,11 @@ module User
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :user
|
||||
has_many :user_proxy_exercise_exercises, as: :user
|
||||
has_many :user_exercise_interventions, as: :user
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
||||
end
|
||||
@@ -21,6 +26,6 @@ module User
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
displayname
|
||||
end
|
||||
end
|
||||
|
@@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base
|
||||
belongs_to :execution_environment
|
||||
has_many :submissions
|
||||
|
||||
has_and_belongs_to_many :proxy_exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
has_and_belongs_to_many :exercise_collections
|
||||
has_many :user_exercise_interventions
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
has_many :exercise_tags
|
||||
has_many :tags, through: :exercise_tags
|
||||
accepts_nested_attributes_for :exercise_tags
|
||||
|
||||
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
|
||||
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
|
||||
alias_method :users, :external_users
|
||||
@@ -48,17 +57,21 @@ class Exercise < ActiveRecord::Base
|
||||
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
|
||||
end
|
||||
|
||||
def time_maximum_score(user)
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0)
|
||||
end
|
||||
|
||||
def user_working_time_query
|
||||
"""
|
||||
SELECT user_id,
|
||||
sum(working_time_new) AS working_time
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
CASE WHEN working_time >= '0:30: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
|
||||
(SELECT user_id,
|
||||
id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_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
|
||||
@@ -66,6 +79,123 @@ class Exercise < ActiveRecord::Base
|
||||
"""
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||
result = self.class.connection.execute("""
|
||||
WITH working_time AS
|
||||
(
|
||||
SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
Max(score) AS max_score,
|
||||
(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}
|
||||
AND user_type = 'ExternalUser'
|
||||
GROUP BY user_id,
|
||||
id,
|
||||
exercise_id), max_points AS
|
||||
(
|
||||
SELECT context_id AS ex_id,
|
||||
Sum(weight) AS max_points
|
||||
FROM files
|
||||
WHERE context_type = 'Exercise'
|
||||
AND context_id = #{id}
|
||||
AND role = 'teacher_defined_test'
|
||||
GROUP BY context_id),
|
||||
-- filter for rows containing max points
|
||||
time_max_score AS
|
||||
(
|
||||
SELECT *
|
||||
FROM working_time W1,
|
||||
max_points MS
|
||||
WHERE w1.exercise_id = ex_id
|
||||
AND w1.max_score = ms.max_points),
|
||||
-- find row containing the first time max points
|
||||
first_time_max_score AS
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
rn
|
||||
FROM (
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
Row_number() OVER(partition BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM time_max_score) T
|
||||
WHERE rn = 1), times_until_max_points AS
|
||||
(
|
||||
SELECT w.id,
|
||||
w.user_id,
|
||||
w.exercise_id,
|
||||
w.max_score,
|
||||
w.working_time,
|
||||
m.id AS reachedmax_at
|
||||
FROM working_time W,
|
||||
first_time_max_score M
|
||||
WHERE w.user_id = m.user_id
|
||||
AND w.exercise_id = m.exercise_id
|
||||
AND w.id <= m.id),
|
||||
-- if user never makes it to max points, take all times
|
||||
all_working_times_until_max AS (
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
FROM times_until_max_points)
|
||||
UNION ALL
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
FROM working_time W1
|
||||
WHERE NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM first_time_max_score F
|
||||
WHERE f.user_id = w1.user_id
|
||||
AND f.exercise_id = w1.exercise_id))), filtered_times_until_max AS
|
||||
(
|
||||
SELECT user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
CASE
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM all_working_times_until_max ), result AS
|
||||
(
|
||||
SELECT e.external_id AS external_user_id,
|
||||
f.user_id,
|
||||
exercise_id,
|
||||
Max(max_score) AS max_score,
|
||||
Sum(working_time_new) AS working_time
|
||||
FROM filtered_times_until_max f,
|
||||
external_users e
|
||||
WHERE f.user_id = e.id
|
||||
GROUP BY e.external_id,
|
||||
f.user_id,
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
""")
|
||||
if result.count > 0
|
||||
quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight}
|
||||
else
|
||||
quantiles.map{|q| 0}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@working_time_statistics = {}
|
||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
||||
@@ -88,23 +218,64 @@ class Exercise < ActiveRecord::Base
|
||||
@working_time_statistics[user_id]["working_time"]
|
||||
end
|
||||
|
||||
def average_working_time_for_only(user_id)
|
||||
self.class.connection.execute("""
|
||||
SELECT sum(working_time_new) AS working_time
|
||||
FROM
|
||||
(SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id} and user_id=#{user_id}) AS foo) AS bar
|
||||
""").first["working_time"]
|
||||
def accumulated_working_time_for_only(user)
|
||||
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
||||
Time.parse(self.class.connection.execute("""
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(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} AND user_id = #{user.id} AND user_type = '#{user_type}'
|
||||
GROUP BY user_id, id, exercise_id),
|
||||
MAX_POINTS AS
|
||||
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
|
||||
|
||||
-- filter for rows containing max points
|
||||
TIME_MAX_SCORE AS
|
||||
(SELECT *
|
||||
FROM WORKING_TIME W1, MAX_POINTS MS
|
||||
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
|
||||
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
FROM (
|
||||
SELECT id,USER_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM TIME_MAX_SCORE) T
|
||||
WHERE rn = 1),
|
||||
|
||||
TIMES_UNTIL_MAX_POINTS AS (
|
||||
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
|
||||
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
|
||||
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
|
||||
-- if user never makes it to max points, take all times
|
||||
ALL_WORKING_TIMES_UNTIL_MAX AS
|
||||
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
|
||||
UNION ALL
|
||||
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
|
||||
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
|
||||
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
|
||||
""").first["working_time"]).seconds_since_midnight rescue 0
|
||||
end
|
||||
|
||||
def duplicate(attributes = {})
|
||||
exercise = dup
|
||||
exercise.attributes = attributes
|
||||
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
|
||||
files.each { |file| exercise.files << file.dup }
|
||||
exercise
|
||||
end
|
||||
@@ -162,8 +333,16 @@ class Exercise < ActiveRecord::Base
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def maximum_score
|
||||
files.teacher_defined_tests.sum(:weight)
|
||||
def maximum_score(user = nil)
|
||||
if user
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
|
||||
else
|
||||
files.teacher_defined_tests.sum(:weight)
|
||||
end
|
||||
end
|
||||
|
||||
def has_user_solved(user)
|
||||
return maximum_score(user).to_i == maximum_score.to_i
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
|
5
app/models/exercise_collection.rb
Normal file
5
app/models/exercise_collection.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class ExerciseCollection < ActiveRecord::Base
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
|
||||
end
|
13
app/models/exercise_tag.rb
Normal file
13
app/models/exercise_tag.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class ExerciseTag < ActiveRecord::Base
|
||||
|
||||
belongs_to :tag
|
||||
belongs_to :exercise
|
||||
|
||||
before_save :destroy_if_empty_exercise_or_tag
|
||||
|
||||
private
|
||||
def destroy_if_empty_exercise_or_tag
|
||||
destroy if exercise_id.blank? || tag_id.blank?
|
||||
end
|
||||
|
||||
end
|
@@ -5,8 +5,8 @@ class ExternalUser < ActiveRecord::Base
|
||||
validates :external_id, presence: true
|
||||
|
||||
def displayname
|
||||
result = name
|
||||
if(consumer.name == 'openHPI')
|
||||
result = "User " + id.to_s
|
||||
if(!consumer.nil? && consumer.name == 'openHPI')
|
||||
result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do
|
||||
Xikolo::UserClient.get(external_id.to_s)[:display_name]
|
||||
end
|
||||
|
16
app/models/intervention.rb
Normal file
16
app/models/intervention.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class Intervention < ActiveRecord::Base
|
||||
|
||||
has_many :user_exercise_interventions
|
||||
has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser"
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def self.createDefaultInterventions
|
||||
%w(BreakIntervention QuestionIntervention).each do |name|
|
||||
Intervention.find_or_create_by(name: name)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
258
app/models/proxy_exercise.rb
Normal file
258
app/models/proxy_exercise.rb
Normal file
@@ -0,0 +1,258 @@
|
||||
class ProxyExercise < ActiveRecord::Base
|
||||
|
||||
after_initialize :generate_token
|
||||
after_initialize :set_reason
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
|
||||
def count_files
|
||||
exercises.count
|
||||
end
|
||||
|
||||
def set_reason
|
||||
@reason = {}
|
||||
end
|
||||
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.hex(4)
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def duplicate(attributes = {})
|
||||
proxy_exercise = dup
|
||||
proxy_exercise.attributes = attributes
|
||||
proxy_exercise
|
||||
end
|
||||
|
||||
def to_s
|
||||
title
|
||||
end
|
||||
|
||||
def get_matching_exercise(user)
|
||||
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
|
||||
recommended_exercise =
|
||||
if (assigned_user_proxy_exercise)
|
||||
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
|
||||
assigned_user_proxy_exercise.exercise
|
||||
else
|
||||
matching_exercise =
|
||||
if (token.eql? "47f4c736")
|
||||
Rails.logger.debug("Proxy exercise with token 47f4c736, split user in groups..")
|
||||
group = UserGroupSeparator.getGroupWeek2Testing(user)
|
||||
Rails.logger.debug("user assigned to group #{group}")
|
||||
case group
|
||||
when :group_a
|
||||
exercises.where(id: 348).first
|
||||
when :group_b
|
||||
exercises.where(id: 349).first
|
||||
when :group_c
|
||||
exercises.where(id: 350).first
|
||||
when :group_d
|
||||
exercises.where(id: 351).first
|
||||
end
|
||||
else
|
||||
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
||||
begin
|
||||
find_matching_exercise(user)
|
||||
rescue #fallback
|
||||
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
|
||||
@reason[:reason] = "fallback because of error"
|
||||
@reason[:error] = "#{$!}"
|
||||
exercises.shuffle.first
|
||||
end
|
||||
end
|
||||
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
||||
matching_exercise
|
||||
|
||||
|
||||
end
|
||||
recommended_exercise
|
||||
end
|
||||
|
||||
def find_matching_exercise(user)
|
||||
user_group = UserGroupSeparator.getProxyExerciseGroup(user)
|
||||
case user_group
|
||||
when :dummy_assigment
|
||||
rec_ex = select_easiest_exercise(exercises)
|
||||
@reason[:reason] = "dummy group"
|
||||
Rails.logger.debug("assigned user to dummy group, and gave him exercise: #{rec_ex.title}")
|
||||
rec_ex
|
||||
when :random_assigment
|
||||
@reason[:reason] = "random group"
|
||||
ex = exercises.where("expected_difficulty > 1").shuffle.first
|
||||
Rails.logger.debug("assigned user to random group, and gave him exercise: #{ex.title}")
|
||||
ex
|
||||
when :recommended_assignment
|
||||
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact
|
||||
tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
|
||||
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
|
||||
|
||||
# find exercises
|
||||
potential_recommended_exercises = []
|
||||
exercises.where("expected_difficulty > 1").each do |ex|
|
||||
## find exercises which have only tags the user has already seen
|
||||
if (ex.tags - tags_user_has_seen).empty?
|
||||
potential_recommended_exercises << ex
|
||||
end
|
||||
end
|
||||
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
|
||||
# if all exercises contain tags which the user has never seen, recommend easiest exercise
|
||||
if potential_recommended_exercises.empty?
|
||||
Rails.logger.debug("matched easiest exercise in pool")
|
||||
@reason[:reason] = "easiest exercise in pool. empty potential exercises"
|
||||
select_easiest_exercise(exercises)
|
||||
else
|
||||
recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
recommended_exercise
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
private :find_matching_exercise
|
||||
|
||||
def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed)
|
||||
Rails.logger.debug("topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}")
|
||||
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map{|p| p.id}}")
|
||||
topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge]
|
||||
topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge]
|
||||
current_users_knowledge_lack = {}
|
||||
topic_knowledge_max.keys.each do |tag|
|
||||
current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag]
|
||||
end
|
||||
|
||||
relative_knowledge_improvement = {}
|
||||
potential_recommended_exercises.each do |potex|
|
||||
tags = potex.tags
|
||||
relative_knowledge_improvement[potex] = 0.0
|
||||
Rails.logger.debug("review potential exercise #{potex.id}")
|
||||
tags.each do |tag|
|
||||
tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
|
||||
max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio
|
||||
old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag]
|
||||
new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio)
|
||||
Rails.logger.debug("tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}")
|
||||
relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag
|
||||
end
|
||||
end
|
||||
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0
|
||||
best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
|
||||
@reason[:reason] = "best matching exercise"
|
||||
@reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed
|
||||
@reason[:current_users_knowledge_lack] = current_users_knowledge_lack
|
||||
@reason[:relative_knowledge_improvement] = relative_knowledge_improvement
|
||||
|
||||
Rails.logger.debug("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s)
|
||||
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}")
|
||||
best_matching_exercise
|
||||
end
|
||||
private :select_best_matching_exercise
|
||||
|
||||
def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
|
||||
Rails.logger.debug("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}")
|
||||
sorted_exercises = relative_knowledge_improvement.sort_by{|k,v| v}.reverse
|
||||
|
||||
sorted_exercises.each do |ex,diff|
|
||||
Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}")
|
||||
if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1
|
||||
Rails.logger.debug("matched exercise #{ex.id}")
|
||||
return ex
|
||||
else
|
||||
Rails.logger.debug("exercise #{ex.id} is too difficult")
|
||||
end
|
||||
end
|
||||
easiest_exercise = sorted_exercises.min_by{|k,v| v}.first
|
||||
Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}")
|
||||
easiest_exercise
|
||||
end
|
||||
private :find_best_exercise
|
||||
|
||||
# [score][quantile]
|
||||
def scoring_matrix
|
||||
[
|
||||
[0 ,0 ,0 ,0 ,0 ],
|
||||
[0.2,0.2,0.2,0.2,0.1],
|
||||
[0.5,0.5,0.4,0.4,0.3],
|
||||
[0.6,0.6,0.5,0.5,0.4],
|
||||
[1 ,1 ,0.9,0.8,0.7],
|
||||
]
|
||||
end
|
||||
|
||||
def scoring_matrix_quantiles
|
||||
[0.2,0.4,0.6,0.8]
|
||||
end
|
||||
private :scoring_matrix_quantiles
|
||||
|
||||
def score(user, ex)
|
||||
max_score = ex.maximum_score.to_f
|
||||
if max_score <= 0
|
||||
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0" )
|
||||
return 0.0
|
||||
end
|
||||
points_ratio = ex.maximum_score(user) / max_score
|
||||
if points_ratio == 0.0
|
||||
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" )
|
||||
return 0.0
|
||||
end
|
||||
points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i
|
||||
working_time_user = ex.accumulated_working_time_for_only(user)
|
||||
quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles)
|
||||
quantile_index = quantiles_working_time.size
|
||||
quantiles_working_time.each_with_index do |quantile_time, i|
|
||||
if working_time_user <= quantile_time
|
||||
quantile_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
Rails.logger.debug(
|
||||
"scoring user #{user.id} exercise #{ex.id}: worktime #{working_time_user}, points: #{points_ratio}" \
|
||||
"(index #{points_ratio_index}) quantiles #{quantiles_working_time} placed into quantile index #{quantile_index} " \
|
||||
"score: #{scoring_matrix[points_ratio_index][quantile_index]}")
|
||||
scoring_matrix[points_ratio_index][quantile_index]
|
||||
end
|
||||
private :score
|
||||
|
||||
def get_user_knowledge_and_max_knowledge(user, exercises)
|
||||
# initialize knowledge for each tag with 0
|
||||
all_used_tags_with_count = {}
|
||||
exercises.each do |ex|
|
||||
ex.tags.each do |t|
|
||||
all_used_tags_with_count[t] ||= 0
|
||||
all_used_tags_with_count[t] += 1
|
||||
end
|
||||
end
|
||||
tags_counter = all_used_tags_with_count.keys.map{|tag| [tag,0]}.to_h
|
||||
topic_knowledge_loss_user = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h
|
||||
topic_knowledge_max = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h
|
||||
exercises_sorted = exercises.sort_by { |ex| ex.time_maximum_score(user)}
|
||||
exercises_sorted.each do |ex|
|
||||
Rails.logger.debug("exercise: #{ex.id}: #{ex}")
|
||||
user_score_factor = score(user, ex)
|
||||
ex.tags.each do |t|
|
||||
tags_counter[t] += 1
|
||||
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
|
||||
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
|
||||
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}")
|
||||
Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
|
||||
Rails.logger.debug("tag_ratio #{tag_ratio}")
|
||||
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
|
||||
Rails.logger.debug("topic_knowledge_ratio #{topic_knowledge_ratio}")
|
||||
topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio * tag_diminishing_return_factor
|
||||
topic_knowledge_max[t] += topic_knowledge_ratio * tag_diminishing_return_factor
|
||||
end
|
||||
end
|
||||
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
||||
end
|
||||
private :get_user_knowledge_and_max_knowledge
|
||||
|
||||
def tag_diminishing_return_function(count_tag, total_count_tag)
|
||||
total_count_tag += 1 # bonus exercise comes on top
|
||||
return 1/(1+(Math::E**(-3/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))
|
||||
end
|
||||
|
||||
def select_easiest_exercise(exercises)
|
||||
exercises.order(:expected_difficulty).first
|
||||
end
|
||||
|
||||
end
|
4
app/models/search.rb
Normal file
4
app/models/search.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
class Search < ActiveRecord::Base
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
end
|
22
app/models/tag.rb
Normal file
22
app/models/tag.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class Tag < ActiveRecord::Base
|
||||
|
||||
has_many :exercise_tags
|
||||
has_many :exercises, through: :exercise_tags
|
||||
|
||||
validates_uniqueness_of :name
|
||||
|
||||
def destroy
|
||||
if (can_be_destroyed?)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_destroyed?
|
||||
!exercises.any?
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
end
|
11
app/models/user_exercise_feedback.rb
Normal file
11
app/models/user_exercise_feedback.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class UserExerciseFeedback < ActiveRecord::Base
|
||||
include Creation
|
||||
|
||||
belongs_to :exercise
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }
|
||||
|
||||
def to_s
|
||||
"User Exercise Feedback"
|
||||
end
|
||||
end
|
11
app/models/user_exercise_intervention.rb
Normal file
11
app/models/user_exercise_intervention.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class UserExerciseIntervention < ActiveRecord::Base
|
||||
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :intervention
|
||||
belongs_to :exercise
|
||||
|
||||
validates :user, presence: true
|
||||
validates :exercise, presence: true
|
||||
validates :intervention, presence: true
|
||||
|
||||
end
|
14
app/models/user_proxy_exercise_exercise.rb
Normal file
14
app/models/user_proxy_exercise_exercise.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class UserProxyExerciseExercise < ActiveRecord::Base
|
||||
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
belongs_to :proxy_exercise
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :user_type, presence: true
|
||||
validates :exercise_id, presence: true
|
||||
validates :proxy_exercise_id, presence: true
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] }
|
||||
|
||||
end
|
Reference in New Issue
Block a user