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:
parent
b5cce9d6bb
commit
35b3fd355a
|
@ -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()
|
|
@ -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')
|
||||
|
|
@ -46,7 +46,6 @@ define [
|
|||
# useful for dispaying 'nothingToShow' messages
|
||||
@atLeastOnePageFetched = true
|
||||
|
||||
|
||||
parse: (response, xhr) ->
|
||||
@_parsePageLinks(xhr)
|
||||
super
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue