add multiple students to an observer in the UI, closes #7768

other changes:
- makes it possible to add a student to multiple sections in the course
- pagination of the user lists
- converted user tab js to coffee and backbone

test plan:
- as a teacher go to /courses/*/settings#tab-users
- verify that multiple students can be linked/unlinked to an observer
- verify that users can be added to and removed from multiple sections
- verify that 'Resend Invitation' and 'Remove from course' work

Change-Id: I0f64f72f1937348817990b6f13b6310185b68a73
Reviewed-on: https://gerrit.instructure.com/10865
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Joe Tanner <joe@instructure.com>
This commit is contained in:
Joe Tanner 2012-04-16 09:27:28 -06:00
parent e4630f5b9f
commit d16318c1e0
63 changed files with 2182 additions and 1418 deletions

View File

@ -1,7 +1,27 @@
require [
'compiled/views/course_settings/UserCollectionView'
'compiled/collections/UserCollection'
'compiled/views/course_settings/tabs/tabUsers'
'vendor/jquery.cookie'
'user_lists'
'course_settings'
'external_tools'
'grading_standards'
]
], (UserCollectionView, UserCollection) ->
loadUsersTab = ->
window.app = usersTab: {}
for eType in ['student', 'observer', 'teacher', 'designer', 'ta']
# produces app.usersTab.studentsView .observerView etc.
window.app.usersTab["#{eType}sView"] = new UserCollectionView
el: $("##{eType}_enrollments")
url: ENV.USERS_URL
requestParams:
enrollment_type: eType
$ ->
if $("#tab-users").is(":visible")
loadUsersTab()
$("#course_details_tabs").bind 'tabsshow', (e,ui) ->
if ui.tab.hash == '#tab-users' and not window.app?.usersTab
loadUsersTab()

View File

@ -1,2 +1 @@
require ['user_lists']
require ['compiled/user_lists']

View File

@ -0,0 +1,9 @@
define [
'Backbone'
'compiled/collections/PaginatedCollection'
'compiled/models/User'
], (Backbone, PaginatedCollection, User) ->
class UserCollection extends PaginatedCollection
model: User

View File

@ -1,7 +1,8 @@
define [
'i18n!user'
'underscore'
'Backbone'
], (I18n, Backbone) ->
], (I18n, _, Backbone) ->
class User extends Backbone.Model
@ -17,3 +18,9 @@ define [
invalid: I18n.t("errors.invalid_code", "Invalid code")
terms_of_use:
accepted: I18n.t("errors.terms", "You must agree to the terms")
pending: ->
_.any @get('enrollments'), (e) -> e.enrollment_state in ['creation_pending', 'invited']
hasEnrollmentType: (type) ->
_.any @get('enrollments'), (e) -> e.type == type

View File

@ -0,0 +1,187 @@
define [
'INST'
'i18n!user_lists'
'jquery'
'jquery.ajaxJSON'
'jquery.instructure_forms'
'jquery.instructure_misc_helpers'
'jquery.instructure_misc_plugins'
'jquery.loadingImg'
'jquery.rails_flash_notifications'
'jquery.scrollToVisible'
'jquery.templateData'
'vendor/jquery.scrollTo'
], (INST, I18n, $) ->
$user_lists_processed_person_template = $("#user_lists_processed_person_template").removeAttr("id").detach()
$user_list_no_valid_users = $("#user_list_no_valid_users")
$user_list_with_errors = $("#user_list_with_errors")
$user_lists_processed_people = $("#user_lists_processed_people")
$user_list_duplicates_found = $("#user_list_duplicates_found")
$form = $("#enroll_users_form")
$enrollment_blank = $("#enrollment_blank").removeAttr("id").hide()
user_lists_path = $("#user_lists_path").attr("href")
UL = INST.UserLists =
init: ->
UL.showTextarea()
$form.find(".cancel_button").click(->
$(".add_users_link").show()
$form.hide()
).end().find(".go_back_button").click(UL.showTextarea).end().find(".verify_syntax_button").click((e) ->
e.preventDefault()
UL.showProcessing()
$.ajaxJSON user_lists_path, "POST", $form.getFormData(), UL.showResults
).end().submit (event) ->
event.preventDefault()
event.stopPropagation()
$form.find(".add_users_button").text(I18n.t("adding_users", "Adding Users...")).attr "disabled", true
$.ajaxJSON $form.attr("action"), "POST", $form.getFormData(), UL.success, UL.failure
$form.find("#enrollment_type").change(->
$("#limit_privileges_to_course_section_holder").showIf $(this).val() is "TeacherEnrollment" or $(this).val() is "TaEnrollment"
).change()
$(".unenroll_user_link").click (event) ->
event.preventDefault()
event.stopPropagation()
if $(this).hasClass("cant_unenroll")
alert I18n.t("cant_unenroll", "This user was automatically enrolled using the campus enrollment system, so they can't be manually removed. Please contact your system administrator if you have questions.")
else
$user = $(this).parents(".user")
$sections = $(this).parents(".sections")
$section = $(this).parents(".section")
$toDelete = $user
$toDelete = $section if $sections.find(".section:visible").size() > 1
$toDelete.confirmDelete
message: I18n.t("delete_confirm", "Are you sure you want to remove this user?")
url: $(this).attr("href")
success: ->
$(this).fadeOut ->
UL.updateCounts()
success: (enrollments) ->
$form.find(".user_list").val ""
UL.showTextarea()
return false if not enrollments or not enrollments.length
already_existed = 0
$.each enrollments, ->
already_existed += UL.addUserToList(@enrollment)
addedMsg = I18n.t("users_added",
one: "1 user added"
other: "%{count} users added"
,
count: enrollments.length - already_existed
)
if already_existed > 0
addedMsg += " " + I18n.t("users_existed",
one: "(1 user already existed)"
other: "(%{count} users already existed)"
,
count: already_existed
)
$.flashMessage addedMsg
failure: (data) ->
$.flashError I18n.t("users_adding_failed", "Failed to enroll users")
showTextarea: ->
$form.find(".add_users_button, .go_back_button, #user_list_parsed").hide()
$form.find(".verify_syntax_button, .cancel_button, #user_list_textarea_container").show().removeAttr "disabled"
$form.find(".user_list").removeAttr("disabled").loadingImage("remove").focus().select()
$form.find(".verify_syntax_button").attr("disabled", false).text I18n.t("buttons.continue", "Continue...")
$user_list = $form.find(".user_list").removeAttr('disabled').loadingImage('remove').focus()
$user_list.select() if $user_list.is(':visible') # .select() blows up in IE9 + jQuery 1.7.2 on invisible elements
showProcessing: ->
$form.find(".verify_syntax_button").attr("disabled", true).text I18n.t("messages.processing", "Processing...")
$form.find(".user_list").attr("disabled", true).loadingImage()
showResults: (userList) ->
$form.find(".add_users_button, .go_back_button, #user_list_parsed").show()
$form.find(".add_users_button").attr("disabled", false).text I18n.t("add_n_users",
one: "OK Looks Good, Add This 1 User"
other: "OK Looks Good, Add These %{count} Users"
,
count: userList.users.length
)
$form.find(".verify_syntax_button, .cancel_button, #user_list_textarea_container").hide()
$form.find(".user_list").removeAttr("disabled").loadingImage "remove"
$user_lists_processed_people.html("").show()
if not userList or not userList.users or not userList.users.length
$user_list_no_valid_users.appendTo $user_lists_processed_people
$form.find(".add_users_button").hide()
else
if userList.errored_users and userList.errored_users.length
$user_list_with_errors.appendTo($user_lists_processed_people).find(".message_content").text I18n.t("user_parsing_errors",
one: "There was 1 error parsing that list of users."
other: "There were %{count} errors parsing that list of users."
,
count: userList.errored_users.length
) + " " + I18n.t("invalid_users_notice", "There may be some that were invalid, and you might need to go back and fix any errors.") + " " + I18n.t("users_to_add",
one: "If you proceed as is, 1 user will be added."
other: "If you proceed as is, %{count} users will be added."
,
count: userList.users.length
)
if userList.duplicates and userList.duplicates.length
$user_list_duplicates_found.appendTo($user_lists_processed_people).find(".message_content").text I18n.t("duplicate_users",
one: "1 duplicate user found, duplicates have been removed."
other: "%{count} duplicate user found, duplicates have been removed."
,
count: userList.duplicates.length
)
$.each userList.users, ->
userDiv = $user_lists_processed_person_template.clone(true).fillTemplateData(data: this).appendTo($user_lists_processed_people)
userDiv.addClass("existing-user").attr "title", I18n.t("titles.existing_user", "Existing user") if @user_id
userDiv.show()
updateCounts: ->
$.each [ "student", "teacher", "ta", "teacher_and_ta", "student_and_observer", "observer" ], ->
$("." + this + "_count").text $("." + this + "_enrollments .user:visible").length
addUserToList: (enrollment) ->
enrollmentType = $.underscore(enrollment.type)
$list = $(".user_list." + enrollmentType + "s")
unless $list.length
if enrollmentType is "student_enrollment" or enrollmentType is "observer_enrollment"
$list = $(".user_list.student_and_observer_enrollments")
else
$list = $(".user_list.teacher_and_ta_enrollments")
$list.find(".none").remove()
enrollment.invitation_sent_at = I18n.t("just_now", "Just Now")
$before = null
$list.find(".user").each ->
name = $(this).getTemplateData(textValues: [ "name" ]).name
if name and enrollment.name and name.toLowerCase() > enrollment.name.toLowerCase()
$before = $(this)
false
enrollment.enrollment_id = enrollment.id
already_existed = true
unless $("#enrollment_" + enrollment.id).length
already_existed = false
$enrollment = $enrollment_blank.clone(true).fillTemplateData(
textValues: [ "name", "membership_type", "email", "enrollment_id" ]
id: "enrollment_" + enrollment.id
hrefValues: [ "id", "user_id", "pseudonym_id", "communication_channel_id" ]
data: enrollment
).addClass(enrollmentType).removeClass("nil_class user_").addClass("user_" + enrollment.user_id).toggleClass("pending", enrollment.workflow_state isnt "active")[(if $before then "insertBefore" else "appendTo")](($before or $list)).show().animate(
backgroundColor: "#FFEE88"
, 1000).animate(
display: "block"
, 2000).animate(
backgroundColor: "#FFFFFF"
, 2000, ->
$(this).css "backgroundColor", ""
)
$enrollment.find(".enrollment_link").removeClass("enrollment_blank").addClass "enrollment_" + enrollment.id
$enrollment.parents(".user_list").scrollToVisible $enrollment
UL.updateCounts()
if already_existed then 1 else 0
$ UL.init
UL

View File

@ -0,0 +1,57 @@
define [
'i18n!dialog'
'jquery'
'underscore'
'Backbone'
], (I18n, $, _, Backbone) ->
##
# A Backbone View to extend for creating a jQuery dialog.
#
# Define options for the dialog as an object using the dialogOptions key,
# those options will be merged with the defaultOptions object.
# Begin with id and title options.
class DialogBaseView extends Backbone.View
initialize: ->
@initDialog()
@setElement @dialog
defaultOptions: ->
# id:
# title:
autoOpen: false
width: 420
resizable: false
buttons: [
text: I18n.t 'cancel', 'Cancel'
click: @cancel
,
text: I18n.t 'update', 'Update'
'class' : 'btn-primary'
click: @update
]
initDialog: () ->
opts = _.extend {}, @defaultOptions(), _.result(this, 'dialogOptions')
@dialog = $("<div id=\"#{ opts.id }\"></div>").appendTo('body').dialog opts
##
# Sample
#
# render: ->
# @$el.html someTemplate()
# this
show: ->
@dialog.dialog('open')
close: ->
@dialog.dialog('close')
update: (e) ->
throw 'Not yet implemented'
cancel: (e) =>
e.preventDefault()
@close()

View File

@ -1,7 +1,8 @@
define [
'jquery'
'Backbone'
'jst/PaginatedView'
], ($, template) ->
], ($, Backbone, template) ->
class PaginatedView extends Backbone.View
@ -11,8 +12,11 @@ define [
distanceTillFetchNextPage: 100
initialize: ->
ret = super
# options
# fetchOptions: options passed to the collection's fetch function
initialize: (options) ->
ret = super options
@fetchOptions = options.fetchOptions
@startPaginationListener()
@collection.on 'beforeFetchNextPage', @showPaginationLoader, this
@collection.on 'didFetchNextPage', @hidePaginationLoader, this
@ -51,4 +55,4 @@ define [
@stopPaginationListener() if @collection.length
return
if @distanceToBottom() < @distanceTillFetchNextPage
@collection.fetchNextPage()
@collection.fetchNextPage @fetchOptions

View File

@ -0,0 +1,99 @@
define [
'i18n!course_settings'
'jquery'
'underscore'
'compiled/views/DialogBaseView'
'jst/courses/settings/EditSectionsView'
'compiled/widget/ContextSearch'
'str/htmlEscape'
'jquery.rails_flash_notifications'
'jquery.disableWhileLoading'
], (I18n, $, _, DialogBaseView, editSectionsViewTemplate, ContextSearch, h) ->
class EditSectionsView extends DialogBaseView
events:
'click #user_sections li a': 'removeSection'
dialogOptions:
id: 'edit_sections'
title: I18n.t 'titles.section_enrollments', 'Section Enrollments'
render: ->
@$el.html editSectionsViewTemplate
sectionsUrl: ENV.SEARCH_URL
@setupContextSearch()
this
setupContextSearch: ->
@$('#section_input').contextSearch
contexts: ENV.CONTEXTS
placeholder: I18n.t 'edit_sections_placeholder', 'Enter a section name'
added: (data, $token, newToken) =>
@$('#user_sections').append $token
selector:
baseData:
type: 'section'
context: "course_#{ENV.COURSE_ID}"
exclude: _.map(@model.get('enrollments'), (e) -> "section_#{e.course_section_id}")
preparer: (postData, data, parent) ->
row.noExpand = true for row in data
browser:
data:
per_page: 100
type: 'section'
search_all_contexts: true
input = @$('#section_input').data('token_input')
input.$fakeInput.css('width', '100%')
input.tokenValues = =>
input.value for input in @$('#user_sections input')
$sections = @$('#user_sections')
for e in @model.get('enrollments')
if section = ENV.CONTEXTS['sections'][e.course_section_id]
sectionName = h section.name
$sections.append $ """<li>
<div class="ellipsis" title="#{sectionName}">#{sectionName}</div>
<a></a>
<input type="hidden" name="sections[]" value="section_#{section.id}">
</li>"""
update: (e) =>
e.preventDefault()
enrollment = @model.get('enrollments')[0]
currentIds = _.map @model.get('enrollments'), (en) -> en.course_section_id
sectionIds = _.map $('#user_sections').find('input'), (i) -> parseInt($(i).val().split('_')[1])
newSections = _.reject sectionIds, (i) => _.include currentIds, i
deferreds = []
# create new enrollments
for id in newSections
url = "/api/v1/sections/#{id}/enrollments"
data =
enrollment:
user_id: @model.get('id')
type: enrollment.type
limit_privileges_to_course_section: enrollment.limit_priveleges_to_course_section
deferreds.push $.ajaxJSON url, 'POST', data
# delete old section enrollments
sectionsToRemove = _.difference currentIds, sectionIds
unenrolls = _.filter @model.get('enrollments'), (en) -> _.include sectionsToRemove, en.course_section_id
for en in unenrolls
url = "#{ENV.COURSE_ROOT_URL}/unenroll/#{en.id}"
deferreds.push $.ajaxJSON url, 'DELETE'
combined = $.when(deferreds...)
.done =>
@trigger 'updated'
$.flashMessage I18n.t('flash.sections', 'Section enrollments successfully updated')
.fail ->
$.flashError I18n.t('flash.sectionError', "Something went wrong updating the user's sections. Please try again later.")
.always => @close()
@$el.disableWhileLoading combined, buttons: {'.btn-primary .ui-button-text': I18n.t('updating', 'Updating...')}
removeSection: (e) ->
$token = $(e.currentTarget).closest('li')
if $token.closest('ul').children().length > 1
$token.remove()

View File

@ -0,0 +1,57 @@
define [
'i18n!course_settings'
'jquery'
'underscore'
'compiled/views/DialogBaseView'
'jst/courses/settings/InvitationsView'
], (I18n, $, _, DialogBaseView, invitationsViewTemplate) ->
class InvitationsView extends DialogBaseView
dialogOptions: ->
id: 'enrollment_dialog'
title: I18n.t 're_send_invitation', 'Re-Send Invitation'
buttons: [
text: I18n.t 'cancel', 'Cancel'
click: @cancel
,
text: I18n.t 're_send_invitation', 'Re-Send Invitation'
'class' : 'btn-primary'
click: @resend
]
render: ->
@showDialogButtons()
data = @model.toJSON()
data.time = $.parseFromISO(_.last(@model.get('enrollments')).updated_at).datetime_formatted
data.course = ENV.CONTEXTS['courses'][ENV.COURSE_ID]
@$el.html invitationsViewTemplate data
pending = @model.pending()
admin = @$el.parents(".teacher_enrollments,.ta_enrollments").length > 0
@$('.student_enrollment_re_send').showIf(pending && !admin)
@$('.admin_enrollment_re_send').showIf(pending && admin)
@$('.accepted_enrollment_re_send').showIf(!pending)
if pending && !admin && !data.course.available
@hideDialogButtons()
this
showDialogButtons: ->
@$el.parent().next('.ui-dialog-buttonpane').show()
hideDialogButtons: ->
@$el.parent().next('.ui-dialog-buttonpane').hide()
resend: (e) =>
e.preventDefault()
@close()
for e in @model.get('enrollments')
url = "/confirmations/#{ @model.get('id') }/re_send?enrollment_id=#{ e.id }"
$.ajaxJSON url
removeSection: (e) ->
$token = $(e.currentTarget).closest('li')
if $token.closest('ul').children().length > 1
$token.remove()

View File

@ -0,0 +1,102 @@
define [
'i18n!course_settings'
'jquery'
'underscore'
'compiled/views/DialogBaseView'
'jst/courses/settings/LinkToStudentsView'
'jquery.disableWhileLoading'
], (I18n, $, _, DialogBaseView, linkToStudentsViewTemplate) ->
class LinkToStudentsView extends DialogBaseView
dialogOptions:
id: 'link_students'
title: I18n.t 'titles.link_to_students', 'Link to Students'
render: ->
data = @model.toJSON()
data.studentsUrl = ENV.SEARCH_URL
@$el.html linkToStudentsViewTemplate data
dfd = $.Deferred()
@disable dfd.promise()
dfds = []
@students = []
@$('#student_input').contextSearch
contexts: ENV.CONTEXTS
placeholder: I18n.t 'link_students_placeholder', 'Enter a student name'
change: (tokens) =>
@students = _.map tokens, (id) -> parseInt id
selector:
baseData:
type: 'user'
context: "course_#{ENV.COURSE_ID}"
exclude: [@model.get('id')]
preparer: (postData, data, parent) ->
row.noExpand = true for row in data
browser:
data:
per_page: 100
type: 'user'
input = @$('#student_input').data('token_input')
input.$fakeInput.css('width', '100%')
for e in @model.get('enrollments') when e.associated_user_id
dfds.push @getUserData(e.associated_user_id).done (user) ->
input.addToken
value: user.id
text: user.name
data: user
$.when(dfds...).done -> dfd.resolve()
this
getUserData: (id) ->
$.get("/api/v1/courses/#{ENV.COURSE_ID}/users/#{id}", include:['enrollments'])
update: (e) =>
e.preventDefault()
dfds = []
enrollments = @model.get('enrollments')
enrollment = enrollments[0]
unlinkedEnrolls = _.filter enrollments, (en) -> !en.associated_user_id # keep the original observer enrollment around
currentLinks = _.compact _.pluck(enrollments, 'associated_user_id')
newLinks = _.difference @students, currentLinks
removeLinks = _.difference currentLinks, @students
# create new links
for id in newLinks
dfd = $.Deferred()
dfds.push dfd.promise()
@getUserData(id).done (user) =>
udfds = []
sections = _.map user.enrollments, (en) -> en.course_section_id
for sId in sections
url = "/api/v1/sections/#{sId}/enrollments"
data =
enrollment:
user_id: @model.get('id')
associated_user_id: id
type: enrollment.type
limit_privileges_to_course_section: enrollment.limit_priveleges_to_course_section
udfds.push $.ajaxJSON url, 'POST', data
$.when(udfds...).done -> dfd.resolve()
# delete old links
unenrolls = _.filter enrollments, (en) -> _.include removeLinks, en.associated_user_id
for en in unenrolls
url = "#{ENV.COURSE_ROOT_URL}/unenroll/#{en.id}"
dfds.push $.ajaxJSON url, 'DELETE'
@disable($.when(dfds...)
.done =>
@trigger 'updated'
$.flashMessage I18n.t('flash.links', 'Student links successfully updated')
.fail ->
$.flashError I18n.t('flash.linkError', "Something went wrong updating the user's student links. Please try again later.")
.always => @close())
disable: (dfds) ->
@$el.disableWhileLoading dfds, buttons: {'.btn-primary .ui-button-text': I18n.t('updating', 'Updating...')}

View File

@ -0,0 +1,28 @@
define [
'jquery'
'underscore'
'compiled/views/PaginatedView'
'compiled/views/course_settings/UserView'
'compiled/collections/UserCollection'
'compiled/models/User'
], ($, _, PaginatedView, UserView, UserCollection, User) ->
class UserCollectionView extends PaginatedView
# options.requestParams are merged with UserCollection#fetch request params
initialize: (options) ->
@fetchOptions = data: $.extend ENV.USER_PARAMS, options.requestParams
@collection = new UserCollection()
@collection.url = options.url
@collection.on 'add', @renderUser
@collection.on 'reset', @render
@$el.disableWhileLoading @collection.fetch(@fetchOptions)
@paginationScrollContainer = @$el
super fetchOptions: @fetchOptions
render: ->
@collection.each (user) => @renderUser user
super
renderUser: (user) =>
@$el.append (new UserView model: user).render().el

View File

@ -0,0 +1,112 @@
define [
'i18n!course_settings'
'jquery'
'underscore'
'Backbone'
'compiled/views/course_settings/EditSectionsView'
'compiled/views/course_settings/InvitationsView'
'compiled/views/course_settings/LinkToStudentsView'
'compiled/models/User'
'jst/courses/settings/UserView'
'compiled/str/underscore'
'str/htmlEscape'
'compiled/jquery.kylemenu'
], (I18n, $, _, Backbone, EditSectionsView, InvitationsView, LinkToStudentsView, User, userViewTemplate, toUnderscore, h) ->
editSectionsDialog = null
linkToStudentsDialog = null
invitationDialog = null
class UserView extends Backbone.View
tagName: 'li'
className: 'user admin-link-hover-area'
events:
'click .admin-links [data-event]': 'handleMenuEvent'
render: =>
@$el.disableWhileLoading @data().done (data) =>
@$el.addClass data.typeClass
@$el.attr 'id', "user_#{data.id}"
@$el.attr 'title', if data.isPending
I18n.t 'pending', 'pending acceptance'
else
"#{data.name}: #{data.login_id}"
@$el.html userViewTemplate(data)
this
getUserData: (id) ->
$.get("/api/v1/courses/#{ENV.COURSE_ID}/users/#{id}", include:['enrollments'])
data: ->
dfd = $.Deferred()
dfds = []
data = $.extend @model.toJSON(),
url: "#{ENV.COURSE_ROOT_URL}/users/#{@model.get('id')}"
permissions: ENV.PERMISSIONS
isObserver: @model.hasEnrollmentType('ObserverEnrollment')
isPending: @model.pending()
for en in data.enrollments
en.pending = @model.pending()
en.typeClass = toUnderscore en.type
section = ENV.CONTEXTS['sections'][en.course_section_id]
en.sectionTitle = h section.name if section
if data.isObserver
users = {}
for en in data.enrollments
users[en.associated_user_id] ||= en.enrollment_state in ['creation_pending', 'invited'] if en.associated_user_id
data.enrollments = []
for id, pending of users
dfds.push @getUserData(id).done (user) =>
ob = {pending}
ob.sectionTitle = I18n.t('observing_user', '*Observing*: %{user_name}', wrapper: '<i>$1</i>', user_name: user.name)
for en in user.enrollments
section = ENV.CONTEXTS['sections'][en.course_section_id]
ob.sectionTitle += h(I18n.t('#support.array.words_connector') + section.name) if section
data.enrollments.push ob
$.when(dfds...).done -> dfd.resolve data
dfd.promise()
reload: =>
@$el.disableWhileLoading @model.fetch
data: ENV.USER_PARAMS
success: => @render()
resendInvitation: (e) ->
invitationDialog ||= new InvitationsView
invitationDialog.model = @model
invitationDialog.render().show()
editSections: (e) ->
editSectionsDialog ||= new EditSectionsView
editSectionsDialog.model = @model
editSectionsDialog.off 'updated'
editSectionsDialog.on 'updated', @reload
editSectionsDialog.render().show()
linkToStudents: (e) ->
linkToStudentsDialog ||= new LinkToStudentsView
linkToStudentsDialog.model = @model
linkToStudentsDialog.off 'updated'
linkToStudentsDialog.on 'updated', @reload
linkToStudentsDialog.render().show()
removeFromCourse: (e) ->
return unless confirm I18n.t('links.unenroll_user_course', 'Remove User from Course')
@$el.hide()
success = =>
for e in @model.get('enrollments')
e_type = e.typeClass.split('_')[0]
c.innerText = parseInt(c.innerText) - 1 for c in $(".#{e_type}_count")
failure = =>
@$el.show()
deferreds = _.map @model.get('enrollments'), (e) ->
$.ajaxJSON "#{ENV.COURSE_ROOT_URL}/unenroll/#{e.id}", 'DELETE'
$.when(deferreds...).then success, failure
handleMenuEvent : (e) =>
e.preventDefault()
method = $(e.currentTarget).data 'event'
@[method].call this, e

View File

@ -0,0 +1,32 @@
define [
'jquery'
'compiled/user_lists'
'compiled/models/User'
], ($, UL, User) ->
$enrollUsersForm = $("#enroll_users_form")
$enrollUsersForm.hide()
$(".add_users_link").click (event) ->
$(this).hide()
event.preventDefault()
$enrollUsersForm.show()
$("html,body").scrollTo $enrollUsersForm
$enrollUsersForm.find("textarea").focus().select()
# override adding to the users lists to work with UserCollectionView
UL.addUserToList = (enrollment) ->
alreadyExisted = false
enrollmentType = $.underscore(enrollment.type)
$(".user_list." + enrollmentType + "s").find(".none").remove()
viewName = enrollmentType.split('_')[0] + 'sView'
users = app.usersTab[viewName].collection
if ($userEl = $("#user_" + enrollment.user_id)).length
$userEl.remove()
users.remove users.get(enrollment.user_id)
alreadyExisted = true
user = new User id: enrollment.user_id
user.urlRoot = users.url
user.fetch
data: ENV.USER_PARAMS
success: (u, r) -> users.add u
if alreadyExisted then 1 else 0

View File

@ -25,16 +25,10 @@ define [
class ContextSearch extends TokenInput
i18n:
placeholderText: I18n.t('context_search_placeholder', 'Enter a name, course, or group')
noResultsText: I18n.t('no_results', 'No results found')
everyoneText: I18n.t('enrollments_everyone', 'Everyone')
selectAllText: I18n.t('select_all', 'Select All')
defaults: ->
placeholder: @i18n.placeholderText
placeholder: I18n.t('context_search_placeholder', 'Enter a name, course, or group')
selector:
messages: {noResults: @i18n.noResultsText}
messages: {noResults: I18n.t('no_results', 'No results found')}
limiter: (o) => 5
populator: @populator()
preparer: @preparer
@ -101,7 +95,7 @@ define [
# i.e. we are listing synthetic contexts under a course or section
data.unshift
id: "#{context}_all"
name: @i18n.everyoneText
name: I18n.t('enrollments_everyone', 'Everyone')
user_count: parent.data('user_data').user_count
type: 'context'
avatar_url: parent.data('user_data').avatar_url
@ -110,7 +104,7 @@ define [
# i.e. we are listing all users in a group or synthetic context
data.unshift
id: context
name: @i18n.selectAllText
name: I18n.t('select_all', 'Select All')
user_count: parent.data('user_data').user_count
type: 'context'
avatar_url: parent.data('user_data').avatar_url
@ -154,8 +148,6 @@ define [
$.fn.contextSearch = (options) ->
@each ->
$el = $ this
$el.data 'contextSearch', new ContextSearch($(this), $.extend(true, {}, options))
ContextSearch
new ContextSearch $(this), $.extend(true, {}, options)
ContextSearch

View File

@ -223,4 +223,3 @@ define [
new TokenInput $(this), $.extend(true, {}, options)
TokenInput

View File

@ -183,7 +183,7 @@ define [
@renderList(@queryCache[thisQuery], options, postData)
return
@fetchListAjaxRequests.push @load $.ajaxJSON @url, 'POST', $.extend({}, postData),
@fetchListAjaxRequests.push @load $.ajaxJSON @url, 'GET', $.extend({}, postData),
(data) =>
@queryCache[thisQuery] = data
if JSON.stringify(@preparePost(options.data ? {})) is thisQuery # i.e. only if it hasn't subsequently changed (and thus triggered another call)
@ -207,7 +207,7 @@ define [
text: user.name
data: user
@load $.ajaxJSON( @url, 'POST', { user_id: userId, from_conversation_id: fromConversationId}, success, @close )
@load $.ajaxJSON( @url, 'GET', { user_id: userId, from_conversation_id: fromConversationId}, success, @close )
open: ->
@$container.show()
@ -420,7 +420,8 @@ define [
preparePost: (data) ->
postData = $.extend({}, @options.baseData ? {}, data, {search: @input.val().replace(/^\s+|\s+$/g, "")})
postData.exclude = @input.baseExclude.concat(if @stack.length then [] else @input.tokenValues())
excludes = @input.baseExclude.concat(if @stack.length then [] else @input.tokenValues())
postData.exclude = if postData.exclude then postData.exclude.concat excludes else excludes
postData.context = @stack[@stack.length - 1][0].data('id') if @listExpanded()
postData.per_page ?= @options.limiter?(level: @stack.length)
postData

View File

@ -308,7 +308,7 @@ class CommunicationChannelsController < ApplicationController
if @enrollment && (@enrollment.invited? || @enrollment.active?)
@enrollment.re_send_confirmation!
else
@cc = @user.communication_channels.find(params[:id])
@cc = params[:id].present? ? @user.communication_channels.find(params[:id]) : @user.communication_channel
@cc.send_confirmation!(@domain_root_account)
end
render :json => {:re_sent => true}

View File

@ -296,8 +296,8 @@ class ContextController < ApplicationController
@students = @context.
students_visible_to(@current_user).
scoped(:conditions => "enrollments.type != 'StudentViewEnrollment'").
find(:all, :order => User.sortable_name_order_by_clause).uniq
@teachers = @context.instructors.find(:all, :order => User.sortable_name_order_by_clause).uniq
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]
@ -305,10 +305,10 @@ class ContextController < ApplicationController
@primary_users = {t('roster.students', 'Students') => @students}
@secondary_users = {t('roster.teachers', 'Teachers & TAs') => @teachers}
elsif @context.is_a?(Group)
@users = @context.participating_users.find(:all, :order => User.sortable_name_order_by_clause).uniq
@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.find(:all, :order => User.sortable_name_order_by_clause).uniq}
@secondary_users = {t('roster.teachers', 'Teachers & TAs') => @context.context.instructors.order_by_sortable_name.uniq}
end
end
@secondary_users ||= {}

View File

@ -21,6 +21,7 @@
# API for creating, accessing and updating user conversations.
class ConversationsController < ApplicationController
include ConversationsHelper
include SearchHelper
include Api::V1::Submission
include Api::V1::Attachment
@ -524,109 +525,6 @@ class ConversationsController < ApplicationController
end
end
# @API Find recipients
# Find valid recipients (users, courses and groups) that the current user
# can send messages to.
#
# Pagination is supported if an explicit type is given (but there is no last
# link). If no type is given, results will be limited to 10 by default (can
# be overridden via per_page).
#
# @argument search Search terms used for matching users/courses/groups (e.g.
# "bob smith"). If multiple terms are given (separated via whitespace),
# only results matching all terms will be returned.
# @argument context Limit the search to a particular course/group (e.g.
# "course_3" or "group_4").
# @argument exclude[] Array of ids to exclude from the search. These may be
# user ids or course/group ids prefixed with "course_" or "group_" respectively,
# e.g. exclude[]=1&exclude[]=2&exclude[]=course_3
# @argument type ["user"|"context"] Limit the search just to users or contexts (groups/courses).
# @argument user_id [Integer] Search for a specific user id. This ignores the other above parameters, and will never return more than one result.
# @argument from_conversation_id [Integer] When searching by user_id, only users that could be normally messaged by this user will be returned. This parameter allows you to specify a conversation that will be referenced for a shared context -- if both the current user and the searched user are in the conversation, the user will be returned. This is used to start new side conversations.
#
# @example_response
# [
# {"id": "group_1", "name": "the group", "type": "context", "user_count": 3},
# {"id": 2, "name": "greg", "common_courses": {}, "common_groups": {"1": ["Member"]}}
# ]
#
# @response_field id The unique identifier for the user/context. For
# groups/courses, the id is prefixed by "group_"/"course_" respectively.
# @response_field name The name of the user/context
# @response_field avatar_url Avatar image url for the user/context
# @response_field type ["context"|"course"|"section"|"group"|"user"|null]
# Type of recipients to return, defaults to null (all). "context"
# encompasses "course", "section" and "group"
# @response_field types[] Array of recipient types to return (see type
# above), e.g. types[]=user&types[]=course
# @response_field user_count Only set for contexts, indicates number of
# messageable users
# @response_field common_courses Only set for users. Hash of course ids and
# enrollment types for each course to show what they share with this user
# @response_field common_groups Only set for users. Hash of group ids and
# enrollment types for each group to show what they share with this user
def find_recipients
types = (params[:types] || [] + [params[:type]]).compact
types |= [:course, :section, :group] if types.delete('context')
types = if types.present?
{:user => types.delete('user').present?, :context => types.present? && types.map(&:to_sym)}
else
{:user => true, :context => [:course, :section, :group]}
end
@blank_fallback = !api_request?
max_results = [params[:per_page].try(:to_i) || 10, 50].min
if max_results < 1
if !types[:user] || params[:context]
max_results = nil # i.e. all results
else
max_results = params[:per_page] = 10
end
end
limit = max_results ? max_results + 1 : nil
page = params[:page].try(:to_i) || 1
offset = max_results ? (page - 1) * max_results : 0
exclude = params[:exclude] || []
recipients = []
if params[:user_id]
recipients = matching_participants(:ids => [params[:user_id]], :conversation_id => params[:from_conversation_id])
elsif (params[:context] || params[:search])
options = {:search => params[:search], :context => params[:context], :limit => limit, :offset => offset, :synthetic_contexts => params[:synthetic_contexts]}
rank_results = params[:search].present?
contexts = types[:context] ? matching_contexts(options.merge(:rank_results => rank_results, :include_inactive => params[:include_inactive], :exclude_ids => exclude.grep(User::MESSAGEABLE_USER_CONTEXT_REGEX), :types => types[:context])) : []
participants = types[:user] && !@skip_users ? matching_participants(options.merge(:rank_results => rank_results, :exclude_ids => exclude.grep(/\A\d+\z/).map(&:to_i))) : []
if max_results
if types[:user] ^ types[:context]
recipients = contexts + participants
has_next_page = recipients.size > max_results
recipients = recipients[0, max_results]
recipients.instance_eval <<-CODE
def paginate(*args); self; end
def next_page; #{has_next_page ? page + 1 : 'nil'}; end
def previous_page; #{page > 1 ? page - 1 : 'nil'}; end
def total_pages; nil; end
def per_page; #{max_results}; end
CODE
recipients = Api.paginate(recipients, self, request.request_uri.gsub(/(per_)?page=[^&]*(&|\z)/, '').sub(/[&?]\z/, ''))
else
if contexts.size <= max_results / 2
recipients = contexts + participants
elsif participants.size <= max_results / 2
recipients = contexts[0, max_results - participants.size] + participants
else
recipients = contexts[0, max_results / 2] + participants
end
recipients = recipients[0, max_results]
end
else
recipients = contexts + participants
end
end
render :json => recipients
end
def public_feed
return unless get_feed_context(:only => [:user])
@current_user = @context
@ -692,8 +590,6 @@ class ConversationsController < ApplicationController
render :json => {}
end
attr_reader :avatar_size
private
def infer_scope
@ -732,11 +628,6 @@ class ConversationsController < ApplicationController
end
end
def set_avatar_size
@avatar_size = params[:avatar_size].to_i
@avatar_size = 50 unless [32, 50].include?(@avatar_size)
end
def normalize_recipients
if params[:recipients]
recipient_ids = params[:recipients]
@ -761,131 +652,6 @@ class ConversationsController < ApplicationController
@tags = tags.uniq
end
def messageable_context_states
{:active => true, :recently_active => true, :inactive => false}
end
def context_state_ranks
{:active => 0, :recently_active => 1, :inactive => 2}
end
def context_type_ranks
{:course => 0, :section => 1, :group => 2}
end
def can_add_notes_to?(course)
course.enable_user_notes && course.grants_right?(@current_user, nil, :manage_user_notes)
end
def matching_contexts(options)
context_name = options[:context]
avatar_url = avatar_url_for_group(blank_fallback)
user_counts = {
:course => @current_user.enrollment_visibility[:user_counts],
:group => @current_user.group_membership_visibility[:user_counts],
:section => @current_user.enrollment_visibility[:section_user_counts]
}
terms = options[:search].to_s.downcase.strip.split(/\s+/)
exclude = options[:exclude_ids] || []
result = []
if context_name.nil?
result = if terms.blank?
courses = @contexts[:courses].values
group_ids = @current_user.current_groups.map(&:id)
groups = @contexts[:groups].slice(*group_ids).values
courses + groups
else
@contexts.values_at(*options[:types].map{|t|t.to_s.pluralize.to_sym}).compact.map(&:values).flatten
end
elsif options[:synthetic_contexts]
if context_name =~ /\Acourse_(\d+)(_(groups|sections))?\z/ && (course = @contexts[:courses][$1.to_i]) && messageable_context_states[course[:state]]
course = Course.find_by_id(course[:id])
sections = @contexts[:sections].values.select{ |section| section[:parent] == {:course => course.id} }
groups = @contexts[:groups].values.select{ |group| group[:parent] == {:course => course.id} }
case context_name
when /\Acourse_\d+\z/
if terms.present? # search all groups and sections (and users)
result = sections + groups
else # otherwise we show synthetic contexts
result = synthetic_contexts_for(course, context_name)
result << {:id => "#{context_name}_sections", :name => t(:course_sections, "Course Sections"), :item_count => sections.size, :type => :context} if sections.size > 1
result << {:id => "#{context_name}_groups", :name => t(:student_groups, "Student Groups"), :item_count => groups.size, :type => :context} if groups.size > 0
return result
end
when /\Acourse_\d+_groups\z/
@skip_users = true # whether searching or just enumerating, we just want groups
result = groups
when /\Acourse_\d+_sections\z/
@skip_users = true # ditto
result = sections
end
elsif context_name =~ /\Asection_(\d+)\z/ && (section = @contexts[:sections][$1.to_i]) && messageable_context_states[section[:state]]
if terms.present? # we'll just search the users
result = []
else
section = CourseSection.find_by_id(section[:id])
return synthetic_contexts_for(section.course, context_name)
end
end
end
result = if options[:rank_results]
result.sort_by{ |context|
[
context_state_ranks[context[:state]],
context_type_ranks[context[:type]],
context[:name].downcase
]
}
else
result.sort_by{ |context| context[:name].downcase }
end
result = result.reject{ |context| context[:state] == :inactive } unless options[:include_inactive]
result = result.map{ |context|
ret = {
:id => "#{context[:type]}_#{context[:id]}",
:name => context[:name],
:avatar_url => avatar_url,
:type => :context,
:user_count => user_counts[context[:type]][context[:id]]
}
ret[:context_name] = context[:context_name] if context[:context_name] && context_name.nil?
ret
}
result.reject!{ |context| terms.any?{ |part| !context[:name].downcase.include?(part) } } if terms.present?
result.reject!{ |context| exclude.include?(context[:id]) }
offset = options[:offset] || 0
options[:limit] ? result[offset, offset + options[:limit]] : result
end
def synthetic_contexts_for(course, context)
@skip_users = true
# TODO: move the aggregation entirely into the DB. we only select a little
# bit of data per user, but this still isn't ideal
users = @current_user.messageable_users(:context => context)
enrollment_counts = {:all => users.size}
users.each do |user|
user.common_courses[course.id].uniq.each do |role|
enrollment_counts[role] ||= 0
enrollment_counts[role] += 1
end
end
avatar_url = avatar_url_for_group(blank_fallback)
result = []
result << {:id => "#{context}_teachers", :name => t(:enrollments_teachers, "Teachers"), :user_count => enrollment_counts['TeacherEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['TeacherEnrollment'].to_i > 0
result << {:id => "#{context}_tas", :name => t(:enrollments_tas, "Teaching Assistants"), :user_count => enrollment_counts['TaEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['TaEnrollment'].to_i > 0
result << {:id => "#{context}_students", :name => t(:enrollments_students, "Students"), :user_count => enrollment_counts['StudentEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['StudentEnrollment'].to_i > 0
result << {:id => "#{context}_observers", :name => t(:enrollments_observers, "Observers"), :user_count => enrollment_counts['ObserverEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['ObserverEnrollment'].to_i > 0
result
end
def matching_participants(options)
jsonify_users(@current_user.messageable_users(options), options.merge(:include_participant_avatars => true, :include_participant_contexts => true))
end
def get_conversation(allow_deleted = false)
scope = @current_user.all_conversations
scope = scope.scoped(:conditions => "message_count > 0") unless allow_deleted
@ -959,25 +725,6 @@ class ConversationsController < ApplicationController
}
end
def jsonify_users(users, options = {})
options = {
:include_participant_avatars => true,
:include_participant_contexts => true
}.merge(options)
users.map { |user|
hash = {
:id => user.id,
:name => user.short_name
}
if options[:include_participant_contexts]
hash[:common_courses] = user.common_courses
hash[:common_groups] = user.common_groups
end
hash[:avatar_url] = avatar_url_for_user(user, blank_fallback) if options[:include_participant_avatars]
hash
}
end
# TODO API v2: default to true, like we do in the UI
def interleave_submissions
params[:interleave_submissions] || !api_request?
@ -988,13 +735,10 @@ class ConversationsController < ApplicationController
["1", "true"].include?(enabled.to_s)
end
def blank_fallback
params[:blank_avatar_fallback] || @blank_fallback
end
# TODO API v2: default to false, like we do in the UI
def auto_mark_as_read?
params[:auto_mark_as_read] ||= api_request?
value_to_boolean(params[:auto_mark_as_read])
end
end

View File

@ -22,6 +22,8 @@ require 'set'
#
# API for accessing course information.
class CoursesController < ApplicationController
include SearchHelper
before_filter :require_user, :only => [:index]
before_filter :require_context, :only => [:roster, :locks, :switch_role, :create_file]
@ -255,6 +257,7 @@ class CoursesController < ApplicationController
# @argument include[] ["enrollments"] Optionally include with each Course the
# user's current and invited enrollments.
# @argument include[] ["locked"] Optionally include whether an enrollment is locked.
# @argument include[] ["avatar_url"] Optionally include avatar_url.
#
# @response_field id The unique identifier for the user.
# @response_field name The full user name.
@ -274,25 +277,49 @@ class CoursesController < ApplicationController
get_context
if authorized_action(@context, @current_user, :read_roster)
enrollment_type = "#{params[:enrollment_type].capitalize}Enrollment" if params[:enrollment_type]
users = (enrollment_type ?
@context.users.scoped(:conditions => ["enrollments.type = ? ", enrollment_type]) :
@context.users)
users = @context.users_visible_to(@current_user)
users = users.scoped(:order => "users.sortable_name")
users = users.scoped(:conditions => ["enrollments.type = ? ", enrollment_type]) if enrollment_type
if user_json_is_admin?
users = users.scoped(:include => {:pseudonym => :communication_channel})
end
users = Api.paginate(users, self, api_v1_course_users_path)
includes = Array(params[:include])
if includes.include?('enrollments')
User.send(:preload_associations, users, :current_and_invited_enrollments,
User.send(:preload_associations, users, :not_ended_enrollments,
:conditions => ['enrollments.course_id = ?', @context.id])
end
render :json => users.uniq.map { |u|
enrollments = u.current_and_invited_enrollments if includes.include?('enrollments')
render :json => users.map { |u|
enrollments = u.not_ended_enrollments if includes.include?('enrollments')
user_json(u, @current_user, session, includes, @context, enrollments)
}
end
end
# @API
# Return information on a single user.
#
# Accepts the same include[] parameters as the :users: action, and returns a
# single user with the same fields as that action.
def user
get_context
if authorized_action(@context, @current_user, :read_roster)
users = @context.users_visible_to(@current_user)
users = users.scoped(:conditions => ['users.id = ?', params[:id]])
if user_json_is_admin?
users = users.scoped(:include => {:pseudonym => :communication_channel})
end
includes = Array(params[:include])
if includes.include?('enrollments')
User.send(:preload_associations, users, :not_ended_enrollments,
:conditions => ['enrollments.course_id = ?', @context.id])
end
user = users.first
enrollments = user.not_ended_enrollments if includes.include?('enrollments')
render :json => user_json(user, @current_user, session, includes, @context, enrollments)
end
end
include Api::V1::StreamItem
# @API Course activity stream
# Returns the current user's course-specific activity stream, paginated.
@ -390,6 +417,30 @@ class CoursesController < ApplicationController
def settings
get_context
if authorized_action(@context, @current_user, :read_as_admin)
load_all_contexts
users_scope = @context.users_visible_to(@current_user)
enrollment_counts = users_scope.count(:distinct => true, :group => 'enrollments.type', :select => 'users.id')
@user_counts = {
:student => enrollment_counts['StudentEnrollment'] || 0,
:teacher => enrollment_counts['TeacherEnrollment'] || 0,
:ta => enrollment_counts['TaEnrollment'] || 0,
:observer => enrollment_counts['ObserverEnrollment'] || 0,
:designer => enrollment_counts['DesignerEnrollment'] || 0,
:invited => users_scope.count(:distinct => true, :select => 'users.id', :conditions => ["enrollments.workflow_state = 'invited'"])
}
js_env(:COURSE_ID => @context.id,
:USER_COUNTS => @user_counts,
:USERS_URL => "/api/v1/courses/#{ @context.id }/users",
:COURSE_ROOT_URL => "/courses/#{ @context.id }",
:SEARCH_URL => search_recipients_url,
:CONTEXTS => @contexts,
:USER_PARAMS => {:include => ['email', 'enrollments', 'locked']},
:PERMISSIONS => {
:manage_students => (@context.grants_right?(@current_user, session, :manage_students) ||
@context.grants_right?(@current_user, session, :manage_admin_users)),
:manage_account_settings => @context.account.grants_right?(@current_user, session, :manage_account_settings),
})
@alerts = @context.alerts
@role_types = []
add_crumb(t('#crumbs.settings', "Settings"), named_context_url(@context, :context_details_url))
@ -411,8 +462,8 @@ class CoursesController < ApplicationController
def roster
if authorized_action(@context, @current_user, :read_roster)
log_asset_access("roster:#{@context.asset_string}", "roster", "other")
@students = @context.participating_students.find(:all, :order => User.sortable_name_order_by_clause)
@teachers = @context.instructors.find(:all, :order => User.sortable_name_order_by_clause)
@students = @context.participating_students.order_by_sortable_name
@teachers = @context.instructors.order_by_sortable_name
@groups = @context.groups.active
end
end

View File

@ -187,7 +187,7 @@ class EnrollmentsApiController < ApplicationController
params[:enrollment][:section] = @context.course_sections.active.find params[:enrollment].delete(:course_section_id)
end
user = api_find(User, params[:enrollment].delete(:user_id))
@enrollment = @context.enroll_user(user, type, params[:enrollment])
@enrollment = @context.enroll_user(user, type, params[:enrollment].merge(:allow_multiple_enrollments => true))
@enrollment.valid? ?
render(:json => enrollment_json(@enrollment, @current_user, session).to_json) :
render(:json => @enrollment.errors.to_json)

View File

@ -170,7 +170,7 @@ class QuizzesController < ApplicationController
submission_ids = {}
@submissions.each{|s| submission_ids[s.user_id] = s.id }
submission_users = @submissions.map{|s| s.user_id}
students = @context.students.find(:all, :order => User.sortable_name_order_by_clause).to_a
students = @context.students.order_by_sortable_name.to_a
@submitted_students = students.select{|stu| submission_ids[stu.id] }
if @quiz.survey? && @quiz.anonymous_submissions
@submitted_students = @submitted_students.sort_by{|stu| submission_ids[stu.id] }

View File

@ -17,7 +17,11 @@
#
class SearchController < ApplicationController
include SearchHelper
before_filter :get_context
before_filter :set_avatar_size
before_filter :load_all_contexts, :only => :recipients
def rubrics
contexts = @current_user.management_contexts rescue []
@ -29,4 +33,236 @@ class SearchController < ApplicationController
res = res.select{|r| r.title.downcase.match(params[:q].downcase) }
render :json => res.to_json
end
# @API Find recipients
# Find valid recipients (users, courses and groups) that the current user
# can send messages to.
#
# Pagination is supported if an explicit type is given (but there is no last
# link). If no type is given, results will be limited to 10 by default (can
# be overridden via per_page).
#
# @argument search Search terms used for matching users/courses/groups (e.g.
# "bob smith"). If multiple terms are given (separated via whitespace),
# only results matching all terms will be returned.
# @argument context Limit the search to a particular course/group (e.g.
# "course_3" or "group_4").
# @argument exclude[] Array of ids to exclude from the search. These may be
# user ids or course/group ids prefixed with "course_" or "group_" respectively,
# e.g. exclude[]=1&exclude[]=2&exclude[]=course_3
# @argument type ["user"|"context"] Limit the search just to users or contexts (groups/courses).
# @argument user_id [Integer] Search for a specific user id. This ignores the other above parameters, and will never return more than one result.
# @argument from_conversation_id [Integer] When searching by user_id, only users that could be normally messaged by this user will be returned. This parameter allows you to specify a conversation that will be referenced for a shared context -- if both the current user and the searched user are in the conversation, the user will be returned. This is used to start new side conversations.
#
# @example_response
# [
# {"id": "group_1", "name": "the group", "type": "context", "user_count": 3},
# {"id": 2, "name": "greg", "common_courses": {}, "common_groups": {"1": ["Member"]}}
# ]
#
# @response_field id The unique identifier for the user/context. For
# groups/courses, the id is prefixed by "group_"/"course_" respectively.
# @response_field name The name of the user/context
# @response_field avatar_url Avatar image url for the user/context
# @response_field type ["context"|"course"|"section"|"group"|"user"|null]
# Type of recipients to return, defaults to null (all). "context"
# encompasses "course", "section" and "group"
# @response_field types[] Array of recipient types to return (see type
# above), e.g. types[]=user&types[]=course
# @response_field user_count Only set for contexts, indicates number of
# messageable users
# @response_field common_courses Only set for users. Hash of course ids and
# enrollment types for each course to show what they share with this user
# @response_field common_groups Only set for users. Hash of group ids and
# enrollment types for each group to show what they share with this user
def recipients
types = (params[:types] || [] + [params[:type]]).compact
types |= [:course, :section, :group] if types.delete('context')
types = if types.present?
{:user => types.delete('user').present?, :context => types.present? && types.map(&:to_sym)}
else
{:user => true, :context => [:course, :section, :group]}
end
@blank_fallback = !api_request?
max_results = [params[:per_page].try(:to_i) || 10, 50].min
if max_results < 1
if !types[:user] || params[:context]
max_results = nil # i.e. all results
else
max_results = params[:per_page] = 10
end
end
limit = max_results ? max_results + 1 : nil
page = params[:page].try(:to_i) || 1
offset = max_results ? (page - 1) * max_results : 0
exclude = params[:exclude] || []
recipients = []
if params[:user_id]
recipients = matching_participants(:ids => [params[:user_id]], :conversation_id => params[:from_conversation_id])
elsif (params[:context] || params[:search])
options = {:search => params[:search], :context => params[:context], :limit => limit, :offset => offset, :synthetic_contexts => params[:synthetic_contexts]}
rank_results = params[:search].present?
contexts = types[:context] ? matching_contexts(options.merge(:rank_results => rank_results,
:include_inactive => params[:include_inactive],
:exclude_ids => exclude.grep(User::MESSAGEABLE_USER_CONTEXT_REGEX),
:search_all_contexts => params[:search_all_contexts],
:types => types[:context])) : []
participants = types[:user] && !@skip_users ? matching_participants(options.merge(:rank_results => rank_results, :exclude_ids => exclude.grep(/\A\d+\z/).map(&:to_i))) : []
if max_results
if types[:user] ^ types[:context]
recipients = contexts + participants
has_next_page = recipients.size > max_results
recipients = recipients[0, max_results]
recipients.instance_eval <<-CODE
def paginate(*args); self; end
def next_page; #{has_next_page ? page + 1 : 'nil'}; end
def previous_page; #{page > 1 ? page - 1 : 'nil'}; end
def total_pages; nil; end
def per_page; #{max_results}; end
CODE
recipients = Api.paginate(recipients, self, request.request_uri.gsub(/(per_)?page=[^&]*(&|\z)/, '').sub(/[&?]\z/, ''))
else
if contexts.size <= max_results / 2
recipients = contexts + participants
elsif participants.size <= max_results / 2
recipients = contexts[0, max_results - participants.size] + participants
else
recipients = contexts[0, max_results / 2] + participants
end
recipients = recipients[0, max_results]
end
else
recipients = contexts + participants
end
end
render :json => recipients
end
private
def matching_participants(options)
jsonify_users(@current_user.messageable_users(options), options.merge(:include_participant_avatars => true, :include_participant_contexts => true))
end
def matching_contexts(options)
context_name = options[:context]
avatar_url = avatar_url_for_group(blank_fallback)
user_counts = {
:course => @current_user.enrollment_visibility[:user_counts],
:group => @current_user.group_membership_visibility[:user_counts],
:section => @current_user.enrollment_visibility[:section_user_counts]
}
terms = options[:search].to_s.downcase.strip.split(/\s+/)
exclude = options[:exclude_ids] || []
result = []
if context_name.nil?
result = if terms.blank?
courses = @contexts[:courses].values
group_ids = @current_user.current_groups.map(&:id)
groups = @contexts[:groups].slice(*group_ids).values
courses + groups
else
@contexts.values_at(*options[:types].map{|t|t.to_s.pluralize.to_sym}).compact.map(&:values).flatten
end
elsif options[:synthetic_contexts]
if context_name =~ /\Acourse_(\d+)(_(groups|sections))?\z/ && (course = @contexts[:courses][$1.to_i]) && messageable_context_states[course[:state]]
course = Course.find_by_id(course[:id])
sections = @contexts[:sections].values.select{ |section| section[:parent] == {:course => course.id} }
groups = @contexts[:groups].values.select{ |group| group[:parent] == {:course => course.id} }
case context_name
when /\Acourse_\d+\z/
if terms.present? || options[:search_all_contexts] # search all groups and sections (and users)
result = sections + groups
else # otherwise we show synthetic contexts
result = synthetic_contexts_for(course, context_name)
result << {:id => "#{context_name}_sections", :name => t(:course_sections, "Course Sections"), :item_count => sections.size, :type => :context} if sections.size > 1
result << {:id => "#{context_name}_groups", :name => t(:student_groups, "Student Groups"), :item_count => groups.size, :type => :context} if groups.size > 0
return result
end
when /\Acourse_\d+_groups\z/
@skip_users = true # whether searching or just enumerating, we just want groups
result = groups
when /\Acourse_\d+_sections\z/
@skip_users = true # ditto
result = sections
end
elsif context_name =~ /\Asection_(\d+)\z/ && (section = @contexts[:sections][$1.to_i]) && messageable_context_states[section[:state]]
if terms.present? # we'll just search the users
result = []
else
section = CourseSection.find_by_id(section[:id])
return synthetic_contexts_for(section.course, context_name)
end
end
end
result = if options[:rank_results]
result.sort_by{ |context|
[
context_state_ranks[context[:state]],
context_type_ranks[context[:type]],
context[:name].downcase
]
}
else
result.sort_by{ |context| context[:name].downcase }
end
result = result.reject{ |context| context[:state] == :inactive } unless options[:include_inactive]
result = result.map{ |context|
ret = {
:id => "#{context[:type]}_#{context[:id]}",
:name => context[:name],
:avatar_url => avatar_url,
:type => :context,
:user_count => user_counts[context[:type]][context[:id]]
}
ret[:context_name] = context[:context_name] if context[:context_name] && context_name.nil?
ret
}
result.reject!{ |context| terms.any?{ |part| !context[:name].downcase.include?(part) } } if terms.present?
result.reject!{ |context| exclude.include?(context[:id]) }
offset = options[:offset] || 0
options[:limit] ? result[offset, offset + options[:limit]] : result
end
def synthetic_contexts_for(course, context)
@skip_users = true
# TODO: move the aggregation entirely into the DB. we only select a little
# bit of data per user, but this still isn't ideal
users = @current_user.messageable_users(:context => context)
enrollment_counts = {:all => users.size}
users.each do |user|
user.common_courses[course.id].uniq.each do |role|
enrollment_counts[role] ||= 0
enrollment_counts[role] += 1
end
end
avatar_url = avatar_url_for_group(blank_fallback)
result = []
result << {:id => "#{context}_teachers", :name => t(:enrollments_teachers, "Teachers"), :user_count => enrollment_counts['TeacherEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['TeacherEnrollment'].to_i > 0
result << {:id => "#{context}_tas", :name => t(:enrollments_tas, "Teaching Assistants"), :user_count => enrollment_counts['TaEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['TaEnrollment'].to_i > 0
result << {:id => "#{context}_students", :name => t(:enrollments_students, "Students"), :user_count => enrollment_counts['StudentEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['StudentEnrollment'].to_i > 0
result << {:id => "#{context}_observers", :name => t(:enrollments_observers, "Observers"), :user_count => enrollment_counts['ObserverEnrollment'], :avatar_url => avatar_url, :type => :context} if enrollment_counts['ObserverEnrollment'].to_i > 0
result
end
def context_state_ranks
{:active => 0, :recently_active => 1, :inactive => 2}
end
def context_type_ranks
{:course => 0, :section => 1, :group => 2}
end
def messageable_context_states
{:active => true, :recently_active => true, :inactive => false}
end
end

View File

@ -41,7 +41,7 @@ class UserNotesController < ApplicationController
@is_course = true
end
count = @users.count
@users = @users.scoped(:select=> "name, users.id, last_user_note", :order=>"last_user_note ASC, #{User.sortable_name_order_by_clause} ASC")
@users = @users.scoped(:select=> "name, users.id, last_user_note", :order=>"last_user_note ASC, #{User.sortable_name_order_by_clause} ASC", :extend => User::SortableNameExtension)
@users = @users.paginate(:page => params[:page], :per_page => 20, :total_entries=>count)
# rails gets confused by :include => :courses, because has_current_student_enrollments above references courses in a subquery
User.send(:preload_associations, @users, :courses)

View File

@ -0,0 +1,45 @@
module AvatarHelper
def set_avatar_size
@avatar_size = params[:avatar_size].to_i
@avatar_size = 50 unless [32, 50].include?(@avatar_size)
end
def avatar_size
@avatar_size || set_avatar_size
end
def avatar_url_for(conversation, participants = conversation.participants)
if participants.size == 1
avatar_url_for_user(participants.first)
elsif participants.size == 2
avatar_url_for_user(participants.select{ |u| u.id != conversation.user_id }.first)
else
avatar_url_for_group
end
end
def avatar_url_for_group(blank_fallback=false)
"#{request.protocol}#{request.host_with_port}" + (blank_fallback ?
"/images/blank.png" :
"/images/messages/avatar-group-#{avatar_size}.png"
)
end
def avatar_url_for_user(user, blank_fallback=false)
default_avatar = "#{request.protocol}#{request.host_with_port}" + (blank_fallback ?
"/images/blank.png" :
"/images/messages/avatar-#{avatar_size}.png"
)
if service_enabled?(:avatars)
avatar_image_url(User.avatar_key(user.id), :fallback => default_avatar)
else
default_avatar
end
end
def blank_fallback
params[:blank_avatar_fallback] || @blank_fallback
end
end

View File

@ -1,4 +1,5 @@
module ConversationsHelper
def contexts_for(audience, context_tags)
result = {:courses => {}, :groups => {}}
return result if audience.empty?
@ -18,32 +19,4 @@ module ConversationsHelper
result
end
def avatar_url_for(conversation, participants = conversation.participants)
if participants.size == 1
avatar_url_for_user(participants.first)
elsif participants.size == 2
avatar_url_for_user(participants.select{ |u| u.id != conversation.user_id }.first)
else
avatar_url_for_group
end
end
def avatar_url_for_group(blank_fallback=false)
"#{request.protocol}#{request.host_with_port}" + (blank_fallback ?
"/images/blank.png" :
"/images/messages/avatar-group-#{avatar_size}.png"
)
end
def avatar_url_for_user(user, blank_fallback=false)
default_avatar = "#{request.protocol}#{request.host_with_port}" + (blank_fallback ?
"/images/blank.png" :
"/images/messages/avatar-#{avatar_size}.png"
)
if service_enabled?(:avatars)
avatar_image_url(user.id, :fallback => default_avatar)
else
default_avatar
end
end
end

View File

@ -17,4 +17,91 @@
#
module SearchHelper
include AvatarHelper
def load_all_contexts
@contexts = Rails.cache.fetch(['all_conversation_contexts', @current_user].cache_key, :expires_in => 10.minutes) do
contexts = {:courses => {}, :groups => {}, :sections => {}}
term_for_course = lambda do |course|
course.enrollment_term.default_term? ? nil : course.enrollment_term.name
end
@current_user.concluded_courses.each do |course|
contexts[:courses][course.id] = {
:id => course.id,
:url => course_url(course),
:name => course.name,
:type => :course,
:term => term_for_course.call(course),
:state => course.recently_ended? ? :recently_active : :inactive,
:available => false,
:can_add_notes => can_add_notes_to?(course)
}
end
@current_user.courses.each do |course|
contexts[:courses][course.id] = {
:id => course.id,
:url => course_url(course),
:name => course.name,
:type => :course,
:term => term_for_course.call(course),
:state => :active,
:available => course.available?,
:can_add_notes => can_add_notes_to?(course)
}
end
section_ids = @current_user.enrollment_visibility[:section_user_counts].keys
CourseSection.find(:all, :conditions => {:id => section_ids}).each do |section|
contexts[:sections][section.id] = {
:id => section.id,
:name => section.name,
:type => :section,
:term => contexts[:courses][section.course_id][:term],
:state => contexts[:courses][section.course_id][:state],
:parent => {:course => section.course_id},
:context_name => contexts[:courses][section.course_id][:name]
}
end if section_ids.present?
@current_user.messageable_groups.each do |group|
contexts[:groups][group.id] = {
:id => group.id,
:name => group.name,
:type => :group,
:state => group.active? ? :active : :inactive,
:parent => group.context_type == 'Course' ? {:course => group.context.id} : nil,
:context_name => group.context.name
}
end
contexts
end
end
def jsonify_users(users, options = {})
options = {
:include_participant_avatars => true,
:include_participant_contexts => true
}.merge(options)
users.map { |user|
hash = {
:id => user.id,
:name => user.short_name
}
if options[:include_participant_contexts]
hash[:common_courses] = user.common_courses
hash[:common_groups] = user.common_groups
end
hash[:avatar_url] = avatar_url_for_user(user, blank_fallback) if options[:include_participant_avatars]
hash
}
end
def can_add_notes_to?(course)
course.enable_user_notes && course.grants_right?(@current_user, nil, :manage_user_notes)
end
end

View File

@ -109,7 +109,8 @@ class Course < ActiveRecord::Base
has_many :learning_outcome_groups, :as => :context
has_many :course_account_associations
has_many :non_unique_associated_accounts, :source => :account, :through => :course_account_associations, :order => 'course_account_associations.depth'
has_many :users, :through => :enrollments, :source => :user
has_many :users, :through => :enrollments, :source => :user, :uniq => true
has_many :current_users, :through => :current_enrollments, :source => :user, :uniq => true
has_many :group_categories, :as => :context, :conditions => ['deleted_at IS NULL']
has_many :all_group_categories, :class_name => 'GroupCategory', :as => :context
has_many :groups, :as => :context
@ -1187,7 +1188,7 @@ class Course < ActiveRecord::Base
# 'publish_final_grades'
self.recompute_student_scores_without_send_later
enrollments = self.student_enrollments.not_fake.scoped({:include => [:user, :course_section]}).find(:all, :order => User.sortable_name_order_by_clause('users'))
enrollments = self.student_enrollments.not_fake.scoped(:include => [:user, :course_section], :order => User.sortable_name_order_by_clause('users'), :extend => User::SortableNameExtension)
errors = []
posts_to_make = []
@ -1273,7 +1274,7 @@ class Course < ActiveRecord::Base
includes = [:user, :course_section]
includes = {:user => :pseudonyms, :course_section => []} if options[:include_sis_id]
scope = options[:user] ? self.enrollments_visible_to(options[:user]) : self.student_enrollments
student_enrollments = scope.scoped(:include => includes).find(:all, :order => User.sortable_name_order_by_clause('users'))
student_enrollments = scope.scoped(:include => includes, :order => User.sortable_name_order_by_clause('users'), :extend => User::SortableNameExtension)
# remove duplicate enrollments for students enrolled in multiple sections
seen_users = []
student_enrollments.reject! { |e| seen_users.include?(e.user_id) ? true : (seen_users << e.user_id; false) }
@ -1374,6 +1375,7 @@ class Course < ActiveRecord::Base
enrollment_state = opts[:enrollment_state]
section = opts[:section]
limit_privileges_to_course_section = opts[:limit_privileges_to_course_section]
associated_user_id = opts[:associated_user_id]
section ||= self.default_section
enrollment_state ||= self.available? ? "invited" : "creation_pending"
if type == 'TeacherEnrollment' || type == 'TaEnrollment' || type == 'DesignerEnrollment'
@ -1381,7 +1383,9 @@ class Course < ActiveRecord::Base
else
enrollment_state = 'creation_pending' if enrollment_state == 'invited' && !self.available?
end
if opts[:allow_multiple_enrollments]
if opts[:allow_multiple_enrollments] && associated_user_id
e = self.enrollments.find_by_user_id_and_type_and_course_section_id_and_associated_user_id(user.id, type, section.id, associated_user_id)
elsif opts[:allow_multiple_enrollments]
e = self.enrollments.find_by_user_id_and_type_and_course_section_id(user.id, type, section.id)
else
e = self.enrollments.find_by_user_id_and_type(user.id, type)
@ -1402,6 +1406,7 @@ class Course < ActiveRecord::Base
:workflow_state => enrollment_state,
:limit_privileges_to_course_section => limit_privileges_to_course_section)
end
e.associated_user_id = associated_user_id
if e.changed?
if opts[:no_notify]
e.save_without_broadcasting
@ -2405,7 +2410,19 @@ class Course < ActiveRecord::Base
when :full then scope
when :sections then scope.scoped(:conditions => ["enrollments.course_section_id IN (?) OR (enrollments.limit_privileges_to_course_section=? AND enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment'))", visibilities.map{|s| s[:course_section_id]}, false])
when :restricted then scope.scoped({:conditions => "enrollments.user_id IN (#{(visibilities.map{|s| s[:associated_user_id]}.compact + [user.id]).join(",")})"})
else scope.scoped({:conditions => "FALSE"})
else scope.scoped({:conditions => ["?", false]})
end
end
def users_visible_to(user, include_priors=false)
visibilities = section_visibilities_for(user)
scope = include_priors ? users : current_users
# See also Users#messageable_users (same logic used to get users across multiple courses)
case enrollment_visibility_level_for(user, visibilities)
when :full then scope
when :sections then scope.scoped({:conditions => "enrollments.course_section_id IN (#{visibilities.map{|s| s[:course_section_id]}.join(",")})"})
when :restricted then scope.scoped({:conditions => "enrollments.user_id IN (#{(visibilities.map{|s| s[:associated_user_id]}.compact + [user.id]).join(",")})"})
else scope.scoped({:conditions => ["?", false]})
end
end

View File

@ -23,6 +23,23 @@ class User < ActiveRecord::Base
best_unicode_collation_key(col)
end
module SortableNameExtension
# only works with scopes i.e. named_scopes and scoped()
def find(*args)
options = args.last.is_a?(::Hash) ? args.last : {}
scope = scope(:find)
select = if options[:select]
options[:select]
elsif scope[:select]
scope[:select]
else
"#{proxy_scope.quoted_table_name}.*"
end
options[:select] = select + ', ' + User.sortable_name_order_by_clause
super args.first, options
end
end
include Context
include UserFollow::FollowedItem
@ -53,7 +70,7 @@ class User < ActiveRecord::Base
has_many :invited_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => ENROLLMENT_CONDITIONS[:invited], :order => 'enrollments.created_at'
has_many :current_and_invited_enrollments, :class_name => 'Enrollment', :include => [:course], :order => 'enrollments.created_at',
:conditions => [ENROLLMENT_CONDITIONS[:active], ENROLLMENT_CONDITIONS[:invited]].join(' OR ')
has_many :not_ended_enrollments, :class_name => 'Enrollment', :conditions => ["enrollments.workflow_state NOT IN (?)", ['rejected', 'completed', 'deleted']]
has_many :not_ended_enrollments, :class_name => 'Enrollment', :conditions => ["enrollments.workflow_state NOT IN (?)", ['rejected', 'completed', 'deleted']], :order => 'enrollments.created_at'
has_many :concluded_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => ENROLLMENT_CONDITIONS[:completed], :order => 'enrollments.created_at'
has_many :observer_enrollments
has_many :observee_enrollments, :foreign_key => :associated_user_id, :class_name => 'ObserverEnrollment'
@ -185,7 +202,7 @@ class User < ActiveRecord::Base
named_scope :has_current_student_enrollments, :conditions => "EXISTS (SELECT * FROM enrollments JOIN courses ON courses.id = enrollments.course_id AND courses.workflow_state = 'available' WHERE enrollments.user_id = users.id AND enrollments.workflow_state IN ('active','invited') AND enrollments.type = 'StudentEnrollment')"
def self.order_by_sortable_name
scoped(:order => sortable_name_order_by_clause)
scoped(:order => sortable_name_order_by_clause, :extend => SortableNameExtension)
end
named_scope :enrolled_in_course_between, lambda{|course_ids, start_at, end_at|

View File

@ -44,6 +44,8 @@ $blue-box-background-bottom: #238edc
// this is the blue color that we use all over the place when not in a gradiants
$highlight-color: #06a9ee
// light blue hover color
$hover-color: #E5F3FF
// this is the grey color for when things are disabled, or meant to contrast with $highlight-color
$diminutive-color: #acacac

View File

@ -13,24 +13,56 @@
#course_form .date_entry
width: 100px
.users-wrapper
border: 1px solid #CCC
margin-bottom: 1.4em
h3
margin: 0
padding: 8px
background-color: #F7F7F7
border-bottom: 1px solid #CCC
.pagination-loading
padding: 8px
display: block
text-align: center
text-decoration: none
background-color: #06A9EE
color: white
ul.user_list
list-style: none
padding-left: 0px
margin-top: 0px
margin-bottom: 20px
padding: 0px
margin: 0
max-height: 300px
min-height: 50px
overflow: auto
li.none
padding: 8px
ul.user_list li.user
min-height: 30px
padding-left: 10px
color: #444
line-height: 1.0em
-moz-border-radius: 5px
padding: 3px 0 3px 3px
margin-bottom: 2px
padding: 8px
margin: 0
border-bottom: 1px solid #CCC
&:last-child
border-bottom: 0
> span.pending
width: 0
height: 0
padding: 0
margin: -8px 0 0 -8px
border-bottom: 10px solid transparent
border-left: 10px solid #06A9EE
display: block
position: relative
ul.user_list li.user:hover
background-color: #eee
background-color: $hover-color
ul.user_list li.user .email
font-size: 0.8em
@ -73,7 +105,7 @@ ul.user_list li.user:hover .links
padding-right: 20px
visibility: visible
ul.user_list li.user.pending
ul.user_list div.enrollment_type.pending
color: #888
font-style: italic
@ -82,6 +114,25 @@ h3 .tally
font-size: 12px
padding-left: 5px
.add-course-users
border: 1px solid #CCC
margin-bottom: 1.4em
.header
padding: 8px
border-bottom: 1px solid #CCC
background-color: #F7F7F7
.id-holder
float: left
padding-right: 1em
#user_list_boxes
overflow: hidden
margin: 8px
#user_list_textarea_container
height: auto
#user_list_parsed
height: auto
width: 98%
#sections
+reset-list
max-width: 500px
@ -152,4 +203,236 @@ h3 .tally
font-size: 0.8em
padding-left: 10px
line-height: 0.7em
font-style: italic
font-style: italic
.autocomplete_menu
position: absolute
z-index: 2000
overflow: hidden
margin-left: -12px
padding: 0 12px 12px
> div
background: #ebebeb
border: 1px solid #999999
+border-radius(0 0 10px 10px)
box-shadow 0 0 12px 3px rgba(0,0,0,0.15)
overflow: hidden
width: 341px
> div // gets shifted left/right as menu slides
position: relative
> div // each menu level (contains two uls)
float: left
width: 341px
ul.heading
margin: 0
overflow: visible
ul
list-style: none
padding: 0
margin: 0 0 10px
overflow: auto
max-height: 218px
position: relative
li
position: relative
overflow: hidden
border-top: 1px solid #fff
border-bottom: 1px solid #d4d5d7
padding: 5px
li.first
border-top: none
li.last
border-bottom: none
li.expanded
cursor: default
background: #fff
position: relative
z-index: 1
box-shadow 0 0 4px 2px rgba(0,0,0,0.1)
li.message
line-height: 32px
text-align: center
border-bottom: none
li.active
border-top-color: #d7eefb
border-bottom-color: #297fd1
background: #2da5f0 url(/images/messages/finder-active.png) repeat-x 0 0
li.active.expanded
background: #f4fbff
border-top-color: #fff
border-bottom-color: #d4d5d7
li, li.active.expanded
height: 32px
cursor: pointer
a
position: relative
padding: 1px
background: transparent
i
display: block
width: 30px
height: 30px
a.expand
float: right
i
background: transparent url(/images/messages/expand-context.png) no-repeat 10px -23px
a.toggle
float: left
margin: 0 14px 0 6px
i
width: 16px
height: 16px
margin: 7px
img.avatar
width: 32px
height: 32px
float: left
margin-right: 6px
background: transparent url(/images/messages/avatar-sprites.png) 0 0 no-repeat
span.name, span.details
white-space: nowrap
overflow: hidden
display: block
b
color: #000
text-shadow: none
span.details, span.context_info
color: #2571bd
font-weight: bold
font-size: 0.8em
span.details
display: block
span.context_info
padding-left: 6px
li.toggleable a.toggle i
background: transparent url(/images/messages/checkbox-sprite.png) no-repeat 0 0
li.context img.avatar, li.context.active.expanded img.avatar
background-position: 0 -64px
li.user.active img.avatar
background-position: 0 -32px
li.context.active img.avatar
background-position: 0 -96px
li.active
a.expand
i
background-position: 10px 10px
a.expand:hover
padding: 0
border: 1px solid #2da5f0
background: #2da5f0 url(/images/messages/finder-active.png) repeat-x 0 0
b
color: #fff
text-shadow: 0px 1px 1px #0587bb
span
color: #0d4276
a.toggle i
background-position: 0 -48px
img.user.avatar
background-position: 0 -32px
img.context.avatar
background-position: 0 -96px
li.toggleable.on a.toggle i
background-position: 0 -32px
li.toggleable.on.active a.toggle i
background-position: 0 -80px
li.toggleable.on.active a.toggle:hover i
background-position: 0 -64px
li.expanded, li.active.expanded
cursor: default
a.expand
i
background-position: 10px -55px
a.toggle
display: none
li.active.expanded
a.expand:hover
cursor: pointer
background: #fff
border-color: #e4ebef
&.with-toggles
li.expanded, li.active.expanded
padding-left: 11px
img.avatar
margin: 0 14px 0 0
.token_input
width: 100%
line-height: 1.1em
min-height: 20px
display: -moz-inline-box
display: inline-block
border: 1px solid #999999
border-top-color: #737373
padding: 0
background: #fff
box-sizing: border-box
-webkit-box-sizing: border-box
-moz-box-sizing: border-box
cursor: text
position: relative
a.browser
position: absolute
top: 0
right: 0
cursor: pointer
overflow: hidden
height: 18px
width: 18px
text-indent: -9999em
background: transparent url(/images/messages/address-book-icon-sprite.png) 0 0 no-repeat
a.browser:hover
background-position: 0 -18px
> span
float: left
color: #888
margin: 2px 1px 0
> div
overflow: auto
max-height: 90px
&.browsable > div
padding-right: 20px
ul
list-style: none
margin: 0
padding: 0
li
+name_bubbles
li.selected
background-color: #5b89f3
border-color: #5b89f3
color: #fff
a
background-position: -10px center
li.details
div
padding: 0 11px
span
padding: 0 15px 0 4px
input
-webkit-box-shadow: none
-moz-box-shadow: none
box-shadow: none
float: left
border: 0
outline: none
padding: 0
margin: 1px 0
.token_input.browse
a.browser
background-position: 0 -36px
.token_input.active
box-shadow: #68B4DF 0 0 3px 2px
#user_sections
list-style: none
margin: 0
padding: 0
li
+name_bubbles
clear: left
#edit_sections
a.browser
background: transparent url(/images/messages/context-search-sprite.png) 0 0 no-repeat

View File

@ -1135,6 +1135,7 @@ html > body .new_activity_message form textarea
#user_list_boxes
position: relative
height: 152px
margin-top: 10px
#user_list_textarea_container, #user_list_processing, #user_list_parsed
height: 152px
width: 100%
@ -1148,15 +1149,30 @@ html > body .new_activity_message form textarea
overflow: auto
height: 126px
.ui-widget
margin: 2px
margin: 0 8px 8px 0
.person
float: left
margin: 2px
background: #FFF
margin: 0 8px 8px 0
padding: 8px
font-size: 11px
height: 33px
padding: 0 4px
height: 20px
-webkit-box-shadow: inset 33px 0 0 #F7F7F7, inset 34px 0 0 #CCC
-moz-box-shadow: inset 33px 0 0 #F7F7F7, inset 34px 0 0 #CCC
box-shadow: inset 33px 0 0 #F7F7F7, inset 34px 0 0 #CCC
div
margin: 2px 0 0 33px
.address, .login
font-weight: normal
span
height: 20px
width: 20px
float: left
background: #F7F7F7 url(/images/circle-plus.png) no-repeat center center
&.existing-user
span
background: #F7F7F7 url(/images/circle-check.png) no-repeat center center
body > #ui-datepicker-div
display: none

View File

@ -10,7 +10,7 @@ sample markup:
<li><a href="#"><span class="ui-icon ui-icon-pencil" />Edit</a></li>
<li><a href="#"><span class="ui-icon ui-icon-trash" />Delete (from database)</a></li>
<li><a href="#"><span class="ui-icon ui-icon-close" />Hide from this screen</a></li>
<li><a href="#">an option with no icon/a></li>
<li><a href="#">an option with no icon</a></li>
</ul>
</div>
put all of the content here

View File

@ -47,7 +47,7 @@
<label for="context_tags">
<%= before_label :filter, "Filter" %>
</label>
<%= text_field_tag :context_tags, nil, 'data-finder_url' => conversations_find_recipients_url %>
<%= text_field_tag :context_tags, nil, 'data-finder_url' => search_recipients_url %>
</div>
<div class="clear"></div>
</div>
@ -70,7 +70,7 @@
</li>
</ul>
<table>
<tr id="recipient_info"><th><%= label_tag :recipients, :to, :en => "To", :before => true %></th><td><%= text_field_tag :recipients, nil, 'data-finder_url' => conversations_find_recipients_url, :class => "recipients" %></td></tr>
<tr id="recipient_info"><th><%= label_tag :recipients, :to, :en => "To", :before => true %></th><td><%= text_field_tag :recipients, nil, 'data-finder_url' => search_recipients_url, :class => "recipients" %></td></tr>
<tr><th><%= label_tag :body, :message, :en => "Message", :before => true %></th><td><%= text_area_tag :body %></td></tr>
<tr id="group_conversation_info"><th></th><td><%= check_box_tag :group_conversation %> <%= label_tag :group_conversation, :en => "This is a group conversation. Participants will see everyone's replies" %></td></tr>
<tr id="user_note_info"><th></th><td><%= check_box_tag :user_note, "1", false, :id => :add_to_faculty_journal %> <%= label_tag :add_to_faculty_journal, :en => "Add as a Faculty Journal entry" %></td></tr>
@ -189,12 +189,12 @@
<p>
<%= t :add_recipients_instructions, "People you add to the conversation will see all previous messages." %>
</p>
<%= text_field_tag :recipients, nil, :id => 'add_recipients', 'data-finder_url' => conversations_find_recipients_url, :class => "recipients", :style => "width: 370px" %>
<%= text_field_tag :recipients, nil, :id => 'add_recipients', 'data-finder_url' => search_recipients_url, :class => "recipients", :style => "width: 370px" %>
</form>
<% form_tag conversations_url, :id => 'forward_message_form' do %>
<table>
<tr id="recipient_info"><th><%= label_tag :forward_recipients, :to, :en => "To", :before => true %></th><td><%= text_field_tag :recipients, nil, :id => :forward_recipients, 'data-finder_url' => conversations_find_recipients_url, :class => "recipients" %></td></tr>
<tr id="recipient_info"><th><%= label_tag :forward_recipients, :to, :en => "To", :before => true %></th><td><%= text_field_tag :recipients, nil, :id => :forward_recipients, 'data-finder_url' => search_recipients_url, :class => "recipients" %></td></tr>
<tr><th><%= label_tag :body, :message, :en => "Message", :before => true %></th><td><%= text_area_tag :body, nil, :id => :forward_body %></td></tr>
</table>
<ul class="messages"></ul>

View File

@ -4,22 +4,7 @@
js_bundle :course_settings
@active_tab = "settings"
enrollment_hashes = {}
['StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'ObserverEnrollment', 'DesignerEnrollment'].each do |type|
enrollment_hashes[type] = {}
list = @context.detailed_enrollments.select{|e| e.type == type && e.active_or_pending?}.sort_by{|e| e.user.sortable_name.downcase rescue 'a' }
if type == 'StudentEnrollment' && @context.visibility_limited_to_course_sections?(@current_user)
user_ids = @context.students_visible_to(@current_user).map(&:id)
list = list.select{|e| user_ids.include?(e.user_id) }
end
list.each{|e| enrollment_hashes[type][e.user] ||= []; enrollment_hashes[type][e.user] << e }
end
can_rename_course = can_do(@context.account, @current_user, :manage_courses) || !@context.root_account.settings[:prevent_course_renaming_by_teachers]
students = enrollment_hashes['StudentEnrollment'].map{|k, v| v[0]}
teachers = enrollment_hashes['TeacherEnrollment'].map{|k, v| v[0]}
tas = enrollment_hashes['TaEnrollment'].map{|k, v| v[0]}
observers = enrollment_hashes['ObserverEnrollment'].map{|k, v| v[0]}
designers = enrollment_hashes['DesignerEnrollment'].map{|k, v| v[0]}
has_multiple_sections = @context.course_sections.active.count > 1
publishing_enabled = @context.allows_grade_publishing_by(@current_user) && can_do(@context, @current_user, :manage_grades)
@ -80,23 +65,23 @@
</thead>
<tr>
<td><%= before_label('students', %{Students}) %></td>
<td class="student_count"><%= students.empty? ? t('none', "None") : students.length %></td>
<td class="student_count"><%= @user_counts[:student] == 0 ? t('none', "None") : @user_counts[:student] %></td>
</tr>
<tr>
<td><%= before_label('teachers', %{Teachers}) %></td>
<td class="teacher_count"><%= teachers.empty? ? t('none', "None") : teachers.length %></td>
<td class="teacher_count"><%= @user_counts[:teacher] ? t('none', "None") : @user_counts[:teacher] %></td>
</tr>
<tr>
<td><%= before_label('tas', %{TAs}) %></td>
<td class="ta_count"><%= tas.empty? ? t('none', "None") : tas.length %></td>
<td class="ta_count"><%= @user_counts[:ta] ? t('none', "None") : @user_counts[:ta] %></td>
</tr>
<tr>
<td><%= before_label('observers', %{Observers}) %></td>
<td class="observer_count"><%= observers.empty? ? t('none', "None") : observers.length %></td>
<td class="observer_count"><%= @user_counts[:observer] ? t('none', "None") : @user_counts[:observer] %></td>
</tr>
<tr>
<td><%= before_label('designers', %{Designers}) %></td>
<td class="designer_count"><%= designers.empty? ? t('none', "None") : designers.length %></td>
<td class="designer_count"><%= @user_counts[:designer] ? t('none', "None") : @user_counts[:designer] %></td>
</tr>
</table>
</div>
@ -474,8 +459,8 @@ Hashtags should consist of letters, numbers, dashes and underscores (no spaces).
<% if can_see_users %>
<div id="tab-users">
<h2 style="margin-top: 10px;"><%= t('headings.current_users', %{Current Users}) %></h2>
<div style="margin-bottom: 10px; <%= hidden if (students + teachers + tas + observers).select{|e| e.invited? }.empty? %>">
<%= t 'unaccepted_invitations', "%{count} Invitations haven't been accepted. You can click a user's name to re-send their invitation", :count => %[<span class="unaccepted_invitations">#{(students + teachers + tas + observers).select{|e| e.invited? }.length}</span>].html_safe %>
<div style="margin-bottom: 10px; <%= hidden if @user_counts[:invited] == 0 %>">
<%= t 'unaccepted_invitations', "%{count} Invitations haven't been accepted. You can click a user's name to re-send their invitation", :count => %[<span class="unaccepted_invitations">#{@user_counts[:invited]}</span>].html_safe %>
<% if @context.available? %>
<% ot 're_send_all', 'or *Re-Send All Unaccepted Invitations*', :wrapper => capture { %><div><span><button type="button" href="<%= course_re_send_invitations_url %>" class="button re_send_invitations_link">\1</button></span>
@ -485,49 +470,28 @@ Hashtags should consist of letters, numbers, dashes and underscores (no spaces).
<table style="width: 100%;">
<tr>
<td style="vertical-align: top; padding-right: 30px;">
<h3><%= t 'headings.students', 'Students' %> <span class="tally">(<span class="student_count"><%= students.length %></span>)</span></h3>
<ul class="user_list student_enrollments">
<% if students.empty? %>
<li class="none"><%= t('no_students', %{No Students Enrolled}) %></li>
<% else %>
<%= render :partial => 'shared/enrollment', :collection => enrollment_hashes['StudentEnrollment'].to_a.sort_by{|e| e[0].sortable_name.downcase rescue 'a' }, :locals => {:show_section => has_multiple_sections, :show_information_link => true} %>
<% end %>
<%= render :partial => 'shared/enrollment', :object => nil, :locals => {:show_information_link => true} %>
</ul>
<h3><%= t('headings.observers', 'Observers') %> <span class="tally">(<span class="observer_count"><%= observers.length %></span>)</span></h3>
<ul class="user_list observer_enrollments">
<% if observers.empty? %>
<li class="none"><%= t('no_observers', %{No Observers Enrolled}) %></li>
<% else %>
<%= render :partial => 'shared/enrollment', :collection => enrollment_hashes['ObserverEnrollment'].to_a.sort_by{|e| e[0].sortable_name.downcase rescue 'a' }, :locals => {:show_information_link => true} %>
<% end %>
</ul>
<div class="users-wrapper">
<h3><%= t 'headings.students', 'Students' %> <span class="tally">(<span class="student_count"><%= @user_counts[:student] %></span>)</span></h3>
<ul id="student_enrollments" class="user_list student_enrollments"></ul>
</div>
<div class="users-wrapper">
<h3><%= t('headings.observers', 'Observers') %> <span class="tally">(<span class="observer_count"><%= @user_counts[:observer] %></span>)</span></h3>
<ul id="observer_enrollments" class="user_list observer_enrollments"></ul>
</div>
</td>
<td style="vertical-align: top;">
<h3><%= t('headings.teachers', %{Teachers}) %> <span class="tally">(<span class="teacher_count"><%= teachers.length %></span>)</span></h3>
<ul class="user_list teacher_enrollments">
<% if teachers.empty? %>
<li class="none"><%= t('no_teachers', %{No Teachers Assigned}) %></li>
<% else %>
<%= render :partial => 'shared/enrollment', :collection => enrollment_hashes['TeacherEnrollment'].to_a.sort_by{|e| e[0].sortable_name.downcase rescue 'a' }, :locals => {:show_section => has_multiple_sections, :show_information_link => true} %>
<% end %>
</ul>
<h3><%= t('headings.designers', %{Designers}) %> <span class="tally">(<span class="designer_count"><%= designers.length %></span>)</span></h3>
<ul class="user_list designer_enrollments">
<% if designers.empty? %>
<li class="none"><%= t('no_designers', %{No Designers Assigned}) %></li>
<% else %>
<%= render :partial => 'shared/enrollment', :collection => enrollment_hashes['DesignerEnrollment'].to_a.sort_by{|e| e[0].sortable_name.downcase rescue 'a' }, :locals => {:show_section => has_multiple_sections, :show_information_link => true} %>
<% end %>
</ul>
<h3><%= t('headings.tas', %{TAs}) %> <span class="tally">(<span class="ta_count"><%= tas.length %></span>)</span></h3>
<ul class="user_list ta_enrollments">
<% if tas.empty? %>
<li class="none"><%= t('no_tas', %{No TAs Assigned}) %></li>
<% else %>
<%= render :partial => 'shared/enrollment', :collection => enrollment_hashes['TaEnrollment'].to_a.sort_by{|e| e[0].sortable_name.downcase rescue 'a' }, :locals => {:show_section => has_multiple_sections, :show_information_link => true} %>
<% end %>
</ul>
<div class="users-wrapper">
<h3><%= t('headings.teachers', %{Teachers}) %> <span class="tally">(<span class="teacher_count"><%= @user_counts[:teacher] %></span>)</span></h3>
<ul id="teacher_enrollments" class="user_list teacher_enrollments"></ul>
</div>
<div class="users-wrapper">
<h3><%= t('headings.designers', %{Designers}) %> <span class="tally">(<span class="designer_count"><%= @user_counts[:designer] %></span>)</span></h3>
<ul id="designer_enrollments" class="user_list designer_enrollments"></ul>
</div>
<div class="users-wrapper">
<h3><%= t('headings.tas', %{TAs}) %> <span class="tally">(<span class="ta_count"><%= @user_counts[:ta] %></span>)</span></h3>
<ul id="ta_enrollments" class="user_list ta_enrollments"></ul>
</div>
</td>
</tr>
</table>
@ -536,39 +500,41 @@ Hashtags should consist of letters, numbers, dashes and underscores (no spaces).
<% end %>
<% form_tag course_enroll_users_url(@context), {:id => "enroll_users_form", :style => "display: none;"} do %>
<h2><%= t('headings.add_users', %{Add Course Users}) %></h2>
<div style="margin-top: 5px;">
<div>
<%= label_tag :enrollment_type, :en => "Add More" %>
<select name="enrollment_type" title="<%= t('titles.enrollment_type', 'Enrollment Type') %>" id="enrollment_type">
<% if can_do(@context, @current_user, :manage_students) %>
<option value="StudentEnrollment"><%= t('option.students', %{Students}) %></option>
<% end %>
<% if can_do(@context, @current_user, :manage_admin_users) %>
<option value="TeacherEnrollment"><%= t('option.teachers', %{Teachers}) %></option>
<option value="TaEnrollment"><%= t('option.tas', %{TAs}) %></option>
<% elsif @context.teacherless? %>
<option value="TeacherEnrollment" class="teacherless_invite"><%= t('option.teachers', %{Teachers}) %></option>
<% end %>
<option value="ObserverEnrollment"><%= t('option.observers', %{Observers}) %></option>
<option value="DesignerEnrollment"><%= t('option.designers', %{Designers}) %></option>
</select>
</div>
<div style="<%= hidden unless has_multiple_sections %>" id="course_section_id_holder">
<%= label_tag :course_section_id, :en => 'For the section' %>
<select title="<%= t('titles.course_section', 'Course Section') %>" name="course_section_id" id="course_section_id">
<% @context.course_sections.active.each do |section| %>
<option value="<%= section.id %>" class="option_for_section_<%= section.id %>"><%= section.display_name %></option>
<% end %>
</select>
</div>
<div>
<span id="limit_privileges_to_course_section_holder" style="padding-left: 10px; white-space: nowrap;">
<input type="checkbox" id="limit_privileges_to_course_section" name="limit_privileges_to_course_section" value="1"/>
<%= label_tag :limit_privileges_to_course_section, :en => 'these admins can only grade students in their section' %>
</span>
</div>
<div class="teacherless_invite_message" style="<%= hidden unless @context.teacherless? %> font-size: 0.8em;">
<%= t('teacherless_invite_details', %{If you invite a teacher and they accept, you will give up administrative privileges for this course.}) %>
<div class="add-course-users">
<div class="header">
<div class="id-holder">
<%= label_tag :enrollment_type, :en => "Add More" %>
<select name="enrollment_type" title="<%= t('titles.enrollment_type', 'Enrollment Type') %>" id="enrollment_type">
<% if can_do(@context, @current_user, :manage_students) %>
<option value="StudentEnrollment"><%= t('option.students', %{Students}) %></option>
<% end %>
<% if can_do(@context, @current_user, :manage_admin_users) %>
<option value="TeacherEnrollment"><%= t('option.teachers', %{Teachers}) %></option>
<option value="TaEnrollment"><%= t('option.tas', %{TAs}) %></option>
<% elsif @context.teacherless? %>
<option value="TeacherEnrollment" class="teacherless_invite"><%= t('option.teachers', %{Teachers}) %></option>
<% end %>
<option value="ObserverEnrollment"><%= t('option.observers', %{Observers}) %></option>
<option value="DesignerEnrollment"><%= t('option.designers', %{Designers}) %></option>
</select>
</div>
<div class="id-holder" style="<%= hidden unless has_multiple_sections %>" id="course_section_id_holder">
<%= label_tag :course_section_id, :en => 'For the section' %>
<select title="<%= t('titles.course_section', 'Course Section') %>" name="course_section_id" id="course_section_id">
<% @context.course_sections.active.each do |section| %>
<option value="<%= section.id %>" class="option_for_section_<%= section.id %>"><%= section.display_name %></option>
<% end %>
</select>
</div>
<div style="clear:both">
<span id="limit_privileges_to_course_section_holder" style="padding-left: 10px; white-space: nowrap;">
<input type="checkbox" id="limit_privileges_to_course_section" name="limit_privileges_to_course_section" value="1"/>
<%= label_tag :limit_privileges_to_course_section, :en => 'these admins can only grade students in their section' %>
</span>
</div>
<div class="teacherless_invite_message" style="<%= hidden unless @context.teacherless? %> font-size: 0.8em;">
<%= t('teacherless_invite_details', %{If you invite a teacher and they accept, you will give up administrative privileges for this course.}) %>
</div>
</div>
<%= render :partial => 'shared/user_lists' %>
<% unless @context.available? %>
@ -584,38 +550,6 @@ Hashtags should consist of letters, numbers, dashes and underscores (no spaces).
<button type="button" class="cancel_button button-secondary"><%= t('#buttons.cancel', %{Cancel}) %></button>
</div>
<% end %>
<div style="text-align: center; display: none;" id="enrollment_dialog">
<% user_snippet = capture { %><span class="name"></span><% } %>
<% time_snippet = capture { %><div style="margin: 10px;" class="invitation_sent_at"></div><% } %>
<div class="admin_enrollment_re_send">
<%= t 'admin_invitation_unaccepted', "%{user} hasn't yet accepted the invitation to join the course. The invitation was sent: %{time}", :user => user_snippet, :time => time_snippet %>
</div>
<div class="student_enrollment_re_send">
<% if @context.available? %>
<%= t 'student_invitation_unaccepted_available', "%{user} hasn't yet accepted the invitation to join the course. The invitation was sent: %{time}", :user => user_snippet, :time => time_snippet %>
<% else %>
<%= t 'student_invitation_unaccepted_unavailable', "%{user} was added to the course: %{time}", :user => user_snippet, :time => time_snippet %>
<% end %>
</div>
<div class="accepted_enrollment_re_send">
<%= t 'accepted_invitation', "%{user} has already received and accepted the invitation to join the course, but you can still re-send the invitation if they need it.", :user => user_snippet %>
</div>
<div style="margin: 15px 10px; font-weight: bold;">
<div class="admin_enrollment_re_send">
<a href="#" class="re_send_invitation_link button"><%= t('links.re_send_invitation', %{Re-Send Invitation}) %></a>
</div>
<div class="accepted_enrollment_re_send">
<a href="#" class="re_send_invitation_link button"><%= t('links.re_send_invitation', %{Re-Send Invitation}) %></a>
</div>
<div class="student_enrollment_re_send">
<% if @context.available? %>
<a href="#" class="re_send_invitation_link button"><%= t('links.re_send_invitation', %{Re-Send Invitation}) %></a>
<% else %>
<%= t 'invitations_pending_publish', "Invitations aren't sent until the course is *published*", :wrapper => %{<a href="#{context_url(@context, :context_url)}">\\1</a>} %>
<% end %>
</div>
</div>
</div>
<%= render :partial => 'link_enrollment' %>
</div>
<% end %>

View File

@ -0,0 +1,5 @@
<p>{{#t "edit_sections_desc"}}Sections are an additional way to organize users. This can allow you to teach multiple classes from the same course, so that you can have the course content all in one place. Below you can move a user to a different section, or add/remove section enrollments. Users must be in at least one section at all times.{{/t}}</p>
<input id="section_input" name="sections" data-finder_url="{{sectionsUrl}}" type="text" style="display:none;">
<ul id="user_sections"></ul>

View File

@ -0,0 +1,18 @@
<div class="admin_enrollment_re_send">
{{#t "admin_invitation_unaccepted"}}{{name}} hasn't yet accepted the invitation to join the course. The invitation was sent: {{time}}{{/t}}
</div>
<div class="student_enrollment_re_send">
{{#if course.available}}
{{#t "student_invitation_unaccepted_available"}}{{name}} hasn't yet accepted the invitation to join the course. The invitation was sent: {{time}}{{/t}}
{{else}}
{{#t "student_invitation_unaccepted_unavailable"}}{{name}} was added to the course: {{time}}{{/t}}
{{/if}}
{{#unless course.available}}
<div class="invitations_pending_publish">
{{#t "invitations_pending_publish"}}Invitations aren't sent until the course is <b>published</b>{{/t}}
</div>
{{/unless}}
</div>
<div class="accepted_enrollment_re_send">
{{#t "accepted_invitation"}}{{name}} has already received and accepted the invitation to join the course, but you can still re-send the invitation if it is needed.{{/t}}
</div>

View File

@ -0,0 +1,5 @@
<p>{{#t "link_student_desc"}}When an observer is linked to a student, they have access to that student's grades and course interactions.{{/t}}</p>
<p>{{#t "link_student_action"}}To link the course observer <b>{{name}}</b> to a student, select the student's name from the list below.{{/t}}</p>
<input id="student_input" name="students" data-finder_url="{{studentsUrl}}" type="text" style="display:none;">

View File

@ -0,0 +1,32 @@
<div class="admin-links">
<button class="al-trigger"><span class="al-trigger-inner">{{#t "manage"}}Manage{{/t}}</span></button>
<ul class="al-options">
<li><a href="#" data-event="resendInvitation"><span class="ui-icon ui-icon-mail-closed" />{{#t "links.resend_invitation"}}Resend Invitation{{/t}}</a></li>
{{#if isObserver}}
<li><a href="#" data-event="linkToStudents"><span class="ui-icon ui-icon-link" />{{#t "links.link_to_students"}}Link to Students{{/t}}</a></li>
{{else}}
<li><a href="#" data-event="editSections"><span class="ui-icon ui-icon-pencil" />{{#t "links.edit_sections"}}Edit Sections{{/t}}</a></li>
{{/if}}
<li><a href="{{url}}"><span class="ui-icon ui-icon-person" />{{#t "links.user_details"}}User Details{{/t}}</a></li>
{{#if permissions.manage_students}}
<li class="ui-menu-item"><hr /></li>
<li><a href="#" data-event="removeFromCourse"><span class="ui-icon ui-icon-trash" />{{#t "links.remove_from_course"}}Remove From Course{{/t}}</a></li>
{{/if}}
</ul>
</div>
{{#if isPending}}
<span class="pending"></span>
{{/if}}
<a href="{{url}}" class="name">{{sortable_name}}</a>
<div class="short_name">{{short_name}}</div>
<div class="email">{{email}}</div>
<div class="sections">
{{#enrollments}}
<div class="section enrollment_type {{#if pending}}pending{{/if}}">{{{sectionTitle}}}</div>
{{/enrollments}}
</div>
<span class="clear"></span>

View File

@ -12,7 +12,7 @@
<% end %></textarea>
</div>
<div id="user_list_parsed" style="z-index: 1; display:none;">
<div class="ui-helper-reset ui-widget-content ui-corner-all" id="user_lists_processed_people">
<div class="ui-helper-reset" id="user_lists_processed_people">
<div id="user_list_duplicates_found" class="ui-widget">
<div class="ui-state-highlight ui-corner-all">
<p>
@ -38,10 +38,9 @@
</div>
</div>
<div class="person ui-helper-reset ui-state-default ui-corner-all" id="user_lists_processed_person_template" style="display:none;">
<div class="name"></div>
<div class="person ui-helper-reset ui-state-default ui-corner-all" id="user_lists_processed_person_template" title="<%= t 'titles.new_user','New user' %>" style="display:none;">
<span class="ui-icon"></span>
<div class="address"></div>
<div class="login"></div>
</div>
</div>
</div>

View File

@ -12,7 +12,8 @@ ActionController::Routing::Routes.draw do |map|
map.conversations_starred 'conversations/starred', :controller => 'conversations', :action => 'index', :redirect_scope => 'starred'
map.conversations_sent 'conversations/sent', :controller => 'conversations', :action => 'index', :redirect_scope => 'sent'
map.conversations_archived 'conversations/archived', :controller => 'conversations', :action => 'index', :redirect_scope => 'archived'
map.conversations_find_recipients 'conversations/find_recipients', :controller => 'conversations', :action => 'find_recipients'
map.connect 'conversations/find_recipients', :controller => 'search', :action => 'recipients' # use search_recipients_url instead
map.search_recipients 'search/recipients', :controller => 'search', :action => 'recipients'
map.conversations_mark_all_as_read 'conversations/mark_all_as_read', :controller => 'conversations', :action => 'mark_all_as_read', :conditions => {:method => :post}
map.conversations_watched_intro 'conversations/watched_intro', :controller => 'conversations', :action => 'watched_intro', :conditions => {:method => :post}
map.resources :conversations, :only => [:index, :show, :update, :create, :destroy] do |conversation|
@ -660,6 +661,7 @@ ActionController::Routing::Routes.draw do |map|
courses.get 'courses/:id', :action => :show
courses.get 'courses/:course_id/students', :action => :students
courses.get 'courses/:course_id/users', :action => :users, :path_name => 'course_users'
courses.get 'courses/:course_id/users/:id', :action => :user, :path_name => 'course_user'
courses.get 'courses/:course_id/activity_stream', :action => :activity_stream, :path_name => 'course_activity_stream'
courses.get 'courses/:course_id/todo', :action => :todo_items
courses.delete 'courses/:id', :action => :destroy
@ -825,7 +827,6 @@ ActionController::Routing::Routes.draw do |map|
api.with_options(:controller => :conversations) do |conversations|
conversations.get 'conversations', :action => :index
conversations.post 'conversations', :action => :create
conversations.get 'conversations/find_recipients', :action => :find_recipients
conversations.post 'conversations/mark_all_as_read', :action => :mark_all_as_read
conversations.get 'conversations/:id', :action => :show
conversations.put 'conversations/:id', :action => :update # stars, subscribed-ness, workflow_state
@ -899,6 +900,11 @@ ActionController::Routing::Routes.draw do |map|
end
end
api.with_options(:controller => :search) do |search|
search.get 'search/rubrics', :action => 'rubrics', :path_name => 'search_rubrics'
search.get 'search/recipients', :action => 'recipients', :path_name => 'search_recipients'
end
api.post 'files/:id/create_success', :controller => :files, :action => :api_create_success, :path_name => 'files_create_success'
api.get 'files/:id/create_success', :controller => :files, :action => :api_create_success, :path_name => 'files_create_success'

View File

@ -19,6 +19,7 @@
# includes Enrollment json helpers
module Api::V1::User
include Api::V1::Json
include AvatarHelper
API_USER_JSON_OPTS = {
:only => %w(id name email),
@ -42,7 +43,7 @@ module Api::V1::User
end
end
if service_enabled?(:avatars) && includes.include?('avatar_url')
json[:avatar_url] = avatar_image_url(User.avatar_key(user.id))
json[:avatar_url] = avatar_url_for_user(user, blank_fallback)
end
if enrollments
json[:enrollments] = enrollments.map { |e| enrollment_json(e, current_user, session, includes) }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -116,7 +116,6 @@ define([
$course_form = $("#course_form"),
$hashtag_form = $(".hashtag_form"),
$course_hashtag = $("#course_hashtag"),
$enroll_users_form = $("#enroll_users_form"),
$enrollment_dialog = $("#enrollment_dialog");
$("#course_details_tabs").tabs({cookie: {}}).show();
@ -363,14 +362,6 @@ define([
.find(":text:not(.date_entry)").keycodes('esc', function() {
$course_form.find(".cancel_button:first").click();
});
$enroll_users_form.hide();
$(".add_users_link").click(function(event) {
$(this).hide();
event.preventDefault();
$enroll_users_form.show();
$("html,body").scrollTo($enroll_users_form);
$enroll_users_form.find("textarea").focus().select();
});
$(".associated_user_link").click(function(event) {
event.preventDefault();
var $user = $(this).parents(".user");
@ -401,71 +392,10 @@ define([
event.preventDefault();
$(".course_form_more_options").slideToggle();
});
$(".user_list").delegate('.user', 'mouseover', function(event) {
var $this = $(this),
title = $this.attr('title'),
pending_message = I18n.t('details.re_send_invitation', "This user has not yet accepted their invitation. Click to re-send invitation.");
if(title != pending_message) {
$this.data('real_title', title);
}
if($this.hasClass('pending')) {
$this.attr('title', pending_message).css('cursor', 'pointer');
} else {
$this.attr('title', $(this).data('real_title') || I18n.t('defaults.user_name', "User")).css('cursor', '');
}
});
$enrollment_dialog.find(".cancel_button").click(function() {
$enrollment_dialog.find(".cancel_button").click(function() {
$enrollment_dialog.dialog('close');
});
$(".user_list").delegate('.user_information_link', 'click', function(event) {
var $this = $(this),
$user = $this.closest('.user'),
pending = $user.hasClass('pending'),
data = $user.getTemplateData({textValues: ['name', 'invitation_sent_at']}),
admin = $user.parents(".teacher_enrollments,.ta_enrollments").length > 0;
data.re_send_invitation_link = I18n.t('links.re_send_invitation', "Re-Send Invitation");
$enrollment_dialog
.data('user', $user)
.find(".re_send_invitation_link")
.attr('href', $user.find(".re_send_confirmation_url").attr('href')).end()
.find(".student_enrollment_re_send").showIf(pending && !admin).end()
.find(".admin_enrollment_re_send").showIf(pending && admin).end()
.find(".accepted_enrollment_re_send").showIf(!pending).end()
.find(".invitation_sent_at").showIf(pending).end()
.fillTemplateData({data: data})
.dialog('close')
.dialog({ autoOpen: false, title: I18n.t('titles.enrollment_details', "Enrollment Details") })
.dialog('open');
return false;
});
$('.user_list .edit_section_link').click(function(event) {
event.preventDefault();
var $this = $(this);
var $user = $this.parents('.user');
var $sections = $user.find('.sections');
$sections.find('.section_name').toggle();
$sections.find('.enrollment_course_section_form').toggle();
});
$('.user_list .enrollment_course_section_form .course_section_id').change(function (event) {
var $this = $(this);
var $sections = $this.parents('.sections');
var $form = $this.parent('form');
var $section_name = $form.prev('.section_name');
$.ajaxJSON($form.attr('action'), 'POST', $form.getFormData(), function(data) {
$section_name.html($this.find('option[value="' + $this.val() + '"]').html());
$sections.find('.section_name').toggle();
$sections.find('.enrollment_course_section_form').toggle();
}, function(data) {
if (data && data.enrollment) {
$this.val(data.enrollment.course_section_id);
}
$.flashError(I18n.t('errors.move_user', "Something went wrong moving the user to the new section. Please try again later."));
});
});
$enrollment_dialog.find(".re_send_invitation_link").click(function(event) {
event.preventDefault();
var $link = $(this);

View File

@ -43,6 +43,7 @@ define([
disabled = true;
$inputsToDisable.prop('disabled', true);
$spinHolder.show().spin(options);
$($spinHolder.data().spinner.el).css({'max-width':'100px'});
$disabledArea.css('opacity', function(i, currentOpacity){
$(this).data(dataKey+'opacityBefore', this.style.opacity);
return opts.opacity;
@ -68,8 +69,9 @@ define([
$spinHolder.css('display', previousSpinHolderDisplay).spin(false); // stop spinner
$disabledArea.css('opacity', function(){ return $(this).data(dataKey+'opacityBefore') });
$inputsToDisable.prop('disabled', false);
$.each(opts.buttons, function() {
$disabledArea.find(''+this).text(function() { return $(this).data(dataKey) });
$.each(opts.buttons, function(selector, text) {
if(typeof selector === 'number') var selector = ''+this; // for arrays
$disabledArea.find(selector).text(function() { return $(this).data(dataKey) });
});
thingsToWaitOn.erase(myDeferred); //speed up so that $.when doesn't have to look at myDeferred any more
myDeferred.resolve();

View File

@ -17,7 +17,7 @@ define([
], function(I18n, $, _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, PaginatedList, enrollmentTemplate, sectionEnrollmentPresenter) {
$(document).ready(function() {
var section_id = window.location.pathname.split('/')[4]
var section_id = window.location.pathname.split('/')[4],
$edit_section_form = $("#edit_section_form"),
$edit_section_link = $(".edit_section_link"),
currentEnrollmentList = new PaginatedList($('#current-enrollment-list'), {

View File

@ -1,232 +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/>.
*/
define([
'INST' /* INST */,
'i18n!user_lists',
'jquery' /* $ */,
'jquery.ajaxJSON' /* ajaxJSON */,
'jquery.instructure_forms' /* getFormData */,
'jquery.instructure_misc_helpers' /* /\$\.underscore/ */,
'jquery.instructure_misc_plugins' /* confirmDelete, showIf */,
'jquery.loadingImg' /* loadingImg, loadingImage */,
'jquery.rails_flash_notifications' /* flashMessage, flashError */,
'jquery.scrollToVisible' /* scrollToVisible */,
'jquery.templateData' /* fillTemplateData, getTemplateData */,
'vendor/jquery.scrollTo' /* /\.scrollTo/ */
], function(INST, I18n, $) {
var $user_lists_processed_person_template = $("#user_lists_processed_person_template").removeAttr('id').detach(),
$user_list_no_valid_users = $("#user_list_no_valid_users"),
$user_list_with_errors = $("#user_list_with_errors"),
$user_lists_processed_people = $("#user_lists_processed_people"),
$user_list_duplicates_found = $("#user_list_duplicates_found"),
$form = $("#enroll_users_form"),
$enrollment_blank = $("#enrollment_blank").removeAttr('id').hide(),
user_lists_path = $("#user_lists_path").attr('href');
var UL = INST.UserLists = {
init: function(){
UL.showTextarea();
$form
.find(".cancel_button")
.click(function() {
$('.add_users_link').show();
$form.hide();
})
.end()
.find(".go_back_button")
.click(UL.showTextarea)
.end()
.find(".verify_syntax_button")
.click(function(e){
e.preventDefault();
UL.showProcessing();
$.ajaxJSON(user_lists_path, 'POST', $form.getFormData(), UL.showResults);
})
.end()
.submit(function(event) {
event.preventDefault();
event.stopPropagation();
$form.find(".add_users_button").text(I18n.t("adding_users", "Adding Users...")).attr('disabled', true);
$.ajaxJSON($form.attr('action'), 'POST', $form.getFormData(), function(enrollments) {
$form.find(".user_list").val("");
UL.showTextarea();
if (!enrollments || !enrollments.length) { return false; }
var already_existed = 0;
$.each( enrollments, function(){
already_existed += UL.addUserToList(this.enrollment);
});
var addedMsg = I18n.t("users_added", { one: "1 user added", other: "%{count} users added" }, { count: enrollments.length - already_existed });
if (already_existed > 0) {
addedMsg += " " + I18n.t("users_existed", { one: "(1 user already existed)", other: "(%{count} users already existed)" }, { count: already_existed });
}
$.flashMessage(addedMsg);
}, function(data) {
$.flashError(I18n.t("users_adding_failed", "Failed to enroll users"));
});
});
$form.find("#enrollment_type").change(function() {
$("#limit_privileges_to_course_section_holder").showIf($(this).val() == "TeacherEnrollment" || $(this).val() == "TaEnrollment");
}).change();
$(".unenroll_user_link").click(function(event) {
event.preventDefault();
event.stopPropagation();
if($(this).hasClass('cant_unenroll')) {
alert(I18n.t("cant_unenroll", "This user was automatically enrolled using the campus enrollment system, so they can't be manually removed. Please contact your system administrator if you have questions."));
} else {
$user = $(this).parents('.user')
$sections = $(this).parents('.sections')
$section = $(this).parents('.section')
var $toDelete = $user;
if ($sections.find('.section:visible').size() > 1) {
$toDelete = $section;
}
$toDelete.confirmDelete({
message: I18n.t("delete_confirm", "Are you sure you want to remove this user?"),
url: $(this).attr('href'),
success: function() {
$(this).fadeOut(function() {
UL.updateCounts();
});
}
});
}
});
},
showTextarea: function(){
$form.find(".add_users_button, .go_back_button, #user_list_parsed").hide();
$form.find(".verify_syntax_button, .cancel_button, #user_list_textarea_container").show().removeAttr('disabled');
$form.find(".verify_syntax_button").attr('disabled', false).text(I18n.t('buttons.continue', "Continue..."));
$user_list = $form.find(".user_list").removeAttr('disabled').loadingImage('remove').focus();
if ($user_list.is(':visible')) { $user_list.select(); } // .select() blows up in IE9 + jQuery 1.7.2 on invisible elements
},
showProcessing: function(){
$form.find(".verify_syntax_button").attr('disabled', true).text(I18n.t('messages.processing', "Processing..."));
$form.find(".user_list").attr('disabled', true).loadingImage();
},
showResults: function(userList){
$form.find(".add_users_button, .go_back_button, #user_list_parsed").show();
$form.find(".add_users_button").attr('disabled', false).text(I18n.t("add_n_users", {one: "OK Looks Good, Add This 1 User", other: "OK Looks Good, Add These %{count} Users"}, {count: userList.users.length}));
$form.find(".verify_syntax_button, .cancel_button, #user_list_textarea_container").hide();
$form.find(".user_list").removeAttr('disabled').loadingImage('remove');
$user_lists_processed_people.html("").show();
if (!userList || !userList.users || !userList.users.length) {
$user_list_no_valid_users.appendTo($user_lists_processed_people);
$form.find(".add_users_button").hide();
}
else {
if (userList.errored_users && userList.errored_users.length) {
$user_list_with_errors
.appendTo($user_lists_processed_people)
.find('.message_content')
.html(I18n.t("user_parsing_errors", { one: "There was 1 error parsing that list of users.", other: "There were %{count} errors parsing that list of users."}, {count:userList.errored_users.length}) + " " + I18n.t("invalid_users_notice", "There may be some that were invalid, and you might need to go back and fix any errors.") + " " + I18n.t("users_to_add", { one: "If you proceed as is, 1 user will be added.", other: "If you proceed as is, %{count} users will be added." }, {count: userList.users.length}));
}
if (userList.duplicates && userList.duplicates.length) {
$user_list_duplicates_found
.appendTo($user_lists_processed_people)
.find('.message_content')
.html(I18n.t("duplicate_users", { one: "1 duplicate user found, duplicates have been removed.", other: "%{count} duplicate user found, duplicates have been removed."}, {count:userList.duplicates.length}))
}
$.each(userList.users, function(){
$user_lists_processed_person_template
.clone(true)
.fillTemplateData({ data: this })
.appendTo($user_lists_processed_people)
.show();
});
}
},
updateCounts: function() {
$.each(['student', 'teacher', 'ta', 'teacher_and_ta', 'student_and_observer', 'observer'], function(){
$("." + this + "_count").text( $("." + this + "_enrollments .user:visible").length );
});
},
addUserToList: function(enrollment){
var enrollmentType = $.underscore(enrollment.type);
var $list = $(".user_list." + enrollmentType + "s");
if(!$list.length) {
if(enrollmentType == 'student_enrollment' || enrollmentType == 'observer_enrollment') {
$list = $(".user_list.student_and_observer_enrollments");
} else {
$list = $(".user_list.teacher_and_ta_enrollments");
}
}
$list.find(".none").remove();
enrollment.invitation_sent_at = I18n.t("just_now", "Just Now");
var $before = null;
$list.find(".user").each(function() {
var name = $(this).getTemplateData({textValues: ['name']}).name;
if(name && enrollment.name && name.toLowerCase() > enrollment.name.toLowerCase()) {
$before = $(this);
return false;
}
});
enrollment.enrollment_id = enrollment.id;
var already_existed = true;
if(!$("#enrollment_" + enrollment.id).length) {
already_existed = false;
var $enrollment = $enrollment_blank
.clone(true)
.fillTemplateData({
textValues: ['name', 'membership_type', 'email', 'enrollment_id'],
id: 'enrollment_' + enrollment.id,
hrefValues: ['id', 'user_id', 'pseudonym_id', 'communication_channel_id'],
data: enrollment
})
.addClass(enrollmentType)
.removeClass('nil_class user_')
.addClass('user_' + enrollment.user_id)
.toggleClass('pending', enrollment.workflow_state != 'active')
[($before ? 'insertBefore' : 'appendTo')]( ($before || $list) )
.show()
.animate({'backgroundColor': '#FFEE88'}, 1000)
.animate({'display': 'block'}, 2000)
.animate({'backgroundColor': '#FFFFFF'}, 2000, function() {
$(this).css('backgroundColor', '');
});
$enrollment.find('.enrollment_link')
.removeClass('enrollment_blank')
.addClass('enrollment_' + enrollment.id);
$enrollment
.parents(".user_list")
.scrollToVisible($enrollment);
}
UL.updateCounts();
return already_existed ? 1 : 0;
}
};
// run the init function on domready
$(INST.UserLists.init);
});

