Skip to content

Commit b68c8fc

Browse files
committed
Automatically Assign group leaders
closes: CNVS-11834 This creates a way for an instructor to assign a random student as the group leader. It only applies when an instructor is having groups created automatically at the time of defining a group category. This also take an opportunity to refactor out some bloated code from the group_categories_controller and move it into some separate objects that can be more easily understood and rapidly unit tested through all the necessary permutations (allowing higher level integration tests to just cover a case or two) It ALSO removes group leadership knowledge into it's own object so that the callbacks in other objects are simple and the logic regarding how to do group leadership management is in one place. TEST PLAN: AUTO_DISTRIBUTION: 1) login as an instructor 2) go to the "people" tab and try to create a group set. 3) click on the "Create [0] groups for me" radio button; verify you now have controls for assigning a group leader automatically and that the strategy radio buttons are greyed out. 4) check the "Assign a group leader automatically" checkbox; verify the 2 nested radio buttons for "random" and "first" strategies become enabled 5) select a strategy and fill out the rest of the form, then submit (make sure your background job is running) 6) verify after groups are created that each group has a leader, and that the leader is in fact a member of the group. SelfSignup: 1) login as an instructor 2) go to the "people" tab and try to create a group set. 3) enable self-signup. 4) check the "Assign a group leader automatically" checkbox; verify the 2 nested radio buttons for "random" and "first" strategies become enabled 5) select a strategy and fill out the rest of the form, then submit. 6) Login as a student for the same course and join the group. 7) verify that the student has been made the group leader. Change-Id: I2cdd9f5ed2fd577469beec4ab7369c69ecf7eaa6 Reviewed-on: https://gerrit.instructure.com/35130 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Braden Anderson <banderson@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
1 parent 6fd0c9f commit b68c8fc

20 files changed

Lines changed: 750 additions & 165 deletions

