api functionality: unassigned group members, with search
adds list and search users functionality to group_category api, including the ability to filter to only list/search unassigned users. depends on #CNVS-6152 test plan) 0) verify that the api doc is error free and makes sense 1) make the following requests for both a course and an acccount group category, then verify the results: a) list all the users (i.e. no search_term) b) list all the unassigned users (i.e. no search_term) c) search users (e.g. search_term=bob ) d) search unassigned users (e.g. search_term=bob&unassigned=true) fixes #CNVS-6151 Change-Id: I99b33f29531579478ccece595a20971a1f8ad914 Reviewed-on: https://gerrit.instructure.com/21292 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Jon Jensen <jon@instructure.com> Product-Review: Marc LeGendre <marc@instructure.com> QA-Review: Marc LeGendre <marc@instructure.com> Tested-by: Landon Wilkins <lwilkins@instructure.com>
This commit is contained in:
parent
8f95d3b6de
commit
6441ef9d14
|
@ -55,7 +55,7 @@
|
|||
class GroupCategoriesController < ApplicationController
|
||||
before_filter :get_context
|
||||
before_filter :require_context, :only => [:create, :index]
|
||||
before_filter :get_category_context, :only => [:show, :update, :destroy, :groups]
|
||||
before_filter :get_category_context, :only => [:show, :update, :destroy, :groups, :users]
|
||||
|
||||
include Api::V1::Attachment
|
||||
include Api::V1::GroupCategory
|
||||
|
@ -190,7 +190,7 @@ class GroupCategoriesController < ApplicationController
|
|||
# categories can not be deleted, i.e. "communities", "student_organized", and "imported".
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
|
||||
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
|
||||
# -X DELETE \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
|
@ -233,6 +233,57 @@ class GroupCategoriesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
include Api::V1::User
|
||||
# @API List users
|
||||
#
|
||||
# Returns a list of users in the group category.
|
||||
#
|
||||
# @argument search_term (optional)
|
||||
# The partial name or full ID of the users to match and return in the results list.
|
||||
# Must be at least 3 characters.
|
||||
#
|
||||
# @argument unassigned (optional)
|
||||
# Set this value to true if you wish only to search unassigned users in the group category
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/group_categories/1/users \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
# @returns [User]
|
||||
def users
|
||||
if @context.is_a? Course
|
||||
return unless authorized_action(@context, @current_user, :read_roster)
|
||||
else
|
||||
return unless authorized_action(@context, @current_user, :read)
|
||||
end
|
||||
|
||||
search_term = params[:search_term]
|
||||
|
||||
if search_term && search_term.size < 3
|
||||
return render \
|
||||
:json => {
|
||||
"status" => "argument_error",
|
||||
"message" => "search_term of 3 or more characters is required" },
|
||||
:status => :bad_request
|
||||
end
|
||||
|
||||
search_params = params.slice(:search_term)
|
||||
search_params[:enrollment_role] = "StudentEnrollment" if @context.is_a? Course
|
||||
|
||||
@group_category ||= @context.group_categories.find_by_id(params[:category_id])
|
||||
exclude_groups = params[:unassigned] ? @group_category.groups.active : []
|
||||
search_params[:exclude_groups] = exclude_groups
|
||||
|
||||
if search_term
|
||||
users = UserSearch.for_user_in_context(search_term, @context, @current_user, search_params)
|
||||
else
|
||||
users = UserSearch.scope_for(@context, @current_user, search_params)
|
||||
end
|
||||
|
||||
users = Api.paginate(users, self, api_v1_group_category_users_url)
|
||||
render :json => users.map { |u| user_json(u, @current_user, session, [], @context) }
|
||||
end
|
||||
|
||||
def populate_group_category_from_params
|
||||
if api_request?
|
||||
args = params
|
||||
|
|
|
@ -331,7 +331,11 @@ class Account < ActiveRecord::Base
|
|||
end
|
||||
res
|
||||
end
|
||||
|
||||
|
||||
def users_visible_to(user)
|
||||
self.grants_right?(user, nil, :read) ? self.all_users : self.all_users.where("?", false)
|
||||
end
|
||||
|
||||
def users_name_like(query="")
|
||||
@cached_users_name_like ||= {}
|
||||
@cached_users_name_like[query] ||= self.fast_all_users.name_like(query)
|
||||
|
@ -362,10 +366,10 @@ class Account < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def users_not_in_groups_sql(groups, opts={})
|
||||
["SELECT u.id, u.name
|
||||
FROM users u
|
||||
INNER JOIN user_account_associations uaa on uaa.user_id = u.id
|
||||
WHERE uaa.account_id = ? AND u.workflow_state != 'deleted'
|
||||
["SELECT users.id, users.name
|
||||
FROM users
|
||||
INNER JOIN user_account_associations uaa on uaa.user_id = users.id
|
||||
WHERE uaa.account_id = ? AND users.workflow_state != 'deleted'
|
||||
#{Group.not_in_group_sql_fragment(groups)}
|
||||
#{"ORDER BY #{opts[:order_by]}" if opts[:order_by].present?}", self.id]
|
||||
end
|
||||
|
@ -375,7 +379,7 @@ class Account < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def paginate_users_not_in_groups(groups, page, per_page = 15)
|
||||
User.paginate_by_sql(users_not_in_groups_sql(groups, :order_by => "#{User.sortable_name_order_by_clause('u')} ASC"),
|
||||
User.paginate_by_sql(users_not_in_groups_sql(groups, :order_by => "#{User.sortable_name_order_by_clause('users')} ASC"),
|
||||
:page => page, :per_page => per_page)
|
||||
end
|
||||
|
||||
|
|
|
@ -509,9 +509,9 @@ class Course < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def users_not_in_groups_sql(groups, opts={})
|
||||
["SELECT DISTINCT u.id, u.name#{", #{opts[:order_by]}" if opts[:order_by].present?}
|
||||
FROM users u
|
||||
INNER JOIN enrollments e ON e.user_id = u.id
|
||||
["SELECT DISTINCT users.id, users.name#{", #{opts[:order_by]}" if opts[:order_by].present?}
|
||||
FROM users
|
||||
INNER JOIN enrollments e ON e.user_id = users.id
|
||||
WHERE e.course_id = ? AND e.workflow_state NOT IN ('rejected', 'completed', 'deleted') AND e.type = 'StudentEnrollment'
|
||||
#{Group.not_in_group_sql_fragment(groups)}
|
||||
#{"ORDER BY #{opts[:order_by]}" if opts[:order_by].present?}
|
||||
|
@ -523,7 +523,7 @@ class Course < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def paginate_users_not_in_groups(groups, page, per_page = 15)
|
||||
User.paginate_by_sql(users_not_in_groups_sql(groups, :order_by => "#{User.sortable_name_order_by_clause('u')}", :order_by_dir => "ASC"),
|
||||
User.paginate_by_sql(users_not_in_groups_sql(groups, :order_by => "#{User.sortable_name_order_by_clause('users')}", :order_by_dir => "ASC"),
|
||||
:page => page, :per_page => per_page)
|
||||
end
|
||||
|
||||
|
|
|
@ -152,9 +152,9 @@ class Group < ActiveRecord::Base
|
|||
Group.find(ids)
|
||||
end
|
||||
|
||||
def self.not_in_group_sql_fragment(groups)
|
||||
"AND NOT EXISTS (SELECT * FROM group_memberships gm
|
||||
WHERE gm.user_id = u.id AND
|
||||
def self.not_in_group_sql_fragment(groups, prepend_and = true)
|
||||
"#{"AND" if prepend_and} NOT EXISTS (SELECT * FROM group_memberships gm
|
||||
WHERE gm.user_id = users.id AND
|
||||
gm.workflow_state != 'deleted' AND
|
||||
gm.group_id IN (#{groups.map(&:id).join ','}))" unless groups.empty?
|
||||
|
||||
|
|
|
@ -1156,6 +1156,7 @@ ActionController::Routing::Routes.draw do |map|
|
|||
group_categories.post 'accounts/:account_id/group_categories', :action => :create
|
||||
group_categories.post 'courses/:course_id/group_categories', :action => :create
|
||||
group_categories.get 'group_categories/:group_category_id/groups', :action => :groups, :path_name => 'group_category_groups'
|
||||
group_categories.get 'group_categories/:group_category_id/users', :action => :users, :path_name => 'group_category_users'
|
||||
end
|
||||
|
||||
api.with_options(:controller => :progress) do |progress|
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module UserSearch
|
||||
|
||||
def self.for_user_in_context(search_term, context, searcher, options = {})
|
||||
base_scope = scope_for(context, searcher, options.slice(:enrollment_type, :enrollment_role))
|
||||
base_scope = scope_for(context, searcher, options.slice(:enrollment_type, :enrollment_role, :exclude_groups))
|
||||
if search_term.to_s =~ Api::ID_REGEX
|
||||
user = base_scope.find_by_id(search_term)
|
||||
return [user] if user
|
||||
|
@ -32,6 +32,7 @@ module UserSearch
|
|||
def self.scope_for(context, searcher, options={})
|
||||
enrollment_role = Array(options[:enrollment_role]) if options[:enrollment_role]
|
||||
enrollment_type = Array(options[:enrollment_type]) if options[:enrollment_type]
|
||||
exclude_groups = Array(options[:exclude_groups]) if options[:exclude_groups]
|
||||
|
||||
users = context.users_visible_to(searcher).uniq.order_by_sortable_name
|
||||
|
||||
|
@ -44,6 +45,11 @@ module UserSearch
|
|||
end
|
||||
users = users.where(:enrollments => { :type => enrollment_type })
|
||||
end
|
||||
|
||||
if exclude_groups
|
||||
users = users.where(Group.not_in_group_sql_fragment(exclude_groups, false))
|
||||
end
|
||||
|
||||
users
|
||||
end
|
||||
|
||||
|
|
|
@ -42,6 +42,90 @@ describe "Group Categories API", :type => :integration do
|
|||
@category = GroupCategory.student_organized_for(@course)
|
||||
end
|
||||
|
||||
describe "users" do
|
||||
let(:api_url) { "/api/v1/group_categories/#{@category2.id}/users.json" }
|
||||
let(:api_route) do
|
||||
{
|
||||
:controller => 'group_categories',
|
||||
:action => 'users',
|
||||
:group_category_id => @category2.to_param,
|
||||
:format => 'json'
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
@user = user(:name => "joe mcCool")
|
||||
@course.enroll_user(@user,'TeacherEnrollment',:enrollment_state => :active)
|
||||
|
||||
@user_waldo = user(:name => "waldo")
|
||||
@course.enroll_user(@user,'StudentEnrollment',:enrollment_state => :active)
|
||||
|
||||
|
||||
6.times { course_with_student({:course => @course}) }
|
||||
@user = @course.teacher_enrollments.first.user
|
||||
|
||||
json = api_call(:post, "/api/v1/courses/#{@course.id}/group_categories",
|
||||
@category_path_options.merge(:action => 'create',
|
||||
:course_id => @course.to_param),
|
||||
{ 'name' => @name, 'split_group_count' => 3 })
|
||||
|
||||
@user_antisocial = user(:name => "antisocial")
|
||||
@course.enroll_user(@user,'StudentEnrollment',:enrollment_state => :active)
|
||||
|
||||
@category2 = GroupCategory.find(json["id"])
|
||||
|
||||
@category_users = @category2.groups.inject([]){|result, group| result.concat(group.users)} << @user
|
||||
@category_assigned_users = @category2.groups.active.inject([]){|result, group| result.concat(group.users)}
|
||||
@category_unassigned_users = @category_users - @category_assigned_users
|
||||
end
|
||||
|
||||
it "should return users in a group_category" do
|
||||
expected_keys = %w{id name sortable_name short_name}
|
||||
json = api_call(:get, api_url, api_route)
|
||||
json.count.should == 8
|
||||
json.each do |user|
|
||||
(user.keys & expected_keys).sort.should == expected_keys.sort
|
||||
@category_users.map(&:id).should include(user['id'])
|
||||
end
|
||||
end
|
||||
|
||||
it "should return 401 for users outside the group_category" do
|
||||
user # ?
|
||||
raw_api_call(:get, api_url, api_route)
|
||||
response.code.should == '401'
|
||||
end
|
||||
|
||||
it "returns an error when search_term is fewer than 3 characters" do
|
||||
json = api_call(:get, api_url, api_route, {:search_term => '12'}, {}, :expected_status => 400)
|
||||
json["status"].should == "argument_error"
|
||||
json["message"].should == "search_term of 3 or more characters is required"
|
||||
end
|
||||
|
||||
it "returns a list of users" do
|
||||
expected_keys = %w{id name sortable_name short_name}
|
||||
|
||||
json = api_call(:get, api_url, api_route, {:search_term => 'waldo'})
|
||||
|
||||
json.count.should == 1
|
||||
json.each do |user|
|
||||
(user.keys & expected_keys).sort.should == expected_keys.sort
|
||||
@category_users.map(&:id).should include(user['id'])
|
||||
end
|
||||
end
|
||||
|
||||
it "returns a list of unassigned users" do
|
||||
expected_keys = %w{id name sortable_name short_name}
|
||||
|
||||
json = api_call(:get, api_url, api_route, {:search_term => 'antisocial', :unassigned => 'true'})
|
||||
|
||||
json.count.should == 1
|
||||
json.each do |user|
|
||||
(user.keys & expected_keys).sort.should == expected_keys.sort
|
||||
@category_unassigned_users.map(&:id).should include(user['id'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "teacher actions with no group" do
|
||||
before do
|
||||
@name = 'some group name'
|
||||
|
|
Loading…
Reference in New Issue