View File

@ -43,18 +43,6 @@ describe ConversationsController, :type => :integration do
u
end
def observer_in_course(options = {})
section = options.delete(:section)
associated_user = options.delete(:associated_user)
u = User.create(options)
enrollment = @course.enroll_user(u, 'ObserverEnrollment', :section => section)
enrollment.associated_user = associated_user
enrollment.workflow_state = 'active'
enrollment.save
u.associated_accounts << Account.default
u
end
context "conversations" do
it "should return the conversation list" do
@c1 = conversation(@bob, :workflow_state => 'read')
@ -465,293 +453,6 @@ describe ConversationsController, :type => :integration do
end
end
context "find_recipients" do
before do
@group = @course.groups.create(:name => "the group")
@group.users = [@me, @bob, @joe]
end
it "should return recipients" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=o",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'o' })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "course_#{@course.id}", "name" => "the course", "type" => "context", "user_count" => 6},
{"id" => "section_#{@other_section.id}", "name" => "the other section", "type" => "context", "user_count" => 1, "context_name" => "the course"},
{"id" => "section_#{@course.default_section.id}", "name" => "the section", "type" => "context", "user_count" => 5, "context_name" => "the course"},
{"id" => "group_#{@group.id}", "name" => "the group", "type" => "context", "user_count" => 3, "context_name" => "the course"},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
it "should return recipients for a given course" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
it "should return recipients for a given group" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=group_#{@group.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "group_#{@group.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {}, "common_groups" => {@group.id.to_s => ["Member"]}}
]
end
it "should return recipients for a given section" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=section_#{@course.default_section.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "section_#{@course.default_section.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}}
]
end
it "should return recipients found by id" do
json = api_call(:get, "/api/v1/conversations/find_recipients?user_id=#{@bob.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :user_id => @bob.id.to_s })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
]
end
it "should ignore other parameters when searching by id" do
json = api_call(:get, "/api/v1/conversations/find_recipients?user_id=#{@bob.id}&search=asdf",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :user_id => @bob.id.to_s, :search => "asdf" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
]
end
it "should return recipients by id if contactable, or if a shared conversation is referenced" do
other = User.create(:name => "other personage")
json = api_call(:get, "/api/v1/conversations/find_recipients?user_id=#{other.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :user_id => other.id.to_s })
json.should == []
# now they have a conversation in common
c = Conversation.initiate([@user.id, other.id], true)
json = api_call(:get, "/api/v1/conversations/find_recipients?user_id=#{other.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :user_id => other.id.to_s })
json.should == []
# ... but it has to be explicity referenced via from_conversation_id
json = api_call(:get, "/api/v1/conversations/find_recipients?user_id=#{other.id}&from_conversation_id=#{c.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :user_id => other.id.to_s, :from_conversation_id => c.id.to_s })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => other.id, "name" => "other personage", "common_courses" => {}, "common_groups" => {}},
]
end
context "observers" do
before do
@bobs_mom = observer_in_course(:name => "bob's mom", :associated_user => @bob)
@lonely = observer_in_course(:name => "lonely observer")
end
it "should show all observers to a teacher" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bobs_mom.id, "name" => "bob's mom", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @lonely.id, "name" => "lonely observer", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
it "should not show non-linked students to observers" do
json = api_call_as_user(@bobs_mom, :get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bobs_mom.id, "name" => "bob's mom", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}}
]
json = api_call_as_user(@lonely, :get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @lonely.id, "name" => "lonely observer", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}}
]
end
it "should not show non-linked observers to students" do
json = api_call_as_user(@bob, :get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bobs_mom.id, "name" => "bob's mom", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
# must not include lonely observer here
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
json = api_call_as_user(@billy, :get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
# must not include bob's mom here
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
# must not include lonely observer here
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
end
context "synthetic contexts" do
it "should return synthetic contexts within a course" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}&synthetic_contexts=1",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "course_#{@course.id}_teachers", "name" => "Teachers", "type" => "context", "user_count" => 1},
{"id" => "course_#{@course.id}_students", "name" => "Students", "type" => "context", "user_count" => 5},
{"id" => "course_#{@course.id}_sections", "name" => "Course Sections", "type" => "context", "item_count" => 2},
{"id" => "course_#{@course.id}_groups", "name" => "Student Groups", "type" => "context", "item_count" => 1}
]
end
it "should return synthetic contexts within a section" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=section_#{@course.default_section.id}&synthetic_contexts=1",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "section_#{@course.default_section.id}", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "section_#{@course.default_section.id}_teachers", "name" => "Teachers", "type" => "context", "user_count" => 1},
{"id" => "section_#{@course.default_section.id}_students", "name" => "Students", "type" => "context", "user_count" => 4}
]
end
it "should return groups within a course" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}_groups&synthetic_contexts=1",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}_groups", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "group_#{@group.id}", "name" => "the group", "type" => "context", "user_count" => 3}
]
end
it "should return sections within a course" do
json = api_call(:get, "/api/v1/conversations/find_recipients.json?context=course_#{@course.id}_sections&synthetic_contexts=1",
{ :controller => 'conversations', :action => 'find_recipients', :format => 'json', :context => "course_#{@course.id}_sections", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "section_#{@other_section.id}", "name" => @other_section.name, "type" => "context", "user_count" => 1},
{"id" => "section_#{@course.default_section.id}", "name" => @course.default_section.name, "type" => "context", "user_count" => 5}
]
end
end
context "pagination" do
it "should not paginate if no type is specified" do
# it's a synthetic result (we might a few of each type), making
# pagination pretty tricksy. so we don't allow it
4.times{ student_in_course(:name => "cletus") }
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=cletus&per_page=3",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'cletus', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should be_nil
end
it "should paginate users and return proper pagination headers" do
4.times{ student_in_course(:name => "cletus") }
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=cletus&type=user&per_page=3",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'cletus', :type => 'user', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/conversations/find_recipients.json?search=cletus&type=user&page=2&per_page=3>; rel="next",</api/v1/conversations/find_recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="first"})
# get the next page
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=cletus&type=user&page=2&per_page=3",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'cletus', :type => 'user', :page => '2', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/conversations/find_recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="prev",</api/v1/conversations/find_recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="first"})
end
it "should allow fetching all users iff a context is specified" do
# for admins in particular, there may be *lots* of messageable users,
# so we don't allow retrieval of all of them unless a context is given
11.times{ student_in_course(:name => "cletus") }
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=cletus&type=user&per_page=-1",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'cletus', :type => 'user', :per_page => '-1'})
json.size.should eql 10
response.headers['Link'].should eql(%{</api/v1/conversations/find_recipients.json?search=cletus&type=user&page=2&per_page=10>; rel="next",</api/v1/conversations/find_recipients.json?search=cletus&type=user&page=1&per_page=10>; rel="first"})
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=cletus&type=user&context=course_#{@course.id}&per_page=-1",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'cletus', :context => "course_#{@course.id}", :type => 'user', :per_page => '-1'})
json.size.should eql 11
response.headers['Link'].should be_nil
end
it "should paginate contexts and return proper pagination headers" do
4.times{
course_with_teacher(:active_course => true, :active_enrollment => true, :user => @user)
@course.update_attribute(:name, "ofcourse")
}
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=ofcourse&type=context&per_page=3",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/conversations/find_recipients.json?search=ofcourse&type=context&page=2&per_page=3>; rel="next",</api/v1/conversations/find_recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="first"})
# get the next page
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=ofcourse&type=context&page=2&per_page=3",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :page => '2', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/conversations/find_recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="prev",</api/v1/conversations/find_recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="first"})
end
it "should allow fetching all contexts" do
4.times{
course_with_teacher(:active_course => true, :active_enrollment => true, :user => @user)
@course.update_attribute(:name, "ofcourse")
}
json = api_call(:get, "/api/v1/conversations/find_recipients.json?search=ofcourse&type=context&per_page=-1",
{:controller => 'conversations', :action => 'find_recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :per_page => '-1'})
json.size.should eql 4
response.headers['Link'].should be_nil
end
end
end
context "conversation" do
it "should return the conversation" do
conversation = conversation(@bob)

View File

@ -0,0 +1,327 @@
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
describe SearchController, :type => :integration do
before do
course_with_teacher(:active_course => true, :active_enrollment => true, :user => user_with_pseudonym(:active_user => true))
@course.update_attribute(:name, "the course")
@course.default_section.update_attributes(:name => "the section")
@other_section = @course.course_sections.create(:name => "the other section")
@me = @user
@bob = student_in_course(:name => "bob")
@billy = student_in_course(:name => "billy")
@jane = student_in_course(:name => "jane")
@joe = student_in_course(:name => "joe")
@tommy = student_in_course(:name => "tommy", :section => @other_section)
end
def student_in_course(options = {})
section = options.delete(:section)
u = User.create(options)
enrollment = @course.enroll_user(u, 'StudentEnrollment', :section => section)
enrollment.workflow_state = 'active'
enrollment.save
u.associated_accounts << Account.default
u
end
context "recipients" do
before do
@group = @course.groups.create(:name => "the group")
@group.users = [@me, @bob, @joe]
end
it "should return recipients" do
json = api_call(:get, "/api/v1/search/recipients.json?search=o",
{ :controller => 'search', :action => 'recipients', :format => 'json', :search => 'o' })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "course_#{@course.id}", "name" => "the course", "type" => "context", "user_count" => 6},
{"id" => "section_#{@other_section.id}", "name" => "the other section", "type" => "context", "user_count" => 1, "context_name" => "the course"},
{"id" => "section_#{@course.default_section.id}", "name" => "the section", "type" => "context", "user_count" => 5, "context_name" => "the course"},
{"id" => "group_#{@group.id}", "name" => "the group", "type" => "context", "user_count" => 3, "context_name" => "the course"},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
it "should return recipients for a given course" do
json = api_call(:get, "/api/v1/search/recipients.json?context=course_#{@course.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
it "should return recipients for a given group" do
json = api_call(:get, "/api/v1/search/recipients.json?context=group_#{@group.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "group_#{@group.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {}, "common_groups" => {@group.id.to_s => ["Member"]}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {}, "common_groups" => {@group.id.to_s => ["Member"]}}
]
end
it "should return recipients for a given section" do
json = api_call(:get, "/api/v1/search/recipients.json?context=section_#{@course.default_section.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "section_#{@course.default_section.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}}
]
end
it "should return recipients found by id" do
json = api_call(:get, "/api/v1/search/recipients?user_id=#{@bob.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :user_id => @bob.id.to_s })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
]
end
it "should ignore other parameters when searching by id" do
json = api_call(:get, "/api/v1/search/recipients?user_id=#{@bob.id}&search=asdf",
{ :controller => 'search', :action => 'recipients', :format => 'json', :user_id => @bob.id.to_s, :search => "asdf" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {@group.id.to_s => ["Member"]}},
]
end
it "should return recipients by id if contactable, or if a shared conversation is referenced" do
other = User.create(:name => "other personage")
json = api_call(:get, "/api/v1/search/recipients?user_id=#{other.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :user_id => other.id.to_s })
json.should == []
# now they have a conversation in common
c = Conversation.initiate([@user.id, other.id], true)
json = api_call(:get, "/api/v1/search/recipients?user_id=#{other.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :user_id => other.id.to_s })
json.should == []
# ... but it has to be explicity referenced via from_conversation_id
json = api_call(:get, "/api/v1/search/recipients?user_id=#{other.id}&from_conversation_id=#{c.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :user_id => other.id.to_s, :from_conversation_id => c.id.to_s })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => other.id, "name" => "other personage", "common_courses" => {}, "common_groups" => {}},
]
end
context "observers" do
def observer_in_course(options = {})
section = options.delete(:section)
associated_user = options.delete(:associated_user)
u = User.create(options)
enrollment = @course.enroll_user(u, 'ObserverEnrollment', :section => section)
enrollment.associated_user = associated_user
enrollment.workflow_state = 'active'
enrollment.save
u.associated_accounts << Account.default
u
end
before do
@bobs_mom = observer_in_course(:name => "bob's mom", :associated_user => @bob)
@lonely = observer_in_course(:name => "lonely observer")
end
it "should show all observers to a teacher" do
json = api_call(:get, "/api/v1/search/recipients.json?context=course_#{@course.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bobs_mom.id, "name" => "bob's mom", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @lonely.id, "name" => "lonely observer", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
it "should not show non-linked students to observers" do
json = api_call_as_user(@bobs_mom, :get, "/api/v1/search/recipients.json?context=course_#{@course.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bobs_mom.id, "name" => "bob's mom", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}}
]
json = api_call_as_user(@lonely, :get, "/api/v1/search/recipients.json?context=course_#{@course.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @lonely.id, "name" => "lonely observer", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}}
]
end
it "should not show non-linked observers to students" do
json = api_call_as_user(@bob, :get, "/api/v1/search/recipients.json?context=course_#{@course.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bobs_mom.id, "name" => "bob's mom", "common_courses" => {@course.id.to_s => ["ObserverEnrollment"]}, "common_groups" => {}},
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
# must not include lonely observer here
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
json = api_call_as_user(@billy, :get, "/api/v1/search/recipients.json?context=course_#{@course.id}",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => @billy.id, "name" => "billy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @bob.id, "name" => "bob", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
# must not include bob's mom here
{"id" => @jane.id, "name" => "jane", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
{"id" => @joe.id, "name" => "joe", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}},
# must not include lonely observer here
{"id" => @me.id, "name" => @me.name, "common_courses" => {@course.id.to_s => ["TeacherEnrollment"]}, "common_groups" => {}},
{"id" => @tommy.id, "name" => "tommy", "common_courses" => {@course.id.to_s => ["StudentEnrollment"]}, "common_groups" => {}}
]
end
end
context "synthetic contexts" do
it "should return synthetic contexts within a course" do
json = api_call(:get, "/api/v1/search/recipients.json?context=course_#{@course.id}&synthetic_contexts=1",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "course_#{@course.id}_teachers", "name" => "Teachers", "type" => "context", "user_count" => 1},
{"id" => "course_#{@course.id}_students", "name" => "Students", "type" => "context", "user_count" => 5},
{"id" => "course_#{@course.id}_sections", "name" => "Course Sections", "type" => "context", "item_count" => 2},
{"id" => "course_#{@course.id}_groups", "name" => "Student Groups", "type" => "context", "item_count" => 1}
]
end
it "should return synthetic contexts within a section" do
json = api_call(:get, "/api/v1/search/recipients.json?context=section_#{@course.default_section.id}&synthetic_contexts=1",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "section_#{@course.default_section.id}", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "section_#{@course.default_section.id}_teachers", "name" => "Teachers", "type" => "context", "user_count" => 1},
{"id" => "section_#{@course.default_section.id}_students", "name" => "Students", "type" => "context", "user_count" => 4}
]
end
it "should return groups within a course" do
json = api_call(:get, "/api/v1/search/recipients.json?context=course_#{@course.id}_groups&synthetic_contexts=1",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}_groups", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "group_#{@group.id}", "name" => "the group", "type" => "context", "user_count" => 3}
]
end
it "should return sections within a course" do
json = api_call(:get, "/api/v1/search/recipients.json?context=course_#{@course.id}_sections&synthetic_contexts=1",
{ :controller => 'search', :action => 'recipients', :format => 'json', :context => "course_#{@course.id}_sections", :synthetic_contexts => "1" })
json.each { |c| c.delete("avatar_url") }
json.should eql [
{"id" => "section_#{@other_section.id}", "name" => @other_section.name, "type" => "context", "user_count" => 1},
{"id" => "section_#{@course.default_section.id}", "name" => @course.default_section.name, "type" => "context", "user_count" => 5}
]
end
end
context "pagination" do
it "should not paginate if no type is specified" do
# it's a synthetic result (we might a few of each type), making
# pagination pretty tricksy. so we don't allow it
4.times{ student_in_course(:name => "cletus") }
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should be_nil
end
it "should paginate users and return proper pagination headers" do
4.times{ student_in_course(:name => "cletus") }
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :type => 'user', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=cletus&type=user&page=2&per_page=3>; rel="next",</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="first"})
# get the next page
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&page=2&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :type => 'user', :page => '2', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="prev",</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="first"})
end
it "should allow fetching all users iff a context is specified" do
# for admins in particular, there may be *lots* of messageable users,
# so we don't allow retrieval of all of them unless a context is given
11.times{ student_in_course(:name => "cletus") }
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&per_page=-1",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :type => 'user', :per_page => '-1'})
json.size.should eql 10
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=cletus&type=user&page=2&per_page=10>; rel="next",</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=10>; rel="first"})
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&context=course_#{@course.id}&per_page=-1",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :context => "course_#{@course.id}", :type => 'user', :per_page => '-1'})
json.size.should eql 11
response.headers['Link'].should be_nil
end
it "should paginate contexts and return proper pagination headers" do
4.times{
course_with_teacher(:active_course => true, :active_enrollment => true, :user => @user)
@course.update_attribute(:name, "ofcourse")
}
json = api_call(:get, "/api/v1/search/recipients.json?search=ofcourse&type=context&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=ofcourse&type=context&page=2&per_page=3>; rel="next",</api/v1/search/recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="first"})
# get the next page
json = api_call(:get, "/api/v1/search/recipients.json?search=ofcourse&type=context&page=2&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :page => '2', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="prev",</api/v1/search/recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="first"})
end
it "should allow fetching all contexts" do
4.times{
course_with_teacher(:active_course => true, :active_enrollment => true, :user => @user)
@course.update_attribute(:name, "ofcourse")
}
json = api_call(:get, "/api/v1/search/recipients.json?search=ofcourse&type=context&per_page=-1",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :per_page => '-1'})
json.size.should eql 4
response.headers['Link'].should be_nil
end
end
end
end

View File

@ -21,11 +21,13 @@ require File.expand_path(File.dirname(__FILE__) + '/../file_uploads_spec_helper'
class TestUserApi
include Api::V1::User
attr_accessor :services_enabled, :context, :current_user
attr_accessor :services_enabled, :context, :current_user, :params, :request
def service_enabled?(service); @services_enabled.include? service; end
def avatar_image_url(user_id); "avatar_image_url(#{user_id})"; end
def avatar_image_url(*args); "avatar_image_url(#{args.first})"; end
def initialize
@domain_root_account = Account.default
@params = {}
@request = OpenStruct.new
end
end
@ -175,6 +177,10 @@ describe Api::V1::User do
end
describe "Users API", :type => :integration do
def avatar_url(id)
"http://www.example.com/images/users/#{User.avatar_key(id)}?fallback=http%3A%2F%2Fwww.example.com%2Fimages%2Fmessages%2Favatar-50.png"
end
before do
@admin = account_admin_user
course_with_student(:user => user_with_pseudonym(:name => 'Student', :username => 'pvuser@example.com'))
@ -196,7 +202,7 @@ describe "Users API", :type => :integration do
'sis_user_id' => 'sis-user-id',
'sis_login_id' => 'pvuser@example.com',
'login_id' => 'pvuser@example.com',
'avatar_url' => "http://www.example.com/images/users/#{User.avatar_key(@student.id)}",
'avatar_url' => avatar_url(@student.id),
}
end
@ -221,7 +227,7 @@ describe "Users API", :type => :integration do
'short_name' => 'new guy',
'login_id' => nil,
'primary_email' => nil,
'avatar_url' => "http://www.example.com/images/users/#{User.avatar_key(new_user.id)}",
'avatar_url' => avatar_url(new_user.id),
}
get("/courses/#{@course.id}/students")
@ -237,7 +243,7 @@ describe "Users API", :type => :integration do
'short_name' => 'User',
'primary_email' => 'nobody@example.com',
'login_id' => 'nobody@example.com',
'avatar_url' => "http://www.example.com/images/users/#{User.avatar_key(@admin.id)}",
'avatar_url' => avatar_url(@admin.id),
'calendar' => { 'ics' => "http://www.example.com/feeds/calendars/user_#{@admin.uuid}.ics" },
}
end
@ -253,7 +259,7 @@ describe "Users API", :type => :integration do
'short_name' => 'Student',
'primary_email' => 'pvuser@example.com',
'login_id' => 'pvuser@example.com',
'avatar_url' => "http://www.example.com/images/users/#{User.avatar_key(@student.id)}",
'avatar_url' => avatar_url(@student.id),
'calendar' => { 'ics' => "http://www.example.com/feeds/calendars/user_#{@student.uuid}.ics" },
}
end

