Apply automatic rubocop fixes

This commit is contained in:
Sebastian Serth
2021-05-14 10:51:44 +02:00
parent fe4000916c
commit 6cbecb5b39
440 changed files with 2705 additions and 1853 deletions

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class AnomalyNotification < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Context
extend ActiveSupport::Concern

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Creation
extend ActiveSupport::Concern

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module DefaultValues
def set_default_values_if_present(options = {})
options.each do |attribute, value|

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class Consumer < ApplicationRecord
has_many :users

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class ErrorTemplate < ApplicationRecord
belongs_to :execution_environment
has_and_belongs_to_many :error_template_attributes

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class ErrorTemplateAttribute < ApplicationRecord
has_and_belongs_to_many :error_template

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class Event < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class ExerciseCollectionItem < ApplicationRecord
belongs_to :exercise_collection
belongs_to :exercise

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,10 +1,9 @@
# frozen_string_literal: true
class FileTemplate < ApplicationRecord
belongs_to :file_type
def to_s
name
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,6 @@
# frozen_string_literal: true
class Search < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class Subscription < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :request_for_comment

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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