2017-04-28 10:57:24 +08:00
|
|
|
#
|
|
|
|
# Copyright (C) 2012 - 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/>.
|
|
|
|
|
2012-12-29 03:28:09 +08:00
|
|
|
module UserSearch
|
|
|
|
|
2013-08-13 22:22:50 +08:00
|
|
|
def self.for_user_in_context(search_term, context, searcher, session=nil, options = {})
|
2013-07-13 05:25:33 +08:00
|
|
|
search_term = search_term.to_s
|
2015-08-11 23:16:47 +08:00
|
|
|
return User.none if search_term.strip.empty?
|
2017-07-18 06:07:20 +08:00
|
|
|
base_scope = scope_for(context, searcher, options.slice(:enrollment_type, :enrollment_role,
|
2017-06-15 06:40:09 +08:00
|
|
|
:enrollment_role_id, :exclude_groups, :enrollment_state, :include_inactive_enrollments, :sort, :order))
|
2018-02-22 16:33:58 +08:00
|
|
|
@db_union = nil
|
2013-02-26 02:14:24 +08:00
|
|
|
if search_term.to_s =~ Api::ID_REGEX
|
2014-02-03 21:54:01 +08:00
|
|
|
db_id = Shard.relative_id_for(search_term, Shard.current, Shard.current)
|
2016-03-11 07:46:28 +08:00
|
|
|
scope = base_scope.where(id: db_id)
|
2018-02-22 16:33:58 +08:00
|
|
|
# if the search_term is an id that is really big we will assume it is a
|
|
|
|
# canvas global_id
|
|
|
|
return scope if db_id > Shard::IDS_PER_SHARD
|
|
|
|
@db_union = "SELECT id FROM #{User.quoted_table_name} WHERE users.id IN (:db_id) \n UNION" if scope.exists?
|
|
|
|
unless SearchTermHelper.valid_search_term?(search_term)
|
|
|
|
return User.none unless scope.exists?
|
2013-07-13 05:25:33 +08:00
|
|
|
end
|
2013-02-26 02:14:24 +08:00
|
|
|
# no user found by id, so lets go ahead with the regular search, maybe this person just has a ton of numbers in their name
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
2013-07-17 21:34:01 +08:00
|
|
|
SearchTermHelper.validate_search_term(search_term)
|
2013-07-13 05:25:33 +08:00
|
|
|
|
2013-08-13 22:22:50 +08:00
|
|
|
unless context.grants_right?(searcher, session, :manage_students) ||
|
|
|
|
context.grants_right?(searcher, session, :manage_admin_users)
|
|
|
|
restrict_search = true
|
|
|
|
end
|
2017-04-20 03:28:46 +08:00
|
|
|
context.shard.activate do
|
2018-02-06 12:24:54 +08:00
|
|
|
# TODO: Need to optimize this as it's not using the base scope filter for the conditions statement query
|
2018-03-19 23:47:59 +08:00
|
|
|
base_scope.where(conditions_statement(search_term, context.root_account, {restrict_search: restrict_search}))
|
2017-04-20 03:28:46 +08:00
|
|
|
end
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
2017-10-18 02:12:48 +08:00
|
|
|
def self.conditions_statement(search_term, root_account, options={})
|
2012-12-29 03:28:09 +08:00
|
|
|
pattern = like_string_for(search_term)
|
|
|
|
conditions = []
|
|
|
|
|
2013-08-13 22:22:50 +08:00
|
|
|
if complex_search_enabled? && !options[:restrict_search]
|
2018-02-22 16:33:58 +08:00
|
|
|
# db_id is only used if the search_term.to_s =~ Api::ID_REGEX
|
|
|
|
params = {pattern: pattern, account: root_account, path_type: CommunicationChannel::TYPE_EMAIL, db_id: search_term}
|
|
|
|
conditions << complex_sql << params
|
2012-12-29 03:28:09 +08:00
|
|
|
else
|
2018-02-22 16:33:58 +08:00
|
|
|
conditions << like_condition('users.name') << {pattern: pattern}
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
conditions
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.like_string_for(search_term)
|
|
|
|
pattern_type = (gist_search_enabled? ? :full : :right)
|
2013-07-19 02:19:35 +08:00
|
|
|
wildcard_pattern(search_term, :type => pattern_type, :case_sensitive => false)
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
2013-06-06 09:21:29 +08:00
|
|
|
def self.scope_for(context, searcher, options={})
|
2014-09-08 20:48:45 +08:00
|
|
|
enrollment_roles = Array(options[:enrollment_role]) if options[:enrollment_role]
|
|
|
|
enrollment_role_ids = Array(options[:enrollment_role_id]) if options[:enrollment_role_id]
|
|
|
|
enrollment_types = Array(options[:enrollment_type]) if options[:enrollment_type]
|
2014-11-06 01:22:32 +08:00
|
|
|
enrollment_states = Array(options[:enrollment_state]) if options[:enrollment_state]
|
|
|
|
include_prior_enrollments = !options[:enrollment_state].nil?
|
2015-11-21 00:44:25 +08:00
|
|
|
include_inactive_enrollments = !!options[:include_inactive_enrollments]
|
2013-06-07 20:59:27 +08:00
|
|
|
exclude_groups = Array(options[:exclude_groups]) if options[:exclude_groups]
|
2012-12-29 03:28:09 +08:00
|
|
|
|
2014-11-06 01:22:32 +08:00
|
|
|
users = if context.is_a?(Account)
|
2015-11-13 01:21:53 +08:00
|
|
|
User.of_account(context).active
|
2016-09-28 01:37:35 +08:00
|
|
|
elsif context.is_a?(Course)
|
2015-11-21 00:44:25 +08:00
|
|
|
context.users_visible_to(searcher, include_prior_enrollments,
|
2017-03-14 05:55:15 +08:00
|
|
|
enrollment_state: enrollment_states, include_inactive: include_inactive_enrollments).distinct
|
2014-11-06 01:22:32 +08:00
|
|
|
else
|
2017-07-19 22:13:01 +08:00
|
|
|
context.users_visible_to(searcher).distinct
|
2014-11-06 01:22:32 +08:00
|
|
|
end
|
2017-06-15 06:40:09 +08:00
|
|
|
|
|
|
|
users = if options[:sort] == "last_login"
|
|
|
|
if options[:order] == 'desc'
|
|
|
|
users.order("MAX(current_login_at) desc, id desc")
|
|
|
|
else
|
|
|
|
users.order("MAX(current_login_at), id")
|
|
|
|
end
|
|
|
|
elsif options[:sort] == "username"
|
|
|
|
if options[:order] == 'desc'
|
|
|
|
users.order_by_sortable_name(direction: :descending)
|
|
|
|
else
|
|
|
|
users.order_by_sortable_name
|
|
|
|
end
|
|
|
|
elsif options[:sort] == "email"
|
|
|
|
if options[:order] == 'desc'
|
|
|
|
users.order("(SELECT unique_id FROM #{Pseudonym.quoted_table_name}
|
|
|
|
WHERE #{Pseudonym.quoted_table_name}.user_id = #{User.quoted_table_name}.id
|
|
|
|
AND unique_id ~* \'\\A([^@\\s]+)@((?:[-a-z0-9]+\\.)+[a-z]{2,})\\Z\'
|
|
|
|
LIMIT 1)
|
|
|
|
DESC, id DESC")
|
|
|
|
else
|
|
|
|
users.order("(SELECT unique_id FROM #{Pseudonym.quoted_table_name}
|
|
|
|
WHERE #{Pseudonym.quoted_table_name}.user_id = #{User.quoted_table_name}.id
|
|
|
|
AND unique_id ~* \'\\A([^@\\s]+)@((?:[-a-z0-9]+\\.)+[a-z]{2,})\\Z\'
|
|
|
|
LIMIT 1)")
|
|
|
|
end
|
|
|
|
elsif options[:sort] == "sis_id"
|
|
|
|
if options[:order] == 'desc'
|
|
|
|
users.order("(SELECT sis_user_id FROM #{Pseudonym.quoted_table_name}
|
|
|
|
WHERE #{Pseudonym.quoted_table_name}.user_id = #{User.quoted_table_name}.id
|
|
|
|
LIMIT 1) DESC, id DESC")
|
|
|
|
else
|
|
|
|
users.order("(SELECT sis_user_id FROM #{Pseudonym.quoted_table_name}
|
|
|
|
WHERE #{Pseudonym.quoted_table_name}.user_id = #{User.quoted_table_name}.id
|
|
|
|
LIMIT 1)")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
users.order_by_sortable_name
|
|
|
|
end
|
|
|
|
|
2014-09-08 20:48:45 +08:00
|
|
|
if enrollment_role_ids || enrollment_roles
|
2018-03-19 23:47:59 +08:00
|
|
|
users = users.joins(:not_removed_enrollments).distinct if context.is_a?(Account)
|
2017-07-18 06:07:20 +08:00
|
|
|
roles = if enrollment_role_ids
|
|
|
|
enrollment_role_ids.map{|id| Role.get_role_by_id(id)}.compact
|
|
|
|
else
|
|
|
|
enrollment_roles.map{|name| context.is_a?(Account) ? context.get_course_role_by_name(name) :
|
|
|
|
context.account.get_course_role_by_name(name)}.compact
|
|
|
|
end
|
2018-03-19 23:47:59 +08:00
|
|
|
users = users.where("role_id IN (?)", roles.map(&:id))
|
2014-09-08 20:48:45 +08:00
|
|
|
elsif enrollment_types
|
2016-03-15 06:48:10 +08:00
|
|
|
enrollment_types = enrollment_types.map { |e| "#{e.camelize}Enrollment" }
|
2014-09-08 20:48:45 +08:00
|
|
|
if enrollment_types.any?{ |et| !Enrollment.readable_types.keys.include?(et) }
|
2013-01-09 06:07:36 +08:00
|
|
|
raise ArgumentError, 'Invalid Enrollment Type'
|
|
|
|
end
|
2016-07-07 04:23:58 +08:00
|
|
|
if context.is_a?(Group) && context.context_type == "Course"
|
|
|
|
users = users.joins(:enrollments).where(:enrollments => {:course_id => context.context_id})
|
|
|
|
end
|
|
|
|
users = users.where(:enrollments => { :type => enrollment_types })
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
2013-06-07 20:59:27 +08:00
|
|
|
|
|
|
|
if exclude_groups
|
2013-11-21 08:15:12 +08:00
|
|
|
users = users.where(Group.not_in_group_sql_fragment(exclude_groups))
|
2013-06-07 20:59:27 +08:00
|
|
|
end
|
|
|
|
|
2012-12-29 03:28:09 +08:00
|
|
|
users
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def self.complex_sql
|
2016-04-29 03:08:52 +08:00
|
|
|
<<-SQL
|
2017-10-18 02:12:48 +08:00
|
|
|
users.id IN (
|
|
|
|
SELECT user_id FROM #{Pseudonym.quoted_table_name}
|
|
|
|
WHERE (#{like_condition('pseudonyms.sis_user_id')} OR
|
|
|
|
#{like_condition('pseudonyms.unique_id')})
|
|
|
|
AND pseudonyms.workflow_state='active'
|
2018-02-22 16:33:58 +08:00
|
|
|
AND pseudonyms.account_id = :account
|
2017-10-18 02:12:48 +08:00
|
|
|
UNION
|
|
|
|
SELECT id FROM #{User.quoted_table_name} WHERE (#{like_condition('users.name')})
|
|
|
|
UNION
|
2018-02-22 16:33:58 +08:00
|
|
|
#{@db_union}
|
2018-02-22 17:13:38 +08:00
|
|
|
SELECT communication_channels.user_id FROM #{CommunicationChannel.quoted_table_name}
|
|
|
|
WHERE EXISTS (SELECT 1 FROM #{UserAccountAssociation.quoted_table_name} AS uaa
|
|
|
|
WHERE uaa.account_id= :account
|
|
|
|
AND uaa.user_id=communication_channels.user_id)
|
|
|
|
AND communication_channels.path_type = :path_type
|
2017-10-18 02:12:48 +08:00
|
|
|
AND #{like_condition('communication_channels.path')}
|
2018-02-22 16:33:58 +08:00
|
|
|
AND communication_channels.workflow_state IN ('active', 'unconfirmed')
|
2017-10-18 02:12:48 +08:00
|
|
|
)
|
2012-12-29 03:28:09 +08:00
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.gist_search_enabled?
|
2013-10-05 04:02:49 +08:00
|
|
|
Setting.get('user_search_with_gist', 'true') == 'true'
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.complex_search_enabled?
|
2013-10-05 04:02:49 +08:00
|
|
|
Setting.get('user_search_with_full_complexity', 'true') == 'true'
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.like_condition(value)
|
2018-02-22 16:33:58 +08:00
|
|
|
ActiveRecord::Base.like_condition(value, 'lower(:pattern)')
|
2012-12-29 03:28:09 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.wildcard_pattern(value, options)
|
|
|
|
ActiveRecord::Base.wildcard_pattern(value, options)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
end
|