View File

@ -361,57 +361,6 @@ describe ConversationsController do
end
end
describe "GET 'find_recipients'" do
it "should assign variables" do
course_with_student_logged_in(:active_all => true)
@course.update_attribute(:name, "this_is_a_test_course")
other = User.create(:name => 'this_is_a_test_user')
enrollment = @course.enroll_student(other)
enrollment.workflow_state = 'active'
enrollment.save
group = @course.groups.create(:name => 'this_is_a_test_group')
group.users = [@user, other]
get 'find_recipients', :search => 'this_is_a_test_'
response.should be_success
response.body.should include(@course.name)
response.body.should include(group.name)
response.body.should include(other.name)
end
it "should not sort by rank if a search term is not used" do
course_with_student_logged_in(:active_all => true)
@user.update_attribute(:name, 'billy')
other = User.create(:name => 'bob')
@course.enroll_student(other).tap{ |e| e.workflow_state = 'active'; e.save! }
group = @course.groups.create(:name => 'group')
group.users << other
get 'find_recipients', :context => @course.asset_string, :per_page => '1', :type => 'user'
response.should be_success
response.body.should include('billy')
response.body.should_not include('bob')
end
it "should sort by rank if a search term is used" do
course_with_student_logged_in(:active_all => true)
@user.update_attribute(:name, 'billy')
other = User.create(:name => 'bob')
@course.enroll_student(other).tap{ |e| e.workflow_state = 'active'; e.save! }
group = @course.groups.create(:name => 'group')
group.users << other
get 'find_recipients', :search => 'b', :per_page => '1', :type => 'user'
response.should be_success
response.body.should include('bob')
response.body.should_not include('billy')
end
end
describe "GET 'public_feed.atom'" do
it "should require authorization" do
course_with_student

View File

@ -0,0 +1,56 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe SearchController do
describe "GET 'recipients'" do
it "should assign variables" do
course_with_student_logged_in(:active_all => true)
@course.update_attribute(:name, "this_is_a_test_course")
other = User.create(:name => 'this_is_a_test_user')
enrollment = @course.enroll_student(other)
enrollment.workflow_state = 'active'
enrollment.save
group = @course.groups.create(:name => 'this_is_a_test_group')
group.users = [@user, other]
get 'recipients', :search => 'this_is_a_test_'
response.should be_success
response.body.should include(@course.name)
response.body.should include(group.name)
response.body.should include(other.name)
end
it "should not sort by rank if a search term is not used" do
course_with_student_logged_in(:active_all => true)
@user.update_attribute(:name, 'billy')
other = User.create(:name => 'bob')
@course.enroll_student(other).tap{ |e| e.workflow_state = 'active'; e.save! }
group = @course.groups.create(:name => 'group')
group.users << other
get 'recipients', :context => @course.asset_string, :per_page => '1', :type => 'user'
response.should be_success
response.body.should include('billy')
response.body.should_not include('bob')
end
it "should sort by rank if a search term is used" do
course_with_student_logged_in(:active_all => true)
@user.update_attribute(:name, 'billy')
other = User.create(:name => 'bob')
@course.enroll_student(other).tap{ |e| e.workflow_state = 'active'; e.save! }
group = @course.groups.create(:name => 'group')
group.users << other
get 'recipients', :search => 'b', :per_page => '1', :type => 'user'
response.should be_success
response.body.should include('bob')
response.body.should_not include('billy')
end
end
end

View File

@ -18,9 +18,9 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe ConversationsHelper do
include ConversationsHelper
describe AvatarHelper do
include AvatarHelper
context "avatars" do
before do
@services = {}

View File

@ -2556,6 +2556,11 @@ describe Course, "section_visibility" do
it "should return user's sections if a student" do
@course.sections_visible_to(@student1).should eql [@course.default_section]
end
it "should return users from all sections" do
@course.users_visible_to(@teacher).sort_by(&:id).should eql [@teacher, @ta, @student1, @student2, @observer]
@course.users_visible_to(@ta).sort_by(&:id).should eql [@teacher, @ta, @student1, @observer]
end
end
context "sections" do

View File

@ -4,7 +4,7 @@ describe "course settings" do
it_should_behave_like "in-process server selenium tests"
before (:each) do
course_with_teacher_logged_in
course_with_teacher_logged_in :limit_privileges_to_course_section => false
end
describe "course details" do
@ -136,13 +136,36 @@ describe "course settings" do
end
describe "course users" do
def select_from_auto_complete(text, input_id)
fj(".token_input input:visible").send_keys(text)
keep_trying_until do
driver.execute_script("return $('##{input_id}').data('token_input').selector.lastSearch") == text
end
elements = driver.execute_script("return $('.autocomplete_menu:visible .list').last().find('ul').last().find('li').toArray();").map { |e|
[e, (e.find_element(:tag_name, :b).text rescue e.text)]
}
element = elements.detect { |e| e.last == text } or raise "menu item does not exist"
element.first.click
end
def go_to_users_tab
get "/courses/#{@course.id}/settings#tab-users"
wait_for_ajaximations
end
def open_kyle_menu(user)
cog = f("#user_#{user.id} .admin-links")
f('button', cog).click
cog
end
it "should add a user to a section" do
user = user_with_pseudonym(:active_user => true, :username => 'user@example.com', :name => 'user@example.com')
section_name = 'Add User Section'
add_section(section_name)
get "/courses/#{@course.id}/settings#tab-users"
get "/courses/#{@course.id}/settings#tab-users"
add_button = driver.find_element(:css, '.add_users_link')
keep_trying_until { add_button.should be_displayed }
add_button.click
@ -159,157 +182,107 @@ describe "course settings" do
new_section.find_element(:css, '.users_count').should include_text("1")
end
it "should remove a user from a section" do
it "should remove a user from the course" do
username = "user@example.com"
student_in_course(:name => username)
@enrollment.course_section = @course_section; @enrollment.save
add_section('Section1')
@enrollment.course_section = @course_section; @enrollment.save!
get "/courses/#{@course.id}/settings#tab-users"
driver.execute_script("$('#enrollment_#{@enrollment.id} .unenroll_user_link').click()")
go_to_users_tab
f('#tab-users').should include_text(username)
cog = open_kyle_menu(@student)
f('a[data-event="removeFromCourse"]', cog).click
driver.switch_to.alert.accept
wait_for_ajaximations
driver.find_element(:id, 'tab-users').should_not include_text(username)
f('#tab-users').should_not include_text(username)
end
it "should move a user to a new section" do
section_name = 'Move to Course Section'
it "should add a user to another section" do
section_name = 'Another Section'
add_section(section_name)
student_in_course
@enrollment.course_section = @course_section; @enrollment.save
get "/courses/#{@course.id}/settings#tab-users"
driver.execute_script("$('#enrollment_#{@enrollment.id} .edit_section_link').click()")
click_option("#enrollment_#{@enrollment.id} .course_section_id", section_name)
# open tab
go_to_users_tab
f("#user_#{@student.id} .section").should_not include_text(section_name)
# open dialog
cog = open_kyle_menu(@student)
f('a[data-event="editSections"]', cog).click
wait_for_ajaximations
driver.find_element(:css, "#enrollment_#{@enrollment.id} .section").should include_text(section_name)
# choose section
select_from_auto_complete(section_name, 'section_input')
f('.ui-dialog-buttonpane .btn-primary').click
wait_for_ajaximations
# expect
f("#user_#{@student.id} .sections").should include_text(section_name)
ff("#user_#{@student.id} .section").length.should == 2
end
it "should view the users enrollment details" do
username = "user@example.com"
# add_section 'foo'
student_in_course(:name => username, :active_all => true)
@enrollment.course_section = @course_section; @enrollment.save
get "/courses/#{@course.id}/settings#tab-users"
driver.execute_script("$('#enrollment_#{@enrollment.id} .user_information_link').click()")
enrollment_dialog = driver.find_element(:id, 'enrollment_dialog')
enrollment_dialog.should be_displayed
enrollment_dialog.should include_text(username + ' has already received and accepted the invitation')
go_to_users_tab
# open dialog
open_kyle_menu(@student)
# when
link = driver.find_element(:link, 'User Details')
href = link['href']
link.click
wait_for_ajax_requests
# expect
driver.current_url.should include(href)
end
def link_to_student(link, student)
assoc_links = link.find_elements(:css, ".associated_user_link")
if assoc_links[0].displayed? then
assoc_links[0].click
else
assoc_links[1].click
end
wait_for_ajax_requests
click_option("#student_enrollment_link_option", student.try(:name) || "[ No Link ]")
submit_form("#link_student_dialog_form")
wait_for_ajax_requests
def use_link_dialog(observer)
cog = open_kyle_menu(observer)
f('a[data-event="linkToStudents"]', cog).click
wait_for_ajaximations
yield
f('.ui-dialog-buttonpane .btn-primary').click
wait_for_ajaximations
end
it "should deal with observers linked to multiple students" do
@students = []
@obs = user_model(:name => "The Observer")
students = []
obs = user_model(:name => "The Observer")
2.times do |i|
student_in_course(:name => "Student #{i}")
@students << @student
e = @course.observer_enrollments.create!(:user => @obs, :workflow_state => 'active')
students << @student
e = @course.observer_enrollments.create!(:user => obs, :workflow_state => 'active')
e.associated_user_id = @student.id
e.save!
end
student_in_course(:name => "Student 3")
students << @student
2.times do |i|
student_in_course(:name => "Student #{i+2}")
@students << @student
go_to_users_tab
observeds = ff("#user_#{obs.id} .enrollment_type")
observeds.length.should == 2
observeds_txt = observeds.map(&:text).join(',')
observeds_txt.should include_text students[0].name
observeds_txt.should include_text students[1].name
# remove an observer
use_link_dialog(obs) do
fj("#link_students input:visible").send_keys(:backspace)
end
get "/courses/#{@course.id}/settings#tab-users"
links = driver.find_elements(:css, ".user_#{@obs.id} .enrollment_link")
links.length.should == 2
links[0].find_element(:css, ".associated_user_name").should include_text @students[0].name
links[1].find_element(:css, ".associated_user_name").should include_text @students[1].name
link_to_student(links[0], @students[2])
links[0].find_element(:css, ".associated_user_name").should include_text @students[2].name
links[1].find_element(:css, ".associated_user_name").should include_text @students[1].name
link_to_student(links[1], @students[3])
links[0].find_element(:css, ".associated_user_name").should include_text @students[2].name
links[1].find_element(:css, ".associated_user_name").should include_text @students[3].name
@obs.reload
@obs.enrollments.map { |e| e.associated_user_id }.sort.should == [@students[2].id, @students[3].id]
link_to_student(links[0], nil)
link_to_student(links[1], nil)
links[0].find_element(:css, ".unassociated").should include_text "link to"
links[1].find_element(:css, ".unassociated").should include_text "link to"
link_to_student(links[0], @students[0])
link_to_student(links[1], @students[1])
links[0].find_element(:css, ".associated_user_name").should include_text @students[0].name
links[1].find_element(:css, ".associated_user_name").should include_text @students[1].name
@obs.reload
@obs.enrollments.map { |e| e.associated_user_id }.sort.should == [@students[0].id, @students[1].id]
# expect
obs.reload.not_ended_enrollments.count.should == 1
# add an observer
use_link_dialog(obs) do
select_from_auto_complete(students[2].name, 'student_input')
end
# expect
obs.reload.not_ended_enrollments.count.should == 2
obs.reload.not_ended_enrollments.map {|e| e.associated_user_id}.sort.should include(students[2].id)
end
it "should not show the student view student" do
@fake_student = @course.student_view_student
get "/courses/#{@course.id}/settings#tab-users"
ff(".student_enrollments .user_#{@fake_student.id}").should be_empty
end
end
context "course users multiple enrollments" do
before (:each) do
@username = "multiple@example.com"
add_section("Section 1")
@old_section = @course_section
student_in_course(:name => @username)
@enrollment.course_section = @course_section; @enrollment.save
add_section("Section 2")
multiple_student_enrollment(@user, @course_section)
end
it "should coalesce multiple enrollments under a single student" do
get "/courses/#{@course.id}/settings#tab-users"
driver.find_elements(:css, ".user_#{@user.id} .section").length.should == 2
driver.find_elements(:css, ".user_#{@user.id} .links .unenroll_user_link").length.should == 0
end
it "should show individual course section remove icons" do
get "/courses/#{@course.id}/settings#tab-users"
driver.execute_script("$('.user_#{@user.id} .edit_section_link').click()")
find_all_with_jquery(".user_#{@user.id} .sections .unenroll_user_link:visible").length.should == 2
end
it "should only remove a user from a single section" do
get "/courses/#{@course.id}/settings#tab-users"
driver.execute_script("$('.user_#{@user.id} .section_#{@course_section.id} .unenroll_user_link').click()")
driver.switch_to.alert.accept
wait_for_ajaximations
driver.find_element(:id, 'tab-users').should include_text(@username)
driver.find_element(:css, ".user_#{@user.id}").should include_text(@old_section.name)
end
it "should change the correct section when editing" do
add_section("Section 3")
get "/courses/#{@course.id}/settings#tab-users"
driver.execute_script("$('.user_#{@user.id} .edit_section_link').click()")
click_option(".user_#{@user.id} .section_#{@old_section.id} .course_section_id", @course_section.name)
wait_for_ajaximations
driver.find_element(:css, ".user_#{@user.id}").should_not include_text(@old_section.name)
driver.find_element(:css, ".user_#{@user.id}").should include_text(@course_section.name)
go_to_users_tab
ff(".student_enrollments #user_#{@fake_student.id}").should be_empty
end
end