app/coffeescripts/views/groups/manage/GroupCategoryCreateView.coffee

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,49 @@ define [
1515

1616
defaults:
1717
width: 600
18-
height: if ENV.allow_self_signup then 460 else 310
18+
height: if ENV.allow_self_signup then 520 else 310
1919
title: I18n.t('create_group_set', 'Create Group Set')
2020

2121
els: _.extend {},
2222
GroupCategoryEditView::els
2323
'.admin-signup-controls': '$adminSignupControls'
2424
'#split_groups': '$splitGroups'
25+
'.auto-group-leader-toggle': '$autoGroupLeaderToggle'
26+
'.auto-group-leader-controls': '$autoGroupLeaderControls'
27+
'.admin-signup-controls input[name=split_groups][value=1]': '$autoGroupSplitControl'
2528

2629
events: _.extend {},
2730
GroupCategoryEditView::events
2831
'click .admin-signup-controls [name=create_group_count]': 'clickSplitGroups'
32+
'click .auto-group-leader-toggle': 'toggleAutoGroupLeader'
33+
'click .admin-signup-controls input[name=split_groups]' : 'setVisibilityOfGroupLeaderControls'
34+
35+
36+
afterRender: ->
37+
super()
38+
@setVisibilityOfGroupLeaderControls()
39+
@toggleAutoGroupLeader()
40+
41+
toggleAutoGroupLeader: ->
42+
enabled = @$autoGroupLeaderToggle.prop 'checked'
43+
@$autoGroupLeaderControls.find('label.radio').css opacity: if enabled then 1 else 0.5
44+
@$autoGroupLeaderControls.find('input[name=auto_leader_type]').prop('disabled', !enabled)
45+
46+
setVisibilityOfGroupLeaderControls: ->
47+
splitGroupsChecked = @$autoGroupSplitControl.prop("checked")
48+
show = (@selfSignupIsEnabled() or splitGroupsChecked)
49+
@$autoGroupLeaderControls.toggle(show)
2950

3051
toggleSelfSignup: ->
31-
enabled = @$selfSignupToggle.prop('checked')
52+
enabled = @selfSignupIsEnabled()
3253
@$el.toggleClass('group-category-self-signup', enabled)
3354
@$el.toggleClass('group-category-admin-signup', !enabled)
3455
@$selfSignupControls.find(':input').prop 'disabled', !enabled
3556
@$adminSignupControls.find(':input').prop 'disabled', enabled
57+
@setVisibilityOfGroupLeaderControls()
58+
59+
selfSignupIsEnabled: ->
60+
@$selfSignupToggle.prop('checked')
3661

3762
clickSplitGroups: (e) ->
3863
# firefox doesn't like multiple inputs in the same label, so a little js to the rescue
@@ -53,4 +78,4 @@ define [
5378
create_group_count = parseInt(data.create_group_count)
5479
unless create_group_count > 0
5580
errors["create_group_count"] = [{type: 'positive_group_count', message: @messages.positive_group_count}]
56-
errors
81+
errors

app/controllers/group_categories_controller.rb

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
# ]
5353
# }
5454
# },
55+
# "auto_leader": {
56+
# "description": "Gives instructors the ability to automatically have group leaders assigned. Values include 'random', 'first', and null; 'random' picks a student from the group at random as the leader, 'first' sets the first student to be assigned to the group as the leader",
57+
# "type": "string",
58+
# "allowableValues": {
59+
# "values": [
60+
# "first",
61+
# "random"
62+
# ]
63+
# }
64+
# },
5565
# "context_type": {
5666
# "description": "The course or account that the category group belongs to. The pattern here is that whatever the context_type is, there will be an _id field named after that type. So if instead context_type was 'Course', the course_id field would be replaced by an course_id field.",
5767
# "example": "Account",
@@ -91,7 +101,7 @@ class GroupCategoriesController < ApplicationController
91101
# Returns a list of group categories in a context
92102
#
93103
# @example_request
94-
# curl https://<canvas>/api/v1/accounts/<account_id>/group_categories \
104+
# curl https://<canvas>/api/v1/accounts/<account_id>/group_categories \
95105
# -H 'Authorization: Bearer <token>'
96106
#
97107
# @returns [GroupCategory]
@@ -116,7 +126,7 @@ def index
116126
# the rights to see it.
117127
#
118128
# @example_request
119-
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
129+
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
120130
# -H 'Authorization: Bearer <token>'
121131
#
122132
# @returns GroupCategory
@@ -145,6 +155,13 @@ def show
145155
# "enabled":: allows students to self sign up for any group in course
146156
# "restricted":: allows students to self sign up only for groups in the
147157
# same section null disallows self sign up
158+
#
159+
# @argument auto_leader [Optional, "first"|"random"]
160+
# Assigns group leaders automatically when generating and allocating students to groups
161+
# Valid values are:
162+
# "first":: the first student to be allocated to a group is the leader
163+
# "random":: a random student from all members is chosen as the leader
164+
#
148165
# @argument group_limit [Optional]
149166
# Limit the maximum number of users in each group (Course Only). Requires
150167
# self signup.
@@ -160,14 +177,13 @@ def show
160177
# (Course Only)
161178
#
162179
# @example_request
163-
# curl htps://<canvas>/api/v1/courses/<course_id>/group_categories \
164-
# -F 'name=Project Groups' \
180+
# curl htps://<canvas>/api/v1/courses/<course_id>/group_categories \
181+
# -F 'name=Project Groups' \
165182
# -H 'Authorization: Bearer <token>'
166183
#
167184
# @returns GroupCategory
168185
def create
169186
if authorized_action(@context, @current_user, :manage_groups)
170-
process_group_category_api_params if api_request?
171187
@group_category = @context.group_categories.build
172188
if populate_group_category_from_params
173189
if api_request?
@@ -194,6 +210,13 @@ def create
194210
# "enabled":: allows students to self sign up for any group in course
195211
# "restricted":: allows students to self sign up only for groups in the
196212
# same section null disallows self sign up
213+
#
214+
# @argument auto_leader [Optional, "first"|"random"]
215+
# Assigns group leaders automatically when generating and allocating students to groups
216+
# Valid values are:
217+
# "first":: the first student to be allocated to a group is the leader
218+
# "random":: a random student from all members is chosen as the leader
219+
#
197220
# @argument group_limit [Optional]
198221
# Limit the maximum number of users in each group (Course Only). Requires
199222
# self signup.
@@ -209,17 +232,16 @@ def create
209232
# (Course Only)
210233
#
211234
# @example_request
212-
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
213-
# -X PUT \
214-
# -F 'name=Project Groups' \
235+
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
236+
# -X PUT \
237+
# -F 'name=Project Groups' \
215238
# -H 'Authorization: Bearer <token>'
216239
#
217240
# @returns GroupCategory
218241
def update
219242
if authorized_action(@context, @current_user, :manage_groups)
220243
@group_category ||= @context.group_categories.find_by_id(params[:category_id])
221244
if api_request?
222-
process_group_category_api_params
223245
if populate_group_category_from_params
224246
includes = ['progress_url']
225247
includes.concat(params[:includes]) if params[:includes]
@@ -242,7 +264,7 @@ def update
242264
#
243265
# @example_request
244266
# curl https://<canvas>/api/v1/group_categories/<group_category_id> \
245-
# -X DELETE \
267+
# -X DELETE \
246268
# -H 'Authorization: Bearer <token>'
247269
#
248270
def destroy
@@ -272,7 +294,7 @@ def destroy
272294
# Returns a list of groups in a group category
273295
#
274296
# @example_request
275-
# curl https://<canvas>/api/v1/group_categories/<group_cateogry_id>/groups \
297+
# curl https://<canvas>/api/v1/group_categories/<group_cateogry_id>/groups \
276298
# -H 'Authorization: Bearer <token>'
277299
#
278300
# @returns [Group]
@@ -443,35 +465,14 @@ def assign_unassigned_members
443465

444466
def populate_group_category_from_params
445467
args = api_request? ? params : params[:category]
446-
name = args[:name] || @group_category.name
447-
enable_self_signup = value_to_boolean args[:enable_self_signup]
448-
restrict_self_signup = value_to_boolean args[:restrict_self_signup]
449-
@group_category.name = name
450-
@group_category.configure_self_signup(enable_self_signup, restrict_self_signup)
451-
if @context.is_a?(Course)
452-
if @group_category.self_signup
453-
@group_category.create_group_count = args[:create_group_count].to_i
454-
elsif args[:split_groups] != '0'
455-
@group_category.create_group_count = args[:split_group_count] ? args[:split_group_count].to_i : args[:create_group_count].to_i
456-
@group_category.assign_unassigned_members = true if @group_category.create_group_count
457-
end
458-
end
459-
@group_category.group_limit = args[:group_limit]
468+
@group_category = GroupCategories::ParamsPolicy.new(@group_category, @context).populate_with(args)
460469
unless @group_category.save
461470
render :json => @group_category.errors, :status => :bad_request
462471
return false
463472
end
464473
true
465474
end
466475

467-
def process_group_category_api_params
468-
if params.has_key? 'self_signup'
469-
self_signup = params[:self_signup].to_s.downcase
470-
params[:enable_self_signup] = "1" if %w(enabled restricted).include? self_signup
471-
params[:restrict_self_signup] = "1" if "restricted" == self_signup
472-
end
473-
end
474-
475476
protected
476477
def get_category_context
477478
begin
@@ -483,7 +484,3 @@ def get_category_context
483484
end
484485

485486
end
486-
487-
488-
489-

app/models/group.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ def full?
133133
def group_category_limit_met?
134134
group_category && group_category.group_limit && participating_users.size >= group_category.group_limit
135135
end
136+
private :group_category_limit_met?
136137

137138
def student_organized?
138139
group_category && group_category.student_organized?
139140
end
140-
private :group_category_limit_met?
141141

142142
def update_max_membership_from_group_category
143143
if group_category && group_category.group_limit && (!max_membership || max_membership == 0)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
module GroupCategories
2+
3+
class Params < Struct.new(:name, :group_limit)
4+
5+
attr_reader :raw_params
6+
7+
def initialize(args, opts={})
8+
super(args[:name], args[:group_limit])
9+
@boolean_translator = opts.fetch(:boolean_translator){ Canvas::Plugin }
10+
@raw_params = args
11+
end
12+
13+
def self_signup
14+
return _self_signup if _self_signup
15+
return nil if !enable_self_signup
16+
return 'restricted' if restrict_self_signup
17+
'enabled'
18+
end
19+
20+
def auto_leader
21+
return _auto_leader if _auto_leader
22+
return nil if !enable_auto_leader
23+
return auto_leader_type if ['first', 'random'].include?(auto_leader_type)
24+
raise(ArgumentError, "Invalid AutoLeader Type #{auto_leader_type}")
25+
end
26+
27+
def create_group_count
28+
return _create_group_count if self_signup
29+
return nil unless split_group_enabled?
30+
split_group_count
31+
end
32+
33+
def assign_unassigned_members
34+
return false if self_signup
35+
split_group_enabled? && create_group_count && create_group_count > 0
36+
end
37+
38+
private
39+
40+
def value_to_boolean(value)
41+
@boolean_translator.value_to_boolean(value)
42+
end
43+
44+
def split_group_enabled?
45+
raw_params[:split_groups] != '0'
46+
end
47+
48+
def split_group_count
49+
if raw_params[:split_group_count]
50+
raw_params[:split_group_count].to_i
51+
else
52+
_create_group_count
53+
end
54+
end
55+
56+
def _create_group_count
57+
raw_params[:create_group_count].to_i
58+
end
59+
60+
def _self_signup
61+
raw_value = raw_params[:self_signup]
62+
return nil unless raw_value
63+
raw_value = raw_value.to_s.downcase
64+
%w(enabled restricted).include?(raw_value) ? raw_value : nil
65+
end
66+
67+
def _auto_leader
68+
raw_value = raw_params[:auto_leader]
69+
return nil unless raw_value
70+
raw_value = raw_value.to_s.downcase
71+
%w(random first).include?(raw_value) ? raw_value : nil
72+
end
73+
74+
def auto_leader_type
75+
raw_params[:auto_leader_type].downcase
76+
end
77+
78+
def enable_self_signup
79+
value_to_boolean raw_params[:enable_self_signup]
80+
end
81+
82+
def restrict_self_signup
83+
value_to_boolean raw_params[:restrict_self_signup]
84+
end
85+
86+
def enable_auto_leader
87+
value_to_boolean raw_params[:enable_auto_leader]
88+
end
89+
90+
end
91+
92+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module GroupCategories
2+
3+
class ParamsPolicy
4+
attr_reader :group_category, :context
5+
6+
def initialize(category, category_context)
7+
@group_category = category
8+
@context = category_context
9+
end
10+
11+
def populate_with(args, populate_opts={})
12+
params = Params.new(args, populate_opts)
13+
group_category.name = (params.name || group_category.name)
14+
group_category.self_signup = params.self_signup
15+
group_category.auto_leader = params.auto_leader
16+
group_category.group_limit = params.group_limit
17+
if context.is_a?(Course)
18+
group_category.create_group_count = params.create_group_count
19+
group_category.assign_unassigned_members = params.assign_unassigned_members
20+
end
21+
group_category
22+
end
23+
24+
end
25+
26+
end

0 commit comments

Comments
 (0)