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:
parent
e4630f5b9f
commit
d16318c1e0
|
@ -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()
|
|
@ -1,2 +1 @@
|
|||
require ['user_lists']
|
||||
|
||||
require ['compiled/user_lists']
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'compiled/collections/PaginatedCollection'
|
||||
'compiled/models/User'
|
||||
], (Backbone, PaginatedCollection, User) ->
|
||||
|
||||
class UserCollection extends PaginatedCollection
|
||||
|
||||
model: User
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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...')}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -223,4 +223,3 @@ define [
|
|||
new TokenInput $(this), $.extend(true, {}, options)
|
||||
|
||||
TokenInput
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 ||= {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;">
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'), {
|
||||
|
|
|
@ -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);
|
||||
|
||||
});
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 = {}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue