remove old conversations and conversation submission comments

fixes CNVS-12330, CNVS-9234, CNVS-8099

before you check out
- configure a user to use old conversations

test plan
- ensure that everyone get new conversations, always, even if
 they explicitly told us that they really like old conversations
 better

- as a student with an existing conversation with a teacher,
 make an assignment submission and a submission comment
- as the teacher, ensure that your unread message count did not
 increase because of the submission comment

Change-Id: If5ae7143abbc5cf5e035f5ed9ea2e5728f70cd45
Reviewed-on: https://gerrit.instructure.com/34343
Reviewed-by: Braden Anderson <banderson@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Steven Shepherd <sshepherd@instructure.com>
Product-Review: Joel Hough <joel@instructure.com>
This commit is contained in:
Joel Hough 2014-05-05 15:12:00 -06:00
parent 5880ea9f92
commit 38c0bfd032
37 changed files with 48 additions and 5124 deletions

View File

@ -1,23 +0,0 @@
require [
'jquery'
'compiled/conversations/Inbox'
'jquery.google-analytics'
], ($, Inbox) ->
new Inbox(ENV.CONVERSATIONS)
# Google Analytics
$('#create_message_form').on 'click', 'div.token_input', (e) ->
$.trackEvent('Compose Message', 'Select Recipient', 'Text Field')
$('#create_message_form').on 'click', 'a.browser', (e) ->
$.trackEvent('Compose Message', 'Select Recipient', 'Picker Button')
$('body').on 'click', 'div.autocomplete_menu li.selectable', (e) ->
label = if $(e.currentTarget).hasClass('context') then 'Course/Group' else 'User'
$.trackEvent('Autocomplete', 'Click', label)
$('#context_tags_filter').on 'click', 'div.token_input', (e) ->
$.trackEvent('Filter', 'From/To', 'Text Field')
$('#context_tags_filter').on 'click', 'a.browser', (e) ->
$.trackEvent('Filter', 'From/To', 'Picker Button')

View File

@ -1,33 +0,0 @@
define [
'jquery'
'Backbone'
'jquery.ajaxJSON'
'jquery.disableWhileLoading'
], ($, Backbone) ->
class Conversation extends Backbone.Model
# NOTE: This class should be considered deprecated. Please be careful
# when modifying it, especially adding functionality.
#
# Try adding to app/coffeescripts/models/Conversation.coffee first,
# which is a version of this model that uses the API.
defaults:
audience: []
# we don't currently save the model directly, rather we do inbox actions
inboxAction: (options) ->
defaults =
url: @url()
method: 'POST'
success: (data) => @list.updateItem(data)
options = $.extend(true, {}, defaults, options)
options.data = $.extend(@list.baseData(), options.data ? {})
ajaxRequest = $.ajaxJSON options.url, options.method, options.data, (data) =>
options.success?(data)
@list.updateItem(data)
# TODO: use $el
@list.$item(@id)?.disableWhileLoading(ajaxRequest)
url: (action='') -> "/conversations/#{@id}/#{action}?#{$.param(@list.baseData())}"

View File

@ -1,162 +0,0 @@
define [
'i18n!conversations.conversations_list'
'jquery'
'compiled/widget/ScrollableList'
'compiled/conversations/Conversation'
'jst/conversations/conversationItem'
'jquery.instructure_date_and_time'
'jst/_avatar' # needed by conversationItem template
], (I18n, $, ScrollableList, Conversation, conversationItemTemplate) ->
class extends ScrollableList
constructor: (@pane, $scroller) ->
@app = @pane.app
@$empty = @pane.$pane.find('#no_messages')
super $scroller,
model: Conversation
itemTemplate: @conversationItem
elementHeight: 76
itemIdsKey: 'conversation_ids'
itemsKey: 'conversations'
sortKey: 'last_message_at'
sortDir: 'desc'
baseUrl: '/conversations?include_all_conversation_ids=1&include_beta=1'
noAutoLoad: true
$('#menu-wrapper').on('click', 'a.standard_action', @triggerConversationAction)
@$list.on('click', 'li[data-id] > a.standard_action', @triggerConversationAction)
@$list.on('mousedown keydown', 'button.al-trigger', @pane.filterMenu.bind(@pane))
$(window).unload(=> clearTimeout(@markAsUnread))
triggerConversationAction: (e) =>
e.preventDefault()
@pane.action($(e.currentTarget), method: 'PUT')
baseData: ->
{scope: @scope, filter: @filters}
load: (params, cb) ->
@scope = params.scope
@filters = params.filter ? []
super
sortKey: "#{@lastMessageKey()}_at"
params: @baseData()
force: params.force
loadId: params.id # if set, make sure it's loaded, and scroll into view
cb: =>
@emptyCheck()
cb?()
# item will either be the loadId's item or null
loaded: (id, item, $node) =>
if id and not item? # invalid id (deleted or not relevant to scope/filter)
@app.updateHashData id: null
else
@activate item, $node
added: (conversation, $node) ->
@$empty.hide()
updated: (conversation, $node) ->
@emptyCheck()
if @isActive(conversation.id) and conversation.get('workflow_state') is 'unread'
@markAsUnread = setTimeout =>
return unless @isActive(conversation.id) and @$item(conversation.id)
conversation.inboxAction
method: 'PUT'
data: {conversation: {workflow_state: 'read'}}
success: (data) -> data.defer_visibility_check = true
, 2000
removed: (data, $node) ->
@emptyCheck()
@activate(null) if @isActive(data.id)
clicked: (e) =>
# ignore clicks that come from the gear menu
unless $(e.target).closest('.admin-links').length
@select $(e.currentTarget).attr('data-id')
lastMessageKey: ->
if @scope is 'sent'
'last_authored_message'
else
'last_message'
emptyCheck: ->
map = @app.filterNameMap
text = switch @scope
when 'unread' then I18n.t 'no_unread_messages', 'You have no unread messages'
when 'starred' then I18n.t 'no_starred_messages', 'You have no starred messages'
when 'sent' then I18n.t 'no_sent_messages', 'You have no sent messages'
when 'archived' then I18n.t 'no_archived_messages', 'You have no archived messages'
else I18n.t 'no_messages', 'You have no messages'
filterNames = (map[i] for i in @filters when map[i])
text += " (#{(filterNames.join(', '))})" if filterNames.length
@$empty.text text
@$empty.showIf !@$items().length
select: (id, activate=true) ->
@ensureSelected(id, activate)
@app.updateHashData id: id if activate
isActive: (id) ->
@active and @active.id is id
deactivate: ->
return unless @active and item = @item(@active.id)
delete @active
@$item(item.id)?.removeClass('selected')
@removeItem(item) unless item.get('visible')
clearTimeout @markAsUnread
ensureSelected: (id, activate=true) ->
if activate # deselect any existing selection(s) ... soon we will have bulk conversation actions, so this will make more sense
@selected = []
@$items().removeClass('selected')
@deactivate() unless @isActive(id)
else
@selected ?= []
if id?
@$item(id).addClass('selected')
@selected.push id
activate: (conversation, $node) ->
if conversation and @isActive(conversation?.id)
@app.deselectMessages()
return
@ensureSelected(conversation?.id)
@active = conversation
@app.loadConversation @active, $node, =>
if $node?.hasClass 'unread'
# we've already done this server-side
$node.removeClass('read unread archived').addClass 'read'
conversationItem: (item) =>
data = $.extend({}, item.toJSON())
if data.participants
for user in data.participants when !@app.userCache[user.id]
@app.userCache[user.id] = user
if data.audience
data.audienceHtml = @app.htmlAudience(data, highlightFilters: true)
@app.formPane.refresh() if @isActive(data.id)
data.lastMessage = data[@lastMessageKey()]
data.lastMessageAt = $.friendlyDatetime($.fudgeDateForProfileTimezone(data[@lastMessageKey() + "_at"]))
data.hideCount = data.message_count is 1
classes = (property for property in data.properties)
classes.push data.workflow_state
classes.push 'private' if data['private']
classes.push 'starred' if data.starred
classes.push 'unsubscribed' unless data.subscribed
classes.push 'selected' if $.inArray(data.id, @selected) >= 0
data.classes = classes.join(' ')
conversationItemTemplate(data)

View File

@ -1,81 +0,0 @@
define [
'i18n!conversations.conversations_pane'
'jquery'
'compiled/conversations/ConversationsList'
'str/htmlEscape'
'compiled/util/shortcut'
'compiled/jquery/offsetFrom'
], (I18n, $, ConversationsList, h, shortcut) ->
class
shortcut this, 'list',
'baseData'
'updateItems'
'isActive'
constructor: (@app, @$pane) ->
@list = new ConversationsList(this, @$pane.find('> div.conversations'))
@selected = []
@initializeActions()
initializeActions: ->
$('#menu-wrapper').on 'click', 'a.action_delete_all', (e) =>
e.preventDefault()
if confirm I18n.t('confirm.delete_conversation', "Are you sure you want to delete your copy of this conversation? This action cannot be undone.")
@action($(e.currentTarget), method: 'DELETE')
updateView: (params) ->
@list.load params
action: ($actionNode, options) ->
conversationId = options.conversationId or
$actionNode.closest('div.conversations li').data('id') or
$actionNode.parents('ul[data-id]:first').data('id')
conversation = @list.item(conversationId)
options = $.extend(true, {}, {url: @actionUrlFor($actionNode, conversationId)}, options)
origCb = options.success
options.success = (data) =>
@app.addMessage(data.messages[0]) if data.messages?.length
origCb?(data)
conversation.inboxAction options
actionUrlFor: ($actionNode, conversationId) ->
url = $.replaceTags($actionNode.attr('href'), 'id', conversationId)
url + (if url.match(/\?/) then '&' else '?') + $.param(@baseData())
active: ->
@list.active
filterMenu: (e) ->
$conversation = $(e.currentTarget).parents('li:first')
$list = $(e.currentTarget).siblings('ul:first')
# reset visibility of all actions
$list.find('li').show()
# get current state of the conversation
isRead = $conversation.hasClass('read')
isStarred = $conversation.hasClass('starred')
isPrivate = $conversation.hasClass('private')
isSubscribed = !$conversation.hasClass('unsubscribed')
isArchived = $conversation.hasClass('archived')
# set action visibility based on current state
$list.find('.action_mark_as_read').parent().remove() if isRead
$list.find('.action_mark_as_unread').parent().remove() unless isRead
$list.find('.action_star').parent().remove() if isStarred
$list.find('.action_unstar').parent().remove() unless isStarred
$list.find('.action_archive').parent().remove() if isArchived
$list.find('.action_unarchive').parent().remove() unless isArchived
if isArchived
$list.find('.action_mark_as_read').parent().remove()
$list.find('.action_mark_as_unread').parent().remove()
if isPrivate
$list.find('.action_subscribe, .action_unsubscribe').parent().remove()
else
$list.find('.action_subscribe').parent().remove() if isSubscribed
$list.find('.action_unsubscribe').parent().remove() unless isSubscribed
resize: (newHeight) ->
@list.$scroller.height(newHeight - $('#actions').outerHeight(true))
@list.fetchVisible()

View File

