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:
Ryan Florence 2013-03-08 14:24:22 -07:00
parent a3cc4a5631
commit 85b4a76ed1
16 changed files with 151 additions and 301 deletions

View File

@ -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))

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1,13 @@
define [
'Backbone'
'jst/courses/rosterUserView'
], (Backbone, template) ->
class UserView extends Backbone.View
tagName: 'tr'
className: 'rosterUser'
template: template

View File

@ -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')))

View File

@ -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

View File

@ -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; }
}

View File

@ -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>

View File

@ -1,5 +0,0 @@
<div class="users-wrapper">
<h2 class="h3">{{title}}</h2>
<ul class="user_list">
</ul>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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

View File

@ -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

View File

@ -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