paginate course people page w/ ajax. fixes #9678

return students and teachers/TAs 50 users at a time, and
display them in an infinitely scrolling div. also update
the styling of the scrolling divs to better reflect that they
are scrollable to users w/o permanent scrollbars (e.g. OS X).

test plan:
  * create a course with over 50 student or teacher/ta
    enrollments;
  * view the course people page and verify that student
    and teacher enrollments load as expected;
  * scroll the field with the most enrollments and verify
    that when the bottom is reached more enrollments load;
  * verify that when all enrollments have loaded, the div
    no longer attempts to load new enrollments;
  * create a new course section and add enrollments to it;
  * as a user with permissions limited to section, verify
    that only enrollments in the allowed section are
    displayed.

Change-Id: I2e6485a2edf950acf58f5ccbc75c2965297aed04
Reviewed-on: https://gerrit.instructure.com/12680
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Zach Pendleton <zachp@instructure.com>
This commit is contained in:
Zach Pendleton 2012-07-31 13:31:39 -06:00
parent b5cce9d6bb
commit 35b3fd355a
22 changed files with 636 additions and 251 deletions

View File

@ -0,0 +1,72 @@
#
# 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/>.
#
require [
'jquery'
'underscore'
'compiled/collections/EnrollmentCollection'
'compiled/collections/SectionCollection'
'compiled/views/courses/RosterView'
], ($, _, EnrollmentCollection, SectionCollection, RosterView) ->
rosterPage =
init: ->
@loadEnvironment()
@cacheElements()
@createCollections()
# Get the course ID and create the enrollments API url.
#
# @api public
# @return nothing
loadEnvironment: ->
@course = ENV.context_asset_string.split('_')[1]
@url = "/api/v1/courses/#{@course}/enrollments"
# Store DOM elements used.
#
# @api public
# @return nothing
cacheElements: ->
@$studentList = $('.student_roster .user_list')
@$teacherList = $('.teacher_roster .user_list')
# Create the view and collection objects needed for the page.
#
# @api public
# @return nothing
createCollections: ->
@sections = new SectionCollection(ENV.SECTIONS)
students = new EnrollmentCollection
teachers = new EnrollmentCollection
_.each [students, teachers], (c) =>
c.url = @url
c.sections = @sections
@studentView = new RosterView
el: @$studentList
collection: students
requestOptions: type: ['StudentEnrollment']
@teacherView = new RosterView
el: @$teacherList
collection: teachers
requestOptions: type: ['TeacherEnrollment', 'TaEnrollment']
# Start loading the page.
rosterPage.init()

View File

@ -0,0 +1,70 @@
#
# 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 [
'underscore'
'compiled/collections/PaginatedCollection'
'compiled/models/Enrollment'
], (_, PaginatedCollection, Enrollment) ->
# A collection for managing responses from EnrollmentsApiController.
# Extends PaginatedCollection to allow for paging of returned results.
class EnrollmentCollection extends PaginatedCollection
model: Enrollment
# Format returned responses by flattening the enrollment/user objects
# returned and adding a section name if given sections.
#
# @param response {Object} - A parsed JSON object from the server.
#
# @api private
# @return a formatted JSON response
parse: (response) ->
_.map(response, @flattenEnrollment)
super
# Add the returned user elements to the parent enrollment object to
# make templating easier (e.g. remove all {{#with}} calls in Handlebars.
#
# @param enrollment {Object} - An enrollment object w/ a user sub-object.
#
# @api private
# @return a formatted enrollment JSON object
flattenEnrollment: (enrollment) =>
id = enrollment.user.id
delete enrollment.user.id
enrollment[key] = value for key, value of enrollment.user
enrollment.user.id = id
@storeSection(enrollment) if @sections?
enrollment
# If the collection has been assigned a SectionCollection as @sections,
# use the course_section_id to find the section name and add it as
# course_section_name to the enrollment.
#
# NOTE: This function side-effects the passed enrollment to add the
# given column. It doesn't return anything.
#
# @param enrollment {Object} - An enrollment object.
#
# @api private
# @return nothing
storeSection: (enrollment) ->
section = @sections.find((section) -> section.get('id') == enrollment.course_section_id)
enrollment.course_section_name = section.get('name')

View File