@ -1,639 +0,0 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'i18n!conversations'
'jquery'
'underscore'
'str/htmlEscape'
'compiled/conversations/introSlideshow'
'compiled/conversations/ConversationsPane'
'compiled/conversations/MessageFormPane'
'compiled/conversations/audienceList'
'compiled/util/contextList'
'compiled/widget/ContextSearch'
'compiled/str/TextHelper'
'jst/_avatar'
'jquery.ajaxJSON'
'jquery.instructure_date_and_time'
'jquery.instructure_forms'
'jqueryui/dialog'
'jquery.instructure_misc_helpers'
'jquery.disableWhileLoading'
'compiled/jquery.rails_flash_notifications'
'compiled/jquery/offsetFrom'
'media_comments'
'vendor/jquery.ba-hashchange'
'vendor/jquery.elastic'
'jqueryui/position'
], (I18n, $, _, h, introSlideshow, ConversationsPane, MessageFormPane, audienceList, contextList, TokenInput, TextHelper, avatarPartial) ->
class
constructor: (@options) ->
@currentUser = @options.USER
@contexts = @options.CONTEXTS
@userCache = {}
@userCache[@currentUser.id] = @currentUser
$ @render
render: =>
@$inbox = $('#inbox')
@minHeight = parseInt @$inbox.css('min-height').replace('px', '')
@$conversations = $('#conversations')
@$messages = $('#messages')
@$messageList = @$messages.find('ul.messages')
@$others = $('<div class="others" id="others_popup" />')
@initializeHelp()
@initializeForms()
@initializeMenus()
@initializeMessageActions()
@initializeTokenInputs()
@initializeConversationsPane()
@initializeMessageFormPane()
@initializeAutoResize()
@initializeHashChange()
if @options.SHOW_INTRO
introSlideshow()
filters: ->
@conversations.baseData().filter ? []
htmlAudience: (conversation, options = {}) ->
conversation ?= @conversations.active()?.attributes
unless conversation?
return h(I18n.t('headings.new_message', 'New Message'))
filters = options.filters = if options.highlightFilters then @filters() else []
audience = for id in conversation.audience
{
id: id
name: @userCache[id].name
activeFilter: _.include(filters, "user_#{id}")
}
ret = audienceList(audience, options)
if audience.length
ret += " <em>" + @htmlContextList(conversation.audience_contexts, options) + "</em>"
ret
htmlContextList: (contexts, options = {}) ->
contexts = {courses: _.keys(contexts.courses), groups: _.keys(contexts.groups)}
contextList(contexts, @contexts, options)
htmlNameForUser: (user, contexts = {courses: user.common_courses, groups: user.common_groups}) ->
h(user.name) + if contexts.courses?.length or contexts.groups?.length then " <em>" + @htmlContextList(contexts) + "</em>" else ''
canAddNotesFor: (userOrId) =>
return false unless @options.NOTES_ENABLED
user = if typeof userOrId is 'object' then userOrId else @userCache[userOrId]
return false unless user?
for id, roles of user.common_courses
return true if 'StudentEnrollment' in roles and (@options.CAN_ADD_NOTES_FOR_ACCOUNT or @contexts.courses[id]?.permissions?.manage_user_notes)
false
loadConversation: (conversation, $node, cb) ->
@$messageList.removeClass('private').hide().html ''
@$messageList.addClass('private') if $conversation?.hasClass('private')
@resetMessageForm(conversation)
@toggleMessageActions(off)
return cb() unless conversation?
url = "#{conversation.url()}&include_beta=1"
@$messageList.show().disableWhileLoading $.ajaxJSON url, 'GET', {}, (data) =>
@conversations.updateItems [data]
return unless @conversations.isActive(data.id)
for user in data.participants when !@userCache[user.id]?.avatar_url
@userCache[user.id] = user
user.htmlName = @htmlNameForUser(user)
if data['private'] and user = (user for user in data.participants when user.id isnt @currentUser.id)[0]
@formPane.resetForParticipant(user)
@resize()
@$messages.show()
@currentConversation = data
@$messageList.append @buildMessage(message) for message in data.messages
@$messageList.show()
@formPane.form.setAuthor(data.messages, data.participants)
cb()
resetMessageForm: (conversation) ->
$('#action_compose_message').toggleClass 'active', !conversation?
baseData = @conversations.baseData()
@formPane.reset(_.extend({}, @currentHashData(),
conversation: conversation
audience: @htmlAudience(null, linkToContexts: true, highlightFilters: true)
addRecipientsEnabled: conversation? and !conversation.get('private')
mediaCommentsEnabled: @options.MEDIA_COMMENTS_ENABLED
filter: baseData.filter
scope: baseData.scope
))
updatedConversation: (data) ->
@formPane.refresh @htmlAudience(null, linkToContexts: true, highlightFilters: true)
return unless data.length
@conversations.updateItems data
if data.length is 1
conversation = data[0]
if @conversations.isActive(conversation.id)
@buildMessage(conversation.messages[0]).prependTo(@$messageList).slideDown 'fast'
if conversation.visible
@updateHashData id: conversation.id
deselectMessages: ->
@$messageList.find('li.selected').removeClass 'selected'
addMessage: (message) ->
@toggleMessageActions(off)
@buildMessage(message).prependTo(@$messageList).slideDown 'fast'
UNKNOWN_USER_NAMES: [I18n.t('unknown_user', 'Unknown user'), h(I18n.t('unknown_user', 'Unknown user'))]
# Returns [userName, htmlName]
userNames: (user) ->
return @UNKNOWN_USER_NAMES unless user
user.htmlName ?= @htmlNameForUser(user)
[user.name, user.htmlName]
buildMessage: (data) ->
return @buildSubmission(data) if data.submission
$message = $("#message_blank").clone(true).attr('id', 'message_' + data.id)
$message.data('id', data.id)
$message.addClass(if data.generated
'generated'
else if data.author_id is @currentUser.id
'self'
else
'other'
)
$message.addClass('forwardable')
user = @userCache[data.author_id]
[userName, htmlName] = @userNames user
$message.prepend avatarPartial avatar_url: user.avatar_url, display_name: userName if user
$message.find('.audience').html htmlName
$message.find('span.date').text $.datetimeString(data.created_at)
$message.find('p').html TextHelper.formatMessage(data.body)
$message.find("a.show_quoted_text_link").click (e) =>
$target = $(e.currentTarget)
$text = $target.parents(".quoted_text_holder").children(".quoted_text")
if $text.length
event.stopPropagation()
event.preventDefault()
$text.show()
$target.hide()
$pmAction = $message.find('a.send_private_message')
$pmAction.on 'click', (e) =>
e.preventDefault()
e.stopPropagation()
user = @userCache[data.author_id]
# Click the "New Message" button and after a short delay,
# add the clicked user's token to the input.
$('#action_compose_message').trigger('click')
clearTimeout @addUserTokenCb if @addUserTokenCb
@addUserTokenCb = setTimeout =>
delete @addUserTokenCb
@formPane.form.addToken
value: user.id
text: user.name
data: user
,
100
if data.forwarded_messages?.length
$ul = $('<ul class="messages"></ul>')
for submessage in data.forwarded_messages
$ul.append @buildMessage(submessage)
$message.append $ul
$ul = $message.find('ul.message_attachments').first().detach()
$mediaObjectBlank = $ul.find('.media_object_blank').detach()
$attachmentBlank = $ul.find('.attachment_blank').detach()
if data.media_comment? or data.attachments?.length
$message.append $ul
if data.media_comment?
$ul.append @buildMediaObject($mediaObjectBlank, data.media_comment)
if data.attachments?
for attachment in data.attachments
$ul.append @buildAttachment($attachmentBlank, attachment)
$message
buildMediaObject: (blank, data) ->
$mediaObject = blank.clone(true).attr('id', 'media_comment_' + data.media_id)
$mediaObject.find('span.title').html h(data.display_name)
$mediaObject.find('span.media_comment_id').html h(data.media_id)
$mediaObject.find('.instructure_inline_media_comment').data('media_comment_type', data.media_type)
$mediaObject
buildAttachment: (blank, data) ->
$attachment = blank.clone(true).attr('id', 'attachment_' + data.id)
$attachment.data('id', data.id)
$attachment.find('span.title').html h(data.display_name)
$link = $attachment.find('a')
$link.attr('href', data.url)
$link.click (e) =>
e.stopPropagation()
$attachment
buildSubmission: (data) ->
$submission = $("#submission_blank").clone(true).attr('id', data.id)
$submission.data('id', data.id)
data = data.submission
$ul = $submission.find('ul')
$header = $ul.find('li.header')
href = $.replaceTags($header.find('a').attr('href'), course_id: data.assignment.course_id, assignment_id: data.assignment_id, id: data.user_id)
$header.find('a').attr('href', href)
user = @userCache[data.user_id]
[userName, htmlName] = @userNames user
$header.find('.title').html h(data.assignment.name)
$header.find('span.date').text(if data.submitted_at
$.datetimeString(data.submitted_at)
else
I18n.t('not_applicable', 'N/A')
)
$header.find('.audience').html htmlName
if data.score && data.assignment.points_possible
score = "#{data.score} / #{data.assignment.points_possible}"
else
score = data.score ? I18n.t('not_scored', 'no score')
$header.find('.score').html(score)
$commentBlank = $ul.find('.comment').detach()
index = 0
initiallyShown = 4
for idx in [data.submission_comments.length - 1 .. 0] by -1
comment = data.submission_comments[idx]
break if index >= 10
index++
$comment = @buildSubmissionComment($commentBlank, comment)
$comment.hide() if index > initiallyShown
$ul.append $comment
$moreLink = $ul.find('.more').detach()
# the submission response isn't yet paginating/limiting the number of
# comments returned, but we don't want to display more than 10 here, so we
# artificially limit it.
if index > initiallyShown
$inlineMore = $moreLink.clone(true)
$inlineMore.find('.hidden').text(index - initiallyShown)
$inlineMore.attr('title', h(I18n.t('titles.expand_inline', "Show more comments")))
$inlineMore.click (e) =>
$target = $(e.currentTarget)
$submission = $target.closest('.submission')
$submission.find('.more:hidden').show()
$target.hide()
$submission.find('.comment:hidden').slideDown('fast')
@resize()
return false
$ul.append $inlineMore
if data.submission_comments.length > index
$moreLink.find('a').attr('href', href).attr('target', '_blank')
$moreLink.find('.hidden').text(data.submission_comments.length - index)
$moreLink.attr('title', h(I18n.t('titles.view_submission', "Open submission in new window.")))
$moreLink.hide() if data.submission_comments.length > initiallyShown
$ul.append $moreLink
$submission
buildSubmissionComment: (blank, data) ->
$comment = blank.clone(true)
user = @userCache[data.author_id]
[userName, htmlName] = @userNames user
$comment.prepend avatarPartial avatar_url: user.avatar_url, display_name: userName if user
$comment.find('.audience').html htmlName
$comment.find('span.date').text $.datetimeString(data.created_at)
$comment.find('p').html h(data.comment).replace(/\n/g, '<br />')
$comment
closeMenus: () ->
$('#actions .menus > li, #conversation_actions, #conversations .actions').removeClass('selected')
openMenu: ($menu) ->
@closeMenus()
unless $menu.hasClass('disabled')
$div = $menu.parent('li, span').addClass('selected').find('div')
# TODO: move this out in the DOM so we can center it and not have it get clipped
offset = -($div.parent().position().left + $div.parent().outerWidth() / 2) + 6 # for box shadow
offset = -($div.outerWidth() / 2) if offset < -($div.outerWidth() / 2)
$div.css 'margin-left', offset + 'px'
resize: (delay=0) ->
clearTimeout @resizeCb if @resizeCb
@resizeCb = setTimeout =>
delete @resizeCb
availableHeight = $(window).height() - $('#header').outerHeight(true) - ($('#wrapper-container').outerHeight(true) - $('#wrapper-container').height()) - ($('#main').outerHeight(true) - $('#main').height()) - $('#breadcrumbs').outerHeight(true) - $('#footer').outerHeight(true)
availableHeight = @minHeight if availableHeight < @minHeight
$(document.body).toggleClass('too_small', availableHeight <= @minHeight)
@$inbox.height(availableHeight)
@$messageList.height(availableHeight - @formPane.height())
@conversations.resize(availableHeight)
, delay
toggleMessageActions: (state) ->
if state?
@$messageList.find('> li').removeClass('selected')
@$messageList.find('> li :checkbox').attr('checked', false)
else
state = !!@$messageList.find('li.selected').length
$('#action_forward').parent().showIf(state and @$messageList.find('li.selected.forwardable').length)
if state then $("#message_actions").slideDown(100) else $("#message_actions").slideUp(100)
@formPane.toggle(state)
updateHashData: (changes) ->
data = $.extend(@currentHashData(), changes)
hash = $.encodeToHex(JSON.stringify(data))
if hash isnt location.hash.substring(1)
location.hash = hash
$(document).triggerHandler('document_fragment_change', hash)
initializeHelp: ->
$('#conversations-intro-menu-item, #conversations-intro-btn').click (e) =>
e.preventDefault()
introSlideshow()
prepareTextareas: ($nodes) ->
$nodes.elastic()
$nodes.keypress (e) =>
if e.which is 13 and e.shiftKey
$(e.target).closest('form').submit()
false
initializeForms: ->
@$addForm = $('#add_recipients_form')
@$forwardForm = $('#forward_message_form')
@prepareTextareas(@$forwardForm.find('textarea'))
@$addForm.submit (e) =>
valid = !!(@$addForm.find('.token_input li').length)
e.stopImmediatePropagation() unless valid
valid
@$addForm.formSubmit
disableWhileLoading: true,
success: (data) =>
@updatedConversation(data)
@$addForm.dialog('close')
error: (data) =>
@$addForm.dialog('close')
@$forwardForm.submit (e) =>
valid = !!(@$forwardForm.find('#forward_body').val() and @$forwardForm.find('.token_input li').length)
e.stopImmediatePropagation() unless valid
valid
@$forwardForm.formSubmit
disableWhileLoading: true,
success: (data) =>
@updatedConversation(data)
@$forwardForm.dialog('close')
error: (data) =>
@$forwardForm.dialog('close')
@$messageList.click (e) =>
if $(e.target).closest('a.instructure_inline_media_comment, .mejs-container').length
# a.instructure_inline_media_comment clicks have to propagate to the
# top due to "live" handling; if it's one of those, it's not really
# intended for us, just let it go
# also, dont catch clicks on mediaelementjs videos. that is for play/pause
else
$message = $(e.target).closest('#messages > ul > li')
unless $message.hasClass('generated')
$message.toggleClass('selected')
$message.find('> :checkbox').attr('checked', $message.hasClass('selected'))
@toggleMessageActions()
initializeMenus: =>
$('.menus > li > a').click (e) =>
e.preventDefault(e)
@openMenu $(e.currentTarget)
.focus (e) =>
@openMenu $(e.currentTarget)
$(document).bind 'mousedown', (e) =>
unless $(e.target).closest("#others_popup").length
@$others.hide()
@closeMenus() unless $(e.target).closest(".menus > li, #conversation_actions, #conversations .actions").length
@$menuViews = $('#menu_views')
@$menuViewsList = @$menuViews.parent()
@$menuViewsList.find('li a').click (e) =>
@closeMenus()
if scope = $(e.target).closest('li').data('scope')
e.preventDefault()
@updateHashData scope: scope
$('#conversations ul, #create_message_form').on 'click', '.others', (e) =>
$this = $(e.currentTarget)
$container = $this.closest('li').offsetParent()
offset = $this.offsetFrom($container)
@$others.empty().append($this.find('> span').clone()).css
left: offset.left
top: offset.top + $this.height() + $container.scrollTop()
fontSize: $this.css('fontSize')
$container.append(@$others.show())
return false # i.e. don't select conversation
setScope: (scope) ->
$items = @$menuViewsList.find('li')
$items.removeClass('checked')
$item = $items.filter("[data-scope=#{scope}]")
$item = $items.filter("[data-scope=inbox]") unless $item.length
$item.addClass('checked')
@$menuViews.text $item.text()
initializeMessageActions: ->
$('#message_actions').find('a').click (e) =>
e.preventDefault()
$('#cancel_bulk_message_action').click =>
@toggleMessageActions off
$('#action_delete').click (e) =>
active = @conversations.active()
return unless active?
$selectedMessages = @$messageList.find('.selected')
message = if $selectedMessages.length > 1
I18n.t('confirm.delete_messages', "Are you sure you want to delete your copy of these messages? This action cannot be undone.")
else
I18n.t('confirm.delete_message', "Are you sure you want to delete your copy of this message? This action cannot be undone.")
if confirm message
$selectedMessages.fadeOut 'fast'
@conversations.action $(e.currentTarget),
conversationId: active.id
data: {remove: ($(message).data('id') for message in $selectedMessages)}
success: => @toggleMessageActions(off)
error: => $selectedMessages.show()
$('#action_forward').click (e) =>
return unless @conversations.active()?
@$forwardForm.find("input[name!=authenticity_token], textarea").val('').change()
$preview = @$forwardForm.find('ul.messages').first()
$preview.html('')
$preview.html(@$messageList.find('> li.selected.forwardable').clone(true).removeAttr('id').removeClass('self'))
$preview.find('> li')
.removeClass('selected odd')
.find('> :checkbox')
.attr('checked', true)
.attr('name', 'forwarded_message_ids[]')
.val ->
$(this).closest('li').data('id')
$preview.find('> li').last().addClass('last')
@$forwardForm.css('max-height', ($(window).height() - 300) + 'px')
.dialog
position: 'center'
height: 'auto'
width: 510
title: I18n.t('title.forward_messages', 'Forward Messages')
buttons: [
text: I18n.t('#buttons.cancel', 'Cancel')
click: -> $(this).dialog('close')
,
text: I18n.t('buttons.send_message', 'Send')
click: -> $(this).submit()
class: 'btn-primary'
]
open: =>
@$forwardForm.attr action: '/conversations?' + $.param(@conversations.baseData())
close: =>
$('#forward_recipients').data('token_input').$input.blur()
$('#action_compose_message').click (e) =>
e.preventDefault()
@updateHashData id: null
addRecipients: ($node) ->
return unless @conversations.active()?
@$addForm
.attr('action', $node.prop('href'))
.dialog
width: 420
title: I18n.t('title.add_recipients', 'Add Recipients')
buttons: [
{
text: I18n.t('buttons.add_people', 'Add People')
click: => @$addForm.submit()
}
{
text: I18n.t('#buttons.cancel', 'Cancel')
click: => @$addForm.dialog('close')
}
]
open: =>
tokenInput = $('#add_recipients').data('token_input')
tokenInput.baseExclude = @conversations.active().get('audience')
@$addForm.find("input[name!=authenticity_token]").val('').change()
close: =>
$('#add_recipients').data('token_input').$input.blur()
initializeTokenInputs: ($scope) ->
everyoneText = I18n.t('enrollments_everyone', "Everyone")
selectAllText = I18n.t('select_all', "Select All")
($scope ? $(document)).find('.recipients').contextSearch
contexts: @contexts
added: (data, $token, newToken) =>
data.id = "#{data.id}"
if newToken and data.rootId
$token.append("<input type='hidden' name='tags[]' value='#{data.rootId}'>")
if newToken and data.type
$token.addClass(data.type)
if data.user_count?
$token.addClass('details')
$details = $('<span />')
$details.text(I18n.t('people_count', 'person', {count: data.user_count}))
$token.append($details)
$token.data('user_count', data.user_count)
unless data.id.match(/^(course|group)_/)
data = $.extend({}, data)
delete data.avatar_url # since it's the wrong size and possibly a blank image
currentData = @userCache[data.id] ? {}
@userCache[data.id] = $.extend(currentData, data)
canToggle: (data) ->
data.type is 'user' or data.permissions?.send_messages_all
selector:
showToggles: true
includeEveryoneOption: (postData, parent) =>
# i.e. we are listing synthetic contexts under a course or section
if postData.context?.match(/^(course|section)_\d+$/)
everyoneText
includeSelectAllOption: (postData, parent) =>
# i.e. we are listing all users in a group or synthetic context
if postData.context?.match(/^((course|section)_\d+_.*|group_\d+)$/) and not postData.context?.match(/^(course|section)_\d+$/) and not postData.context?.match(/^course_\d+_(groups|sections)$/) and parent.data('user_data').permissions.send_messages_all
selectAllText
baseData:
permissions: ["send_messages_all"]
messageable_only: true
return if $scope
@filterNameMap = {}
$('#context_tags').contextSearch
contexts: @contexts
prefixUserIds: true
added: (data, $token, newToken) =>
$token.prevAll().remove() # only one token at a time
tokenWrapBuffer: 80
selector:
includeEveryoneOption: (postData, parent) =>
if postData.context?.match(/^course_\d+$/)
everyoneText
includeFilterOption: (postData) =>
if postData.context?.match(/^course_\d+$/)
I18n.t('filter_by_course', 'Filter by this course')
else if postData.context?.match(/^group_\d+$/)
I18n.t('filter_by_group', 'Filter by this group')
baseData:
synthetic_contexts: 1
types: ['course', 'user', 'group']
include_inactive: true
blank_avatar_fallback: false
filterInput = $('#context_tags').data('token_input')
filterInput.change = (tokenValues) =>
filters = for pair in filterInput.tokenPairs()
@filterNameMap[pair[0]] = pair[1]
pair[0]
@updateHashData filter: filters
initializeConversationsPane: () ->
@conversations = new ConversationsPane this, @$conversations
initializeMessageFormPane: () ->
@formPane = new MessageFormPane(this, folderId: @options.FOLDER_ID)
addedMessageForm: ($form) ->
@prepareTextareas($form.find('textarea'))
@initializeTokenInputs($form)
initializeAutoResize: ->
$(window).resize => @resize(50)
@resize()
currentHashData: ->
try
data = $.parseJSON($.decodeFromHex(location.hash.substring(1))) || {}
catch e
data = {}
data
updateView: (force = false) =>
hash = location.hash
data = @currentHashData()
data.force = force
if data.filter
data.filter = (id for id in data.filter when @filterNameMap[id])
return @updateHashData(filter: null) if not data.filter.length
@setScope(data.scope)
@conversations.updateView(data)
initializeHashChange: ->
$(window).bind('hashchange', => @updateView()).triggerHandler('hashchange')

View File

