diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 05b2f558..1f0aa2ae 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -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 diff --git a/app/controllers/concerns/file_parameters.rb b/app/controllers/concerns/file_parameters.rb index 3f0e746f..9b52992b 100644 --- a/app/controllers/concerns/file_parameters.rb +++ b/app/controllers/concerns/file_parameters.rb @@ -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. diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb index 0f71a280..37dd82a3 100644 --- a/app/controllers/concerns/lti.rb +++ b/app/controllers/concerns/lti.rb @@ -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? diff --git a/app/controllers/concerns/redirect_behavior.rb b/app/controllers/concerns/redirect_behavior.rb index aca1308d..f9fc1035 100644 --- a/app/controllers/concerns/redirect_behavior.rb +++ b/app/controllers/concerns/redirect_behavior.rb @@ -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}) } diff --git a/app/controllers/concerns/submission_parameters.rb b/app/controllers/concerns/submission_parameters.rb index fb565c65..b513dbc0 100644 --- a/app/controllers/concerns/submission_parameters.rb +++ b/app/controllers/concerns/submission_parameters.rb @@ -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 diff --git a/app/controllers/execution_environments_controller.rb b/app/controllers/execution_environments_controller.rb index c5224f36..2600a237 100644 --- a/app/controllers/execution_environments_controller.rb +++ b/app/controllers/execution_environments_controller.rb @@ -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 diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 32575cfd..e1767ea0 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -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| diff --git a/app/controllers/flowr_controller.rb b/app/controllers/flowr_controller.rb index 278fa31b..397b2d0e 100644 --- a/app/controllers/flowr_controller.rb +++ b/app/controllers/flowr_controller.rb @@ -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 diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index b557782b..bd367ddc 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -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 diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index bc2ca034..f118a8ef 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -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 diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb index 14dec41d..57921821 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -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?) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index b4260563..d8e5abc7 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -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, }, ] diff --git a/app/models/concerns/contributor.rb b/app/models/concerns/contributor.rb new file mode 100644 index 00000000..624818ea --- /dev/null +++ b/app/models/concerns/contributor.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Contributor + extend ActiveSupport::Concern + + included do + has_many :submissions, as: :contributor + end +end diff --git a/app/models/concerns/contributor_creation.rb b/app/models/concerns/contributor_creation.rb new file mode 100644 index 00000000..376f8e71 --- /dev/null +++ b/app/models/concerns/contributor_creation.rb @@ -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 diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 35ebfdce..17feeb51 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -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 diff --git a/app/models/submission.rb b/app/models/submission.rb index 485c040b..bf110622 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 6d51a8da..f980ca47 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/execution_environments/statistics.html.slim b/app/views/execution_environments/statistics.html.slim index a296b1ca..53fc392c 100644 --- a/app/views/execution_environments/statistics.html.slim +++ b/app/views/execution_environments/statistics.html.slim @@ -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) diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index bab6b76b..9f43fefd 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -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') diff --git a/app/views/exercises/statistics.html.slim b/app/views/exercises/statistics.html.slim index d50194a8..2caa92de 100644 --- a/app/views/exercises/statistics.html.slim +++ b/app/views/exercises/statistics.html.slim @@ -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? diff --git a/app/views/submissions/index.html.slim b/app/views/submissions/index.html.slim index d1a46ab7..b5df6709 100644 --- a/app/views/submissions/index.html.slim +++ b/app/views/submissions/index.html.slim @@ -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) diff --git a/app/views/submissions/show.html.slim b/app/views/submissions/show.html.slim index 19b60d77..7109046a 100644 --- a/app/views/submissions/show.html.slim +++ b/app/views/submissions/show.html.slim @@ -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) diff --git a/config/locales/de.yml b/config/locales/de.yml index 95403022..bf00edda 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index f675c6ad..de22f48f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/db/migrate/20230710130036_rename_user_columns_to_contributor_in_submissions.rb b/db/migrate/20230710130036_rename_user_columns_to_contributor_in_submissions.rb new file mode 100644 index 00000000..fafc5f2c --- /dev/null +++ b/db/migrate/20230710130036_rename_user_columns_to_contributor_in_submissions.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index fcea3229..a0271ab5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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| diff --git a/db/seeds/development.rb b/db/seeds/development.rb index 833dc6d4..7bc56c1c 100644 --- a/db/seeds/development.rb +++ b/db/seeds/development.rb @@ -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 diff --git a/spec/concerns/file_parameters_spec.rb b/spec/concerns/file_parameters_spec.rb index e148e4a8..02d64715 100644 --- a/spec/concerns/file_parameters_spec.rb +++ b/spec/concerns/file_parameters_spec.rb @@ -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) diff --git a/spec/concerns/lti_spec.rb b/spec/concerns/lti_spec.rb index 820b1ee8..e661cde3 100644 --- a/spec/concerns/lti_spec.rb +++ b/spec/concerns/lti_spec.rb @@ -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') diff --git a/spec/controllers/code_ocean/files_controller_spec.rb b/spec/controllers/code_ocean/files_controller_spec.rb index f78ed419..e230ba5f 100644 --- a/spec/controllers/code_ocean/files_controller_spec.rb +++ b/spec/controllers/code_ocean/files_controller_spec.rb @@ -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} } } diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index 1383c1be..c32061ce 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -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 diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index cf727914..9a15f02e 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -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 diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index 59b75d8c..0b9f6660 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -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 diff --git a/spec/features/editor_spec.rb b/spec/features/editor_spec.rb index fb09bd79..a615d01c 100644 --- a/spec/features/editor_spec.rb +++ b/spec/features/editor_spec.rb @@ -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')) diff --git a/spec/features/external_user_statistics_spec.rb b/spec/features/external_user_statistics_spec.rb index 0da5f4ba..f579a635 100644 --- a/spec/features/external_user_statistics_spec.rb +++ b/spec/features/external_user_statistics_spec.rb @@ -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 diff --git a/spec/models/exercise_spec.rb b/spec/models/exercise_spec.rb index 20f917ed..3f11d0c3 100644 --- a/spec/models/exercise_spec.rb +++ b/spec/models/exercise_spec.rb @@ -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 diff --git a/spec/models/submission_spec.rb b/spec/models/submission_spec.rb index d6ec7f1b..42685572 100644 --- a/spec/models/submission_spec.rb +++ b/spec/models/submission_spec.rb @@ -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 diff --git a/spec/policies/request_for_comment_policy_spec.rb b/spec/policies/request_for_comment_policy_spec.rb index ab883637..707066be 100644 --- a/spec/policies/request_for_comment_policy_spec.rb +++ b/spec/policies/request_for_comment_policy_spec.rb @@ -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 diff --git a/spec/policies/submission_policy_spec.rb b/spec/policies/submission_policy_spec.rb index bb36c542..4d571c4f 100644 --- a/spec/policies/submission_policy_spec.rb +++ b/spec/policies/submission_policy_spec.rb @@ -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