adds api endpoint for randomly assigning unassigned members in a group category

fixes CNVS-6696

test plan:
1) use postman to make a request to this new endpoint for a group category that
   has unassigned members
   --asynchronous example:
   POST http://localhost:3000/api/v1/group_categories/<group_category_id>/assign_unassigned_members
   --synchronous example:
   POST http://localhost:3000/api/v1/group_categories/<group_category_id> \
   /assign_unassigned_members?sync=true
2) verify that the group category's unassigned members have now been assigned
   among the available groups
   --NOTE: this may take a minute depending on the number of unassigned members
3) verify the api documentation for Group Categories - "Assign unassigned members" makes sense
4) verify that an asychronous request returns a Progress JSON
   (see api documentation for example)
5) verify that a synchronous request returns an array of Group Memberships
   (see documentation for example)

NOTE: I've made some tweaks to the current Groups page such that you can
  still randomly assign students. However, this shouldn't matter because
  it's all getting replaced anyway.

Change-Id: I894ff2b1e11c7919a110b5159710683869caedc4
Reviewed-on: https://gerrit.instructure.com/22044
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Product-Review: Marc LeGendre <marc@instructure.com>
QA-Review: Marc LeGendre <marc@instructure.com>
This commit is contained in:
Landon Wilkins 2013-07-05 10:32:47 -06:00 committed by Landon Wilkins
parent c2a5ae1239
commit 261d7725dc
8 changed files with 224 additions and 106 deletions

View File

@ -55,11 +55,12 @@
class GroupCategoriesController < ApplicationController
before_filter :get_context
before_filter :require_context, :only => [:create, :index]
before_filter :get_category_context, :only => [:show, :update, :destroy, :groups, :users]
before_filter :get_category_context, :only => [:show, :update, :destroy, :groups, :users, :assign_unassigned_members]
include Api::V1::Attachment
include Api::V1::GroupCategory
include Api::V1::Group
include Api::V1::Progress
SETTABLE_GROUP_ATTRIBUTES = %w(name description join_level is_public group_category avatar_attachment)
@ -284,6 +285,119 @@ class GroupCategoriesController < ApplicationController
render :json => users.map { |u| user_json(u, @current_user, session, [], @context) }
end
# @API Assign unassigned members
#
# Assign all unassigned members as evenly as possible among the existing
# student groups.
#
# @argument sync (optional)
# The assigning is done asynchronously by default. If you would like to
# override this and have the assigning done synchronously, set this value
# to true.
#
# @example_request
# curl https://<canvas>/api/v1/group_categories/1/assign_unassigned_members \
# -H 'Authorization: Bearer <token>'
#
# @example_response
# # Progress (default)
# {
# "completion": 0,
# "context_id": 20,
# "context_type": "GroupCategory",
# "created_at": "2013-07-05T10:57:48-06:00",
# "id": 2,
# "message": null,
# "tag": "assign_unassigned_members",
# "updated_at": "2013-07-05T10:57:48-06:00",
# "user_id": null,
# "workflow_state": "running",
# "url": "http://localhost:3000/api/v1/progress/2"
# }
#
# @example_response
# # New Group Memberships (when sync = true)
# [
# {
# "id": 65,
# "new_members": [
# {
# "user_id": 2,
# "name": "Sam",
# "display_name": "Sam",
# "sections": [
# {
# "section_id": 1,
# "section_code": "Section 1"
# }
# ]
# },
# {
# "user_id": 3,
# "name": "Sue",
# "display_name": "Sue",
# "sections": [
# {
# "section_id": 2,
# "section_code": "Section 2"
# }
# ]
# }
# ]
# },
# {
# "id": 66,
# "new_members": [
# {
# "user_id": 5,
# "name": "Joe",
# "display_name": "Joe",
# "sections": [
# {
# "section_id": 2,
# "section_code": "Section 2"
# }
# ]
# },
# {
# "user_id": 11,
# "name": "Cecil",
# "display_name": "Cecil",
# "sections": [
# {
# "section_id": 3,
# "section_code": "Section 3"
# }
# ]
# }
# ]
# }
# ]
#
# @returns Group Membership or Progress
def assign_unassigned_members
return unless authorized_action(@context, @current_user, :manage_groups)
# option disabled for student organized groups or section-restricted
# self-signup groups. (but self-signup is ignored for non-Course groups)
return render(:json => {}, :status => :bad_request) if @group_category.student_organized?
return render(:json => {}, :status => :bad_request) if @context.is_a?(Course) && @group_category.restricted_self_signup?
if value_to_boolean(params[:sync])
# do the distribution and note the changes
memberships = @group_category.assign_unassigned_members
# render the changes
json = memberships.group_by{ |m| m.group_id }.map do |group_id, new_members|
{ :id => group_id, :new_members => new_members.map{ |m| m.user.group_member_json(@context) } }
end
render :json => json
else
@group_category.assign_unassigned_members_in_background
render :json => progress_json(@group_category.current_progress, @current_user, session)
end
end
def populate_group_category_from_params
if api_request?
args = params
@ -372,3 +486,7 @@ class GroupCategoriesController < ApplicationController
end
end