@ -46,7 +46,6 @@ define [
# useful for dispaying 'nothingToShow' messages
@atLeastOnePageFetched = true
parse: (response, xhr) ->
@_parsePageLinks(xhr)
super

View File

@ -16,19 +16,13 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
define [
'Backbone'
'compiled/models/Section'
], ({Collection}, Section) ->
describe ContextController do
it "should show multiple enrollment sections on the users page" do
course_with_teacher_logged_in(:active_all => true)
student_in_course(:active_all => true, :name => "Test User")
add_section("Other Section")
multiple_student_enrollment(@student, @course_section)
# A collection for working with course sections returned from
# CoursesController#sections.
class SectionCollection extends Collection
get "/courses/#{@course.id}/users"
response.should be_success
student_div = Nokogiri::HTML(response.body).at_css("#user_#{@student.id}")
student_div.text.should match @course.default_section.name
student_div.text.should match /Other Section/
end
end
model: Section

View File

@ -1,9 +1,26 @@
#
# 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 [
'Backbone'
'compiled/collections/PaginatedCollection'
'compiled/models/User'
], (Backbone, PaginatedCollection, User) ->
], (PaginatedCollection, User) ->
class UserCollection extends PaginatedCollection
model: User
model: User

View File

@ -0,0 +1,21 @@
#
# 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 ['Backbone'], ({Model}) ->
class Enrollment extends Model

View File

@ -0,0 +1,21 @@
#
# 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 ['Backbone'], ({Model}) ->
class Section extends Model

View File

