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:
Landon Wilkins 2013-06-07 06:59:27 -06:00 committed by Landon Wilkins
parent 8f95d3b6de
commit 6441ef9d14
7 changed files with 162 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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|

View File

@ -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

View File

@ -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'