canvas-lms/app/models/enrollment.rb

1156 lines
38 KiB
Ruby

#
# Copyright (C) 2011 - 2015 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/>.
#
require 'atom'
class Enrollment < ActiveRecord::Base
SIS_TYPES = {
'TeacherEnrollment' => 'teacher',
'TaEnrollment' => 'ta',
'DesignerEnrollment' => 'designer',
'StudentEnrollment' => 'student',
'ObserverEnrollment' => 'observer'
}
include Workflow
belongs_to :course, :touch => true, :inverse_of => :enrollments
belongs_to :course_section
belongs_to :root_account, :class_name => 'Account'
belongs_to :user
belongs_to :associated_user, :class_name => 'User'
belongs_to :role
include Role::AssociationHelper
has_many :role_overrides, :as => :context
has_many :pseudonyms, :primary_key => :user_id, :foreign_key => :user_id
has_many :course_account_associations, :foreign_key => 'course_id', :primary_key => 'course_id'
has_many :grading_period_grades, dependent: :destroy
validates_presence_of :user_id, :course_id, :type, :root_account_id, :course_section_id, :workflow_state, :role_id
validates_inclusion_of :limit_privileges_to_course_section, :in => [true, false]
validates_inclusion_of :associated_user_id, :in => [nil],
:unless => lambda { |enrollment| enrollment.type == 'ObserverEnrollment' },
:message => "only ObserverEnrollments may have an associated_user_id"
validate :cant_observe_self, :if => lambda { |enrollment| enrollment.type == 'ObserverEnrollment' }
validate :valid_role?
before_save :assign_uuid
before_validation :assert_section
after_save :update_user_account_associations_if_necessary
before_save :audit_groups_for_deleted_enrollments
before_validation :ensure_role_id
before_validation :infer_privileges
after_create :create_linked_enrollments
after_save :clear_email_caches
after_save :cancel_future_appointments
after_save :update_linked_enrollments
after_save :update_cached_due_dates
after_save :touch_graders_if_needed
after_save :reset_notifications_cache
attr_accessor :already_enrolled, :available_at, :soft_completed_at
attr_accessible :user, :course, :workflow_state, :course_section, :limit_privileges_to_course_section, :already_enrolled, :start_at, :end_at
scope :current, -> { joins(:course).where(QueryBuilder.new(:active).conditions).readonly(false) }
scope :current_and_invited, -> { joins(:course).where(QueryBuilder.new(:current_and_invited).conditions).readonly(false) }
scope :current_and_future, -> { joins(:course).where(QueryBuilder.new(:current_and_future).conditions).readonly(false) }
scope :concluded, -> { joins(:course).where(QueryBuilder.new(:completed).conditions).readonly(false) }
scope :current_and_concluded, -> { joins(:course).where(QueryBuilder.new(:current_and_concluded).conditions).readonly(false) }
def ensure_role_id
self.role_id ||= self.role.id
end
def cant_observe_self
self.errors.add(:associated_user_id, "Cannot observe yourself") if self.user_id == self.associated_user_id
end
def valid_role?
return true if role.built_in?
unless self.role.base_role_type == self.type
self.errors.add(:role_id, "is not valid for the enrollment type")
end
unless self.course.account.valid_role?(role)
self.errors.add(:role_id, "is not an available role for this course's account")
end
end
def self.get_built_in_role_for_type(enrollment_type)
role = Role.get_built_in_role("StudentEnrollment") if enrollment_type == "StudentViewEnrollment"
role ||= Role.get_built_in_role(enrollment_type)
role
end
def default_role
Enrollment.get_built_in_role_for_type(self.type)
end
# see #active_student?
def self.active_student_conditions
"(enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') AND enrollments.workflow_state = 'active')"
end
# see .active_student_conditions
def active_student?(was = false)
suffix = was ? "_was" : ""
%w[StudentEnrollment StudentViewEnrollment].include?(send("type#{suffix}")) &&
send("workflow_state#{suffix}") == "active"
end
def active_student_changed?
active_student? != active_student?(:was)
end
def adjust_needs_grading_count(mode = :increment)
amount = mode == :increment ? 1 : -1
Assignment.
where(context_id: course_id, context_type: 'Course').
where("EXISTS (?) AND NOT EXISTS (?)",
Submission.where(user_id: user_id).
where("assignment_id=assignments.id").
where(Submission.needs_grading_conditions),
Enrollment.where(Enrollment.active_student_conditions).
where(user_id: user_id, course_id: course_id).
where("id<>?", self)).
update_all(["needs_grading_count=needs_grading_count+?, updated_at=?", amount, Time.now.utc])
end
after_create :update_needs_grading_count, if: :active_student?
after_update :update_needs_grading_count, if: :active_student_changed?
def update_needs_grading_count
self.class.connection.after_transaction_commit do
adjust_needs_grading_count(active_student? ? :increment : :decrement)
end
end
include StickySisFields
are_sis_sticky :start_at, :end_at
has_a_broadcast_policy
set_broadcast_policy do |p|
p.dispatch :enrollment_invitation
p.to { self.user }
p.whenever { |record|
!record.self_enrolled &&
record.course &&
record.user.registered? &&
!record.observer? &&
((record.just_created && record.invited?) || record.changed_state(:invited) || @re_send_confirmation)
}
p.dispatch :enrollment_registration
p.to { self.user.communication_channel }
p.whenever { |record|
!record.self_enrolled &&
record.course &&
!record.user.registered? &&
!record.observer? &&
((record.just_created && record.invited?) || record.changed_state(:invited) || @re_send_confirmation)
}
p.dispatch :enrollment_notification
p.to { self.user }
p.whenever { |record|
!record.self_enrolled &&
record.course &&
!record.course.created? &&
!record.observer? &&
record.just_created && record.active?
}
p.dispatch :enrollment_accepted
p.to {self.course.admins - [self.user] }
p.whenever { |record|
record.course &&
!record.observer? &&
!record.just_created && (record.changed_state(:active, :invited) || record.changed_state(:active, :creation_pending))
}
end
scope :active, -> { where("enrollments.workflow_state<>'deleted'") }
scope :admin, -> {
select(:course_id).
joins(:course).
where("enrollments.type IN ('TeacherEnrollment','TaEnrollment', 'DesignerEnrollment') AND (courses.workflow_state='claimed' OR (enrollments.workflow_state='active' AND courses.workflow_state='available'))") }
scope :instructor, -> {
select(:course_id).
joins(:course).
where("enrollments.type IN ('TeacherEnrollment','TaEnrollment') AND (courses.workflow_state='claimed' OR (enrollments.workflow_state='active' AND courses.workflow_state='available'))") }
scope :of_admin_type, -> { where(:type => ['TeacherEnrollment','TaEnrollment', 'DesignerEnrollment']) }
scope :of_instructor_type, -> { where(:type => ['TeacherEnrollment', 'TaEnrollment']) }
scope :of_content_admins, -> { where(:type => ['TeacherEnrollment', 'DesignerEnrollment']) }
scope :student, -> {
select(:course_id).
joins(:course).
where(:type => 'StudentEnrollment', :workflow_state => 'active', :courses => { :workflow_state => 'available' }) }
scope :student_in_claimed_or_available, -> {
select(:course_id).
joins(:course).
where(:type => 'StudentEnrollment', :workflow_state => 'active', :courses => { :workflow_state => ['available', 'claimed'] }) }
scope :all_student, -> {
eager_load(:course).
where("(enrollments.type = 'StudentEnrollment'
AND enrollments.workflow_state IN ('invited', 'active', 'completed')
AND courses.workflow_state IN ('available', 'completed')) OR
(enrollments.type = 'StudentViewEnrollment'
AND enrollments.workflow_state = 'active'
AND courses.workflow_state != 'deleted')") }
scope :not_deleted, -> {
joins(:course).
where("(courses.workflow_state<>'deleted') AND (enrollments.workflow_state<>'deleted')")
}
scope :not_fake, -> { where("enrollments.type<>'StudentViewEnrollment'") }
def self.readable_types
# with enough use, even translations can add up
RequestCache.cache('enrollment_readable_types') do
{
'TeacherEnrollment' => t('#enrollment.roles.teacher', "Teacher"),
'TaEnrollment' => t('#enrollment.roles.ta', "TA"),
'DesignerEnrollment' => t('#enrollment.roles.designer', "Designer"),
'StudentEnrollment' => t('#enrollment.roles.student', "Student"),
'StudentViewEnrollment' => t('#enrollment.roles.student', "Student"),
'ObserverEnrollment' => t('#enrollment.roles.observer', "Observer")
}
end
end
def self.readable_type(type)
readable_types[type] || readable_types['StudentEnrollment']
end
def self.sis_type(type)
SIS_TYPES[type] || SIS_TYPES['StudentEnrollment']
end
def sis_type
Enrollment.sis_type(self.type)
end
def sis_role
(!self.role.built_in? && self.role.name) || Enrollment.sis_type(self.type)
end
def self.valid_types
SIS_TYPES.keys
end
def self.valid_type?(type)
SIS_TYPES.has_key?(type)
end
def self.types_with_indefinite_article
{
'TeacherEnrollment' => t('#enrollment.roles.teacher_with_indefinite_article', "A Teacher"),
'TaEnrollment' => t('#enrollment.roles.ta_with_indefinite_article', "A TA"),
'DesignerEnrollment' => t('#enrollment.roles.designer_with_indefinite_article', "A Designer"),
'StudentEnrollment' => t('#enrollment.roles.student_with_indefinite_article', "A Student"),
'StudentViewEnrollment' => t('#enrollment.roles.student_with_indefinite_article', "A Student"),
'ObserverEnrollment' => t('#enrollment.roles.observer_with_indefinite_article', "An Observer")
}
end
def self.type_with_indefinite_article(type)
types_with_indefinite_article[type] || types_with_indefinite_article['StudentEnrollment']
end
def reload(options = nil)
@enrollment_dates = nil
super
end
def should_update_user_account_association?
self.new_record? || self.course_id_changed? || self.course_section_id_changed? || self.root_account_id_changed?
end
def update_user_account_associations_if_necessary
return if self.fake_student?
if id_was.nil?
return if %w{creation_pending deleted}.include?(self.user.workflow_state)
associations = User.calculate_account_associations_from_accounts([self.course.account_id, self.course_section.course.account_id, self.course_section.nonxlist_course.try(:account_id)].compact.uniq)
self.user.update_account_associations(:incremental => true, :precalculated_associations => associations)
elsif should_update_user_account_association?
self.user.update_account_associations_later
end
end
protected :update_user_account_associations_if_necessary
def other_section_enrollment_count
# The number of other active sessions that the user is enrolled in.
self.course.student_enrollments.active.for_user(self.user).where("id != ?", self.id).count
end
def audit_groups_for_deleted_enrollments
# did the student cease to be enrolled in a non-deleted state in a section?
had_section = self.course_section_id_was.present?
was_active = (self.workflow_state_was != 'deleted')
return unless had_section && was_active &&
(self.course_section_id_changed? || self.workflow_state == 'deleted')
# what section the user is abandoning, and the section they're moving to
# (if it's in the same course and the enrollment's not deleted)
section = CourseSection.find(self.course_section_id_was)
# ok, consider groups the user is in from the abandoned section's course
self.user.groups.preload(:group_category).where(
:context_type => 'Course', :context_id => section.course_id).each do |group|
# check group deletion criteria if either enrollment is not a deletion
# or it may be a deletion/unenrollment from a section but not from the course as a whole (still enrolled in another section)
if self.workflow_state != 'deleted' || other_section_enrollment_count > 0
# don't bother unless the group's category has section restrictions
next unless group.group_category && group.group_category.restricted_self_signup?
# skip if the user is the only user in the group. there's no one to have
# a conflicting section.
next if group.users.count == 1
# check if the group has the section the user is abandoning as a common
# section (from CourseSection#common_to_users? view, the enrollment is
# still there since it queries the db directly and we haven't saved yet);
# if not, dropping the section is not necessary
next unless section.common_to_users?(group.users)
end
# at this point, the group is restricted, there's more than one user and
# it appears that the group is common to the section being left by the user so
# remove the user from the group. Or the student was only enrolled in one section and
# by leaving the section he/she is completely leaving the course so remove the
# user from any group related to the course.
membership = group.group_memberships.where(user_id: self.user_id).first
membership.destroy if membership
end
end
protected :audit_groups_for_deleted_enrollments
def observers
student? ? user.observers.active : []
end
def create_linked_enrollments
observers.each do |observer|
create_linked_enrollment_for(observer)
end
end
def update_linked_enrollments
observers.each do |observer|
if enrollment = active_linked_enrollment_for(observer)
enrollment.update_from(self)
end
end
end
def create_linked_enrollment_for(observer)
# we don't want to create a new observer enrollment if one exists
enrollment = linked_enrollment_for(observer)
return true if enrollment && !enrollment.deleted?
return false unless observer.can_be_enrolled_in_course?(course)
enrollment ||= observer.observer_enrollments.build
enrollment.associated_user_id = user_id
enrollment.shard = shard if enrollment.new_record?
enrollment.update_from(self, !!@skip_broadcasts)
end
def linked_enrollment_for(observer)
observer.observer_enrollments.where(
:associated_user_id => user_id,
:course_id => course_id,
:course_section_id => course_section_id_was || course_section_id).
shard(Shard.shard_for(course_id)).first
end
def active_linked_enrollment_for(observer)
enrollment = linked_enrollment_for(observer)
# we don't want to "undelete" observer enrollments that have been
# explicitly deleted
return nil if enrollment && enrollment.deleted? && workflow_state_was != 'deleted'
enrollment
end
def update_cached_due_dates
if workflow_state_changed? && course
DueDateCacher.recompute_course(course)
end
end
def update_from(other, skip_broadcasts=false)
self.course_id = other.course_id
if self.type == 'ObserverEnrollment' && other.workflow_state == 'invited'
self.workflow_state = 'active'
else
self.workflow_state = other.workflow_state
end
self.start_at = other.start_at
self.end_at = other.end_at
self.course_section_id = other.course_section_id
self.root_account_id = other.root_account_id
self.user.touch if workflow_state_changed?
if skip_broadcasts
save_without_broadcasting!
else
save!
end
end
def clear_email_caches
if self.workflow_state_changed? && (self.workflow_state_was == 'invited' || self.workflow_state == 'invited')
if Enrollment.cross_shard_invitations?
Shard.birth.activate do
self.user.communication_channels.email.unretired.each { |cc| Rails.cache.delete([cc.path, 'all_invited_enrollments2'].cache_key)}
end
else
self.user.communication_channels.email.unretired.each { |cc| Rails.cache.delete([cc.path, 'invited_enrollments2'].cache_key)}
end
end
end
def cancel_future_appointments
if workflow_state_changed? && completed?
course.appointment_participants.active.current.for_context_codes(user.asset_string).update_all(:workflow_state => 'deleted')
end
end
def conclude
self.workflow_state = "completed"
self.completed_at = Time.now
self.user.touch
self.save
end
def unconclude
self.workflow_state = 'active'
self.completed_at = nil
self.user.touch
self.save
end
def inactivate
self.workflow_state = "inactive"
self.user.touch
self.save
end
def reactivate
self.workflow_state = "active"
self.user.touch
self.save
end
def defined_by_sis?
!!self.sis_source_id
end
def assigned_observer?
self.observer? && self.associated_user_id
end
def participating?
self.state_based_on_date == :active
end
def participating_student?
self.student? && self.participating?
end
def participating_observer?
self.observer? && self.participating?
end
def participating_teacher?
self.teacher? && self.participating?
end
def participating_ta?
self.ta? && self.participating?
end
def participating_instructor?
self.instructor? && self.participating?
end
def participating_designer?
self.designer? && self.participating?
end
def participating_admin?
self.admin? && self.participating?
end
def participating_content_admin?
self.content_admin? && self.participating?
end
def associated_user_name
self.associated_user && self.associated_user.short_name
end
def assert_section
self.course_section = self.course.default_section if !self.course_section_id && self.course
self.root_account_id ||= self.course.root_account_id rescue nil
end
def infer_privileges
self.limit_privileges_to_course_section = false if self.limit_privileges_to_course_section.nil?
true
end
def course_name
self.course.nickname_for(self.user) || t('#enrollment.default_course_name', "Course")
end
def short_name(length=nil)
return @short_name if @short_name
@short_name = self.course_section.display_name if self.course_section && self.root_account && self.root_account.show_section_name_as_course_name
@short_name ||= self.course_name
@short_name = @short_name[0..length] if length
@short_name
end
def long_name
return @long_name if @long_name
@long_name = self.course_name
@long_name = t('#enrollment.with_section', "%{course_name}, %{section_name}", :course_name => @long_name, :section_name => self.course_section.display_name) if self.course_section && self.course_section.display_name && self.course_section.display_name != self.course_name
@long_name
end
TYPE_RANKS = {
:default => ['TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentEnrollment','StudentViewEnrollment','ObserverEnrollment'],
:student => ['StudentEnrollment','TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment','ObserverEnrollment']
}
TYPE_RANK_HASHES = Hash[TYPE_RANKS.map{ |k, v| [k, rank_hash(v)] }]
def self.type_rank_sql(order = :default)
# don't call rank_sql during class load
rank_sql(TYPE_RANKS[order], 'enrollments.type')
end
def rank_sortable(order = :default)
TYPE_RANK_HASHES[order][self.class.to_s]
end
STATE_RANK = ['active', ['invited', 'creation_pending'], 'inactive', 'completed', 'rejected', 'deleted']
STATE_RANK_HASH = rank_hash(STATE_RANK)
def self.state_rank_sql
# don't call rank_sql during class load
@state_rank_sql ||= rank_sql(STATE_RANK, 'enrollments.workflow_state')
end
def state_sortable
STATE_RANK_HASH[state.to_s]
end
def state_with_date_sortable
STATE_RANK_HASH[state_based_on_date.to_s]
end
def accept!
res = accept
raise "can't accept" unless res
res
end
def accept(force = false)
return false unless force || invited?
ids = self.user.dashboard_messages.where(:context_id => self, :context_type => 'Enrollment').pluck(:id) if self.user
Message.where(:id => ids).delete_all if ids.present?
update_attribute(:workflow_state, 'active')
if self.type == 'StudentEnrollment'
Enrollment.recompute_final_score([self.user_id], self.course_id)
end
touch_user
end
def reset_notifications_cache
if self.workflow_state_changed?
StreamItemCache.invalidate_recent_stream_items(self.user_id, "Course", self.course_id)
end
end
workflow do
state :invited do
event :reject, :transitions_to => :rejected do self.user.touch; end
event :complete, :transitions_to => :completed
event :pend, :transitions_to => :pending
end
state :creation_pending do
event :invite, :transitions_to => :invited
end
state :active do
event :reject, :transitions_to => :rejected do self.user.touch; end
event :complete, :transitions_to => :completed
event :pend, :transitions_to => :pending
end
state :deleted
state :rejected do
event :unreject, :transitions_to => :invited
end
state :completed
# Inactive is a "hard" state, i.e. tuition not paid
state :inactive
end
def enrollment_dates
Canvas::Builders::EnrollmentDateBuilder.preload([self]) unless @enrollment_dates
@enrollment_dates
end
def state_based_on_date
RequestCache.cache('enrollment_state_based_on_date', self) do
calculated_state_based_on_date
end
end
def calculated_state_based_on_date
if state == :completed || ([:invited, :active].include?(state) && self.course.completed?)
if self.restrict_past_view?
return :inactive
else
return :completed
end
end
unless [:invited, :active].include?(state)
return state
end
ranges = self.enrollment_dates
now = Time.now
ranges.each do |range|
start_at, end_at = range
# start_at <= now <= end_at, allowing for open ranges on either end
return state if (start_at || now) <= now && now <= (end_at || now)
end
# Not strictly within any range
return state unless global_start_at = ranges.map(&:compact).map(&:min).compact.min
if global_start_at < now
self.soft_completed_at = ranges.map(&:last).compact.min
self.restrict_past_view? ? :inactive : :completed
elsif self.fake_student? # Allow student view students to use the course before the term starts
state
elsif view_restrictable? && !self.restrict_future_view?
self.available_at = global_start_at
if state == :active
# an accepted enrollment state means they still can't participate yet,
# but should be able to view just like an invited enrollment
:accepted
else
state
end
else
:inactive
end
end
def view_restrictable?
self.student? || self.observer?
end
def restrict_past_view?
self.view_restrictable? && RequestCache.cache('restrict_student_past_view', self.global_course_id) do
self.course.restrict_student_past_view?
end
end
def restrict_future_view?
self.view_restrictable? && RequestCache.cache('restrict_student_future_view', self.global_course_id) do
self.course.restrict_student_future_view?
end
end
def active?
state_based_on_date == :active
end
def inactive?
state_based_on_date == :inactive
end
def invited?
state_based_on_date == :invited
end
def accepted?
state_based_on_date == :accepted
end
def completed?
s = self.state_based_on_date
(s == :completed) || (s == :inactive && !!completed_at)
end
def explicitly_completed?
state == :completed
end
def completed_at
self.read_attribute(:completed_at) || (self.state_based_on_date && self.soft_completed_at) # soft_completed_at is loaded through state_based_on_date
end
alias_method :destroy_permanently!, :destroy
def destroy
self.workflow_state = 'deleted'
result = self.save
if result
self.user.try(:update_account_associations)
self.user.touch
grading_period_grades.destroy_all
end
result
end
def restore
self.workflow_state = 'active'
self.completed_at = nil
self.save
end
def re_send_confirmation!
@re_send_confirmation = true
self.save
@re_send_confirmation = false
true
end
def has_permission_to?(action)
@permission_lookup ||= {}
unless @permission_lookup.has_key? action
@permission_lookup[action] = RoleOverride.enabled_for?(course, action, self.role)
end
@permission_lookup[action].include?(:self)
end
def base_role_name
self.class.to_s
end
# Determine if a user has permissions to conclude this enrollment.
#
# user - The user requesting permission to conclude/delete enrollment.
# context - The current context, e.g. course or section.
# session - The current user's session (pass nil if not available).
#
# return Boolean
def can_be_concluded_by(user, context, session)
can_remove = [StudentEnrollment].include?(self.class) &&
context.grants_right?(user, session, :manage_students)
can_remove ||= context.grants_right?(user, session, :manage_admin_users)
end
# Determine if a user has permissions to delete this enrollment.
#
# user - The user requesting permission to conclude/delete enrollment.
# context - The current context, e.g. course or section.
# session - The current user's session (pass nil if not available).
#
# return Boolean
def can_be_deleted_by(user, context, session)
can_remove = [StudentEnrollment, ObserverEnrollment].include?(self.class) &&
context.grants_right?(user, session, :manage_students)
can_remove ||= context.grants_right?(user, session, :manage_admin_users)
can_remove &&= self.user_id != user.id ||
context.account.grants_right?(user, session, :manage_admin_users)
end
def pending?
self.invited? || self.creation_pending?
end
def email
self.user.email rescue t('#enrollment.default_email', "No Email")
end
def user_name
read_attribute(:user_name) || self.user.name rescue t('#enrollment.default_user_name', "Unknown User")
end
def context
@context ||= course
end
def context_id
@context_id ||= course_id
end
def can_switch_to?(type)
case type
when 'ObserverEnrollment'
['TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment'].include?(self.type)
when 'StudentEnrollment'
['TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment'].include?(self.type)
when 'TaEnrollment'
['TeacherEnrollment'].include?(self.type)
else
false
end
end
def self.workflow_readable_type(state)
case state.to_s
when 'active'
t('#enrollment.workflow.active', "Active")
when 'completed'
t('#enrollment.workflow.completed', "Completed")
when 'deleted'
t('#enrollment.workflow.deleted', "Deleted")
when 'invited'
t('#enrollment.workflow.invited', "Invited")
when 'pending'
t('#enrollment.workflow.pending', "Pending")
when 'rejected'
t('#enrollment.workflow.rejected', "Rejected")
when 'inactive'
t('#enrollment.workflow.inactive', "Inactive")
end
end
def workflow_readable_type
Enrollment.workflow_readable_type(self.workflow_state)
end
def readable_role_name
self.role.built_in? ? self.readable_type : self.role.name
end
def readable_type
Enrollment.readable_type(self.class.to_s)
end
def self.recompute_final_scores(user_id)
user = User.find(user_id)
enrollments = user.student_enrollments.to_a.uniq { |e| e.course_id }
enrollments.each do |enrollment|
send_later(:recompute_final_score, user_id, enrollment.course_id)
end
end
# This is called to recompute the users' cached scores for a given course
# when:
#
# * The user is merged with another user; the scores are recomputed for the
# new user in each of his/her courses.
#
# * An assignment's default grade is changed; all users in the assignment's
# course have their scores for that course recomputed.
#
# * A course is merged into another, a section is crosslisted/uncrosslisted,
# or a section is otherwise moved between courses; scores are recomputed
# for all users in the target course.
#
# * A course's group_weighting_scheme is changed; scores are recomputed for
# all users in the course.
#
# * Assignments are reordered (since an assignment may change groups, which
# may have weights); scores are recomputed for all users in the associated
# course.
#
# * An assignment's points_possible is changed; scores are recomputed for all
# users in the associated course.
#
# * An assignment group's rules or group_weight are changed; scores are
# recomputed for all users in the associated course.
#
# * A submission's score is changed; scores for the submission owner in the
# associated course are recomputed.
#
# * An assignment is deleted/undeleted
#
# * An enrollment is accepted (to address the scenario where a student
# is transferred from one section to another, and final grades need
# to be transferred)
#
# If some new feature comes up that affects calculation of a user's score,
# please add appropriate calls to this so that the cached values don't get
# stale! And once you've added the call, add the condition to the comment
# here for future enlightenment.
def self.recompute_final_score(user_ids, course_id)
GradeCalculator.recompute_final_score(user_ids, course_id)
end
def self.recompute_final_score_if_stale(course, user=nil)
Rails.cache.fetch(['recompute_final_scores', course.id, user].cache_key, :expires_in => Setting.get('recompute_grades_window', 600).to_i.seconds) do
recompute_final_score user ? user.id : course.student_enrollments.except(:preload).select(:user_id).uniq.map(&:user_id), course.id
yield if block_given?
true
end
end
def computed_current_grade
self.course.score_to_grade(self.computed_current_score)
end
def computed_final_grade
self.course.score_to_grade(self.computed_final_score)
end
def self.typed_enrollment(type)
return nil unless ['StudentEnrollment', 'StudentViewEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'ObserverEnrollment', 'DesignerEnrollment'].include?(type)
type.constantize
end
# overridden to return true in appropriate subclasses
def student?
false
end
def fake_student?
false
end
def observer?
false
end
def teacher?
false
end
def ta?
false
end
def designer?
false
end
def instructor?
teacher? || ta?
end
def admin?
instructor? || designer?
end
def content_admin?
teacher? || designer?
end
def to_atom
Atom::Entry.new do |entry|
entry.title = t('#enrollment.title', "%{user_name} in %{course_name}", :user_name => self.user_name, :course_name => self.course_name)
entry.updated = self.updated_at
entry.published = self.created_at
entry.links << Atom::Link.new(:rel => 'alternate',
:href => "/courses/#{self.course.id}/enrollments/#{self.id}")
end
end
set_policy do
given {|user, session| self.course.grants_any_right?(user, session, :manage_students, :manage_admin_users, :read_roster)}
can :read
given { |user| self.user == user }
can :read and can :read_grades
given { |user, session| self.course.students_visible_to(user, true).where(:id => self.user_id).exists? &&
self.course.grants_any_right?(user, session, :manage_grades, :view_all_grades) }
can :read and can :read_grades
given { |user| course.observer_enrollments.where(user_id: user, associated_user_id: self.user_id).exists? }
can :read and can :read_grades
given {|user, session| self.course.grants_right?(user, session, :participate_as_student) && self.user.show_user_services }
can :read_services
# read_services says this person has permission to see what web services this enrollment has linked to their account
given {|user, session| self.grants_right?(user, session, :read) && self.user.show_user_services }
can :read_services
end
scope :before, lambda { |date|
where("enrollments.created_at<?", date)
}
scope :for_user, lambda { |user| where(:user_id => user) }
scope :for_courses_with_user_name, lambda { |courses|
where(:course_id => courses).
joins(:user).
select("user_id, course_id, users.name AS user_name")
}
scope :invited, -> { where(:workflow_state => 'invited') }
scope :accepted, -> { where("enrollments.workflow_state<>'invited'") }
scope :active_or_pending, -> { where("enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive')") }
scope :all_active_or_pending, -> { where("enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted')") } # includes inactive
scope :currently_online, -> { joins(:pseudonyms).where("pseudonyms.last_request_at>?", 5.minutes.ago) }
# this returns enrollments for creation_pending users; should always be used in conjunction with the invited scope
scope :for_email, lambda { |email|
joins(:user => :communication_channels).
where("users.workflow_state='creation_pending' AND communication_channels.workflow_state='unconfirmed' AND path_type='email' AND LOWER(path)=LOWER(?)", email).
select("enrollments.*").
readonly(false)
}
def self.cached_temporary_invitations(email)
if Enrollment.cross_shard_invitations?
Shard.birth.activate do
invitations = Rails.cache.fetch([email, 'all_invited_enrollments2'].cache_key) do
Shard.with_each_shard(CommunicationChannel.associated_shards(email)) do
Enrollment.invited.for_email(email).to_a
end
end
end
else
Rails.cache.fetch([email, 'invited_enrollments2'].cache_key) do
Enrollment.invited.for_email(email).to_a
end
end
end
def self.order_by_sortable_name
clause = User.sortable_name_order_by_clause('users')
scope = self.order(clause)
if scope.select_values.present?
scope = scope.select(clause)
else
scope = scope.select(self.arel_table[Arel.star])
end
scope
end
def self.top_enrollment_by(key, rank_order = :default)
raise "top_enrollment_by_user must be scoped" unless all.where_values.present?
key = key.to_s
order("#{key}, #{type_rank_sql(rank_order)}").distinct_on(key)
end
def assign_uuid
# DON'T use ||=, because that will cause an immediate save to the db if it
# doesn't already exist
self.uuid = CanvasSlug.generate_securish_uuid if !read_attribute(:uuid)
end
protected :assign_uuid
def uuid
if !read_attribute(:uuid)
self.update_attribute(:uuid, CanvasSlug.generate_securish_uuid)
end
read_attribute(:uuid)
end
def self.limit_privileges_to_course_section!(course, user, limit)
course.shard.activate do
Enrollment.where(:course_id => course, :user_id => user).update_all(:limit_privileges_to_course_section => !!limit)
end
user.touch
end
def self.course_user_state(course, uuid)
Rails.cache.fetch(['user_state', course, uuid].cache_key) do
enrollment = course.enrollments.where(uuid: uuid).first
if enrollment
{
:enrollment_state => enrollment.workflow_state,
:user_state => enrollment.user.state,
:is_admin => enrollment.admin?
}
else
nil
end
end
end
def self.serialization_excludes; [:uuid,:computed_final_score, :computed_current_score]; end
# enrollment term per-section is deprecated; a section's term is inherited from the
# course it is currently tied to
def enrollment_term
self.course.enrollment_term
end
def effective_start_at
# try and use the enrollment dates logic first, since it knows about
# overrides, etc. but if it doesn't find anything, start guessing by
# looking at the enrollment, section, course, then term. if we still didn't
# find it, fall back to the section or course creation date.
enrollment_dates.map(&:first).compact.min ||
start_at ||
course_section && course_section.start_at ||
course.start_at ||
course.enrollment_term && course.enrollment_term.start_at ||
course_section && course_section.created_at ||
course.created_at
end
def effective_end_at
# try and use the enrollment dates logic first, since it knows about
# overrides, etc. but if it doesn't find anything, start guessing by
# looking at the enrollment, section, course, then term.
enrollment_dates.map(&:last).compact.max ||
end_at ||
course_section && course_section.end_at ||
course.conclude_at ||
course.enrollment_term && course.enrollment_term.end_at
end
def self.cross_shard_invitations?
false
end
# DO NOT TRUST
# This is only a convenience method to assist in identifying which enrollment
# goes to which user when users have accidentally been merged together
# This is the *only* reason the sis_source_id column has not been dropped
def sis_user_id
return @sis_user_id if @sis_user_id
sis_source_id_parts = sis_source_id ? sis_source_id.split(':') : []
if sis_source_id_parts.length == 4
@sis_user_id = sis_source_id_parts[1]
else
@sis_user_id = sis_source_id_parts[0]
end
@sis_user_id
end
def total_activity_time
self.read_attribute(:total_activity_time).to_i
end
def touch_graders_if_needed
if !active_student? && active_student?(:was) && self.course.submissions.where(:user_id => self.user_id).exists?
self.class.connection.after_transaction_commit do
User.where(id: self.course.admins).touch_all
end
end
end
end