@ -1,167 +0,0 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'i18n!conversations'
'jquery'
'underscore'
'compiled/util/shortcut'
'jst/conversations/MessageForm'
'jst/conversations/addAttachment'
], (I18n, $, _, shortcut, messageFormTemplate, addAttachmentTemplate) ->
class MessageForm
shortcut this, 'pane',
'resize'
constructor: (@pane, @canAddNotesFor, @options) ->
templateOptions = _.extend({}, @options, conversation: @options.conversation?.toJSON())
@$form = $(messageFormTemplate(templateOptions))
@$mediaComment = @$form.find('.media_comment')
@$mediaCommentId = @$form.find("input[name=media_comment_id]")
@$mediaCommentType = @$form.find("input[name=media_comment_type]")
@$addMediaComment = @$form.find(".action_media_comment")
@$attachments = @$form.find('.attachment_list')
initialize: ->
if @tokenInput = @$form.find('.recipients').data('token_input')
# since it doesn't infer percentage widths, just whatever the current pixels are
@tokenInput.$fakeInput.css('width', '100%')
if @options.user_id
query = { user_id: @options.user_id, from_conversation_id: @options.from_conversation_id }
$.ajaxJSON @tokenInput.selector.url, 'GET', query, (data) =>
if data.length
@tokenInput.addToken
value: data[0].id
text: data[0].name
data: data[0]
@initializeActions()
if !$(document.activeElement).filter(':input').length and window.location.hash isnt ''
@$form.find(':input:visible:first').focus()
setAuthor: (messages, participants) ->
@messageList or= messages.reverse()
message = _.find(@messageList, (m) -> m.author_id isnt ENV.current_user_id)
return unless message
@messageAuthor = _.find(participants, (p) -> p.id == message.author_id)
initializeActions: ->
if @tokenInput
@tokenInput.change = @recipientIdsChanged
$('[type=submit]').on('click', ((e) => @replyToAuthor = true))
$('[type=button]').on('click', ((e) => @$form.submit()))
@$form.formSubmit
fileUpload: => (@$form.find(".file_input:visible").length > 0)
preparedFileUpload: true
context_code: "user_" + ENV.current_user_id
folder_id: @options.folderId
intent: 'message'
formDataTarget: 'url'
disableWhileLoading: true
required: ['body']
property_validations:
token_capture: => I18n.t('errors.field_is_required', "This field is required") if @tokenInput and !@tokenInput.tokenValues().length
handle_files: (attachments, data) ->
data.attachment_ids = (a.attachment.id for a in attachments)
data
onSubmit: (@request, data) =>
if !@messageAuthor and @pane.app.currentConversation
conversation = @pane.app.currentConversation
@setAuthor(conversation.messages, conversation.participants)
if @options.conversation?.get('beta') and @replyToAuthor
data['recipients[]'] = @messageAuthor.id
@pane.addingMessage(@messageData(data), @request)
@replyToAuthor = false
true
recipientIdsChanged: (recipientIds) =>
if recipientIds.length > 1 or recipientIds[0]?.match(/^(course|group)_/)
@toggleOptions(user_note: off, group_conversation: on)
else
@toggleOptions(user_note: @canAddNotesFor(recipientIds[0]), group_conversation: off)
@resize()
addAttachment: ->
$attachment = $(addAttachmentTemplate())
@$attachments.append($attachment)
$attachment.slideDown "fast", => @resize() # shortcuts aren't bound to instance, so this don't "optimize" this :P
removeAttachment: ($node) ->
$attachment = $node.closest(".attachment")
$attachment.slideUp "fast", =>
@resize()
$attachment.remove()
addToken: (userData) ->
input = @$form.find('.recipients').data('token_input')
input.addToken(userData) if input
addMediaComment: ->
@$mediaComment.mediaComment 'create', 'any', (id, type) =>
@$mediaCommentId.val(id)
@$mediaCommentType.val(type)
@$mediaComment.show()
@$addMediaComment.hide()
removeMediaComment: ->
@$mediaCommentId.val('')
@$mediaCommentType.val('')
@$mediaComment.hide()
@$addMediaComment.show()
messageData: (data) ->
numRecipients = if @options.conversation
if data['recipients[]']
1
else
Math.max(@options.conversation.get('audience').length, 1)
else
# note: this number may be high, if users appear in multiple of the
# specified recipient contexts. there's no way of knowing without going
# to the server first, which is what we're trying to avoid.
_.reduce @tokenInput.$tokens.find('input[name="recipients[]"]'),
(memo, node) -> memo + ($(node).closest('li').data('user_count') ? 1),
0
{recipient_count: numRecipients, message: {body: data.body}}
resetForParticipant: (user) ->
@toggleOptions(user_note: on) if @canAddNotesFor(user)
toggleOptions: (options) ->
for key, enabled of options
$node = @$form.find(".#{key}_info")
$node.showIf(enabled)
$node.find("input[type=checkbox][name=#{key}]").prop('checked', false) unless enabled
toggle: (state) ->
@$form[if state then 'addClass' else 'removeClass']('disabled')
height: ->
@$form.outerHeight(true)
refresh: (audienceHtml) ->
@$form.find('.audience').html audienceHtml
@resize()
destroy: ->
@$form.hideErrors()
@$form.css(position: 'absolute', zIndex: -1)
$.when(@request).then => @$form.remove()

View File

@ -1,76 +0,0 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'i18n!conversations'
'jquery'
'underscore'
'compiled/util/shortcut'
'compiled/conversations/MessageForm'
'compiled/conversations/MessageProgressTracker'
'compiled/fn/preventDefault'
], (I18n, $, _, shortcut, MessageForm, MessageProgressTracker, preventDefault) ->
class MessageFormPane
shortcut this, 'form',
'refresh'
'toggle'
'resetForParticipant'
shortcut this, 'app',
'resize'
constructor: (@app, @formOptions) ->
@$node = $('#create_message_form')
@initializeActions()
@tracker = new MessageProgressTracker(@app)
@tracker.batchPoller()
height: ->
(@form?.height() ? 0) + @tracker.height()
reset: (@options) ->
@form?.destroy()
@form = new MessageForm(this, @app.canAddNotesFor, _.defaults(@options, @formOptions))
@$node.append(@form.$form)
@app.addedMessageForm(@form.$form)
@form.initialize()
initializeActions: ->
@$node.click => @app.toggleMessageActions off
@$node.on 'click', '.action_add_attachment', preventDefault =>
@form.addAttachment()
@$node.on 'click', '.attachment a.remove_link', preventDefault (e) =>
@form.removeAttachment($(e.currentTarget))
@$node.on 'click', '.action_media_comment', preventDefault =>
@form.addMediaComment()
@$node.on 'click', '.media_comment a.remove_link', preventDefault =>
@form.removeMediaComment()
@$node.on 'click', '.action_add_recipients', preventDefault (e) =>
@app.addRecipients($(e.currentTarget))
addingMessage: (data, deferred) ->
@reset(@options)
@tracker.track(data, deferred)
$.when(deferred).then (data) =>
data = [data] unless data.length?
@app.updatedConversation(data)

View File

@ -1,74 +0,0 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'i18n!conversations'
'jquery'
'underscore'
'jst/conversations/MessageProgressBarText'
'compiled/str/TextHelper'
'jquery.ajaxJSON'
], (I18n, $, _, messageProgressBarTextTemplate, {truncateText}) ->
class MessageProgressBar
constructor: (@tracker, @data) ->
@$node = $('<li class="progress-bar" />')
messageId = _.uniqueId('progress_')
@$message = $('<span />', id: messageId)
@$bar = $('<div />',
tabIndex: -1
role: 'progressbar'
'aria-valuemin': 0
'aria-valuemax': 1
'aria-valuenow': @data.completion
'aria-describedby': messageId
)
@$completion = $('<b />').appendTo(@$bar)
@$node.append(@$message, @$bar)
@update(@data)
update: (@data) ->
@data.status = if @data.error
'error'
else if @data.completion?
if @data.completion is 1 then 'complete' else 'determinate'
else
'indeterminate'
@data.num_people = I18n.t('people_count', 'person', {count: @data.recipient_count})
@data.message_preview = truncateText(@data.message.body, max: 20)
@$node.attr('class', "progress-bar progress-bar-#{@data.status}")
@$message.html messageProgressBarTextTemplate(@data)
@$message.attr title: @data.message_preview
@$bar.showIf(@data.status isnt 'error')
percent = parseInt(100 * (@data.completion ? 0)) + "%"
@$bar.attr('aria-valuenow', @data.completion)
@$completion.css width: percent
error: (error) ->
@update _.extend(@data, error: error, completion: 1)
@destroy()
complete: () ->
@update _.extend(@data, completion: 1)
@destroy()
destroy: () ->
setTimeout =>
@$node.fadeTo('fast', 0).animate(width: 0, 'fast', => @$node.remove())
, 5000

View File

@ -1,87 +0,0 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'i18n!conversations'
'jquery'
'underscore'
'compiled/conversations/MessageProgressBar'
'jquery.ajaxJSON'
], (I18n, $, _, MessageProgressBar) ->
class MessageProgressTracker
constructor: (@app) ->
@batchItems = {}
@$list = $('#message_status')
track: (data, deferred) ->
item = new MessageProgressBar(this, data)
@$list.append(item.$node)
item.$bar.focus()
# when the formSubmit deferred is done, we're done, unless this is a bulk
# private message, in which case we kick of the poller/updater fu to
# track its progress
if deferred
$.when(deferred).then (data, submitParam, xhr) =>
if xhr.status is 202
if batchId = xhr.getResponseHeader('X-Conversation-Batch-Id')
@batchItems[batchId] = item
@batchPoller() unless @polling
else
item.complete()
, (data) =>
if data.isRejected?() # e.g. refreshed the page, thus aborting the request
item.complete()
else
error = if data[0]?.attribute is 'recipients' and data[0].message is 'invalid'
I18n.t('recipient_error', 'The course or group you have selected has no valid recipients')
else if data[0]?.attribute is 'attachment' and data[0].message is 'upload failed'
I18n.t('attachment_error', 'Attachment failed to upload, please try again.')
else
I18n.t('unspecified_error', 'An unexpected error occurred, please try again')
item.error(error)
item
batchPoller: =>
@polling = true
$.ajaxJSON '/conversations/batches', 'GET', {}, (data) =>
@updateItems(data)
if data.length > 0
setTimeout(@batchPoller, 3000)
else
@polling = false
updateItems: (data) ->
dataHash = _.reduce(data, (h, i) ->
h[i.id] = i
h
, {})
for id, data of dataHash
@batchItems[id]?.update(data) ? @batchItems[id] = @track(data)
# remove stuff that has finished
completed = for id, item of @batchItems when not dataHash[id]
item.complete()
delete @batchItems[id]
if completed.length
@app.updateView(true)
height: ->
@$list.outerHeight(true)

View File

@ -1,24 +0,0 @@
define [
'i18n!conversations'
'jquery'
'underscore'
'str/htmlEscape'
'compiled/util/listWithOthers'
'jquery.instructure_misc_helpers'
], (I18n, $, _, h, listWithOthers) ->
format = (person) ->
str = h(person.name)
str = "<span class='active-filter'>#{str}</span>" if person.activeFilter
$.raw str
(audience, options={}) ->
if options.highlightFilters
audience = _.groupBy(audience, (user) -> user.activeFilter)
audience = (audience[true] ? []).concat(audience[false] ? [])
audience = (format(person) for person in audience)
if audience.length == 0
"<span>#{h(I18n.t('notes_to_self', 'Monologue'))}</span>"
else
listWithOthers(audience)

View File

@ -1,60 +0,0 @@
define [
'i18n!conversations_intro'
'jquery'
'compiled/widget/slideshow'
'jquery.ajaxJSON'
], (I18n, $, Slideshow) ->
->
introSlideshow = new Slideshow('conversations_intro')
introSlideshow.addSlide I18n.t('titles.slide1', 'Slide 1'), (slide) ->
slide.addImage('/images/conversations/intro/icon.png', 'icon')
slide.addParagraph(I18n.t('slide1.paragraph1', 'Take a look at your Inbox!'), 'large')
slide.addParagraph(I18n.t('slide1.paragraph2', 'Conversations—the new Canvas messaging system—has arrived!'), 'large')
slide.addParagraph(I18n.t('slide1.paragraph3', 'Use Conversations to send a private message to a classmate or use Conversations to talk to an entire group of people.'), 'large_and_blue')
slide.addParagraph(I18n.t('slide1.paragraph4', 'Ready for a short intro? Click the right arrow to get started.'), 'large')
introSlideshow.addSlide I18n.t('titles.slide2', 'Slide 2'), (slide) ->
slide.addImage('/images/conversations/intro/image2.png', 'screenshot')
slide.addParagraph(I18n.t('slide2.paragraph1', 'All your conversations are shown on the left side.'))
slide.addParagraph(I18n.t('slide2.paragraph2', 'You can see who the conversation is with, how many messages there are, and a few lines from the newest message in each conversation.'))
introSlideshow.addSlide I18n.t('titles.slide3', 'Slide 3'), (slide) ->
slide.addImage('/images/conversations/intro/image3.png', 'screenshot')
slide.addParagraph(I18n.t('slide3.paragraph1', 'Conversations can be marked as read/unread, starred, or archived using the "actions" button on the message.'))
slide.addParagraph(I18n.t('slide3.paragraph2', 'Archived messages aren\'t deleted, they\'re just moved out of your inbox, so you can access them again if needed.'))
introSlideshow.addSlide I18n.t('titles.slide4', 'Slide 4'), (slide) ->
slide.addImage('/images/conversations/intro/image4.png', 'screenshot')
slide.addParagraph(I18n.t('slide4.paragraph1', 'When you select a conversation, all the messages for that conversation are shown in the panel on the right. When conversations get long, you can always scroll down to see earlier messages.'))
introSlideshow.addSlide I18n.t('titles.slide5', 'Slide 5'), (slide) ->
slide.addImage('/images/conversations/intro/image5.png', 'screenshot')
slide.addParagraph(I18n.t('slide5.paragraph1', 'To begin a message, start by clicking the Compose icon.'))
introSlideshow.addSlide I18n.t('titles.slide6', 'Slide 6'), (slide) ->
slide.addImage('/images/conversations/intro/image6.png', 'screenshot')
slide.addParagraph(I18n.t('slide6.paragraph1', 'In the New Message box, start typing the person, or group\'s name, and they\'ll show up in the dropdown. Alternatively, you can click the address book icon to find someone if you don\'t remember a name.'))
slide.addParagraph(I18n.t('slide6.paragraph2', 'Type your message, click Send, and you\'re golden.'))
introSlideshow.addSlide I18n.t('titles.slide7', 'Slide 7'), (slide) ->
slide.addImage('/images/conversations/intro/image7.png', 'screenshot')
slide.addParagraph(I18n.t('slide7.paragraph1', 'You can send messages to one person or to a group of people. By default all group messages are available to everyone in the group.'))
slide.addParagraph(I18n.t('slide7.paragraph2', 'If you wanted to send the message privately to all the recipients, uncheck the "This is a group conversation" checkbox.'))
introSlideshow.addSlide I18n.t('titles.slide8', 'Slide 8'), (slide) ->
slide.addImage('/images/conversations/intro/image8.png', 'screenshot')
slide.addParagraph(I18n.t('slide8.paragraph1', 'You can select one or more messages by clicking the checkbox on the right hand side.'))
slide.addParagraph(I18n.t('slide8.paragraph2', 'Once selected, you can forward them to someone else, or delete them.'))
introSlideshow.addSlide I18n.t('titles.slide9', 'Slide 9'), (slide) ->
slide.addImage('/images/conversations/intro/image9.png', 'screenshot', 'http://www.youtube.com/watch?v=NWqIaEyVWZM')
slide.addParagraph(I18n.t('slide9.paragraph1', 'We think you\'ll find Conversations simple and easy to use.'))
slide.addParagraph(I18n.t('slide9.paragraph2', 'Check out this short introduction video to see Conversations in action.'))
slide.addParagraph(I18n.t('slide9.paragraph3', 'Your old messages have been organized into Conversations for you. So what are you waiting for? Get started!'))
introSlideshow.dom.bind 'dialogclose', ->
$.ajaxJSON '/conversations/watched_intro', 'POST', {}
introSlideshow.start()

View File

@ -1220,7 +1220,7 @@ class ApplicationController < ActionController::Base
helper_method :calendar_url_for, :files_url_for
def conversations_path(params={})
if @current_user and @current_user.preferences[:use_new_conversations]
if @current_user and @current_user.use_new_conversations?
query_string = params.slice(:context_id, :user_id, :user_name).inject([]) do |res, (k, v)|
res << "#{k}=#{v}"
res

View File

@ -144,9 +144,8 @@ class ConversationsController < ApplicationController
# filtering conversations that at have at least all of the contexts ("and")
# or at least one of the contexts ("or")
#
# @argument interleave_submissions [Boolean] Default is false. If true, the
# message_count will also include these submission-based messages in the
# total. See the show action for more information.
# @argument interleave_submissions [Boolean] (Obsolete) Submissions are no
# longer linked to conversations. This parameter is ignored.
#
# @argument include_all_conversation_ids [Boolean] Default is false. If true,
# the top-level element of the response will be an object rather than
@ -232,37 +231,18 @@ class ConversationsController < ApplicationController
load_all_contexts :permissions => [:manage_user_notes]
notes_enabled = @current_user.associated_accounts.any?{|a| a.enable_user_notes }
can_add_notes_for_account = notes_enabled && @current_user.associated_accounts.any?{|a| a.grants_right?(@current_user, nil, :manage_students) }
if @current_user.use_new_conversations?
js_env(:CONVERSATIONS => {
:ATTACHMENTS_FOLDER_ID => @current_user.conversation_attachments_folder.id,
:ACCOUNT_CONTEXT_CODE => "account_#{@domain_root_account.id}",
:CONTEXTS => @contexts,
:NOTES_ENABLED => notes_enabled,
:CAN_ADD_NOTES_FOR_ACCOUNT => can_add_notes_for_account,
})
return render :template => 'conversations/index_new'
else
@current_user.reset_unread_conversations_counter
current_user_json = conversation_user_json(@current_user, @current_user, session, :include_participant_avatars => true)
current_user_json[:id] = current_user_json[:id].to_s
hash = {:CONVERSATIONS => {
:USER => current_user_json,
:CONTEXTS => @contexts,
:NOTES_ENABLED => notes_enabled,
:CAN_ADD_NOTES_FOR_ACCOUNT => can_add_notes_for_account,
:SHOW_INTRO => !@current_user.watched_conversations_intro?,
:FOLDER_ID => @current_user.conversation_attachments_folder.id,
:MEDIA_COMMENTS_ENABLED => feature_enabled?(:kaltura),
}, :CONTEXT_ACTION_SOURCE => :conversation}
append_sis_data(hash)
js_env(hash)
end
js_env(:CONVERSATIONS => {
:ATTACHMENTS_FOLDER_ID => @current_user.conversation_attachments_folder.id,
:ACCOUNT_CONTEXT_CODE => "account_#{@domain_root_account.id}",
:CONTEXTS => @contexts,
:NOTES_ENABLED => notes_enabled,
:CAN_ADD_NOTES_FOR_ACCOUNT => can_add_notes_for_account,
})
return render :template => 'conversations/index_new'
end
end
def toggle_new_conversations
@current_user.preferences[:use_new_conversations] = value_to_boolean(params[:use_new_conversations])
@current_user.save!
redirect_to action: 'index'
end
@ -399,14 +379,11 @@ class ConversationsController < ApplicationController
# @API Get a single conversation
# Returns information for a single conversation. Response includes all
# fields that are present in the list/index action, as well as messages,
# submissions, and extended participant information.
# fields that are present in the list/index action as well as messages
# and extended participant information.
#
# @argument interleave_submissions [Boolean] Default false. If true,
# submission data will be returned as first class messages interleaved
# with other messages. The submission details (comments, assignment, etc.)
# will be stored as the submission property on the message. Note that if
# set, the message_count will also include these messages in the total.
# @argument interleave_submissions [Boolean] (Obsolete) Submissions are no
# longer linked to conversations. This parameter is ignored.
#
# @argument scope [Optional, String, "unread"|"starred"|"archived"]
# Used when generating "visible" in the API response. See the explanation
@ -436,11 +413,9 @@ class ConversationsController < ApplicationController
# media_comment:: Audio/video comment data for this message (if applicable). Fields include: display_name, content-type, media_id, media_type, url
# forwarded_messages:: If this message contains forwarded messages, they will be included here (same format as this list). Note that those messages may have forwarded messages of their own, etc.
# attachments:: Array of attachments for this message. Fields include: display_name, content-type, filename, url
# @response_field submissions Array of assignment submissions having
# comments relevant to this conversation. These should be interleaved with
# the messages when displaying to the user. See the {api:SubmissionsApiController#index Submissions API documentation}
# for details on the fields included. This response includes
# the submission_comments and assignment associations.
# @response_field submissions (Obsolete) Array of assignment submissions having
# comments relevant to this conversation. Submissions are no longer linked to conversations.
# This field will always be nil or empty.
#
# @example_response
# {
@ -508,24 +483,17 @@ class ConversationsController < ApplicationController
end
@conversation.update_attribute(:workflow_state, "read") if @conversation.unread? && auto_mark_as_read?
messages = submissions = nil
messages = nil
Shackles.activate(:slave) do
messages = @conversation.messages
ConversationMessage.send(:preload_associations, messages, :asset)
submissions = messages.map(&:submission).compact
Submission.send(:preload_associations, submissions, [:assignment, :submission_comments])
if interleave_submissions
submissions = nil
else
messages = messages.select{ |message| message.submission.nil? }
end
end
render :json => conversation_json(@conversation,
@current_user,
session,
include_indirect_participants: true,
messages: messages,
submissions: submissions,
submissions: [],
include_beta: params[:include_beta],
include_context_name: true)
end
@ -629,7 +597,7 @@ class ConversationsController < ApplicationController
# @API Add recipients
# Add recipients to an existing group conversation. Response is similar to
# the GET/show action, except that omits submissions and only includes the
# the GET/show action, except that only includes the
# latest message (e.g. "joe was added to the conversation by bob")
#
# @argument recipients[] [String]
@ -679,7 +647,7 @@ class ConversationsController < ApplicationController
# @API Add a message
# Add a message to an existing conversation. Response is similar to the
# GET/show action, except that omits submissions and only includes the
# GET/show action, except that only includes the
# latest message (i.e. what we just sent)
#
# @argument body [String]
@ -1028,9 +996,9 @@ class ConversationsController < ApplicationController
end
end
# TODO API v2: default to true, like we do in the UI
# Obsolete. Forced to false until we go through and clean it up thoroughly
def interleave_submissions
value_to_boolean(params[:interleave_submissions]) || !api_request?
false
end
def include_private_conversation_enrollments