View File

@ -93,119 +93,7 @@ describe "courses" do
@course.storage_quota.should == 10.megabytes
end
it "should allow moving a student to a different section" do
# this spec does lots of find_element where we expect that it won't exist.
driver.manage.timeouts.implicit_wait = 0
c = course :active_course => true
users = {:plain => {}, :sis => {}}
[:plain, :sis].each do |sis_type|
[:student, :observer, :ta, :teacher].each do |enrollment_type|
user = {
:username => "#{enrollment_type}+#{sis_type}@example.com",
:password => "#{enrollment_type}#{sis_type}1"
}
user[:user] = user_with_pseudonym :active_user => true,
:username => user[:username],
:password => user[:password]
user[:enrollment] = c.enroll_user(user[:user], "#{enrollment_type.to_s.capitalize}Enrollment", :enrollment_state => 'active')
if sis_type == :sis
user[:enrollment].sis_source_id = "#{enrollment_type}.sis.1"
user[:enrollment].save!
end
users[sis_type][enrollment_type] = user
end
end
admin = {
:username => 'admin@example.com',
:password => 'admin1'
}
admin[:user] = account_admin_user :active_user => true
user_with_pseudonym :user=> admin[:user],
:username => admin[:username],
:password => admin[:password]
users[:plain][:admin] = admin
section = c.course_sections.create!(:name => 'M/W/F')
users[:plain].each do |user_type, logged_in_user|
# Students and Observers can't do anything
next if user_type == :student || user_type == :observer
create_session(logged_in_user[:user].pseudonyms.first, false)
get "/courses/#{c.id}/details"
driver.find_element(:css, '#tab-users-link').click
users.each do |sis_type, users2|
users2.each do |enrollment_type, user|
# Admin isn't actually enrolled
next if enrollment_type == :admin
# You can't move yourself
next if user == logged_in_user
enrollment = user[:enrollment]
enrollment_element = driver.find_element(:css, "#enrollment_#{enrollment.id}")
section_label = enrollment_element.find_element(:css, ".section_name") rescue nil
section_dropdown = enrollment_element.find_element(:css, ".enrollment_course_section_form .course_section_id") rescue nil
edit_section_link = enrollment_element.find_element(:css, ".edit_section_link") rescue nil
unenroll_user_link = enrollment_element.find_element(:css, ".unenroll_user_link") rescue nil
# Observers don't have a section
if enrollment_type == :observer
edit_section_link.should be_nil
section_label.should be_nil
next
end
section_label.should_not be_nil
section_label.should be_displayed
# "hover" over the user to make the links appear
driver.execute_script("$('.user_list #enrollment_#{enrollment.id} .links').css('visibility', 'visible')")
# All users can manage students; admins and teachers can manage all enrollment types
can_modify = enrollment_type == :student || [:admin, :teacher].include?(user_type)
if sis_type == :plain || logged_in_user == admin
section_dropdown.should_not be_displayed
if can_modify
edit_section_link.should_not be_nil
unenroll_user_link.should_not be_nil
# Move sections
edit_section_link.click
section_label.should_not be_displayed
section_dropdown.should be_displayed
section_dropdown.find_element(:css, "option[value=\"#{section.id.to_s}\"]").click
keep_trying_until { !section_dropdown.should_not be_displayed }
enrollment.reload
enrollment.course_section_id.should == section.id
section_label.should be_displayed
section_label.text.should == section.name
# reset this enrollment for the next user
enrollment.course_section = c.default_section
enrollment.save!
else
edit_section_link.should be_nil
unenroll_user_link.should be_nil
end
else
edit_section_link.should be_nil
if can_modify
unenroll_user_link.should_not be_nil
unenroll_user_link.should have_class('cant_unenroll')
else
unenroll_user_link.should be_nil
end
end
end
end
end
end
it "should redirect to the gradebook when switching courses when viewing a student's grades" do
it "should redirect to the gradebook when switching courses when viewing a student's grades" do
teacher = user_with_pseudonym(:username => 'teacher@example.com', :active_all => 1)
student = user_with_pseudonym(:username => 'student@example.com', :active_all => 1)
course1 = course_with_teacher_logged_in(:user => teacher, :active_all => 1).course

