315 lines
11 KiB
Ruby
315 lines
11 KiB
Ruby
#
|
|
# 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 CourseSection < ActiveRecord::Base
|
|
include Workflow
|
|
|
|
belongs_to :course
|
|
belongs_to :nonxlist_course, :class_name => 'Course'
|
|
belongs_to :root_account, :class_name => 'Account'
|
|
belongs_to :enrollment_term
|
|
has_many :enrollments, -> { preload(:user).where("enrollments.workflow_state<>'deleted'") }, dependent: :destroy
|
|
has_many :all_enrollments, :class_name => 'Enrollment'
|
|
has_many :students, :through => :student_enrollments, :source => :user
|
|
has_many :student_enrollments, -> { where("enrollments.workflow_state NOT IN ('deleted', 'completed', 'rejected', 'inactive')").preload(:user) }, class_name: 'StudentEnrollment'
|
|
has_many :all_student_enrollments, -> { where("enrollments.workflow_state<>'deleted'").preload(:user) }, class_name: 'StudentEnrollment'
|
|
has_many :instructor_enrollments, -> { where(type: ['TaEnrollment', 'TeacherEnrollment']) }, class_name: 'Enrollment'
|
|
has_many :admin_enrollments, -> { where(type: ['TaEnrollment', 'TeacherEnrollment', 'DesignerEnrollment']) }, class_name: 'Enrollment'
|
|
has_many :users, :through => :enrollments
|
|
has_many :course_account_associations
|
|
has_many :calendar_events, :as => :context, :inverse_of => :context
|
|
has_many :assignment_overrides, :as => :set, :dependent => :destroy
|
|
|
|
before_validation :infer_defaults, :verify_unique_sis_source_id
|
|
validates_presence_of :course_id, :root_account_id, :workflow_state
|
|
validates_length_of :sis_source_id, :maximum => maximum_string_length, :allow_nil => true, :allow_blank => false
|
|
validates_length_of :name, :maximum => maximum_string_length, :allow_nil => false, :allow_blank => false
|
|
validate :validate_section_dates
|
|
|
|
has_many :sis_post_grades_statuses
|
|
|
|
before_save :maybe_touch_all_enrollments
|
|
after_save :update_account_associations_if_changed
|
|
after_save :delete_enrollments_later_if_deleted
|
|
after_save :update_enrollment_states_if_necessary
|
|
|
|
include StickySisFields
|
|
are_sis_sticky :course_id, :name, :start_at, :end_at, :restrict_enrollments_to_section_dates
|
|
|
|
def validate_section_dates
|
|
if start_at.present? && end_at.present? && end_at < start_at
|
|
self.errors.add(:end_at, t("End date cannot be before start date"))
|
|
false
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def maybe_touch_all_enrollments
|
|
self.touch_all_enrollments if self.start_at_changed? || self.end_at_changed? || self.restrict_enrollments_to_section_dates_changed? || self.course_id_changed?
|
|
end
|
|
|
|
def delete_enrollments_later_if_deleted
|
|
send_later_if_production(:delete_enrollments_if_deleted) if workflow_state == 'deleted' && workflow_state_changed?
|
|
end
|
|
|
|
def delete_enrollments_if_deleted
|
|
if workflow_state == 'deleted'
|
|
self.enrollments.active.find_each(&:destroy)
|
|
end
|
|
end
|
|
|
|
def participating_observers
|
|
User.observing_students_in_course(participating_students.map(&:id), course.id)
|
|
end
|
|
|
|
def participating_observers_by_date
|
|
User.observing_students_in_course(participating_students_by_date.map(&:id), course.id)
|
|
end
|
|
|
|
def participating_students
|
|
course.participating_students.where(:enrollments => { :course_section_id => self })
|
|
end
|
|
|
|
def participating_students_by_date
|
|
course.participating_students_by_date.where(:enrollments => { :course_section_id => self })
|
|
end
|
|
|
|
def participating_admins
|
|
course.participating_admins.where("enrollments.course_section_id = ? OR NOT COALESCE(enrollments.limit_privileges_to_course_section, ?)", self, false)
|
|
end
|
|
|
|
def participating_admins_by_date
|
|
course.participating_admins.where("enrollments.course_section_id = ? OR NOT COALESCE(enrollments.limit_privileges_to_course_section, ?)", self, false)
|
|
end
|
|
|
|
def participants(opts={})
|
|
ps = nil
|
|
if opts[:by_date]
|
|
ps = participating_students_by_date + participating_admins_by_date
|
|
ps += participating_observers_by_date if opts[:include_observers]
|
|
else
|
|
ps = participating_students + participating_admins
|
|
ps += participating_observers if opts[:include_observers]
|
|
end
|
|
ps
|
|
end
|
|
|
|
def available?
|
|
course.available?
|
|
end
|
|
|
|
def concluded?
|
|
now = Time.now
|
|
if self.end_at && self.restrict_enrollments_to_section_dates
|
|
self.end_at < now
|
|
else
|
|
self.course.concluded?
|
|
end
|
|
end
|
|
|
|
def touch_all_enrollments
|
|
return if new_record?
|
|
self.enrollments.touch_all
|
|
User.where(id: all_enrollments.select(:user_id)).
|
|
update_all(updated_at: Time.now.utc)
|
|
end
|
|
|
|
set_policy do
|
|
given { |user, session| self.course.grants_right?(user, session, :manage_sections) }
|
|
can :read and can :create and can :update and can :delete
|
|
|
|
given { |user, session| self.course.grants_any_right?(user, session, :manage_students, :manage_admin_users) }
|
|
can :read
|
|
|
|
given { |user| self.course.account_membership_allows(user, :read_roster) }
|
|
can :read
|
|
|
|
given { |user, session| self.course.grants_right?(user, session, :manage_calendar) }
|
|
can :manage_calendar
|
|
|
|
given { |user, session|
|
|
user &&
|
|
self.course.sections_visible_to(user).where(:id => self).exists? &&
|
|
self.course.grants_right?(user, session, :read_roster)
|
|
}
|
|
can :read
|
|
|
|
given { |user, session| self.course.grants_right?(user, session, :manage_grades) }
|
|
can :manage_grades
|
|
|
|
|
|
given { |user, session| self.course.grants_right?(user, session, :read_as_admin) }
|
|
can :read_as_admin
|
|
end
|
|
|
|
def update_account_associations_if_changed
|
|
if (self.course_id_changed? || self.nonxlist_course_id_changed?) && !Course.skip_updating_account_associations?
|
|
Course.send_later_if_production(:update_account_associations,
|
|
[self.course_id, self.course_id_was, self.nonxlist_course_id, self.nonxlist_course_id_was].compact.uniq)
|
|
end
|
|
end
|
|
|
|
def update_account_associations
|
|
Course.update_account_associations([self.course_id, self.nonxlist_course_id].compact)
|
|
end
|
|
|
|
def verify_unique_sis_source_id
|
|
return true unless self.sis_source_id
|
|
return true if !root_account_id_changed? && !sis_source_id_changed?
|
|
|
|
scope = root_account.course_sections.where(sis_source_id: self.sis_source_id)
|
|
scope = scope.where("id<>?", self) unless self.new_record?
|
|
|
|
return true unless scope.exists?
|
|
|
|
self.errors.add(:sis_source_id, t('sis_id_taken', "SIS ID \"%{sis_id}\" is already in use", :sis_id => self.sis_source_id))
|
|
throw :abort unless CANVAS_RAILS4_2
|
|
false
|
|
end
|
|
|
|
alias_method :parent_event_context, :course
|
|
|
|
def section_code
|
|
self.name
|
|
end
|
|
|
|
def infer_defaults
|
|
self.root_account_id ||= (self.course.root_account_id rescue nil) || Account.default.id
|
|
raise "Course required" unless self.course
|
|
self.root_account_id = self.course.root_account_id || Account.default.id
|
|
# This is messy, and I hate it.
|
|
# The SIS import actually gives us three names for a section
|
|
# and I don't know which one is best, or which one to show.
|
|
name_had_changed = name_changed?
|
|
# Here's the current plan:
|
|
# - otherwise, just use name
|
|
# - use the method display_name to consolidate this logic
|
|
self.name ||= self.course.name if self.default_section
|
|
self.name ||= "#{self.course.name} #{Time.zone.today}"
|
|
end
|
|
|
|
def defined_by_sis?
|
|
!!self.sis_source_id
|
|
end
|
|
|
|
# NOTE: Don't assume the section_name contains the course name
|
|
# it might include it if the SIS specifies, but you shouldn't
|
|
# assume that this method on its own will be enough for a user
|
|
# to recognize their course from a list
|
|
# The only place this is used by itself right now is when listing
|
|
# enrollments within a course
|
|
def display_name
|
|
@section_display_name ||= self.name
|
|
end
|
|
|
|
def move_to_course(course, *opts)
|
|
return self if self.course_id == course.id
|
|
old_course = self.course
|
|
self.course = course
|
|
self.root_account_id = course.root_account_id
|
|
self.default_section = (course.course_sections.active.size == 0)
|
|
old_course.course_sections.reset
|
|
course.course_sections.reset
|
|
assignment_overrides.active.destroy_all
|
|
user_ids = self.all_enrollments.map(&:user_id).uniq
|
|
|
|
all_attrs = { course_id: course.id }
|
|
if self.root_account_id_changed?
|
|
all_attrs[:root_account_id] = self.root_account_id
|
|
end
|
|
self.save!
|
|
self.all_enrollments.update_all all_attrs
|
|
Assignment.joins(:submissions)
|
|
.where(context: [old_course, self.course])
|
|
.where(submissions: { user_id: user_ids }).touch_all
|
|
EnrollmentState.send_later_if_production(:invalidate_states_for_course_or_section, self)
|
|
User.send_later_if_production(:update_account_associations, user_ids) if old_course.account_id != course.account_id && !User.skip_updating_account_associations?
|
|
if old_course.id != self.course_id && old_course.id != self.nonxlist_course_id
|
|
old_course.send_later_if_production(:update_account_associations) unless Course.skip_updating_account_associations?
|
|
end
|
|
if opts.include?(:run_jobs_immediately)
|
|
course.recompute_student_scores_without_send_later(user_ids)
|
|
else
|
|
course.recompute_student_scores(user_ids)
|
|
end
|
|
end
|
|
|
|
def crosslist_to_course(course, *opts)
|
|
return self if self.course_id == course.id
|
|
self.nonxlist_course_id ||= self.course_id
|
|
self.move_to_course(course, *opts)
|
|
end
|
|
|
|
def uncrosslist(*opts)
|
|
return unless self.nonxlist_course_id
|
|
if self.nonxlist_course.workflow_state == "deleted"
|
|
self.nonxlist_course.workflow_state = "claimed"
|
|
self.nonxlist_course.save!
|
|
end
|
|
nonxlist_course = self.nonxlist_course
|
|
self.nonxlist_course = nil
|
|
self.move_to_course(nonxlist_course, *opts)
|
|
end
|
|
|
|
def crosslisted?
|
|
return !!self.nonxlist_course_id
|
|
end
|
|
|
|
def destroy_course_if_no_more_sections
|
|
if self.deleted? && self.course.course_sections.active.empty?
|
|
self.course.destroy
|
|
end
|
|
end
|
|
|
|
def deletable?
|
|
!self.enrollments.where.not(:workflow_state => 'rejected').not_fake.exists?
|
|
end
|
|
|
|
def enroll_user(user, type, state='invited')
|
|
self.course.enroll_user(user, type, :enrollment_state => state, :section => self)
|
|
end
|
|
|
|
workflow do
|
|
state :active
|
|
state :deleted
|
|
end
|
|
|
|
alias_method :destroy_permanently!, :destroy
|
|
def destroy
|
|
self.workflow_state = 'deleted'
|
|
self.enrollments.not_fake.each(&:destroy)
|
|
self.assignment_overrides.each(&:destroy)
|
|
save!
|
|
end
|
|
|
|
scope :active, -> { where("course_sections.workflow_state<>'deleted'") }
|
|
|
|
scope :sis_sections, lambda { |account, *source_ids| where(:root_account_id => account, :sis_source_id => source_ids).order(:sis_source_id) }
|
|
|
|
def common_to_users?(users)
|
|
users.all?{ |user| self.student_enrollments.active.for_user(user).count > 0 }
|
|
end
|
|
|
|
def update_enrollment_states_if_necessary
|
|
if self.restrict_enrollments_to_section_dates_changed? || (self.restrict_enrollments_to_section_dates? && (changes.keys & %w{start_at end_at}).any?)
|
|
EnrollmentState.send_later_if_production(:invalidate_states_for_course_or_section, self)
|
|
end
|
|
end
|
|
end
|