Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ module Course::Assessment::LiveFeedback::ThreadConcern
extend ActiveSupport::Concern

def safe_create_and_save_thread_info
# `@submission` is the extension (`@assessment.submissions.find`), whose own id differs from the
# attempt id that `submission_questions.submission_id` references. Match on the attempt id.
submission_question = Course::Assessment::SubmissionQuestion.where(
submission_id: @submission, question_id: @answer.question
submission_id: @submission.attempt_id, question_id: @answer.question
).first

submission_question.with_lock do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def submission_status_hash
def process_all_submissions
create_new_submissions_if_not_existing

@submission_hash = Course::Assessment::Submission.where(assessment: @assessment).to_h do |s|
@submission_hash = @assessment.submissions.to_h do |s|
[s.creator_id, s]
end

Expand All @@ -59,8 +59,10 @@ def process_submission(submission, cm_submission)
end

def create_new_submissions_if_not_existing
existing_submission_user_ids = Course::Assessment::Submission.where(assessment: @assessment).
pluck(:creator_id)
# `creator_id` lives on the base (`course_assessment_submissions`), not on Submission's own
# table. Unqualified, `pluck` is ambiguous — `acts_as :experience_points_record` also joins
# `course_experience_points_records`, which also has `creator_id`. Qualify explicitly.
existing_submission_user_ids = @assessment.submissions.pluck('course_assessment_submissions.creator_id')
koditsu_submission_user_ids = @cu_submission_hash.keys.map { |creator, _| creator.id }
user_ids_without_submission = koditsu_submission_user_ids - existing_submission_user_ids

Expand All @@ -77,8 +79,7 @@ def create_new_submissions_if_not_existing

def create_new_submission_for(creator, course_user)
User.with_stamper(creator) do
new_submission = @assessment.submissions.new(creator: creator,
course_user: course_user)
new_submission = @assessment.build_submission(creator: creator, course_user: course_user)
success = @assessment.create_new_submission(new_submission, course_user)

raise ActiveRecord::Rollback unless success
Expand All @@ -96,7 +97,10 @@ def update_submission(cm_submission, state, submitted_at)
end

def process_submission_answers(submission, cm_submission)
answers = Course::Assessment::Answer.includes(:question).where(submission_id: cm_submission.id)
# `cm_submission` is a Submission (extension), whose own `id` is an independent serial, NOT the
# attempt id that `answers.submission_id` references. Use `cm_submission.attempt_id` (the
# extension's FK to its attempt) to find the answers.
answers = Course::Assessment::Answer.includes(:question).where(submission_id: cm_submission.attempt_id)

build_answer_hash(answers)