View File

@ -111,23 +111,6 @@ describe "people" do
add_user("Observers", @test_observer.name, 'ul.user_list.observer_enrollments')
end
it "should link an observer to student after adding the observer to the course" do
expect_new_page_load { driver.find_element(:link, 'Manage Users').click }
add_users_button = driver.find_element(:css, '.add_users_link')
add_users_button.click
add_user("Observers", @test_observer.name, 'ul.user_list.observer_enrollments')
ObserverEnrollment.count.should == 1
oe = ObserverEnrollment.first
find_with_jquery('.associated_user_link:visible').click #driver.find_element = element hidden
link_student_form = f('#link_student_dialog_form')
click_option('#student_enrollment_link_option', @student_1.name)
link_student_form.find_element(:css, '.save_button').click
wait_for_ajaximations
f(".enrollment_#{oe.id} .associated_user_name").should include_text @student_1.name
oe.reload.associated_user_id.should == @student_1.id
end
it "should make a new set of student groups" do
create_student_group
end

View File

@ -325,6 +325,7 @@ describe "quizzes" do
in_frame f('.essay_question iframe')[:id] do
f('#tinymce').send_keys :shift # no content, but it gives the iframe focus
end
wait_for_ajax_requests
ff('#question_list .answered').size.should eql 1
input[:value].should eql "1.0000"
end