@ -43,7 +43,7 @@ define [
containerScrollHeight - $container.scrollTop() - $container.height()
startPaginationListener: ->
$(@paginationScrollContainer).on "scroll.pagination#{@cid} resize.pagination#{@cid}", $.proxy @fetchNextPageIfNeeded, this
$(@paginationScrollContainer).on "scroll.pagination#{@cid}, resize.pagination#{@cid}", $.proxy @fetchNextPageIfNeeded, this
@fetchNextPageIfNeeded()
stopPaginationListener: ->

View File

@ -0,0 +1,112 @@
#
# 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) ->
# This view displays a paginated collection of users inside of a course.
#
# @examples
#
# view = RosterView.new
# el: $('...')
# collection: EnrollmentCollection.new(...')
#
# view.collection.on('reset', view.render)
class RosterView extends PaginatedView
# Default options to be passed to the server on each request for new
# collection records.
fetchOptions:
include: ['avatar_url']
per_page: 50
# Create and configure a new RosterView.
#
# @param el {jQuery} - The parent element (should have overflow: hidden and
# a height for infinite scroll).
# @param collection {EnrollmentCollection} - The collection to retrieve
# results from.
# @param options {Object} - Configuration options.
# - requestOptions: options to be passed w/ every server call.
#
# @examples
#
# view = new RosterView
# el: $(...)
# collection: new EnrollmentCollection
# url: ...
# sections: ENV.SECTIONS
# requestOptions:
# type: ['StudentEnrollment']
# include: ['avatar_url']
# per_page: 25
#
# @api public
# @return a RosterView.
initialize: (options) ->
@fetchOptions =
data: _.extend({}, @fetchOptions, options.requestOptions)
add: false
@collection.on('reset', @render, this)
@paginationScrollContainer = @$el
@$el.disableWhileLoading(@collection.fetch(@fetchOptions))
super(fetchOptions: @fetchOptions)
# Append newly fetched records to the roster list.
#
# @api private
# @return nothing.
render: ->
users = @combinedSectionEnrollments(@collection)
enrollments = _.map(users, @renderUser)
@$el.append(enrollments.join(''))
super
# Create the HTML for a given user record.
#
# @param enrollment - An enrollment model.
#
# @api private
# @return nothing.
renderUser: (enrollment) ->
rosterUser(enrollment.toJSON())
# Take users in multiple sections and combine their section names
# into an array to be displayed in a list.
#
# @param collection {EnrollmentCollection} - Enrollments to format.
#
# @api private
# @return an array of user models.
combinedSectionEnrollments: (collection) ->
users = collection.groupBy (enrollment) -> enrollment.get('user_id')
enrollments = _.reduce users, (list, enrollments, key) ->
enrollment = enrollments[0]
names = _.map(enrollments, (e) -> e.get('course_section_name'))
# do it this way instead of calling .set(...) so that we don't fire an
# extra page load from PaginatedView.
enrollment.attributes.course_section_name = _.uniq(names)
list.push(enrollment)
list
, []
enrollments

View File

@ -286,36 +286,27 @@ class ContextController < ApplicationController
format.json { render :json => {:marked_as_read => true}.to_json }
end
end
def roster
if authorized_action(@context, @current_user, [:read_roster, :manage_students, :manage_admin_users])
log_asset_access("roster:#{@context.asset_string}", "roster", "other")
if @context.is_a?(Course)
@enrollments_hash = Hash.new{ |hash,key| hash[key] = [] }
@context.enrollments.sort_by{|e| [e.state_sortable, e.rank_sortable] }.each{ |e| @enrollments_hash[e.user_id] << e }
@students = @context.
students_visible_to(@current_user).
scoped(:conditions => "enrollments.type != 'StudentViewEnrollment'").
order_by_sortable_name.uniq
@teachers = @context.instructors.order_by_sortable_name.uniq
user_ids = @students.map(&:id) + @teachers.map(&:id)
if @context.visibility_limited_to_course_sections?(@current_user)
user_ids = @students.map(&:id) + [@current_user.id]
end
@primary_users = {t('roster.students', 'Students') => @students}
@secondary_users = {t('roster.teachers', 'Teachers & TAs') => @teachers}
elsif @context.is_a?(Group)
@users = @context.participating_users.order_by_sortable_name.uniq
@primary_users = {t('roster.group_members', 'Group Members') => @users}
if @context.context && @context.context.is_a?(Course)
@secondary_users = {t('roster.teachers', 'Teachers & TAs') => @context.context.instructors.order_by_sortable_name.uniq}
end
return unless authorized_action(@context, @current_user, [:read_roster, :manage_students, :manage_admin_users])
log_asset_access("roster:#{@context.asset_string}", 'roster', 'other')
if @context.is_a?(Course)
sections = @context.course_sections(:select => 'id, name')
js_env :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)
@secondary_users = { t('roster.teachers', 'Teachers & TAs') => course.instructors.order_by_sortable_name.uniq }
end
@secondary_users ||= {}
@groups = @context.groups.active rescue []
end
@secondary_users ||= {}
@groups = @context.groups.active rescue []
end
def prior_users
if authorized_action(@context, @current_user, [:manage_students, :manage_admin_users, :read_prior_roster])
@prior_memberships = @context.enrollments.not_fake.scoped(:conditions => {:workflow_state => 'completed'}, :include => :user).to_a.once_per(&:user_id).sort_by{|e| [e.rank_sortable(true), e.user.sortable_name.downcase] }

View File

@ -426,6 +426,19 @@ module ApplicationHelper
@domain_root_account.manually_created_courses_account.grants_rights?(user, session, :create_courses, :manage_courses).values.any?
end
# Public: Create HTML for a sidebar button w/ icon.
#
# url - The url the button should link to.
# img - The path to an image (e.g. 'icon.png')
# label - The text to display on the button (should already be internationalized).
#
# Returns an HTML string.
def sidebar_button(url, label, img = nil)
link_to(url, :class => 'button button-sidebar-wide') do
img ? image_tag(img) + label : label
end
end
def hash_get(hash, key, default=nil)
if hash
if hash[key.to_s] != nil

View File

@ -16,7 +16,8 @@
.users-wrapper
border: 1px solid #CCC
margin-bottom: 1.4em
h3
h3, .h3
font-size: 18px
margin: 0
padding: 8px
background-color: #F7F7F7
@ -439,3 +440,4 @@ h3 .tally
#right-side table.summary
margin-top: 20px

View File