View File

@ -119,84 +119,6 @@ class Conversation < ActiveRecord::Base
end
end
#
# ==== Arguments
# * <tt>asset</tt> - The asset with conversation_messages to update.
# * <tt>options</tt> - Options for special behavior.
#
# ==== Options
# * <tt>:delete_all</tt> - Boolean option. If +true+, all of the asset's conversation messages are destroyed.
# * <tt>:only_existing</tt> - Boolean option. If +true+, only existing ones are updated. No new ones are created.
# Additional options are passed on further but not directly used here.
# * <tt>:update_participants</tt> - Boolean option.
# * <tt>:skip_users</tt> - Array of users to skip.
# * <tt>:recalculate_count</tt> - Boolean
# * <tt>:recalculate_last_authored_at</tt> - Boolean
def self.update_all_for_asset(asset, options)
transaction do
asset.lock!
if options[:delete_all]
asset.conversation_messages.destroy_all
return
end
groups = asset.conversation_groups
conversations = if groups.empty?
[]
elsif options[:only_existing]
groups.first.first.shard.activate do
conversation_ids = ConversationParticipant.select(:conversation_id).uniq.
where(:private_hash => groups.map { |g|
private_hash_for(g)}).map(&:conversation_id)
if conversation_ids.empty?
[]
else
find_all_by_id(conversation_ids, :lock => true)
end
end
else
groups.map{ |g| initiate(g, true) }.each(&:lock!)
end
current_messages = conversations.map{ |c| c.update_for_asset(asset, options) }
# delete asset messages from obsolete conversations (e.g. once the first
# instructor comments on a submission, remove it from conversations
# between the submitter and other instructors)
(asset.conversation_messages - current_messages).each(&:destroy)
end
end
#
# ==== Arguments
# * <tt>asset</tt> - The asset with conversation_messages to update.
# * <tt>options</tt> - Options for special behavior.
#
# ==== Options
# * <tt>:update_participants</tt> - Boolean option.
def update_for_asset(asset, options)
message = asset.conversation_messages.detect { |m| m.conversation_id == id }
if message
add_message_to_participants(message, options) # make sure it gets re-added
else
message = add_message(asset.user, '', options.merge(:asset => asset, :update_participants => false, :root_account_id => asset.context.try(:root_account_id)))
end
if (data = asset.conversation_message_data).present?
message.created_at = data[:created_at]
message.author = data[:author]
message.body = data[:body]
message.save!
end
if options[:update_participants]
update_participants message, options
else
conversation_participants.each{ |cp| cp.update_cached_data!(options.merge(:set_last_message_at => false)) }
end
message
end
def add_participants(current_user, users, options={})
self.shard.activate do
user_ids = users.map(&:id).uniq

View File

@ -33,7 +33,9 @@ class ConversationMessage < ActiveRecord::Base
has_many :conversation_message_participants
has_many :attachment_associations, :as => :context
has_many :attachments, :through => :attachment_associations, :order => 'attachments.created_at, attachments.id'
belongs_to :asset, :polymorphic => true, :types => :submission # TODO: move media comments into this
# we used to attach submission comments to conversations via this asset
# TODO: remove this column when we're sure we don't want this relation anymore
belongs_to :asset, :polymorphic => true, :types => :submission
delegate :participants, :to => :conversation
delegate :subscribed_participants, :to => :conversation
attr_accessible

View File

@ -248,11 +248,7 @@ class ConversationParticipant < ActiveRecord::Base
Rails.cache.fetch([conversation, user, 'participants', options].cache_key) do
participants = conversation.participants
if options[:include_indirect_participants]
user_ids =
messages.map(&:all_forwarded_messages).flatten.map(&:author_id) |
messages.map{
|m| m.submission.submission_comments.map(&:author_id) if m.submission
}.compact.flatten
user_ids = messages.map(&:all_forwarded_messages).flatten.map(&:author_id)
user_ids -= participants.map(&:id)
participants += Shackles.activate(:slave) { MessageableUser.available.where(:id => user_ids).all }
end

View File

@ -37,7 +37,12 @@ class Submission < ActiveRecord::Base
has_many :rubric_assessments, :as => :artifact
has_many :attachment_associations, :as => :context
has_many :attachments, :through => :attachment_associations
# we no longer link submission comments and conversations, but we haven't fixed up existing
# linked conversations so this relation might be useful
# TODO: remove this when removing the conversationmessage asset columns
has_many :conversation_messages, :as => :asset # one message per private conversation
has_many :content_participations, :as => :content
EXPORTABLE_ATTRIBUTES = [
@ -49,7 +54,7 @@ class Submission < ActiveRecord::Base
EXPORTABLE_ASSOCIATIONS = [
:attachment, :assignment, :user, :grader, :group, :media_object, :student, :submission_comments, :assessment_requests, :assigned_assessments, :quiz_submission,
:rubric_assessment, :rubric_assessments, :attachments, :conversation_messages, :content_participations
:rubric_assessment, :rubric_assessments, :attachments, :content_participations
]
serialize :turnitin_data, Hash
@ -854,19 +859,6 @@ class Submission < ActiveRecord::Base
comment
end
def conversation_groups
participating_instructors.map{ |i| [user, i] }
end
def conversation_message_data
latest = visible_submission_comments.where(:author_id => possible_participants_ids).last or return
{
:created_at => latest.created_at,
:author => latest.author,
:body => latest.comment
}
end
def comment_authors
visible_submission_comments(:include => :author).map(&:author)
end
@ -883,55 +875,6 @@ class Submission < ActiveRecord::Base
[user_id] + context.participating_instructors.uniq.map(&:id)
end
# ensure that conversations/messages are created/updated for all relevant
# participants as submission comments are added/removed. there should be a
# conversation between the submitter and each participating admin, and it
# should have a single conversation_message that represents the submission
# (there may of course be other regular messages in the conversation)
#
# ==== Arguments
# * <tt>trigger</tt> - Values of :create, :destroy, :migrate are supported.
# * <tt>overrides</tt> - Hash of overrides that can be passed through when
# updating the conversation.
#
# ==== Overrides
# * <tt>:skip_users</tt> - Gets passed through to <tt>Conversation</tt>.<tt>update_all_for_asset</tt>.
# nil by default, which means mark-as-unread for
# everyone but the author.
def create_or_update_conversations!(trigger, overrides={})
options = {}
case trigger
when :create
options[:update_participants] = true
options[:update_for_skips] = false
options[:skip_users] = overrides[:skip_users] || [conversation_message_data[:author]] # don't mark-as-unread for the author
options[:skip_users] << user if user.preferences[:use_new_conversations]
participating_instructors.each do |t|
# Check their settings and add to :skip_users if set to suppress.
if t.preferences[:no_submission_comments_inbox] == true ||
t.preferences[:use_new_conversations]
options[:skip_users] << t
end
end
when :destroy
options[:delete_all] = visible_submission_comments.empty?
options[:only_existing] = true
when :migrate # don't mark-as-unread for anybody or add to empty conversations
return unless conversation_message_data
options[:recalculate_count] = true
options[:recalculate_last_authored_at] = true
options[:only_existing] = true
end
Conversation.update_all_for_asset(self, options)
end
def self.batch_migrate_conversations!(ids)
find_all_by_id(ids).each do |sub|
sub.create_or_update_conversations!(:migrate)
end
end
def limit_comments(user, session=nil)
@comment_limiting_user = user
@comment_limiting_session = session

View File

@ -46,8 +46,6 @@ class SubmissionComment < ActiveRecord::Base
after_save :check_for_media_object
after_destroy :delete_other_comments_in_this_group
after_create :update_participants
after_create { |c| c.submission.create_or_update_conversations!(:create) if c.send_to_conversations? }
after_destroy { |c| c.submission.create_or_update_conversations!(:destroy) if c.send_to_conversations? }
serialize :cached_attachments
@ -220,10 +218,6 @@ class SubmissionComment < ActiveRecord::Base
"/images/users/#{User.avatar_key(self.author_id)}"
end
def send_to_conversations?
!hidden? && submission.possible_participants_ids.include?(author_id)
end
def serialization_methods
context.root_account.service_enabled?(:avatars) ? [:avatar_path] : []
end

View File

@ -1420,7 +1420,7 @@ class User < ActiveRecord::Base
end
def use_new_conversations?
preferences[:use_new_conversations] == true
true
end
def ignore_item!(asset, purpose, permanent = false)

File diff suppressed because it is too large Load Diff

View File

@ -1,218 +0,0 @@
<% @body_classes << "full-width" %>
<% content_for :page_title, t(:page_title, "Conversations") %>
<% content_for :auto_discovery do %>
<% if @current_user %>
<%= auto_discovery_link_tag(:atom, feeds_conversation_format_path(@current_user.feed_code, :atom), {:title => t('titles.rss.conversations', "Conversations Atom Feed")}) %>
<% end %>
<% end %>
<% js_bundle :conversations %>
<% jammit_css :conversations %>
<div id="inbox">
<div id="conversations">
<div id="actions">
<ul class="buttons">
<% unless @disallow_messages %>
<li>
<a href="#"
id="action_compose_message"
title="<%= t('titles.compose_new_message', 'Write a new message') %>"
data-track-category="Compose Message"
data-track-action="Click"
data-track-label="New Message Symbol"
style="background-image: url(/images/messages/compose-button-sprite.png)"><%= t('links.compose_new_message', 'New Message') %></a>
</li>
<% end %>
</ul>
<ul class="menus">
<li><a href="#" id="menu_views"></a>
<span></span>
<div>
<ul class="first">
<li><b><%= t(:inbox_view, "VIEW") %></b></li>
</ul>
<ul>
<li data-scope="inbox"
data-track-category="Combo_Box"
data-track-action="Select_Folder"
data-track-label="Inbox"><%= link_to t('inbox_views.inbox', "Inbox"), '#' %></li>
<li data-scope="unread"
data-track-category="Combo_Box"
data-track-action="Select_Folder"
data-track-label="Unread"><%= link_to t('inbox_views.unread_messages', "Unread"), '#' %></li>
<li data-scope="starred"
data-track-category="Combo_Box"
data-track-action="Select_Folder"
data-track-label="Starred"><%= link_to t('inbox_views.starred_messages', "Starred"), '#' %></li>
<li data-scope="sent"
data-track-category="Combo_Box"
data-track-action="Select_Folder"
data-track-label="Sent"><%= link_to t('inbox_views.sent_messages', "Sent"), '#' %></li>
</ul>
<ul class="last">
<li data-scope="archived"
data-track-category="Combo_Box"
data-track-action="Select_Folder"
data-track-label="Archived"><%= link_to t('inbox_views.archived_messages', "Archived"), '#' %></li>
</ul>
</div>
</li>
</ul>
<div id="context_tags_filter">
<label for="context_tags">
<%= before_label :filter, "Filter" %>
</label>
<%= text_field_tag :context_tags, nil, 'data-finder_url' => search_recipients_url %>
</div>
<div class="clear"></div>
</div>
<div class="conversations"><ul></ul></div>
<p id="no_messages" style="display:none"></p>
</div>
<div id="messages">
<div aria-live="polite">
<div id="message_actions">
<ul>
<li><%= link_to t('inbox_actions.forward', "Forward"), {}, :id => :action_forward %></li>
<li><%= link_to t('inbox_actions.delete', "Delete"), conversation_remove_messages_url('{{ id }}'), :id => :action_delete %></li>
</ul>
<a href="#" id="cancel_bulk_message_action"><%= t '#buttons.cancel', 'Cancel' %></a>
</div>
</div>
<ul id="message_status"></ul>
<div role="application">
<a id="help-btn"
class="al-trigger"
data-track-category="Help"
data-track-action="Click"
data-track-label="Question Mark"
href="#"
role="button"
aria-haspopup="true"
aria-owns="help-menu-items">
<i class="icon-question"></i>
<i class="icon-mini-arrow-down"></i>
<span class="screenreader-only"><%= t 'help', "Help" %></span>
</a>
<ul id="help-menu-items"
class="al-options"
role="menu"
tabindex="0"
aria-hidden="true"
aria-expanded="false"
aria-activedescendant="new-conversations-opt-in-menu-item">
<li role="presentation">
<a id="try-new-conversations-menu-item" href="/conversations/toggle_new_conversations?use_new_conversations=1" data-method="post" rel="nofollow" tabindex="-1" role="menuitem" title="<%= t 'try_new_conversations', "Try New Conversations" %>"><%= t 'try_new_conversations', "Try New Conversations" %></a>
</li>
<li role="presentation">
<a href="#" id="conversations-intro-menu-item" tabindex="-1" role="menuitem" title="<%= t 'conversations_intro', "Conversations Intro" %>"><%= t 'conversations_intro', "Conversations Intro" %></a>
</li>
</ul>
</div>
<div id="create_message_form"></div>
<ul class="messages">
</ul>
</div>
<div class="spin_holder"></div>
</div>
<div id="menu-wrapper"></div>
<ul style="display:none">
<li id="message_blank"
data-track-category="Individual Message"
data-track-action="Click"
data-track-label="Message Block"
class="message">
<span class="date"></span>
<b class="audience"></b>
<span class="actions">
<a href="#"
data-track-category="Individual Message"
data-track-action="Click"
data-track-label="New Message..."
class="send_private_message"><%= t :send_private_message, "New message..." %></a>
</span>
<p></p>
<input type="checkbox"
data-track-category="Individual Message"
data-track-action="Click"
data-track-label="Check Box"
/>
<ul class='message_attachments'>
<li class='media_object_blank'>
<a href="#" class="instructure_inline_media_comment media_comment no-underline" title="<%= t('titles.play_media_comment', "Play media comment") %>">
<%= image_tag "messages/media-gray.png" %>
<span class="media_comment_id" style="display: none;"></span>
<span class='title'></span>
</a>
</li>
<li class='attachment_blank'>
<a href="#" title="<%= t('titles.download_attachment', "Download attachment") %>">
<%= image_tag "messages/attach-gray.png" %>
<span class='title'></span>
</a>
</li>
</ul>
</li>
<li id="submission_blank" class="submission">
<ul>
<li class="header">
<span class="submission_date"><%= before_label('submitted', "Submitted") %> <span class="date">&nbsp;</span></span>
<a target="_blank"
data-track-category="Individual Message"
data-track-action="Click"
data-track-label="Open message in new window"
href="<%= course_assignment_submission_path(:course_id => '{{ course_id }}',
:assignment_id => '{{ assignment_id }}',
:id => '{{ id }}') %>"
style="text-decoration: none;"
title="<%= t('titles.view_submission', "Open submission in new window.") %>">
<b class="title"></b>
</a>
<span class="audience" title="<%= t('titles.submission_author', "Submission Author") %>"></span>
<span class="score" title="<%= t('titles.submission_score', "Submission Score") %>"></span>
<div class="clear"></div>
</li>
<li class="comment">
<span class="date"></span>
<b class="audience"></b>
<p></p>
<div class="clear"></div>
</li>
<li class="more">
<a href="#">
<%= t('more_messages', "%{count} more...", :count => raw('<span class="hidden"></span>')) %>
</a>
</li>
</ul>
<input type="checkbox" />
</li>
</ul>
<form id="add_recipients_form" method="post">
<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' => 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' => 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>
<input type="hidden" name="group_conversation" value="1" />
<% end %>

View File