View File

@ -9,7 +9,7 @@ describe "user" do
account.save!
end
def add_users_to_user_list(include_short_name = true, enrollment_type = 'StudentEnrollment')
def add_users_to_user_list(include_short_name = true, enrollment_type = 'StudentEnrollment', use_user_id = false)
user = User.create!(:name => 'login_name user')
user.pseudonyms.create!(:unique_id => "A124123", :account => @course.root_account)
user.communication_channels.create!(:path => "A124123")
@ -21,12 +21,9 @@ eolist
end
f("textarea.user_list").send_keys(user_list)
f("button.verify_syntax_button").click
wait_for_ajax_requests
f("button.add_users_button").click
wait_for_ajaximations
enrollment = user.reload.enrollments.last
extra_line = ""
extra_line = "\nlink to a student" if enrollment_type == 'ObserverEnrollment'
keep_trying_until { f("#enrollment_#{enrollment.id}").text.should == ("user, login_name" + (include_short_name ? "\nlogin_name user" : "") + "\nA124123" + extra_line) }
unique_ids = ["user1@example.com", "bob@thesagatfamily.name", "A124123"]
browser_text = ["user1@example.com\nuser1@example.com\nuser1@example.com", "sagat, bob\nbob sagat\nbob@thesagatfamily.name", "user, login_name\nlogin_name user\nA124123"] if include_short_name
browser_text = ["user1@example.com\nuser1@example.com", "sagat, bob\nbob@thesagatfamily.name", "user, login_name\nA124123"] unless include_short_name
@ -35,7 +32,8 @@ eolist
unique_ids.each do |id|
enrollment = find_enrollment_by_id(enrollments, id)
enrollment.should be_present
f("#enrollment_#{enrollment.id}").text.should == browser_text.shift + extra_line
selector = use_user_id ? "#user_#{enrollment.user_id}" : "#enrollment_#{enrollment.id}"
f(selector).text.should include(browser_text.shift)
end
end
@ -60,23 +58,23 @@ eolist
end
it "should support adding student enrollments" do
add_users_to_user_list(true)
add_users_to_user_list(true, 'StudentEnrollment', true)
end
it "should support adding teacher enrollments" do
add_users_to_user_list(true, 'TeacherEnrollment')
add_users_to_user_list(true, 'TeacherEnrollment', true)
end
it "should support adding Ta enrollments" do
add_users_to_user_list(true, 'TaEnrollment')
add_users_to_user_list(true, 'TaEnrollment', true)
end
it "should support adding observer enrollments" do
add_users_to_user_list(true, 'ObserverEnrollment')
add_users_to_user_list(true, 'ObserverEnrollment', true)
end
it "should support adding designer enrollments" do
add_users_to_user_list(true, 'DesignerEnrollment')
add_users_to_user_list(true, 'DesignerEnrollment', true)
end
end
@ -95,6 +93,6 @@ eolist
get "/courses/#{@course.id}/details"
f("#tab-users-link").click
f("#tab-users a.add_users_link").click
add_users_to_user_list
add_users_to_user_list(true, 'StudentEnrollment', true)
end
end

View File

@ -283,7 +283,7 @@ Spec::Runner.configure do |config|
def course_with_user(enrollment_type, opts={})
@course = opts[:course] || course(opts)
@user = opts[:user] || user(opts)
@enrollment = @course.enroll_user(@user, enrollment_type)
@enrollment = @course.enroll_user(@user, enrollment_type, opts)
@enrollment.course = @course # set the reverse association
if opts[:active_enrollment] || opts[:active_all]
@enrollment.workflow_state = 'active'

View File

@ -27,6 +27,7 @@ describe "courses/settings.html.erb" do
@course.workflow_state = 'claimed'
@course.save
assigns[:context] = @course
assigns[:user_counts] = {}
end
it "should not show to teacher" do