app/controllers/course/assessment/submission/submissions_controller.rb
# frozen_string_literal: true
class Course::Assessment::Submission::SubmissionsController < # rubocop:disable Metrics/ClassLength
Course::Assessment::Submission::Controller
include Course::Assessment::Submission::SubmissionsControllerServiceConcern
include Signals::EmissionConcern
include Course::Assessment::Submission::MonitoringConcern
include Course::Assessment::SubmissionConcern
include Course::Assessment::Submission::KoditsuSubmissionsConcern
before_action :authorize_assessment!, only: :create
skip_authorize_resource :submission, only: [:edit, :update, :auto_grade]
before_action :authorize_submission!, only: [:edit, :update]
before_action :check_password, only: [:edit, :update]
before_action :load_or_create_answers, only: [:edit, :update]
before_action :check_zombie_jobs, only: [:edit, :update]
# Questions may be added to assessments with existing submissions.
# In these cases, new submission_questions must be created when the submission is next
# edited or updated.
before_action :load_or_create_submission_questions, only: [:edit, :update]
signals :assessment_submissions, after: [:unsubmit, :delete]
signals :assessment_submissions, after: [:update], if: -> { @submission.saved_change_to_workflow_state? }
delegate_to_service(:update)
delegate_to_service(:load_or_create_answers)
delegate_to_service(:load_or_create_submission_questions)
COURSE_USERS = { my_students: 'my_students',
my_students_w_phantom: 'my_students_w_phantom',
students: 'students',
students_w_phantom: 'students_w_phantom',
staff: 'staff',
staff_w_phantom: 'staff_w_phantom' }.freeze
def index
authorize!(:view_all_submissions, @assessment)
@assessment = @assessment.calculated(:maximum_grade)
@submissions = @submissions.calculated(:log_count, :graded_at, :grade, :grader_ids)
@my_students = current_course_user&.my_students || []
@course_users = current_course.course_users.order_phantom_user.order_alphabetically
end
def create # rubocop:disable Metrics/AbcSize
authorize! :access, @assessment
existing_submission = @assessment.submissions.find_by(creator: current_user)
create_success_response(existing_submission) and return if existing_submission
ActiveRecord::Base.transaction do
@submission.session_id = authentication_service.generate_authentication_token
success = @assessment.create_new_submission(@submission, current_user)
raise ActiveRecord::Rollback unless success
authentication_service.save_token_to_redis(@submission.session_id)
log_service.log_submission_access(request) if @assessment.session_password_protected?
monitoring_service&.create_new_session_if_not_exist! if should_monitor?
create_success_response(@submission)
end
rescue StandardError
error_message = @submission.errors.full_messages.to_sentence
render json: { error: error_message }, status: :bad_request
end
def edit
return render json: { isSubmissionBlocked: true } if @submission.submission_view_blocked?(current_course_user)
@monitoring_session_id = monitoring_service&.session&.id if should_monitor?
@submission = @submission.calculated(:graded_at, :grade) unless @submission.attempting?
end
def auto_grade
authorize!(:grade, @submission)
job = @submission.auto_grade!
render partial: 'jobs/submitted', locals: { job: job.job }
end
def reevaluate_answer
@answer = @submission.answers.find_by(id: reload_answer_params[:answer_id])
return head :bad_request if @answer.nil?
job = @answer.auto_grade!(redirect_to_path: nil, reduce_priority: true)
render partial: 'jobs/submitted', locals: { job: job.job }
end
def generate_feedback
@answer = @submission.answers.find_by(id: reload_answer_params[:answer_id])
return head :bad_request if @answer.nil?
job = @answer.generate_feedback
render partial: 'jobs/submitted', locals: { job: job }
end
def generate_live_feedback
@answer = @submission.answers.find_by(id: reload_answer_params[:answer_id])
return head :bad_request if @answer.nil?
response_status, response_body = @answer.generate_live_feedback
response_body['feedbackUrl'] = ENV.fetch('CODAVERI_URL')
live_feedback = Course::Assessment::LiveFeedback.create_with_codes(
@submission.assessment_id,
@answer.question_id,
@submission.creator,
response_body['transactionId'],
@answer.actable.files
)
if response_status == 200
params[:live_feedback_id] = live_feedback.id
params[:feedback_files] = response_body['data']['feedbackFiles']
save_live_feedback
end
response_body['liveFeedbackId'] = live_feedback.id
render json: response_body, status: response_status
end
# Reload the current answer or reset it, depending on parameters.
# current_answer has the most recent copy of the answer.
def reload_answer
@answer = @submission.answers.find_by(id: reload_answer_params[:answer_id])
if @answer.nil?
head :bad_request
return
elsif reload_answer_params[:reset_answer]
@new_answer = @answer.reset_answer
else
@new_answer = @answer
end
render @new_answer
end
# Publish all the graded submissions.
def publish_all
authorize!(:publish_grades, @assessment)
graded_submission_ids = @assessment.submissions.with_graded_state.by_users(course_user_ids).pluck(:id)
if graded_submission_ids.empty?
head :ok
else
job = Course::Assessment::Submission::PublishingJob.
perform_later(graded_submission_ids, @assessment, current_user).job
render partial: 'jobs/submitted', locals: { job: job }
end
end
# Force submit all submissions.
def force_submit_all
authorize!(:force_submit_assessment_submission, @assessment)
attempting_submissions = @assessment.submissions.by_users(course_user_ids).with_attempting_state
if !attempting_submissions.empty? || !user_ids_without_submission.empty?
job = Course::Assessment::Submission::ForceSubmittingJob.
perform_later(@assessment, course_user_ids.pluck(:user_id), user_ids_without_submission, current_user).job
render partial: 'jobs/submitted', locals: { job: job }
else
head :ok
end
end
def fetch_submissions_from_koditsu
authorize!(:fetch_submissions_from_koditsu, @assessment)
is_course_koditsu_enabled = current_course.component_enabled?(Course::KoditsuPlatformComponent)
is_assessment_koditsu_enabled = @assessment.koditsu_assessment_id && @assessment.is_koditsu_enabled
is_koditsu_enabled = is_course_koditsu_enabled && is_assessment_koditsu_enabled
fetch_all_submissions_from_koditsu(@assessment, current_user) if is_koditsu_enabled
head :ok
end
# Download either all of or a subset of submissions for an assessment.
def download_all
authorize!(:manage, @assessment)
if not_downloadable
head :bad_request
else
render partial: 'jobs/submitted', locals: { job: download_job }
end
end
def download_statistics
authorize!(:manage, @assessment)
submission_ids = @assessment.submissions.by_users(course_user_ids).pluck(:id)
if submission_ids.empty?
return render json: {
error: I18n.t('course.assessment.submission.submissions.download_statistics.no_submission_statistics')
}, status: :bad_request
end
job = Course::Assessment::Submission::StatisticsDownloadJob.
perform_later(current_course, current_user, submission_ids).job
render partial: 'jobs/submitted', locals: { job: job }
end
def unsubmit
authorize!(:update, @assessment)
@submission = @assessment.submissions.find(params[:submission_id])
success = @submission.transaction do
@submission.update!('unmark' => 'true') if @submission.graded?
@submission.update!('unsubmit' => 'true')
monitoring_service&.continue_listening!
true
end
if success
head :ok
else
logger.error("Failed to unsubmit submission: #{@submission.errors.inspect}")
render json: { errors: @submission.errors }, status: :bad_request
end
end
def unsubmit_all
authorize!(:update, @assessment)
submission_ids = @assessment.submissions.by_users(course_user_ids).pluck(:id)
return head :ok if submission_ids.empty?
job = Course::Assessment::Submission::UnsubmittingJob.
perform_later(current_user, submission_ids, @assessment, nil).job
render partial: 'jobs/submitted', locals: { job: job }
end
def delete
@submission = @assessment.submissions.find(params[:submission_id])
authorize!(:delete_submission, @submission)
ActiveRecord::Base.transaction do
reset_question_bundle_assignments if @assessment.randomization == 'prepared'
monitoring_service&.stop!
@submission.destroy!
head :ok
end
rescue StandardError
logger.error("Failed to delete submission: #{@submission.errors.inspect}")
render json: { errors: @submission.errors }, status: :bad_request
end
def reset_question_bundle_assignments
qbas = @assessment.question_bundle_assignments.where(submission: @submission).lock!
raise ActiveRecord::Rollback unless qbas.update_all(submission_id: nil)
end
def delete_all
authorize!(:delete_all_submissions, @assessment)
submission_ids = @assessment.submissions.by_users(course_user_ids).pluck(:id)
return head :ok if submission_ids.empty?
job = Course::Assessment::Submission::DeletingJob.
perform_later(current_user, submission_ids, @assessment).job
render partial: 'jobs/submitted', locals: { job: job }
end
private
def create_params
{ course_user: current_course_user }
end
def create_success_response(submission)
is_course_koditsu_enabled = current_course.component_enabled?(Course::KoditsuPlatformComponent)
is_assessment_koditsu_enabled = @assessment.koditsu_assessment_id && @assessment.is_koditsu_enabled
if is_course_koditsu_enabled && is_assessment_koditsu_enabled
submission.create_new_answers
redirect_url = KoditsuAsyncApiService.assessment_url(@assessment.koditsu_assessment_id)
else
redirect_url = edit_course_assessment_submission_path(current_course, @assessment, submission)
end
render json: { redirectUrl: redirect_url }
end
def authorize_assessment!
authorize!(:attempt, @assessment)
end
def reload_answer_params
params.permit(:answer_id, :reset_answer)
end
def not_downloadable
@assessment.submissions.confirmed.empty? ||
(params[:download_format] == 'zip' && !@assessment.files_downloadable?) ||
(params[:download_format] == 'csv' && !@assessment.csv_downloadable?)
end
def download_job
if params[:download_format] == 'csv'
Course::Assessment::Submission::CsvDownloadJob.
perform_later(current_course_user, @assessment, params[:course_users]).job
else
Course::Assessment::Submission::ZipDownloadJob.
perform_later(current_course_user, @assessment, params[:course_users]).job
end
end
# Check for zombie jobs, create new grading jobs if there's any zombie jobs.
# TODO: Remove this method after found the cause of the dead jobs.
def check_zombie_jobs # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
return unless @submission.attempting? || @submission.submitted?
submitted_answers = @submission.answers.where(workflow_state: 'submitted')
return if submitted_answers.empty?
dead_answers = submitted_answers.select do |a|
job = a.auto_grading&.job
job&.submitted? && !job.in_queue?
end
dead_answers.each do |a|
old_job = a.auto_grading.job
job = a.auto_grade!(redirect_to_path: old_job.redirect_to, reduce_priority: true)
logger.debug(message: 'Restart Answer Grading', answer_id: a.id, job_id: job.job.id,
old_job_id: old_job.id)
end
end
def course_user_ids # rubocop:disable Metrics/AbcSize
@course_user_ids ||= case params[:course_users]
when COURSE_USERS[:my_students]
current_course_user.my_students.without_phantom_users
when COURSE_USERS[:my_students_w_phantom]
current_course_user.my_students
when COURSE_USERS[:students_w_phantom]
@assessment.course.course_users.students
when COURSE_USERS[:staff]
@assessment.course.course_users.staff.without_phantom_users
when COURSE_USERS[:staff_w_phantom]
@assessment.course.course_users.staff
else
@assessment.course.course_users.students.without_phantom_users
end.select(:user_id)
end
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)
course_user_ids.pluck(:user_id) - user_ids_with_submission
end
end