first iteration of new course/users page
test plan: 1. visit /courses/:id/people - users should populate the page 2. scroll down - should lazy load next pages 3. enter a search term - should render results 4. scroll down - should lazy load large result sets Change-Id: I20b2aa2ec5dab74053b78340ac9d2920b0521e8b Reviewed-on: https://gerrit.instructure.com/18447 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Joe Tanner <joe@instructure.com> QA-Review: Ryan Florence <ryanf@instructure.com>
This commit is contained in:
parent
a3cc4a5631
commit
85b4a76ed1
|
@ -15,46 +15,42 @@
|
|||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require [
|
||||
'jquery'
|
||||
'underscore'
|
||||
'compiled/collections/UserCollection'
|
||||
'jst/courses/rosterSearch'
|
||||
'jst/courses/rosterUsers'
|
||||
'compiled/collections/RosterUserCollection'
|
||||
'compiled/collections/SectionCollection'
|
||||
'compiled/views/courses/RosterView'
|
||||
'jst/courses/Roster'
|
||||
], ($, _, UserCollection, SectionCollection, RosterView, roster) ->
|
||||
'compiled/views/InputFilterView'
|
||||
'compiled/views/PaginatedCollectionView'
|
||||
'compiled/views/courses/RosterUserView'
|
||||
'compiled/views/SearchView'
|
||||
'jquery'
|
||||
], (rosterSearchTemplate, rosterUsersTemplate, UserCollection, SectionCollection, InputFilterView, PaginatedCollectionView, RosterUserView, SearchView, $) ->
|
||||
|
||||
# Load environment
|
||||
course = ENV.context_asset_string.split('_')[1]
|
||||
url = "/api/v1/courses/#{course}/users"
|
||||
fetchOptions =
|
||||
include: ['avatar_url', 'enrollments', 'email']
|
||||
per_page: 50
|
||||
users = new UserCollection null,
|
||||
course_id: ENV.context_asset_string.split('_')[1]
|
||||
sections: new SectionCollection ENV.SECTIONS
|
||||
params: fetchOptions
|
||||
inputView = new InputFilterView
|
||||
usersView = new PaginatedCollectionView
|
||||
collection: users
|
||||
itemView: RosterUserView
|
||||
buffer: 1000
|
||||
template: rosterUsersTemplate
|
||||
searchView = new SearchView
|
||||
collectionView: usersView
|
||||
inputFilterView: inputView
|
||||
template: rosterSearchTemplate
|
||||
|
||||
sections = new SectionCollection(ENV.SECTIONS)
|
||||
columns =
|
||||
students: $('.roster .student_roster')
|
||||
teachers: $('.roster .teacher_roster')
|
||||
users.on 'beforeFetch', =>
|
||||
inputView.$el.addClass 'loading'
|
||||
users.on 'fetch', =>
|
||||
inputView.$el.removeClass 'loading'
|
||||
|
||||
for roster_data in ENV.COURSE_ROSTERS
|
||||
users = new UserCollection
|
||||
users.url = url
|
||||
users.sections = sections
|
||||
users.roles = roster_data['roles']
|
||||
searchView.render()
|
||||
searchView.$el.appendTo $('#content')
|
||||
users.fetch()
|
||||
|
||||
usersOptions = add: false, data: _.extend({}, fetchOptions, enrollment_role: roster_data['roles'])
|
||||
|
||||
column = columns[roster_data['column']]
|
||||
html = roster
|
||||
title: roster_data['title']
|
||||
column.append(html)
|
||||
list = column.find('.user_list').last()
|
||||
|
||||
usersView = new RosterView
|
||||
collection: users
|
||||
el: list
|
||||
fetchOptions: usersOptions
|
||||
|
||||
users.on('reset', usersView.render, usersView)
|
||||
usersView.$el.disableWhileLoading(users.fetch(usersOptions))
|
|
@ -0,0 +1,22 @@
|
|||
define [
|
||||
'compiled/collections/PaginatedCollection'
|
||||
'compiled/models/RosterUser'
|
||||
], (PaginatedCollection, RosterUser) ->
|
||||
|
||||
class RosterUserCollection extends PaginatedCollection
|
||||
|
||||
model: RosterUser
|
||||
|
||||
##
|
||||
# The course id the users belong to
|
||||
|
||||
@optionProperty 'course_id'
|
||||
|
||||
##
|
||||
# A SectionCollection
|
||||
|
||||
@optionProperty 'sections'
|
||||
|
||||
url: ->
|
||||
"/api/v1/courses/#{@options.course_id}/users"
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
define ['compiled/models/User'], (User) ->
|
||||
|
||||
class RosterUser extends User
|
||||
|
||||
computedAttributes: [
|
||||
'sections'
|
||||
{name: 'html_url', deps: ['enrollments']}
|
||||
]
|
||||
|
||||
html_url: ->
|
||||
@get('enrollments')[0].html_url
|
||||
|
||||
sections: ->
|
||||
return [] unless @collection?.sections?
|
||||
{sections} = @collection
|
||||
for {course_section_id} in @get('enrollments')
|
||||
sections.get(course_section_id).attributes
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'jst/courses/rosterUserView'
|
||||
], (Backbone, template) ->
|
||||
|
||||
class UserView extends Backbone.View
|
||||
|
||||
tagName: 'tr'
|
||||
|
||||
className: 'rosterUser'
|
||||
|
||||
template: template
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2012 Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
define [
|
||||
'jquery'
|
||||
'underscore'
|
||||
'compiled/views/PaginatedView'
|
||||
'jst/courses/RosterUser'
|
||||
], ($, _, PaginatedView, rosterUser) ->
|
||||
|
||||
# RosterView: Display a paginated collection of users inside of a course.
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
# view = new RosterView el: $('..'), collection: new UserCollection(...)
|
||||
# view.collection.on('reset', view.render, view)
|
||||
# view.collection.fetch(...)
|
||||
class RosterView extends PaginatedView
|
||||
# Public: Create a new instance.
|
||||
#
|
||||
# fetchOptions - Options to be passed to @collection.fetch(). Needs to be
|
||||
# passed for subsequent page gets (see PaginatedView).
|
||||
initialize: ({fetchOptions}) ->
|
||||
@paginationScrollContainer = @$el
|
||||
super(fetchOptions: fetchOptions)
|
||||
|
||||
# Public: Append new records to the roster list.
|
||||
#
|
||||
# Returns nothing.
|
||||
render: ->
|
||||
@combineSectionNames(@collection)
|
||||
@appendCourseId(@collection)
|
||||
html = _.map(@collection.models, @renderUser)
|
||||
@$el.append(html.join(''))
|
||||
super
|
||||
|
||||
# Public: Return HTML for a given record.
|
||||
#
|
||||
# user - The user object to render as HTML.
|
||||
#
|
||||
# Returns an HTML string.
|
||||
renderUser: (user) ->
|
||||
rosterUser(user.toJSON())
|
||||
|
||||
# Internal: Mutate a user collection, adding a sectionNames property to
|
||||
# each child model.
|
||||
#
|
||||
# collection - The collection to alter.
|
||||
#
|
||||
# Returns nothing.
|
||||
combineSectionNames: (collection) ->
|
||||
collection.each (user) =>
|
||||
user.set('sectionNames', @getSections(user), silent: true)
|
||||
|
||||
# Internal: Mutate a user collection, adding a course_id attribute to
|
||||
# each child model.
|
||||
#
|
||||
# collection - The collection to alter.
|
||||
#
|
||||
# Returns nothing
|
||||
appendCourseId: (collection) ->
|
||||
collection.each (user) ->
|
||||
user.set('course_id', user.get('enrollments')[0].course_id, silent: true)
|
||||
|
||||
# Internal: Get the names of a user's sections.
|
||||
#
|
||||
# user - The user to return section names for.
|
||||
#
|
||||
# Return an array of section names.
|
||||
getSections: (user) ->
|
||||
enrollments = _.filter(user.get('enrollments'), ((enrollment) ->
|
||||
_.contains(@roles, enrollment.role))
|
||||
, @collection)
|
||||
|
||||
sections = _.map enrollments, (enrollment) =>
|
||||
@collection.sections.find (section) -> enrollment.course_section_id == section.id
|
||||
|
||||
_.uniq(_.map(sections, (section) -> section.get('name')))
|
||||
|
|
@ -303,31 +303,14 @@ class ContextController < ApplicationController
|
|||
|
||||
if @context.is_a?(Course)
|
||||
sections = @context.course_sections(:select => 'id, name')
|
||||
js_env :SECTIONS => sections.map { |s| { :id => s.id, :name => s.name } }
|
||||
|
||||
all_roles = Role.custom_roles_and_counts_for_course(@context, @current_user)
|
||||
header_rosters = [
|
||||
{:title => t('roster.students', 'Students'), :roles => ['StudentEnrollment'], :column => 'students'},
|
||||
{:title => t('roster.teachers', 'Teachers'), :roles => ['TeacherEnrollment'], :column => 'teachers'},
|
||||
{:title => t('roster.tas', 'TAs'), :roles => ['TaEnrollment'], :column => 'teachers'}
|
||||
]
|
||||
@display_rosters = []
|
||||
|
||||
header_rosters.each do |hr|
|
||||
base_roles = all_roles.select{|r| hr[:roles].include?(r[:base_role_name])}
|
||||
@display_rosters << hr if base_roles.find{|br| br[:count] && br[:count] > 0}.present?
|
||||
|
||||
base_roles.each do |br|
|
||||
br[:custom_roles].select{|cr| cr[:count] && cr[:count] > 0}.each do |cr|
|
||||
@display_rosters << {:title => cr[:label], :roles => [cr[:name]], :column => hr[:column]}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
js_env({
|
||||
:ALL_ROLES => all_roles,
|
||||
:SECTIONS => sections.map { |s| { :id => s.id, :name => s.name } }
|
||||
})
|
||||
elsif @context.is_a?(Group)
|
||||
@users = @context.participating_users.order_by_sortable_name.uniq
|
||||
@primary_users = { t('roster.group_members', 'Group Members') => @users }
|
||||
|
||||
if course = @context.context.try(:is_a?, Course) && @context.context
|
||||
@secondary_users = { t('roster.teachers_and_tas', 'Teachers & TAs') => course.instructors.order_by_sortable_name.uniq }
|
||||
end
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
@import 'environment';
|
||||
|
||||
.roster {
|
||||
min-width: 515px;
|
||||
overflow: hidden;
|
||||
|
||||
.users-column-wrapper {
|
||||
float: left;
|
||||
margin-right: 2%;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
float: left;
|
||||
padding-right: 5px;
|
||||
|
||||
&:hover { text-decoration: none; }
|
||||
img { height: 64px; width: 64px; }
|
||||
}
|
||||
|
||||
.user-details {
|
||||
float: left;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.more_info {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.sections {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.roster .avatar img { height: 32px; width: 32px; }
|
||||
}
|
||||
|
|
@ -9,21 +9,8 @@
|
|||
<% content_for :right_side, render(:partial => 'context/roster_right_side') %>
|
||||
|
||||
<% if @context.is_a?(Course) %>
|
||||
<div class="roster">
|
||||
<div class="users-column-wrapper student_roster">
|
||||
</div>
|
||||
<div class="users-column-wrapper teacher_roster">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @display_rosters.any? %>
|
||||
<% jammit_css :roster %>
|
||||
<% js_env :COURSE_ROSTERS => @display_rosters %>
|
||||
<% js_bundle :roster %>
|
||||
<% else %>
|
||||
<%= t('roster.empty', 'No one is currently enrolled in this course.') %>
|
||||
<% end %>
|
||||
|
||||
<% jammit_css :roster %>
|
||||
<% js_bundle :roster %>
|
||||
<% else %>
|
||||
<% content_for :stylesheets do %>
|
||||
<style>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<div class="users-wrapper">
|
||||
<h2 class="h3">{{title}}</h2>
|
||||
<ul class="user_list">
|
||||
</ul>
|
||||
</div>
|
|
@ -1,24 +0,0 @@
|
|||
<li class="user" id="user_{{id}}">
|
||||
<a class="avatar" href="/courses/{{course_id}}/users/{{id}}">
|
||||
<span class="hidden-inline-text">{{name}} {{#t "profile"}}Profile{{/t}}</span>
|
||||
<img src="{{avatar_url}}" alt="" />
|
||||
</a>
|
||||
|
||||
<div class="user-details">
|
||||
<a class="user_name" href="/courses/{{course_id}}/users/{{id}}">
|
||||
{{name}}
|
||||
<span class="hidden-inline-text">{{#t "course_profile"}}Course Profile{{/t}}</span>
|
||||
</a>
|
||||
<div class="more_info">
|
||||
<div class="short_name">{{short_name}}</div>
|
||||
<div class="email">{{email}}</div>
|
||||
<ul class="sections">
|
||||
{{#each sectionNames}}
|
||||
<li class="section">{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="clear: left;"></div>
|
||||
</li>
|
|
@ -0,0 +1,11 @@
|
|||
<div class="form-inline">
|
||||
<input
|
||||
type="text"
|
||||
name="inputFilter"
|
||||
class="inputFilterView"
|
||||
placeholder='{{#t "search"}}Search{{/t}}'
|
||||
aria-label='{{#t "search_"}}Search{{/t}}'>
|
||||
</div>
|
||||
|
||||
<div class="v-gutter collectionView"></div>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<td><a href="{{html_url}}" class="roster_user_name">{{name}}</a></td>
|
||||
<td>
|
||||
{{#each sections}}
|
||||
<div class="section">{{name}}</div>
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>
|
||||
{{#each enrollments}}
|
||||
<div>{{enrollmentName type}}</div>
|
||||
{{/each}}
|
||||
</td>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{{#if collection.length}}
|
||||
|
||||
<table class="roster table table-bordered table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Section</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="collectionViewItems"></tbody>
|
||||
</table>
|
||||
<div class="paginatedLoadingIndicator"></div>
|
||||
|
||||
{{else}}
|
||||
|
||||
{{#if collection.atLeastOnePageFetched}}
|
||||
<div class="alert alert-info">
|
||||
<p class="lead">{{#t "no_people_found"}}No people found{{/t}}</p>
|
||||
<p>{{#t "you_can_search_by"}}You can search by:{{/t}}</p>
|
||||
<ul>
|
||||
<li>{{#t "name"}}Name{{/t}}</li>
|
||||
<li>{{#t "sis_id"}}SIS ID{{/t}}</li>
|
||||
<li>{{#t "email"}}Email{{/t}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="paginatedLoadingIndicator"></div>
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
|
|
@ -123,16 +123,6 @@ describe UsersController do
|
|||
response.body.should match /Olds, JT.*St\. Clair, John/m
|
||||
end
|
||||
|
||||
it "should not show student view student in a course context" do
|
||||
course_with_teacher_logged_in(:active_all => true)
|
||||
@fake_student = @course.student_view_student
|
||||
|
||||
get course_users_url @course.id
|
||||
body = Nokogiri::HTML(response.body)
|
||||
body.css("#user_#{@fake_student.id}").should be_empty
|
||||
body.at_css('.student_roster').text.should_not match(/Test Student/)
|
||||
end
|
||||
|
||||
it "should not show any student view students at the account level" do
|
||||
course_with_teacher(:active_all => true)
|
||||
@fake_student = @course.student_view_student
|
||||
|
|
|
@ -132,59 +132,7 @@ describe "courses" do
|
|||
get "/courses/#{@course.id}/users"
|
||||
wait_for_ajaximations
|
||||
ff('.ui-state-error').length.should == 0
|
||||
ff('.student_roster .user').length.should == 50
|
||||
ff('.teacher_roster .user').length.should == 2
|
||||
ff('.teacher_roster .user_list').length.should == 2
|
||||
|
||||
# Test the infinite scroll.
|
||||
driver.execute_script <<-END
|
||||
$list = $('.student_roster .users-wrapper:first-child .user_list'),
|
||||
$list[0].scrollTop = $list[0].scrollHeight - $list.height();
|
||||
END
|
||||
wait_for_ajaximations
|
||||
ff('.student_roster .user').length.should == 60
|
||||
end
|
||||
|
||||
it "should include separate course roles sections on users page" do
|
||||
course_with_teacher_logged_in
|
||||
|
||||
@course.enroll_user(user, 'TaEnrollment')
|
||||
@course.enroll_user(user, 'StudentEnrollment')
|
||||
|
||||
roles = [
|
||||
['Student', 51, '.student_roster .users-wrapper:nth-child(2)'],
|
||||
['Teacher', 52, '.teacher_roster .users-wrapper:nth-child(2)'],
|
||||
['Ta', 53, '.teacher_roster .users-wrapper:nth-child(4)']
|
||||
]
|
||||
roles.each do |type, num, css|
|
||||
role = @course.account.roles.build :name => "Custom#{type}"
|
||||
role.base_role_type = "#{type}Enrollment"
|
||||
role.save!
|
||||
|
||||
num.times do |n|
|
||||
@course.enroll_user(user, "#{type}Enrollment", :role_name => role.name)
|
||||
end
|
||||
end
|
||||
|
||||
# Test that the page loads properly the first time.
|
||||
get "/courses/#{@course.id}/users"
|
||||
wait_for_ajaximations
|
||||
ff('.ui-state-error').length.should == 0
|
||||
|
||||
roles.each do |type, num, css|
|
||||
ff("#{css} h2").first.text.should == "Custom#{type}"
|
||||
ff("#{css} .user").length.should == 50
|
||||
|
||||
# Test the infinite scroll.
|
||||
|
||||
driver.execute_script <<-END
|
||||
$list = $('#{css} .user_list'),
|
||||
$list[0].scrollTop = $list[0].scrollHeight - $list.height();
|
||||
END
|
||||
wait_for_ajaximations
|
||||
|
||||
ff("#{css} .user").length.should == num
|
||||
end
|
||||
ff('.roster .rosterUser').length.should == 50
|
||||
end
|
||||
|
||||
it "should only show users that a user has permissions to view" do
|
||||
|
@ -203,7 +151,7 @@ describe "courses" do
|
|||
# Test that only users in the approved section are displayed.
|
||||
get "/courses/#{@course.id}/users"
|
||||
wait_for_ajaximations
|
||||
ff('.student_roster .user').length.should == 1
|
||||
ff('.roster .rosterUser').length.should == 2
|
||||
end
|
||||
|
||||
it "should display users section name" do
|
||||
|
@ -222,8 +170,8 @@ describe "courses" do
|
|||
|
||||
get "/courses/#{@course.id}/users"
|
||||
wait_for_ajaximations
|
||||
sections = ff('.student_roster .section')
|
||||
sections.map(&:text).sort.should == %w{One One Two}
|
||||
sections = ff('.roster .section')
|
||||
sections.map(&:text).sort.should == ["One", "One", "Two", "Unnamed Course", "Unnamed Course"]
|
||||
end
|
||||
|
||||
it "should display users section name properly when separated by custom roles" do
|
||||
|
|
|
@ -86,9 +86,9 @@ describe "people" do
|
|||
end
|
||||
|
||||
it "should validate the main page" do
|
||||
users = ff('.user_name')
|
||||
users[0].text.should match @teacher.name
|
||||
users = ff('.roster_user_name')
|
||||
users[1].text.should match @student_1.name
|
||||
users[0].text.should match @teacher.name
|
||||
end
|
||||
|
||||
it "should navigate to registered services on profile page" do
|
||||
|
|
Loading…
Reference in New Issue