@ -86,37 +86,6 @@
<div id="sending-spinner"></div>
</div>
<div class="admin-link pull-right" role="application">
<a id="help-btn"
class="al-trigger help-btn"
href="#"
role="button"
aria-haspopup="true"
aria-owns="help-menu-items">
<i class="icon-question"></i>
<i class="icon-mini-arrow-down"></i>
<span class="screenreader-only"><%= t(:help, "Help") %></span>
</a>
<ul id="help-menu-items"
class="al-options"
role="menu"
tabindex="0"
aria-hidden="true"
aria-expanded="false"
aria-activedescendant="switch-to-old-conversations-menu-item">
<li role="presentation">
<a id="switch-to-old-conversations-menu-item"
href="/conversations/toggle_new_conversations"
data-method="post"
rel="nofollow"
tabindex="-1"
role="menuitem"
title="<%= t(:switch_to_old_conversations, "Switch Back to Old Conversations") %>"><%= t(:switch_to_old_conversations, "Switch Back to Old Conversations")%></a>
</li>
</ul>
</div>
<div role="search" class="search pull-right form-search">
<div class="ac" id="search-autocomplete">
<div class="ac-input-box">

View File

@ -75,10 +75,6 @@ module Mutable
[submission, comments.map(&:author_id).uniq.size == 1 ? [comments.last.author] : []]
}.compact
SubmissionComment.where(:hidden => true, :submission_id => submissions).update_all(:hidden => false)
Submission.send(:preload_associations, outstanding.map(&:first), :visible_submission_comments)
outstanding.each do |submission, skip_users|
submission.create_or_update_conversations!(:create, :skip_users => skip_users)
end
end
end
end

View File

@ -224,7 +224,7 @@ describe ConversationsController, type: :request do
{ :controller => 'conversations', :action => 'show', :id => @c3.conversation.id.to_s, :format => 'json' })
json["context_name"].should be_nil
end
end
end
end
context "filtering by tags" do
@ -1059,64 +1059,19 @@ describe ConversationsController, type: :request do
json["starred"].should be_true
end
context "submission comments" do
before do
submission1 = submission_model(:course => @course, :user => @bob)
submission2 = submission_model(:course => @course, :user => @bob)
conversation(@bob)
submission1.add_comment(:comment => "hey bob", :author => @me)
submission1.add_comment(:comment => "wut up teacher", :author => @bob)
submission2.add_comment(:comment => "my name is bob", :author => @bob)
end
it "should not link submission comments and conversations anymore" do
submission1 = submission_model(:course => @course, :user => @bob)
submission2 = submission_model(:course => @course, :user => @bob)
conversation(@bob)
submission1.add_comment(:comment => "hey bob", :author => @me)
submission1.add_comment(:comment => "wut up teacher", :author => @bob)
submission2.add_comment(:comment => "my name is bob", :author => @bob)
it "should return submission and comments with the conversation in api format" do
json = api_call(:get, "/api/v1/conversations/#{@conversation.conversation_id}",
{ :controller => 'conversations', :action => 'show', :id => @conversation.conversation_id.to_s, :format => 'json' })
json['messages'].size.should == 1
json['submissions'].size.should == 2
jsub = json['submissions'][1]
jsub['assignment'].should be_present # includes & ['assignment']
jcom = jsub['submission_comments']
jcom.should be_present # includes & ['submission_comments']
jcom.size.should == 2
jcom[0]['author_id'].should == @me.id
jcom[1]['author_id'].should == @bob.id
jsub = json['submissions'][0]
jcom = jsub['submission_comments']
jcom.size.should == 1
jcom[0]['author_id'].should == @bob.id
end
it "should interleave submission and comments in the conversation" do
@conversation.add_message("another message!")
json = api_call(:get, "/api/v1/conversations/#{@conversation.conversation_id}?interleave_submissions=1",
{ :controller => 'conversations', :action => 'show', :id => @conversation.conversation_id.to_s, :format => 'json', :interleave_submissions => '1' })
json['submissions'].should be_nil
json['messages'].size.should eql 4
json['messages'][0]['body'].should eql 'another message!'
json['messages'][1]['body'].should eql 'my name is bob'
jsub = json['messages'][1]['submission']
jsub['assignment'].should be_present
jcom = jsub['submission_comments']
jcom.should be_present
jcom.size.should == 1
jcom[0]['author_id'].should == @bob.id
json['messages'][2]['body'].should eql 'wut up teacher' # most recent comment
jsub = json['messages'][2]['submission']
jcom = jsub['submission_comments']
jcom.size.should == 2
jcom[0]['author_id'].should == @me.id
jcom[1]['author_id'].should == @bob.id
json['messages'][3]['body'].should eql 'test'
end
json = api_call(:get, "/api/v1/conversations/#{@conversation.conversation_id}",
{ :controller => 'conversations', :action => 'show', :id => @conversation.conversation_id.to_s, :format => 'json' })
json['messages'].size.should == 1
json['submissions'].size.should == 0
end
it "should add a message to the conversation" do

View File

@ -157,19 +157,6 @@ describe ConversationsController do
assert_unauthorized
end
it "should recompute inbox count" do
# In an effort to make the data fix easy to do and self-healing,
# recompute the unread inbox count when the page is loaded.
course_with_student_logged_in(:active_all => true)
@user.update_attribute(:unread_conversations_count, -20) # create invalid starting value
@c1 = conversation
get 'index'
response.should be_success
@user.reload
@user.unread_conversations_count.should == 0
end
context "masquerading" do
before do
a = Account.default
@ -573,33 +560,9 @@ describe ConversationsController do
course_with_student_logged_in(:active_all => true)
end
it "should enable new conversations for a user" do
@user.preferences[:use_new_conversations] = false
@user.save!
@user.use_new_conversations?.should be_false
post 'toggle_new_conversations', :use_new_conversations => true
@user.reload
@user.use_new_conversations?.should be_true
end
it "should disable new conversations for a user" do
@user.preferences[:use_new_conversations] = true
@user.save!
it "should not disable new conversations for a user anymore" do
post 'toggle_new_conversations'
@user.reload
@user.use_new_conversations?.should be_false
end
it "should be idempotent" do
@user.use_new_conversations?.should be_false
post 'toggle_new_conversations'
@user.reload
@user.use_new_conversations?.should be_false
post 'toggle_new_conversations', :use_new_conversations => 1
@user.reload
@user.use_new_conversations?.should be_true
post 'toggle_new_conversations', :use_new_conversations => 1
@user.reload
@user.use_new_conversations?.should be_true
end
end

View File

@ -494,101 +494,6 @@ describe Conversation do
end
end
context "update_all_for_asset" do
it "should delete all messages if requested" do
asset = mock
asset_messages = mock
asset_messages.expects(:destroy_all).returns([])
asset.expects(:lock!).returns(true)
asset.expects(:conversation_messages).at_least_once.returns(asset_messages)
Conversation.update_all_for_asset asset, :delete_all => true
end
it "should not create conversations if only_existing is set" do
u1 = user
u2 = user
conversation = Conversation.initiate([u1, u2], true)
asset = Submission.new(:user => u1)
asset.expects(:conversation_groups).returns([[u1, u2]])
asset.expects(:lock!).returns(true)
asset.expects(:conversation_messages).at_least_once.returns([])
asset.expects(:conversation_message_data).returns({:created_at => Time.now.utc, :author => u1, :body => "asdf"})
Conversation.update_all_for_asset asset, :update_message => true, :only_existing => true
conversation.conversation_messages.size.should eql 1
end
it "should undelete visible soft-deleted message participants" do
u1 = user
u2 = user
conversation = Conversation.initiate([u1, u2], true)
# a message to keep the conversation visible to u2 after remove_messages on the asset's message
previous_message = conversation.add_message(u1, 'hello')
message = conversation.add_message(u1, 'test message')
# make u1's conversation invisible so they won't be updated.
u1.conversations.first.remove_messages(previous_message, message)
u1.conversations.should be_empty
# u2 only deletes the asset's message, but conversations is still visible
u2.conversations.first.remove_messages(message)
u2.conversations.first.messages.size.should eql 1
asset = Submission.new(:user => u1)
asset.expects(:conversation_groups).returns([[u1, u2]])
asset.expects(:lock!).returns(true)
asset.expects(:conversation_messages).at_least_once.returns([message])
asset.expects(:conversation_message_data).returns({:created_at => Time.now.utc, :author => u1, :body => "asdf"})
Conversation.update_all_for_asset asset, :update_message => true, :only_existing => true
conversation.conversation_messages.size.should eql 2
u1.conversations.should be_empty
# but u1 should still have the soft-deleted participant
u1.all_conversations.first.all_messages.size.should eql 2
u2.conversations.first.messages.size.should eql 2
end
context "sharding" do
specs_require_sharding
it "should re-use conversations from another shard" do
u1 = @shard1.activate { user }
u2 = user
conversation = @shard2.activate { Conversation.initiate([u1, u2], true) }
asset = Submission.new(:user => u1)
asset.expects(:conversation_groups).returns([[u1, u2]])
asset.expects(:lock!).returns(true)
asset.expects(:conversation_messages).at_least_once.returns([])
asset.expects(:conversation_message_data).returns({:created_at => Time.now.utc, :author => u1, :body => "asdf"})
Conversation.update_all_for_asset asset, :update_message => true, :only_existing => true
conversation.conversation_messages.size.should eql 1
end
end
it "should create conversations by default" do
u1 = user
u2 = user
conversation = Conversation.initiate([u1, u2], true)
asset = Submission.new(:user => u1)
asset.expects(:conversation_groups).returns([[u1, u2]])
asset.expects(:lock!).returns(true)
asset.expects(:conversation_messages).at_least_once.returns([])
asset.expects(:conversation_message_data).returns({:created_at => Time.now.utc, :author => u1, :body => "asdf"})
Conversation.expects(:initiate).returns(conversation)
Conversation.update_all_for_asset asset, :update_message => true
conversation.conversation_messages.size.should eql 1
end
it "should delete obsolete messages" do
old_message = mock
old_message.expects(:destroy).returns(true)
asset = mock
asset.expects(:lock!).returns(true)
asset.expects(:conversation_groups).returns([])
asset.expects(:conversation_messages).at_least_once.returns([old_message])
Conversation.update_all_for_asset(asset, {})
end
end
context "context tags" do
context "current_context_strings" do
it "should not double-count duplicate enrollments" do

View File