@ -0,0 +1,39 @@
@import 'environment';
.roster {
min-width: 515px;
overflow: hidden;
.users-wrapper {
float: left;
margin-right: 2%;
width: 47%;
}
.avatar {
float: left;
padding-right: 5px;
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

@ -0,0 +1,33 @@
<div class="rs-margin-lr rs-margin-top">
<% if @context.is_a?(Course) %>
<% if can_do(@context, @current_user, :read_roster) %>
<% sidebar_button context_url(@context, :context_groups_url),
t('links.view_student_groups', 'View Student Groups'),
'group.png' %>
<% end %>
<% if can_do(@context, @current_user, :manage_students && @context.enable_user_notes) %>
<% sidebar_button course_user_notes_path(@context),
t('links.view_faculty_journals', 'View Faculty Journals') %>
<% end %>
<% if can_do(@context, @current_user, :manage_admin_users, :manage_students, :read_prior_roster) %>
<% sidebar_button course_prior_users_path(@context),
t('links.view_prior_enrollments', 'View Prior Enrollments'),
'history.png' %>
<% end %>
<% end %>
<% if can_do(@context, @current_user, :read_roster) %>
<% sidebar_button context_url(@context, :context_user_services_url),
t('links.view_services', 'View Registered Services'),
'link.png' %>
<% end %>
<% if @context.is_a?(Course) && can_do(@context, @current_user, :manage_students) %>
<% sidebar_button "#{context_url(@context, :context_details_url)}#tab-users",
t('links.manage_users', 'Manage Users'),
'group.png' %>
<% end %>
</div>

View File

@ -1,117 +1,116 @@
<%
add_crumb(t('#crumbs.people', "People"), context_url(@context, :context_users_url))
@active_tab="people"
content_for :page_title, case @context
when Course
join_title t('titles.course_roster', "Course Roster"), @context.name
when Group
join_title t('titles.group_roster', "Group Roster"), @context.name
end
<%
add_crumb t('#crumbs.people', 'People'), context_url(@context, :context_users_url)
@active_tab = 'people'
translated_title = @context.is_a?(Course) ? t('titles.course_roster', 'Course Roster') : t('titles.group_roster', 'Group Roster')
join_title(translated_title, @context.name)
%>
<% content_for :right_side do %>
<div class="rs-margin-lr rs-margin-top">
<% if @context.is_a?(Course) %>
<% if can_do(@context, @current_user, :read_roster) %>
<a href="<%= context_url(@context, :context_groups_url) %>" class="button button-sidebar-wide"><%= image_tag "group.png" %> <%= t('links.view_student_groups', %{View Student Groups}) %></a>
<% end %>
<% if can_do(@context, @current_user, :manage_students) && @context.enable_user_notes %>
<a href="<%= course_user_notes_path(@context) %>" class="button button-sidebar-wide"><%= t('links.view_faculty_journals', %{View Faculty Journals}) %></a>
<% end %>
<% if can_do(@context, @current_user, :manage_admin_users, :manage_students, :read_prior_roster) %>
<a href="<%= course_prior_users_path(@context) %>" class="button button-sidebar-wide"><%= image_tag "history.png" %> <%= t('links.view_prior_enrollments', %{View Prior Enrollments}) %></a>
<% end %>
<% end %>
<% if can_do(@context, @current_user, :read_roster) %>
<a href="<%= context_url(@context, :context_user_services_url) %>" class="button button-sidebar-wide"><%= image_tag "link.png" %> <%= t('links.view_services', %{View Registered Services}) %></a>
<% end %>
<% if @context.is_a?(Course) && can_do(@context, @current_user, :manage_students) %>
<a href="<%= context_url(@context, :context_details_url) %>#tab-users" class="button button-sidebar-wide"><%= image_tag "group.png" %> <%= t('links.manage_users', %{Manage Users}) %></a>
<% end %>
<% content_for :right_side, render(:partial => 'context/roster_right_side') %>
<% if @context.is_a?(Course) %>
<div class="roster">
<div class="users-wrapper student_roster">
<h2 class="h3"><%= t('roster.students', 'Students') %></h2>
<ul class="user_list student_list">
<!-- This list is populated w/ JavaScript -->
</ul>
</div>
<div class="users-wrapper teacher_roster">
<h2 class="h3"><%= t('roster.teachers', 'Teachers & TAs') %></h2>
<ul class="user_list teachers_list">
<!-- This list is populated w/ JavaScript -->
</ul>
</div>
</div>
<% jammit_css :roster %>
<% js_bundle :roster %>
<% else %>
<% content_for :stylesheets do %>
<style>
.roster .more_info {
font-size: 0.85em;
margin-left: 10px;
}
.roster div.avatar {
float: left;
padding-right: 5px;
}
.fill_height_div { overflow: auto; }
</style>
<% end %>
<table style="width: 100%;">
<tr>
<td style="width: 50%; vertical-align: top;" class="roster student_roster">
<h2><%= @primary_users.keys.first %></h2>
<div style="max-height: 300px; margin-right: 20px;" class="fill_height_div">
<table>
<% @primary_users[@primary_users.keys.first].each do |student| %>
<tr class="user" id="user_<%= student.id %>">
<td style="vertical-align: top;">
<% if service_enabled?(:avatars) %>
<div class="avatar"><%= avatar(student.id, @context.asset_string, 30) %></div>
<% end %>
</td>
<td>
<div><a href="<%= context_url(@context, :context_user_url, student.id) %>" class="user_name"><%= can_do(@context, @current_user, :manage_students) ? student.name : student.short_name %></a></div>
<% if can_do(@context, @current_user, :manage_students) %>
<div class="more_info">
<div class="short_name"><%= student.short_name %></div>
<div class="email"><%= student.email %></div>
<% if @enrollments_hash %>
<% @enrollments_hash[student.id].each do |e| %>
<div class="course_section"><%= e.try(:course_section).try(:display_name) %></div>
<% end %>
<% end %>
</div>
<% end %>
<div style="clear: left;"></div>
</td>
</tr>
<% end %>
</table>
</div>
</td>
<% unless !@secondary_users || @secondary_users.empty? %>
<td style="vertical-align: top;" class="roster teacher_roster">
<h2><%= @secondary_users.keys.first %></h2>
<div style="max-height: 300px;" class="fill_height_div">
<table>
<% @secondary_users[@secondary_users.keys.first].each do |teacher| %>
<tr class="user" id="user_<%= teacher.id %>">
<td style="vertical-align: top;">
<% if service_enabled?(:avatars) %>
<div class="avatar"><%= avatar(teacher.id, @context.asset_string, 30) %></div>
<% end %>
</td>
<td>
<div><a href="<%= context_url(@context, :context_user_url, teacher.id) %>" class="user_name"><%= teacher.name %></a></div>
<% if can_do(@context, @current_user, :manage_admin_users) %>
<div class="more_info">
<div class="short_name"><%= teacher.short_name %></div>
<div class="email"><%= teacher.email %></div>
<% if @enrollments_hash %>
<% @enrollments_hash[teacher.id].each do |e| %>
<div class="course_section"><%= e.try(:course_section).try(:display_name) %></div>
<% end %>
<% end %>
</div>
<% end %>
<div style="clear: left;"></div>
</td>
</tr>
<% end %>
</table>
</div>
</td>
<% end %>
</tr>
</table>
<% end %>
<% content_for :stylesheets do %>
<style>
.roster .more_info {
margin-left: 10px;
font-size: 0.85em;
}
.roster div.avatar {
float: left;
padding-right: 5px;
}
.fill_height_div {
overflow: auto;
}
</style>
<% end %>
<table style="width: 100%;">
<tr>
<td style="width: 50%; vertical-align: top;" class="roster student_roster">
<h2><%= @primary_users.keys.first %></h2>
<div style="max-height: 300px; margin-right: 20px;" class="fill_height_div">
<table>
<% @primary_users[@primary_users.keys.first].each do |student| %>
<tr class="user" id="user_<%= student.id %>">
<td style="vertical-align: top;">
<% if service_enabled?(:avatars) %>
<div class="avatar"><%= avatar(student.id, @context.asset_string, 30) %></div>
<% end %>
</td>
<td>
<div><a href="<%= context_url(@context, :context_user_url, student.id) %>" class="user_name"><%= can_do(@context, @current_user, :manage_students) ? student.name : student.short_name %></a></div>
<% if can_do(@context, @current_user, :manage_students) %>
<div class="more_info">
<div class="short_name"><%= student.short_name %></div>
<div class="email"><%= student.email %></div>
<% if @enrollments_hash %>
<% @enrollments_hash[student.id].each do |e| %>
<div class="course_section"><%= e.try(:course_section).try(:display_name) %></div>
<% end %>
<% end %>
</div>
<% end %>
<div style="clear: left;"></div>
</td>
</tr>
<% end %>
</table>
</div>
</td>
<% unless !@secondary_users || @secondary_users.empty? %>
<td style="vertical-align: top;" class="roster teacher_roster">
<h2><%= @secondary_users.keys.first %></h2>
<div style="max-height: 300px;" class="fill_height_div">
<table>
<% @secondary_users[@secondary_users.keys.first].each do |teacher| %>
<tr class="user" id="user_<%= teacher.id %>">
<td style="vertical-align: top;">
<% if service_enabled?(:avatars) %>
<div class="avatar"><%= avatar(teacher.id, @context.asset_string, 30) %></div>
<% end %>
</td>
<td>
<div><a href="<%= context_url(@context, :context_user_url, teacher.id) %>" class="user_name"><%= teacher.name %></a></div>
<% if can_do(@context, @current_user, :manage_admin_users) %>
<div class="more_info">
<div class="short_name"><%= teacher.short_name %></div>
<div class="email"><%= teacher.email %></div>
<% if @enrollments_hash %>
<% @enrollments_hash[teacher.id].each do |e| %>
<div class="course_section"><%= e.try(:course_section).try(:display_name) %></div>
<% end %>
<% end %>
</div>
<% end %>
<div style="clear: left;"></div>
</td>
</tr>
<% end %>
</table>
</div>
</td>
<% end %>
</tr>
</table>

View File

@ -0,0 +1,20 @@
<li class="user" id="user_{{id}}">
<a class="avatar" href="/users/{{id}}">
<img src="{{avatar_url}}" alt="" />
</a>
<div class="user-details">
<a class="user_name" href="/courses/{{course_id}}/users/{{user_id}}">{{name}}</a>
<div class="more_info">
<div class="short_name">{{short_name}}</div>
<div class="email">{{login_id}}</div>
<ul class="sections">
{{#each course_section_name}}
<li class="section">{{this}}</li>
{{/each}}
</ul>
</div>
</div>
<div style="clear: left;"></div>
</li>

View File

@ -213,6 +213,9 @@ stylesheets:
- public/stylesheets/compiled/grading_standards.css
login:
- public/stylesheets/compiled/login.css
roster:
- public/stylesheets/compiled/course_settings.css
- public/stylesheets/compiled/roster.css
roster_user:
- public/stylesheets/compiled/roster_user.css
learning_outcomes:

View File

@ -25,46 +25,15 @@ describe ContextController do
get 'roster', :course_id => @course.id
assert_unauthorized
end
it "should assign variables" do
course_with_student_logged_in(:active_all => true)
get 'roster', :course_id => @course.id
assigns[:students].should_not be_nil
assigns[:teachers].should_not be_nil
end
it "should retrieve students and teachers" do
course_with_student_logged_in(:active_all => true)
@student = @user
@teacher = user(:active_all => true)
@teacher = @course.enroll_teacher(@teacher)
@teacher.accept!
@teacher = @teacher.user
get 'roster', :course_id => @course.id
assigns[:students].should_not be_nil
assigns[:students].should_not be_empty
assigns[:students].should be_include(@student) #[0].should eql(@user)
assigns[:teachers].should_not be_nil
assigns[:teachers].should_not be_empty
assigns[:teachers].should be_include(@teacher) #[0].should eql(@teacher)
end
it "should not include designers as teachers" do
course_with_student_logged_in(:active_all => true)
@designer = user(:active_all => true)
@course.enroll_designer(@designer).accept!
get 'roster', :course_id => @course.id
assigns[:teachers].should_not be_include(@designer)
end
end
describe "GET 'roster_user'" do
it "should require authorization" do
course_with_teacher(:active_all => true)
get 'roster_user', :course_id => @course.id, :id => @user.id
assert_unauthorized
end
it "should assign variables" do
course_with_student_logged_in(:active_all => true)
@enrollment = @course.enroll_student(user(:active_all => true))
@ -86,14 +55,14 @@ describe ContextController do
get 'chat', :course_id => @course.id, :id => @user.id
response.should be_redirect
end
it "should require authorization" do
PluginSetting.create!(:name => "tinychat", :settings => {})
course_with_teacher(:active_all => true)
get 'chat', :course_id => @course.id, :id => @user.id
assert_unauthorized
end
it "should redirect 'disabled', if disabled by the teacher" do
PluginSetting.create!(:name => "tinychat", :settings => {})
course_with_student_logged_in(:active_all => true)

View File

@ -1,57 +0,0 @@
#
# Copyright (C) 2011 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/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../apis/api_spec_helper')
describe ContextController, :type => :integration do
it "should not include user avatars if avatars are not enabled" do
course_with_student_logged_in(:active_all => true)
get "/courses/#{@course.id}/users"
response.should be_success
page = Nokogiri::HTML(response.body)
page.css(".roster .user").length.should == 2
page.css(".roster .user .avatar").length.should == 0
end
it "should include user avatars if avatars are enabled" do
course_with_student_logged_in(:active_all => true)
@account = Account.default
@account.enable_service(:avatars)
@account.save!
@account.service_enabled?(:avatars).should be_true
get "/courses/#{@course.id}/users"
response.should be_success
page = Nokogiri::HTML(response.body)
page.css(".roster .user").length.should == 2
page.css(".roster .user div.avatar").length.should == 2
page.css(".roster .user div.avatar img")[0]['src'].should match(/\/images\/users\/#{@user.id}/)
page.css(".roster .user div.avatar img")[1]['src'].should match(/\/images\/users\/#{@teacher.id}/)
@group = @course.groups.create!(:name => "sub-group")
@group.add_user(@user)
get "/groups/#{@group.id}/users"
response.should be_success
page = Nokogiri::HTML(response.body)
page.css(".roster .user").length.should == 2
page.css(".roster .user div.avatar").length.should == 2
page.css(".roster .user div.avatar img")[0]['src'].should match(/\/images\/users\/#{@user.id}/)
page.css(".roster .user div.avatar img")[1]['src'].should match(/\/images\/users\/#{@teacher.id}/)
end
end

View File

@ -112,6 +112,72 @@ describe "courses" do
driver.current_url.should match %r{/courses/#{course2.id}/grades}
end
it "should load the users page using ajax" do
course_with_teacher_logged_in
# Setup the course with > 50 users (to test scrolling)
100.times do |n|
@course.enroll_student(user).accept!
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
ff('.student_roster .user').length.should == 50
ff('.teacher_roster .user').length.should == 1
# Test the infinite scroll.
driver.execute_script <<-END
var $wrapper = $('.student_roster .fill_height_div'),
$list = $('.student_list'),
scroll = $list.height() - $wrapper.height();
$wrapper.scrollTo(scroll);
END
wait_for_ajaximations
ff('.student_roster li').length.should == 100
end
it "should only show users that a user has permissions to view" do
# Set up the test
course(:active_course => true)
%w[One Two].each do |name|
section = @course.course_sections.create!(:name => name)
@course.enroll_student(user, :section => section).accept!
end
user_logged_in
enrollment = @course.enroll_ta(@user)
enrollment.accept!
enrollment.update_attributes(:limit_privileges_to_course_section => true,
:course_section => CourseSection.find_by_name('Two'))
# 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
end
it "should display users' section name" do
course_with_teacher_logged_in(:active_all => true)
user1, user2 = [user, user]
section1 = @course.course_sections.create!(:name => 'One')
section2 = @course.course_sections.create!(:name => 'Two')
@course.enroll_student(user1, :section => section1).accept!
[section1, section2].each do |section|
e = user2.student_enrollments.build
e.workflow_state = 'active'
e.course = @course
e.course_section = section
e.save!
end
get "/courses/#{@course.id}/users"
wait_for_ajaximations
sections = ff('.student_roster .section')
sections.map(&:text).sort.should == %w{One One Two}
end
end
context "course as a student" do

View File

@ -69,7 +69,7 @@ shared_examples_for "discussions selenium tests" do
end
def validate_entry_text(discussion_entry, text)
li_selector = %([data-id$="#{discussion_entry.id}"])
li_selector = %(.discussion-entries [data-id$="#{discussion_entry.id}"])
keep_trying_until do
fj(li_selector).should include_text(text)
end

View File

@ -87,6 +87,7 @@ describe "people" do
@test_observer = create_user('observer@test.com')
get "/courses/#{@course.id}/users"
wait_for_ajaximations
end
it "should validate the main page" do