View File

@ -98,7 +98,6 @@ class GroupsController < ApplicationController
include Api::V1::Attachment
include Api::V1::Group
include Api::V1::UserFollow
include Api::V1::Progress
SETTABLE_GROUP_ATTRIBUTES = %w(name description join_level is_public group_category avatar_attachment storage_quota_mb)
@ -636,33 +635,6 @@ class GroupsController < ApplicationController
end
end
def assign_unassigned_members
return unless authorized_action(@context, @current_user, :manage_groups)
# valid category?
category = @context.group_categories.find_by_id(params[:category_id])
return render(:json => {}, :status => :not_found) unless category
# option disabled for student organized groups or section-restricted
# self-signup groups. (but self-signup is ignored for non-Course groups)
return render(:json => {}, :status => :bad_request) if category.student_organized?
return render(:json => {}, :status => :bad_request) if @context.is_a?(Course) && category.restricted_self_signup?
if value_to_boolean(params[:async])
category.assign_unassigned_members_in_background
render :json => progress_json(category.current_progress, @current_user, session)
else
# do the distribution and note the changes
memberships = category.assign_unassigned_members
# render the changes
json = memberships.group_by{ |m| m.group_id }.map do |group_id, new_members|
{ :id => group_id, :new_members => new_members.map{ |m| m.user.group_member_json(@context) } }
end
render :json => json
end
end
# @API Upload a file
#
# Upload a file to the group.

View File

@ -197,6 +197,11 @@ class GroupCategory < ActiveRecord::Base
send_later_enqueue_args :assign_unassigned_members, :priority => Delayed::LOW_PRIORITY
end
set_policy do
given { |user, session| context.grants_right?(user, session, :read) }
can :read
end
protected
def start_progress

View File