@ -144,493 +144,6 @@ This text has a http://www.google.com link in it...
@comment = @submission.add_comment(:author => se.user, :media_comment_type => 'audio', :media_comment_id => 'fake')
end
context "conversations" do
before do
assignment_model
@assignment.workflow_state = 'published'
@assignment.save
@course.offer
@course.enroll_teacher(user).accept
@teacher1 = @user
@course.enroll_teacher(user).accept
@teacher2 = @user
@assignment.reload
@course.enroll_student(user)
@student1 = @user
@assignment.context.reload
@submission1 = @assignment.submit_homework(@student1, :body => 'some message')
end
context "creation" do
it "should send submitter comments to all instructors if no instructors have commented" do
@submission1.add_comment(:author => @student1, :comment => "hello")
@teacher1.conversations.size.should eql 1
tc1 = @teacher1.conversations.first
tc1.messages.size.should eql 1
tc1.messages.first.asset.should eql @submission1
@teacher2.conversations.size.should eql 1
tc2 = @teacher2.conversations.first
tc2.messages.size.should eql 1
tc2.messages.first.asset.should eql @submission1
end
it "should not send non-participant comments to anyone" do
@submission1.add_comment(:author => user, :comment => "ohai im in ur group")
@teacher1.conversations.size.should eql 0 # if we actually set up a group assignment and had this comment on all submissions, the teacher would have one conversation with that commenter
@student1.conversations.size.should eql 0
end
it "should just create a single message for all comments" do
@submission1.add_comment(:author => @student1, :comment => "hello")
@submission1.add_comment(:author => @student1, :comment => "hello again!")
@submission1.add_comment(:author => @student1, :comment => "hello hello hello!")
@teacher1.conversations.size.should eql 1
tc1 = @teacher1.conversations.first
tc1.messages.size.should eql 1
tc1.messages.first.asset.should eql @submission1
end
it "should set the most recent comment as the message data" do
SubmissionComment.any_instance.stubs(:current_time_from_proper_timezone).returns(Time.now.utc, Time.now.utc + 1.hour)
c1 = @submission1.add_comment(:author => @student1, :comment => "hello")
c2 = @submission1.add_comment(:author => @teacher1, :comment => "hello again!").reload
@teacher1.conversations.size.should eql 1
tc1 = @teacher1.conversations.first
tc1.last_message_at.to_i.should eql c1.created_at.to_i
tc1.messages.last.body.should eql c2.comment
tc1.messages.last.author.should eql @teacher1
end
it "should set the root_account_ids" do
@submission1.add_comment(:author => @student1, :comment => "hello")
@teacher.conversations.where(:root_account_ids => nil).any?.should be_false
@submission1.add_comment(:author => @teacher1, :comment => "sup")
@teacher.conversations.where(:root_account_ids => nil).any?.should be_false
@student.conversations.where(:root_account_ids => nil).any?.should be_false
end
it "should not be visible to the student until an instructor comments" do
@submission1.add_comment(:author => @student1, :comment => "hello")
@student1.conversations.size.should eql 0
@submission1.add_comment(:author => @teacher1, :comment => "sup")
@student1.conversations.reload.size.should eql 1
end
it "should not be visible to other instructors once the first instructor comments" do
@submission1.add_comment(:author => @student1, :comment => "hello")
@teacher1.conversations.size.should eql 1
@teacher2.conversations.size.should eql 1
@submission1.add_comment(:author => @teacher1, :comment => "hello")
@teacher2.reload.conversations.size.should eql 0
@teacher2.all_conversations.size.should eql 1 # still there, the message was just deleted
end
it "should set the unread count/status for everyone but the author" do
@submission1.add_comment(:author => @student1, :comment => "hello")
tconvo = @teacher1.conversations.first
tconvo.should be_unread
tconvo.update_attribute :workflow_state, 'read'
@submission1.add_comment(:author => @teacher1, :comment => "hi")
sconvo = @student1.conversations.first
sconvo.should be_unread
tconvo.reload.should be_read
end
it "should not create conversations for teachers in new conversations" do
@teacher1.preferences[:use_new_conversations] = true
@teacher1.save!
@submission1.add_comment(author: @student1, comment: 'test')
@teacher1.reload.unread_conversations_count.should == 0
end
it "should not create conversations for students in new conversations" do
@student1.preferences[:use_new_conversations] = true
@student1.save!
@submission1.add_comment(author: @teacher1, comment: 'test')
@student1.reload.unread_conversations_count.should == 0
end
context "teacher makes first submission comment" do
it "should only show as sent for the teacher if private converstation does not already exist" do
@submission1.add_comment(:author => @teacher1, :comment => "test comment")
@teacher1.conversations.should be_empty
@teacher1.all_conversations.size.should eql 1
@teacher1.all_conversations.sent.size.should eql 1
end
it "should reuse an existing private conversation, but not change its state for teacher" do
convo = Conversation.initiate([@teacher1, @student1], true)
convo.add_message(@teacher1, 'direct message')
@teacher1.conversations.count.should == 1
convo = @teacher1.conversations.first
convo.workflow_state = 'archived'
convo.save!
@teacher1.reload.conversations.default.should be_empty
@submission1.add_comment(:author => @teacher1, :comment => "test comment")
@teacher1.reload
@teacher1.all_conversations.size.should eql 1
@teacher1.conversations.default.should be_empty
@teacher1.all_conversations.archived.size.should eql 1
end
end
context "with no_submission_comments_inbox" do
context "when teacher sets after conversation started" do
before :each do
@submission1.add_comment(:author => @student1, :comment => 'Test comment')
@submission1.add_comment(:author => @teacher1, :comment => 'Test response')
@student1.mark_all_conversations_as_read!
@teacher1.mark_all_conversations_as_read!
end
it "should keep unread 0 when comments added" do
@teacher1.conversations.unread.count.should == 0
# Disable notification with existing conversation
@teacher1.preferences[:no_submission_comments_inbox] = true
@teacher1.save!
# Student adds another comment
@submission1.add_comment(:author => @student1, :comment => 'New comment')
@teacher1.conversations.unread.count.should == 0
end
end
context "when not set" do
before :each do
@submission1.add_comment(:author => @student1, :comment => 'Test comment')
end
it "should add an unread comment" do
@teacher1.conversations.unread.count.should == 1
end
end
context "when preference set for teacher" do
before :each do
# setup user setting
@teacher1.preferences[:no_submission_comments_inbox] = true
@teacher1.save!
end
it "should not create new conversations" do
@teacher1.conversations.count.should == 0
@submission1.add_comment(:author => @student1, :comment => 'New comment')
@teacher1.conversations.count.should == 0
end
it "should create conversations after re-enabling the notification" do
@submission1.add_comment(:author => @student1, :comment => 'New comment')
@teacher1.conversations.count.should == 0
@teacher1.preferences[:no_submission_comments_inbox] = false
@teacher1.save!
# Student adds another comment
@submission1.add_comment(:author => @student1, :comment => 'Another comment')
@teacher1.conversations.count.should == 1
end
it "should show teacher comment as new to student" do
@submission1.add_comment(:author => @student1, :comment => 'Test comment')
@submission1.add_comment(:author => @teacher1, :comment => 'Test response')
@student1.conversations.unread.count.should == 1
end
it "should not block direct message from student" do
convo = Conversation.initiate([@student1, @teacher], false)
convo.add_message(@student1, 'My direct message')
@teacher.conversations.unread.count.should == 1
end
it "should add submission comments to existing conversations" do
convo = Conversation.initiate([@student1, @teacher1], true)
convo.add_message(@student1, 'My direct message')
c = @teacher1.conversations.unread.first
c.should_not be_nil
c.update_attribute(:workflow_state, 'read')
@submission1.add_comment(:author => @student1, :comment => 'A comment')
c.reload
c.should be_read # still read, since we don't care to be notified
c.messages.size.should eql 2 # but the submission is visible
end
end
end
end
context "unmuting" do
before do
@assignment.mute!
end
it "should update conversations when assignments are unmuted" do
@submission1.add_comment(:author => @teacher1, :comment => "!", :hidden => true)
@teacher1.conversations.size.should eql 0
@teacher1.all_conversations.sent.size.should eql 0
@student1.conversations.size.should eql 0
@assignment.unmute!
@teacher1.reload.conversations.size.should eql 0
@teacher1.all_conversations.sent.size.should eql 1
@student1.reload.conversations.size.should eql 1
@student1.conversations.first.should be_unread
end
it "should not set an older created_at/message" do
SubmissionComment.any_instance.stubs(:current_time_from_proper_timezone).returns(Time.now.utc, Time.now.utc + 1.hour)
c1 = @submission1.add_comment(:author => @teacher1, :comment => "!", :hidden => true)
c2 = @submission1.add_comment(:author => @student1, :comment => "a new comment").reload
@teacher1.conversations.size.should eql 1
@teacher1.conversations.first.messages.last.created_at.to_i.should eql c2.created_at.to_i
@teacher1.conversations.first.messages.last.body.should eql c2.comment
@teacher2.conversations.size.should eql 1
@student1.conversations.size.should eql 0
@assignment.unmute!
@teacher1.reload.conversations.size.should eql 1
@teacher1.conversations.first.messages.last.created_at.to_i.should eql c2.created_at.to_i
@teacher1.conversations.first.messages.last.body.should eql c2.comment
@teacher2.reload.conversations.size.should eql 0
@student1.reload.conversations.size.should eql 1
@student1.conversations.first.should be_unread
end
it "should mark-as-unread for everyone if there are multiple authors of hidden comments" do
c1 = @submission1.add_comment(:author => @student1, :comment => "help!")
c2 = @submission1.add_comment(:author => @teacher1, :comment => "ok", :hidden => true)
c3 = @submission1.add_comment(:author => @teacher2, :comment => "no", :hidden => true)
@student1.conversations.size.should eql 0
t1convo = @teacher1.conversations.first
t1convo.workflow_state = :read
t1convo.save!
t2convo = @teacher2.conversations.first
t2convo.workflow_state = :read
t2convo.save!
@assignment.unmute!
t1convo.reload.should be_unread
t2convo.reload.should be_unread
@student1.reload.conversations.size.should eql 2
@student1.conversations.first.should be_unread
@student1.conversations.last.should be_unread
end
it "should respect the no_submission_comments_inbox setting" do
@teacher1.preferences[:no_submission_comments_inbox] = true
@teacher1.save!
c1 = @submission1.add_comment(:author => @student1, :comment => "help!")
c2 = @submission1.add_comment(:author => @teacher1, :comment => "ok", :hidden => true)
c3 = @submission1.add_comment(:author => @teacher2, :comment => "no", :hidden => true)
@student1.conversations.size.should eql 0
@teacher1.conversations.size.should eql 0
@teacher1.all_conversations.sent.size.should eql 0
t2convo = @teacher2.conversations.first
t2convo.workflow_state = :read
t2convo.save!
@assignment.unmute!
# If there is more than one author in the set of submission comments,
# then it is treated as a new message for everyone.
@teacher1.reload.conversations.should be_empty
@teacher1.all_conversations.size.should eql 1
@teacher1.all_conversations.sent.size.should eql 0
t2convo.reload.should be_unread
@student1.reload.conversations.size.should eql 2
@student1.conversations.first.should be_unread
@student1.conversations.last.should be_unread
end
it "should reuse an existing private conversation, but not change its state for teacher on unmute" do
convo = Conversation.initiate([@teacher1, @student1], true)
convo.add_message(@teacher1, 'direct message')
@teacher1.conversations.count.should == 1
convo = @teacher1.conversations.first
convo.workflow_state = 'archived'
convo.save!
@submission1.add_comment(:author => @teacher1, :comment => "test comment")
@assignment.unmute!
@teacher1.reload.conversations.default.should be_empty
@teacher1.all_conversations.size.should eql 1
@teacher1.all_conversations.archived.size.should eql 1
@teacher1.all_conversations.sent.size.should eql 1
end
end
context "deletion" do
it "should update the message correctly if the most recent comment is deleted" do
SubmissionComment.any_instance.stubs(:current_time_from_proper_timezone).returns(Time.now.utc, Time.now.utc + 1.hour)
c1 = @submission1.add_comment(:author => @student1, :comment => "hello").reload
c2 = @submission1.add_comment(:author => @teacher1, :comment => "hello again!")
c2.destroy
@teacher1.conversations.size.should eql 1
tc1 = @teacher1.conversations.first
tc1.last_message_at.to_i.should eql c1.created_at.to_i
tc1.messages.last.body.should eql c1.comment
tc1.messages.last.author.should eql @student1
# it won't reappear in the other teacher's conversation until another
# non-instructor comment is added, since we don't know if it was
# deleted automatically or explicitly by the teacher
@teacher2.conversations.size.should eql 0
end
it "should not change the message preview/timestamp if the deleted message was by a non-participant" do
SubmissionComment.any_instance.stubs(:current_time_from_proper_timezone).returns(Time.now.utc, Time.now.utc + 1.hour)
c1 = @submission1.add_comment(:author => @student1, :comment => "hello")
c2 = @submission1.add_comment(:author => @teacher1, :comment => "hello again!").reload
c3 = @submission1.add_comment(:author => user, :comment => "ohai im in ur group")
tc1 = @teacher1.conversations.first
tc1.last_message_at.to_i.should eql c1.created_at.to_i
tc1.messages.last.body.should eql c2.comment
tc1.messages.last.author.should eql @teacher1
c3.destroy
tc1.reload
tc1.last_message_at.to_i.should eql c1.created_at.to_i
tc1.messages.last.body.should eql c2.comment
tc1.messages.last.author.should eql @teacher1
end
it "should not re-add the message to users who have deleted it" do
c1 = @submission1.add_comment(:author => @student1, :comment => "hello")
c2 = @submission1.add_comment(:author => @student1, :comment => "hello again!")
@teacher1.conversations.size.should eql 1
tc1 = @teacher1.conversations.first
tc1.remove_messages(:all)
@teacher1.conversations.should be_empty
c2.destroy
@teacher1.conversations.reload.should be_empty
end
it "should remove the message from conversations when the last comment is deleted" do
c1 = @submission1.add_comment(:author => @student1, :comment => "hello")
@teacher1.conversations.size.should eql 1
c1.destroy
@teacher1.reload.conversations.size.should eql 0
end
it "should delete other comments for the same assignment with the same group_comment_id" do
c1 = @submission1.add_comment(:comment => "hai")
c1.update_attribute(:group_comment_id, "testid1")
@submission2 = @assignment.submit_homework(@course.enroll_student(user).user, :body => 'sub2')
c2 = @submission2.add_comment(:comment => "hai2")
c2.update_attribute(:group_comment_id, "testid1")
@assignment2 = assignment_model(:course => @course)
@assignment2.update_attribute(:workflow_state, 'published')
@submission3 = @assignment2.submit_homework(@student1, :body => 'sub3')
c3 = @submission3.add_comment(:comment => "hai3")
c3.update_attribute(:group_comment_id, "testid1")
c1.destroy
SubmissionComment.find_by_id(c1.id).should be_nil
SubmissionComment.find_by_id(c2.id).should be_nil
SubmissionComment.find_by_id(c3.id).should == c3
end
end
context "migration" do
def raw_comment(submission, author, comment, time=Time.now.utc)
c = Submission.connection
c.execute <<-SQL
INSERT INTO submission_comments(submission_id, author_id, created_at, comment)
VALUES(#{c.quote(submission.id)}, #{c.quote(author.id)}, #{c.quote(time)}, #{c.quote(comment)})
SQL
end
before do
@course.enroll_student(user)
@student2 = @user
@submission2 = @assignment.submit_homework(@student2, :body => 'some message')
end
it "should only create messages where conversations already exist" do
convo1 = @student1.initiate_conversation([@teacher1])
convo1.add_message('ohai')
convo2 = @student1.initiate_conversation([@teacher2])
convo2.add_message('hey', :update_for_sender => false) # like if the student did a bulk private message
@student1.conversations.size.should eql 1 # second one is not visible to student
@student1.conversations.first.messages.size.should eql 1
@student2.conversations.size.should eql 0
@teacher1.conversations.size.should eql 1
@teacher1.conversations.first.messages.size.should eql 1
@teacher2.conversations.size.should eql 1
@teacher2.conversations.first.messages.size.should eql 1
raw_comment(@submission1, @student1, "hello")
@submission1.create_or_update_conversations!(:migrate)
raw_comment(@submission2, @student2, "yo")
@submission2.create_or_update_conversations!(:migrate)
# same number of conversations, but existing ones got the new message
@student1.conversations.size.should eql 1 # second one is still not visible to student
@student1.conversations.first.messages.size.should eql 2
@student2.conversations.size.should eql 0
@teacher1.conversations.size.should eql 1
@teacher1.conversations.first.messages.size.should eql 2
@teacher2.conversations.size.should eql 1
@teacher2.conversations.first.messages.size.should eql 2
end
it "should not change any unread count/status" do
convo = @student1.initiate_conversation([@teacher1])
convo.add_message('ohai')
@student1.conversations.size.should eql 1
convo.messages.size.should eql 1
@teacher1.conversations.size.should eql 1
tconvo = @teacher1.conversations.first
tconvo.messages.size.should eql 1
tconvo.workflow_state = :read
tconvo.save!
@teacher1.reload.unread_conversations_count.should eql 0
raw_comment(@submission1, @student1, "hello")
@submission1.create_or_update_conversations!(:migrate)
convo.reload.messages.size.should eql 2
convo.should be_read
@student1.reload.unread_conversations_count.should eql 0
tconvo.reload.messages.size.should eql 2
tconvo.should be_read
@teacher1.reload.unread_conversations_count.should eql 0
end
it "should update last_message_at, message_count and last_authored_at" do
convo = @student1.initiate_conversation([@teacher1])
convo.add_message('ohai')
tconvo = @teacher1.conversations.first
raw_comment(@submission1, @student1, "hello", Time.now.utc + 1.day)
raw_comment(@submission1, @student1, "hello!", Time.now.utc + 2.day)
@submission1.create_or_update_conversations!(:migrate)
comments = @submission1.submission_comments
convo.reload.messages.size.should eql 2
convo.last_message_at.to_i.should eql comments.last.created_at.to_i
convo.last_authored_at.to_i.should eql comments.last.created_at.to_i
convo.messages.first.created_at.to_i.should eql comments.last.created_at.to_i
tconvo.reload.messages.size.should eql 2
tconvo.last_message_at.to_i.should eql comments.last.created_at.to_i
tconvo.last_authored_at.should be_nil
tconvo.messages.first.created_at.to_i.should eql comments.last.created_at.to_i
end
it "should skip submissions with no participant comments" do
convo = @student1.initiate_conversation([@teacher1])
message = convo.add_message('ohai').reload
tconvo = @teacher1.conversations.first
raw_comment(@submission1, user, "ohai im in ur group", Time.now.utc + 1.day)
# should not add a submission message
@submission1.create_or_update_conversations!(:migrate)
convo.reload.messages.size.should eql 1
convo.last_message_at.to_i.should eql message.created_at.to_i
convo.last_authored_at.to_i.should eql message.created_at.to_i
convo.messages.first.created_at.to_i.should eql message.created_at.to_i
tconvo.reload.messages.size.should eql 1
tconvo.last_message_at.to_i.should eql message.created_at.to_i
tconvo.last_authored_at.should be_nil
tconvo.messages.first.created_at.to_i.should eql message.created_at.to_i
end
end
end
it "should prevent peer reviewer from seeing other comments" do
@student1 = @student
@student2 = student_in_course(:active_all => true).user

View File

@ -102,28 +102,6 @@ describe Submission do
end
end
it "should not return duplicate conversation groups" do
assignment_model
@assignment.workflow_state = 'published'
@assignment.save!
section1 = @course.course_sections.create(name: '1')
section2 = @course.course_sections.create(name: '2')
section3 = @course.course_sections.create(name: '3')
section4 = @course.course_sections.create(name: '4')
section5 = @course.course_sections.create(name: '5')
section1.enroll_user(@teacher, 'TeacherEnrollment', 'accepted')
section2.enroll_user(@teacher, 'TeacherEnrollment', 'accepted')
section3.enroll_user(@teacher, 'TeacherEnrollment', 'accepted')
section4.enroll_user(@teacher, 'TeacherEnrollment', 'invited')
section5.enroll_user(@teacher, 'TeacherEnrollment', 'completed')
@course.offer!
@course.enroll_student(@student = user)
@assignment.context.reload
@submission = @assignment.submit_homework(@student, :body => 'some message')
@submission.conversation_groups.should eql @submission.conversation_groups.uniq
end
it "should ensure the media object exists" do
assignment_model
se = @course.enroll_student(user)

View File

@ -1,261 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations attachments local tests" do
include_examples "in-process server selenium tests"
before do
conversation_setup
local_storage!
end
it "should be able to add an attachment" do
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
new_conversation
submit_message_form(:attachments => [fullpath])
@user.all_conversations.order("conversation_id DESC").last.has_attachments.should be_true
@user.conversation_attachments_folder.attachments.count.should == 1
end
it "should be able to remove attachments from the message form" do
new_conversation
add_attachment_link = f(".action_add_attachment")
add_attachment_link.click
wait_for_ajaximations
add_attachment_link.click
wait_for_ajaximations
ffj(".attachment_list > .attachment:visible .remove_link")[1].click
wait_for_ajaximations
ffj(".attachment_list > .attachment:visible").size.should == 1
ffj(".attachment_list > .attachment:visible .remove_link")[0].click
submit_message_form
@user.all_conversations.order("conversation_id DESC").last.has_attachments.should be_false
end
it "should save just one attachment when sending a bulk private message" do
student_in_course
@course.enroll_user(User.create(:name => "student1"))
@course.enroll_user(User.create(:name => "student2"))
@course.enroll_user(User.create(:name => "student3"))
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
add_recipient("student1")
add_recipient("student2")
add_recipient("student3")
ConversationBatch.any_instance.stubs(:mode).returns(:sync)
expect {
submit_message_form(:attachments => [fullpath], :add_recipient => false, :group_conversation => false)
}.to change(Attachment, :count).by(1)
end
it "should save attachments on new messages on existing conversations" do
student_in_course
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
submit_message_form
message = submit_message_form(:attachments => [fullpath])
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
message = "#message_#{message.id}"
ffj("#{message} .message_attachments li").size.should == 1
end
it "should save multiple attachments" do
student_in_course
file1 = get_file("testfile1.txt")
file2 = get_file("testfile2.txt")
new_conversation
message = submit_message_form(:attachments => [file1[1], file2[1]])
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
message = "#message_#{message.id}"
ffj("#{message} .message_attachments li").size.should == 2
fj("#{message} .message_attachments li:first a .title").text.should == file1[0]
fj("#{message} .message_attachments li:last a .title").text.should == file2[0]
end
it "should show forwarded attachments" do
student_in_course
@course.enroll_user(User.create(:name => 'student1'))
@course.enroll_user(User.create(:name => 'student2'))
filename, fullpath, data = get_file('testfile1.txt')
new_conversation
add_recipient('student1')
submit_message_form(:attachments => [fullpath], :add_recipient => false)
wait_for_ajaximations
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
get_messages(false).first.click
wait_for_ajaximations
f('#action_forward').click
add_recipient('student2', '#forward_recipients')
f('#forward_body').send_keys('ohai look an attachment')
f('#forward_message_form').submit
wait_for_ajaximations
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
ff('img.attachments').size.should == 2
messages = get_messages(false) # conversation already loaded
messages.size.should == 1
messages.first.text.should include "ohai look an attachment"
messages.first.text.should include filename
end
it "should save attachments on initial messages on new conversations" do
pending('connection refused - connect(2) - line 108')
student_in_course
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
message = submit_message_form(:attachments => [fullpath])
message = "#message_#{message.id}"
ffj("#{message} .message_attachments li").size.should == 1
fj("#{message} .message_attachments li a .title").text.should == filename
download_link = f("#{message} .message_attachments li a")
keep_trying_until do
file = open(download_link.attribute('href'))
file.read.should match data
end
end
end
describe "conversations attachments S3 tests" do
include_examples "in-process server selenium tests"
before do
conversation_setup
s3_storage!(:stubs => false)
end
it "should be able to add an attachment" do
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
new_conversation
submit_message_form(:attachments => [fullpath])
@user.all_conversations.order("conversation_id DESC").last.has_attachments.should be_true
@user.conversation_attachments_folder.attachments.count.should == 1
end
it "should be able to remove attachments from the message form" do
new_conversation
add_attachment_link = f(".action_add_attachment")
add_attachment_link.click
wait_for_ajaximations
add_attachment_link.click
wait_for_ajaximations
ffj(".attachment_list > .attachment:visible .remove_link")[1].click
wait_for_ajaximations
ffj(".attachment_list > .attachment:visible").size.should == 1
ffj(".attachment_list > .attachment:visible .remove_link")[0].click
submit_message_form
@user.all_conversations.order("conversation_id DESC").last.has_attachments.should be_false
end
it "should save just one attachment when sending a bulk private message" do
student_in_course
@course.enroll_user(User.create(:name => "student1"))
@course.enroll_user(User.create(:name => "student2"))
@course.enroll_user(User.create(:name => "student3"))
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
add_recipient("student1")
add_recipient("student2")
add_recipient("student3")
ConversationBatch.any_instance.stubs(:mode).returns(:sync)
expect {
submit_message_form(:attachments => [fullpath], :add_recipient => false, :group_conversation => false)
}.to change(Attachment, :count).by(1)
end
it "should save attachments on new messages on existing conversations" do
student_in_course
filename, fullpath, data = get_file("testfile1.txt")
new_conversation
submit_message_form
message = submit_message_form(:attachments => [fullpath])
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
message = "#message_#{message.id}"
ffj("#{message} .message_attachments li").size.should == 1
end
it "should save multiple attachments" do
student_in_course
file1 = get_file("testfile1.txt")
file2 = get_file("testfile2.txt")
new_conversation
message = submit_message_form(:attachments => [file1[1], file2[1]])
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
message = "#message_#{message.id}"
ffj("#{message} .message_attachments li").size.should == 2
fj("#{message} .message_attachments li:first a .title").text.should == file1[0]
fj("#{message} .message_attachments li:last a .title").text.should == file2[0]
end
it "should show forwarded attachments" do
student_in_course
@course.enroll_user(User.create(:name => 'student1'))
@course.enroll_user(User.create(:name => 'student2'))
filename, fullpath, data = get_file('testfile1.txt')
new_conversation
add_recipient('student1')
submit_message_form(:attachments => [fullpath], :add_recipient => false)
wait_for_ajaximations
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
get_messages(false).first.click
wait_for_ajaximations
f('#action_forward').click
add_recipient('student2', '#forward_recipients')
f('#forward_body').send_keys('ohai look an attachment')
f('#forward_message_form').submit
wait_for_ajaximations
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
ff('img.attachments').size.should == 2
messages = get_messages(false) # conversation already loaded
messages.size.should == 1
messages.first.text.should include "ohai look an attachment"
messages.first.text.should include filename
end
end

View File

@ -1,234 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations context filtering" do
include_examples "in-process server selenium tests"
before (:each) do
conversation_setup
@course.update_attribute(:name, "the course")
@course1 = @course
@s1 = User.create(:name => "student1")
@s2 = User.create(:name => "student2")
@course1.enroll_user(@s1).update_attribute(:workflow_state, 'active')
@course1.enroll_user(@s2).update_attribute(:workflow_state, 'active')
@group = @course1.groups.create(:name => "the group")
@group.users << @user << @s1 << @s2
@course2 = course(:active_all => true, :course_name => "that course")
@course2.enroll_teacher(@user).accept
@course2.enroll_user(@s1).update_attribute(:workflow_state, 'active')
end
it "should capture the course when sending a message to a group" do
new_conversation
browse_menu
browse("the course", "Student Groups", "the group") { click "Select All" }
submit_message_form(:add_recipient => false)
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
audience = fj("#create_message_form ul.conversations .audience")
audience.text.should include @course1.name
audience.text.should_not include @course2.name
audience.text.should include @group.name
end
it "should capture the course when sending a message to a user under a course" do
new_conversation
browse_menu
browse("the course") { search("stu") { click "student1" } }
submit_message_form(:add_recipient => false)
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
audience = fj("#create_message_form ul.conversations .audience")
audience.text.should include @course1.name
audience.text.should_not include @course2.name
audience.text.should_not include @group.name
end
it "should order by active-ness before name or type" do
@course2.complete!
new_conversation
@input = fj("#context_tags_filter input:visible")
search("th", "#context_tags") do
menu.should == ["the course", "the group", "that course"]
end
end
it "should let you browse for filters" do
new_conversation
@browser = fj("#context_tags_filter .browser:visible")
@input = fj("#context_tags_filter input:visible")
browse_menu
menu.should == ["that course", "the course", "the group"]
browse "that course" do
menu.should == ["that course", "Everyone", "Teachers", "Students"]
browse("Everyone") { menu.should == ["nobody@example.com", "student1", "User"] }
browse("Teachers") { menu.should == ["nobody@example.com", "User"] }
browse("Students") { menu.should == ["student1"] }
end
browse "the course" do
menu.should == ["the course", "Everyone", "Teachers", "Students", "Student Groups"]
browse("Everyone") { menu.should == ["nobody@example.com", "student1", "student2"] }
browse("Teachers") { menu.should == ["nobody@example.com"] }
browse("Students") { menu.should == ["student1", "student2"] }
browse "Student Groups" do
menu.should == ["the group"]
browse("the group") { menu.should == ["the group", "nobody@example.com", "student1", "student2"] }
end
end
browse("the group") { menu.should == ["the group", "nobody@example.com", "student1", "student2"] }
end
it "should let you filter by a course" do
pending("xvfb issues")
new_conversation
browse_menu
browse("the course", "Everyone") { click "student2" }
browse_menu
browse("that course", "Everyone") { click "student1" }
submit_message_form(:add_recipient => false, :message => "asdf") # tagged with both courses
new_conversation(false)
browse_menu
browse("that course", "Everyone") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "qwerty")
get_conversations.size.should == 2
@input = fj("#context_tags_filter input:visible")
search("the course", "#context_tags") { browse("the course") { click("the course") } }
keep_trying_until do
conversations = get_conversations
conversations.size.should == 1
conversations.first.find_element(:css, 'p').text.should == 'asdf'
end
#filtered course should be first in the audience's contexts
get_conversations.first.find_element(:css, '.audience em').text.should == 'the course and that course'
end
it "should let you filter by a course that was concluded a long time ago" do
new_conversation
browse_menu
browse("the course", "Everyone") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "asdf")
new_conversation(false)
browse_menu
browse("that course", "Everyone") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "qwerty")
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
get_conversations.size.should == 2
@course1.complete!
@course1.update_attribute :conclude_at, 1.year.ago
get "/conversations/sent"
@input = fj("#context_tags_filter input:visible")
search("the course", "#context_tags") { browse("the course") { click("the course") } }
keep_trying_until do
conversations = get_conversations
conversations.size.should == 1
conversations.first.find_element(:css, 'p').text.should == 'asdf'
end
end
it "should let you filter by a user" do
pending("need to fix")
new_conversation
browse_menu
browse("the course", "Everyone") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "asdf")
new_conversation(false)
browse_menu
browse("that course", "Everyone") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "qwerty")
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
@input = fj("#context_tags_filter input:visible")
search("student2", "#context_tags") { click("student2") }
keep_trying_until do
conversations = get_conversations
conversations.size.should == 1
conversations.first.find_element(:css, 'p').text.should == 'asdf'
end
# filtered student should be first in the audience
get_conversations.first.find_element(:css, '.audience').text.should == 'student2 and student1 the course'
end
it "should let you filter by a group" do
pending("xvfb issues")
new_conversation
browse_menu
browse("the course", "Everyone") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "asdf")
new_conversation(false)
browse_menu
browse("the group") { click "Select All" }
submit_message_form(:add_recipient => false, :message => "qwerty")
@input = fj("#context_tags_filter input:visible")
search("the group", "#context_tags") {
menu.should == ["the group"]
elements.first.first.text.should include "the course" # make sure the group context is shown
browse("the group") { click("the group") }
}
keep_trying_until do
conversations = get_conversations
conversations.size.should == 1
conversations.first.find_element(:css, 'p').text.should == 'qwerty'
end
end
it "should show the term name by the course" do
new_conversation
browse_menu
browse("the course") { search("stu") { click "student1" } }
submit_message_form(:add_recipient => false)
@input = fj("#context_tags_filter input:visible")
search("the course", "#context_tags") do
term_info = f('.autocomplete_menu .name .context_info')
term_info.text.should == "(#{@course1.enrollment_term.name})"
end
end
it "should not show the default term name" do
new_conversation
browse_menu
browse("the course") { search("stu") { click "student1" } }
submit_message_form(:add_recipient => false)
@input = fj("#context_tags_filter input:visible")
search("that course", "#context_tags") do
term_info = f('.autocomplete_menu .name')
term_info.text.should == "that course"
end
end
end

