Add shortcuts for adding all members of a group or section to a conference
closes VICE-1880 flag=none This change adds checkboxes for groups and sections to the members list section of the dialog for creating / editing a conference. These checkboxes are "shortcuts" that allow the user to quickly add all members of a specific section or group to a conference. These shortcuts are implemented client side and the model for conferences is unchanged. Test Plan: - Make sure the conferences feature is enabled - In a course open the dialog to create a new conference - Uncheck the "Invite All" box - Verify the following in the member list: - A checkbox for each section should appear in the "Sections" partition (only if there is more than one section in the course) - A checkbox for each group should appear in the "Groups" partition (only if there is at least one section in the course) - The headers for "Sections" and "Groups" should only appear if there is atleast item in the respective partition - The header for "Users" should only appear if one or more of the other partitions is present - Checking the box for section / group also checks the boxes for each of the users in that section / group - Unchecking the box for section / group also unchecks the boxes for each of the users in that section / group provided that another section / group that contains that user isn't also checked - If a user is added to the conference via a section or group you should not be able to uncheck the checkbox for that user. Change-Id: I6896d9ac3066a4a66a118ec510e64e9b324cba8f Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/271824 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Product-Review: Katrina Hess <khess@instructure.com> QA-Review: Chawn Neal <chawn.neal@instructure.com> Reviewed-by: Chawn Neal <chawn.neal@instructure.com>
This commit is contained in:
parent
94004d3208
commit
8a3c737b92
|
@ -278,6 +278,18 @@ class ConferencesController < ApplicationController
|
|||
@render_alternatives = WebConference.conference_types(@context).all? { |ct| ct[:replace_with_alternatives] }
|
||||
case @context
|
||||
when Course
|
||||
@sections = @context.course_sections
|
||||
@groups = @context.active_groups
|
||||
|
||||
@group_user_ids_map = @groups.to_a.each_with_object({}) do |group, acc|
|
||||
acc[group.id] = group.participating_users_in_context.map{|u| u.id.to_s}
|
||||
acc
|
||||
end
|
||||
|
||||
@section_user_ids_map = @sections.to_a.each_with_object({}) do |section, acc|
|
||||
acc[section.id] = section.participants.map{|u| u.id.to_s}
|
||||
acc
|
||||
end
|
||||
@users = User.where(:id => @context.current_enrollments.not_fake.active_by_date.where.not(:user_id => @current_user).select(:user_id)).
|
||||
order(User.sortable_name_order_by_clause).to_a
|
||||
@render_alternatives ||= @context.settings[:show_conference_alternatives].present?
|
||||
|
@ -296,6 +308,10 @@ class ConferencesController < ApplicationController
|
|||
default_conference: default_conference_json(@context, @current_user, session),
|
||||
conference_type_details: conference_types_json(WebConference.conference_types(@context)),
|
||||
users: @users.map { |u| {:id => u.id, :name => u.last_name_first} },
|
||||
groups: @groups&.map { |g| {:id => g.id, :name => g.full_name} },
|
||||
sections: @sections&.map { |s| {:id => s.id, :name => s.display_name} },
|
||||
group_user_ids_map: @group_user_ids_map,
|
||||
section_user_ids_map: @section_user_ids_map,
|
||||
can_create_conferences: @context.grants_right?(@current_user, session, :create_conferences),
|
||||
render_alternatives: @render_alternatives
|
||||
)
|
||||
|
|
|
@ -119,6 +119,16 @@ describe ConferencesController do
|
|||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it "should include group and section data in the js_env" do
|
||||
group(context: @course)
|
||||
user_session(@teacher)
|
||||
get 'index', params: {course_id: @course.id}
|
||||
expect(assigns[:js_env][:groups]).to be_truthy
|
||||
expect(assigns[:js_env][:sections]).to be_truthy
|
||||
expect(assigns[:js_env][:group_user_ids_map]).to be_truthy
|
||||
expect(assigns[:js_env][:section_user_ids_map]).to be_truthy
|
||||
end
|
||||
|
||||
context "sets render_alternatives variable" do
|
||||
it "should set to false by default" do
|
||||
user_session(@teacher)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import EditConferenceView from 'ui/features/conferences/backbone/views/EditConferenceView.coffee'
|
||||
import Conference from 'ui/features/conferences/backbone/models/Conference.js'
|
||||
import Conference from 'ui/features/conferences/backbone/models/Conference'
|
||||
import tz from '@canvas/timezone'
|
||||
import french from 'timezone/fr_FR'
|
||||
import I18nStubber from 'helpers/I18nStubber'
|
||||
|
@ -30,7 +30,21 @@ QUnit.module('EditConferenceView', {
|
|||
this.datepickerSetting = {field: 'datepickerSetting', type: 'date_picker'}
|
||||
fakeENV.setup({
|
||||
conference_type_details: [{settings: [this.datepickerSetting]}],
|
||||
users: [{id: 1, name: 'Owlswick Clamp'}]
|
||||
users: [
|
||||
{id: 1, name: 'Owlswick Clamp'},
|
||||
{id: 2, name: 'Abby Zollinger'},
|
||||
{id: 3, name: 'Bruce Young'}
|
||||
],
|
||||
sections: [
|
||||
{id: 1, name: 'Section 1'},
|
||||
{id: 2, name: 'Section 2'}
|
||||
],
|
||||
groups: [
|
||||
{id: 1, name: 'Study Group 1'},
|
||||
{id: 2, name: 'Study Group 2'}
|
||||
],
|
||||
section_user_ids_map: {1: [1, 2], 2: [3]},
|
||||
group_user_ids_map: {1: [1], 2: [1, 2]}
|
||||
})
|
||||
},
|
||||
teardown() {
|
||||
|
@ -41,7 +55,7 @@ QUnit.module('EditConferenceView', {
|
|||
}
|
||||
})
|
||||
|
||||
test('updateConferenceUserSettingDetailsForConference localizes values for datepicker settings', function() {
|
||||
test('updateConferenceUserSettingDetailsForConference localizes values for datepicker settings', function () {
|
||||
tz.changeLocale(french, 'fr_FR', 'fr')
|
||||
I18nStubber.pushFrame()
|
||||
I18nStubber.setLocale('fr_FR')
|
||||
|
@ -52,7 +66,7 @@ test('updateConferenceUserSettingDetailsForConference localizes values for datep
|
|||
equal(this.datepickerSetting.value, 'ven. 7 août, 2015 17:00')
|
||||
})
|
||||
|
||||
test('#show sets the proper title for new conferences', function() {
|
||||
test('#show sets the proper title for new conferences', function () {
|
||||
const expectedTitle = 'New Conference'
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
|
@ -70,7 +84,7 @@ test('#show sets the proper title for new conferences', function() {
|
|||
equal(title, expectedTitle)
|
||||
})
|
||||
|
||||
test('#show sets the proper title for editing conferences', function() {
|
||||
test('#show sets the proper title for editing conferences', function () {
|
||||
const expectedTitle = 'Edit "InstructureCon"'
|
||||
const attributes = {
|
||||
title: 'InstructureCon',
|
||||
|
@ -89,7 +103,7 @@ test('#show sets the proper title for editing conferences', function() {
|
|||
equal(title, expectedTitle)
|
||||
})
|
||||
|
||||
test('#show sets localized durataion when editing conference', function() {
|
||||
test('#show sets localized durataion when editing conference', function () {
|
||||
const expectedDuration = '1,234.5'
|
||||
const attributes = {
|
||||
title: 'InstructureCon',
|
||||
|
@ -109,7 +123,7 @@ test('#show sets localized durataion when editing conference', function() {
|
|||
equal(duration, expectedDuration)
|
||||
})
|
||||
|
||||
test('"remove observers" modifies "invite all course members"', function() {
|
||||
test('"remove observers" modifies "invite all course members"', function () {
|
||||
const attributes = {
|
||||
title: 'Making Money',
|
||||
recordings: [],
|
||||
|
@ -129,3 +143,259 @@ test('"remove observers" modifies "invite all course members"', function() {
|
|||
ok(this.view.$('#members_list').is(':visible'))
|
||||
ok(this.view.$('#observers_remove').is(':disabled'))
|
||||
})
|
||||
|
||||
test('sections should appear in member list if course has more than one section', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
ok(this.view.$('#section_1').is(':visible'))
|
||||
ok(this.view.$('#section_2').is(':visible'))
|
||||
})
|
||||
|
||||
test('sections should not appear in member list if course has only section', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
window.ENV.sections = [{name: 'Section 1', id: 1}]
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
ok(!this.view.$('#section_1').is(':visible'))
|
||||
})
|
||||
|
||||
test('groups should appear in member list if course has one or more groups', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
ok(this.view.$('#group_1').is(':visible'))
|
||||
})
|
||||
|
||||
test('checking/unchecking a section also checks/unchecks the members that are in that section', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
this.view.$('#section_2').click()
|
||||
ok(!this.view.$('#user_1').is(':checked'))
|
||||
this.view.$('#section_1').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
this.view.$('#section_1').click()
|
||||
ok(!this.view.$('#user_1').is(':checked'))
|
||||
})
|
||||
|
||||
test('checking/unchecking a groups also checks/unchecks the members that are in that group', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
ok(!this.view.$('#user_1').is(':checked'))
|
||||
this.view.$('#group_1').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
this.view.$('#group_1').click()
|
||||
ok(!this.view.$('#user_1').is(':checked'))
|
||||
})
|
||||
|
||||
test('unchecking a group only unchecks members that have not been selected by section also', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
ok(!this.view.$('#user_1').is(':checked'))
|
||||
this.view.$('#group_1').click()
|
||||
this.view.$('#section_1').click()
|
||||
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(this.view.$('#user_2').is(':checked'))
|
||||
this.view.$('#group_1').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
})
|
||||
|
||||
test('unchecking a section only unchecks members that have not been selected by group also', function () {
|
||||
const attributes = {
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
this.view.$('#user_all').click()
|
||||
ok(!this.view.$('#user_1').is(':checked'))
|
||||
this.view.$('#group_1').click()
|
||||
this.view.$('#section_1').click()
|
||||
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(this.view.$('#user_2').is(':checked'))
|
||||
this.view.$('#section_1').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(!this.view.$('#user_2').is(':checked'))
|
||||
})
|
||||
|
||||
test('While editing a conference the box for a group should be checked and disabled if everyone in the group is a participant', function () {
|
||||
const attributes = {
|
||||
title: 'Making Money',
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
},
|
||||
user_ids: [1]
|
||||
}
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference, {isEditing: true})
|
||||
|
||||
ok(this.view.$('#group_1').is(':checked'))
|
||||
ok(this.view.$('#group_1').is(':disabled'))
|
||||
})
|
||||
|
||||
test('While editing a conference the box for a section should be checked and disabled if everyone in the section is a participant', function () {
|
||||
const attributes = {
|
||||
title: 'Making Money',
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
},
|
||||
user_ids: [3]
|
||||
}
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference, {isEditing: true})
|
||||
|
||||
ok(this.view.$('#section_2').is(':checked'))
|
||||
ok(this.view.$('#section_2').is(':disabled'))
|
||||
})
|
||||
|
||||
test('While editing a conference unchecking a group should only uncheck members who are not a part of the existing conference', function () {
|
||||
const attributes = {
|
||||
title: 'Making Money',
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
},
|
||||
user_ids: [1]
|
||||
}
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference, {isEditing: true})
|
||||
ok(!this.view.$('#group_2').is(':checked'))
|
||||
ok(!this.view.$('#user_2').is(':checked'))
|
||||
|
||||
this.view.$('#group_2').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(this.view.$('#user_2').is(':checked'))
|
||||
|
||||
this.view.$('#group_2').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(!this.view.$('#user_2').is(':checked'))
|
||||
})
|
||||
|
||||
test('While editing a conference unchecking a section should only uncheck member who are not a part of the existing conference', function () {
|
||||
const attributes = {
|
||||
title: 'Making Money',
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
},
|
||||
user_ids: [1]
|
||||
}
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference, {isEditing: true})
|
||||
ok(!this.view.$('#section_1').is(':checked'))
|
||||
ok(!this.view.$('#user_2').is(':checked'))
|
||||
|
||||
this.view.$('#section_1').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(this.view.$('#user_2').is(':checked'))
|
||||
|
||||
this.view.$('#section_1').click()
|
||||
ok(this.view.$('#user_1').is(':checked'))
|
||||
ok(!this.view.$('#user_2').is(':checked'))
|
||||
})
|
||||
|
||||
test('while context_is_group = true no sections or groups should appear in the member list', function () {
|
||||
const attributes = {
|
||||
title: 'Making Money',
|
||||
recordings: [],
|
||||
user_settings: {
|
||||
scheduled_date: new Date()
|
||||
},
|
||||
permissions: {
|
||||
update: true
|
||||
}
|
||||
}
|
||||
window.ENV.context_is_group = true
|
||||
const conference = new Conference(attributes)
|
||||
this.view.show(conference)
|
||||
ok(!this.view.$('#section_1').is(':visible'))
|
||||
ok(!this.view.$('#section_2').is(':visible'))
|
||||
ok(!this.view.$('#group_1').is(':visible'))
|
||||
ok(!this.view.$('#group_2').is(':visible'))
|
||||
})
|
||||
|
|
|
@ -45,7 +45,9 @@ export default class EditConferenceView extends DialogBaseView
|
|||
@delegateEvents()
|
||||
@toggleAllUsers()
|
||||
@markInvitedUsers()
|
||||
@markInvitedSectionsAndGroups()
|
||||
@renderConferenceFormUserSettings()
|
||||
@setupGroupAndSectionEventListeners()
|
||||
@$('form').formSubmit(
|
||||
object_name: 'web_conference'
|
||||
beforeSubmit: (data) =>
|
||||
|
@ -105,14 +107,23 @@ export default class EditConferenceView extends DialogBaseView
|
|||
if numberHelper.validate(conferenceData.duration)
|
||||
conferenceData.duration = I18n.n(conferenceData.duration)
|
||||
|
||||
hide_groups = !ENV.groups || ENV.groups.length == 0
|
||||
hide_sections = !ENV.sections || ENV.sections.length <= 1
|
||||
hide_user_header = hide_groups && hide_sections
|
||||
|
||||
json =
|
||||
settings:
|
||||
is_editing: is_editing
|
||||
is_adding: is_adding
|
||||
disable_duration_changes: ((conferenceData['long_running'] || is_editing) && conferenceData['started_at'])
|
||||
auth_token: authenticity_token()
|
||||
hide_sections: hide_sections
|
||||
hide_groups: hide_groups
|
||||
hide_user_header: hide_user_header
|
||||
conferenceData: conferenceData
|
||||
users: ENV.users
|
||||
sections: ENV.sections
|
||||
groups: ENV.groups
|
||||
context_is_group: ENV.context_asset_string.split("_")[0] == "group"
|
||||
conferenceTypes: ENV.conference_type_details.map((type) ->
|
||||
{name: type.name, type: type.type, selected: (conferenceData.conference_type == type.type)}
|
||||
|
@ -203,9 +214,71 @@ export default class EditConferenceView extends DialogBaseView
|
|||
el.attr('disabled', true)
|
||||
)
|
||||
|
||||
markInvitedSectionsAndGroups: ->
|
||||
_.each(ENV.sections, (section) =>
|
||||
section_user_ids = ENV.section_user_ids_map[section.id]
|
||||
intersection = _.intersection(section_user_ids, @model.get("user_ids"))
|
||||
if (intersection.length == section_user_ids.length)
|
||||
el = $("#members_list .member.section_" + section.id).find(":checkbox")
|
||||
el.attr('checked', true)
|
||||
el.attr('disabled', true)
|
||||
)
|
||||
|
||||
_.each(ENV.groups, (group) =>
|
||||
group_user_ids = ENV.group_user_ids_map[group.id]
|
||||
intersection = _.intersection(group_user_ids, @model.get("user_ids"))
|
||||
if (intersection.length == group_user_ids.length)
|
||||
el = $("#members_list .member.group_" + group.id).find(":checkbox")
|
||||
el.attr('checked', true)
|
||||
el.attr('disabled', true)
|
||||
)
|
||||
|
||||
|
||||
changeLongRunning: (e) ->
|
||||
if ($(e.currentTarget).is(':checked'))
|
||||
$('#web_conference_duration').prop('disabled', true).val('')
|
||||
else
|
||||
# use restore time from data attribute
|
||||
$('#web_conference_duration').prop('disabled', false).val($('#web_conference_duration').data('restore-value'))
|
||||
|
||||
setupGroupAndSectionEventListeners: () ->
|
||||
selectedBySection = []
|
||||
selectedByGroup = []
|
||||
toggleMember = (id, checked) ->
|
||||
memberEl = $("#members_list .member.user_" + id).find(":checkbox")
|
||||
memberEl.attr('checked', checked)
|
||||
memberEl.attr('disabled', checked)
|
||||
|
||||
_.each(ENV.groups, (group) =>
|
||||
el = $("#members_list .member.group_" + group.id)
|
||||
el.on("change", (e) =>
|
||||
_.each(
|
||||
ENV.group_user_ids_map[group.id],
|
||||
(id) =>
|
||||
if (e.target.checked)
|
||||
selectedByGroup.push(id)
|
||||
toggleMember(id, e.target.checked)
|
||||
else
|
||||
selectedByGroup = _.without(selectedByGroup, id)
|
||||
if (!_.contains(selectedBySection, id) && !_.contains(@model.get("user_ids"), id))
|
||||
toggleMember(id, e.target.checked)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
_.each(ENV.sections, (section) =>
|
||||
el = $("#members_list .member.section_" + section.id)
|
||||
el.on("change", (e) =>
|
||||
_.each(
|
||||
ENV.section_user_ids_map[section.id],
|
||||
(id) =>
|
||||
if (e.target.checked)
|
||||
selectedBySection.push(id)
|
||||
toggleMember(id, e.target.checked)
|
||||
else
|
||||
selectedBySection = _.without(selectedBySection, id)
|
||||
if (!_.contains(selectedByGroup, id) && !_.contains(@model.get("user_ids"), id))
|
||||
toggleMember(id, e.target.checked)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -74,6 +74,41 @@
|
|||
{{/unless}}
|
||||
<div>
|
||||
<ul id="members_list" style="border: 1px solid #333; padding: 10px; overflow-y: auto; max-height: 150px;">
|
||||
{{#unless context_is_group}}
|
||||
{{#unless settings.hide_sections}}
|
||||
<li>
|
||||
<strong>{{#t}}Sections{{/t}}</strong>
|
||||
</li>
|
||||
{{# each sections }}
|
||||
<li class="member section_{{id}}">
|
||||
<label class="checkbox" for="section_{{id}}">
|
||||
<input name="section[{{id}}]" type="hidden" value="0">
|
||||
<input id="section_{{id}}" name="section[{{id}}]" type="checkbox" value="1">
|
||||
{{name}}
|
||||
</label>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/unless}}
|
||||
{{#unless settings.hide_groups}}
|
||||
<li>
|
||||
<strong>{{#t}}Groups{{/t}}</strong>
|
||||
</li>
|
||||
{{# each groups }}
|
||||
<li class="member group_{{id}}">
|
||||
<label class="checkbox" for="group_{{id}}">
|
||||
<input name="group[{{id}}]" type="hidden" value="0">
|
||||
<input id="group_{{id}}" name="group[{{id}}]" type="checkbox" value="1">
|
||||
{{name}}
|
||||
</label>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/unless}}
|
||||
{{#unless settings.hide_user_header}}
|
||||
<li>
|
||||
<strong>{{#t}}Users{{/t}}</strong>
|
||||
</li>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
{{# each users}}
|
||||
<li class="member user_{{id}}">
|
||||
<label class="checkbox" for="user_{{id}}">
|
||||
|
|
Loading…
Reference in New Issue