Rename user to contributor in submission
This commit is contained in:

committed by
Sebastian Serth

parent
97138288f4
commit
0234414bae
@ -919,10 +919,6 @@ var CodeOceanEditor = {
|
||||
const delta = 100; // time in ms to wait for window event before time gets stopped
|
||||
let tid;
|
||||
$.ajax({
|
||||
data: {
|
||||
exercise_id: editor.data('exercise-id'),
|
||||
user_id: editor.data('user-id')
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
// get working times for this exercise
|
||||
|
@ -17,7 +17,7 @@ module FileParameters
|
||||
# avoid that public files from other contexts can be created
|
||||
# `next` is similar to an early return and will proceed with the next iteration of the loop
|
||||
next true if file.context_type == 'Exercise' && file.context_id != exercise.id
|
||||
next true if file.context_type == 'Submission' && (file.context.user_id != current_user.id || file.context.user_type != current_user.class.name)
|
||||
next true if file.context_type == 'Submission' && (file.context.contributor_id != current_user.id || file.context.contributor_type != current_user.class.name)
|
||||
next true if file.context_type == 'CommunitySolution' && controller_name != 'community_solutions'
|
||||
|
||||
# Optimization: We already queried the ancestor file, let's reuse the object.
|
||||
|
@ -141,12 +141,12 @@ module Lti
|
||||
raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!")
|
||||
end
|
||||
|
||||
if submission.user.consumer
|
||||
lti_parameter = LtiParameter.where(consumers_id: submission.user.consumer.id,
|
||||
external_users_id: submission.user_id,
|
||||
if submission.contributor.consumer
|
||||
lti_parameter = LtiParameter.where(consumers_id: submission.contributor.consumer.id,
|
||||
external_users_id: submission.contributor_id,
|
||||
exercises_id: submission.exercise_id).last
|
||||
|
||||
provider = build_tool_provider(consumer: submission.user.consumer, parameters: lti_parameter.lti_parameters)
|
||||
provider = build_tool_provider(consumer: submission.contributor.consumer, parameters: lti_parameter.lti_parameters)
|
||||
end
|
||||
|
||||
if provider.nil?
|
||||
|
@ -16,7 +16,7 @@ module RedirectBehavior
|
||||
# redirect 10 percent pseudorandomly to the feedback page
|
||||
if current_user.respond_to? :external_id
|
||||
if @submission.redirect_to_feedback? && !@embed_options[:disable_redirect_to_feedback]
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
clear_lti_session_data(@submission.exercise_id)
|
||||
redirect_to_user_feedback
|
||||
return
|
||||
end
|
||||
@ -27,7 +27,7 @@ module RedirectBehavior
|
||||
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc')
|
||||
flash.keep(:notice)
|
||||
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
clear_lti_session_data(@submission.exercise_id)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(rfc) }
|
||||
format.json { render(json: {redirect: url_for(rfc)}) }
|
||||
@ -45,7 +45,7 @@ module RedirectBehavior
|
||||
# increase counter 'times_featured' in rfc
|
||||
rfc.increment(:times_featured)
|
||||
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
clear_lti_session_data(@submission.exercise_id)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(rfc) }
|
||||
format.json { render(json: {redirect: url_for(rfc)}) }
|
||||
@ -56,7 +56,7 @@ module RedirectBehavior
|
||||
else
|
||||
# redirect to feedback page if score is less than 100 percent
|
||||
if @exercise.needs_more_feedback?(@submission) && !@embed_options[:disable_redirect_to_feedback]
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
clear_lti_session_data(@submission.exercise_id)
|
||||
redirect_to_user_feedback
|
||||
else
|
||||
redirect_to_lti_return_path
|
||||
@ -118,8 +118,8 @@ module RedirectBehavior
|
||||
|
||||
def redirect_to_lti_return_path
|
||||
Sentry.set_extras(
|
||||
consumers_id: @submission.user&.consumer,
|
||||
external_users_id: @submission.user_id,
|
||||
consumers_id: current_user.consumer_id,
|
||||
external_users_id: current_user.id,
|
||||
exercises_id: @submission.exercise_id,
|
||||
session: session.to_hash,
|
||||
submission: @submission.inspect,
|
||||
@ -128,7 +128,7 @@ module RedirectBehavior
|
||||
)
|
||||
|
||||
path = lti_return_path(submission_id: @submission.id)
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
clear_lti_session_data(@submission.exercise_id)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(path) }
|
||||
format.json { render(json: {redirect: path}) }
|
||||
|
@ -22,7 +22,8 @@ module SubmissionParameters
|
||||
def merge_user(params)
|
||||
# The study_group_id might not be present in the session (e.g. for internal users), resulting in session[:study_group_id] = nil which is intended.
|
||||
params.merge(
|
||||
user: current_user,
|
||||
contributor_id: current_user.id,
|
||||
contributor_type: current_user.class.name,
|
||||
study_group_id: current_user.current_study_group_id
|
||||
)
|
||||
end
|
||||
|
@ -55,32 +55,32 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
SELECT exercise_id, avg(working_time) as average_time, stddev_samp(extract('epoch' from working_time)) * interval '1 second' as stddev_time
|
||||
FROM
|
||||
(
|
||||
SELECT user_id,
|
||||
SELECT contributor_id,
|
||||
exercise_id,
|
||||
sum(working_time_new) AS working_time
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
(SELECT contributor_id,
|
||||
exercise_id,
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
(SELECT contributor_id,
|
||||
exercise_id,
|
||||
id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
(created_at - lag(created_at) over (PARTITION BY contributor_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id IN (SELECT ID FROM exercises WHERE #{ExecutionEnvironment.sanitize_sql(['execution_environment_id = ?', @execution_environment.id])})
|
||||
GROUP BY exercise_id, user_id, id) AS foo) AS bar
|
||||
GROUP BY user_id, exercise_id
|
||||
GROUP BY exercise_id, contributor_id, id) AS foo) AS bar
|
||||
GROUP BY contributor_id, exercise_id
|
||||
) AS baz GROUP BY exercise_id;
|
||||
"
|
||||
end
|
||||
|
||||
def user_query
|
||||
def contributor_query
|
||||
"
|
||||
SELECT
|
||||
id AS exercise_id,
|
||||
COUNT(DISTINCT user_id) AS users,
|
||||
COUNT(DISTINCT contributor_id) AS contributors,
|
||||
AVG(score) AS average_score,
|
||||
MAX(score) AS maximum_score,
|
||||
stddev_samp(score) as stddev_score,
|
||||
@ -88,24 +88,24 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
WHEN MAX(score)=0 THEN 0
|
||||
ELSE 100 / MAX(score) * AVG(score)
|
||||
END AS percent_correct,
|
||||
SUM(submission_count) / COUNT(DISTINCT user_id) AS average_submission_count
|
||||
SUM(submission_count) / COUNT(DISTINCT contributor_id) AS average_submission_count
|
||||
FROM
|
||||
(SELECT e.id,
|
||||
s.user_id,
|
||||
s.contributor_id,
|
||||
MAX(s.score) AS score,
|
||||
COUNT(s.id) AS submission_count
|
||||
FROM submissions s
|
||||
JOIN exercises e ON e.id = s.exercise_id
|
||||
WHERE #{ExecutionEnvironment.sanitize_sql(['e.execution_environment_id = ?', @execution_environment.id])}
|
||||
GROUP BY e.id,
|
||||
s.user_id) AS inner_query
|
||||
s.contributor_id) AS inner_query
|
||||
GROUP BY id;
|
||||
"
|
||||
end
|
||||
|
||||
def statistics
|
||||
working_time_statistics = {}
|
||||
user_statistics = {}
|
||||
contributor_statistics = {}
|
||||
|
||||
ApplicationRecord.connection.exec_query(working_time_query).each do |tuple|
|
||||
tuple = tuple.merge({
|
||||
@ -115,13 +115,13 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
working_time_statistics[tuple['exercise_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
ApplicationRecord.connection.exec_query(user_query).each do |tuple|
|
||||
user_statistics[tuple['exercise_id'].to_i] = tuple
|
||||
ApplicationRecord.connection.exec_query(contributor_query).each do |tuple|
|
||||
contributor_statistics[tuple['exercise_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
render locals: {
|
||||
working_time_statistics:,
|
||||
user_statistics:,
|
||||
contributor_statistics:,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -467,9 +467,9 @@ class ExercisesController < ApplicationController
|
||||
# Show general statistic page for specific exercise
|
||||
user_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
|
||||
query = Submission.select('user_id, user_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
|
||||
query = Submission.select('contributor_id, contributor_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
|
||||
.where(exercise_id: @exercise.id)
|
||||
.group('user_id, user_type')
|
||||
.group('contributor_id, contributor_type')
|
||||
|
||||
query = if policy(@exercise).detailed_statistics?
|
||||
query
|
||||
@ -481,7 +481,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
query.each do |tuple|
|
||||
user_statistics[tuple['user_type']][tuple['user_id'].to_i] = tuple
|
||||
user_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
render locals: {
|
||||
@ -493,7 +493,7 @@ class ExercisesController < ApplicationController
|
||||
# Render statistics page for one specific external user
|
||||
|
||||
if policy(@exercise).detailed_statistics?
|
||||
submissions = Submission.where(user: @external_user, exercise: @exercise)
|
||||
submissions = Submission.where(contributor: @external_user, exercise: @exercise)
|
||||
.in_study_group_of(current_user)
|
||||
.order('created_at')
|
||||
@show_autosaves = params[:show_autosaves] == 'true' || submissions.none? {|s| s.cause != 'autosave' }
|
||||
@ -510,7 +510,7 @@ class ExercisesController < ApplicationController
|
||||
@working_times_until.push((format_time_difference(@deltas[0..index].sum) if index.positive?))
|
||||
end
|
||||
else
|
||||
final_submissions = Submission.where(user: @external_user,
|
||||
final_submissions = Submission.where(contributor: @external_user,
|
||||
exercise_id: @exercise.id).in_study_group_of(current_user).final
|
||||
submissions = []
|
||||
%i[before_deadline within_grace_period after_late_deadline].each do |filter|
|
||||
|
@ -5,7 +5,7 @@ class FlowrController < ApplicationController
|
||||
require_user!
|
||||
# get the latest submission for this user that also has a test run (i.e. structured_errors if applicable)
|
||||
submission = Submission.joins(:testruns)
|
||||
.where(submissions: {user: current_user})
|
||||
.where(submissions: {contributor: current_user})
|
||||
.includes(structured_errors: [structured_error_attributes: [:error_template_attribute]])
|
||||
.merge(Testrun.order(created_at: :desc)).first
|
||||
|
||||
|
@ -42,10 +42,10 @@ class SessionsController < ApplicationController
|
||||
def destroy_through_lti
|
||||
@submission = Submission.find(params[:submission_id])
|
||||
authorize(@submission, :show?)
|
||||
lti_parameter = LtiParameter.where(external_users_id: @submission.user_id, exercises_id: @submission.exercise_id).last
|
||||
@url = consumer_return_url(build_tool_provider(consumer: @submission.user.consumer, parameters: lti_parameter&.lti_parameters))
|
||||
lti_parameter = LtiParameter.where(external_users_id: current_user.id, exercises_id: @submission.exercise_id).last
|
||||
@url = consumer_return_url(build_tool_provider(consumer: current_user.consumer, parameters: lti_parameter&.lti_parameters))
|
||||
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
clear_lti_session_data(@submission.exercise_id)
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
@ -22,7 +22,7 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = Submission.ransack(params[:q])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page], per_page: per_page_param)
|
||||
@submissions = @search.result.includes(:exercise, :contributor).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -126,7 +126,7 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
user_id = current_user.id
|
||||
user_type = current_user.class.name
|
||||
latest_submission = Submission
|
||||
.where(user_id:, user_type:, exercise_id:)
|
||||
.where(contributor_id: user_id, contributor_type: user_type, exercise_id:)
|
||||
.order(created_at: :desc).final.first
|
||||
|
||||
authorize(latest_submission, :show?)
|
||||
|
@ -43,7 +43,7 @@ module StatisticsHelper
|
||||
{
|
||||
key: 'currently_active',
|
||||
name: t('statistics.entries.users.currently_active'),
|
||||
data: Submission.where(created_at: 5.minutes.ago.., user_type: ExternalUser.name).distinct.count(:user_id),
|
||||
data: Submission.where(created_at: 5.minutes.ago.., contributor_type: ExternalUser.name).distinct.count(:contributor_id),
|
||||
url: statistics_graphs_path,
|
||||
},
|
||||
]
|
||||
|
9
app/models/concerns/contributor.rb
Normal file
9
app/models/concerns/contributor.rb
Normal file
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Contributor
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :submissions, as: :contributor
|
||||
end
|
||||
end
|
14
app/models/concerns/contributor_creation.rb
Normal file
14
app/models/concerns/contributor_creation.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ContributorCreation
|
||||
extend ActiveSupport::Concern
|
||||
include Contributor
|
||||
|
||||
included do
|
||||
belongs_to :contributor, polymorphic: true
|
||||
alias_method :user, :contributor
|
||||
alias_method :user=, :contributor=
|
||||
alias_method :author, :user
|
||||
alias_method :creator, :user
|
||||
end
|
||||
end
|
@ -27,8 +27,8 @@ class Exercise < ApplicationRecord
|
||||
has_many :exercise_tips
|
||||
has_many :tips, through: :exercise_tips
|
||||
|
||||
has_many :external_users, source: :user, source_type: 'ExternalUser', through: :submissions
|
||||
has_many :internal_users, source: :user, source_type: 'InternalUser', through: :submissions
|
||||
has_many :external_users, source: :contributor, source_type: 'ExternalUser', through: :submissions
|
||||
has_many :internal_users, source: :contributor, source_type: 'InternalUser', through: :submissions
|
||||
alias users external_users
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
|
||||
@ -65,12 +65,10 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def average_score
|
||||
if submissions.exists?(cause: 'submit')
|
||||
maximum_scores_query = submissions.select('MAX(score) AS maximum_score').group(:user_id).to_sql.sub('$1', id.to_s)
|
||||
self.class.connection.exec_query("SELECT AVG(maximum_score) AS average_score FROM (#{maximum_scores_query}) AS maximum_scores").first['average_score'].to_f
|
||||
else
|
||||
0
|
||||
end
|
||||
Submission.from(
|
||||
submissions.group(:contributor_id, :contributor_type)
|
||||
.select('MAX(score) as max_score')
|
||||
).average(:max_score).to_f
|
||||
end
|
||||
|
||||
def average_number_of_submissions
|
||||
@ -78,64 +76,66 @@ class Exercise < ApplicationRecord
|
||||
user_count.zero? ? 0 : submissions.count / user_count.to_f
|
||||
end
|
||||
|
||||
def time_maximum_score(user)
|
||||
submissions.where(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)
|
||||
def time_maximum_score(contributor)
|
||||
submissions
|
||||
.where(contributor:, cause: %w[submit assess])
|
||||
.where.not(score: nil)
|
||||
.order(score: :desc, created_at: :asc)
|
||||
.first&.created_at || Time.zone.at(0)
|
||||
end
|
||||
|
||||
def user_working_time_query
|
||||
"
|
||||
SELECT user_id,
|
||||
user_type,
|
||||
SELECT contributor_id,
|
||||
contributor_type,
|
||||
SUM(working_time_new) AS working_time,
|
||||
MAX(score) AS score
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
(SELECT contributor_id,
|
||||
contributor_type,
|
||||
score,
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
(SELECT contributor_id,
|
||||
contributor_type,
|
||||
score,
|
||||
id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
(created_at - lag(created_at) over (PARTITION BY contributor_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ?', id])}) AS foo) AS bar
|
||||
GROUP BY user_id, user_type
|
||||
GROUP BY contributor_id, contributor_type
|
||||
"
|
||||
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,
|
||||
SELECT submissions.contributor_id,
|
||||
submissions.contributor_type,
|
||||
score,
|
||||
created_at,
|
||||
(created_at - lag(created_at) over (PARTITION BY submissions.user_type, submissions.user_id, exercise_id
|
||||
(created_at - lag(created_at) over (PARTITION BY submissions.contributor_type, submissions.contributor_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ? and study_group_id = ?', exercise_id, study_group_id])} #{self.class.sanitize_sql(additional_filter)}),
|
||||
working_time_with_deltas_ignored AS (
|
||||
SELECT user_id,
|
||||
user_type,
|
||||
SELECT contributor_id,
|
||||
contributor_type,
|
||||
score,
|
||||
sum(CASE WHEN score IS NOT NULL THEN 1 ELSE 0 END)
|
||||
over (ORDER BY user_type, user_id, created_at ASC) AS change_in_score,
|
||||
over (ORDER BY contributor_type, contributor_id, created_at ASC) AS change_in_score,
|
||||
created_at,
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_filtered
|
||||
FROM working_time_between_submissions
|
||||
),
|
||||
working_times_with_score_expanded AS (
|
||||
SELECT user_id,
|
||||
user_type,
|
||||
SELECT contributor_id,
|
||||
contributor_type,
|
||||
created_at,
|
||||
working_time_filtered,
|
||||
first_value(score)
|
||||
over (PARTITION BY user_type, user_id, change_in_score ORDER BY created_at ASC) AS corrected_score
|
||||
over (PARTITION BY contributor_type, contributor_id, change_in_score ORDER BY created_at ASC) AS corrected_score
|
||||
FROM working_time_with_deltas_ignored
|
||||
),
|
||||
working_times_with_duplicated_last_row_per_score AS (
|
||||
@ -145,62 +145,62 @@ class Exercise < ApplicationRecord
|
||||
-- Duplicate last row per user and score and make it unique by setting another created_at timestamp.
|
||||
-- In addition, the working time is set to zero in order to prevent getting a wrong time.
|
||||
-- This duplication is needed, as we will shift the scores and working times by one and need to ensure not to loose any information.
|
||||
SELECT DISTINCT ON (user_type, user_id, corrected_score) user_id,
|
||||
user_type,
|
||||
SELECT DISTINCT ON (contributor_type, contributor_id, corrected_score) contributor_id,
|
||||
contributor_type,
|
||||
created_at + INTERVAL '1us',
|
||||
'00:00:00' as working_time_filtered,
|
||||
corrected_score
|
||||
FROM working_times_with_score_expanded
|
||||
),
|
||||
working_times_with_score_not_null_and_shifted AS (
|
||||
SELECT user_id,
|
||||
user_type,
|
||||
coalesce(lag(corrected_score) over (PARTITION BY user_type, user_id ORDER BY created_at ASC),
|
||||
SELECT contributor_id,
|
||||
contributor_type,
|
||||
coalesce(lag(corrected_score) over (PARTITION BY contributor_type, contributor_id ORDER BY created_at ASC),
|
||||
0) AS shifted_score,
|
||||
created_at,
|
||||
working_time_filtered
|
||||
FROM working_times_with_duplicated_last_row_per_score
|
||||
),
|
||||
working_times_to_be_sorted AS (
|
||||
SELECT user_id,
|
||||
user_type,
|
||||
SELECT contributor_id,
|
||||
contributor_type,
|
||||
shifted_score AS score,
|
||||
MIN(created_at) AS start_time,
|
||||
SUM(working_time_filtered) AS working_time_per_score,
|
||||
SUM(SUM(working_time_filtered)) over (PARTITION BY user_type, user_id) AS total_working_time
|
||||
SUM(SUM(working_time_filtered)) over (PARTITION BY contributor_type, contributor_id) AS total_working_time
|
||||
FROM working_times_with_score_not_null_and_shifted
|
||||
GROUP BY user_id, user_type, score
|
||||
GROUP BY contributor_id, contributor_type, score
|
||||
),
|
||||
working_times_with_index AS (
|
||||
SELECT (dense_rank() over (ORDER BY total_working_time, user_type, user_id ASC) - 1) AS index,
|
||||
user_id,
|
||||
user_type,
|
||||
SELECT (dense_rank() over (ORDER BY total_working_time, contributor_type, contributor_id ASC) - 1) AS index,
|
||||
contributor_id,
|
||||
contributor_type,
|
||||
score,
|
||||
start_time,
|
||||
working_time_per_score,
|
||||
total_working_time
|
||||
FROM working_times_to_be_sorted)
|
||||
SELECT index,
|
||||
user_id,
|
||||
user_type,
|
||||
contributor_id,
|
||||
contributor_type,
|
||||
name,
|
||||
score,
|
||||
start_time,
|
||||
working_time_per_score,
|
||||
total_working_time
|
||||
FROM working_times_with_index
|
||||
JOIN external_users ON user_type = 'ExternalUser' AND user_id = external_users.id
|
||||
JOIN external_users ON contributor_type = 'ExternalUser' AND contributor_id = external_users.id
|
||||
UNION ALL
|
||||
SELECT index,
|
||||
user_id,
|
||||
user_type,
|
||||
contributor_id,
|
||||
contributor_type,
|
||||
name,
|
||||
score,
|
||||
start_time,
|
||||
working_time_per_score,
|
||||
total_working_time
|
||||
FROM working_times_with_index
|
||||
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
|
||||
JOIN internal_users ON contributor_type = 'InternalUser' AND contributor_id = internal_users.id
|
||||
ORDER BY index, score ASC;
|
||||
"
|
||||
end
|
||||
@ -218,7 +218,7 @@ class Exercise < ApplicationRecord
|
||||
additional_filter = if user.blank?
|
||||
''
|
||||
else
|
||||
"AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
|
||||
"AND contributor_id = #{user.id} AND contributor_type = '#{user.class.name}'"
|
||||
end
|
||||
|
||||
results = self.class.connection.exec_query(study_group_working_time_query(id, study_group_id,
|
||||
@ -236,12 +236,12 @@ class Exercise < ApplicationRecord
|
||||
user_progress[bucket][tuple['index']] = format_time_difference(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: ERB::Util.html_escape(tuple['name'])}
|
||||
{id: tuple['contributor_id'], type: tuple['contributor_type'], name: ERB::Util.html_escape(tuple['name'])}
|
||||
end
|
||||
|
||||
if results.ntuples.positive?
|
||||
if results.size.positive?
|
||||
first_index = results[0]['index']
|
||||
last_index = results[results.ntuples - 1]['index']
|
||||
last_index = results[results.size - 1]['index']
|
||||
buckets = last_index - first_index
|
||||
user_progress.each do |timings_array|
|
||||
timings_array[buckets] = nil if timings_array.present? && timings_array.length != buckets + 1
|
||||
@ -255,15 +255,15 @@ class Exercise < ApplicationRecord
|
||||
result = self.class.connection.exec_query("
|
||||
WITH working_time AS
|
||||
(
|
||||
SELECT user_id,
|
||||
SELECT contributor_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
|
||||
(created_at - Lag(created_at) OVER (partition BY contributor_id, exercise_id ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ?', id])}
|
||||
AND user_type = 'ExternalUser'
|
||||
GROUP BY user_id,
|
||||
AND contributor_type = 'ExternalUser'
|
||||
GROUP BY contributor_id,
|
||||
id,
|
||||
exercise_id), max_points AS
|
||||
(
|
||||
@ -286,37 +286,37 @@ class Exercise < ApplicationRecord
|
||||
first_time_max_score AS
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
contributor_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
rn
|
||||
FROM (
|
||||
SELECT id,
|
||||
user_id,
|
||||
contributor_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
Row_number() OVER(partition BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
Row_number() OVER(partition BY contributor_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.contributor_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
|
||||
WHERE w.contributor_id = m.contributor_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,
|
||||
contributor_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
@ -324,7 +324,7 @@ class Exercise < ApplicationRecord
|
||||
UNION ALL
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
contributor_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
@ -333,10 +333,10 @@ class Exercise < ApplicationRecord
|
||||
(
|
||||
SELECT 1
|
||||
FROM first_time_max_score F
|
||||
WHERE f.user_id = w1.user_id
|
||||
WHERE f.contributor_id = w1.contributor_id
|
||||
AND f.exercise_id = w1.exercise_id))), filtered_times_until_max AS
|
||||
(
|
||||
SELECT user_id,
|
||||
SELECT contributor_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
CASE
|
||||
@ -345,16 +345,16 @@ class Exercise < ApplicationRecord
|
||||
END AS working_time_new
|
||||
FROM all_working_times_until_max ), result AS
|
||||
(
|
||||
SELECT e.external_id AS external_user_id,
|
||||
f.user_id,
|
||||
SELECT e.external_id AS external_contributor_id,
|
||||
f.contributor_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
|
||||
WHERE f.contributor_id = e.id
|
||||
GROUP BY e.external_id,
|
||||
f.user_id,
|
||||
f.contributor_id,
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(#{self.class.sanitize_sql(['array[?]', quantiles])}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
@ -370,7 +370,7 @@ class Exercise < ApplicationRecord
|
||||
@working_time_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
self.class.connection.exec_query(user_working_time_query).each do |tuple|
|
||||
tuple = tuple.merge('working_time' => format_time_difference(tuple['working_time']))
|
||||
@working_time_statistics[tuple['user_type']][tuple['user_id'].to_i] = tuple
|
||||
@working_time_statistics[tuple['contributor_type']][tuple['contributor_id'].to_i] = tuple
|
||||
end
|
||||
end
|
||||
|
||||
@ -388,20 +388,20 @@ class Exercise < ApplicationRecord
|
||||
@working_time_statistics[user.class.name][user.id]['working_time']
|
||||
end
|
||||
|
||||
def accumulated_working_time_for_only(user)
|
||||
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
|
||||
def accumulated_working_time_for_only(contributor)
|
||||
contributor_type = contributor.class.name
|
||||
begin
|
||||
result = self.class.connection.exec_query("
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
(SELECT contributor_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
(created_at - lag(created_at) OVER (PARTITION BY contributor_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),
|
||||
WHERE exercise_id = #{id} AND contributor_id = #{contributor.id} AND contributor_type = '#{contributor_type}'
|
||||
GROUP BY contributor_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 IN ('teacher_defined_test', 'teacher_defined_linter') GROUP BY context_id),
|
||||
|
||||
@ -413,33 +413,33 @@ class Exercise < ApplicationRecord
|
||||
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
( SELECT id,contributor_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
|
||||
SELECT id,contributor_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY contributor_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
|
||||
SELECT W.id, W.contributor_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),
|
||||
WHERE W.contributor_id = M.contributor_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
|
||||
-- if user never makes it to max points, take all times
|
||||
-- if contributor 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)
|
||||
((SELECT id, contributor_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))),
|
||||
(SELECT id, contributor_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.contributor_id = W1.contributor_id AND F.exercise_id = W1.exercise_id))),
|
||||
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
SELECT contributor_id,exercise_id, max_score, CASE WHEN #{StatisticsHelper.working_time_larger_delta} 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
|
||||
SELECT e.external_id AS external_contributor_id, f.contributor_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
|
||||
WHERE f.contributor_id = e.id GROUP BY e.external_id, f.contributor_id, exercise_id
|
||||
")
|
||||
parse_duration(result.first['working_time']).to_f
|
||||
rescue StandardError
|
||||
@ -508,14 +508,13 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def maximum_score(user = nil)
|
||||
if user
|
||||
# FIXME: where(user: user) will not work here!
|
||||
begin
|
||||
submissions.where(user:).where("cause IN ('submit','assess')").where.not(score: nil).order('score DESC').first.score || 0
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
def maximum_score(contributor = nil)
|
||||
if contributor
|
||||
submissions
|
||||
.where(contributor:, cause: %w[submit assess])
|
||||
.where.not(score: nil)
|
||||
.order(score: :desc)
|
||||
.first&.score || 0
|
||||
else
|
||||
@maximum_score ||= if files.loaded?
|
||||
files.filter(&:teacher_defined_assessment?).pluck(:weight).sum
|
||||
@ -525,12 +524,12 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def final_submission(user)
|
||||
submissions.final.where(user_id: user.id, user_type: user.class.name).order(created_at: :desc).first
|
||||
def final_submission(contributor)
|
||||
submissions.final.order(created_at: :desc).find_by(contributor:)
|
||||
end
|
||||
|
||||
def solved_by?(user)
|
||||
maximum_score(user).to_i == maximum_score.to_i
|
||||
def solved_by?(contributor)
|
||||
maximum_score(contributor).to_i == maximum_score.to_i
|
||||
end
|
||||
|
||||
def finishers
|
||||
@ -587,9 +586,9 @@ cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
|
||||
def last_submission_per_user
|
||||
Submission.joins("JOIN (
|
||||
SELECT
|
||||
user_id,
|
||||
user_type,
|
||||
first_value(id) OVER (PARTITION BY user_id ORDER BY created_at DESC) AS fv
|
||||
contributor_id,
|
||||
contributor_type,
|
||||
first_value(id) OVER (PARTITION BY contributor_id ORDER BY created_at DESC) AS fv
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id}
|
||||
) AS t ON t.fv = submissions.id").distinct
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class Submission < ApplicationRecord
|
||||
include Context
|
||||
include Creation
|
||||
include ContributorCreation
|
||||
include ActionCableHelper
|
||||
|
||||
CAUSES = %w[assess download file render run save submit test autosave requestComments remoteAssess
|
||||
@ -19,12 +19,11 @@ class Submission < ApplicationRecord
|
||||
has_many :comments, through: :files
|
||||
|
||||
belongs_to :external_users, lambda {
|
||||
where(submissions: {user_type: 'ExternalUser'}).includes(:submissions)
|
||||
}, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
|
||||
where(submissions: {contributor_type: 'ExternalUser'}).includes(:submissions)
|
||||
}, foreign_key: :contributor_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
|
||||
|
||||
where(submissions: {contributor_type: 'InternalUser'}).includes(:submissions)
|
||||
}, foreign_key: :contributor_id, class_name: 'InternalUser', optional: true
|
||||
delegate :execution_environment, to: :exercise
|
||||
|
||||
scope :final, -> { where(cause: %w[submit remoteSubmit]) }
|
||||
@ -88,7 +87,7 @@ class Submission < ApplicationRecord
|
||||
end
|
||||
|
||||
def siblings
|
||||
user.submissions.where(exercise_id:)
|
||||
contributor.submissions.where(exercise_id:)
|
||||
end
|
||||
|
||||
def to_s
|
||||
@ -125,11 +124,11 @@ class Submission < ApplicationRecord
|
||||
# Redirect 10% of users to the exercise feedback page. Ensure, that always the same
|
||||
# users get redirected per exercise and different users for different exercises. If
|
||||
# desired, the number of feedbacks can be limited with exercise.needs_more_feedback?(submission)
|
||||
(user_id + exercise.created_at.to_i) % 10 == 1
|
||||
(contributor_id + exercise.created_at.to_i) % 10 == 1
|
||||
end
|
||||
|
||||
def own_unsolved_rfc(user = self.user)
|
||||
Pundit.policy_scope(user, RequestForComment).unsolved.find_by(exercise_id: exercise, user_id:)
|
||||
Pundit.policy_scope(user, RequestForComment).unsolved.find_by(exercise:, user:)
|
||||
end
|
||||
|
||||
def unsolved_rfc(user = self.user)
|
||||
@ -208,11 +207,11 @@ class Submission < ApplicationRecord
|
||||
def prepared_runner
|
||||
request_time = Time.zone.now
|
||||
begin
|
||||
runner = Runner.for(user, exercise.execution_environment)
|
||||
runner = Runner.for(contributor, exercise.execution_environment)
|
||||
files = collect_files
|
||||
files.reject!(&:reference_implementation?) if cause == 'run'
|
||||
files.reject!(&:teacher_defined_assessment?) if cause == 'run'
|
||||
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Copying files to Runner #{runner.id} for #{user_type} #{user_id} and Submission #{id}." }
|
||||
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Copying files to Runner #{runner.id} for #{contributor_type} #{contributor_id} and Submission #{id}." }
|
||||
runner.copy_files(files)
|
||||
rescue Runner::Error => e
|
||||
e.waiting_duration = Time.zone.now - request_time
|
||||
@ -313,7 +312,7 @@ class Submission < ApplicationRecord
|
||||
update(score: score.to_d)
|
||||
if normalized_score.to_d == BigDecimal('1.0')
|
||||
Thread.new do
|
||||
RequestForComment.where(exercise_id:, user_id:, user_type:).find_each do |rfc|
|
||||
RequestForComment.joins(:submission).where(submission: {contributor:}, exercise:).find_each do |rfc|
|
||||
rfc.full_score_reached = true
|
||||
rfc.save
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class User < ApplicationRecord
|
||||
has_many :study_groups, through: :study_group_memberships, as: :user
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :user
|
||||
has_many :submissions, as: :contributor
|
||||
has_many :participations, through: :submissions, source: :exercise, as: :user
|
||||
has_many :user_proxy_exercise_exercises, as: :user
|
||||
has_many :user_exercise_interventions, as: :user
|
||||
|
@ -8,14 +8,14 @@ h1 = @execution_environment
|
||||
th.header = t(title)
|
||||
tbody
|
||||
- @execution_environment.exercises.each do |exercise|
|
||||
- us = user_statistics[exercise.id]
|
||||
- if not us then us = {"users" => 0, "average_score" => 0.0, "maximum_score" => 0, "stddev_score" => 0.0, "percent_correct" => nil, "average_submission_count" => 0}
|
||||
- us = contributor_statistics[exercise.id]
|
||||
- if not us then us = {"contributors" => 0, "average_score" => 0.0, "maximum_score" => 0, "stddev_score" => 0.0, "percent_correct" => nil, "average_submission_count" => 0}
|
||||
- wts = working_time_statistics[exercise.id]
|
||||
- if wts then average_time = wts["average_time"] else 0
|
||||
- if wts then stddev_time = wts["stddev_time"] else 0
|
||||
tr
|
||||
td = link_to_if policy(exercise).statistics?, exercise.title, controller: "exercises", action: "statistics", id: exercise.id, 'data-turbolinks' => "false"
|
||||
td = us["users"]
|
||||
td = us["contributors"]
|
||||
td = us["average_score"].to_f.round(4)
|
||||
td = us["maximum_score"].to_f.round(2)
|
||||
td = us["stddev_score"].to_f.round(4)
|
||||
|
@ -3,14 +3,14 @@ h1 = @exercise_collection
|
||||
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||
= row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count)
|
||||
= row(label: 'exercise_collections.users', value: @exercise_collection.exercises.joins(:submissions).group("submissions.user_id").count.count)
|
||||
= row(label: 'exercise_collections.solutions', value: @exercise_collection.exercises.joins(:submissions).group("submissions.user_id").group("id").count.count)
|
||||
= row(label: 'exercise_collections.users_and_programming_groups', value: Submission.from(@exercise_collection.exercises.joins(:submissions).group(:contributor_id, :contributor_type).select(:contributor_id, :contributor_type)).count)
|
||||
= row(label: 'exercise_collections.solutions', value: Submission.from(@exercise_collection.exercises.joins(:submissions).group(:contributor_id, :contributor_type, :id).select(:contributor_id, :contributor_type)).count)
|
||||
= row(label: 'exercise_collections.submissions', value: @exercise_collection.exercises.joins(:submissions).count)
|
||||
/ further metrics:
|
||||
/ number of users that attempted at least one exercise @exercise_collection.exercises.joins(:submissions).group("submissions.user_id").count.count
|
||||
/ number of solutions: @exercise_collection.exercises.joins(:submissions).group("submissions.user_id").group("id").count.count
|
||||
/ number of contributors that attempted at least one exercise @exercise_collection.exercises.joins(:submissions).group("submissions.contributor_id", "submissions.contributor_type").count.count
|
||||
/ number of solutions: @exercise_collection.exercises.joins(:submissions).group("submissions.contributor_id", "submissions.contributor_type").group("id").count.count
|
||||
/ further filters:
|
||||
/ Only before specific date: date = DateTime.parse("2015-01-01 00:00:00.000000") ; @exercise_collection.exercises.joins(:submissions).where(["submissions.created_at > ?", date]).group("submissions.user_id").count.count
|
||||
/ Only before specific date: date = DateTime.parse("2015-01-01 00:00:00.000000") ; @exercise_collection.exercises.joins(:submissions).where(["submissions.created_at > ?", date]).group("submissions.contributor_id", "submissions.contributor_type").count.count
|
||||
/ Only with specific cause: @exercise_collection.exercises.joins(:submissions).where("submissions.cause" == 'assess').count
|
||||
|
||||
= row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's')
|
||||
|
@ -9,7 +9,7 @@ h1 = @exercise
|
||||
|
||||
- [:intermediate, :final].each do |scope|
|
||||
= row(label: ".#{scope}_submissions") do
|
||||
= "#{@exercise.submissions.send(scope).count} (#{t('.users', count: @exercise.submissions.send(scope).distinct.count(:user_id))})"
|
||||
= "#{@exercise.submissions.send(scope).count} (#{t('.users', count: @exercise.submissions.send(scope).distinct.count(:contributor_id))})"
|
||||
|
||||
= row(label: '.finishing_rate') do
|
||||
p
|
||||
@ -43,7 +43,7 @@ h1 = @exercise
|
||||
p = @exercise.average_working_time
|
||||
|
||||
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
|
||||
- submissions = Submission.where(user: @exercise.send(symbol), exercise: @exercise).in_study_group_of(current_user)
|
||||
- submissions = Submission.where(contributor: @exercise.send(symbol), exercise: @exercise).in_study_group_of(current_user)
|
||||
- if !policy(@exercise).detailed_statistics?
|
||||
- submissions = submissions.final
|
||||
- if submissions.any?
|
||||
@ -60,9 +60,9 @@ h1 = @exercise
|
||||
hr
|
||||
div#chart_2
|
||||
hr
|
||||
- users = symbol.to_s.classify.constantize.where(id: submissions.joins(symbol).group(:user_id).select(:user_id).distinct)
|
||||
- contributors = symbol.to_s.classify.constantize.where(id: submissions.joins(symbol).group(:contributor_id).select(:contributor_id).distinct)
|
||||
.table-responsive.mb-4
|
||||
table.table.table-striped class="#{users.present? ? 'sortable' : ''}"
|
||||
table.table.table-striped class="#{contributors.present? ? 'sortable' : ''}"
|
||||
thead
|
||||
tr
|
||||
th.header = t('.user')
|
||||
@ -71,14 +71,14 @@ h1 = @exercise
|
||||
th.header = t('.runs') if policy(@exercise).detailed_statistics?
|
||||
th.header = t('.worktime') if policy(@exercise).detailed_statistics?
|
||||
tbody
|
||||
- users.each do |user|
|
||||
- if user_statistics[user.class.name][user.id] then us = user_statistics[user.class.name][user.id] else us = {"maximum_score" => nil, "runs" => nil}
|
||||
- label = "#{user.displayname}"
|
||||
- contributors.each do |contributor|
|
||||
- if contributor_statistics[contributor.class.name][contributor.id] then us = contributor_statistics[contributor.class.name][contributor.id] else us = {"maximum_score" => nil, "runs" => nil}
|
||||
- label = "#{contributor.displayname}"
|
||||
tr
|
||||
td = link_to_if symbol==:external_users && policy(user).statistics?, label, {controller: "exercises", action: "external_user_statistics", external_user_id: user.id, id: @exercise.id}
|
||||
td = link_to_if symbol==:external_users && policy(contributor).statistics?, label, {controller: "exercises", action: "external_user_statistics", external_user_id: contributor.id, id: @exercise.id}
|
||||
td = us['maximum_score'] or 0
|
||||
td.align-middle
|
||||
- latest_user_submission = submissions.where(user: user).final.latest
|
||||
- latest_user_submission = submissions.where(contributor:).final.latest
|
||||
- if latest_user_submission.present?
|
||||
- if latest_user_submission.before_deadline?
|
||||
.unit-test-result.positive-result
|
||||
@ -87,4 +87,4 @@ h1 = @exercise
|
||||
- elsif latest_user_submission.after_late_deadline?
|
||||
.unit-test-result.negative-result
|
||||
td = us['runs'] if policy(@exercise).detailed_statistics?
|
||||
td = @exercise.average_working_time_for(user) or 0 if policy(@exercise).detailed_statistics?
|
||||
td = @exercise.average_working_time_for(contributor) or 0 if policy(@exercise).detailed_statistics?
|
||||
|
@ -13,7 +13,7 @@ h1 = Submission.model_name.human(count: 2)
|
||||
thead
|
||||
tr
|
||||
th = sort_link(@search, :exercise_id, t('activerecord.attributes.submission.exercise'))
|
||||
th = sort_link(@search, :user_id, t('activerecord.attributes.submission.user'))
|
||||
th = sort_link(@search, :user_id, t('activerecord.attributes.submission.contributor'))
|
||||
th = sort_link(@search, :cause, t('activerecord.attributes.submission.cause'))
|
||||
th = sort_link(@search, :score, t('activerecord.attributes.submission.score'))
|
||||
th = sort_link(@search, :created_at, t('shared.created_at'))
|
||||
@ -22,7 +22,7 @@ h1 = Submission.model_name.human(count: 2)
|
||||
- @submissions.each do |submission|
|
||||
tr
|
||||
td = link_to_if(submission.exercise && policy(submission.exercise).show?, submission.exercise, submission.exercise)
|
||||
td = link_to_if(policy(submission.user).show?, submission.user, submission.user)
|
||||
td = link_to_if(policy(submission.contributor).show?, submission.contributor, submission.contributor)
|
||||
td = t("submissions.causes.#{submission.cause}")
|
||||
td = submission.score
|
||||
td = l(submission.created_at, format: :short)
|
||||
|
@ -8,7 +8,7 @@
|
||||
h1 = @submission
|
||||
|
||||
= row(label: 'submission.exercise', value: link_to_if(policy(@submission.exercise).show?, @submission.exercise, @submission.exercise))
|
||||
= row(label: 'submission.user', value: link_to_if(policy(@submission.user).show?, @submission.user, @submission.user))
|
||||
= row(label: 'submission.contributor', value: link_to_if(policy(@submission.contributor).show?, @submission.contributor, @submission.contributor))
|
||||
= row(label: 'submission.study_group', value: link_to_if(@submission.study_group.present? && policy(@submission.study_group).show?, @submission.study_group, @submission.study_group))
|
||||
= row(label: 'submission.cause', value: t("submissions.causes.#{@submission.cause}"))
|
||||
= row(label: 'submission.score', value: @submission.score)
|
||||
|
@ -119,10 +119,10 @@ de:
|
||||
submission:
|
||||
cause: Anlass
|
||||
code: Code
|
||||
contributor: Mitwirkende:r
|
||||
exercise: Aufgabe
|
||||
files: Dateien
|
||||
score: Punktzahl
|
||||
user: Autor
|
||||
study_group: Lerngruppe
|
||||
study_group:
|
||||
name: Name
|
||||
|
@ -119,10 +119,10 @@ en:
|
||||
submission:
|
||||
cause: Cause
|
||||
code: Code
|
||||
contributor: Contributor
|
||||
exercise: Exercise
|
||||
files: Files
|
||||
score: Score
|
||||
user: Author
|
||||
study_group: Study Group
|
||||
study_group:
|
||||
name: Name
|
||||
|
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RenameUserColumnsToContributorInSubmissions < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
change_table :submissions do |t|
|
||||
t.rename :user_id, :contributor_id
|
||||
t.rename :user_type, :contributor_type
|
||||
end
|
||||
end
|
||||
end
|
@ -454,15 +454,15 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_27_080619) do
|
||||
create_table "submissions", id: :serial, force: :cascade do |t|
|
||||
t.integer "exercise_id"
|
||||
t.float "score"
|
||||
t.integer "user_id"
|
||||
t.integer "contributor_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "cause"
|
||||
t.string "user_type"
|
||||
t.string "contributor_type"
|
||||
t.bigint "study_group_id"
|
||||
t.index ["contributor_id"], name: "index_submissions_on_contributor_id"
|
||||
t.index ["exercise_id"], name: "index_submissions_on_exercise_id"
|
||||
t.index ["study_group_id"], name: "index_submissions_on_study_group_id"
|
||||
t.index ["user_id"], name: "index_submissions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "subscriptions", id: :serial, force: :cascade do |t|
|
||||
|
@ -28,7 +28,7 @@ ExecutionEnvironment.create_factories user: admin
|
||||
@exercises = find_factories_by_class(Exercise).map(&:name).index_with {|factory_name| FactoryBot.create(factory_name, user: teacher) }
|
||||
|
||||
# submissions
|
||||
FactoryBot.create(:submission, exercise: @exercises[:fibonacci], user: external_user)
|
||||
FactoryBot.create(:submission, exercise: @exercises[:fibonacci], contributor: external_user)
|
||||
|
||||
# The old images included in the seed data do not feature a dedicated `user` and therefore require a privileged execution.
|
||||
ExecutionEnvironment.update_all privileged_execution: true # rubocop:disable Rails/SkipsModelValidations
|
||||
|
@ -25,7 +25,7 @@ describe FileParameters do
|
||||
|
||||
it 'new file' do
|
||||
submission = create(:submission, exercise: hello_world, id: 1337)
|
||||
controller.instance_variable_set(:@current_user, submission.user)
|
||||
controller.instance_variable_set(:@current_user, submission.contributor)
|
||||
|
||||
new_file = create(:file, context: submission)
|
||||
expect(file_accepted?(new_file)).to be true
|
||||
@ -58,8 +58,8 @@ describe FileParameters do
|
||||
it 'file of another submission' do
|
||||
learner1 = create(:learner)
|
||||
learner2 = create(:learner)
|
||||
submission_learner1 = create(:submission, exercise: hello_world, user: learner1)
|
||||
_submission_learner2 = create(:submission, exercise: hello_world, user: learner2)
|
||||
submission_learner1 = create(:submission, exercise: hello_world, contributor: learner1)
|
||||
_submission_learner2 = create(:submission, exercise: hello_world, contributor: learner2)
|
||||
|
||||
controller.instance_variable_set(:@current_user, learner2)
|
||||
other_submissions_file = create(:file, context: submission_learner1)
|
||||
|
@ -107,7 +107,7 @@ describe Lti do
|
||||
let(:submission) { create(:submission) }
|
||||
|
||||
before do
|
||||
create(:lti_parameter, consumers_id: consumer.id, external_users_id: submission.user_id, exercises_id: submission.exercise_id)
|
||||
create(:lti_parameter, consumers_id: consumer.id, external_users_id: submission.contributor_id, exercises_id: submission.exercise_id)
|
||||
end
|
||||
|
||||
context 'with an invalid score' do
|
||||
@ -156,7 +156,7 @@ describe Lti do
|
||||
|
||||
context 'without a tool consumer' do
|
||||
it 'returns a corresponding status' do
|
||||
submission.user.consumer = nil
|
||||
submission.contributor.consumer = nil
|
||||
|
||||
allow(submission).to receive(:normalized_score).and_return score
|
||||
expect(controller.send(:send_score, submission)[:status]).to eq('error')
|
||||
|
@ -5,9 +5,9 @@ require 'rails_helper'
|
||||
describe CodeOcean::FilesController do
|
||||
render_views
|
||||
|
||||
let(:user) { create(:admin) }
|
||||
let(:contributor) { create(:admin) }
|
||||
|
||||
before { allow(controller).to receive(:current_user).and_return(user) }
|
||||
before { allow(controller).to receive(:current_user).and_return(contributor) }
|
||||
|
||||
describe 'GET #show_protected_upload' do
|
||||
context 'with a valid filename' do
|
||||
@ -30,7 +30,7 @@ describe CodeOcean::FilesController do
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:submission) { create(:submission, user:) }
|
||||
let(:submission) { create(:submission, contributor:) }
|
||||
|
||||
context 'with a valid file' do
|
||||
let(:perform_request) { proc { post :create, params: {code_ocean_file: build(:file, context: submission).attributes, format: :json} } }
|
||||
|
@ -164,7 +164,7 @@ describe ExercisesController do
|
||||
expect_assigns(exercise: :exercise)
|
||||
|
||||
context 'with an existing submission' do
|
||||
let!(:submission) { create(:submission, exercise_id: exercise.id, user_id: user.id, user_type: user.class.name) }
|
||||
let!(:submission) { create(:submission, exercise:, contributor: user) }
|
||||
|
||||
it "populates the editors with the submission's files' content" do
|
||||
perform_request.call
|
||||
@ -260,18 +260,18 @@ describe ExercisesController do
|
||||
let(:external_user) { create(:external_user) }
|
||||
|
||||
before do
|
||||
2.times { create(:submission, cause: 'autosave', user: external_user, exercise:) }
|
||||
2.times { create(:submission, cause: 'run', user: external_user, exercise:) }
|
||||
create(:submission, cause: 'assess', user: external_user, exercise:)
|
||||
create_list(:submission, 2, cause: 'autosave', contributor: external_user, exercise:)
|
||||
create_list(:submission, 2, cause: 'run', contributor: external_user, exercise:)
|
||||
create(:submission, cause: 'assess', contributor: external_user, exercise:)
|
||||
end
|
||||
|
||||
context 'when viewing the default submission statistics page without a parameter' do
|
||||
it 'does not list autosaved submissions' do
|
||||
perform_request
|
||||
expect(assigns(:all_events).filter {|event| event.is_a? Submission }).to contain_exactly(
|
||||
an_object_having_attributes(cause: 'run', user_id: external_user.id),
|
||||
an_object_having_attributes(cause: 'assess', user_id: external_user.id),
|
||||
an_object_having_attributes(cause: 'run', user_id: external_user.id)
|
||||
an_object_having_attributes(cause: 'run', contributor: external_user),
|
||||
an_object_having_attributes(cause: 'assess', contributor: external_user),
|
||||
an_object_having_attributes(cause: 'run', contributor: external_user)
|
||||
)
|
||||
end
|
||||
end
|
||||
@ -283,7 +283,7 @@ describe ExercisesController do
|
||||
perform_request
|
||||
submissions = assigns(:all_events).filter {|event| event.is_a? Submission }
|
||||
expect(submissions).to match_array Submission.all
|
||||
expect(submissions).to include an_object_having_attributes(cause: 'autosave', user_id: external_user.id)
|
||||
expect(submissions).to include an_object_having_attributes(cause: 'autosave', contributor: external_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -291,7 +291,7 @@ describe ExercisesController do
|
||||
describe 'POST #submit' do
|
||||
let(:output) { {} }
|
||||
let(:perform_request) { post :submit, format: :json, params: {id: exercise.id, submission: {cause: 'submit', exercise_id: exercise.id}} }
|
||||
let(:user) { create(:external_user) }
|
||||
let(:contributor) { create(:external_user) }
|
||||
let(:scoring_response) do
|
||||
[{
|
||||
status: :ok,
|
||||
@ -312,8 +312,8 @@ describe ExercisesController do
|
||||
end
|
||||
|
||||
before do
|
||||
create(:lti_parameter, external_user: user, exercise:)
|
||||
submission = build(:submission, exercise:, user:)
|
||||
create(:lti_parameter, external_user: contributor, exercise:)
|
||||
submission = build(:submission, exercise:, contributor:)
|
||||
allow(submission).to receive_messages(normalized_score: 1, calculate_score: scoring_response, redirect_to_feedback?: false)
|
||||
allow(Submission).to receive(:create).and_return(submission)
|
||||
end
|
||||
|
@ -210,7 +210,7 @@ describe SessionsController do
|
||||
# Todo replace session with lti_parameter
|
||||
# Todo create LtiParameter Object
|
||||
# session[:lti_parameters] = {}
|
||||
allow(controller).to receive(:current_user).and_return(submission.user)
|
||||
allow(controller).to receive(:current_user).and_return(submission.contributor)
|
||||
perform_request.call
|
||||
end
|
||||
|
||||
|
@ -6,9 +6,9 @@ describe SubmissionsController do
|
||||
render_views
|
||||
|
||||
let(:submission) { create(:submission) }
|
||||
let(:user) { create(:admin) }
|
||||
let(:contributor) { create(:admin) }
|
||||
|
||||
before { allow(controller).to receive(:current_user).and_return(user) }
|
||||
before { allow(controller).to receive(:current_user).and_return(contributor) }
|
||||
|
||||
describe 'POST #create' do
|
||||
before do
|
||||
|
@ -22,12 +22,12 @@ describe 'Editor', js: true do
|
||||
weight: 2.0,
|
||||
}]
|
||||
end
|
||||
let(:user) { create(:teacher) }
|
||||
let(:contributor) { create(:teacher) }
|
||||
let(:exercise_without_test) { create(:tdd) }
|
||||
|
||||
before do
|
||||
visit(sign_in_path)
|
||||
fill_in('email', with: user.email)
|
||||
fill_in('email', with: contributor.email)
|
||||
fill_in('password', with: attributes_for(:teacher)[:password])
|
||||
click_button(I18n.t('sessions.new.link'))
|
||||
allow_any_instance_of(LtiHelper).to receive(:lti_outcome_service?).and_return(true)
|
||||
@ -111,7 +111,7 @@ describe 'Editor', js: true do
|
||||
end
|
||||
|
||||
it 'contains a button for submitting the exercise' do
|
||||
submission = build(:submission, user:, exercise:)
|
||||
submission = build(:submission, contributor:, exercise:)
|
||||
allow(submission).to receive(:calculate_score).and_return(scoring_response)
|
||||
allow(Submission).to receive(:find).and_return(submission)
|
||||
click_button(I18n.t('exercises.editor.score'))
|
||||
|
@ -9,10 +9,10 @@ describe 'ExternalUserStatistics', js: true do
|
||||
let(:password) { 'password123456' }
|
||||
|
||||
before do
|
||||
2.times { create(:submission, cause: 'autosave', user: learner, exercise:, study_group:) }
|
||||
2.times { create(:submission, cause: 'run', user: learner, exercise:, study_group:) }
|
||||
create(:submission, cause: 'assess', user: learner, exercise:, study_group:)
|
||||
create(:submission, cause: 'submit', user: learner, exercise:, study_group:)
|
||||
2.times { create(:submission, cause: 'autosave', contributor: learner, exercise:, study_group:) }
|
||||
2.times { create(:submission, cause: 'run', contributor: learner, exercise:, study_group:) }
|
||||
create(:submission, cause: 'assess', contributor: learner, exercise:, study_group:)
|
||||
create(:submission, cause: 'submit', contributor: learner, exercise:, study_group:)
|
||||
|
||||
study_group.external_users << learner
|
||||
study_group.internal_users << user
|
||||
|
@ -7,7 +7,7 @@ describe Exercise do
|
||||
let(:users) { create_list(:external_user, 10) }
|
||||
|
||||
def create_submissions
|
||||
create_list(:submission, 10, cause: 'submit', exercise:, score: Forgery(:basic).number, user: users.sample)
|
||||
create_list(:submission, 10, cause: 'submit', exercise:, score: Forgery(:basic).number, contributor: users.sample)
|
||||
end
|
||||
|
||||
it 'validates the number of main files' do
|
||||
@ -77,7 +77,10 @@ describe Exercise do
|
||||
before { create_submissions }
|
||||
|
||||
it 'returns the average score expressed as a percentage' do
|
||||
maximum_percentages = exercise.submissions.group_by(&:user_id).values.map {|submission| submission.max_by(&:score).score / exercise.maximum_score * 100 }
|
||||
maximum_percentages = exercise.submissions.group_by do |s|
|
||||
[s.contributor_type,
|
||||
s.contributor_id]
|
||||
end.values.map {|submission| submission.max_by(&:score).score / exercise.maximum_score * 100 }
|
||||
expect(exercise.average_percentage).to eq(maximum_percentages.average.round(2))
|
||||
end
|
||||
end
|
||||
@ -96,7 +99,10 @@ describe Exercise do
|
||||
before { create_submissions }
|
||||
|
||||
it "returns the average of all users' maximum scores" do
|
||||
maximum_scores = exercise.submissions.group_by(&:user_id).values.map {|submission| submission.max_by(&:score).score }
|
||||
maximum_scores = exercise.submissions.group_by do |s|
|
||||
[s.contributor_type,
|
||||
s.contributor_id]
|
||||
end.values.map {|submission| submission.max_by(&:score).score }
|
||||
expect(exercise.average_score).to be_within(0.1).of(maximum_scores.average)
|
||||
end
|
||||
end
|
||||
|
@ -13,8 +13,8 @@ describe Submission do
|
||||
expect(described_class.create.errors[:exercise]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a user' do
|
||||
expect(described_class.create.errors[:user]).to be_present
|
||||
it 'validates the presence of a contributor' do
|
||||
expect(described_class.create.errors[:contributor]).to be_present
|
||||
end
|
||||
|
||||
describe '#main_file' do
|
||||
@ -67,19 +67,19 @@ describe Submission do
|
||||
end
|
||||
|
||||
describe '#siblings' do
|
||||
let(:siblings) { described_class.find_by(user:).siblings }
|
||||
let(:user) { create(:external_user) }
|
||||
let(:siblings) { described_class.find_by(contributor:).siblings }
|
||||
let(:contributor) { create(:external_user) }
|
||||
|
||||
before do
|
||||
10.times.each_with_index do |_, index|
|
||||
create(:submission, exercise: submission.exercise, user: (index.even? ? user : create(:external_user)))
|
||||
create(:submission, exercise: submission.exercise, contributor: (index.even? ? contributor : create(:external_user)))
|
||||
end
|
||||
end
|
||||
|
||||
it "returns all the creator's submissions for the same exercise" do
|
||||
expect(siblings).to be_an(ActiveRecord::Relation)
|
||||
expect(siblings.map(&:exercise).uniq).to eq([submission.exercise])
|
||||
expect(siblings.map(&:user).uniq).to eq([user])
|
||||
expect(siblings.map(&:contributor).uniq).to eq([contributor])
|
||||
end
|
||||
end
|
||||
|
||||
@ -92,8 +92,8 @@ describe Submission do
|
||||
describe '#redirect_to_feedback?' do
|
||||
context 'with no exercise feedback' do
|
||||
let(:exercise) { create(:dummy) }
|
||||
let(:user) { build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) % 10) }
|
||||
let(:submission) { build(:submission, exercise:, user:) }
|
||||
let(:contributor) { build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) % 10) }
|
||||
let(:submission) { build(:submission, exercise:, contributor:) }
|
||||
|
||||
it 'sends 10% of users to feedback page' do
|
||||
expect(submission.send(:redirect_to_feedback?)).to be_truthy
|
||||
@ -101,7 +101,7 @@ describe Submission do
|
||||
|
||||
it 'does not redirect other users' do
|
||||
9.times do |i|
|
||||
submission = build(:submission, exercise:, user: build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) - i - 1))
|
||||
submission = build(:submission, exercise:, contributor: build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) - i - 1))
|
||||
expect(submission.send(:redirect_to_feedback?)).to be_falsey
|
||||
end
|
||||
end
|
||||
@ -109,8 +109,8 @@ describe Submission do
|
||||
|
||||
context 'with little exercise feedback' do
|
||||
let(:exercise) { create(:dummy_with_user_feedbacks) }
|
||||
let(:user) { build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) % 10) }
|
||||
let(:submission) { build(:submission, exercise:, user:) }
|
||||
let(:contributor) { build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) % 10) }
|
||||
let(:submission) { build(:submission, exercise:, contributor:) }
|
||||
|
||||
it 'sends 10% of users to feedback page' do
|
||||
expect(submission.send(:redirect_to_feedback?)).to be_truthy
|
||||
@ -118,7 +118,7 @@ describe Submission do
|
||||
|
||||
it 'does not redirect other users' do
|
||||
9.times do |i|
|
||||
submission = build(:submission, exercise:, user: build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) - i - 1))
|
||||
submission = build(:submission, exercise:, contributor: build(:external_user, id: (11 - (exercise.created_at.to_i % 10)) - i - 1))
|
||||
expect(submission.send(:redirect_to_feedback?)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
@ -7,7 +7,7 @@ describe RequestForCommentPolicy do
|
||||
|
||||
context 'when the RfC visibility is not considered' do
|
||||
let(:submission) { create(:submission, study_group: create(:study_group)) }
|
||||
let(:rfc) { create(:rfc, submission:, user: submission.user) }
|
||||
let(:rfc) { create(:rfc, submission:, user: submission.contributor) }
|
||||
|
||||
%i[destroy? edit?].each do |action|
|
||||
permissions(action) do
|
||||
|
@ -20,8 +20,8 @@ describe SubmissionPolicy do
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
user = create(:external_user)
|
||||
expect(policy).to permit(user, build(:submission, exercise: Exercise.new, user_id: user.id, user_type: user.class.name))
|
||||
contributor = create(:external_user)
|
||||
expect(policy).to permit(contributor, build(:submission, exercise: Exercise.new, contributor:))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user