View File

@ -1,84 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations group" do
include_examples "in-process server selenium tests"
before(:each) do
conversation_setup
@course.update_attribute(:name, "the course")
@course.default_section.update_attribute(:name, "the section")
@other_section = @course.course_sections.create(:name => "the other section")
s1 = User.create(:name => "student 1")
@course.enroll_user(s1)
s2 = User.create(:name => "student 2")
@course.enroll_user(s2, "StudentEnrollment", :section => @other_section)
@group = @course.groups.create(:name => "the group")
@group.users << s1
new_conversation
@input = fj("#create_message_form input:visible")
@checkbox = f(".group_conversation")
end
def choose_recipient(*names)
name = names.shift
level = 1
@input.send_keys(name)
wait_for_ajaximations(250)
loop do
keep_trying_until { ffj('.autocomplete_menu:visible .list').size == level }
driver.execute_script("return $('.autocomplete_menu:visible .list').last().find('ul').last().find('li').toArray();").detect { |e|
(e.find_element(:tag_name, :b).text rescue e.text) == name
}.click
wait_for_ajaximations(250)
break if names.empty?
level += 1
name = names.shift
end
keep_trying_until { fj('.autocomplete_menu:visible').nil? }
end
it "should not be an option with no recipients" do
@checkbox.should_not be_displayed
end
it "should not be an option for a single individual recipient" do
choose_recipient("student 1")
@checkbox.should_not be_displayed
end
it "should be an option, default false, for a single bulk recipient" do
choose_recipient("the course", "Everyone", "Select All")
@checkbox.should be_displayed
is_checked(".group_conversation").should be_false
end
it "should be an option, default false, for multiple individual recipients" do
choose_recipient("student 1")
choose_recipient("student 2")
@checkbox.should be_displayed
is_checked(".group_conversation").should be_false
end
it "should disappear when there are no longer multiple recipients" do
choose_recipient("student 1")
choose_recipient("student 2")
@input.send_keys([:backspace])
@checkbox.should_not be_displayed
end
it "should revert to false after disappearing and reappearing" do
choose_recipient("student 1")
choose_recipient("student 2")
@checkbox.click
@input.send_keys([:backspace])
choose_recipient("student 2")
@checkbox.should be_displayed
is_checked(".group_conversation").should be_false
end
end

View File

@ -137,9 +137,6 @@ describe "conversations new" do
before do
conversation_setup
@teacher.preferences[:use_new_conversations] = true
@teacher.save!
@s1 = user(name: "first student")
@s2 = user(name: "second student")
[@s1, @s2].each { |s| @course.enroll_student(s).update_attribute(:workflow_state, 'active') }
@ -165,8 +162,6 @@ describe "conversations new" do
it "should allow admins to send a message without picking a context" do
user = account_admin_user
user.preferences[:use_new_conversations] = true
user.save!
user_logged_in({:user => user})
get_conversations
compose to: [@s1], subject: 'context-free', body: 'hallo!'
@ -184,8 +179,6 @@ describe "conversations new" do
it "should allow admins to message users from their profiles" do
user = account_admin_user
user.preferences[:use_new_conversations] = true
user.save!
user_logged_in({:user => user})
get "/accounts/#{Account.default.id}/users"
wait_for_ajaximations
@ -223,8 +216,6 @@ describe "conversations new" do
end
it "should not be allowed for students" do
@s1.preferences[:use_new_conversations] = true
@s1.save!
user_session(@s1)
get_conversations
compose course: @course, to: [@s2], body: 'hallo!', send: false

View File

@ -1,279 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations recipient finder" do
include_examples "in-process server selenium tests"
def conversations_path(params={})
hash = params.to_json.unpack('H*').first
"/conversations##{hash}"
end
before(:each) do
conversation_setup
@course.update_attribute(:name, "the course")
@course.default_section.update_attribute(:name, "the section")
@other_section = @course.course_sections.create(:name => "the other section")
@s1 = User.create(:name => "student 1")
@course.enroll_user(@s1)
@s2 = User.create(:name => "student 2")
@course.enroll_user(@s2, "StudentEnrollment", :section => @other_section)
@group = @course.groups.create(:name => "the group")
@group.users << @s1 << @user
new_conversation
end
it "should allow browsing" do
browse_menu
menu.should == ["the course", "the group"]
browse "the course" do
menu.should == ["Everyone", "Teachers", "Students", "Course Sections", "Student Groups"]
toggleable.should == ["Everyone", "Teachers", "Students"]
browse("Everyone") { menu.should == ["Select All", "nobody@example.com", "student 1", "student 2"] }
browse("Teachers") { menu.should == ["nobody@example.com"] }
browse("Students") { menu.should == ["Select All", "student 1", "student 2"] }
browse "Course Sections" do
menu.should == ["the other section", "the section"]
browse "the other section" do
menu.should == ["Students"]
browse("Students") { menu.should == ["student 2"] }
end
browse "the section" do
menu.should == ["Everyone", "Teachers", "Students"]
browse("Everyone") { menu.should == ["Select All", "nobody@example.com", "student 1"] }
browse("Teachers") { menu.should == ["nobody@example.com"] }
browse("Students") { menu.should == ["student 1"] }
end
end
browse "Student Groups" do
menu.should == ["the group"]
browse("the group") { menu.should == ["Select All", "nobody@example.com", "student 1"] }
end
end
browse("the group") { menu.should == ["Select All", "nobody@example.com", "student 1"] }
end
it "should respect permissions" do
RoleOverride.create!(:context => Account.default, :permission => 'send_messages_all', :enrollment_type => 'TeacherEnrollment', :enabled => false)
browse_menu
menu.should == ["the course", "the group"]
browse "the course" do
menu.should == ["Everyone", "Teachers", "Students", "Course Sections", "Student Groups"]
toggleable.should == []
browse("Everyone") { menu.should == ["nobody@example.com", "student 1", "student 2"] }
browse("Teachers") { menu.should == ["nobody@example.com"] }
browse("Students") { menu.should == ["student 1", "student 2"] }
browse "Course Sections" do
menu.should == ["the other section", "the section"]
browse "the other section" do
menu.should == ["Students"]
browse("Students") { menu.should == ["student 2"] }
end
browse "the section" do
menu.should == ["Everyone", "Teachers", "Students"]
browse("Everyone") { menu.should == ["nobody@example.com", "student 1"] }
browse("Teachers") { menu.should == ["nobody@example.com"] }
browse("Students") { menu.should == ["student 1"] }
end
end
browse "Student Groups" do
menu.should == ["the group"]
browse("the group") { menu.should == ["nobody@example.com", "student 1"] }
end
end
browse("the group") { menu.should == ["nobody@example.com", "student 1"] }
end
it "should not show concluded enrollments as students in the course" do
pending('bug 7583 - concluded students in a live course still show up as students in the course when addressing messages in the inbox') do
student_1_enrollment = @s1.enrollments.last
student_1_enrollment.update_attributes(:workflow_state => 'completed')
student_1_enrollment.save!
student_1_enrollment.reload
browse_menu
browse("the course") do
browse("Students") { menu.should == ["Select All", "student 2"] }
end
end
end
it "should not return courses concluded a long time ago" do
@course.complete!
@course.update_attribute :conclude_at, 1.year.ago
browse_menu
menu.should == ["No results found"]
search("course") do
menu.should == ["No results found"]
end
end
it "should check already-added tokens when browsing" do
browse_menu
browse("the group") do
menu.should == ["Select All", "nobody@example.com", "student 1"]
toggle "student 1"
tokens.should == ["student 1"]
end
browse("the course") do
browse("Everyone") do
toggled.should == ["student 1"]
end
end
end
it "should have working select all checkboxes in appropriate contexts" do
browse_menu
browse "the course" do
toggle "Everyone"
toggled.should == ["Everyone", "Teachers", "Students"]
tokens.should == ["the course: Everyone"]
toggle "Everyone"
toggled.should == []
tokens.should == []
toggle "Students"
toggled.should == ["Students"]
tokens.should == ["the course: Students"]
toggle "Teachers"
toggled.should == ["Everyone", "Teachers", "Students"]
tokens.should == ["the course: Everyone"]
toggle "Teachers"
toggled.should == ["Students"]
tokens.should == ["the course: Students"]
browse "Teachers" do
toggle "nobody@example.com"
toggled.should == ["nobody@example.com"]
tokens.should == ["the course: Students", "nobody@example.com"]
toggle "nobody@example.com"
toggled.should == []
tokens.should == ["the course: Students"]
end
toggled.should == ["Students"]
toggle "Teachers"
toggled.should == ["Everyone", "Teachers", "Students"]
tokens.should == ["the course: Everyone"]
browse "Students" do
toggle "Select All"
toggled.should == []
tokens.should == ["the course: Teachers"]
toggle "student 1"
toggle "student 2"
toggled.should == ["Select All", "student 1", "student 2"]
tokens.should == ["the course: Everyone"]
end
toggled.should == ["Everyone", "Teachers", "Students"]
browse "Everyone" do
toggle "student 1"
toggled.should == ["nobody@example.com", "student 2"]
tokens.should == ["nobody@example.com", "student 2"]
end
toggled.should == []
end
end
it "should allow searching" do
search("t") do
menu.should == ["the course", "the other section", "the section", "the group", "student 1", "student 2"]
end
end
it "should show the group context when searching at the top level" do
search("the group") do
menu.first.should == "the group"
elements.first.first.text.should include "the course"
end
end
it "should omit already-added tokens when searching" do
search("student") do
menu.should == ["student 1", "student 2"]
click "student 1"
end
tokens.should == ["student 1"]
search("stu") do
menu.should == ["student 2"]
end
end
it "should allow searching under supported contexts" do
browse_menu
browse "the course" do
search("t") { menu.should == ["the other section", "the section", "the group", "student 1", "student 2"] }
browse "Everyone" do
# only returns users
search("T") { menu.should == ["student 1", "student 2"] }
end
browse "Course Sections" do
# only returns sections
search("student") { menu.should == ["No results found"] }
search("r") { menu.should == ["the other section"] }
browse "the section" do
search("s") { menu.should == ["student 1"] }
end
end
browse "Student Groups" do
# only returns groups
search("student") { menu.should == ["No results found"] }
search("the") { menu.should == ["the group"] }
browse "the group" do
search("s") { menu.should == ["student 1"] }
search("group") { menu.should == ["No results found"] }
end
end
end
end
it "should allow a user id in the url hash to add recipient" do
# check without any user_name
get conversations_path(:user_id => @s1.id)
wait_for_ajaximations
tokens.should == ["student 1"]
# explanation of user_name param: we used to pass the user name in the
# hash fragment, and it was spoofable. now we load that data via ajax.
get conversations_path(:user_id => @s1.id, :user_name => "some_fake_name")
wait_for_ajaximations
tokens.should == ["student 1"]
end
it "should reject a non-contactable user id in the url hash" do
other = User.create(:name => "other guy")
get conversations_path(:user_id => other.id)
wait_for_ajaximations
tokens.should == []
end
it "should allow a non-contactable user in the hash if a shared conversation exists" do
other = User.create(:name => "other guy")
# if the users have a conversation in common already, then the recipient can be added
c = Conversation.initiate([@user, other], true)
get conversations_path(:user_id => other.id, :from_conversation_id => c.id)
wait_for_ajaximations
tokens.should == ["other guy"]
end
it "should not show student view student to other students" do
@fake_student = @course.student_view_student
search(@fake_student.name) do
menu.should == ["No results found"]
end
end
end

