Add submission deadline to exercises and allow teachers to view their submissions
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'nokogiri'
|
||||
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
|
||||
require File.expand_path('../../lib/active_model/validations/boolean_presence_validator', __dir__)
|
||||
|
||||
class Exercise < ApplicationRecord
|
||||
include Context
|
||||
@ -25,11 +27,12 @@ class Exercise < ApplicationRecord
|
||||
|
||||
has_many :external_users, source: :user, source_type: 'ExternalUser', through: :submissions
|
||||
has_many :internal_users, source: :user, source_type: 'InternalUser', through: :submissions
|
||||
alias_method :users, :external_users
|
||||
alias users external_users
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||
|
||||
validate :valid_main_file?
|
||||
validate :valid_submission_deadlines?
|
||||
validates :description, presence: true
|
||||
validates :execution_environment, presence: true, if: -> { !unpublished? }
|
||||
validates :public, boolean_presence: true
|
||||
@ -44,7 +47,7 @@ class Exercise < ApplicationRecord
|
||||
MAX_EXERCISE_FEEDBACKS = 20
|
||||
|
||||
def average_percentage
|
||||
if average_score and maximum_score != 0.0 and submissions.exists?(cause: 'submit')
|
||||
if average_score && (maximum_score != 0.0) && submissions.exists?(cause: 'submit')
|
||||
(average_score / maximum_score * 100).round(2)
|
||||
else
|
||||
0
|
||||
@ -68,11 +71,13 @@ class Exercise < ApplicationRecord
|
||||
|
||||
def average_number_of_submissions
|
||||
user_count = internal_users.distinct.count + external_users.distinct.count
|
||||
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
|
||||
user_count == 0 ? 0 : submissions.count / user_count.to_f
|
||||
end
|
||||
|
||||
def time_maximum_score(user)
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0)
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC, created_at ASC').first.created_at
|
||||
rescue StandardError
|
||||
Time.zone.at(0)
|
||||
end
|
||||
|
||||
def user_working_time_query
|
||||
@ -100,7 +105,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,
|
||||
@ -193,7 +198,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)
|
||||
@ -202,36 +207,34 @@ class Exercise < ApplicationRecord
|
||||
max_bucket = 100
|
||||
maximum_score = self.maximum_score
|
||||
|
||||
if user.blank?
|
||||
additional_filter = ''
|
||||
else
|
||||
additional_filter = "AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
|
||||
end
|
||||
additional_filter = if user.blank?
|
||||
''
|
||||
else
|
||||
"AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
|
||||
end
|
||||
|
||||
results = self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
|
||||
if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||
bucket = (tuple['score'] / maximum_score * max_bucket).round
|
||||
else
|
||||
bucket = max_bucket # maximum_score / maximum_score will always be 1
|
||||
end
|
||||
bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
|
||||
(tuple['score'] / maximum_score * max_bucket).round
|
||||
else
|
||||
max_bucket # maximum_score / maximum_score will always be 1
|
||||
end
|
||||
|
||||
user_progress[bucket] ||= []
|
||||
additional_user_data[bucket] ||= []
|
||||
additional_user_data[max_bucket + 1] ||= []
|
||||
|
||||
user_progress[bucket][tuple['index']] = tuple["working_time_per_score"]
|
||||
additional_user_data[bucket][tuple['index']] = {start_time: tuple["start_time"], score: tuple["score"]}
|
||||
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']}
|
||||
end
|
||||
|
||||
if results.ntuples > 0
|
||||
first_index = results[0]['index']
|
||||
last_index = results[results.ntuples-1]['index']
|
||||
last_index = results[results.ntuples - 1]['index']
|
||||
buckets = last_index - first_index
|
||||
user_progress.each do |timings_array|
|
||||
if timings_array.present? && timings_array.length != buckets + 1
|
||||
timings_array[buckets] = nil
|
||||
end
|
||||
timings_array[buckets] = nil if timings_array.present? && timings_array.length != buckets + 1
|
||||
end
|
||||
end
|
||||
|
||||
@ -239,8 +242,8 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||
result = self.class.connection.execute("""
|
||||
quantiles_str = '[' + quantiles.join(',') + ']'
|
||||
result = self.class.connection.execute(''"
|
||||
WITH working_time AS
|
||||
(
|
||||
SELECT user_id,
|
||||
@ -346,13 +349,12 @@ class Exercise < ApplicationRecord
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
""")
|
||||
"'')
|
||||
if result.count > 0
|
||||
quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight}
|
||||
quantiles.each_with_index.map { |_q, i| Time.parse(result[i]['unnest']).seconds_since_midnight }
|
||||
else
|
||||
quantiles.map{|q| 0}
|
||||
quantiles.map { |_q| 0 }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@ -363,72 +365,74 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def average_working_time
|
||||
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
|
||||
|
||||
def average_working_time_for(user_id)
|
||||
if @working_time_statistics == nil
|
||||
retrieve_working_time_statistics()
|
||||
end
|
||||
@working_time_statistics[user_id]["working_time"]
|
||||
retrieve_working_time_statistics if @working_time_statistics.nil?
|
||||
@working_time_statistics[user_id]['working_time']
|
||||
end
|
||||
|
||||
def accumulated_working_time_for_only(user)
|
||||
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
||||
Time.parse(self.class.connection.execute("""
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
|
||||
GROUP BY user_id, id, exercise_id),
|
||||
MAX_POINTS AS
|
||||
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
|
||||
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
|
||||
begin
|
||||
Time.parse(self.class.connection.execute(''"
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
|
||||
GROUP BY user_id, id, exercise_id),
|
||||
MAX_POINTS AS
|
||||
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
|
||||
|
||||
-- filter for rows containing max points
|
||||
TIME_MAX_SCORE AS
|
||||
(SELECT *
|
||||
FROM WORKING_TIME W1, MAX_POINTS MS
|
||||
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
|
||||
-- filter for rows containing max points
|
||||
TIME_MAX_SCORE AS
|
||||
(SELECT *
|
||||
FROM WORKING_TIME W1, MAX_POINTS MS
|
||||
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
|
||||
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
FROM (
|
||||
SELECT id,USER_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM TIME_MAX_SCORE) T
|
||||
WHERE rn = 1),
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
FROM (
|
||||
SELECT id,USER_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM TIME_MAX_SCORE) T
|
||||
WHERE rn = 1),
|
||||
|
||||
TIMES_UNTIL_MAX_POINTS AS (
|
||||
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
|
||||
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
|
||||
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
TIMES_UNTIL_MAX_POINTS AS (
|
||||
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
|
||||
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
|
||||
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
|
||||
-- if user never makes it to max points, take all times
|
||||
ALL_WORKING_TIMES_UNTIL_MAX AS
|
||||
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
|
||||
UNION ALL
|
||||
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
|
||||
-- if user never makes it to max points, take all times
|
||||
ALL_WORKING_TIMES_UNTIL_MAX AS
|
||||
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
|
||||
UNION ALL
|
||||
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
|
||||
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
|
||||
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
|
||||
""").first["working_time"]).seconds_since_midnight rescue 0
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
|
||||
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
|
||||
"'').first['working_time']).seconds_since_midnight
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate(attributes = {})
|
||||
@ -453,7 +457,8 @@ class Exercise < ApplicationRecord
|
||||
return 'main_file'
|
||||
elsif (file_class == 'internal') && (comment == 'main')
|
||||
end
|
||||
return 'regular_file'
|
||||
|
||||
'regular_file'
|
||||
end
|
||||
|
||||
def from_proforma_xml(xml_string)
|
||||
@ -467,23 +472,23 @@ class Exercise < ApplicationRecord
|
||||
description: description,
|
||||
instructions: description
|
||||
}
|
||||
task_node.xpath('p:files/p:file').all? { |file|
|
||||
task_node.xpath('p:files/p:file').all? do |file|
|
||||
file_name_split = file.xpath('@filename').first.value.split('.')
|
||||
file_class = file.xpath('@class').first.value
|
||||
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()")
|
||||
feedback_message_nodes = task_node.xpath('p:tests/p:test/p:test-configuration/c:feedback-message/text()')
|
||||
files.build({
|
||||
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_extension: ".#{file_name_split.second}"
|
||||
).take
|
||||
})
|
||||
}
|
||||
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_extension: ".#{file_name_split.second}"
|
||||
).take
|
||||
})
|
||||
end
|
||||
self.execution_environment_id = 1
|
||||
end
|
||||
|
||||
@ -495,7 +500,11 @@ class Exercise < ApplicationRecord
|
||||
def maximum_score(user = nil)
|
||||
if user
|
||||
# FIXME: where(user: user) will not work here!
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
|
||||
begin
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC').first.score || 0
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
else
|
||||
files.teacher_defined_tests.sum(:weight)
|
||||
end
|
||||
@ -510,7 +519,7 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def finishers
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w(submit assess)}).distinct
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w[submit assess]}).distinct
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
@ -523,12 +532,22 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def valid_main_file?
|
||||
if files.main_files.count > 1
|
||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
|
||||
end
|
||||
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file')) if files.main_files.count > 1
|
||||
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 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'))
|
||||
end
|
||||
end
|
||||
private :valid_submission_deadlines?
|
||||
|
||||
def needs_more_feedback?
|
||||
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
|
||||
end
|
||||
@ -543,5 +562,4 @@ class Exercise < ApplicationRecord
|
||||
WHERE exercise_id = #{id}
|
||||
) AS t ON t.fv = submissions.id").distinct
|
||||
end
|
||||
|
||||
end
|
||||
|
Reference in New Issue
Block a user