301 lines
11 KiB
Ruby
301 lines
11 KiB
Ruby
#
|
|
# Copyright (C) 2011 Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
class ConversationMessage < ActiveRecord::Base
|
|
include ActionController::UrlWriter
|
|
include SendToStream
|
|
include SimpleTags::ReaderInstanceMethods
|
|
|
|
belongs_to :conversation
|
|
belongs_to :author, :class_name => 'User'
|
|
belongs_to :context, :polymorphic => true
|
|
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
|
|
delegate :participants, :to => :conversation
|
|
delegate :subscribed_participants, :to => :conversation
|
|
attr_accessible
|
|
|
|
named_scope :human, :conditions => "NOT generated"
|
|
named_scope :with_attachments, :conditions => "attachment_ids <> '' OR has_attachments" # TODO: simplify post-migration
|
|
named_scope :with_media_comments, :conditions => "media_comment_id IS NOT NULL OR has_media_objects" # TODO: simplify post-migration
|
|
named_scope :by_user, lambda { |user_or_id|
|
|
user_or_id = user_or_id.id if user_or_id.is_a?(User)
|
|
{:conditions => {:author_id => user_or_id}}
|
|
}
|
|
def self.preload_latest(conversation_participants, author_id=nil)
|
|
return unless conversation_participants.present?
|
|
base_conditions = sanitize_sql([
|
|
"conversation_id IN (?) AND conversation_participant_id in (?) AND NOT generated",
|
|
conversation_participants.map(&:conversation_id),
|
|
conversation_participants.map(&:id)
|
|
])
|
|
base_conditions << sanitize_sql([" AND author_id = ?", author_id]) if author_id
|
|
|
|
# limit it for non-postgres so we can reduce the amount of extra data we
|
|
# crunch in ruby (generally none, unless a conversation has multiple
|
|
# most-recent messages, i.e. same created_at)
|
|
unless connection.adapter_name == 'PostgreSQL'
|
|
base_conditions << <<-SQL
|
|
AND conversation_messages.created_at = (
|
|
SELECT MAX(created_at)
|
|
FROM conversation_messages cm2
|
|
JOIN conversation_message_participants cmp2 ON cm2.id = conversation_message_id
|
|
WHERE cm2.conversation_id = conversation_messages.conversation_id
|
|
AND #{base_conditions}
|
|
)
|
|
SQL
|
|
end
|
|
|
|
ret = distinct_on('conversation_participant_id',
|
|
:select => "conversation_messages.*, conversation_participant_id, conversation_message_participants.tags",
|
|
:joins => 'JOIN conversation_message_participants ON conversation_messages.id = conversation_message_id',
|
|
:conditions => base_conditions,
|
|
:order => 'conversation_participant_id, created_at DESC'
|
|
)
|
|
map = Hash[ret.map{ |m| [m.conversation_participant_id.to_i, m]}]
|
|
if author_id
|
|
conversation_participants.each{ |cp| cp.last_authored_message = map[cp.id] }
|
|
else
|
|
conversation_participants.each{ |cp| cp.last_message = map[cp.id] }
|
|
end
|
|
end
|
|
|
|
validates_length_of :body, :maximum => maximum_text_length
|
|
|
|
has_a_broadcast_policy
|
|
set_broadcast_policy do |p|
|
|
p.dispatch :conversation_message
|
|
p.to { self.recipients }
|
|
p.whenever {|record| (record.just_created || @re_send_message) && !record.generated && !record.submission}
|
|
|
|
p.dispatch :added_to_conversation
|
|
p.to { self.new_recipients }
|
|
p.whenever {|record| (record.just_created || @re_send_message) && record.generated && record.event_data[:event_type] == :users_added}
|
|
end
|
|
|
|
on_create_send_to_streams do
|
|
self.recipients unless submission # we still render them w/ the conversation in the stream item, we just don't cause it to jump to the top
|
|
end
|
|
|
|
before_save :infer_values
|
|
before_destroy :delete_from_participants
|
|
|
|
def infer_values
|
|
self.media_comment_id = nil if self.media_comment_id && self.media_comment_id.strip.empty?
|
|
if self.media_comment_id && self.media_comment_id_changed?
|
|
@media_comment = MediaObject.by_media_id(self.media_comment_id).first
|
|
self.media_comment_id = nil unless @media_comment
|
|
self.media_comment_type = @media_comment.media_type if @media_comment
|
|
end
|
|
self.media_comment_type = nil unless self.media_comment_id
|
|
self.has_attachments = attachment_ids.present? || forwarded_messages.any?(&:has_attachments?)
|
|
self.has_media_objects = media_comment_id.present? || forwarded_messages.any?(&:has_media_objects?)
|
|
true
|
|
end
|
|
|
|
# override AR association magic
|
|
def attachment_ids
|
|
read_attribute :attachment_ids
|
|
end
|
|
|
|
def attachment_ids=(ids)
|
|
self.attachments = author.conversation_attachments_folder.attachments.find_all_by_id(ids.map(&:to_i))
|
|
write_attribute(:attachment_ids, attachments.map(&:id).join(','))
|
|
end
|
|
|
|
def delete_from_participants
|
|
conversation.conversation_participants.each do |p|
|
|
p.remove_messages(self) # ensures cached stuff gets updated, etc.
|
|
end
|
|
end
|
|
|
|
# TODO: remove once data has been migrated
|
|
def has_attachments?
|
|
ret = read_attribute(:has_attachments)
|
|
return ret unless ret.nil?
|
|
attachment_ids.present? || forwarded_messages.any?(&:has_attachments?)
|
|
end
|
|
|
|
# TODO: remove once data has been migrated
|
|
def has_media_objects?
|
|
ret = read_attribute(:has_media_objects)
|
|
return ret unless ret.nil?
|
|
media_comment_id.present? || forwarded_messages.any?(&:has_media_objects?)
|
|
end
|
|
|
|
def media_comment
|
|
if !@media_comment && self.media_comment_id
|
|
@media_comment = MediaObject.by_media_id(self.media_comment_id).first
|
|
end
|
|
@media_comment
|
|
end
|
|
|
|
def media_comment=(media_comment)
|
|
self.media_comment_id = media_comment.media_id
|
|
self.media_comment_type = media_comment.media_type
|
|
@media_comment = media_comment
|
|
end
|
|
|
|
def recipients
|
|
self.subscribed_participants.reject{ |u| u.id == self.author_id }
|
|
end
|
|
|
|
def new_recipients
|
|
return [] unless generated? and event_data[:event_type] == :users_added
|
|
recipients.select{ |u| event_data[:user_ids].include?(u.id) }
|
|
end
|
|
|
|
# for developer use on console only
|
|
def resend_message!
|
|
@re_send_message = true
|
|
self.save!
|
|
@re_send_message = false
|
|
end
|
|
|
|
def body
|
|
if generated?
|
|
format_event_message
|
|
else
|
|
read_attribute(:body)
|
|
end
|
|
end
|
|
|
|
def event_data
|
|
return {} unless generated?
|
|
@event_data ||= YAML.load(read_attribute(:body))
|
|
end
|
|
|
|
def format_event_message
|
|
case event_data[:event_type]
|
|
when :users_added
|
|
user_names = User.find_all_by_id(event_data[:user_ids], :order => "id").map(&:short_name)
|
|
EventFormatter.users_added(author.short_name, user_names)
|
|
end
|
|
end
|
|
|
|
def generate_user_note
|
|
return unless recipients.size == 1
|
|
recipient = recipients.first
|
|
return unless recipient.grants_right?(author, :create_user_notes) && recipient.associated_accounts.any?{|a| a.enable_user_notes }
|
|
|
|
self.extend TextHelper
|
|
title = t(:subject, "Private message, %{timestamp}", :timestamp => date_string(created_at))
|
|
note = format_message(body).first
|
|
recipient.user_notes.create(:creator => author, :title => title, :note => note)
|
|
end
|
|
|
|
def formatted_body(truncate=nil)
|
|
self.extend TextHelper
|
|
res = format_message(body).first
|
|
res = truncate_html(res, :max_length => truncate, :words => true) if truncate
|
|
res
|
|
end
|
|
|
|
def root_account_id
|
|
context_id if context_type == 'Account'
|
|
end
|
|
|
|
def reply_from(opts)
|
|
conversation.reply_from(opts.merge(:root_account_id => self.root_account_id))
|
|
end
|
|
|
|
def forwarded_messages
|
|
@forwarded_messages ||= forwarded_message_ids && self.class.send(:with_exclusive_scope){ self.class.find_all_by_id(forwarded_message_ids.split(','), :order => 'created_at DESC')} || []
|
|
end
|
|
|
|
def all_forwarded_messages
|
|
forwarded_messages.inject([]) { |result, message|
|
|
result << message
|
|
result.concat(message.all_forwarded_messages)
|
|
}
|
|
end
|
|
|
|
def forwardable?
|
|
submission.nil?
|
|
end
|
|
|
|
def as_json(options = {})
|
|
super(:only => [:id, :created_at, :body, :generated, :author_id])['conversation_message'].merge({
|
|
'forwarded_messages' => forwarded_messages,
|
|
'attachments' => attachments,
|
|
'media_comment' => media_comment
|
|
})
|
|
end
|
|
|
|
def to_atom(opts={})
|
|
extend ApplicationHelper
|
|
extend ConversationsHelper
|
|
|
|
title = ERB::Util.h(truncate_text(self.body, :max_words => 8, :max_length => 80))
|
|
|
|
# build content, should be:
|
|
# message body
|
|
# [list of attachments]
|
|
# -----
|
|
# context
|
|
content = "<div>#{ERB::Util.h(self.body)}</div>"
|
|
if !self.attachments.empty?
|
|
content += "<ul>"
|
|
self.attachments.each do |attachment|
|
|
href = file_download_url(attachment, :verifier => attachment.uuid,
|
|
:download => '1',
|
|
:download_frd => '1',
|
|
:host => HostUrl.context_host(self.context))
|
|
content += "<li><a href='#{href}'>#{ERB::Util.h(attachment.display_name)}</a></li>"
|
|
end
|
|
content += "</ul>"
|
|
end
|
|
|
|
content += opts[:additional_content] if opts[:additional_content]
|
|
|
|
Atom::Entry.new do |entry|
|
|
entry.title = title
|
|
entry.authors << Atom::Person.new(:name => self.author.name)
|
|
entry.updated = self.created_at.utc
|
|
entry.published = self.created_at.utc
|
|
entry.id = "tag:#{HostUrl.context_host(self.context)},#{self.created_at.strftime("%Y-%m-%d")}:/conversations/#{self.feed_code}"
|
|
entry.links << Atom::Link.new(:rel => 'alternate',
|
|
:href => "http://#{HostUrl.context_host(self.context)}/conversations/#{self.conversation.id}")
|
|
self.attachments.each do |attachment|
|
|
entry.links << Atom::Link.new(:rel => 'enclosure',
|
|
:href => file_download_url(attachment, :verifier => attachment.uuid,
|
|
:download => '1',
|
|
:download_frd => '1',
|
|
:host => HostUrl.context_host(self.context)))
|
|
end
|
|
entry.content = Atom::Content::Html.new(content)
|
|
end
|
|
end
|
|
|
|
class EventFormatter
|
|
def self.users_added(author_name, user_names)
|
|
I18n.t 'conversation_message.users_added', {
|
|
:one => "%{user} was added to the conversation by %{current_user}",
|
|
:other => "%{list_of_users} were added to the conversation by %{current_user}"
|
|
},
|
|
:count => user_names.size,
|
|
:user => user_names.first,
|
|
:list_of_users => user_names.all?(&:html_safe?) ? user_names.to_sentence.html_safe : user_names.to_sentence,
|
|
:current_user => author_name
|
|
end
|
|
end
|
|
end
|
|
|