@ -169,7 +169,7 @@ a.load_members_link, .loading_members {
:remove_user_url => group_remove_user_url("{{ id }}"),
:list_users_url => group_members_url("{{ id }}") ,
:list_unassigned_users_url => context_url(@context, :context_group_unassigned_members_url),
:assign_unassigned_users_url => context_url(@context, :context_group_assign_unassigned_members_url, :category_id => "{{ category_id }}"),
:assign_unassigned_users_url => api_v1_group_category_assign_unassigned_members_url(:group_category_id => "{{ category_id }}"),
:add_group_url => context_url(@context, :context_groups_url) %>
<div id="tabs_loading_wrapper" style="display: none;">

View File

@ -67,7 +67,6 @@ FakeRails3Routes.draw do
resources :group_categories, :only => [:create, :update, :destroy]
match 'group_unassigned_members' => 'groups#unassigned_members', :as => :group_unassigned_members, :via => :get
match 'group_unassigned_members.:format' => 'groups#unassigned_members', :as => :group_unassigned_members, :via => :get
match 'group_assign_unassigned_members' => 'groups#assign_unassigned_members', :as => :group_assign_unassigned_members, :via => :post
end
concern :files do
@ -1301,6 +1300,7 @@ FakeRails3Routes.draw do
post 'courses/:course_id/group_categories', :action => :create
get 'group_categories/:group_category_id/groups', :action => :groups, :path_name => 'group_category_groups'
get 'group_categories/:group_category_id/users', :action => :users, :path_name => 'group_category_users'
post 'group_categories/:group_category_id/assign_unassigned_members', :action => 'assign_unassigned_members', :path_name => 'group_category_assign_unassigned_members'
end
scope(:controller => :progress) do

View File

@ -740,7 +740,7 @@ define([
// perform ajax request to do the assignment server side
var url = ENV.assign_unassigned_users_url;
url = $.replaceTags(url, "category_id", $category.data('category_id'));
$.ajaxJSON(url, "POST", null, function(data) {
$.ajaxJSON(url, "POST", {'sync': true}, function(data) {
if (!data.length) {
// reset visual state
$unassigned.find(".assign_students_link").show();

View File

@ -369,6 +369,103 @@ describe "Group Categories API", :type => :integration do
response.code.should == '401'
end
end
describe "POST 'assign_unassigned_members'" do
it "should require :manage_groups permission" do
course_with_teacher(:active_all => true)
student = @course.enroll_student(user_model).user
category = @course.group_categories.create(:name => "Group Category")
user_session(student)
raw_api_call :post, "/api/v1/group_categories/#{category.id}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => category.to_param),
{'sync' => true}
response.status.should == '401 Unauthorized'
end
it "should require valid group :category_id" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.create(:name => "Group Category")
raw_api_call :post, "/api/v1/group_categories/#{category.id + 1}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => (category.id + 1).to_param),
{'sync' => true}
response.status.should == '404 Not Found'
end
it "should fail for student organized groups" do
course_with_teacher_logged_in(:active_all => true)
category = GroupCategory.student_organized_for(@course)
raw_api_call :post, "/api/v1/group_categories/#{category.id}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => category.to_param),
{'sync' => true}
response.status.should == '400 Bad Request'
end
it "should fail for restricted self signup groups" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.build(:name => "Group Category")
category.configure_self_signup(true, true)
category.save
raw_api_call :post, "/api/v1/group_categories/#{category.id}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => category.to_param),
{'sync' => true}
response.status.should == '400 Bad Request'
category.configure_self_signup(true, false)
category.save
raw_api_call :post, "/api/v1/group_categories/#{category.id}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => category.to_param),
{'sync' => true}
response.should be_success
end
it "should otherwise assign ungrouped users to groups in the category" do
course_with_teacher_logged_in(:active_all => true)
teacher = @user
category = @course.group_categories.create(:name => "Group Category")
group1 = category.groups.create(:name => "Group 1", :context => @course)
group2 = category.groups.create(:name => "Group 2", :context => @course)
student1 = @course.enroll_student(user_model).user
student2 = @course.enroll_student(user_model).user # not in a group
group2.add_user(student1)
@user = teacher
raw_api_call :post, "/api/v1/group_categories/#{category.id}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => category.to_param)
response.should be_success
run_jobs
group1.reload.users.should include(student2)
end
it "should render progress_json" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.create(:name => "Group Category")
expect {
raw_api_call :post, "/api/v1/group_categories/#{category.id}/assign_unassigned_members",
@category_path_options.merge(:action => 'assign_unassigned_members',
:group_category_id => category.to_param)
response.should be_success
json = JSON.parse(response.body)
json['url'].should =~ Regexp.new("http://www.example.com/api/v1/progress/\\d+")
json['completion'].should == 0
}.to change(Delayed::Job, :count).by(1)
end
end
end
describe "account group categories" do

View File

@ -476,80 +476,6 @@ describe GroupsController do
end
end
context "POST 'assign_unassigned_members'" do
it "should require :manage_groups permission" do
course_with_teacher(:active_all => true)
student = @course.enroll_student(user_model).user
category = @course.group_categories.create(:name => "Group Category")
user_session(student)
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id
response.status.should == '401 Unauthorized'
end
it "should require valid group :category_id" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.create(:name => "Group Category")
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id + 1
response.status.should == '404 Not Found'
end
it "should fail for student organized groups" do
course_with_teacher_logged_in(:active_all => true)
category = GroupCategory.student_organized_for(@course)
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id
response.status.should == '400 Bad Request'
end
it "should fail for restricted self signup groups" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.build(:name => "Group Category")
category.configure_self_signup(true, true)
category.save
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id
response.status.should == '400 Bad Request'
category.configure_self_signup(true, false)
category.save
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id
response.should be_success
end
it "should otherwise assign ungrouped users to groups in the category" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.create(:name => "Group Category")
group1 = category.groups.create(:name => "Group 1", :context => @course)
group2 = category.groups.create(:name => "Group 2", :context => @course)
student1 = @course.enroll_student(user_model).user
student2 = @course.enroll_student(user_model).user # not in a group
group2.add_user(student1)
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id, :async => 1
response.should be_success
run_jobs
group1.reload.users.should include(student2)
end
it "should render progress_json" do
course_with_teacher_logged_in(:active_all => true)
category = @course.group_categories.create(:name => "Group Category")
expect {
post 'assign_unassigned_members', :course_id => @course.id, :category_id => category.id, :async => 1
response.should be_success
json = JSON.parse(response.body)
json['url'].should =~ Regexp.new("http://test.host/api/v1/progress/\\d+")
json['completion'].should == 0
}.to change(Delayed::Job, :count).by(1)
end
end
describe "GET 'public_feed.atom'" do
before(:each) do
group_with_user(:active_all => true)