canvas-lms/lib/messageable_user/calculator.rb

1004 lines
40 KiB
Ruby

#
# Copyright (C) 2013 - 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/>.
#
require_dependency 'messageable_user'
class MessageableUser
class Calculator
CONTEXT_RECIPIENT = /\A(course|section|group)_(\d+)(_([a-z]+))?\z/
INDIVIDUAL_RECIPIENT = /\A\d+\z/
# all work is done within the context of a user. avoid passing it around in
# every single method call by being an object instead of just a collection
# of class methods
def initialize(user)
@user = user
end
# Takes a list of Users (or ids, or MessageableUsers) and:
#
# * turns them into MessageableUsers if they weren't already
# * bulk loads any common contexts for those MessageableUsers
# * filters them to just those actually messageable by the viewing user
#
# with the :strict_checks option false (default: true), all provided users
# will be considered messageable and enrollments in unpublished courses,
# etc. will be included when determining common contexts. should be used
# only when loading contexts for users already in a conversation
#
# the :admin_context option allows specifying a context (course, section,
# or group) to look for users in. the viewing user will be treated as
# having full visibility in that context for the purpose of checking
# messageability and loading common contexts.
#
# the :conversation_id option allows specifying a conversation to look
# for users in. ignored if the viewing user is not already be a participant
# in the conversation. any other user participating in that conversation is
# considered messageable.
def load_messageable_users(users, options={})
strict_checks = {:strict_checks => true}.merge(options)[:strict_checks]
# we can be given user ids, Users (from which we just use the ids), or
# MessageableUsers. if they're not already MessageableUsers, make them so
# (and check availability while at it, unless we're skipping that)
return [] unless users.present?
unless users.first.is_a?(MessageableUser)
scope_options = {:strict_checks => strict_checks, :include_deleted => !strict_checks}
user_ids = users.first.is_a?(User) ? users.map(&:id) : users
users = Shard.partition_by_shard(user_ids) do |shard_user_ids|
MessageableUser.prepped(scope_options).
where(:id => shard_user_ids).to_a
end
return [] unless users
end
# interpret admin context, if any
case options[:admin_context]
when Course then include_course_id = options[:admin_context].id
when CourseSection then include_course_id = options[:admin_context].course_id
when Group then include_group_id = options[:admin_context].id
end
# what common courses and groups do they have, if any. if they had some,
# they're definitely messageable
other_users = users.reject{ |u| u.id == @user.id }
if other_users.present?
load_common_courses_with_users(other_users, include_course_id, strict_checks)
load_common_groups_with_users(other_users, include_group_id, strict_checks)
end
# keep only the ones that look messageable (have a shared context, are
# self, or share the given conversation)
if strict_checks
questionable = users.select do |user|
!user.common_courses.present? &&
!user.common_groups.present? &&
user.id != @user.id
end
if questionable.present? && options[:conversation_id].present?
participants = participants_in_conversation([@user, *questionable], options[:conversation_id].to_i)
if participants.detect{ |user| user == @user }
questionable -= participants
end
end
users -= questionable
end
users
end
# convenience method. as load_messageable_users, but for a single user
def load_messageable_user(user, options={})
load_messageable_users([user], options).first
end
# find and return all the messageable users in a particular context,
# specified by an extended asset string (TODO: legacy, should improve
# interface in the future)
#
# the string should be something like 'group_123', 'section_123_students',
# 'course_123_admins', etc. the third portion of the asset string specifies
# the types of enrollments to include for course or section contexts; it is
# ignored for group contexts. the 'admins' enrollment type refers to
# teachers and tas.
#
# NOTE: the common_courses and common_groups of the returned
# MessageableUser objects will be populated only with the context given.
# this is by design, and desired behavior for the use case this method is
# directed at. it may be confusing, however, to those not expecting it.
def messageable_users_in_context(asset_string, options={})
scope = messageable_users_in_context_scope(asset_string, options)
scope ? scope.to_a : []
end
def count_messageable_users_in_context(asset_string, options={})
scope = messageable_users_in_context_scope(asset_string, options)
count_messageable_users_in_scope(scope)
end
def messageable_users_in_course(course_or_id, options={})
scope = messageable_users_in_course_scope(course_or_id, options[:enrollment_types], options)
scope ? scope.to_a : []
end
def count_messageable_users_in_course(course_or_id, options={})
scope = messageable_users_in_course_scope(course_or_id, options[:enrollment_types], options)
count_messageable_users_in_scope(scope)
end
def messageable_users_in_section(section_or_id, options={})
scope = messageable_users_in_section_scope(section_or_id, options[:enrollment_types], options)
scope ? scope.to_a : []
end
def count_messageable_users_in_section(section_or_id, options={})
scope = messageable_users_in_section_scope(section_or_id, options[:enrollment_types], options)
count_messageable_users_in_scope(scope)
end
def messageable_users_in_group(group_or_id, options={})
scope = messageable_users_in_group_scope(group_or_id, options)
scope ? scope.to_a : []
end
def count_messageable_users_in_group(group_or_id, options={})
scope = messageable_users_in_group_scope(group_or_id, options)
count_messageable_users_in_scope(scope)
end
def count_messageable_users_in_scope(scope)
if scope
scope.except(:select, :group, :order).distinct.count
else
0
end
end
# construct a bookmark-paginated collection of messageable users from
# the various sources (across applicable shards)
def search_messageable_users(options={})
global_exclude_ids = (options[:exclude_ids] || []).
map{ |id| Shard.global_id_for(id) }
# load up the list of scopes that users messageable users might come from
if options[:context]
# messageable_users_in_context_scope may return null, indicating a
# non-visible context, so the result will be empty. but we still need
# to return a bookmark-paginated collection, so we craft an empty scope
# by default
scope = messageable_users_in_context_scope(options.delete(:context), options)
scope = search_scope(scope, options[:search], global_exclude_ids) if scope
scope ||= MessageableUser.where('?', false)
bookmark(scope)
else
scope = self_scope(options)
scope = search_scope(scope, options[:search], global_exclude_ids)
collections = [['self', bookmark(scope)]]
Shard.with_each_shard(@user.associated_shards) do
if all_courses.present?
scope = visible_enrollment_scope(options)
scope = search_scope(scope, options[:search], global_exclude_ids)
collections << ["courses:#{Shard.current.id}", bookmark(scope)]
end
if fully_visible_group_ids.present?
scope = fully_visible_group_user_scope(options)
scope = search_scope(scope, options[:search], options[:exclude_ids])
collections << ["groups:#{Shard.current.id}", bookmark(scope)]
end
if visible_account_ids.present?
scope = visible_account_user_scope(options)
scope = search_scope(scope, options[:search], options[:exclude_ids])
collections << ["accounts:#{Shard.current.id}", bookmark(scope)]
end
end
BookmarkedCollection.merge(*collections) do |u1, u2|
u1.include_common_contexts_from(u2)
end
end
end
def messageable_sections
messageable_sections_by_shard.values.flatten
end
def messageable_groups
messageable_groups_by_shard.values.flatten
end
def self.slave_module
@slave_module ||= Module.new.tap { |m| prepend(m) }
end
def self.slave(method)
slave_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*)
Shackles.activate(:slave) { super }
end
RUBY
end
slave :load_messageable_users
slave :messageable_users_in_context
slave :messageable_users_in_course
slave :messageable_users_in_section
slave :messageable_users_in_group
slave :count_messageable_users_in_context
slave :count_messageable_users_in_course
slave :count_messageable_users_in_section
slave :count_messageable_users_in_group
slave :search_messageable_users
slave :messageable_sections
slave :messageable_groups
# ========================== end of public API ==========================
# | |
# | the rest of these methods are to simplify the implementation of the |
# | above, and increase testability. they're not private so that they |
# | can be tested in isolation, but should not be used directly |
# | |
# =========================================================================
# given a list of MessageableUsers, loads the common courses between each
# of those users and the viewing user into the respective MessageableUser
# objects.
#
# the optional include_course_id lets you treat a specific course as if the
# viewing user had full visibility in it. (TODO: is this right when
# include_course_id came from a CourseSection admin_context?)
#
# the optional strict_checks parameter (default: true) is passed down to
# the queried scopes (see enrollment_scope and account_user_scope).
def load_common_courses_with_users(users, include_course_id=nil, strict_checks=true)
scope_options = {:strict_checks => strict_checks, :include_deleted => !strict_checks}
users.each{ |u| u.global_common_courses = {} }
# with messageability constraints, do I see any of the users in any of my visible
# courses, and if so with which enrollment type(s)?
Shard.with_each_shard(@user.associated_shards) do
if all_courses.present?
reverse_lookup = users.index_by(&:id)
user_ids = reverse_lookup.keys
visible_enrollment_scope(scope_options).where(:id => user_ids).each do |user|
reverse_lookup[user.id].include_common_contexts_from(user)
end
end
end
# skipping messageability constraints, do I see the user in that specific
# course, and if so with which enrollment type(s)?
if include_course_id && course = Course.where(id: include_course_id).first
missing_users = users.reject{ |user| user.global_common_courses.keys.include?(course.global_id) }
if missing_users.present?
course.shard.activate do
reverse_lookup = missing_users.index_by(&:id)
missing_user_ids = reverse_lookup.keys
enrollment_scope(scope_options.merge(:course_workflow_state => course.workflow_state)).
where(id: missing_user_ids, enrollments: { course_id: course }).
each{ |user| reverse_lookup[user.id].include_common_contexts_from(user) }
end
end
end
# do I see the user in any of the accounts I admin, and if so with what
# primary enrollment type?
Shard.with_each_shard(@user.associated_shards) do
if visible_account_ids.present?
reverse_lookup = users.index_by(&:id)
user_ids = reverse_lookup.keys
visible_account_user_scope(scope_options).where(:id => user_ids).each do |user|
reverse_lookup[user.id].include_common_contexts_from(user)
end
end
end
end
# given a list of MessageableUsers, loads the common groups between each
# of those users and the viewing user into the respective MessageableUser
# objects.
#
# the optional include_group_id lets you treat a specific group as if the
# viewing user had full visibility in it.
#
# the optional strict_checks parameter (default: true) is passed down to
# the queried scopes (see group_user_scope).
def load_common_groups_with_users(users, include_group_id=nil, strict_checks=true)
scope_options = {:strict_checks => strict_checks, :include_deleted => !strict_checks}
users.each{ |u| u.global_common_groups = {} }
# with messageability constraints, do I see the user in any of my visible
# groups?
Shard.with_each_shard(@user.associated_shards) do
if fully_visible_group_ids.present?
reverse_lookup = users.index_by(&:id)
user_ids = reverse_lookup.keys
fully_visible_group_user_scope(scope_options).where(:id => user_ids).each do |user|
reverse_lookup[user.id].include_common_contexts_from(user)
end
end
end
# skipping messageability constraints, do I see the user in that specific
# group?
if include_group_id && group = Group.where(id: include_group_id).first
missing_users = users.reject{ |user| user.global_common_groups.keys.include?(group.global_id) }
if missing_users.present?
group.shard.activate do
reverse_lookup = missing_users.index_by(&:id)
missing_user_ids = reverse_lookup.keys
group_user_scope(scope_options).where(:id => missing_user_ids, 'group_memberships.group_id' => group.id).each do |user|
reverse_lookup[user.id].include_common_contexts_from(user)
end
end
end
end
end
# filters the provided list of users to just those that are participants in
# the conversation
def participants_in_conversation(users, conversation_id)
conversation = Conversation.where(id: conversation_id).first
return [] unless conversation
conversation.shard.activate do
reverse_lookup = users.index_by(&:id)
user_ids = reverse_lookup.keys
ConversationParticipant.where(
:user_id => user_ids,
:conversation_id => conversation.id
).pluck(:user_id).map{ |user_id| reverse_lookup[user_id] }
end
end
# wraps a scope in a bookmark-paginated collection
def bookmark(scope)
BookmarkedCollection.wrap(MessageableUser::Bookmarker, scope)
end
# ==========================================
# | top-level query construction methods |
# ==========================================
def messageable_users_in_context_scope(asset_string, options={})
return unless asset_string.sub(/_all\z/, '') =~ CONTEXT_RECIPIENT
context_type = $1
context_id = $2.to_i
enrollment_type = $4
if enrollment_type == 'admins'
enrollment_types = ['TeacherEnrollment','TaEnrollment']
elsif enrollment_type
enrollment_types = ["#{enrollment_type.capitalize.singularize}Enrollment"]
end
case context_type
when 'course' then messageable_users_in_course_scope(context_id, enrollment_types, options)
when 'section' then messageable_users_in_section_scope(context_id, enrollment_types, options)
when 'group' then messageable_users_in_group_scope(context_id, options)
end
end
# find and return all the messageable users in a particular course (see
# messageable_users_in_context). the optional enrollment_type restricts to
# just users with those enrollments
#
# NOTE: the common_courses of the returned MessageableUser objects will be
# populated only with the course given.
def messageable_users_in_course_scope(course_or_id, enrollment_types=nil, options={})
course = course_or_id.is_a?(Course) ? course_or_id : Course.where(id: course_or_id).first
return unless course
course.shard.activate do
# make sure the course is recognized
return unless options[:admin_context] || course = course_index[course.id]
scope = enrollment_scope(options.merge(
:include_concluded_students => false,
:course_workflow_state => course.workflow_state))
scope =
case course_visibility(course)
when :full then scope.where(full_visibility_clause([course]))
when :sections then scope.where(section_visibility_clause([course]))
when :restricted then scope.where(restricted_visibility_clause([course]))
end
scope = scope.where(observer_restriction_clause) if student_courses.present?
scope = scope.where('enrollments.type' => enrollment_types) if enrollment_types
scope
end
end
# find and return all the messageable users in a particular course section
# (see messageable_users_in_context). the optional enrollment_type
# restricts to just users with those enrollments
#
# NOTE: the common_courses of the returned MessageableUser objects will be
# populated only with the course of the given section.
def messageable_users_in_section_scope(section_or_id, enrollment_types=nil, options={})
return unless section_or_id
section = section_or_id.is_a?(CourseSection) ? section_or_id : CourseSection.where(id: section_or_id).first
return unless section
section.shard.activate do
course = course_index[section.course_id]
return unless options[:admin_context] || course
course ||= section.course
return unless options[:admin_context] ||
fully_visible_courses.include?(course) ||
section_visible_courses.include?(course) &&
visible_section_ids_in_courses([course]).include?(section.id)
scope = enrollment_scope(options.merge(
:include_concluded_students => false,
:course_workflow_state => course.workflow_state)).
where('enrollments.course_section_id' => section.id)
scope = scope.where(observer_restriction_clause) if student_courses.present?
scope = scope.where('enrollments.type' => enrollment_types) if enrollment_types
scope
end
end
# find and return all the messageable users in a particular group (see
# messageable_users_in_context).
#
# NOTE: the common_courses of the returned MessageableUser objects will be
# populated only with the group given.
def messageable_users_in_group_scope(group_or_id, options={})
return unless group_or_id
group = group_or_id.is_a?(Group) ? group_or_id : Group.where(id: group_or_id).first
return unless group
group.shard.activate do
if options[:admin_context] || fully_visible_group_ids.include?(group.id)
group_user_scope(options).where('group_memberships.group_id' => group.id)
elsif section_visible_group_ids.include?(group.id)
# group.context is guaranteed to be a course from
# section_visible_courses at this point
course = course_index[group.context_id]
scope = enrollment_scope({ common_group_column: group.id }.merge(options)).where(
"course_section_id IN (?) AND EXISTS (?)",
visible_section_ids_in_courses([course]),
GroupMembership.where(group_id: group, workflow_state: 'accepted').where("user_id=users.id"))
scope = scope.where(observer_restriction_clause) if student_courses.present?
scope
end
end
end
def search_scope(scope, search, global_exclude_ids)
if global_exclude_ids.present?
target_shard = scope.shard_value
exclude_ids = global_exclude_ids.map{ |id| Shard.relative_id_for(id, Shard.current, target_shard) }
scope = scope.where(["users.id NOT IN (?)", exclude_ids])
end
if search.present? && (parts = search.strip.split(/\s+/)).present?
parts.each do |part|
scope = scope.where(@user.wildcard('users.name', 'users.short_name', part))
end
end
scope
end
# restricts enrollments to those with extended states (see
# Enrollment::QueryBuilder) of active, invited, and (conditionally)
# completed. also universally excludes student view enrollments
#
# with the :include_concluded option false (default: true), completed
# enrollments are excluded
#
# with the :include_concluded_students option true (default: true),
# completed student enrollments are included; otherwise only completed
# admin enrollments are included. ignored if :include_concluded is false.
#
# the :strict_checks option (default: true) is per load_messageable_users
# and gets passed along to Enrollment::QueryBuilder.new.
#
# the :course_workflow_state is useful for passing on to
# Enrollment::QueryBuilder to simplify the queries when the enrollments
# are known to come from course(s) with a single workflow_state
#
# may return nil, indicating the scope should be treated as empty.
def self.enrollment_conditions(options={})
options = {
:include_concluded => true,
:include_concluded_students => true,
:strict_checks => true,
:course_workflow_state => nil
}.merge(options)
state_clauses = [
Enrollment::QueryBuilder.new(:active, options.slice(:strict_checks, :course_workflow_state)).conditions,
Enrollment::QueryBuilder.new(:invited, options.slice(:strict_checks, :course_workflow_state)).conditions
]
if options[:include_concluded]
clause = Enrollment::QueryBuilder.new(:completed, options.slice(:strict_checks)).conditions
clause << " AND enrollments.type IN ('TeacherEnrollment', 'TaEnrollment')" unless options[:include_concluded_students]
state_clauses << clause
end
state_clauses.compact!
return nil if state_clauses.empty?
"(#{state_clauses.join(' OR ')}) AND enrollments.type != 'StudentViewEnrollment'"
end
def base_scope(options={})
MessageableUser.prepped(options).shard(Shard.current)
end
# scopes MessageableUsers via enrollments in courses, setting up the common
# context fields to produce common_course entries.
#
# which columns (or values) specify the course id (default:
# enrollments.course_id) and enrollment type (default: enrollments.type)
# for common courses can be overridden by the :common_course_column and
# :common_role_column options.
#
# specifying a column (or value) for :common_group_column (default: none)
# will add that group to the returned users' common_groups.
#
# the :include_concluded, :strict_checks, and :course_workflow_state
# options are passed through to enrollment_conditions; see its
# documentation.
#
# additionally, if :strict_checks is false (default: true), all users will
# be included, not just active users. (see MessageableUser.prepped)
def enrollment_scope(options={})
options = {
:common_course_column => 'enrollments.course_id',
:common_role_column => 'enrollments.type'
}.merge(options)
scope = base_scope(options)
scope = scope.joins("INNER JOIN #{Enrollment.quoted_table_name} ON enrollments.user_id=users.id")
enrollment_conditions = self.class.enrollment_conditions(options)
if enrollment_conditions
scope = scope.joins("INNER JOIN #{Course.quoted_table_name} ON courses.id=enrollments.course_id") unless options[:course_workflow_state]
scope = scope.where(enrollment_conditions)
else
scope = scope.none
end
scope
end
# further restricts the enrollment scope to users whose enrollment is
# visible by the viewing user (see visibility_restriction_clause and
# observer_restriction_clause)
def visible_enrollment_scope(options={})
scope = enrollment_scope(options).where(visibility_restriction_clause)
scope = scope.where(observer_restriction_clause) if student_courses.present?
# redundant with the visibility_restriction_clause, which is narrower, but
# helps out the query planner
scope.where('enrollments.course_id' => all_courses.map(&:id))
end
# sql clause to limit an enrollment scope to those in the viewing user's
# full visiblity courses
def full_visibility_clause(courses=fully_visible_courses)
["course_id IN (?)", courses.map(&:id)]
end
# sql clause to limit an enrollment scope to those in the viewing user's
# sections from his section visibility courses
def section_visibility_clause(courses=section_visible_courses)
["course_section_id IN (?)", visible_section_ids_in_courses(courses)]
end
# sql clause to limit an enrollment scope to those enrollments allowed by
# the viewing user's restricted visibility courses (teachers, tas, and
# observed students)
def restricted_visibility_clause(courses=restricted_visibility_courses)
# TODO: minor bug where if I observer student A in course X and student B in
# course Y, and student A is enrolled in course Y but I do not observer him
# in that class, I will still see that enrollment
[
"(course_id IN (?) AND (enrollments.type IN ('TeacherEnrollment','TaEnrollment') OR enrollments.user_id IN (?)))",
courses.map(&:id),
[@user.id] + observed_student_ids_in_courses(courses)
]
end
# combine the three course visibility clauses into a sql clause to limit an
# enrollment scope to any enrollments visible to the viewing user
def visibility_restriction_clause
clauses = []
clauses << full_visibility_clause if fully_visible_courses.present?
clauses << section_visibility_clause if section_visible_courses.present?
clauses << restricted_visibility_clause if restricted_visibility_courses.present?
sql = "(#{clauses.map(&:shift).join(' OR ')})"
clauses.inject([sql], &:concat)
end
# regardless of course visibility level, if the viewing user's best
# enrollment in a course is as a student, he can't see observers that
# aren't observing him
def observer_restriction_clause
clause = ["enrollments.course_id NOT IN (?) OR enrollments.type != 'ObserverEnrollment'", student_courses.map(&:id)]
if linked_observer_ids.present?
clause.first << " OR enrollments.user_id IN (?)"
clause << linked_observer_ids
end
clause
end
# scopes MessageableUsers via associations with accounts, setting up the
# common context fields to produce fake common_course entries with the
# primary enrollment type in the account (see above).
#
# if :strict_checks is false (default: true), all users will be included,
# not just active users. (see MessageableUser.prepped)
def account_user_scope(options={})
# uses a clearly fake enrollment type for the user across all active courses in the account.
# used to fake a common "course" context with that enrollment type
# in users found via the account roster.
options = {
:common_course_column => 0,
:common_role_column => "'FakeEnrollment'"
}.merge(options)
base_scope(options).
joins("INNER JOIN #{UserAccountAssociation.quoted_table_name} ON user_account_associations.user_id=users.id")
end
# further restricts the account user scope to users associated with
# accounts in which I can read the roster (see visible_account_ids).
def visible_account_user_scope(options={})
account_user_scope(options).where('user_account_associations.account_id' => visible_account_ids)
end
# scopes MessageableUsers via group memberships, setting up the common
# context fields to produce common_groups entries.
#
# if :strict_checks is false (default: true), all users will be included,
# not just active users. (see MessageableUser.prepped)
def group_user_scope(options={})
options = {
:common_group_column => 'group_id'
}.merge(options)
base_scope(options).
joins("INNER JOIN #{GroupMembership.quoted_table_name} ON group_memberships.user_id=users.id").
where(:group_memberships => { :workflow_state => 'accepted' })
end
# further restricts the group user scope to users in groups for which I
# have full visibility (see fully_visible_group_ids).
def fully_visible_group_user_scope(options={})
group_user_scope(options).where('group_memberships.group_id' => fully_visible_group_ids)
end
def self_scope(options={})
@user.shard.activate do
base_scope(options).where('users.id' => @user)
end
end
# ====================================================================
# | uncached utility methods used while populating external caches |
# ====================================================================
def uncached_visible_section_ids
ids = {}
section_visible_courses.each do |course|
ids[course.id] = uncached_visible_section_ids_in_course(course)
end
ids
end
def uncached_visible_section_ids_in_course(course)
course.section_visibilities_for(@user).map{ |s| s[:course_section_id] }
end
def uncached_observed_student_ids
ids = {}
restricted_visibility_courses.each do |course|
ids[course.id] = uncached_observed_student_ids_in_course(course)
end
ids
end
# the associated_user_id should be local to the current shard, but only
# having it in that format and from those enrollments on the current shard
# is acceptable, as this is specific to a course and the enrollments all
# live on the same shard as the course
def uncached_observed_student_ids_in_course(course)
course.section_visibilities_for(@user).map{ |s| s[:associated_user_id] }.compact
end
def uncached_linked_observer_ids
# because this is a has_many, we can't just rely on Shard.current for id
# translation magic. we *have* to use with_each_shard... but we're
# already in a with_each_shard from shard_cached, so we can restrict it
# to this shard
@user.observee_enrollments.shard(Shard.current).map(&:global_user_id)
end
def uncached_visible_account_ids
# ditto
@user.associated_accounts.shard(Shard.current).
select{ |account| account.grants_right?(@user, :read_roster) }.
map(&:id)
end
def uncached_fully_visible_group_ids
# ditto for current groups
course_group_ids = uncached_group_ids_in_courses(recent_fully_visible_courses)
own_group_ids = @user.current_groups.shard(Shard.current).pluck(:id)
(course_group_ids + own_group_ids).uniq
end
def uncached_section_visible_group_ids
course_group_ids = uncached_group_ids_in_courses(recent_section_visible_courses)
course_group_ids - fully_visible_group_ids
end
def uncached_group_ids_in_courses(courses)
Group.active.where(:context_type => 'Course', :context_id => courses).pluck(:id)
end
def uncached_messageable_sections
fully_visible = CourseSection.
where(:course_id => fully_visible_courses)
section_visible = CourseSection.
joins(:enrollments).
where(:course_id => section_visible_courses, :enrollments => { :user_id => @user })
(fully_visible + section_visible).
group_by(&:course_id).values.
select{ |ids| ids.size > 1 }.flatten
end
def uncached_messageable_groups
fully_visible_scope = GroupMembership.
select("group_memberships.group_id AS group_id").
distinct.
joins(:user, :group).
where(:workflow_state => 'accepted').
where("groups.workflow_state<>'deleted'").
where(MessageableUser::AVAILABLE_CONDITIONS).
where(:group_id => fully_visible_group_ids)
section_visible_scope = GroupMembership.
select("group_memberships.group_id AS group_id").
distinct.
joins(:user, :group).
joins(<<-SQL).
INNER JOIN #{Enrollment.quoted_table_name} ON
enrollments.user_id=users.id AND
enrollments.course_id=groups.context_id
INNER JOIN #{Course.quoted_table_name} ON courses.id=enrollments.course_id
SQL
where(:workflow_state => 'accepted').
where("groups.workflow_state<>'deleted'").
where(MessageableUser::AVAILABLE_CONDITIONS).
where(:groups => { :context_type => 'Course' }).
where(self.class.enrollment_conditions).
where(:enrollments => { :course_section_id => visible_section_ids_in_courses(recent_section_visible_courses) })
if student_courses.present?
section_visible_scope = section_visible_scope.
where(observer_restriction_clause)
end
group_ids = [fully_visible_scope, section_visible_scope].map{ |scope| scope.map(&:group_id) }.flatten.uniq
if group_ids.present?
Group.where(id: group_ids).to_a
else
[]
end
end
# ==================================================
# | calculations cached externally with sharding |
# ==================================================
# the optional methods list is a list of methods to call (not results of a
# method call, since if there are multiple we want to distinguish them) to
# get additional objects to include in the per-shard cache keys
def shard_cached(key, *methods)
@shard_caches ||= {}
@shard_caches[key] ||=
begin
by_shard = {}
Shard.with_each_shard(@user.in_region_associated_shards) do
shard_key = [@user, 'messageable_user', key]
methods.each do |method|
canonical = send(method).cache_key
shard_key << method
shard_key << Digest::MD5.hexdigest(canonical)
end
by_shard[Shard.current] = Rails.cache.fetch(shard_key.cache_key, :expires_in => 1.day) { yield }
end
by_shard
end
end
def all_courses_by_shard
@all_courses_by_shard ||=
@user.courses_with_primary_enrollment(:current_and_concluded_courses, nil, :include_completed_courses => true).
group_by(&:shard)
end
def visible_section_ids_by_shard
shard_cached('visible_section_ids', :section_visible_courses) do
uncached_visible_section_ids
end
end
def observed_student_ids_by_shard
shard_cached('observed_student_ids', :restricted_visibility_courses) do
uncached_observed_student_ids
end
end
# unlike the others, these aren't partitioned by shard; whichever shard
# you're on, you'll get the full set when you call this method, since these
# are user ids. but they are cached on the object by shard so that we
# transpose from global ids to shard relative ids at most once per shard.
def linked_observer_ids_by_shard
@global_linked_observer_ids ||=
begin
by_shard = shard_cached('linked_observer_ids') do
uncached_linked_observer_ids
end
by_shard.values.flatten.uniq
end
@linked_observer_ids_by_shard ||= Hash.new do |hash,shard|
hash[shard] = []
Shard.partition_by_shard(@global_linked_observer_ids) do |shard_ids|
if Shard.current == shard
hash[shard].concat(shard_ids)
else
hash[shard].concat(shard_ids.map{ |id| Shard.global_id_for(id) })
end
end
end
end
def visible_account_ids_by_shard
shard_cached('visible_account_ids') do
uncached_visible_account_ids
end
end
def fully_visible_group_ids_by_shard
shard_cached('fully_visible_group_ids', :recent_fully_visible_courses) do
uncached_fully_visible_group_ids
end
end
def section_visible_group_ids_by_shard
shard_cached('section_visible_group_ids', :recent_section_visible_courses, :recent_fully_visible_courses) do
uncached_section_visible_group_ids
end
end
def messageable_sections_by_shard
shard_cached('messageable_sections', :all_courses) do
uncached_messageable_sections
end
end
def messageable_groups_by_shard
shard_cached('messageable_groups', :fully_visible_group_ids, :section_visible_group_ids) do
uncached_messageable_groups
end
end
# =======================================================================
# | shard-implicit object-cached accessors to the main sharded caches |
# =======================================================================
def all_courses
all_courses_by_shard[Shard.current] || []
end
def course_index
@course_index_by_shard ||= {}
@course_index_by_shard[Shard.current] ||= all_courses.index_by(&:id)
end
def course_visibility(course)
@course_visibilities ||= {}
@course_visibilities[course.global_id] ||=
course.enrollment_visibility_level_for(@user, course.section_visibilities_for(@user), true)
end
def all_courses_by_visibility(visibility)
@all_courses_by_visibility_by_shard ||= {}
@all_courses_by_visibility_by_shard[Shard.current] ||=
all_courses.group_by{ |course| course_visibility(course) }
@all_courses_by_visibility_by_shard[Shard.current][visibility] ||= []
end
def fully_visible_courses
all_courses_by_visibility(:full)
end
def section_visible_courses
all_courses_by_visibility(:sections)
end
def restricted_visibility_courses
all_courses_by_visibility(:restricted)
end
def recent_courses
@recent_courses_by_shard ||= {}
@recent_courses_by_shard[Shard.current] ||=
all_courses.reject{ |course| course.conclude_at && course.conclude_at < 1.month.ago }
end
def recent_courses_by_visibility(visibility)
@recent_courses_by_visibility_by_shard ||= {}
@recent_courses_by_visibility_by_shard[Shard.current] ||=
recent_courses.group_by{ |course| course_visibility(course) }
@recent_courses_by_visibility_by_shard[Shard.current][visibility] ||= []
end
def recent_fully_visible_courses
recent_courses_by_visibility(:full)
end
def recent_section_visible_courses
recent_courses_by_visibility(:sections)
end
def student_courses
@student_courses_by_shard ||= {}
@student_courses_by_shard[Shard.current] ||= all_courses.
select{ |course| course.primary_enrollment_type == 'StudentEnrollment' }
end
def visible_section_ids_in_courses(courses)
visible_section_ids = visible_section_ids_by_shard[Shard.current] || {}
visible_section_ids.slice(*courses.map(&:id)).values.flatten.uniq
end
def observed_student_ids_in_courses(courses)
observed_student_ids = observed_student_ids_by_shard[Shard.current] || {}
observed_student_ids.slice(*courses.map(&:id)).values.flatten.uniq
end
def linked_observer_ids
linked_observer_ids_by_shard[Shard.current] || []
end
def visible_account_ids
visible_account_ids_by_shard[Shard.current] || []
end
def fully_visible_group_ids
fully_visible_group_ids_by_shard[Shard.current] || []
end
def section_visible_group_ids
section_visible_group_ids_by_shard[Shard.current] || []
end
end
end