improve student group signup experience
fixes CNVS-11832 fixes CNVS-11836 fixes CNVS-13327 test plan - enable feature student_groups_next for course - regression test groups page student view - regression test instructor groups page - as an instructor have multiple groups in a group set - visit one of the groups - the instructor can switch between the groups in the group set - as a student you should be able to create a group Change-Id: Iacf1eaf4467b57f307d45245b53dfc93f14154ff Reviewed-on: https://gerrit.instructure.com/34454 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Ethan Vizitei <evizitei@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Drew Bowman <dbowman@instructure.com>
This commit is contained in:
parent
923040f873
commit
3c1786adac
|
@ -10,6 +10,12 @@ define [
|
|||
return if $trigger.data('kyleMenu')
|
||||
opts = $.extend {noButton: true}, $trigger.data('kyleMenuOptions')
|
||||
opts.appendMenuTo = 'body' if $trigger.data('append-to-body')
|
||||
opts = $.extend opts,
|
||||
popupOpts:
|
||||
position:
|
||||
my: $trigger.data('popup-my')
|
||||
at: $trigger.data('popup-at')
|
||||
within: $trigger.data('popup-within')
|
||||
new KyleMenu($trigger, opts)
|
||||
|
||||
$trigger.trigger(event)
|
||||
|
|
|
@ -26,13 +26,15 @@ require [
|
|||
'compiled/collections/RosterUserCollection'
|
||||
'compiled/collections/RolesCollection'
|
||||
'compiled/collections/SectionCollection'
|
||||
'compiled/collections/GroupCategoryCollection'
|
||||
'compiled/views/InputFilterView'
|
||||
'compiled/views/PaginatedCollectionView'
|
||||
'compiled/views/courses/roster/RosterUserView'
|
||||
'compiled/views/courses/roster/RosterView'
|
||||
'compiled/views/courses/roster/RosterTabsView'
|
||||
'compiled/views/courses/roster/ResendInvitationsView'
|
||||
'jquery'
|
||||
], (I18n, {Model}, CreateUserList, Role, CreateUsersView, RoleSelectView, rosterUsersTemplate, RosterUserCollection, RolesCollection, SectionCollection, InputFilterView, PaginatedCollectionView, RosterUserView, RosterView, ResendInvitationsView, $) ->
|
||||
], (I18n, {Model}, CreateUserList, Role, CreateUsersView, RoleSelectView, rosterUsersTemplate, RosterUserCollection, RolesCollection, SectionCollection, GroupCategoryCollection, InputFilterView, PaginatedCollectionView, RosterUserView, RosterView, RosterTabsView, ResendInvitationsView, $) ->
|
||||
|
||||
fetchOptions =
|
||||
include: ['avatar_url', 'enrollments', 'email', 'observed_users']
|
||||
|
@ -69,8 +71,16 @@ require [
|
|||
model: course
|
||||
resendInvitationsUrl: ENV.resend_invitations_url
|
||||
canResend: ENV.permissions.manage_students or ENV.permissions.manage_admin_users
|
||||
groupCategories = new (GroupCategoryCollection.extend({url: "/api/v1/courses/#{ENV.course?.id}/group_categories"}))
|
||||
|
||||
rosterTabsView = new RosterTabsView
|
||||
collection: groupCategories
|
||||
|
||||
rosterTabsView.fetch()
|
||||
|
||||
@app = new RosterView
|
||||
usersView: usersView
|
||||
rosterTabsView: rosterTabsView
|
||||
inputFilterView: inputFilterView
|
||||
roleSelectView: roleSelectView
|
||||
createUsersView: createUsersView
|
||||
|
@ -79,7 +89,7 @@ require [
|
|||
roles: ENV.ALL_ROLES
|
||||
permissions: ENV.permissions
|
||||
course: ENV.course
|
||||
|
||||
|
||||
users.once 'reset', ->
|
||||
users.on 'reset', ->
|
||||
numUsers = users.length
|
||||
|
@ -90,7 +100,7 @@ require [
|
|||
else
|
||||
msg = I18n.t "filter_multiple_users_found", "%{userCount} users found.", userCount: numUsers
|
||||
$('#aria_alerts').empty().text msg
|
||||
|
||||
|
||||
|
||||
@app.render()
|
||||
@app.$el.appendTo $('#content')
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
define [
|
||||
'ember'
|
||||
'./dialog_mixin'
|
||||
'../templates/components/form-dialog'
|
||||
], (Em, DialogMixin) ->
|
||||
|
||||
FormDialogComponent = Em.Component.extend DialogMixin
|
||||
|
|
|
@ -6,12 +6,12 @@ define [
|
|||
'../../shared/components/ic_actions_component'
|
||||
'../../shared/components/ic_publish_icon_component'
|
||||
'./date_transform'
|
||||
], (Ember, env, Util, FastSelectComponent) ->
|
||||
], (Ember, env, Util, FastSelectComponent, ConfirmDialogComponent, FormDialogComponent) ->
|
||||
Ember.Util = Util
|
||||
|
||||
Ember.onLoad 'Ember.Application', (Application) ->
|
||||
Application.initializer
|
||||
name: 'FastSelectComponent'
|
||||
name: 'SharedComponents'
|
||||
initialize: (container, application) ->
|
||||
container.register 'component:fast-select', FastSelectComponent
|
||||
Application.initializer
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<form class="form-dialog">
|
||||
<form class="form-dialog" {{bind-attr id=id}}>
|
||||
<div class="form-dialog-content">
|
||||
{{yield}}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
define [
|
||||
'ember'
|
||||
'./dialog_mixin'
|
||||
'i18n!confirm_dialog_component'
|
||||
], (Em, DialogMixin, I18n) ->
|
||||
|
||||
###
|
||||
# All parameters except for title are optional.
|
||||
#
|
||||
# title: I18n'd title that is displayed as the dialog title.
|
||||
#
|
||||
# Defaults:
|
||||
#
|
||||
# on-submit: Sends 'submit' action.
|
||||
# on-cancel: Sends 'cancel' action.
|
||||
# cancel-text: I18n'd version of the word "Cancel"
|
||||
# confirm-text: I18n'd version of the word "Ok"
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# {{#confirm-dialog
|
||||
# on-submit="myConfirmAction"
|
||||
# on-cancel="myCancelAction"
|
||||
# cancel-text=somePropertyWithI18nForCancellingTheAction
|
||||
# title=myI18ndTitleProperty
|
||||
# }}
|
||||
#
|
||||
# {{#t "confirm_deletion_of_quiz"}}
|
||||
# Are you sure you want to delete this quiz?
|
||||
# {{/t}}
|
||||
#
|
||||
# {{/confirm-dialog}}
|
||||
#
|
||||
###
|
||||
|
||||
ConfirmDialogComponent = Em.Component.extend DialogMixin,
|
||||
'on-cancel': 'cancel'
|
||||
'confirm-text': I18n.t('ok', 'Ok')
|
|
@ -0,0 +1,111 @@
|
|||
define [
|
||||
'ember'
|
||||
'jquery'
|
||||
'i18n!confirm_dialog'
|
||||
'jqueryui/dialog'
|
||||
'compiled/jquery/fixDialogButtons'
|
||||
], (Ember, $, I18n) ->
|
||||
|
||||
CONFIRM_BTN = '.confirm-dialog-confirm-btn'
|
||||
CANCEL_BTN = '.confirm-dialog-cancel-btn'
|
||||
|
||||
DialogMixin = Ember.Mixin.create
|
||||
# header element at the top of the page
|
||||
# Need to know this so we can decrease its z-index
|
||||
# so the overlay appears as correctly as possible
|
||||
headerElement: '#header'
|
||||
height: '500'
|
||||
width: '550'
|
||||
position:
|
||||
my: 'center'
|
||||
at: 'center'
|
||||
of: window
|
||||
'fix-dialog-buttons': true
|
||||
'confirm-text': I18n.t('submit', 'Submit')
|
||||
'cancel-text': I18n.t('cancel', 'Cancel')
|
||||
'on-submit': 'submit'
|
||||
|
||||
# Default Destroy Action
|
||||
|
||||
'_destroyAction': '_destroyModal'
|
||||
turnIntoDialog: (->
|
||||
unless @get('title')
|
||||
throw new Em.Error "You must provide a title to a Dialog Component!"
|
||||
$el = @$()
|
||||
$el.dialog
|
||||
autoOpen: false
|
||||
title: @get('title')
|
||||
modal: true
|
||||
height: @get('height')
|
||||
width: @get('width')
|
||||
fixDialogButtons: @get('fix-dialog-buttons')
|
||||
position: @get('position')
|
||||
close: => @sendAction('_destroyAction')
|
||||
|
||||
uiDialog = $el
|
||||
.dialog('open')
|
||||
.data('dialog')
|
||||
.uiDialog
|
||||
|
||||
@_moveWithinEmberAppScope($el, uiDialog)
|
||||
|
||||
uiDialog.on 'keypress', (event) =>
|
||||
Em.run this, 'keyPress', event
|
||||
|
||||
uiDialog.on 'click', CONFIRM_BTN, (event) =>
|
||||
Em.run this, 'closeAndConfirm'
|
||||
|
||||
uiDialog.on 'click', CANCEL_BTN, (event) =>
|
||||
Em.run this, 'closeAndCancel'
|
||||
|
||||
).on 'didInsertElement'
|
||||
|
||||
_close: ->
|
||||
@$().dialog 'close'
|
||||
|
||||
# Need to move the element within the Ember app root element,
|
||||
# otherwise, things like {{action}} won't be handled by Ember.
|
||||
_moveWithinEmberAppScope: ($el, uiDialog) ->
|
||||
$overlay = $el.dialog().data('dialog').overlay.$el
|
||||
$overlay.css('position', 'fixed')
|
||||
rootElement = Em.get('App.rootElement' || '#content')
|
||||
$content = $ rootElement
|
||||
uiDialog.appendTo $content
|
||||
$overlay.appendTo $content
|
||||
uiDialog.position(@get('position'))
|
||||
uiDialog.focus()
|
||||
|
||||
# Bring the "Courses", "Assignments", etc menu down in z-index
|
||||
# so the overlay doesn't get hidden by it.
|
||||
$('#header').css('z-index', '0')
|
||||
|
||||
closeAndConfirm: ->
|
||||
@_close()
|
||||
@sendAction 'on-submit'
|
||||
false
|
||||
|
||||
closeAndCancel: ->
|
||||
@_close()
|
||||
@sendAction 'on-cancel'
|
||||
false
|
||||
|
||||
destroyDialog: (->
|
||||
Ember.$('#header').css 'z-index', '11'
|
||||
@$().data('dialog').uiDialog.off()
|
||||
@$().dialog 'destroy'
|
||||
).on 'willDestroyElement'
|
||||
|
||||
# Send the appropriate event when the enter key is pressed on the confirm
|
||||
# or cancel buttons.
|
||||
#
|
||||
# Will not block event propagation if the event target is not a button.
|
||||
keyPress: (event) ->
|
||||
return true unless event.keyCode is $.ui.keyCode.ENTER
|
||||
$target = $ event.target
|
||||
return true unless $target.is("button")
|
||||
if $target.hasClass CONFIRM_BTN.replace('.', '')
|
||||
@closeAndConfirm()
|
||||
else
|
||||
@closeAndCancel()
|
||||
|
||||
false
|
|
@ -0,0 +1,7 @@
|
|||
define [
|
||||
'ember'
|
||||
'./dialog_mixin'
|
||||
'../templates/components/form-dialog'
|
||||
], (Em, DialogMixin) ->
|
||||
|
||||
FormDialogComponent = Em.Component.extend DialogMixin
|
|
@ -0,0 +1,15 @@
|
|||
<form class="form-dialog">
|
||||
<div class="form-dialog-content">
|
||||
{{yield}}
|
||||
</div>
|
||||
<div class="form-controls">
|
||||
<button class="btn confirm-dialog-cancel-btn" {{action 'cancel'}}>
|
||||
{{cancel-text}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary confirm-dialog-confirm-btn" {{action 'confirm'}}>
|
||||
{{confirm-text}}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,15 @@
|
|||
<form class="form-dialog" {{bind-attr id=id}}>
|
||||
<div class="form-dialog-content">
|
||||
{{yield}}
|
||||
</div>
|
||||
|
||||
<div class="form-controls">
|
||||
<button class="btn confirm-dialog-cancel-btn" {{action 'cancel'}}>
|
||||
{{cancel-text}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary confirm-dialog-confirm-btn" {{action 'confirm'}} {{bind-attr disabled=submit-disabled}}>
|
||||
{{confirm-text}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,22 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
'ic-ajax'
|
||||
], (Ember, DS,ajax) ->
|
||||
GroupAdapter = DS.RESTAdapter.extend
|
||||
namespace: 'api/v1'
|
||||
|
||||
urlFor: (record) ->
|
||||
if record.get('course_id')
|
||||
"/courses/#{record.get('course_id')}/groups"
|
||||
else
|
||||
"/#{@namespace}/groups"
|
||||
|
||||
|
||||
createRecord: (store, type, record) ->
|
||||
url = @urlFor(record)
|
||||
delete record.course_id
|
||||
invites = record.invites
|
||||
delete record.invites
|
||||
@ajax(url, "POST", { data: {group: record, invitees: invites }})
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
'ic-ajax'
|
||||
], (Ember, DS,ajax) ->
|
||||
MembershipAdapter = DS.RESTAdapter.extend
|
||||
namespace: 'api/v1'
|
||||
|
||||
urlFor: (record) ->
|
||||
"/#{@namespace}/groups/#{record.get('group_id')}/memberships"
|
||||
|
||||
|
||||
createRecord: (store, type, record) ->
|
||||
@ajax(@urlFor(record), "POST", { data: { user_id: record.get('user_id') }})
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
define [
|
||||
'ember'
|
||||
'../../shared/components/ic_lazy_list_component'
|
||||
'../../shared/components/form_dialog_component'
|
||||
], (Ember, ICLazyList, FormDialogComponent) ->
|
||||
|
||||
Ember.onLoad 'Ember.Application', (Application) ->
|
||||
Application.initializer
|
||||
name: 'SharedComponents'
|
||||
initialize: (container, application) ->
|
||||
container.register 'component:form-dialog', FormDialogComponent
|
||||
container.register 'component:ic-lazy-list', ICLazyList
|
||||
|
||||
Ember.Application.extend
|
||||
|
||||
rootElement: '#content'
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
define ->
|
||||
route = ->
|
||||
@resource "student_groups", path: '/'
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
define [
|
||||
'i18n!student_groups'
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
], (I18n, Ember, ajax) ->
|
||||
|
||||
GroupController = Ember.ObjectController.extend
|
||||
|
||||
currentUser: {}
|
||||
groupUrl: (->
|
||||
"/groups/#{@get('id')}"
|
||||
).property('id')
|
||||
|
||||
usersUrl: (->
|
||||
"/api/v1/groups/#{@get('id')}/memberships"
|
||||
).property('id')
|
||||
|
||||
i18nStudentsCount: (->
|
||||
I18n.t('students_count', 'student', count: @get('memberCount'))
|
||||
).property('memberCount')
|
||||
|
||||
showWhileSearching: (->
|
||||
if @get('parentController.filterText').length > 0
|
||||
@set('showBody', true)
|
||||
else
|
||||
@set('showBody', false)
|
||||
).observes('parentController.filterText').on('init')
|
||||
|
||||
memberCount: Ember.computed.alias('users.length')
|
||||
|
||||
hasMultipleMembers: Ember.computed.not('memberCount',1)
|
||||
actions:
|
||||
visitGroup: ->
|
||||
if @get('isMember')
|
||||
window.location.href = @get('groupUrl')
|
||||
toggleBody: ->
|
||||
if @.get('memberCount') > 0
|
||||
@toggleProperty('showBody')
|
||||
else
|
||||
@set('showBody', false)
|
||||
join: (group) ->
|
||||
membership = @store.createRecord('membership', {
|
||||
user_id: ENV.current_user_id
|
||||
group_id: @get('id')
|
||||
})
|
||||
controller = this
|
||||
membership.save().then (membership)->
|
||||
controller.parentController.removeFromCategory(controller.get('group_category_id'))
|
||||
controller.get('users').addObject(ENV.current_user)
|
||||
controller.parentController.set('noticeMessage', "Joined Group")
|
||||
leave: (group) ->
|
||||
controller = this
|
||||
Ember.run =>
|
||||
ajax("#{@get('usersUrl')}/self",{type: "DELETE"}).then (response) ->
|
||||
user = controller.get('users').findBy('id', ENV.current_user_id)
|
||||
controller.get('users').removeObject(user)
|
||||
controller.parentController.set('noticeMessage', "Left Group")
|
||||
if controller.get('memberCount') == 0
|
||||
controller.set('showBody', false)
|
||||
|
||||
isMember: (->
|
||||
@get('model').users.findBy('id', ENV.current_user_id)?
|
||||
).property('users.@each.id')
|
||||
|
||||
canSignup: (->
|
||||
@get('group_category.self_signup') == "enabled"
|
||||
).property('group_category.self_signup')
|
||||
|
||||
|
||||
isFull: (->
|
||||
@get('max_membership')? and @get('memberCount') >= @get('max_membership')
|
||||
).property('memberCount')
|
||||
|
||||
isMemberOfCategory: (->
|
||||
@parentController.isMemberOfCategory(@get('group_category_id'))
|
||||
).property('users.@each.id')
|
|
@ -0,0 +1,46 @@
|
|||
define [
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
], (Ember,ajax) ->
|
||||
|
||||
NewGroupController = Ember.ObjectController.extend
|
||||
|
||||
needs: [ 'student_groups', 'users' ]
|
||||
|
||||
joinLevel: 'parent_context_auto_join'
|
||||
name: ''
|
||||
nameError: (->
|
||||
length = @get('name').length
|
||||
length == 0 or length > 255
|
||||
).property('name.length')
|
||||
|
||||
|
||||
|
||||
selectOptions: [
|
||||
{value: 'parent_context_auto_join', desc: 'Course members are free to join'},
|
||||
{value: 'invitation_only', desc: 'Membership by invitation only'},
|
||||
]
|
||||
|
||||
|
||||
buildInvites: ->
|
||||
invites = {}
|
||||
@get('controllers.users').forEach (user) ->
|
||||
invites[user.get('id')] = if user.get('invite') then 1 else 0
|
||||
invites
|
||||
|
||||
|
||||
actions:
|
||||
createGroup: ->
|
||||
unless @nameError
|
||||
invites = @buildInvites()
|
||||
group = @store.createRecord 'group',
|
||||
name: @get('name')
|
||||
join_level: @get('joinLevel')
|
||||
course_id: ENV.course_id
|
||||
invites: invites
|
||||
|
||||
group.save().then (group) =>
|
||||
@set('name','')
|
||||
json = group.toJSON()
|
||||
json.users = [ENV.current_user]
|
||||
@get('controllers.student_groups.groups').pushObject(json)
|
|
@ -0,0 +1,44 @@
|
|||
define [
|
||||
'i18n!student_groups'
|
||||
'ember'
|
||||
], (I18n,Ember) ->
|
||||
|
||||
StudentGroupsController = Ember.ObjectController.extend
|
||||
filterText: ""
|
||||
content: Ember.Object.create()
|
||||
searchPlaceholder: (->
|
||||
I18n.t('search_groups_placeholder',"Search Groups or People")
|
||||
).property()
|
||||
searchAriaLabel: (->
|
||||
I18n.t('student_groups_filter_description',"As you type in this field, the list of groups will be automatically filtered to only include those whose names match your input.")
|
||||
).property()
|
||||
|
||||
usersPath: "/courses/#{ENV.course_id}/users"
|
||||
groupsUrl: "/api/v1/courses/#{ENV.course_id}/groups?include[]=users&include[]=group_category"
|
||||
sortedGroups: (->
|
||||
groups = @get('groups') || []
|
||||
text = @get('filterText').toLowerCase()
|
||||
groups = groups.filter (group) ->
|
||||
text.length == 0 or
|
||||
group.name.toLowerCase().indexOf(text) >= 0 or
|
||||
group.users.find (user) ->
|
||||
(user.display_name and user.display_name.toLowerCase().indexOf(text) >= 0) or
|
||||
(user.name and user.name.toLowerCase().indexOf(text) >= 0)
|
||||
|
||||
groups.sortBy('group_category_id')
|
||||
).property('groups.[]', 'filterText')
|
||||
|
||||
removeFromCategory: (categoryId) ->
|
||||
@groupsForCategory(categoryId).forEach (group) ->
|
||||
user = group.users.findBy('id', ENV.current_user_id)
|
||||
if user
|
||||
group.users.removeObject(user)
|
||||
|
||||
groupsForCategory: (categoryId) ->
|
||||
@get('groups').filterBy('group_category_id', categoryId)
|
||||
|
||||
isMemberOfCategory: (categoryId) ->
|
||||
@groupsForCategory(categoryId).any (group) ->
|
||||
group.users.findBy('id', ENV.current_user_id)
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
define [
|
||||
'i18n!student_groups'
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
], (I18n, Ember, ajax) ->
|
||||
|
||||
UserController = Ember.ObjectController.extend
|
||||
|
||||
idString:(->
|
||||
"invitees_#{@get('id')}"
|
||||
).property('id')
|
||||
|
||||
isCurrentUser: (->
|
||||
ENV.current_user_id == @get('id')
|
||||
).property('id')
|
||||
|
||||
invite:false
|
|
@ -0,0 +1,5 @@
|
|||
define ['ember'], (Ember) ->
|
||||
UsersController = Ember.ArrayController.extend
|
||||
|
||||
usersPath: "/api/v1/courses/#{ENV.course_id}/users"
|
||||
itemController: 'user'
|
|
@ -0,0 +1,19 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
], (Ember,DS) ->
|
||||
|
||||
attr = DS.attr
|
||||
|
||||
Group = DS.Model.extend
|
||||
|
||||
name: attr()
|
||||
join_level: attr()
|
||||
users: attr()
|
||||
|
||||
Group.reopenClass
|
||||
|
||||
url: ->
|
||||
debugger
|
||||
"/api/v1"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
], (Ember, DS) ->
|
||||
Membership = DS.Model.extend
|
||||
user_id: DS.attr('number')
|
||||
group_id: DS.attr('number')
|
|
@ -0,0 +1,16 @@
|
|||
define ['ember'], (Ember) ->
|
||||
|
||||
StudentGroupsRoute = Ember.Route.extend
|
||||
|
||||
actions:
|
||||
# TODO: Create a DialogRoute that has this action.
|
||||
_destroyModal: ->
|
||||
@disconnectOutlet
|
||||
outlet: 'modal'
|
||||
parentView: 'application'
|
||||
|
||||
newGroup: ->
|
||||
@render 'new_group',
|
||||
into: 'application'
|
||||
outlet: 'modal'
|
||||
theParent: @controller
|
|
@ -0,0 +1,24 @@
|
|||
define [
|
||||
'ember-data'
|
||||
], (DS) ->
|
||||
|
||||
ApplicationSerializer = DS.ActiveModelSerializer.extend
|
||||
|
||||
# A lot of CanvasAPI responses look like:
|
||||
# {
|
||||
# "id": "1",
|
||||
# "attribute": "value"
|
||||
# }
|
||||
#
|
||||
# Turn them into:
|
||||
#
|
||||
# {
|
||||
# "pluralized_resource_name": [{
|
||||
# "id": "1",
|
||||
# "attribute": "value"
|
||||
# }]
|
||||
# }
|
||||
extractSingle: (store, type, payload, id, requestType) ->
|
||||
obj = {}
|
||||
obj[type.typeKey.pluralize()] = payload
|
||||
this._super store, type, obj, id, requestType
|
|
@ -0,0 +1,2 @@
|
|||
{{outlet}}
|
||||
{{outlet modal}}
|
|
@ -0,0 +1,63 @@
|
|||
{{#form-dialog
|
||||
id="add_group_form"
|
||||
title="New Student Group"
|
||||
on-submit="createGroup"
|
||||
height=500
|
||||
width=700
|
||||
fix-dialog-buttons=false
|
||||
submit-disabled=nameError
|
||||
}}
|
||||
<p>
|
||||
{{#t 'group_explanation'}}
|
||||
Groups are a good place to collaborate on projects or to figure out schedules for study sessions
|
||||
and the like. Every group gets a calendar, a wiki, discussions, and a little bit of space to store
|
||||
files. Groups can collaborate on documents, or even schedule web conferences.
|
||||
It's really like a mini-course where you can work with a smaller number of students on a
|
||||
more focused project.
|
||||
{{/t}}
|
||||
</p>
|
||||
<table class="formtable">
|
||||
<tr>
|
||||
<td><label for="group_name">{{#t 'name'}}Group Name{{/t}}</label></td>
|
||||
<td>
|
||||
{{view Ember.TextField id="group_name" value=name onEvent="keyPress" type="search" action="validate"}}
|
||||
{{#if nameError}}
|
||||
<span class="text-error">
|
||||
{{#t 'name_error'}}Group name is required{{/t}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="">{{#t 'joining'}}Joining{{/t}}</label></td>
|
||||
<td>
|
||||
{{view Ember.Select
|
||||
id="group_join_level"
|
||||
content=selectOptions
|
||||
value=joinLevel
|
||||
optionLabelPath="content.desc"
|
||||
optionValuePath="content.value"
|
||||
aria-label="Group Join Options"
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="">Invite</label>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="unstyled_list">
|
||||
{{#view controller=controllers.users}}
|
||||
{{#ic-lazy-list data=content href=usersPath}}
|
||||
{{#each }}
|
||||
{{#unless isCurrentUser}}
|
||||
<li><label class="checkbox">{{input type="checkbox" checked=invite id=idString}}{{name}}</label></li>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
{{/ic-lazy-list}}
|
||||
{{/view}}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/form-dialog}}
|
|
@ -0,0 +1,92 @@
|
|||
{{#if noticeMessage}}
|
||||
<p>
|
||||
<div class="alert alert-success">
|
||||
{{noticeMessage}}
|
||||
</div>
|
||||
</p>
|
||||
{{/if}}
|
||||
<div class='pull-right group-categories-actions'>
|
||||
<button class="btn btn-primary add_group_link" aria-controls="testdialog" {{action 'newGroup'}} >
|
||||
<i class='icon-plus' aria-label="new"></i> {{#t 'group'}}Group{{/t}}
|
||||
</button>
|
||||
</div>
|
||||
<div id="group_categories_tabs" class="ui-tabs-minimal ui-tabs ui-widget ui-widget-content ui-corner-all">
|
||||
<ul class='collectionViewItems ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'>
|
||||
<li class="ui-state-default ui-corner-top"><a {{bind-attr href=usersPath}}>{{#t 'student_groups_tabs_everyone'}}Everyone{{/t}}</a></li>
|
||||
<li class="ui-state-default ui-corner-top ui-tabs-active ui-state-active"><a href="#">{{#t 'student_groups_tabs_groups'}}Groups{{/t}}</a></li>
|
||||
</ul>
|
||||
<div class="roster-tab tab-panel">
|
||||
<div class="form-inline clearfix content-box">
|
||||
<label for="search_field" class="screenreader-only">{{searchAriaLabel}}</label>
|
||||
{{view Ember.TextField
|
||||
value=filterText
|
||||
id="search_field"
|
||||
action="filter"
|
||||
type="search"
|
||||
onEvent="keyPress"
|
||||
placeholder=searchPlaceholder
|
||||
aria-label=searchAriaLabel
|
||||
}}
|
||||
</div>
|
||||
{{#ic-lazy-list data=groups href=groupsUrl}}
|
||||
{{#each sortedGroups itemController="group" }}
|
||||
<div {{bind-attr class=":accordion :student-groups :content-box showBody isMember"}}>
|
||||
<div class="student-group-header clearfix" >
|
||||
<i class="icon-mini-arrow-right" aria-hidden="true"></i>
|
||||
<i class="icon-mini-arrow-down" aria-hidden="true"></i>
|
||||
<div class="student-group-title">
|
||||
<h3>
|
||||
<a {{action 'toggleBody'}} href="#" {{bind-attr aria-expanded=showBody}} aria-controls="student-group-body" aria-label="show students for group">{{name}}</a>
|
||||
<small>{{group_category.name}}</small>
|
||||
</h3>
|
||||
{{#if isMember}}
|
||||
<a {{action 'visitGroup'}} href="#" aria-label="visit group page">{{#t 'visit'}}Visit{{/t}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if isMember}}
|
||||
{{#if canSignup}}
|
||||
<a href="#" class="student-group-join" {{action "leave" this}} aria-label="leave group">{{#t 'student_groups_leave'}}Leave{{/t}}</a>
|
||||
{{else}}
|
||||
<span class="student-group-join"> </span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if canSignup}}
|
||||
{{#if isFull}}
|
||||
<span class="student-group-join" aria-hidden="true">{{#t 'full'}}Full{{/t}}</span>
|
||||
<span class="screenreader-only">{{#t 'group_full'}}Group is full{{/t}}</span>
|
||||
{{else}}
|
||||
{{#if isMemberOfCategory}}
|
||||
<a href="#" class="student-group-join" {{action "join" this}} aria-label="switch to group">{{#t 'student_groups_switch'}}Switch To{{/t}}</a>
|
||||
{{else}}
|
||||
<a href="#" class="student-group-join" {{action "join" this}} aria-label="join group">{{#t 'student_groups_join'}}Join{{/t}}</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if isFull}}
|
||||
<span class="student-group-join" aria-label="group is full">{{#t 'full'}}Full{{/t}}</span>
|
||||
{{else}}
|
||||
<span class="student-group-join" aria-label="group is invite only">{{#t 'invite_only'}}Invite Only{{/t}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<span class="student-group-students">
|
||||
{{i18nStudentsCount}}
|
||||
</span>
|
||||
</div>
|
||||
{{#if showBody}}
|
||||
<div class="student-group-body">
|
||||
<ul class="student-group-list clearfix" aria-label="group members">
|
||||
{{#each users}}
|
||||
<li>
|
||||
{{#if display_name}}{{display_name}}{{else}}{{short_name}}{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/ic-lazy-list}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,19 @@
|
|||
define [
|
||||
'./start_app'
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
], (startApp, Ember, ajax) ->
|
||||
|
||||
App = null
|
||||
|
||||
|
||||
module 'student_groups',
|
||||
setup: ->
|
||||
App = startApp()
|
||||
teardown: ->
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
|
||||
test 'Ember is running', ->
|
||||
ok(true)
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
define [
|
||||
'../start_app'
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
'../../controllers/group_controller'
|
||||
'../../controllers/student_groups_controller'
|
||||
'helpers/fakeENV'
|
||||
], (startApp, Ember, ajax, GroupController, StudentGroupsController, fakeENV) ->
|
||||
|
||||
App = null
|
||||
|
||||
{run} = Ember
|
||||
|
||||
|
||||
module 'group_controller',
|
||||
setup: ->
|
||||
fakeENV.setup()
|
||||
App = startApp()
|
||||
run => @sgc = StudentGroupsController.create()
|
||||
run => @gc = GroupController.create(parentController: @sgc, showBody: false)
|
||||
run =>
|
||||
@group =
|
||||
id: 1
|
||||
name: "one"
|
||||
group_category_id: 2
|
||||
users: [{id: 1, name: "steve"}, {id: 2, name: "cliff"}, {id: 3, name: "walt"}, {id: 4, name: "pinkman"}]
|
||||
|
||||
@gc.set('content', @group)
|
||||
teardown: ->
|
||||
run =>
|
||||
fakeENV.teardown()
|
||||
@gc.destroy()
|
||||
@sgc.destroy()
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
|
||||
test 'should show body while searching', ->
|
||||
equal @gc.get('showBody'), false
|
||||
@sgc.set('filterText', "abc")
|
||||
equal @gc.get('showBody'), true
|
||||
|
||||
test 'member count should sum the users', ->
|
||||
equal @gc.get('memberCount'), @group.users.length
|
||||
|
||||
|
||||
test 'toggleBody should change show body if the group has members', ->
|
||||
@gc.send('toggleBody')
|
||||
equal @gc.get('showBody'), true
|
||||
|
||||
test 'leave should remove the current user from the group', ->
|
||||
ENV.current_user_id = 1
|
||||
ajax.defineFixture '/api/v1/groups/1/memberships/self',
|
||||
response:
|
||||
id: "1"
|
||||
testStatus: '200'
|
||||
jqXHR: {}
|
||||
|
||||
@gc.send('leave')
|
||||
|
||||
equal @gc.get('users').length, 3
|
|
@ -0,0 +1,67 @@
|
|||
define [
|
||||
'../start_app'
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
'../../controllers/student_groups_controller'
|
||||
'helpers/fakeENV'
|
||||
], (startApp, Ember, ajax, StudentGroupsController, fakeENV) ->
|
||||
|
||||
App = null
|
||||
|
||||
{run} = Ember
|
||||
|
||||
|
||||
module 'student_groups_controller',
|
||||
setup: ->
|
||||
App = startApp()
|
||||
fakeENV.setup()
|
||||
run => @sgc = StudentGroupsController.create()
|
||||
groups = null
|
||||
run =>
|
||||
groups = [
|
||||
id: 1
|
||||
name: "one"
|
||||
group_category_id: 2
|
||||
users: [{id: 1, name: "steve"}, {id: 2, name: "cliff"}, {id: 3, name: "walt"}, {id: 4, name: "pinkman"}]
|
||||
,
|
||||
id: 2
|
||||
name: "two"
|
||||
group_category_id: 1
|
||||
users: [{id: 1, name: "steve"}, {id: 2, name: "cliff"}, {id: 3, name: "walt"}]
|
||||
]
|
||||
@sgc.set('groups', groups)
|
||||
teardown: ->
|
||||
run =>
|
||||
@sgc.destroy()
|
||||
fakeENV.teardown()
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
|
||||
test 'Groups are sorted group category id', ->
|
||||
sorted = @sgc.get('sortedGroups')
|
||||
equal sorted[0].name, "two"
|
||||
|
||||
test 'filterText will filter groups by user name', ->
|
||||
@sgc.set('filterText', "pink")
|
||||
sorted = @sgc.get('sortedGroups')
|
||||
equal sorted.length, 1
|
||||
equal sorted[0].name, "one"
|
||||
|
||||
|
||||
test 'is member of category should be true if the current user is', ->
|
||||
ENV.current_user_id = 1
|
||||
equal @sgc.isMemberOfCategory(1)?, true
|
||||
|
||||
test 'is member of category should be false if the current user is not', ->
|
||||
ENV.current_user_id = 4
|
||||
equal @sgc.isMemberOfCategory(1)?, false
|
||||
|
||||
test 'remove from category removes the user from any group in the category', ->
|
||||
ENV.current_user_id = 2
|
||||
equal @sgc.isMemberOfCategory(1)?, true
|
||||
@sgc.removeFromCategory(1)
|
||||
equal @sgc.isMemberOfCategory(1)?, false
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
define ['../main'], (Application) ->
|
||||
karmaLoaded = window.__karma__
|
||||
Ember.LOG_VERSION = !karmaLoaded
|
||||
startApp = ->
|
||||
App = null
|
||||
Ember.run.join ->
|
||||
App = Application.create
|
||||
LOG_ACTIVE_GENERATION: !karmaLoaded
|
||||
LOG_MODULE_RESOLVER: !karmaLoaded
|
||||
LOG_TRANSITIONS: !karmaLoaded
|
||||
LOG_TRANSITIONS_INTERNAL: !karmaLoaded
|
||||
LOG_VIEW_LOOKUPS: !karmaLoaded
|
||||
rootElement: '#fixtures'
|
||||
history: 'none'
|
||||
App.Router.reopen history: 'none'
|
||||
App.setupForTesting()
|
||||
App.injectTestHelpers()
|
||||
window.App = App
|
||||
App
|
|
@ -0,0 +1,27 @@
|
|||
define [
|
||||
'i18n!roster'
|
||||
'jquery'
|
||||
'underscore'
|
||||
'Backbone'
|
||||
'jst/courses/roster/rosterTabs'
|
||||
], (I18n, $, _, Backbone, template) ->
|
||||
|
||||
class RosterTabsView extends Backbone.View
|
||||
template: template
|
||||
|
||||
tagName: 'li'
|
||||
className: 'collectionViewItems ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'
|
||||
|
||||
|
||||
attach: ->
|
||||
@collection.on 'reset', @render, this
|
||||
|
||||
fetch: ->
|
||||
if ENV.canManageCourse
|
||||
@collection.fetch()
|
||||
|
||||
toJSON: ->
|
||||
json = {}
|
||||
json.collection = super
|
||||
json.course = ENV.course
|
||||
json
|
|
@ -4,7 +4,8 @@ define [
|
|||
'Backbone'
|
||||
'jst/courses/roster/index'
|
||||
'compiled/views/ValidatedMixin'
|
||||
], (I18n, $, Backbone, template, ValidatedMixin) ->
|
||||
'compiled/models/GroupCategory'
|
||||
], (I18n, $, Backbone, template, ValidatedMixin, GroupCategory) ->
|
||||
|
||||
class RosterView extends Backbone.View
|
||||
|
||||
|
@ -20,12 +21,15 @@ define [
|
|||
|
||||
@child 'resendInvitationsView', '[data-view=resendInvitations]'
|
||||
|
||||
@child 'rosterTabsView', '[data-view=rosterTabs]'
|
||||
|
||||
@optionProperty 'roles'
|
||||
|
||||
@optionProperty 'permissions'
|
||||
|
||||
@optionProperty 'course'
|
||||
|
||||
|
||||
template: template
|
||||
|
||||
els:
|
||||
|
@ -48,6 +52,12 @@ define [
|
|||
@lastRequest?.abort()
|
||||
@lastRequest = @collection.fetch().fail @onFail
|
||||
|
||||
course_id: ->
|
||||
ENV.context_asset_string.split('_')[1]
|
||||
|
||||
canAddCategories: ->
|
||||
ENV.canManageCourse
|
||||
|
||||
toJSON: -> this
|
||||
|
||||
onFail: (xhr) =>
|
||||
|
|
|
@ -21,6 +21,7 @@ define [
|
|||
els: _.extend {},
|
||||
CollectionView::els
|
||||
'#group_categories_tabs': '$tabs'
|
||||
'li.static': '$static'
|
||||
'#add-group-set': '$addGroupSetButton'
|
||||
'.empty-groupset-instructions': '$emptyInstructions'
|
||||
|
||||
|
@ -32,13 +33,28 @@ define [
|
|||
tagName: 'li'
|
||||
template: -> tabTemplate _.extend(@model.present(), id: @model.id ? @model.cid)
|
||||
|
||||
|
||||
render: ->
|
||||
super
|
||||
@reorder() if @collection.length > 1
|
||||
@refreshTabs()
|
||||
@loadTabFromUrl()
|
||||
|
||||
|
||||
refreshTabs: ->
|
||||
if @collection.length > 0
|
||||
@$tabs.find('ul.ui-tabs-nav li.static').remove()
|
||||
@$tabs.find('ul.ui-tabs-nav').prepend(@$static)
|
||||
# setup the tabs
|
||||
if @$tabs.data("tabs")
|
||||
@$tabs.tabs("refresh").show()
|
||||
else
|
||||
@$tabs.tabs({cookie: {}}).show()
|
||||
|
||||
@$tabs.tabs
|
||||
beforeActivate: (event, ui) ->
|
||||
!ui.newTab.hasClass('static')
|
||||
|
||||
# hide/show the instruction text
|
||||
if @collection.length > 0
|
||||
@$emptyInstructions.hide()
|
||||
|
@ -46,6 +62,27 @@ define [
|
|||
@$emptyInstructions.show()
|
||||
# hide the emtpy tab set which may have borders that would otherwise show
|
||||
@$tabs.hide()
|
||||
@$tabs.find('li.static a').unbind()
|
||||
@$tabs.on 'keydown', 'li.static', (event) ->
|
||||
event.stopPropagation()
|
||||
if event.keyCode == 13 or event.keyCode == 32
|
||||
window.location.href = $(this).find('a').attr('href')
|
||||
|
||||
loadTabFromUrl: ->
|
||||
if location.hash == "#new"
|
||||
@addGroupSet()
|
||||
else
|
||||
id = location.hash.split('-')[1]
|
||||
if id?
|
||||
model = @collection.get(id)
|
||||
if model
|
||||
@$tabs.tabs active: @tabOffsetOfModel(model)
|
||||
|
||||
|
||||
tabOffsetOfModel: (model) ->
|
||||
index = @collection.indexOf(model)
|
||||
numStatic = @$static.length
|
||||
index + numStatic
|
||||
|
||||
createItemView: (model) ->
|
||||
# create and add tab panel
|
||||
|
@ -61,7 +98,6 @@ define [
|
|||
view.render()
|
||||
@reorder()
|
||||
@refreshTabs()
|
||||
@$tabs.tabs active: @collection.indexOf(model)
|
||||
view
|
||||
|
||||
renderItem: ->
|
||||
|
@ -76,14 +112,17 @@ define [
|
|||
@refreshTabs()
|
||||
|
||||
addGroupSet: (e) ->
|
||||
e.preventDefault()
|
||||
e.preventDefault() if e?
|
||||
@createView ?= new GroupCategoryCreateView
|
||||
collection: @collection
|
||||
trigger: @$addGroupSetButton
|
||||
cat = new GroupCategory
|
||||
cat.once 'sync', =>
|
||||
window.location.hash = "tab-#{cat.id}"
|
||||
@collection.add(cat)
|
||||
@$tabs.tabs active: @collection.indexOf(cat)
|
||||
@reorder()
|
||||
@refreshTabs()
|
||||
@$tabs.tabs active: @tabOffsetOfModel(cat)
|
||||
@createView.model = cat
|
||||
@createView.open()
|
||||
|
||||
|
@ -102,3 +141,13 @@ define [
|
|||
# store now loaded
|
||||
$panel.data('loaded', true)
|
||||
$panel
|
||||
|
||||
toJSON: ->
|
||||
json = super
|
||||
json.ENV=ENV
|
||||
context = ENV.context_asset_string.split('_')
|
||||
json.context = context[0]
|
||||
json.isCourse = json.context == "course"
|
||||
json.context_id = context[1]
|
||||
json
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ define [
|
|||
|
||||
setUnassignedHeading: ->
|
||||
count = @model.unassignedUsersCount() ? 0
|
||||
@unassignedUsersView.render()
|
||||
@$unassignedUsersHeading.text(
|
||||
if @model.get('allows_multiple_memberships')
|
||||
I18n.t('everyone', "Everyone (%{count})", {count})
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
# @API Groups
|
||||
#
|
||||
# Group memberships are the objects that tie users and groups together.
|
||||
# Group memberships are the objects that tie users and groups together.
|
||||
#
|
||||
# @model GroupMembership
|
||||
# {
|
||||
|
@ -88,8 +88,8 @@ class GroupMembershipsController < ApplicationController
|
|||
# return all memberships.
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships \
|
||||
# -F 'filter_states[]=invited&filter_states[]=requested' \
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships \
|
||||
# -F 'filter_states[]=invited&filter_states[]=requested' \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
# @returns [GroupMembership]
|
||||
|
@ -119,7 +119,7 @@ class GroupMembershipsController < ApplicationController
|
|||
# @argument user_id [String]
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships \
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships \
|
||||
# -F 'user_id=self'
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
#
|
||||
|
@ -150,7 +150,7 @@ class GroupMembershipsController < ApplicationController
|
|||
# @argument moderator
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships/<membership_id> \
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships/<membership_id> \
|
||||
# -F 'moderator=true'
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
# @example_request
|
||||
|
@ -180,8 +180,8 @@ class GroupMembershipsController < ApplicationController
|
|||
# in place of a membership_id.
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships/<membership_id> \
|
||||
# -X DELETE \
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/memberships/<membership_id> \
|
||||
# -X DELETE \
|
||||
# -H 'Authorization: Bearer <token>'
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/groups/<group_id>/users/<user_id> \
|
||||
|
|
|
@ -288,8 +288,8 @@ class GroupsController < ApplicationController
|
|||
|
||||
format.json do
|
||||
path = send("api_v1_#{@context.class.to_s.downcase}_user_groups_url")
|
||||
paginated_groups = Api.paginate(@groups, self, path)
|
||||
render :json => paginated_groups.map { |g| group_json(g, @current_user, session) }
|
||||
paginated_groups = Api.paginate(all_groups, self, path)
|
||||
render :json => paginated_groups.map { |g| group_json(g, @current_user, session, :include => Array(params[:include])) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,3 +31,77 @@
|
|||
a.assign_students_link
|
||||
display: none
|
||||
|
||||
.student-groups
|
||||
border: 1px solid #c1c7cf
|
||||
border-radius: 3px
|
||||
.student-group-header
|
||||
@extend .clearfix
|
||||
padding: 16px 16px 16px 10px
|
||||
.icon-mini-arrow-right, .icon-mini-arrow-down
|
||||
float: left
|
||||
.icon-mini-arrow-down
|
||||
display: none
|
||||
&.show-body
|
||||
.student-group-header
|
||||
background-color: #f5f5f5
|
||||
border-bottom: 1px solid #c1c7cf
|
||||
cursor: pointer
|
||||
.icon-mini-arrow-right
|
||||
display: none
|
||||
.icon-mini-arrow-down
|
||||
display: block
|
||||
.student-group-title
|
||||
float: left
|
||||
h3
|
||||
margin: 0 7px 0 5px
|
||||
font-weight: bold
|
||||
font-size: 14px
|
||||
float: left
|
||||
line-height: 18px
|
||||
|
||||
small
|
||||
font-size: 12px
|
||||
|
||||
a
|
||||
color: #555
|
||||
a
|
||||
position: relative
|
||||
top: -1px
|
||||
.student-group-students
|
||||
float: right
|
||||
color: #555
|
||||
font-weight: normal
|
||||
.student-group-join
|
||||
float: right
|
||||
text-transform: uppercase
|
||||
font-weight: bold
|
||||
width: 175px
|
||||
text-align: right
|
||||
.student-group-body
|
||||
padding: 16px 16px 16px 36px
|
||||
&:last-child
|
||||
border-bottom: 0
|
||||
.student-group-list
|
||||
list-style: none
|
||||
margin: 0
|
||||
li
|
||||
float: left
|
||||
width: 25%
|
||||
.group-categories-actions
|
||||
height: 36px
|
||||
padding-bottom: 10px
|
||||
z-index: 1
|
||||
position: relative
|
||||
|
||||
|
||||
#group_categories_tabs
|
||||
margin: 0 -1em -1em
|
||||
> .collectionViewItems
|
||||
padding: 10px 0 0 1em
|
||||
border-bottom: 0px
|
||||
|
||||
> .tab-panel
|
||||
border-top: 1px solid #aaaaaa
|
||||
> .roster-tab
|
||||
padding-left: 1em
|
||||
padding-right: 1em
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
<% if @context.is_a?(Course) %>
|
||||
<% js_bundle :roster %>
|
||||
<% js_env :canManageCourse => can_do(@context, @current_user, :manage) %>
|
||||
<% else %>
|
||||
<% content_for :stylesheets do %>
|
||||
<style>
|
||||
|
@ -48,7 +49,7 @@
|
|||
<div class="more_info">
|
||||
<div class="short_name"><%= student.short_name %></div>
|
||||
<div class="email"><%= student.email %></div>
|
||||
<% if @enrollments_hash %>
|
||||
<% if @enrollments_hash %>
|
||||
<% @enrollments_hash[student.id].each do |e| %>
|
||||
<div class="course_section"><%= e.try(:course_section).try(:display_name) %></div>
|
||||
<% end %>
|
||||
|
@ -78,7 +79,7 @@
|
|||
<div class="more_info">
|
||||
<div class="short_name"><%= teacher.short_name %></div>
|
||||
<div class="email"><%= teacher.email %></div>
|
||||
<% if @enrollments_hash %>
|
||||
<% if @enrollments_hash %>
|
||||
<% @enrollments_hash[teacher.id].each do |e| %>
|
||||
<div class="course_section"><%= e.try(:course_section).try(:display_name) %></div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<% content_for :page_title do %><%= join_title t(:page_title, 'Course Groups'), @context.name %><% end %>
|
||||
|
||||
<% if !@context.is_a?(Account) && @context.root_account.feature_enabled?(:student_groups_next) %>
|
||||
<% js_env :course_id => @context.id %>
|
||||
<% js_bundle :student_groups %>
|
||||
<% else %>
|
||||
|
||||
<% content_for :right_side do %>
|
||||
<div class="rs-margin-lr rs-margin-top">
|
||||
<% if @context.respond_to?(:allow_student_organized_groups) && @context.allow_student_organized_groups %>
|
||||
|
@ -9,82 +14,90 @@
|
|||
<% else %>
|
||||
<%= t :groups_disabled, 'Student-organized groups have been disabled for this course, so you\'ll have to be content with the groups the teacher makes for you.' %>
|
||||
<% end %>
|
||||
<a href="<%= context_url(@context, :context_users_url) %>" class="btn button-sidebar-wide icon-user"><%= @context.is_a?(Account) ? t('#buttons.view_account_roster', 'View Account Roster') : t('#buttons.view_course_roster', 'View Course Roster') %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% js_bundle :groups %>
|
||||
<h2><%= @context.is_a?(Account) ? t('account_groups', 'Account Groups') : t('course_groups', 'Course Groups') %></h2>
|
||||
<p>
|
||||
<%= t 'group_explanation', <<-TEXT
|
||||
Groups are a good place to collaborate on projects or to figure out schedules for study sessions
|
||||
and the like. Every group gets a calendar, a wiki, discussions, and a little bit of space to store
|
||||
files. Groups can collaborate on documents, or even schedule web conferences.
|
||||
It's really like a mini-course where you can work with a smaller number of students on a
|
||||
more focused project.
|
||||
TEXT
|
||||
%>
|
||||
</p>
|
||||
|
||||
<ul class="group_list">
|
||||
<%= render :partial => 'group', :collection => @user_groups, :locals => {:in_group => true} %>
|
||||
<%= render :partial => 'group', :object => nil, :locals => {:in_group => true} %>
|
||||
</ul>
|
||||
|
||||
<% if @available_groups && !@available_groups.empty? %>
|
||||
<h2><%= t 'headings.available_groups', "Available Groups" %></h2>
|
||||
<ul class="group_list">
|
||||
<%= render :partial => 'group', :collection => @available_groups, :locals => {:in_categories => @user_groups.map{ |g| g.group_category }.compact.uniq, :in_group => false} %>
|
||||
<div id="group_categories_tabs" class="ui-tabs-minimal ui-tabs ui-widget ui-widget-content ui-corner-all">
|
||||
<ul class='collectionViewItems ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'>
|
||||
<li class="ui-state-default ui-corner-top"><a href="<%= context_url(@context, :context_users_url) %>"><%=t('student_groups_tabs_everyone',"Everyone") %></a></li>
|
||||
<li class="ui-state-default ui-corner-top ui-tabs-active ui-state-active"><a href="#"><%= t('student_groups_tabs_groups',"Groups") %></a></li>
|
||||
</ul>
|
||||
<div class="roster-tab tab-panel">
|
||||
<h2><%= @context.is_a?(Account) ? t('account_groups', 'Account Groups') : t('course_groups', 'Course Groups') %></h2>
|
||||
<p>
|
||||
<%= t 'group_explanation', <<-TEXT
|
||||
Groups are a good place to collaborate on projects or to figure out schedules for study sessions
|
||||
and the like. Every group gets a calendar, a wiki, discussions, and a little bit of space to store
|
||||
files. Groups can collaborate on documents, or even schedule web conferences.
|
||||
It's really like a mini-course where you can work with a smaller number of students on a
|
||||
more focused project.
|
||||
TEXT
|
||||
%>
|
||||
</p>
|
||||
<ul class="group_list">
|
||||
<%= render :partial => 'group', :collection => @user_groups, :locals => {:in_group => true} %>
|
||||
<%= render :partial => 'group', :object => nil, :locals => {:in_group => true} %>
|
||||
</ul>
|
||||
|
||||
<% if @available_groups && !@available_groups.empty? %>
|
||||
<h2><%= t 'headings.available_groups', "Available Groups" %></h2>
|
||||
<ul class="group_list">
|
||||
<%= render :partial => 'group', :collection => @available_groups, :locals => {:in_categories => @user_groups.map{ |g| g.group_category }.compact.uniq, :in_group => false} %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<%= form_for :group, :url => context_url(@context, :context_groups_url), :html => {:id => "add_group_form", :style => "display: none;"} do |f| %>
|
||||
<h2><%= t 'headings.new_group', 'Make a New Group' %></h2>
|
||||
<%= image_tag "warning.png" %>
|
||||
<%= t :student_group_warning, <<-TEXT
|
||||
If your teacher has talked about putting you into
|
||||
groups as part of an assignment, this is not the way to make that happen.
|
||||
Groups you organize yourself can't be used for grading... you can still form
|
||||
your own groups, but you won't be able to turn in an electric copy of any
|
||||
assignments unless your teacher builds the groups for you.
|
||||
TEXT
|
||||
%>
|
||||
<table class="formtable">
|
||||
<tr>
|
||||
<td><%= f.blabel :name, :en => "Group Name" %></td>
|
||||
<td><%= f.text_field :name %></td>
|
||||
<% if @context %>
|
||||
<tr>
|
||||
<td><%= before_label :restrictions_for_joining_groups, 'Joining' %></td>
|
||||
<td>
|
||||
<select id="group_join_level" name="group[join_level]">
|
||||
<option value="parent_context_auto_join"><%= t 'options.open_to_course_members', 'Course members are free to join' %></option>
|
||||
<%# Right now the 'parent_context_request' option isn't any different
|
||||
than 'parent_context_auto_join' because we're auto-accepting requested
|
||||
memberships. So, we're removing it until we implement an accept request
|
||||
feature. %>
|
||||
<option value="invitation_only"><%= t 'options.invite_only', 'Membership by invitation only' %></option>
|
||||
</select>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr><tr>
|
||||
<td style="vertical-align: top;">
|
||||
<%= before_label :users_to_invite, 'Invite' %>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="unstyled_list">
|
||||
<% (@context.users - [@current_user]).each do |user| %>
|
||||
<li>
|
||||
<%= check_box :invitees, "#{user.id}".to_sym, :value => user.id %>
|
||||
<%= label :invitees, "#{user.id}".to_sym, context_user_name(@context, user) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td colspan="2">
|
||||
<button type="submit" class="btn submit_button"><%= t 'buttons.create', 'Create Group' %></button>
|
||||
<button type="button" class="cancel_button btn button-secondary"><%= t '#buttons.cancel', 'Cancel' %></button>
|
||||
</td>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= form_for :group, :url => context_url(@context, :context_groups_url), :html => {:id => "add_group_form", :style => "display: none;"} do |f| %>
|
||||
<h2><%= t 'headings.new_group', 'Make a New Group' %></h2>
|
||||
<%= image_tag "warning.png" %>
|
||||
<%= t :student_group_warning, <<-TEXT
|
||||
If your teacher has talked about putting you into
|
||||
groups as part of an assignment, this is not the way to make that happen.
|
||||
Groups you organize yourself can't be used for grading... you can still form
|
||||
your own groups, but you won't be able to turn in an electric copy of any
|
||||
assignments unless your teacher builds the groups for you.
|
||||
TEXT
|
||||
%>
|
||||
<table class="formtable">
|
||||
<tr>
|
||||
<td><%= f.blabel :name, :en => "Group Name" %></td>
|
||||
<td><%= f.text_field :name %></td>
|
||||
<% if @context %>
|
||||
<tr>
|
||||
<td><%= before_label :restrictions_for_joining_groups, 'Joining' %></td>
|
||||
<td>
|
||||
<select id="group_join_level" name="group[join_level]">
|
||||
<option value="parent_context_auto_join"><%= t 'options.open_to_course_members', 'Course members are free to join' %></option>
|
||||
<%# Right now the 'parent_context_request' option isn't any different
|
||||
than 'parent_context_auto_join' because we're auto-accepting requested
|
||||
memberships. So, we're removing it until we implement an accept request
|
||||
feature. %>
|
||||
<option value="invitation_only"><%= t 'options.invite_only', 'Membership by invitation only' %></option>
|
||||
</select>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr><tr>
|
||||
<td style="vertical-align: top;">
|
||||
<%= before_label :users_to_invite, 'Invite' %>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="unstyled_list">
|
||||
<% (@context.users - [@current_user]).each do |user| %>
|
||||
<li>
|
||||
<%= check_box :invitees, "#{user.id}".to_sym, :value => user.id %>
|
||||
<%= label :invitees, "#{user.id}".to_sym, context_user_name(@context, user) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td colspan="2">
|
||||
<button type="submit" class="btn submit_button"><%= t 'buttons.create', 'Create Group' %></button>
|
||||
<button type="button" class="cancel_button btn button-secondary"><%= t '#buttons.cancel', 'Cancel' %></button>
|
||||
</td>
|
||||
</table>
|
||||
<% end %>
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
<div class="form-inline clearfix">
|
||||
<div class="screenreader-only" id="filter_description">
|
||||
{{#t "filter_field_description"}}As you type in this field, the list of people will be automatically filtered to only include those whose names match your input.{{/t}}
|
||||
</div>
|
||||
<input
|
||||
{{#if canAddCategories}}
|
||||
<div class='pull-right group-categories-actions'>
|
||||
<a href="/courses/{{course_id}}/groups#new" class="btn btn-primary">
|
||||
<i class='icon-plus'></i> {{#t 'group_set'}}Group Set{{/t}}
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div id="group_categories_tabs" class="ui-tabs-minimal ui-tabs ui-widget ui-widget-content ui-corner-all" style="display: block;">
|
||||
<ul class="collectionViewItems ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" role="tablist" data-view="rosterTabs"></ul>
|
||||
<div class="roster-tab tab-panel ui-tabs-panel form-inline" style="height:800px">
|
||||
<input
|
||||
type="text"
|
||||
name="search_term"
|
||||
data-view="inputFilter"
|
||||
placeholder='{{#t "search_people"}}Search people{{/t}}'
|
||||
aria-label='{{#t "filter_list_of_people"}}Filter list of people{{/t}}'
|
||||
aria-describedby="filter_description"
|
||||
>
|
||||
aria-label='{{#t "filter_field_description"}}As you type in this field, the list of people will be automatically filtered to only include those whose names match your input.{{/t}}'
|
||||
>
|
||||
|
||||
<select
|
||||
name="enrollment_role"
|
||||
data-view="roleSelect"
|
||||
aria-label='{{#t "role_to_search"}}Limit search to role{{/t}}'
|
||||
></select>
|
||||
<select
|
||||
name="enrollment_role"
|
||||
data-view="roleSelect"
|
||||
aria-label='{{#t "role_to_search"}}Limit search to role{{/t}}'
|
||||
></select>
|
||||
|
||||
{{#if permissions.add_users}}
|
||||
{{#if permissions.add_users}}
|
||||
{{#if course.concluded}}
|
||||
<a
|
||||
href="#"
|
||||
|
@ -27,8 +33,8 @@
|
|||
title='{{#t "cannot_add_users"}}New users can not be added because this course is concluded{{/t}}'
|
||||
aria-label='{{#t "cannot_add_users"}}New users can not be added because this course is concluded{{/t}}'
|
||||
disabled data-tooltip>
|
||||
{{#t "people"}}People{{/t}}
|
||||
</a>
|
||||
{{#t "people"}}People{{/t}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a
|
||||
href="#"
|
||||
|
@ -37,15 +43,16 @@
|
|||
role="button"
|
||||
title='{{#t "title_add_people"}}Add People{{/t}}'
|
||||
aria-label='{{#t "title_add_people"}}Add People{{/t}}'
|
||||
>{{#t "people"}}People{{/t}}</a>
|
||||
>{{#t "people"}}People{{/t}}</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<form data-view="createUsers" class="form-dialog"></form>
|
||||
|
||||
<div class="v-gutter">
|
||||
<div data-view="resendInvitations"></div>
|
||||
<div data-view="users"></div>
|
||||
{{/if}}
|
||||
<form data-view="createUsers" class="form-dialog"></form>
|
||||
|
||||
<div class="v-gutter">
|
||||
<div data-view="resendInvitations"></div>
|
||||
<div data-view="users"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<li class="static ui-state-default ui-corner-top ui-tabs-active ui-state-active" >
|
||||
<a href="/courses/1/users" class="ui-tabs-anchor" >{{#t 'everyone_tab'}}Everyone{{/t}}</a>
|
||||
</li>
|
||||
{{#each collection}}
|
||||
<li class="ui-state-default ui-corner-top" >
|
||||
<a href="/courses/{{course_id}}/groups#tab-{{id}}" class="group-category-tab-link ui-tabs-anchor">
|
||||
{{name}}
|
||||
</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="static ui-state-default ui-corner-top" ><a href="/courses/{{course.id}}/groups" class="ui-tabs-anchor" >{{#t 'groups'}}Groups{{/t}}</a></li>
|
||||
{{/each}}
|
|
@ -31,9 +31,14 @@
|
|||
</p>
|
||||
{{/ifEqual}}
|
||||
</div>
|
||||
|
||||
<div style="display:none">
|
||||
{{#if isCourse}}
|
||||
<li class="static"><a href="/courses/{{context_id}}/users">{{#t 'tabs.everyone'}}Everyone{{/t}}</a></li>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="group_categories_tabs" class="ui-tabs-minimal">
|
||||
<ul class='collectionViewItems'></ul>
|
||||
<ul class='collectionViewItems'>
|
||||
</ul>
|
||||
<!--panels get inserted dynamically-->
|
||||
</div>
|
||||
|
||||
|
|
|
@ -134,6 +134,17 @@
|
|||
<% if @context.try(:short_name) %>
|
||||
<div class='h1' id="section-tabs-header">
|
||||
<%= @context.short_name %>
|
||||
<% if @context && @context.is_a?(Group) && can_do(@context, @current_user, :manage)%>
|
||||
<a class="al-trigger" href="#" style="display:inline-block; float:right" data-popup-within="#wrapper">
|
||||
<i class="icon-mini-arrow-down"></i>
|
||||
<span class="screenreader-only"><%= t("group_switch","Switch")%></span>
|
||||
</a>
|
||||
<ul class="al-options">
|
||||
<% @context.group_category.groups.active.by_name.each do |group| %>
|
||||
<li><%= link_to group.short_name, group_path(group) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% if @context && @context.respond_to?(:context) %>
|
||||
<a href="<%= url_for(@context.context) %>" id="section-tabs-header-subtitle" class="ellipsis"><%= @context.context.name %></a>
|
||||
<% elsif @context && @context.respond_to?(:enrollment_term) && !@context.enrollment_term.default_term? %>
|
||||
|
|
|
@ -48,6 +48,9 @@ module Api::V1::Group
|
|||
# TODO: this should be switched to user_display_json
|
||||
hash['users'] = group.users.map{ |u| user_json(u, user, session) }
|
||||
end
|
||||
if includes.include?('group_category')
|
||||
hash['group_category'] = group_category_json(group.group_category, user, session)
|
||||
end
|
||||
hash['html_url'] = group_url(group) if includes.include? 'html_url'
|
||||
hash['sis_group_id'] = group.sis_source_id if group.context_type == 'Account' && group.root_account.grants_rights?(user, session, :read_sis, :manage_sis).values.any?
|
||||
hash['sis_import_id'] = group.sis_batch_id if group.context_type == 'Account' && group.root_account.grants_right?(user, session, :manage_sis)
|
||||
|
|
|
@ -23,7 +23,7 @@ module Api::V1::User
|
|||
|
||||
API_USER_JSON_OPTS = {
|
||||
:only => %w(id name),
|
||||
:methods => %w(sortable_name short_name)
|
||||
:methods => %w(sortable_name short_name display_name)
|
||||
}
|
||||
|
||||
def user_json_preloads(users, preload_email=false)
|
||||
|
|
|
@ -205,6 +205,18 @@ END
|
|||
state: 'hidden',
|
||||
root_opt_in: true,
|
||||
development: true
|
||||
},
|
||||
'student_groups_next' =>
|
||||
{
|
||||
display_name: -> { I18n.t('features.student_groups', 'New Student Groups Page') },
|
||||
description: -> { I18n.t('student_groups_desc', <<-END) },
|
||||
This enables the new student group page for an account. The new page was build to provide a more dynamic group signup
|
||||
experience.
|
||||
END
|
||||
applies_to: 'RootAccount',
|
||||
state: 'allowed',
|
||||
root_opt_in: true,
|
||||
development: true
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@ describe "Groups API", type: :request do
|
|||
response.code.should == '401'
|
||||
end
|
||||
|
||||
it "should limit students to their own groups" do
|
||||
it "should show students all groups" do
|
||||
course_with_student(:active_all => true)
|
||||
@group_1 = @course.groups.create!(:name => 'Group 1')
|
||||
@group_2 = @course.groups.create!(:name => 'Group 2')
|
||||
|
@ -143,7 +143,7 @@ describe "Groups API", type: :request do
|
|||
json = api_call(:get, "/api/v1/courses/#{@course.to_param}/groups.json",
|
||||
@category_path_options.merge(:action => 'context_index',
|
||||
:course_id => @course.to_param))
|
||||
json.count.should == 1
|
||||
json.count.should == 2
|
||||
json.first['id'].should == @group_1.id
|
||||
end
|
||||
|
||||
|
@ -153,6 +153,12 @@ describe "Groups API", type: :request do
|
|||
json.should == group_json(@community)
|
||||
end
|
||||
|
||||
it "should include the group category" do
|
||||
@user = @member
|
||||
json = api_call(:get, "#{@community_path}.json?include[]=group_category", @category_path_options.merge(:group_id => @community.to_param, :action => "show", :include => [ "group_category" ]))
|
||||
json.has_key?("group_category").should be_true
|
||||
end
|
||||
|
||||
it 'should include permissions' do
|
||||
# Make sure it only returns permissions when asked
|
||||
json = api_call(:get, @community_path, @category_path_options.merge(:group_id => @community.to_param, :action => "show", :format => 'json'))
|
||||
|
|
|
@ -123,4 +123,30 @@ describe "groups" do
|
|||
new_group_el.find_elements(:css, ".student").length.should == 2
|
||||
end
|
||||
|
||||
describe "new groups page" do
|
||||
it "should allow a student to create a group" do
|
||||
course_with_student_logged_in(:active_all => true)
|
||||
@course.root_account.enable_feature!(:student_groups_next)
|
||||
student_in_course
|
||||
student_in_course
|
||||
|
||||
get "/courses/#{@course.id}/groups"
|
||||
wait_for_ajaximations
|
||||
|
||||
keep_trying_until do
|
||||
f(".add_group_link").click
|
||||
wait_for_animations
|
||||
end
|
||||
|
||||
f("#group_name").send_keys("My Group")
|
||||
ff("#group_join_level option").length.should == 2
|
||||
f("#invitees_#{@student.id}").click
|
||||
fj('button.confirm-dialog-confirm-btn').click
|
||||
wait_for_ajaximations
|
||||
|
||||
new_group_el = fj(".student-group-header:first").text
|
||||
new_group_el.should include "My Group"
|
||||
new_group_el.should include "1 student"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,13 +24,14 @@ describe "manage groups" do
|
|||
f('#add-group-set').click
|
||||
set_value f('#new_category_name'), "zomg"
|
||||
f('[name=split_groups]').click
|
||||
set_value f('[name=create_group_count]'), 2
|
||||
driver.execute_script("$('[name=create_group_count]:enabled').val(2)")
|
||||
submit_form f('.group-category-create')
|
||||
|
||||
wait_for_ajaximations
|
||||
|
||||
# yay, added
|
||||
f('#group_categories_tabs .collectionViewItems').text.should == 'zomg'
|
||||
f('#group_categories_tabs .collectionViewItems').text.should include('Everyone')
|
||||
f('#group_categories_tabs .collectionViewItems').text.should include('zomg')
|
||||
|
||||
run_jobs
|
||||
|
||||
|
@ -60,7 +61,7 @@ describe "manage groups" do
|
|||
wait_for_ajaximations
|
||||
|
||||
# verify the group set tab is created
|
||||
fj("#group_categories_tabs li[role='tab']:first").text.should == 'Group Set 1'
|
||||
fj("#group_categories_tabs li[role='tab']:nth-child(2)").text.should == 'Group Set 1'
|
||||
# verify has the two created but unassigned students
|
||||
ff("div[data-view='unassignedUsers'] .group-user-name").length.should == 2
|
||||
|
||||
|
|
|
@ -87,6 +87,10 @@ describe "people" do
|
|||
wait_for_ajaximations
|
||||
end
|
||||
|
||||
it "should have tabs" do
|
||||
fj('.collectionViewItems>li:first').text.should match "Everyone"
|
||||
end
|
||||
|
||||
it "should validate the main page" do
|
||||
users = ff('.roster_user_name')
|
||||
users[1].text.should match @student_1.name
|
||||
|
|
Loading…
Reference in New Issue