Rename user to contributor in submission

This commit is contained in:
kiragrammel
2023-08-11 12:15:36 +02:00
committed by Sebastian Serth
parent 97138288f4
commit 0234414bae
39 changed files with 267 additions and 233 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Contributor
extend ActiveSupport::Concern
included do
has_many :submissions, as: :contributor
end
end

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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