canvas-lms/app/models/student_enrollment.rb

170 lines
6.5 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class StudentEnrollment < Enrollment
belongs_to :student, foreign_key: :user_id, class_name: "User"
has_many :course_paces, through: :student
after_save :evaluate_modules, if: proc { |e|
# if enrollment switches sections or is created
e.saved_change_to_course_section_id? || e.saved_change_to_course_id? ||
# or if an enrollment is deleted and they are in another section of the course
(e.saved_change_to_workflow_state? && e.workflow_state == "deleted" &&
e.user.enrollments.where.not(id: e.id).active.where(course_id: e.course_id).exists?)
}
after_save :restore_submissions_and_scores
after_save :republish_course_pace_if_needed
after_save :republish_base_pace_if_needed
def student?
true
end
def evaluate_modules
ContextModuleProgression.for_user(user_id).for_course(course_id).each(&:mark_as_outdated!)
end
def update_override_score(override_score:, grading_period_id: nil, updating_user:, record_grade_change: true)
score_params = { grading_period_id: } if grading_period_id.present?
score = find_score(score_params)
raise ActiveRecord::RecordNotFound if score.blank?
old_score = score[:override_score]
old_grade = score.override_grade
score.update!(override_score:)
return score unless score.saved_change_to_override_score?
Canvas::LiveEvents.grade_override(score, old_score, self, course)
if record_grade_change && updating_user.present?
override_grade_change = Auditors::GradeChange::OverrideGradeChange.new(
grader: updating_user,
old_grade:,
old_score:,
score:
)
Auditors::GradeChange.record(override_grade_change:)
end
score
end
def update_override_status(custom_grade_status:, grading_period_id: nil)
score_params = { grading_period_id: } if grading_period_id.present?
score = find_score(score_params)
score.update!(custom_grade_status:)
end
class << self
def restore_submissions_and_scores_for_enrollments(enrollments)
raise ArgumentError, "Cannot call with more than 1000 enrollments" if enrollments.count > 1_000
restore_deleted_submissions_for_enrollments(enrollments)
restore_deleted_scores_for_enrollments(enrollments)
end
def restore_deleted_submissions_for_enrollments(student_enrollments)
raise ArgumentError, "Cannot call with more than 1000 enrollments" if student_enrollments.count > 1_000
student_enrollments.group_by(&:course_id).each do |course_id, students|
Submission
.joins(:assignment)
.where(user_id: students.map(&:user_id), workflow_state: "deleted", assignments: { context_id: course_id })
.merge(Assignment.active)
.in_batches
.update_all("workflow_state = #{SubmissionLifecycleManager.infer_submission_workflow_state_sql}")
end
end
def restore_deleted_scores_for_enrollments(student_enrollments)
raise ArgumentError, "Cannot call with more than 1000 enrollments" if student_enrollments.count > 1_000
student_enrollments.group_by(&:course_id).each_value do |students|
course = students.first.course
assignment_groups = course.assignment_groups.active.except(:order)
grading_periods = GradingPeriod.for(course)
Score.where(course_score: true).or(
Score.where(assignment_group: assignment_groups)
).or(
Score.where(grading_period: grading_periods)
).where(enrollment_id: students.map(&:id), workflow_state: "deleted")
.update_all(workflow_state: "active")
end
end
end
private
def restore_submissions_and_scores
return unless being_restored?(to_state: "completed")
# running in an n_strand to handle situations where a SIS import could
# update a ton of enrollments from "deleted" to "completed".
delay_if_production(n_strand: "Enrollment#restore_submissions_and_scores#{root_account.global_id}",
priority: Delayed::LOW_PRIORITY)
.restore_submissions_and_scores_now
end
def restore_submissions_and_scores_now
restore_deleted_submissions
restore_deleted_scores
end
def restore_deleted_submissions
StudentEnrollment.restore_deleted_submissions_for_enrollments([self])
end
def restore_deleted_scores
StudentEnrollment.restore_deleted_scores_for_enrollments([self])
end
def republish_course_pace_if_needed
return unless saved_change_to_id? || saved_change_to_start_at? || (saved_change_to_workflow_state? && workflow_state != "deleted")
return unless course.enable_course_paces?
pace = course.course_paces.published.where(course_section_id:).last
pace ||= course.course_paces.published.for_user(user).take || course.course_paces.published.primary.take
pace&.create_publish_progress
track_multiple_section_paces
end
def republish_base_pace_if_needed
return unless course.enable_course_paces? && course_section_id && workflow_state == "deleted"
student_section_ids = user.enrollments.where(course:).where.not(workflow_state: "deleted").pluck(:course_section_id)
pace = course.course_paces.published.where(course_section_id: student_section_ids).last
pace ||= course.course_paces.published.primary.take
pace&.create_publish_progress
end
def track_multiple_section_paces
section_ids_the_student_is_enrolled_in = user.student_enrollments.where.not(workflow_state: "deleted")
.where(course_section: course.course_sections.pluck(:id))
.pluck(:course_section_id)
if section_ids_the_student_is_enrolled_in.count > 1 && course.course_paces.published.for_section(section_ids_the_student_is_enrolled_in).size > 1
InstStatsd::Statsd.increment("course_pacing.student_with_multiple_sections_with_paces")
end
end
end