Expand Down
4 changes: 4 additions & 0 deletions app/controllers/concerns/course/statistics/counts_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def num_attempted_students_hash
attempted_submissions_count = ActiveRecord::Base.connection.execute("
SELECT cas.assessment_id AS id, COUNT(DISTINCT cas.creator_id) AS count
FROM course_assessment_submissions cas
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
WHERE
cas.creator_id IN (#{@all_students.map(&:user_id).join(', ')})
AND cas.assessment_id IN (#{@assessments.pluck(:id).join(', ')})
Expand All @@ -27,6 +28,7 @@ def num_submitted_students_hash
submitted_submissions_count = ActiveRecord::Base.connection.execute("
SELECT cas.assessment_id AS id, COUNT(DISTINCT cas.creator_id) AS count
FROM course_assessment_submissions cas
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
WHERE
cas.creator_id IN (#{@all_students.map(&:user_id).join(', ')})
AND cas.assessment_id IN (#{@assessments.pluck(:id).join(', ')})
Expand All @@ -46,6 +48,7 @@ def num_late_students_hash
all_submissions = ActiveRecord::Base.connection.execute("
SELECT cu.id AS course_user_id, cas.assessment_id, MAX(cas.submitted_at) as submitted_at
FROM course_assessment_submissions cas
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
JOIN course_users cu
ON cu.user_id = cas.creator_id
WHERE
Expand All @@ -65,6 +68,7 @@ def latest_submission_time_hash
latest_submissions = ActiveRecord::Base.connection.execute("
SELECT cas.assessment_id AS id, MAX(cas.submitted_at) AS latest_submitted_at
FROM course_assessment_submissions cas
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
WHERE
cas.creator_id IN (#{@all_students.map(&:user_id).join(', ')})
AND cas.assessment_id IN (#{@assessments.pluck(:id).join(', ')})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def grade_statistics_hash
FROM (
SELECT cas.creator_id, cas.assessment_id, SUM(caa.grade) AS grade
FROM course_assessment_submissions cas
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
JOIN course_assessment_answers caa ON cas.id = caa.submission_id
WHERE
cas.creator_id IN (#{@all_students.map(&:user_id).join(', ')})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def answer_statistics_hash
course_assessment_answers caa_inner
JOIN
course_assessment_submissions cas_inner ON caa_inner.submission_id = cas_inner.id
INNER JOIN
course_assessment_submission_details cad_inner ON cad_inner.attempt_id = cas_inner.id
WHERE
cas_inner.assessment_id = #{assessment_params[:id]}
) AS caa_ranked
Expand All @@ -57,6 +59,7 @@ def answer_statistics_hash
COUNT(*) AS attempt_count
FROM course_assessment_answers caa
JOIN course_assessment_submissions cas ON caa.submission_id = cas.id
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
WHERE cas.assessment_id = #{assessment_params[:id]} AND caa.workflow_state != 'attempting'
GROUP BY caa.question_id, caa.submission_id
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def duration_statistics_hash
SELECT cas.creator_id, cas.assessment_id,
EXTRACT(EPOCH FROM cas.submitted_at) - EXTRACT(EPOCH FROM cas.created_at) AS duration
FROM course_assessment_submissions cas
INNER JOIN course_assessment_submission_details cad ON cad.attempt_id = cas.id
WHERE
cas.creator_id IN (#{@all_students.map(&:user_id).join(', ')})
AND cas.assessment_id IN (#{@assessments.pluck(:id).join(', ')})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ def submissions
if @assessment.submissions.loaded?
@assessment.submissions.select { |s| s.creator_id == current_user.id }
else
@assessment.submissions.where(creator_id: current_user.id)
@assessment.submissions.by_user(current_user)
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class Course::Assessment::Submission::Answer::Programming::AnnotationsController < \
class Course::Assessment::Submission::Answer::Programming::AnnotationsController <
Course::Assessment::Submission::Answer::Programming::Controller
include Signals::EmissionConcern

Expand Down Expand Up @@ -66,7 +66,7 @@ def create_topic_subscription

# Ensure all group managers get a notification when someone adds a programming annotation
# to the answer.
answer_course_user = @answer.submission.course_user
answer_course_user = @answer.attempt.submission.course_user
answer_course_user.my_managers.each do |manager|
@discussion_topic.ensure_subscribed_by(manager.user)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def index
def create # rubocop:disable Metrics/AbcSize
authorize! :access, @assessment

existing_submission = @assessment.submissions.find_by(creator: current_user)
existing_submission = @assessment.submissions.by_user(current_user).first
create_success_response(existing_submission) and return if existing_submission

ActiveRecord::Base.transaction do
Expand Down Expand Up @@ -423,7 +423,10 @@ def course_user_ids

def user_ids_without_submission
existing_submissions = @assessment.submissions.by_users(course_user_ids.pluck(:user_id))
user_ids_with_submission = existing_submissions.pluck(:creator_id)
# `creator_id` lives on the base (`course_assessment_submissions`), not on Submission's own
# table. Unqualified, `pluck` is ambiguous — `acts_as :experience_points_record` also joins
# `course_experience_points_records`, which also has `creator_id`. Qualify explicitly.
user_ids_with_submission = existing_submissions.pluck('course_assessment_submissions.creator_id')
course_user_ids.pluck(:user_id) - user_ids_with_submission
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def create_topic_subscription
@discussion_topic.ensure_subscribed_by(@submission_question.submission.creator)

# Ensure all group managers get a notification when someone comments on this submission question
submission_question_course_user = @submission_question.submission.course_user
submission_question_course_user = @submission_question.attempt.submission.course_user
submission_question_course_user.my_managers.each do |manager|
@discussion_topic.ensure_subscribed_by(manager.user)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ class Course::Assessment::SubmissionQuestion::SubmissionQuestionsController < Co
load_resource :assessment, class: 'Course::Assessment', through: :course, parent: false

def all_answers
@submission = @assessment.submissions.find(all_answers_params[:submission_id])
# `all_answers_params[:submission_id]` is an Attempt id — the wire key `submissionId` carries
# the attempt's id, not the extension table's own id. Find by attempt, then navigate to the real
# Submission for `authorize!`, whose `can :read, ...Submission` rules match on subject class.
@submission = @assessment.attempts.find(all_answers_params[:submission_id]).submission
# A preview attempt has no Submission extension row; treat it as not found here rather
# than relying on the incidental `authorize!(:read, nil)` denial.
raise ActiveRecord::RecordNotFound if @submission.nil?

authorize!(:read, @submission)
@submission_question = @submission.
submission_questions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def load_submissions
@submissions = Course::Assessment::Submission.by_users(student_ids).
ordered_by_submitted_date.accessible_by(current_ability).
calculated(:grade).
includes(:answers, experience_points_record: { course_user: [:course, :groups] })
includes({ attempt: :answers }, experience_points_record: { course_user: [:course, :groups] })
end

# Load pending submissions, either for the entire course, or for my students only.
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/course/material/materials_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def material_params
def create_submission
current_course_user = current_course.course_users.find_by(user: current_user)
@assessment = @folder.owner
existing_submission = @assessment.submissions.find_by(creator: current_user)
existing_submission = @assessment.submissions.by_user(current_user).first
unless existing_submission
@submission = @assessment.submissions.new(course_user: current_course_user)
@submission = @assessment.build_submission(course_user: current_course_user)
@submission.session_id = authentication_service.generate_authentication_token
success = @assessment.create_new_submission(@submission, current_user)

Expand Down
7 changes: 6 additions & 1 deletion app/controllers/course/statistics/aggregate_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,17 @@ def fetch_course_get_help_data(start_date, end_date)
get_help_data = Course::Assessment::LiveFeedback::Message.find_by_sql(<<-SQL)
SELECT DISTINCT ON (t.submission_creator_id, s.assessment_id, sq.question_id)
m.id, m.content, m.created_at, t.submission_creator_id,
s.assessment_id, sq.submission_id, sq.question_id,
s.assessment_id, sub.id AS submission_id, sq.question_id,
COUNT(*) OVER (
PARTITION BY t.submission_creator_id, s.assessment_id, sq.question_id
) AS message_count
FROM live_feedback_messages m
INNER JOIN live_feedback_threads t ON m.thread_id = t.id
INNER JOIN course_assessment_submission_questions sq ON t.submission_question_id = sq.id
INNER JOIN course_assessment_submissions s ON sq.submission_id = s.id
-- `s.id` is the attempt's id (what the response's `submissionId` carries), which is a different
-- id space from the extension table's own serial `id`. Join to the extension via its `attempt_id`.
INNER JOIN course_assessment_submission_details sub ON sub.attempt_id = s.id
INNER JOIN course_assessments a ON s.assessment_id = a.id
INNER JOIN course_assessment_tabs tab ON a.tab_id = tab.id
INNER JOIN course_assessment_categories cat ON tab.category_id = cat.id
Expand Down Expand Up @@ -147,6 +150,8 @@ def correctness_hash
ON ca.tab_id = tab.id
INNER JOIN course_assessment_submissions cas
ON cas.assessment_id = ca.id
INNER JOIN course_assessment_submission_details cad
ON cad.attempt_id = cas.id
INNER JOIN course_assessment_answers caa
ON caa.submission_id = cas.id
INNER JOIN course_assessment_questions caq
Expand Down
25 changes: 19 additions & 6 deletions app/controllers/course/statistics/assessments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ def submission_statistics
includes(programming_questions: [:language]).
calculated(:maximum_grade, :question_count).
find(assessment_params[:id])
submissions = Course::Assessment::Submission.unscoped.
# `Course::Assessment::Submission` has no `assessment_id` column (it's Attempt-only, reached via
# the delegate), so querying it directly here raises `PG::UndefinedColumn`. `Attempt` carries
# `assessment_id`, `grade`, and `grader_ids` natively/as `calculated`, and this action's jbuilder
# only reads columns present on Attempt, so query Attempt directly.
submissions = Course::Assessment::Attempt.unscoped.
joins(:submission).
where(assessment_id: assessment_params[:id]).
calculated(:grade, :grader_ids)
@course_users_hash = preload_course_users_hash(current_course)
Expand All @@ -39,7 +44,10 @@ def ancestor_statistics
calculated(:maximum_grade).
find(assessment_params[:id])
authorize!(:read_ancestor, @assessment)
submissions = Course::Assessment::Submission.unscoped.
# Same `assessment_id`-column bug as `submission_statistics` above — `Attempt` is the
# drop-in replacement (see the comment there).
submissions = Course::Assessment::Attempt.unscoped.
joins(:submission).
preload(creator: :course_users).
where(assessment_id: assessment_params[:id]).
calculated(:grade)
Expand All @@ -52,8 +60,12 @@ def ancestor_statistics
def live_feedback_statistics
@assessment = Course::Assessment.unscoped.includes(:questions).
find(assessment_params[:id])
@submissions = Course::Assessment::Submission.unscoped.
select(:id, :creator_id, :workflow_state).
# id/creator_id/workflow_state all live on Attempt; this action only reads those three columns
# (unscoped + a narrow .select), so querying Attempt directly is equivalent (every course-member
# attempt has exactly one submission).
@submissions = Course::Assessment::Attempt.unscoped.
joins(:submission).
select('course_assessment_submissions.id', :creator_id, :workflow_state).
where(assessment_id: assessment_params[:id])

create_submission_question_id_hash(@assessment.questions)
Expand All @@ -64,7 +76,8 @@ def live_feedback_statistics

def live_feedback_history
user_id = CourseUser.joins(:user).where(id: params[:course_user_id]).pluck('users.id').first
@submissions = Course::Assessment::Submission.where(assessment_id: assessment_params[:id], creator_id: user_id)
@submissions = Course::Assessment::Attempt.joins(:submission).
where(assessment_id: assessment_params[:id], creator_id: user_id)
@question = Course::Assessment::Question.find(params[:question_id])

create_submission_question_id_hash([@question])
Expand Down Expand Up @@ -238,7 +251,7 @@ def feedback_messages_cte(student_ids, submission_question_ids)
def feedback_answers_cte
<<-SQL
SELECT
a.submission_id,
a.submission_id AS submission_id,
a.question_id,
a.created_at,
a.grade,
Expand Down
1 change: 1 addition & 0 deletions app/controllers/system/admin/get_help_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def fetch_system_get_help_data(start_date, end_date)
INNER JOIN live_feedback_threads t ON m.thread_id = t.id
INNER JOIN course_assessment_submission_questions sq ON t.submission_question_id = sq.id
INNER JOIN course_assessment_submissions s ON sq.submission_id = s.id
INNER JOIN course_assessment_submission_details sd ON sd.attempt_id = s.id
WHERE m.creator_id != #{User::SYSTEM_USER_ID}
AND m.created_at >= '#{start_date.utc.iso8601}'
AND m.created_at <= '#{end_date.utc.iso8601}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def fetch_instance_get_help_data(start_date, end_date)
INNER JOIN live_feedback_threads t ON m.thread_id = t.id
INNER JOIN course_assessment_submission_questions sq ON t.submission_question_id = sq.id
INNER JOIN course_assessment_submissions s ON sq.submission_id = s.id
INNER JOIN course_assessment_submission_details sd ON sd.attempt_id = s.id
INNER JOIN course_assessments a ON s.assessment_id = a.id
INNER JOIN course_assessment_tabs tab ON a.tab_id = tab.id
INNER JOIN course_assessment_categories cat ON tab.category_id = cat.id
Expand Down
8 changes: 6 additions & 2 deletions app/jobs/course/assessment/answer/base_auto_grading_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ def perform_tracked(answer, redirect_to_path = nil)
Course::Assessment::Answer::AutoGradingService.grade(answer)
end

if update_exp?(answer.submission)
Course::Assessment::Submission::CalculateExpService.update_exp(answer.submission)
# `answer.submission` is the Attempt base; EXP (awarder/awarded_at/points_awarded) lives on its
# Submission extension. Recompute against the extension (nil for a preview attempt, which has no
# EXP and must be skipped).
submission = answer.attempt.submission
if submission && update_exp?(submission)
Course::Assessment::Submission::CalculateExpService.update_exp(submission)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ def perform_tracked(assessment, submission_id, submitter)
instance = Course.unscoped { assessment.course.instance }

ActsAsTenant.with_tenant(instance) do
submission = Course::Assessment::Submission.find_by(id: submission_id)
# `submission_id` here is the id `create_force_submission_job` (Attempt#create_force_submission_job)
# scheduled with, which is `attempt.id` — this job's own param name predates the split and is
# not renamed here (renaming it is exactly the kind of cosmetic diff the repo's diff-hygiene
# rule forbids without a functional reason).
submission = Course::Assessment::Attempt.find_by(id: submission_id)&.submission
return unless submission

force_submit(submission, submitter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ def force_create_and_submit_submissions(assessment, user_ids, user_ids_without_s
# @param [Course::Assessment] assessment The assessment of which a submission is to be created.
# @param [CourseUser] course_user The course user whose submission is to be created.
def create_submission(assessment, course_user)
submission = assessment.submissions.new(creator: course_user.user, course_user: course_user)

assessment.submissions.new(creator: course_user.user)
submission = assessment.build_submission(creator: course_user.user, course_user: course_user)
success = assessment.create_new_submission(submission, course_user)

raise ActiveRecord::Rollback unless success
Expand Down
Loading