262 lines
9.8 KiB
Ruby
262 lines
9.8 KiB
Ruby
#
|
|
# Copyright (C) 2011 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 Alert < ActiveRecord::Base
|
|
belongs_to :context, :polymorphic => true # Account or Course
|
|
has_many :criteria, :class_name => 'AlertCriterion', :dependent => :destroy, :autosave => true
|
|
|
|
serialize :recipients
|
|
|
|
attr_accessible :context, :repetition, :criteria, :recipients
|
|
|
|
validates_presence_of :context_id
|
|
validates_presence_of :context_type
|
|
validates_presence_of :criteria
|
|
validates_associated :criteria
|
|
validates_presence_of :recipients
|
|
|
|
before_save :infer_defaults
|
|
|
|
def infer_defaults
|
|
self.repetition = nil if self.repetition.blank?
|
|
end
|
|
|
|
def as_json(*args)
|
|
{
|
|
:id => id,
|
|
:criteria => criteria.map { |c| c.as_json(:include_root => false) },
|
|
:recipients => recipients.try(:map) { |r| (r.is_a?(Symbol) ? ":#{r}" : r) },
|
|
:repetition => repetition
|
|
}
|
|
end
|
|
|
|
def recipients=(recipients)
|
|
write_attribute(:recipients, recipients.map { |r| (r.is_a?(String) && r[0..0] == ':' ? r[1..-1].to_sym : r) })
|
|
end
|
|
|
|
def criteria=(values)
|
|
if values[0].is_a? Hash
|
|
values = values.map do |params|
|
|
if(params[:id].present?)
|
|
id = params.delete(:id).to_i
|
|
criterion = self.criteria.to_ary.find { |c| c.id == id }
|
|
criterion.attributes = params
|
|
else
|
|
criterion = self.criteria.build(params)
|
|
end
|
|
criterion
|
|
end
|
|
end
|
|
self.criteria.replace(values)
|
|
end
|
|
|
|
def resolve_recipients(student_id, teachers = nil)
|
|
include_student = false
|
|
include_teacher = false
|
|
include_teachers = false
|
|
admin_roles = []
|
|
self.recipients.try(:each) do |recipient|
|
|
case
|
|
when recipient == :student
|
|
include_student = true
|
|
when recipient == :teachers
|
|
include_teachers = true
|
|
when recipient.is_a?(String)
|
|
admin_roles << recipient
|
|
else
|
|
raise "Unsupported recipient type!"
|
|
end
|
|
end
|
|
|
|
recipients = []
|
|
|
|
recipients << student_id if include_student
|
|
recipients.concat(Array(teachers)) if teachers.present? && include_teachers
|
|
recipients.concat context.account_users.find_all_by_membership_type(admin_roles).map(&:user_id) if context_type == 'Account' && !admin_roles.empty?
|
|
recipients.uniq
|
|
end
|
|
|
|
def self.process
|
|
Account.root_accounts.find_each do |account|
|
|
Account.with_exclusive_scope do
|
|
self.send_later_if_production_enqueue_args(:evaluate_for_root_account, { :priority => Delayed::LOW_PRIORITY }, account)
|
|
end if account.settings[:enable_alerts]
|
|
end
|
|
end
|
|
|
|
def self.evaluate_for_root_account(account)
|
|
return unless account.settings[:enable_alerts]
|
|
alerts_cache = {}
|
|
account.associated_courses.scoped(:conditions => { :workflow_state => 'available' }).find_each do |course|
|
|
alerts_cache[course.account_id] ||= course.account.account_chain.map { |a| a.alerts.all }.flatten
|
|
self.evaluate_for_course(course, alerts_cache[course.account_id], account.enable_user_notes?)
|
|
end
|
|
end
|
|
|
|
def self.evaluate_for_course(course, account_alerts = nil, include_user_notes = nil)
|
|
return unless course.available?
|
|
|
|
alerts = Array(account_alerts)
|
|
alerts.concat course.alerts.all
|
|
return if alerts.empty?
|
|
|
|
student_enrollments = course.student_enrollments.active
|
|
student_ids = student_enrollments.map(&:user_id)
|
|
return if student_ids.empty?
|
|
student_ids_to_section_ids = {}
|
|
student_enrollments.each do |enrollment|
|
|
student_ids_to_section_ids[enrollment.user_id] ||= []
|
|
student_ids_to_section_ids[enrollment.user_id] << enrollment.course_section_id
|
|
end
|
|
|
|
teacher_enrollments = course.admin_enrollments.active
|
|
teacher_ids = teacher_enrollments.map(&:user_id)
|
|
return if teacher_ids.empty?
|
|
section_ids_to_teachers_list = {}
|
|
teacher_enrollments.each do |enrollment|
|
|
section_id = enrollment.limit_priveleges_to_course_section ? enrollment.course_section_id : nil
|
|
section_ids_to_teachers_list[section_id] ||= []
|
|
section_ids_to_teachers_list[section_id] << enrollment.user_id
|
|
end
|
|
|
|
criterion_types = alerts.map(&:criteria).flatten.map(&:criterion_type).uniq
|
|
data = {}
|
|
student_enrollments.each { |e| data[e.user_id] = {} }
|
|
|
|
# Bulk data gathering
|
|
if criterion_types.include? 'Interaction'
|
|
last_comment_dates = SubmissionComment.for_context(course).maximum(
|
|
:created_at,
|
|
:group => [ :recipient_id, :author_id ],
|
|
:conditions => { :author_id => teacher_ids, :recipient_id => student_ids })
|
|
last_comment_dates.each do |key, date|
|
|
student = data[key.first]
|
|
(student[:last_interaction] ||= {})[key.last] = date
|
|
end
|
|
last_message_dates = ConversationMessage.maximum(
|
|
:created_at,
|
|
:joins => 'INNER JOIN conversation_participants ON conversation_participants.conversation_id=conversation_messages.conversation_id',
|
|
:group => ['conversation_participants.user_id', 'conversation_messages.author_id'],
|
|
:conditions => [ 'conversation_messages.author_id IN (?) AND conversation_participants.user_id IN (?) AND NOT conversation_messages.generated', teacher_ids, student_ids ])
|
|
last_message_dates.each do |key, date|
|
|
student = data[key.first.to_i]
|
|
last_interaction = (student[:last_interaction] ||= {})
|
|
last_interaction[key.last] = [last_interaction[key.last], date].compact.max
|
|
end
|
|
|
|
data.each do |student_id, user_data|
|
|
user_data[:last_interaction] ||= {}
|
|
user_data[:last_interaction][:all] = user_data[:last_interaction].values.max
|
|
end
|
|
end
|
|
if criterion_types.include? 'UngradedCount'
|
|
ungraded_counts = course.submissions.scoped(:include => { :exclude => :quiz_submission }).count(
|
|
:group => :user_id,
|
|
:conditions => ["user_id IN (?) AND #{Submission.needs_grading_conditions}", student_ids])
|
|
ungraded_counts.each do |user_id, count|
|
|
student = data[user_id]
|
|
student[:ungraded_count] = count
|
|
end
|
|
end
|
|
if criterion_types.include? 'UngradedTimespan'
|
|
ungraded_timespans = course.submissions.scoped(:include => { :exclude => :quiz_submission }).minimum(
|
|
:submitted_at,
|
|
:group => :user_id,
|
|
:conditions => ["user_id IN (?) AND #{Submission.needs_grading_conditions}", student_ids])
|
|
ungraded_timespans.each do |user_id, timespan|
|
|
student = data[user_id]
|
|
student[:ungraded_timespan] = timespan
|
|
end
|
|
end
|
|
include_user_notes = course.root_account.enable_user_notes? if include_user_notes.nil?
|
|
if criterion_types.include?('UserNote') && include_user_notes
|
|
note_dates = UserNote.active.maximum(
|
|
:created_at,
|
|
:group => [ :user_id, :created_by_id ],
|
|
:conditions => { :created_by_id => teacher_ids, :user_id => student_ids })
|
|
note_dates.each do |key, date|
|
|
student = data[key.first]
|
|
(student[:last_user_note] ||= {})[key.last] = date
|
|
end
|
|
data.each do |student_id, user_data|
|
|
user_data[:last_user_note] ||= {}
|
|
user_data[:last_user_note][:all] = user_data[:last_user_note].values.max
|
|
end
|
|
end
|
|
|
|
# Evaluate all the criteria for each user for each alert
|
|
today = Time.now.beginning_of_day
|
|
start_at = course.start_at || course.created_at
|
|
|
|
alerts.each do |alert|
|
|
data.each do |user_id, user_data|
|
|
matches = true
|
|
alert.criteria.each do |criterion|
|
|
case criterion.criterion_type
|
|
when 'Interaction'
|
|
if (user_data[:last_interaction][:all] || start_at) + criterion.threshold.days > today
|
|
matches = false
|
|
break
|
|
end
|
|
when 'UngradedCount'
|
|
if (user_data[:ungraded_count].to_i < criterion.threshold.to_i)
|
|
matches = false
|
|
break
|
|
end
|
|
when 'UngradedTimespan'
|
|
if (!user_data[:ungraded_timespan] || user_data[:ungraded_timespan] + criterion.threshold.days > today)
|
|
matches = false
|
|
break
|
|
end
|
|
when 'UserNote'
|
|
if include_user_notes && (user_data[:last_user_note][:all] || start_at) + criterion.threshold.days > today
|
|
matches = false
|
|
break
|
|
end
|
|
end
|
|
end
|
|
cache_key = [alert, user_id].cache_key
|
|
if matches
|
|
last_sent = Rails.cache.fetch(cache_key)
|
|
if last_sent.blank?
|
|
elsif alert.repetition.blank?
|
|
matches = false
|
|
else
|
|
matches = last_sent + alert.repetition.days <= today
|
|
end
|
|
end
|
|
if matches
|
|
Rails.cache.write(cache_key, today)
|
|
|
|
teachers = []
|
|
teachers.concat(section_ids_to_teachers_list[nil]) if section_ids_to_teachers_list[nil]
|
|
student_ids_to_section_ids[user_id].each do |section_id|
|
|
teachers.concat(section_ids_to_teachers_list[section_id]) if section_ids_to_teachers_list[section_id]
|
|
end
|
|
send_alert(alert, alert.resolve_recipients(user_id, teachers), student_enrollments.to_ary.find { |enrollment| enrollment.user_id == user_id } )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.send_alert(alert, user_ids, student_enrollment)
|
|
notification = Notification.find_by_name("Alert")
|
|
notification.create_message(alert, user_ids, {:asset_context => student_enrollment})
|
|
end
|
|
end
|