StudentView.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*
  2. * Copyright (C) 2014 - present Instructure, Inc.
  3. *
  4. * This file is part of Canvas.
  5. *
  6. * Canvas is free software: you can redistribute it and/or modify it under
  7. * the terms of the GNU Affero General Public License as published by the Free
  8. * Software Foundation, version 3 of the License.
  9. *
  10. * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
  11. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. * details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License along
  16. * with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. import React from 'react'
  19. import ReactDOM from 'react-dom'
  20. import $ from 'jquery'
  21. import I18n from 'i18n!student_groups'
  22. import natcompare from 'compiled/util/natcompare'
  23. import Group from 'compiled/models/Group'
  24. import UserCollection from 'compiled/collections/UserCollection'
  25. import ContextGroupCollection from 'compiled/collections/ContextGroupCollection'
  26. import BackboneState from 'jsx/groups/mixins/BackboneState'
  27. import PaginatedGroupList from 'jsx/groups/components/PaginatedGroupList'
  28. import Filter from 'jsx/groups/components/Filter'
  29. import NewGroupDialog from 'jsx/groups/components/NewGroupDialog'
  30. import ManageGroupDialog from 'jsx/groups/components/ManageGroupDialog'
  31. const StudentView = React.createClass({
  32. mixins: [BackboneState],
  33. getInitialState () {
  34. return {
  35. filter: '',
  36. userCollection: new UserCollection(null, {
  37. params: { enrollment_type: 'student' },
  38. comparator: natcompare.byGet('sortable_name'),
  39. }),
  40. groupCollection: new ContextGroupCollection([], { course_id: ENV.course_id })
  41. }
  42. },
  43. openManageGroupDialog (group) {
  44. const $dialog = $('<div>').dialog({
  45. id: 'manage_group_form',
  46. title: 'Manage Student Group',
  47. height: 500,
  48. width: 700,
  49. 'fix-dialog-buttons': false,
  50. close: (e) => {
  51. ReactDOM.unmountComponentAtNode($dialog[0])
  52. $(this).remove()
  53. },
  54. })
  55. const closeDialog = (e) => {
  56. e.preventDefault()
  57. $dialog.dialog('close')
  58. }
  59. ReactDOM.render(<ManageGroupDialog userCollection={this.state.userCollection}
  60. checked={group.users.map((u) => u.id)}
  61. groupId={group.id}
  62. name={group.name}
  63. maxMembership={group.max_membership}
  64. updateGroup={this.updateGroup}
  65. closeDialog={closeDialog}
  66. loadMore={() => this._loadMore(this.state.userCollection)} />, $dialog[0])
  67. },
  68. openNewGroupDialog () {
  69. const $dialog = $('<div>').dialog({
  70. id: 'add_group_form',
  71. title: 'New Student Group',
  72. height: 500,
  73. width: 700,
  74. 'fix-dialog-buttons': false,
  75. close: (e) => {
  76. ReactDOM.unmountComponentAtNode($dialog[0])
  77. $(this).remove()
  78. },
  79. })
  80. const closeDialog = (e) => {
  81. e.preventDefault()
  82. $dialog.dialog('close')
  83. }
  84. ReactDOM.render(<NewGroupDialog userCollection={this.state.userCollection}
  85. createGroup={this.createGroup}
  86. closeDialog={closeDialog}
  87. loadMore={() => this._loadMore(this.state.userCollection)} />, $dialog[0])
  88. },
  89. _categoryGroups (group) {
  90. return this.state.groupCollection.filter((g) => g.get('group_category_id') === group.get('group_category_id'))
  91. },
  92. _onCreateGroup (group) {
  93. this.state.groupCollection.add(group)
  94. $.flashMessage(I18n.t('Created Group %{group_name}', {group_name: group.name}))
  95. },
  96. createGroup (name, joinLevel, invitees) {
  97. $.ajaxJSON(`/courses/${ENV.course_id}/groups`,
  98. 'POST',
  99. {group: {name, join_level: joinLevel}, invitees},
  100. (group) => this._onCreateGroup(group))
  101. },
  102. _onUpdateGroup (group) {
  103. this.state.groupCollection.add(group, {merge: true})
  104. $.flashMessage(I18n.t('Updated Group %{group_name}', {group_name: group.name}))
  105. },
  106. updateGroup (groupId, name, members) {
  107. $.ajaxJSON(`/api/v1/groups/${groupId}`,
  108. 'PUT',
  109. {name: name, members: members},
  110. (group) => this._onUpdateGroup(group))
  111. },
  112. _loadMore (collection) {
  113. if (!collection.loadedAll && !collection.fetchingNextPage) {
  114. // if we specify a page before we actually need it, we lose
  115. // the params being passed to the api
  116. const options = collection.length === 0 ? {} : {page: 'next'}
  117. collection.fetch(options)
  118. }
  119. },
  120. _extendAttribute (model, attribute, hash) {
  121. const copy = Object.assign({}, model.get(attribute))
  122. model.set(attribute, Object.assign(copy, hash))
  123. },
  124. _addUser (groupModel, user) {
  125. groupModel.set('users', groupModel.get('users').concat(user))
  126. },
  127. _removeUser (groupModel, userId) {
  128. groupModel.set('users', groupModel.get('users').filter((u) => u.id !== userId))
  129. // If user was a leader, unset the leader attribute.
  130. const leader = groupModel.get('leader')
  131. if (leader && leader.id === userId) {
  132. groupModel.set('leader', null)
  133. }
  134. },
  135. _onLeave (group) {
  136. const groupModel = this.state.groupCollection.get(group.id)
  137. this._removeUser(groupModel, ENV.current_user_id)
  138. if (!groupModel.get('group_category').allows_multiple_memberships) {
  139. this._categoryGroups(groupModel).forEach((g) => {
  140. this._extendAttribute(g, 'group_category', {is_member: false})
  141. })
  142. }
  143. $.flashMessage(I18n.t('Left Group %{group_name}', {group_name: group.name}))
  144. },
  145. leave (group) {
  146. const dfd = $.ajaxJSON(`/api/v1/groups/${group.id}/memberships/self`,
  147. 'DELETE',
  148. {},
  149. () => this._onLeave(group))
  150. $(ReactDOM.findDOMNode(this.refs.panel)).disableWhileLoading(dfd)
  151. },
  152. _onJoin (group) {
  153. const groupModel = this.state.groupCollection.get(group.id)
  154. this._categoryGroups(groupModel).forEach((g) => {
  155. this._extendAttribute(g, 'group_category', {is_member: true})
  156. if (!groupModel.get('group_category').allows_multiple_memberships) {
  157. this._removeUser(g, ENV.current_user_id)
  158. }
  159. })
  160. this._addUser(groupModel, ENV.current_user)
  161. $.flashMessage(I18n.t('Joined Group %{group_name}', {group_name: group.name}))
  162. },
  163. join (group) {
  164. const dfd = $.ajaxJSON(`/api/v1/groups/${group.id}/memberships`,
  165. 'POST',
  166. {user_id: 'self'},
  167. () => this._onJoin(group),
  168. // This is making an assumption that when the current user can't join a group it is likely beacuse a student
  169. // from another section joined that group after the page loaded for the current user
  170. () => this._extendAttribute(this.state.groupCollection.get(group.id), 'permissions', {join: false}))
  171. $(ReactDOM.findDOMNode(this.refs.panel)).disableWhileLoading(dfd)
  172. },
  173. _filter (group) {
  174. const filter = this.state.filter.toLowerCase()
  175. return (!filter ||
  176. group.name.toLowerCase().indexOf(filter) > -1 ||
  177. group.users.some(u => u.name.toLowerCase().indexOf(filter) > -1))
  178. },
  179. manage (group) {
  180. this.openManageGroupDialog(group)
  181. },
  182. render () {
  183. const filteredGroups = this.state.groupCollection.toJSON().filter(this._filter)
  184. let newGroupButton = null
  185. if (ENV.STUDENT_CAN_ORGANIZE_GROUPS_FOR_COURSE) {
  186. newGroupButton = (
  187. <button aria-label={I18n.t('Add new group')} className="btn btn-primary add_group_link" onClick={this.openNewGroupDialog}>
  188. <i className="icon-plus" />
  189. &nbsp;{I18n.t('Group')}
  190. </button>)
  191. }
  192. return (
  193. <div>
  194. <div id="group_categories_tabs" className="ui-tabs-minimal ui-tabs ui-widget ui-widget-content ui-corner-all">
  195. <ul className="collectionViewItems ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all">
  196. <li className="ui-state-default ui-corner-top">
  197. <a href={`/courses/${ENV.course_id}/users`}>{I18n.t('Everyone')}</a>
  198. </li>
  199. <li className="ui-state-default ui-corner-top ui-tabs-active ui-state-active">
  200. <a href="#" tabIndex="-1">{I18n.t('Groups')}</a>
  201. </li>
  202. </ul>
  203. <div className="pull-right group-categories-actions">
  204. {newGroupButton}
  205. </div>
  206. <div className="roster-tab tab-panel" ref="panel">
  207. <Filter onChange={(e) => this.setState({filter: e.target.value})} />
  208. <PaginatedGroupList loading={this.state.groupCollection.fetchingNextPage}
  209. groups={filteredGroups}
  210. filter={this.state.filter}
  211. loadMore={() => this._loadMore(this.state.groupCollection)}
  212. onLeave={this.leave}
  213. onJoin={this.join}
  214. onManage={this.manage}/>
  215. </div>
  216. </div>
  217. </div>)
  218. },
  219. })
  220. export default <StudentView />