View File

@ -1,70 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations sent filter" do
include_examples "in-process server selenium tests"
before (:each) do
conversation_setup
@course.update_attribute(:name, "the course")
@course1 = @course
@s1 = User.create(:name => "student1")
@s2 = User.create(:name => "student2")
@course1.enroll_user(@s1)
@course1.enroll_user(@s2)
ConversationMessage.any_instance.stubs(:current_time_from_proper_timezone).returns(*100.times.to_a.reverse.map { |h| Time.now.utc - h.hours })
@c1 = conversation(@user, @s1)
@c2 = conversation(@user, @s2)
@c1.add_message('yay i sent this')
@c2.conversation.add_message(@s2, "ohai im not u so this wont show up on the left")
get "/conversations/sent"
conversations = get_conversations
conversations.first.should have_attribute('data-id', @c1.conversation_id.to_s)
conversations.first.should include_text('yay i sent this')
conversations.last.should have_attribute('data-id', @c2.conversation_id.to_s)
conversations.last.should include_text('test')
end
it "should reorder based on last authored message" do
first_message_text = 'qwerty'
get_conversations.last.click
get_messages(false)
submit_message_form(:message => first_message_text, :existing_conversation => true)
ff(".last_author").length.should == 2
ff(".last_author")[0].should include_text(first_message_text)
ff(".last_author")[1].should include_text('yay i sent this')
end
it "should remove the conversation when the last message by the author is deleted" do
get_conversations.last.click
msgs = get_messages(false)
msgs.size.should == 2
msgs.last.click
delete_selected_messages
end
it "should show/update all conversations when sending a bulk private message" do
message_text = 'ohai guys'
@s3 = User.create(:name => "student3")
@course1.enroll_user(@s3)
new_conversation(false)
add_recipient("student1")
add_recipient("student2")
add_recipient("student3")
ConversationBatch.any_instance.stubs(:mode).returns(:sync)
submit_message_form(:message => message_text, :add_recipient => false, :group_conversation => false)
conversations = get_conversations
conversations.size.should == 3
conversations.each { |conversation| conversation.should include_text(message_text) }
end
end

View File

@ -1,386 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations" do
include_examples "in-process server selenium tests"
before (:each) do
conversation_setup
end
it "should not allow double form submissions" do
new_message = 'new conversation message'
@s1 = User.create(:name => 'student1')
@course.enroll_user(@s1)
new_conversation
add_recipient("student1")
expect {
f('#create_message_form .conversation_body').send_keys(new_message)
5.times { submit_form('#create_message_form form') rescue nil }
assert_message_status("sent", new_message[0, 10])
}.to change(ConversationMessage, :count).by(1)
end
describe 'actions' do
def create_conversation(workflow_state = 'unread', starred = false, url = '/conversations')
@me = @user
conversation(@me, user, :workflow_state => workflow_state, :starred => starred)
get url unless url == nil
end
it "should auto-mark as read" do
@me = @user
5.times { conversation(@me, user, :workflow_state => 'unread') }
get "/conversations/unread"
ce = get_conversations.first
ce.should have_class('unread') # not marked immediately
ce.click
wait_for_ajaximations
@me.conversations.unread.size.should == 5
keep_trying_until do
get_conversations.first.should_not have_class('unread')
true
end
@me.conversations.unread.size.should == 4
get_conversations.last.click
get_conversations.size.should == 4 # removed once deselected
end
it "should not open the conversation when the gear menu is clicked" do
create_conversation
wait_for_ajaximations
f('#menu-wrapper .al-options').should be_nil
driver.execute_script "$('.admin-link-hover-area').addClass('active')"
f('.admin-links button').click
wait_for_ajaximations
f('#menu-wrapper .al-options').should be_displayed
f('.messages').should_not be_displayed
end
it "should star a conversation" do
create_conversation
f('#conversations .action_star').click
wait_for_ajaximations
f('#conversations .action_unstar').should be_displayed
f('#conversations .action_star').should_not be_displayed
end
it "should unstar a conversation" do
create_conversation('unread', true)
f('#conversations .action_unstar').click
wait_for_ajaximations
f('#conversations .action_star').should be_displayed
f('#conversations .action_unstar').should_not be_displayed
end
it "should mark a conversation as unread" do
create_conversation('read', false)
f('.action_mark_as_unread').click
wait_for_ajaximations
f('.action_mark_as_unread').should_not be_displayed
f('.action_mark_as_read').should be_displayed
expect_new_page_load { get '/conversations/archived' }
f('.conversations .audience').should include_text('New Message')
end
it "should delete a conversation" do
create_conversation
wait_for_ajaximations
driver.execute_script "$('.admin-link-hover-area').addClass('active')"
f('.admin-links button').click
f('.al-options .action_delete_all').click
driver.switch_to.alert.accept
wait_for_ajaximations
f('#no_messages').should be_displayed
end
it "should archive a conversation" do
create_conversation
wait_for_ajaximations
driver.execute_script("$('.admin-link-hover-area').addClass('active')")
f('.admin-links button').click
f('.al-options .action_archive').click
wait_for_ajaximations
f('#no_messages').should be_displayed
expect_new_page_load { get '/conversations/archived' }
f('.conversations .audience').should include_text('User')
end
it "should allow you to filter a conversation by sent" do
create_conversation
expect_new_page_load { get '/conversations/archived' }
f('.conversations .audience').should include_text('New Message')
end
end
context "New message... link" do
before :each do
@me = @user
@other = user(:name => 'Some OtherDude')
@course.enroll_student(@other)
conversation(@me, @other, :workflow_state => 'unread')
@participant_me = @conversation
@convo = @participant_me.conversation
@convo.add_message(@other, "Hey bud!")
@convo.add_message(@me, "Howdy friend!")
get '/conversations'
f('.unread').click
wait_for_ajaximations
end
it "should not display on my own message" do
# Hover over own message
driver.execute_script("$('.message.self:first .send_private_message').focus()")
f(".message.self .send_private_message").should_not be_displayed
end
it "should display on messages from others" do
# Hover over the message from the other writer to display link
# This spec fails locally in isolation and in this context block.
driver.execute_script("$('.message.other .send_private_message').focus()")
f(".message.other .send_private_message").should be_displayed
end
it "should start new message to the user" do
f(".message.other .send_private_message").click()
wait_for_ajaximations
# token gets added after brief delay
sleep(0.4)
# create "token" with the 'other' user
f("#create_message_form .token_input ul").text().should == @other.name
end
end
context 'messages' do
before(:each) do
@me = @user
conversation(@me, user, :workflow_state => 'unread')
get '/conversations'
f('.unread').click
wait_for_ajaximations
f(".messages #message_#{ConversationMessage.last.id}").click
end
it "should forward a message" do
forward_body_text = 'new forward'
f('#action_forward').click
fj('#forward_message_form .token_input input').send_keys('nobody')
wait_for_ajaximations
f('.selectable').click
f('#forward_body').send_keys(forward_body_text)
f('.ui-dialog-buttonset > .btn-primary').click
wait_for_ajaximations
expect_new_page_load { get '/conversations/sent' }
f('.conversations li.read').should include_text(forward_body_text)
end
it "should delete a message" do
f('#action_delete').click
driver.switch_to.alert.accept
wait_for_ajaximations
f('#no_messages').should be_displayed
end
end
context "conversation loading" do
it "should load all conversations" do
@me = @user
num = 51
num.times { conversation(@me, user) }
get "/conversations"
keep_trying_until do
elements = get_conversations
elements.last.location_once_scrolled_into_view
elements.size.should == num
end
end
it "should properly clear the identity header when conversations are read" do
enable_cache do
@me = @user
5.times { conversation(@me, user, :workflow_state => 'unread') }
get_messages # loads the page, clicks the first conversation
keep_trying_until do
get_conversations.first.should_not have_class('unread')
true
end
get '/conversations'
f('.unread-messages-count').text.should == '4'
end
end
end
context "media comments" do
it "should add audio and video comments to the message form" do
# don't have a good way to test kaltura here, so we just fake it up
CanvasKaltura::ClientV3.expects(:config).at_least(1).returns({})
['audio', 'video'].each_with_index do |media_comment_type, index|
mo = MediaObject.new
mo.media_id = "0_12345678#{index}"
mo.media_type = media_comment_type
mo.context = @user
mo.user = @user
mo.title = "test title"
mo.save!
new_conversation(:message => media_comment_type)
message = submit_message_form(:media_comment => [mo.media_id, mo.media_type])
expect_new_page_load { get '/conversations/sent' }
f('.conversations li').click
wait_for_ajaximations
message = "#message_#{message.id}"
ff("#{message} .message_attachments li").size.should == 1
f("#{message} .message_attachments li a .title").text.should == mo.title
end
end
end
context "form audience" do
before (:each) do
# have @course, @teacher from before
# creates @student
student_in_course(:course => @course, :active_all => true)
@course.update_attribute(:name, "the course")
@group = @course.groups.create(:name => "the group")
@group.participating_users << @student
conversation(@teacher, @student)
end
it "should link to the course page" do
get_messages
expect_new_page_load { fj("#create_message_form .audience a").click }
driver.current_url.should match %r{/courses/#{@course.id}}
end
it "should not be a link in the left conversation list panel" do
new_conversation
ffj("#conversations .audience a").should be_empty
end
end
context "private messages" do
before do
@course.update_attribute(:name, "the course")
@course1 = @course
@s1 = User.create(:name => "student1")
@s2 = User.create(:name => "student2")
@course1.enroll_user(@s1)
@course1.enroll_user(@s2)
ConversationMessage.any_instance.stubs(:current_time_from_proper_timezone).returns(*100.times.to_a.reverse.map { |h| Time.now.utc - h.hours })
@c1 = conversation(@user, @s1)
@c1.add_message('yay i sent this')
end
it "should select the new conversation" do
new_conversation
add_recipient("student2")
submit_message_form(:message => "ohai", :add_recipient => false).should_not be_nil
end
it "should select the existing conversation" do
new_conversation
add_recipient("student1")
submit_message_form(:message => "ohai", :add_recipient => false, :existing_conversation => true).should_not be_nil
end
end
context "batch messages" do
it "shouldn't show anything in conversation list when sending batch messages to new recipients" do
@course.default_section.update_attribute(:name, "the section")
@s1 = User.create(:name => "student1")
@s2 = User.create(:name => "student2")
@course.enroll_user(@s1)
@course.enroll_user(@s2)
new_conversation
add_recipient("student1")
add_recipient("student2")
f("#create_message_form .conversation_body").send_keys "testing testing"
submit_form('#create_message_form')
wait_for_ajaximations
assert_message_status "sending"
run_jobs
assert_message_status "sent"
# no conversations should show up in the conversation list
get_conversations(false).should be_empty
end
end
context "bulk popovers" do
before (:each) do
@number_of_people = 10
@conversation_students = []
@number_of_people.times do |i|
u = User.create!(:name => "conversation student #{i}")
@course.enroll_user(u, "StudentEnrollment").accept!
@conversation_students << u
end
end
it "should validate the others popover" do
new_conversation
@conversation_students.each { |student| add_recipient(student.name) }
f("#create_message_form .conversation_body").send_keys "testing testing"
f('.group_conversation').click
submit_form('#create_message_form')
wait_for_ajaximations
run_jobs
expect_new_page_load { get "/conversations/sent" }
wait_for_ajaximations
f('.others').click
f('#others_popup').should be_displayed
ff('#others_popup li').count.should == (@conversation_students.count - 2) # - 2 because the first 2 show up in the conversation summary
end
end
context "help menu" do
it "should switch to new conversations and redirect" do
site_admin_logged_in
@user.watched_conversations_intro
@user.save
new_conversation
f('#help-btn').click
expect_new_page_load { fj('#try-new-conversations-menu-item').click }
f('#inbox').should be_nil # #inbox is in the old conversations ui and not the new ui
driver.execute_script("$('#help-btn').click()") #selenium.clik() not working in this case...
expect_new_page_load { fj('#switch-to-old-conversations-menu-item').click }
f('#inbox').should be_displayed
end
it "should show the intro" do
site_admin_logged_in
@user.watched_conversations_intro
@user.save
new_conversation
f('#help-btn').click
fj('#conversations-intro-menu-item').click
wait_for_ajaximations
ff('#conversations_intro').last.should be_displayed
end
end
end

View File

@ -1,92 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations submissions" do
include_examples "in-process server selenium tests"
before (:each) do
conversation_setup
end
it "should list submission comments in the conversation" do
@me = @user
@bob = student_in_course(:name => "bob", :active_all => true).user
submission1 = submission_model(:course => @course, :user => @bob)
submission2 = submission_model(:course => @course, :user => @bob)
submission1.add_comment(:comment => "hey bob", :author => @me)
submission1.add_comment(:comment => "wut up teacher", :author => @bob)
submission2.add_comment(:comment => "my name is bob", :author => @bob)
submission2.assignment.grade_student(@bob, {:grade => 0.9})
conversation(@bob)
get "/conversations"
elements = nil
keep_trying_until do
elements = get_conversations
elements.size == 1
end
elements.first.click
wait_for_ajaximations
subs = ff("#messages .submission")
subs.size.should == 2
subs[0].find_element(:css, '.score').text.should == '0.9 / 1.5'
subs[1].find_element(:css, '.score').text.should == 'no score'
coms = subs[0].find_elements(:css, '.comment')
coms.size.should == 1
coms.first.find_element(:css, '.audience').text.should == 'bob'
coms.first.find_element(:css, 'p').text.should == 'my name is bob'
coms = subs[1].find_elements(:css, '.comment')
coms.size.should == 2
coms.first.find_element(:css, '.audience').text.should == 'bob'
coms.first.find_element(:css, 'p').text.should == 'wut up teacher'
coms.last.find_element(:css, '.audience').text.should == 'nobody@example.com'
coms.last.find_element(:css, 'p').text.should == 'hey bob'
end
it "should interleave submissions with messages based on comment time" do
SubmissionComment.any_instance.stubs(:current_time_from_proper_timezone).returns(10.minutes.ago, 8.minutes.ago)
@me = @user
@bob = student_in_course(:name => "bob", :active_all => true).user
@conversation = conversation(@bob).conversation
@conversation.conversation_messages.first.update_attribute(:created_at, 9.minutes.ago)
submission1 = submission_model(:course => @course, :user => @bob)
submission1.add_comment(:comment => "hey bob", :author => @me)
# message comes first, then submission, due to creation times
msgs = get_messages
msgs.size.should == 2
msgs[0].should have_class('message')
msgs[1].should have_class('submission')
# now new submission comment bumps it up
submission1.add_comment(:comment => "hey teach", :author => @bob)
msgs = get_messages
msgs.size.should == 2
msgs[0].should have_class('submission')
msgs[1].should have_class('message')
# new message appears on top, submission now in the middle
@conversation.add_message(@bob, 'ohai there').update_attribute(:created_at, 7.minutes.ago)
msgs = get_messages
msgs.size.should == 3
msgs[0].should have_class('message')
msgs[1].should have_class('submission')
msgs[2].should have_class('message')
end
it "should allow deleting submission messages from the conversation" do
@me = @user
@bob = student_in_course(:name => "bob", :active_all => true).user
submission1 = submission_model(:course => @course, :user => @bob)
submission1.add_comment(:comment => "hey teach", :author => @bob)
@conversation = @me.conversations.first
@conversation.should be_present
msgs = get_messages
msgs.size.should == 1
msgs.first.click
delete_selected_messages
@conversation.reload
@conversation.last_message_at.should be_nil
end
end

View File

@ -1,65 +0,0 @@
require File.expand_path(File.dirname(__FILE__) + '/helpers/conversations_common')
describe "conversations user notes" do
include_examples "in-process server selenium tests"
before(:each) do
conversation_setup
@the_teacher = User.create(:name => "teacher bob")
@course.enroll_teacher(@the_teacher)
@the_student = User.create(:name => "student bob")
@course.enroll_student(@the_student)
end
it "should not allow user notes if not enabled" do
@course.account.update_attribute(:enable_user_notes, false)
new_conversation
add_recipient("student bob")
f(".user_note").should_not be_displayed
end
it "should not allow user notes to teachers" do
@course.account.update_attribute(:enable_user_notes, true)
new_conversation
add_recipient("teacher bob")
f(".user_note").should_not be_displayed
end
it "should not allow user notes on group conversations" do
@course.account.update_attribute(:enable_user_notes, true)
new_conversation
add_recipient("student bob")
add_recipient("teacher bob")
f(".user_note").should_not be_displayed
fj("#create_message_form input:visible").send_keys :backspace
f(".user_note").should be_displayed
end
it "should allow user notes on new private conversations with students" do
@course.account.update_attribute(:enable_user_notes, true)
new_conversation
add_recipient("student bob")
checkbox = f(".user_note")
checkbox.should be_displayed
checkbox.click
submit_message_form(:add_recipient => false)
@the_student.user_notes.size.should == 1
end
it "should allow user notes on existing private conversations with students" do
@course.account.update_attribute(:enable_user_notes, true)
new_conversation
add_recipient("student bob")
submit_message_form(:add_recipient => false)
expect_new_page_load { get "/conversations/sent" }
f(".conversations li").click
wait_for_ajaximations
checkbox = f(".user_note")
checkbox.should be_displayed
checkbox.click
submit_message_form(:existing_conversation => true)
@the_student.user_notes.size.should == 1
end
end