Apply automatic rubocop fixes
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AnomalyNotification < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
|
@@ -1,16 +1,15 @@
|
||||
require File.expand_path('../../../uploaders/file_uploader', __FILE__)
|
||||
require File.expand_path('../../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../../uploaders/file_uploader', __dir__)
|
||||
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __dir__)
|
||||
|
||||
module CodeOcean
|
||||
|
||||
class FileNameValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id,
|
||||
context_id: record.context_id, context_type: record.context_type).to_a
|
||||
unless existing_files.empty?
|
||||
if (not record.context.is_a?(Exercise)) || (record.context.new_record?)
|
||||
record.errors[:base] << 'Duplicate'
|
||||
end
|
||||
if !existing_files.empty? && (!record.context.is_a?(Exercise) || record.context.new_record?)
|
||||
record.errors[:base] << 'Duplicate'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,7 +18,8 @@ module CodeOcean
|
||||
include DefaultValues
|
||||
|
||||
DEFAULT_WEIGHT = 1.0
|
||||
ROLES = %w[regular_file main_file reference_implementation executable_file teacher_defined_test user_defined_file user_defined_test teacher_defined_linter].freeze
|
||||
ROLES = %w[regular_file main_file reference_implementation executable_file teacher_defined_test user_defined_file
|
||||
user_defined_test teacher_defined_linter].freeze
|
||||
TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
|
||||
|
||||
after_initialize :set_default_values
|
||||
@@ -29,13 +29,13 @@ module CodeOcean
|
||||
|
||||
belongs_to :context, polymorphic: true
|
||||
belongs_to :file, class_name: 'CodeOcean::File', optional: true # This is only required for submissions and is validated below
|
||||
alias_method :ancestor, :file
|
||||
alias ancestor file
|
||||
belongs_to :file_type
|
||||
|
||||
has_many :files, class_name: 'CodeOcean::File'
|
||||
has_many :testruns
|
||||
has_many :comments
|
||||
alias_method :descendants, :files
|
||||
alias descendants files
|
||||
|
||||
mount_uploader :native_file, FileUploader
|
||||
|
||||
@@ -61,7 +61,7 @@ module CodeOcean
|
||||
validates :weight, absence: true, unless: :teacher_defined_assessment?
|
||||
validates :file, presence: true if :context.is_a?(Submission)
|
||||
|
||||
validates_with FileNameValidator, fields: [:name, :path, :file_type_id]
|
||||
validates_with FileNameValidator, fields: %i[name path file_type_id]
|
||||
|
||||
ROLES.each do |role|
|
||||
define_method("#{role}?") { self.role == role }
|
||||
@@ -94,7 +94,12 @@ module CodeOcean
|
||||
end
|
||||
|
||||
def hash_content
|
||||
self.hashed_content = Digest::MD5.new.hexdigest(file_type.try(:binary?) ? ::File.new(native_file.file.path, 'r').read : content)
|
||||
self.hashed_content = Digest::MD5.new.hexdigest(if file_type.try(:binary?)
|
||||
::File.new(native_file.file.path,
|
||||
'r').read
|
||||
else
|
||||
content
|
||||
end)
|
||||
end
|
||||
private :hash_content
|
||||
|
||||
@@ -108,7 +113,7 @@ module CodeOcean
|
||||
end
|
||||
|
||||
def set_ancestor_values
|
||||
[:feedback_message, :file_type_id, :hidden, :name, :path, :read_only, :role, :weight].each do |attribute|
|
||||
%i[feedback_message file_type_id hidden name path read_only role weight].each do |attribute|
|
||||
send(:"#{attribute}=", ancestor.send(attribute))
|
||||
end
|
||||
end
|
||||
|
@@ -5,9 +5,7 @@ class CodeharborLink < ApplicationRecord
|
||||
validates :check_uuid_url, presence: true
|
||||
validates :api_key, presence: true
|
||||
|
||||
belongs_to :user, foreign_key: :user_id, class_name: 'InternalUser'
|
||||
belongs_to :user, class_name: 'InternalUser'
|
||||
|
||||
def to_s
|
||||
id.to_s
|
||||
end
|
||||
delegate :to_s, to: :id
|
||||
end
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Context
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Creation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DefaultValues
|
||||
def set_default_values_if_present(options = {})
|
||||
options.each do |attribute, value|
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Consumer < ApplicationRecord
|
||||
has_many :users
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ErrorTemplate < ApplicationRecord
|
||||
belongs_to :execution_environment
|
||||
has_and_belongs_to_many :error_template_attributes
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ErrorTemplateAttribute < ApplicationRecord
|
||||
has_and_belongs_to_many :error_template
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Event < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
|
@@ -1,4 +1,6 @@
|
||||
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../../lib/active_model/validations/boolean_presence_validator', __dir__)
|
||||
|
||||
class ExecutionEnvironment < ApplicationRecord
|
||||
include Creation
|
||||
@@ -17,7 +19,8 @@ class ExecutionEnvironment < ApplicationRecord
|
||||
validate :valid_test_setup?
|
||||
validate :working_docker_image?, if: :validate_docker_image?
|
||||
validates :docker_image, presence: true
|
||||
validates :memory_limit, numericality: {greater_than_or_equal_to: DockerClient::MINIMUM_MEMORY_LIMIT, only_integer: true}, presence: true
|
||||
validates :memory_limit,
|
||||
numericality: {greater_than_or_equal_to: DockerClient::MINIMUM_MEMORY_LIMIT, only_integer: true}, presence: true
|
||||
validates :network_enabled, boolean_presence: true
|
||||
validates :name, presence: true
|
||||
validates :permitted_execution_time, numericality: {only_integer: true}, presence: true
|
||||
@@ -35,7 +38,9 @@ class ExecutionEnvironment < ApplicationRecord
|
||||
|
||||
def valid_test_setup?
|
||||
if test_command? ^ testing_framework?
|
||||
errors.add(:test_command, I18n.t('activerecord.errors.messages.together', attribute: I18n.t('activerecord.attributes.execution_environment.testing_framework')))
|
||||
errors.add(:test_command,
|
||||
I18n.t('activerecord.errors.messages.together',
|
||||
attribute: I18n.t('activerecord.attributes.execution_environment.testing_framework')))
|
||||
end
|
||||
end
|
||||
private :valid_test_setup?
|
||||
@@ -46,11 +51,11 @@ class ExecutionEnvironment < ApplicationRecord
|
||||
private :validate_docker_image?
|
||||
|
||||
def working_docker_image?
|
||||
DockerClient.pull(docker_image) unless DockerClient.find_image_by_tag(docker_image).blank?
|
||||
DockerClient.pull(docker_image) if DockerClient.find_image_by_tag(docker_image).present?
|
||||
output = DockerClient.new(execution_environment: self).execute_arbitrary_command(VALIDATION_COMMAND)
|
||||
errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present?
|
||||
rescue DockerClient::Error => error
|
||||
errors.add(:docker_image, "error: #{error}")
|
||||
rescue DockerClient::Error => e
|
||||
errors.add(:docker_image, "error: #{e}")
|
||||
end
|
||||
private :working_docker_image?
|
||||
end
|
||||
|
@@ -41,7 +41,7 @@ class Exercise < ApplicationRecord
|
||||
validates :unpublished, boolean_presence: true
|
||||
validates :title, presence: true
|
||||
validates :token, presence: true, uniqueness: true
|
||||
validates_uniqueness_of :uuid, if: -> { uuid.present? }
|
||||
validates :uuid, uniqueness: {if: -> { uuid.present? }}
|
||||
|
||||
@working_time_statistics = nil
|
||||
attr_reader :working_time_statistics
|
||||
@@ -57,10 +57,10 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def finishers_percentage
|
||||
if users.distinct.count != 0
|
||||
(100.0 / users.distinct.count * finishers.count).round(2)
|
||||
else
|
||||
if users.distinct.count.zero?
|
||||
0
|
||||
else
|
||||
(100.0 / users.distinct.count * finishers.count).round(2)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,11 +73,11 @@ class Exercise < ApplicationRecord
|
||||
|
||||
def average_number_of_submissions
|
||||
user_count = internal_users.distinct.count + external_users.distinct.count
|
||||
user_count == 0 ? 0 : submissions.count / user_count.to_f
|
||||
user_count.zero? ? 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
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where.not(score: nil).order('score DESC, created_at ASC').first.created_at
|
||||
rescue StandardError
|
||||
Time.zone.at(0)
|
||||
end
|
||||
@@ -107,7 +107,7 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
|
||||
''"
|
||||
"
|
||||
WITH working_time_between_submissions AS (
|
||||
SELECT submissions.user_id,
|
||||
submissions.user_type,
|
||||
@@ -200,7 +200,7 @@ class Exercise < ApplicationRecord
|
||||
FROM working_times_with_index
|
||||
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
|
||||
ORDER BY index, score ASC;
|
||||
"''
|
||||
"
|
||||
end
|
||||
|
||||
def get_working_times_for_study_group(study_group_id, user = nil)
|
||||
@@ -217,7 +217,8 @@ class Exercise < ApplicationRecord
|
||||
|
||||
results = ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'")
|
||||
self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
|
||||
self.class.connection.execute(study_group_working_time_query(id, study_group_id,
|
||||
additional_filter)).each do |tuple|
|
||||
bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||
(tuple['score'] / maximum_score * max_bucket).round
|
||||
else
|
||||
@@ -230,11 +231,12 @@ class Exercise < ApplicationRecord
|
||||
|
||||
user_progress[bucket][tuple['index']] = tuple['working_time_per_score']
|
||||
additional_user_data[bucket][tuple['index']] = {start_time: tuple['start_time'], score: tuple['score']}
|
||||
additional_user_data[max_bucket + 1][tuple['index']] = {id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
|
||||
additional_user_data[max_bucket + 1][tuple['index']] =
|
||||
{id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
|
||||
end
|
||||
end
|
||||
|
||||
if results.ntuples > 0
|
||||
if results.ntuples.positive?
|
||||
first_index = results[0]['index']
|
||||
last_index = results[results.ntuples - 1]['index']
|
||||
buckets = last_index - first_index
|
||||
@@ -247,9 +249,9 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = '[' + quantiles.join(',') + ']'
|
||||
quantiles_str = "[#{quantiles.join(',')}]"
|
||||
result = ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute(''"
|
||||
self.class.connection.execute("
|
||||
SET LOCAL intervalstyle = 'iso_8601';
|
||||
WITH working_time AS
|
||||
(
|
||||
@@ -356,14 +358,14 @@ class Exercise < ApplicationRecord
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
"'')
|
||||
")
|
||||
end
|
||||
if result.count > 0
|
||||
begin
|
||||
quantiles.each_with_index.map { |_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
|
||||
end
|
||||
if result.count.positive?
|
||||
|
||||
quantiles.each_with_index.map {|_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
|
||||
|
||||
else
|
||||
quantiles.map { |_q| 0 }
|
||||
quantiles.map {|_q| 0 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -380,11 +382,11 @@ class Exercise < ApplicationRecord
|
||||
def average_working_time
|
||||
ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'")
|
||||
self.class.connection.execute(''"
|
||||
self.class.connection.execute("
|
||||
SELECT avg(working_time) as average_time
|
||||
FROM
|
||||
(#{user_working_time_query}) AS baz;
|
||||
"'').first['average_time']
|
||||
").first['average_time']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -397,7 +399,7 @@ class Exercise < ApplicationRecord
|
||||
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
|
||||
begin
|
||||
result = ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute(''"
|
||||
self.class.connection.execute("
|
||||
SET LOCAL intervalstyle = 'iso_8601';
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
@@ -447,7 +449,7 @@ class Exercise < ApplicationRecord
|
||||
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
|
||||
"'')
|
||||
")
|
||||
end
|
||||
ActiveSupport::Duration.parse(result.first['working_time']).to_f
|
||||
rescue StandardError
|
||||
@@ -458,8 +460,8 @@ class Exercise < ApplicationRecord
|
||||
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_tags.each {|et| exercise.exercise_tags << et.dup }
|
||||
files.each {|file| exercise.files << file.dup }
|
||||
exercise
|
||||
end
|
||||
|
||||
@@ -490,7 +492,7 @@ class Exercise < ApplicationRecord
|
||||
self.attributes = {
|
||||
title: task_node.xpath('p:meta-data/p:title/text()')[0].content,
|
||||
description: description,
|
||||
instructions: description
|
||||
instructions: description,
|
||||
}
|
||||
task_node.xpath('p:files/p:file').all? do |file|
|
||||
file_name_split = file.xpath('@filename').first.value.split('.')
|
||||
@@ -498,16 +500,16 @@ class Exercise < ApplicationRecord
|
||||
role = determine_file_role_from_proforma_file(task_node, file)
|
||||
feedback_message_nodes = task_node.xpath('p:tests/p:test/p:test-configuration/c:feedback-message/text()')
|
||||
files.build({
|
||||
name: file_name_split.first,
|
||||
name: file_name_split.first,
|
||||
content: file.xpath('text()').first.content,
|
||||
read_only: false,
|
||||
hidden: file_class == 'internal',
|
||||
role: role,
|
||||
feedback_message: role == 'teacher_defined_test' ? feedback_message_nodes.first.content : nil,
|
||||
file_type: FileType.where(
|
||||
file_type: FileType.find_by(
|
||||
file_extension: ".#{file_name_split.second}"
|
||||
).take
|
||||
})
|
||||
),
|
||||
})
|
||||
end
|
||||
self.execution_environment_id = 1
|
||||
end
|
||||
@@ -521,7 +523,7 @@ class Exercise < ApplicationRecord
|
||||
if user
|
||||
# FIXME: where(user: user) will not work here!
|
||||
begin
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC').first.score || 0
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where.not(score: nil).order('score DESC').first.score || 0
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
@@ -539,7 +541,8 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def finishers
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score,
|
||||
cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
@@ -552,18 +555,25 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def valid_main_file?
|
||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file')) if files.main_files.count > 1
|
||||
if files.main_files.count > 1
|
||||
errors.add(:files,
|
||||
I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
|
||||
end
|
||||
end
|
||||
private :valid_main_file?
|
||||
|
||||
def valid_submission_deadlines?
|
||||
return unless submission_deadline.present? || late_submission_deadline.present?
|
||||
|
||||
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_alone')) if late_submission_deadline.present? && submission_deadline.blank?
|
||||
if late_submission_deadline.present? && submission_deadline.blank?
|
||||
errors.add(:late_submission_deadline,
|
||||
I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_alone'))
|
||||
end
|
||||
|
||||
if submission_deadline.present? && late_submission_deadline.present? &&
|
||||
late_submission_deadline < submission_deadline
|
||||
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_before_submission_deadline'))
|
||||
errors.add(:late_submission_deadline,
|
||||
I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_before_submission_deadline'))
|
||||
end
|
||||
end
|
||||
private :valid_submission_deadlines?
|
||||
|
@@ -1,15 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ExerciseCollection < ApplicationRecord
|
||||
include TimeHelper
|
||||
|
||||
has_many :exercise_collection_items, dependent: :delete_all
|
||||
alias_method :items, :exercise_collection_items
|
||||
alias items exercise_collection_items
|
||||
has_many :exercises, through: :exercise_collection_items, inverse_of: :exercise_collections
|
||||
belongs_to :user, polymorphic: true
|
||||
|
||||
def collection_statistics
|
||||
statistics = {}
|
||||
exercise_collection_items.each do |item|
|
||||
statistics[item.position] = {exercise_id: item.exercise.id, exercise_title: item.exercise.title, working_time: time_to_f(item.exercise.average_working_time)}
|
||||
statistics[item.position] =
|
||||
{exercise_id: item.exercise.id, exercise_title: item.exercise.title,
|
||||
working_time: time_to_f(item.exercise.average_working_time)}
|
||||
end
|
||||
statistics
|
||||
end
|
||||
@@ -18,8 +22,8 @@ class ExerciseCollection < ApplicationRecord
|
||||
if exercises.empty?
|
||||
0
|
||||
else
|
||||
values = collection_statistics.values.reject { |o| o[:working_time].nil?}
|
||||
sum = values.reduce(0) {|sum, item| sum + item[:working_time]}
|
||||
values = collection_statistics.values.reject {|o| o[:working_time].nil? }
|
||||
sum = values.reduce(0) {|sum, item| sum + item[:working_time] }
|
||||
sum / values.size
|
||||
end
|
||||
end
|
||||
@@ -27,5 +31,4 @@ class ExerciseCollection < ApplicationRecord
|
||||
def to_s
|
||||
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ExerciseCollectionItem < ApplicationRecord
|
||||
belongs_to :exercise_collection
|
||||
belongs_to :exercise
|
||||
|
@@ -1,13 +1,14 @@
|
||||
class ExerciseTag < ApplicationRecord
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ExerciseTag < ApplicationRecord
|
||||
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
|
||||
def destroy_if_empty_exercise_or_tag
|
||||
destroy if exercise_id.blank? || tag_id.blank?
|
||||
end
|
||||
end
|
||||
|
@@ -13,6 +13,12 @@ class ExerciseTip < ApplicationRecord
|
||||
|
||||
def tip_chain?
|
||||
# Ensure each referenced parent exercise tip is set for this exercise
|
||||
errors.add :parent_exercise_tip, I18n.t('activerecord.errors.messages.together', attribute: I18n.t('activerecord.attributes.exercise_tip.tip')) unless ExerciseTip.exists?(exercise: exercise, id: parent_exercise_tip)
|
||||
unless ExerciseTip.exists?(
|
||||
exercise: exercise, id: parent_exercise_tip
|
||||
)
|
||||
errors.add :parent_exercise_tip,
|
||||
I18n.t('activerecord.errors.messages.together',
|
||||
attribute: I18n.t('activerecord.attributes.exercise_tip.tip'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@@ -1,13 +1,10 @@
|
||||
class ExternalUser < User
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ExternalUser < User
|
||||
validates :consumer_id, presence: true
|
||||
validates :external_id, presence: true
|
||||
|
||||
def displayname
|
||||
if name.blank?
|
||||
"User " + id.to_s
|
||||
else
|
||||
name
|
||||
end
|
||||
name.presence || "User #{id}"
|
||||
end
|
||||
end
|
||||
|
@@ -1,10 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FileTemplate < ApplicationRecord
|
||||
|
||||
belongs_to :file_type
|
||||
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -1,12 +1,14 @@
|
||||
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../../lib/active_model/validations/boolean_presence_validator', __dir__)
|
||||
|
||||
class FileType < ApplicationRecord
|
||||
include Creation
|
||||
include DefaultValues
|
||||
|
||||
AUDIO_FILE_EXTENSIONS = %w(.aac .flac .m4a .mp3 .ogg .wav .wma)
|
||||
IMAGE_FILE_EXTENSIONS = %w(.bmp .gif .jpeg .jpg .png)
|
||||
VIDEO_FILE_EXTENSIONS = %w(.avi .flv .mkv .mp4 .m4v .ogv .webm)
|
||||
AUDIO_FILE_EXTENSIONS = %w[.aac .flac .m4a .mp3 .ogg .wav .wma].freeze
|
||||
IMAGE_FILE_EXTENSIONS = %w[.bmp .gif .jpeg .jpg .png].freeze
|
||||
VIDEO_FILE_EXTENSIONS = %w[.avi .flv .mkv .mp4 .m4v .ogv .webm].freeze
|
||||
|
||||
after_initialize :set_default_values
|
||||
|
||||
@@ -21,7 +23,7 @@ class FileType < ApplicationRecord
|
||||
validates :name, presence: true
|
||||
validates :renderable, boolean_presence: true
|
||||
|
||||
[:audio, :image, :video].each do |type|
|
||||
%i[audio image video].each do |type|
|
||||
define_method("#{type}?") do
|
||||
self.class.const_get("#{type.upcase}_FILE_EXTENSIONS").include?(file_extension)
|
||||
end
|
||||
|
@@ -1,5 +1,6 @@
|
||||
class InternalUser < User
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InternalUser < User
|
||||
authenticates_with_sorcery!
|
||||
|
||||
validates :email, presence: true, uniqueness: true
|
||||
@@ -22,5 +23,4 @@ class InternalUser < User
|
||||
def displayname
|
||||
name
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -1,5 +1,6 @@
|
||||
class Intervention < ApplicationRecord
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Intervention < ApplicationRecord
|
||||
has_many :user_exercise_interventions
|
||||
has_many :users, through: :user_exercise_interventions, source_type: 'ExternalUser'
|
||||
|
||||
@@ -8,9 +9,8 @@ class Intervention < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.createDefaultInterventions
|
||||
%w(BreakIntervention QuestionIntervention).each do |name|
|
||||
%w[BreakIntervention QuestionIntervention].each do |name|
|
||||
Intervention.find_or_create_by(name: name)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
@@ -1,9 +1,11 @@
|
||||
class LtiParameter < ApplicationRecord
|
||||
belongs_to :consumer, foreign_key: "consumers_id"
|
||||
belongs_to :exercise, foreign_key: "exercises_id"
|
||||
belongs_to :external_user, foreign_key: "external_users_id"
|
||||
# frozen_string_literal: true
|
||||
|
||||
scope :lis_outcome_service_url?, -> {
|
||||
class LtiParameter < ApplicationRecord
|
||||
belongs_to :consumer, foreign_key: 'consumers_id'
|
||||
belongs_to :exercise, foreign_key: 'exercises_id'
|
||||
belongs_to :external_user, foreign_key: 'external_users_id'
|
||||
|
||||
scope :lis_outcome_service_url?, lambda {
|
||||
where("lti_parameters.lti_parameters ? 'lis_outcome_service_url'")
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@@ -1,236 +1,245 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProxyExercise < ApplicationRecord
|
||||
include Creation
|
||||
include DefaultValues
|
||||
include Creation
|
||||
include DefaultValues
|
||||
|
||||
after_initialize :generate_token
|
||||
after_initialize :set_reason
|
||||
after_initialize :set_default_values
|
||||
after_initialize :generate_token
|
||||
after_initialize :set_reason
|
||||
after_initialize :set_default_values
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
has_and_belongs_to_many :exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
|
||||
validates :public, boolean_presence: true
|
||||
validates :public, boolean_presence: true
|
||||
|
||||
def count_files
|
||||
exercises.count
|
||||
end
|
||||
def count_files
|
||||
exercises.count
|
||||
end
|
||||
|
||||
def set_reason
|
||||
@reason = {}
|
||||
end
|
||||
def set_reason
|
||||
@reason = {}
|
||||
end
|
||||
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.hex(4)
|
||||
end
|
||||
private :generate_token
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.hex(4)
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def set_default_values
|
||||
set_default_values_if_present(public: false)
|
||||
end
|
||||
private :set_default_values
|
||||
def set_default_values
|
||||
set_default_values_if_present(public: false)
|
||||
end
|
||||
private :set_default_values
|
||||
|
||||
def duplicate(attributes = {})
|
||||
proxy_exercise = dup
|
||||
proxy_exercise.attributes = attributes
|
||||
proxy_exercise
|
||||
end
|
||||
def duplicate(attributes = {})
|
||||
proxy_exercise = dup
|
||||
proxy_exercise.attributes = attributes
|
||||
proxy_exercise
|
||||
end
|
||||
|
||||
def to_s
|
||||
title
|
||||
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
|
||||
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
||||
matching_exercise =
|
||||
begin
|
||||
find_matching_exercise(user)
|
||||
rescue => e #fallback
|
||||
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
|
||||
@reason[:reason] = "fallback because of error"
|
||||
@reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
|
||||
exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
|
||||
end
|
||||
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
||||
matching_exercise
|
||||
def get_matching_exercise(user)
|
||||
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
|
||||
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
|
||||
Rails.logger.debug("find new matching exercise for user #{user.id}")
|
||||
matching_exercise =
|
||||
begin
|
||||
find_matching_exercise(user)
|
||||
rescue StandardError => e # fallback
|
||||
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$ERROR_INFO}")
|
||||
@reason[:reason] = 'fallback because of error'
|
||||
@reason[:error] = "#{$ERROR_INFO}:\n\t#{e.backtrace.join("\n\t")}"
|
||||
exercises.where('expected_difficulty > 1').sample # difficulty should be > 1 to prevent dummy exercise from being chosen.
|
||||
end
|
||||
recommended_exercise
|
||||
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user,
|
||||
exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
||||
matching_exercise
|
||||
end
|
||||
end
|
||||
|
||||
def find_matching_exercise(user)
|
||||
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(",")}")
|
||||
def find_matching_exercise(user)
|
||||
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map(&:exercise).uniq.compact
|
||||
tags_user_has_seen = exercises_user_has_accessed.map(&:tags).uniq.flatten
|
||||
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map(&: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
|
||||
select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
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]
|
||||
# find exercises
|
||||
potential_recommended_exercises = []
|
||||
exercises.where('expected_difficulty >= 1').find_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(&: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
|
||||
select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
end
|
||||
end
|
||||
private :find_matching_exercise
|
||||
|
||||
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
|
||||
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(&: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.each_key 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) do |sum, et|
|
||||
sum += et.factor
|
||||
end
|
||||
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
|
||||
|
||||
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
|
||||
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map(&:expected_difficulty).max || 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
|
||||
|
||||
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
|
||||
Rails.logger.debug('current users knowledge loss: ' + current_users_knowledge_lack.map do |k, v|
|
||||
"#{k} => #{v}"
|
||||
end.to_s)
|
||||
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map {|k, v| "#{k.id}:#{v}" }}")
|
||||
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
|
||||
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
|
||||
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],
|
||||
]
|
||||
# [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
|
||||
|
||||
def scoring_matrix_quantiles
|
||||
[0.2,0.4,0.6,0.8]
|
||||
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
|
||||
elsif points_ratio > 1.0
|
||||
points_ratio = 1.0 # The score of the exercise was adjusted and is now lower than it was
|
||||
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
|
||||
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
|
||||
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
|
||||
elsif points_ratio > 1.0
|
||||
points_ratio = 1.0 # The score of the exercise was adjusted and is now lower than it was
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
tags_counter = all_used_tags_with_count.keys.index_with {|_tag| 0 }
|
||||
topic_knowledge_loss_user = all_used_tags_with_count.keys.index_with {|_t| 0 }
|
||||
topic_knowledge_max = all_used_tags_with_count.keys.index_with {|_t| 0 }
|
||||
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) do |sum, et|
|
||||
sum + et.factor
|
||||
end
|
||||
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0) do |sum, et|
|
||||
sum + et.factor
|
||||
end }")
|
||||
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
|
||||
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
||||
end
|
||||
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
||||
end
|
||||
|
||||
def tag_diminishing_return_function(count_tag, total_count_tag)
|
||||
total_count_tag += 1 # bonus exercise comes on top
|
||||
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
|
||||
def tag_diminishing_return_function(count_tag, total_count_tag)
|
||||
total_count_tag += 1 # bonus exercise comes on top
|
||||
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
|
||||
|
@@ -18,7 +18,7 @@ class RequestForComment < ApplicationRecord
|
||||
|
||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||
scope :in_range, ->(from, to) { where(created_at: from..to) }
|
||||
scope :with_comments, -> { select { |rfc| rfc.comments.any? } }
|
||||
scope :with_comments, -> { select {|rfc| rfc.comments.any? } }
|
||||
|
||||
# after_save :trigger_rfc_action_cable
|
||||
|
||||
@@ -44,7 +44,7 @@ class RequestForComment < ApplicationRecord
|
||||
end
|
||||
|
||||
def comments_count
|
||||
submission.files.map { |file| file.comments.size }.sum
|
||||
submission.files.sum {|file| file.comments.size }
|
||||
end
|
||||
|
||||
def commenters
|
||||
|
@@ -1,4 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Search < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
end
|
||||
end
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StructuredError < ApplicationRecord
|
||||
belongs_to :error_template
|
||||
belongs_to :submission
|
||||
@@ -5,8 +7,8 @@ class StructuredError < ApplicationRecord
|
||||
has_many :structured_error_attributes
|
||||
|
||||
def self.create_from_template(template, message_buffer, submission)
|
||||
instance = self.create(error_template: template, submission: submission)
|
||||
template.error_template_attributes.each do | attribute |
|
||||
instance = create(error_template: template, submission: submission)
|
||||
template.error_template_attributes.each do |attribute|
|
||||
StructuredErrorAttribute.create_from_template(attribute, instance, message_buffer)
|
||||
end
|
||||
instance
|
||||
@@ -14,7 +16,7 @@ class StructuredError < ApplicationRecord
|
||||
|
||||
def hint
|
||||
content = error_template.hint
|
||||
structured_error_attributes.each do | attribute |
|
||||
structured_error_attributes.each do |attribute|
|
||||
content.sub! "{{#{attribute.error_template_attribute.key}}}", attribute.value if attribute.match
|
||||
end
|
||||
content
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StructuredErrorAttribute < ApplicationRecord
|
||||
belongs_to :structured_error
|
||||
belongs_to :error_template_attribute
|
||||
@@ -5,11 +7,10 @@ class StructuredErrorAttribute < ApplicationRecord
|
||||
def self.create_from_template(attribute, structured_error, message_buffer)
|
||||
value = nil
|
||||
result = message_buffer.match(attribute.regex)
|
||||
if result != nil
|
||||
if result.captures.size > 0
|
||||
value = result.captures[0]
|
||||
end
|
||||
if !result.nil? && result.captures.size.positive?
|
||||
value = result.captures[0]
|
||||
end
|
||||
self.create(structured_error: structured_error, error_template_attribute: attribute, value: value, match: result != nil)
|
||||
create(structured_error: structured_error, error_template_attribute: attribute, value: value,
|
||||
match: !result.nil?)
|
||||
end
|
||||
end
|
||||
|
@@ -4,5 +4,5 @@ class StudyGroupMembership < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :study_group
|
||||
|
||||
validates_uniqueness_of :user_id, :scope => [:user_type, :study_group_id]
|
||||
validates :user_id, uniqueness: {scope: %i[user_type study_group_id]}
|
||||
end
|
||||
|
@@ -1,9 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Submission < ApplicationRecord
|
||||
include Context
|
||||
include Creation
|
||||
include ActionCableHelper
|
||||
|
||||
CAUSES = %w(assess download file render run save submit test autosave requestComments remoteAssess remoteSubmit)
|
||||
CAUSES = %w[assess download file render run save submit test autosave requestComments remoteAssess
|
||||
remoteSubmit].freeze
|
||||
FILENAME_URL_PLACEHOLDER = '{filename}'
|
||||
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
|
||||
OLDEST_RFC_TO_SHOW = 6.months
|
||||
@@ -15,17 +18,27 @@ class Submission < ApplicationRecord
|
||||
has_many :structured_errors
|
||||
has_many :comments, through: :files
|
||||
|
||||
belongs_to :external_users, -> { where(submissions: {user_type: 'ExternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
|
||||
belongs_to :internal_users, -> { where(submissions: {user_type: 'InternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'InternalUser', optional: true
|
||||
belongs_to :external_users, lambda {
|
||||
where(submissions: {user_type: 'ExternalUser'}).includes(:submissions)
|
||||
}, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
|
||||
belongs_to :internal_users, lambda {
|
||||
where(submissions: {user_type: 'InternalUser'}).includes(:submissions)
|
||||
}, foreign_key: :user_id, class_name: 'InternalUser', optional: true
|
||||
|
||||
delegate :execution_environment, to: :exercise
|
||||
|
||||
scope :final, -> { where(cause: %w[submit remoteSubmit]) }
|
||||
scope :intermediate, -> { where.not(cause: 'submit') }
|
||||
|
||||
scope :before_deadline, -> { joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL') }
|
||||
scope :within_grace_period, -> { joins(:exercise).where('(submissions.updated_at > exercises.submission_deadline) AND (submissions.updated_at <= exercises.late_submission_deadline OR exercises.late_submission_deadline IS NULL)') }
|
||||
scope :after_late_deadline, -> { joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline') }
|
||||
scope :before_deadline, lambda {
|
||||
joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL')
|
||||
}
|
||||
scope :within_grace_period, lambda {
|
||||
joins(:exercise).where('(submissions.updated_at > exercises.submission_deadline) AND (submissions.updated_at <= exercises.late_submission_deadline OR exercises.late_submission_deadline IS NULL)')
|
||||
}
|
||||
scope :after_late_deadline, lambda {
|
||||
joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline')
|
||||
}
|
||||
|
||||
scope :latest, -> { order(updated_at: :desc).first }
|
||||
|
||||
@@ -36,7 +49,6 @@ class Submission < ApplicationRecord
|
||||
|
||||
# after_save :trigger_working_times_action_cable
|
||||
|
||||
|
||||
def build_files_hash(files, attribute)
|
||||
files.map(&attribute.to_proc).zip(files).to_h
|
||||
end
|
||||
@@ -57,12 +69,12 @@ class Submission < ApplicationRecord
|
||||
# expects the full file path incl. file extension
|
||||
# Caution: There must be no unnecessary path prefix included.
|
||||
# Use `file.ext` rather than `./file.ext`
|
||||
collect_files.detect { |file| file.filepath == file_path }
|
||||
collect_files.detect {|file| file.filepath == file_path }
|
||||
end
|
||||
|
||||
def normalized_score
|
||||
::NewRelic::Agent.add_custom_attributes({unnormalized_score: score})
|
||||
if !score.nil? && !exercise.maximum_score.nil? && (exercise.maximum_score > 0)
|
||||
if !score.nil? && !exercise.maximum_score.nil? && exercise.maximum_score.positive?
|
||||
score / exercise.maximum_score
|
||||
else
|
||||
0
|
||||
@@ -119,6 +131,8 @@ class Submission < ApplicationRecord
|
||||
end
|
||||
|
||||
def unsolved_rfc
|
||||
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order("RANDOM()").find { |rfc_element| ((rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
|
||||
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order('RANDOM()').find do |rfc_element|
|
||||
((rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && !rfc_element.question.empty?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Subscription < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :request_for_comment
|
||||
|
@@ -1,22 +1,22 @@
|
||||
class Tag < ApplicationRecord
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
has_many :exercise_tags
|
||||
has_many :exercises, through: :exercise_tags
|
||||
|
||||
validates_uniqueness_of :name
|
||||
validates :name, uniqueness: true
|
||||
|
||||
def destroy
|
||||
if (can_be_destroyed?)
|
||||
if can_be_destroyed?
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_destroyed?
|
||||
!exercises.any?
|
||||
exercises.none?
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
@@ -1,4 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Testrun < ApplicationRecord
|
||||
belongs_to :file, class_name: 'CodeOcean::File', optional: true
|
||||
belongs_to :submission
|
||||
belongs_to :file, class_name: 'CodeOcean::File', optional: true
|
||||
belongs_to :submission
|
||||
end
|
||||
|
@@ -6,11 +6,16 @@ class Tip < ApplicationRecord
|
||||
has_many :exercise_tips
|
||||
has_many :exercises, through: :exercise_tips
|
||||
belongs_to :file_type, optional: true
|
||||
validates_presence_of :file_type, if: :example?
|
||||
validates :file_type, presence: {if: :example?}
|
||||
validate :content?
|
||||
|
||||
def content?
|
||||
errors.add :description, I18n.t('activerecord.errors.messages.at_least', attribute: I18n.t('activerecord.attributes.tip.example')) unless [description?, example?].include?(true)
|
||||
unless [
|
||||
description?, example?
|
||||
].include?(true)
|
||||
errors.add :description,
|
||||
I18n.t('activerecord.errors.messages.at_least', attribute: I18n.t('activerecord.attributes.tip.example'))
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
|
@@ -3,7 +3,7 @@
|
||||
class User < ApplicationRecord
|
||||
self.abstract_class = true
|
||||
|
||||
ROLES = %w(admin teacher learner)
|
||||
ROLES = %w[admin teacher learner].freeze
|
||||
|
||||
belongs_to :consumer
|
||||
has_many :study_group_memberships, as: :user
|
||||
@@ -19,10 +19,11 @@ class User < ApplicationRecord
|
||||
has_one :codeharbor_link, dependent: :destroy
|
||||
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
||||
|
||||
scope :in_study_group_of, ->(user) { joins(:study_group_memberships).where(study_group_memberships: {study_group_id: user.study_groups}) unless user.admin? }
|
||||
scope :in_study_group_of, lambda {|user|
|
||||
joins(:study_group_memberships).where(study_group_memberships: {study_group_id: user.study_groups}) unless user.admin?
|
||||
}
|
||||
|
||||
ROLES.each do |role|
|
||||
define_method("#{role}?") { try(:role) == role }
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserExerciseFeedback < ApplicationRecord
|
||||
include Creation
|
||||
|
||||
@@ -5,17 +7,17 @@ class UserExerciseFeedback < ApplicationRecord
|
||||
belongs_to :submission, optional: true
|
||||
has_one :execution_environment, through: :exercise
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }
|
||||
validates :user_id, uniqueness: {scope: %i[exercise_id user_type]}
|
||||
|
||||
scope :intermediate, -> { where.not(normalized_score: 1.00) }
|
||||
scope :final, -> { where(normalized_score: 1.00) }
|
||||
|
||||
def to_s
|
||||
"User Exercise Feedback"
|
||||
'User Exercise Feedback'
|
||||
end
|
||||
|
||||
def anomaly_notification
|
||||
AnomalyNotification.where({exercise_id: exercise.id, user_id: user_id, user_type: user_type})
|
||||
.where("created_at < ?", created_at).order("created_at DESC").to_a.first
|
||||
.where('created_at < ?', created_at).order('created_at DESC').to_a.first
|
||||
end
|
||||
end
|
||||
|
@@ -1,5 +1,6 @@
|
||||
class UserExerciseIntervention < ApplicationRecord
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserExerciseIntervention < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :intervention
|
||||
belongs_to :exercise
|
||||
@@ -7,5 +8,4 @@ class UserExerciseIntervention < ApplicationRecord
|
||||
validates :user, presence: true
|
||||
validates :exercise, presence: true
|
||||
validates :intervention, presence: true
|
||||
|
||||
end
|
||||
end
|
||||
|
@@ -1,5 +1,6 @@
|
||||
class UserProxyExerciseExercise < ApplicationRecord
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserProxyExerciseExercise < ApplicationRecord
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
belongs_to :proxy_exercise
|
||||
@@ -9,6 +10,5 @@ class UserProxyExerciseExercise < ApplicationRecord
|
||||
validates :exercise_id, presence: true
|
||||
validates :proxy_exercise_id, presence: true
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] }
|
||||
|
||||
end
|
||||
validates :user_id, uniqueness: {scope: %i[proxy_exercise_id user_type]}
|
||||
end
|
||||
|
Reference in New Issue
Block a user