2011-02-01 09:57:29 +08:00
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
#
|
|
|
|
|
2011-02-10 04:48:42 +08:00
|
|
|
require 'open_object'
|
|
|
|
require 'set'
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
class StreamItem < ActiveRecord::Base
|
|
|
|
serialize :data
|
|
|
|
|
2012-11-17 06:00:05 +08:00
|
|
|
has_many :stream_item_instances
|
2011-02-01 09:57:29 +08:00
|
|
|
has_many :users, :through => :stream_item_instances
|
2012-11-17 06:00:05 +08:00
|
|
|
belongs_to :context, :polymorphic => true
|
|
|
|
belongs_to :asset, :polymorphic => true, :types => [
|
|
|
|
:collaboration, :conversation, :discussion_entry,
|
|
|
|
:discussion_topic, :message, :submission, :web_conference]
|
|
|
|
validates_presence_of :asset_type, :data
|
2011-05-14 00:49:23 +08:00
|
|
|
|
2012-11-17 06:00:05 +08:00
|
|
|
attr_accessible :context, :asset
|
|
|
|
after_destroy :destroy_stream_item_instances
|
|
|
|
|
|
|
|
def self.reconstitute_ar_object(type, data)
|
|
|
|
return nil unless data
|
2012-11-21 02:41:57 +08:00
|
|
|
data = data.instance_variable_get(:@table) if data.is_a?(OpenObject)
|
|
|
|
data = data.with_indifferent_access
|
2012-11-17 06:00:05 +08:00
|
|
|
type = data['type'] || type
|
|
|
|
res = type.constantize.new
|
|
|
|
|
|
|
|
case type
|
|
|
|
when 'DiscussionTopic', 'Announcement'
|
|
|
|
root_discussion_entries = data.delete(:root_discussion_entries)
|
|
|
|
root_discussion_entries = root_discussion_entries.map { |entry| reconstitute_ar_object('DiscussionEntry', entry) }
|
|
|
|
res.root_discussion_entries.target = root_discussion_entries
|
|
|
|
res.attachment = reconstitute_ar_object('Attachment', data.delete(:attachment))
|
|
|
|
when 'Submission'
|
|
|
|
data['body'] = nil
|
|
|
|
end
|
2012-11-29 06:53:00 +08:00
|
|
|
if data.has_key?('users')
|
|
|
|
users = data.delete('users')
|
2012-11-17 06:00:05 +08:00
|
|
|
users = users.map { |user| reconstitute_ar_object('User', user) }
|
2012-11-29 06:53:00 +08:00
|
|
|
res.users.target = users
|
|
|
|
end
|
|
|
|
if data.has_key?('participants')
|
|
|
|
users = data.delete('participants')
|
|
|
|
users = users.map { |user| reconstitute_ar_object('User', user) }
|
|
|
|
res.instance_variable_set(:@participants, users)
|
2012-11-17 06:00:05 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
res.instance_variable_set(:@attributes, data)
|
|
|
|
res.instance_variable_set(:@new_record, false) if data['id']
|
|
|
|
res
|
2011-07-31 08:05:37 +08:00
|
|
|
end
|
2012-11-17 06:00:05 +08:00
|
|
|
|
|
|
|
def data(viewing_user_id = nil)
|
|
|
|
# reconstitute AR objects
|
|
|
|
@ar_data ||= self.shard.activate do
|
|
|
|
self.class.reconstitute_ar_object(asset_type, read_attribute(:data))
|
|
|
|
end
|
|
|
|
res = @ar_data
|
|
|
|
|
|
|
|
if viewing_user_id
|
|
|
|
res.user_id ||= viewing_user_id if asset_type != 'DiscussionTopic' && res.respond_to?(:user_id)
|
|
|
|
post_process(res, viewing_user_id)
|
|
|
|
else
|
|
|
|
res
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-07-31 08:05:37 +08:00
|
|
|
def prepare_user(user)
|
|
|
|
res = user.attributes.slice('id', 'name', 'short_name')
|
|
|
|
res['short_name'] ||= res['name']
|
|
|
|
res
|
|
|
|
end
|
|
|
|
|
|
|
|
def prepare_conversation(conversation)
|
|
|
|
res = conversation.attributes.slice('id', 'has_attachments')
|
|
|
|
res['private'] = conversation.private?
|
2012-11-29 06:53:00 +08:00
|
|
|
res['participant_count'] = conversation.conversation_participants.size
|
2011-07-31 08:05:37 +08:00
|
|
|
# arbitrary limit. would be nice to say "John, Jane, Michael, and 6
|
|
|
|
# others." if there's too many recipients, where those listed are the N
|
|
|
|
# most active posters in the conversation, but we'll just leave it at "9
|
|
|
|
# Participants" for now when the count is > 8.
|
|
|
|
if res['participant_count'] <= 8
|
|
|
|
res['participants'] = conversation.participants.map{ |u| prepare_user(u) }
|
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
res
|
|
|
|
end
|
2012-11-17 06:00:05 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def regenerate!(obj=nil)
|
|
|
|
obj ||= asset
|
2012-11-17 06:00:05 +08:00
|
|
|
return nil if self.asset_type == 'Message' && self.asset_id.nil?
|
2011-02-01 09:57:29 +08:00
|
|
|
if !obj || (obj.respond_to?(:workflow_state) && obj.workflow_state == 'deleted')
|
|
|
|
self.destroy
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
res = generate_data(obj)
|
|
|
|
self.save
|
|
|
|
res
|
|
|
|
end
|
2012-11-17 06:00:05 +08:00
|
|
|
|
|
|
|
def self.delete_all_for(root_asset, asset)
|
2013-03-19 03:07:47 +08:00
|
|
|
item = StreamItem.where(:asset_type => root_asset.first, :asset_id => root_asset.last).first
|
2011-02-01 09:57:29 +08:00
|
|
|
# if this is a sub-message, regenerate instead of deleting
|
2012-11-17 06:00:05 +08:00
|
|
|
if root_asset != asset
|
|
|
|
item.try(:regenerate!)
|
2011-02-01 09:57:29 +08:00
|
|
|
return
|
|
|
|
end
|
|
|
|
# Can't use delete_all here, since we need the destroy to fire and delete
|
|
|
|
# the StreamItemInstances as well.
|
2012-11-17 06:00:05 +08:00
|
|
|
item.try(:destroy)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2013-01-04 01:03:40 +08:00
|
|
|
ROOT_DISCUSSION_ENTRY_LIMIT = 3
|
2011-02-01 09:57:29 +08:00
|
|
|
def generate_data(object)
|
2012-11-17 06:00:05 +08:00
|
|
|
self.context ||= object.context rescue nil
|
2011-02-01 09:57:29 +08:00
|
|
|
|
|
|
|
case object
|
|
|
|
when DiscussionTopic
|
|
|
|
res = object.attributes
|
2011-11-04 05:55:38 +08:00
|
|
|
res['user_ids_that_can_see_responses'] = object.user_ids_who_have_posted_and_admins if object.require_initial_post?
|
2011-02-01 09:57:29 +08:00
|
|
|
res['total_root_discussion_entries'] = object.root_discussion_entries.active.count
|
2013-01-04 01:03:40 +08:00
|
|
|
res[:root_discussion_entries] = object.root_discussion_entries.active.reverse[0,ROOT_DISCUSSION_ENTRY_LIMIT].reverse.map do |entry|
|
2011-02-01 09:57:29 +08:00
|
|
|
hash = entry.attributes
|
|
|
|
hash['user_short_name'] = entry.user.short_name if entry.user
|
2011-08-23 03:00:57 +08:00
|
|
|
hash['message'] = hash['message'][0, 4.kilobytes] if hash['message'].present?
|
2011-02-01 09:57:29 +08:00
|
|
|
hash
|
|
|
|
end
|
|
|
|
if object.attachment
|
|
|
|
hash = object.attachment.attributes.slice('id', 'display_name')
|
|
|
|
hash['scribdable?'] = object.attachment.scribdable?
|
|
|
|
res[:attachment] = hash
|
|
|
|
end
|
2011-07-31 08:05:37 +08:00
|
|
|
when Conversation
|
|
|
|
res = prepare_conversation(object)
|
2011-02-01 09:57:29 +08:00
|
|
|
when Message
|
|
|
|
res = object.attributes
|
2012-01-07 07:58:18 +08:00
|
|
|
res['notification_category'] = object.notification_display_category
|
2012-05-18 02:50:13 +08:00
|
|
|
if !object.context.is_a?(Context) && object.context.respond_to?(:context) && object.context.context.is_a?(Context)
|
2012-11-17 06:00:05 +08:00
|
|
|
self.context = object.context.context
|
2012-05-18 02:50:13 +08:00
|
|
|
elsif object.asset_context_type
|
2012-11-17 06:00:05 +08:00
|
|
|
self.context_type, self.context_id = object.asset_context_type, object.asset_context_id
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
when Submission
|
|
|
|
res = object.attributes
|
2011-04-17 06:21:52 +08:00
|
|
|
res.delete 'body' # this can be pretty large, and we don't display it
|
2012-02-24 08:13:59 +08:00
|
|
|
res['assignment'] = object.assignment.attributes.slice('id', 'title', 'due_at', 'points_possible', 'submission_types', 'group_category_id')
|
2012-06-21 06:29:19 +08:00
|
|
|
res[:course_id] = object.context.id
|
2011-02-01 09:57:29 +08:00
|
|
|
when Collaboration
|
|
|
|
res = object.attributes
|
2011-07-31 08:05:37 +08:00
|
|
|
res['users'] = object.users.map{|u| prepare_user(u)}
|
2011-02-01 09:57:29 +08:00
|
|
|
when WebConference
|
|
|
|
res = object.attributes
|
2011-07-31 08:05:37 +08:00
|
|
|
res['users'] = object.users.map{|u| prepare_user(u)}
|
2012-06-01 22:13:37 +08:00
|
|
|
when CollectionItem
|
|
|
|
res = object.attributes
|
2011-02-01 09:57:29 +08:00
|
|
|
else
|
|
|
|
raise "Unexpected stream item type: #{object.class.to_s}"
|
|
|
|
end
|
2012-11-17 06:00:05 +08:00
|
|
|
if self.context_type
|
|
|
|
res['context_short_name'] = Rails.cache.fetch(['short_name_lookup', self.context_type, self.context_id].cache_key) do
|
|
|
|
self.context.short_name rescue ''
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
res['type'] = object.class.to_s
|
|
|
|
res['user_short_name'] = object.user.short_name rescue nil
|
|
|
|
|
2012-11-17 06:00:05 +08:00
|
|
|
if self.class.new_message?(object)
|
|
|
|
self.asset_type = 'Message'
|
|
|
|
self.asset_id = nil
|
|
|
|
else
|
|
|
|
self.asset = object
|
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
self.data = res
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.generate_or_update(object)
|
|
|
|
item = nil
|
2012-11-17 06:00:05 +08:00
|
|
|
StreamItem.unique_constraint_retry do
|
|
|
|
# we can't coalesce messages that weren't ever saved to the DB
|
|
|
|
if !new_message?(object)
|
|
|
|
item = object.stream_item
|
|
|
|
end
|
|
|
|
if item
|
|
|
|
item.regenerate!(object)
|
|
|
|
else
|
|
|
|
item = self.new
|
|
|
|
item.generate_data(object)
|
|
|
|
item.save!
|
|
|
|
# prepopulate the reverse association
|
|
|
|
# (mostly useful for specs that regenerate stream items
|
|
|
|
# multiple times without reloading the asset)
|
|
|
|
if !new_message?(object)
|
|
|
|
object.stream_item = item
|
|
|
|
end
|
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
item
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.generate_all(object, user_ids)
|
|
|
|
user_ids ||= []
|
|
|
|
user_ids.uniq!
|
|
|
|
return [] if user_ids.empty?
|
|
|
|
|
|
|
|
# Make the StreamItem
|
2012-11-17 06:00:05 +08:00
|
|
|
object = root_object(object)
|
2011-02-01 09:57:29 +08:00
|
|
|
res = StreamItem.generate_or_update(object)
|
new dashboard design
the new dashboard design categorizes recent activity into buckets that can be
expanded/collapsed, and inidividual messages can be dismissed. the categories
are announcements, conversations, discussions and assignments. this redesign
applies to the homepage dashboard, the group home page, and the course homepage
when "recent activity dashboard" is selected as the course home page type.o
the motiviation is that the dashboard should capture and present in one place
important information happening in all the user's courses or groups, and allow
for jumping into this information to see more details:
- announcements/discussions should show on the dashboard when they are created,
or when there are root replies to recent announcements
- conversations should show on the dashboard when there is new activity
- assignments should show on the dashboard when they are created, or when
changes are made at least a couple hours after being created
the presence of a dashboard item means there is activity for that item that may
be of interest to the user. additionally, the dashboard items will show
read/unread state (excluding assignments) for items which the user has not yet
viewed.
additionally, global messages such as course inivitations, account level
announcements, and new user messages have been restyled, but will keep their
place above the recent activity widget on the dashboard.
test plan:
- visit many exising user's dashboards and make sure they are functional in the
new style.
- visit canvas as a brand new user (no enrollments), a new user enrolled in
a new course and make sure the dashboard is restyled and the messaging makes
sense.
- make an account level announcement and make sure it shows up on user's
dashboards.
- create all different types of conversations: single, group, bulk private,
from submission comment, add user to convo, etc. and make sure the
appropriate dashboard items appear and make sense
- create discussions and announcements, reply to them at the root level and at
the sub entry level (sub entries will not make new dashboard items), test
from both a read and unread user's perspective, making sure dashboard items are
correct. (note that read/unread state will not be correct for existing items
before this code is applied, but should be correct for future items moving
forward)
- dismiss dashboard items and account announcements, make sure they stay
dismissed.
- test creating assignments, waiting > 2 hours, and updating due dates or other
assignment details. make sure items appear. note that unread state will not
exist for assignment notifications.
closes #10783
refs #11038
refs #11039
Change-Id: I276a8cb1fae4c8a46425d0a368455e15a0c470c5
Reviewed-on: https://gerrit.instructure.com/14540
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
2012-10-05 05:49:54 +08:00
|
|
|
prepare_object_for_unread(object)
|
2011-02-01 09:57:29 +08:00
|
|
|
|
2011-09-15 06:41:56 +08:00
|
|
|
# set the hidden flag if an assignment and muted
|
|
|
|
hidden = object.is_a?(Submission) && object.assignment.muted? ? true : false
|
|
|
|
|
2012-12-06 07:04:46 +08:00
|
|
|
|
|
|
|
l_context_type = res.context_type
|
2012-06-02 05:06:29 +08:00
|
|
|
Shard.partition_by_shard(user_ids) do |user_ids_subset|
|
2012-12-06 07:04:46 +08:00
|
|
|
#these need to be determined per shard
|
|
|
|
#hence the local caching inside the partition block
|
|
|
|
l_context_id = res.context_id
|
|
|
|
stream_item_id = res.id
|
|
|
|
|
|
|
|
#find out what the current largest stream item instance is so that we can delete them all once the new ones are created
|
2013-03-19 03:07:47 +08:00
|
|
|
greatest_existing_id = StreamItemInstance.where(:stream_item_id => stream_item_id, :user_id => user_ids_subset).maximum(:id) || 0
|
2012-12-06 07:04:46 +08:00
|
|
|
|
|
|
|
inserts = user_ids_subset.map do |user_id|
|
|
|
|
{
|
|
|
|
:stream_item_id => stream_item_id,
|
|
|
|
:user_id => user_id,
|
|
|
|
:hidden => hidden,
|
|
|
|
:workflow_state => object_unread_for_user(object, user_id),
|
|
|
|
:context_type => l_context_type,
|
|
|
|
:context_id => l_context_id,
|
|
|
|
}
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2012-12-06 07:04:46 +08:00
|
|
|
|
|
|
|
StreamItemInstance.connection.bulk_insert('stream_item_instances', inserts)
|
|
|
|
|
|
|
|
#reset caches manually because the observer wont trigger off of the above mass inserts
|
|
|
|
user_ids_subset.each do |user_id|
|
|
|
|
StreamItemCache.invalidate_recent_stream_items(user_id, l_context_type, l_context_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Then delete any old instances from these users' streams.
|
|
|
|
# This won't actually delete StreamItems out of the table, it just deletes
|
|
|
|
# the join table entries.
|
|
|
|
# Old stream items are deleted in a periodic job.
|
2013-03-19 03:07:47 +08:00
|
|
|
StreamItemInstance.where("user_id in (?) AND stream_item_id = ? AND id <= ?",
|
|
|
|
user_ids_subset, stream_item_id, greatest_existing_id).delete_all
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Here is where we used to go through and update the stream item for anybody
|
|
|
|
# not in user_ids who had the item in their stream, so that the item would
|
|
|
|
# be up-to-date, but not jump to the top of their stream. Now that
|
|
|
|
# we're updating StreamItems in-place and just linking to them through
|
|
|
|
# StreamItemInstances, this happens automatically.
|
|
|
|
# If a teacher leaves a comment for a student, for example
|
|
|
|
# we don't want that to jump to the top of the *teacher's* stream, but
|
|
|
|
# if it's still visible on the teacher's stream then it had better show
|
|
|
|
# the teacher's comment even if it is farther down.
|
|
|
|
|
|
|
|
# touch all the users to invalidate the cache
|
2013-03-19 03:07:47 +08:00
|
|
|
if Rails.version < '3.0'
|
|
|
|
User.update_all({:updated_at => Time.now.utc}, {:id => user_ids})
|
|
|
|
else
|
|
|
|
User.where(:id => user_ids).update_all(:updated_at => Time.now.utc)
|
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
|
|
|
|
return [res]
|
|
|
|
end
|
|
|
|
|
2012-11-17 06:00:05 +08:00
|
|
|
def self.root_object(object)
|
new dashboard design
the new dashboard design categorizes recent activity into buckets that can be
expanded/collapsed, and inidividual messages can be dismissed. the categories
are announcements, conversations, discussions and assignments. this redesign
applies to the homepage dashboard, the group home page, and the course homepage
when "recent activity dashboard" is selected as the course home page type.o
the motiviation is that the dashboard should capture and present in one place
important information happening in all the user's courses or groups, and allow
for jumping into this information to see more details:
- announcements/discussions should show on the dashboard when they are created,
or when there are root replies to recent announcements
- conversations should show on the dashboard when there is new activity
- assignments should show on the dashboard when they are created, or when
changes are made at least a couple hours after being created
the presence of a dashboard item means there is activity for that item that may
be of interest to the user. additionally, the dashboard items will show
read/unread state (excluding assignments) for items which the user has not yet
viewed.
additionally, global messages such as course inivitations, account level
announcements, and new user messages have been restyled, but will keep their
place above the recent activity widget on the dashboard.
test plan:
- visit many exising user's dashboards and make sure they are functional in the
new style.
- visit canvas as a brand new user (no enrollments), a new user enrolled in
a new course and make sure the dashboard is restyled and the messaging makes
sense.
- make an account level announcement and make sure it shows up on user's
dashboards.
- create all different types of conversations: single, group, bulk private,
from submission comment, add user to convo, etc. and make sure the
appropriate dashboard items appear and make sense
- create discussions and announcements, reply to them at the root level and at
the sub entry level (sub entries will not make new dashboard items), test
from both a read and unread user's perspective, making sure dashboard items are
correct. (note that read/unread state will not be correct for existing items
before this code is applied, but should be correct for future items moving
forward)
- dismiss dashboard items and account announcements, make sure they stay
dismissed.
- test creating assignments, waiting > 2 hours, and updating due dates or other
assignment details. make sure items appear. note that unread state will not
exist for assignment notifications.
closes #10783
refs #11038
refs #11039
Change-Id: I276a8cb1fae4c8a46425d0a368455e15a0c470c5
Reviewed-on: https://gerrit.instructure.com/14540
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
2012-10-05 05:49:54 +08:00
|
|
|
case object
|
|
|
|
when DiscussionEntry
|
|
|
|
object.discussion_topic
|
|
|
|
when SubmissionComment
|
|
|
|
object.submission
|
|
|
|
when ConversationMessage
|
|
|
|
object.conversation
|
|
|
|
else
|
|
|
|
object
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.prepare_object_for_unread(object)
|
|
|
|
case object
|
|
|
|
when DiscussionTopic
|
|
|
|
DiscussionTopic.send(:preload_associations, object, :discussion_topic_participants)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.object_unread_for_user(object, user_id)
|
|
|
|
case object
|
|
|
|
when DiscussionTopic
|
|
|
|
object.read_state(user_id)
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.update_read_state_for_asset(asset, new_state, user_id)
|
2012-11-20 04:22:16 +08:00
|
|
|
if item = asset.stream_item
|
2012-11-03 05:41:33 +08:00
|
|
|
StreamItemInstance.find_by_user_id_and_stream_item_id(user_id, item.id).try(:update_attribute, :workflow_state, new_state)
|
new dashboard design
the new dashboard design categorizes recent activity into buckets that can be
expanded/collapsed, and inidividual messages can be dismissed. the categories
are announcements, conversations, discussions and assignments. this redesign
applies to the homepage dashboard, the group home page, and the course homepage
when "recent activity dashboard" is selected as the course home page type.o
the motiviation is that the dashboard should capture and present in one place
important information happening in all the user's courses or groups, and allow
for jumping into this information to see more details:
- announcements/discussions should show on the dashboard when they are created,
or when there are root replies to recent announcements
- conversations should show on the dashboard when there is new activity
- assignments should show on the dashboard when they are created, or when
changes are made at least a couple hours after being created
the presence of a dashboard item means there is activity for that item that may
be of interest to the user. additionally, the dashboard items will show
read/unread state (excluding assignments) for items which the user has not yet
viewed.
additionally, global messages such as course inivitations, account level
announcements, and new user messages have been restyled, but will keep their
place above the recent activity widget on the dashboard.
test plan:
- visit many exising user's dashboards and make sure they are functional in the
new style.
- visit canvas as a brand new user (no enrollments), a new user enrolled in
a new course and make sure the dashboard is restyled and the messaging makes
sense.
- make an account level announcement and make sure it shows up on user's
dashboards.
- create all different types of conversations: single, group, bulk private,
from submission comment, add user to convo, etc. and make sure the
appropriate dashboard items appear and make sense
- create discussions and announcements, reply to them at the root level and at
the sub entry level (sub entries will not make new dashboard items), test
from both a read and unread user's perspective, making sure dashboard items are
correct. (note that read/unread state will not be correct for existing items
before this code is applied, but should be correct for future items moving
forward)
- dismiss dashboard items and account announcements, make sure they stay
dismissed.
- test creating assignments, waiting > 2 hours, and updating due dates or other
assignment details. make sure items appear. note that unread state will not
exist for assignment notifications.
closes #10783
refs #11038
refs #11039
Change-Id: I276a8cb1fae4c8a46425d0a368455e15a0c470c5
Reviewed-on: https://gerrit.instructure.com/14540
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
2012-10-05 05:49:54 +08:00
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2012-05-11 03:59:21 +08:00
|
|
|
# call destroy_stream_items using a before_date based on the global setting
|
|
|
|
def self.destroy_stream_items_using_setting
|
2012-10-04 04:09:08 +08:00
|
|
|
ttl = Setting.get('stream_items_ttl', 4.weeks).to_i.ago
|
2012-05-11 03:59:21 +08:00
|
|
|
# we pass false for the touch_users argument, on the assumption that these
|
|
|
|
# stream items that we delete aren't visible on the user's dashboard anymore
|
|
|
|
# anyway, so there's no need to invalidate all the caches.
|
|
|
|
destroy_stream_items(ttl, false)
|
|
|
|
end
|
|
|
|
|
2011-02-10 04:48:42 +08:00
|
|
|
# delete old stream items and the corresponding instances before a given date
|
|
|
|
# returns the number of destroyed stream items
|
|
|
|
def self.destroy_stream_items(before_date, touch_users = true)
|
|
|
|
user_ids = Set.new
|
|
|
|
count = 0
|
|
|
|
|
2012-11-17 06:00:05 +08:00
|
|
|
query = { :conditions => ['updated_at < ?', before_date], :include => [:context] }
|
2011-02-10 04:48:42 +08:00
|
|
|
if touch_users
|
2012-11-17 06:00:05 +08:00
|
|
|
query[:include] << 'stream_item_instances'
|
2011-02-10 04:48:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
self.find_each(query) do |item|
|
|
|
|
count += 1
|
|
|
|
if touch_users
|
|
|
|
user_ids.add(item.stream_item_instances.map { |i| i.user_id })
|
|
|
|
end
|
|
|
|
# this will destroy the associated stream_item_instances as well
|
|
|
|
item.destroy
|
|
|
|
end
|
|
|
|
|
|
|
|
unless user_ids.empty?
|
|
|
|
# touch all the users to invalidate the cache
|
2013-03-19 03:07:47 +08:00
|
|
|
if Rails.version < '3.0'
|
|
|
|
User.update_all({:updated_at => Time.now.utc}, {:id => user_ids.to_a})
|
|
|
|
else
|
|
|
|
User.where(:id => user_ids.to_a).update_all(:updated_at => Time.now.utc)
|
|
|
|
end
|
2011-02-10 04:48:42 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
count
|
|
|
|
end
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
named_scope :before, lambda {|id|
|
|
|
|
{:conditions => ['id < ?', id], :order => 'updated_at DESC', :limit => 21 }
|
|
|
|
}
|
|
|
|
named_scope :after, lambda {|start_at|
|
|
|
|
{:conditions => ['updated_at > ?', start_at], :order => 'updated_at DESC', :limit => 21 }
|
|
|
|
}
|
2012-11-17 06:00:05 +08:00
|
|
|
|
|
|
|
def associated_shards
|
|
|
|
if self.context.try(:respond_to?, :associated_shards)
|
|
|
|
self.context.associated_shards
|
|
|
|
elsif self.data.respond_to?(:associated_shards)
|
|
|
|
self.data.associated_shards
|
|
|
|
else
|
|
|
|
[self.shard]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-04 05:55:38 +08:00
|
|
|
private
|
2012-11-17 06:00:05 +08:00
|
|
|
|
|
|
|
def self.new_message?(object)
|
|
|
|
object.is_a?(Message) && object.new_record?
|
|
|
|
end
|
|
|
|
|
2013-01-10 07:42:56 +08:00
|
|
|
# Internal: Format the stream item's asset to avoid showing hidden data.
|
|
|
|
#
|
|
|
|
# res - The stream item asset.
|
|
|
|
# viewing_user_id - The ID of the user to prepare the stream item for.
|
|
|
|
#
|
|
|
|
# Returns the stream item asset minus any hidden data.
|
2011-11-04 05:55:38 +08:00
|
|
|
def post_process(res, viewing_user_id)
|
2012-11-17 06:00:05 +08:00
|
|
|
case res
|
|
|
|
when DiscussionTopic, Announcement
|
2011-11-04 05:55:38 +08:00
|
|
|
if res.require_initial_post
|
2012-11-17 06:00:05 +08:00
|
|
|
res.write_attribute(:user_has_posted, true)
|
|
|
|
if res.user_ids_that_can_see_responses && !res.user_ids_that_can_see_responses.member?(viewing_user_id)
|
2013-01-10 07:42:56 +08:00
|
|
|
original_res = res
|
|
|
|
res = original_res.clone
|
|
|
|
res.id = original_res.id
|
2011-11-04 05:55:38 +08:00
|
|
|
res.root_discussion_entries = []
|
|
|
|
res.user_has_posted = false
|
2013-01-10 07:42:56 +08:00
|
|
|
res.readonly!
|
2011-11-04 05:55:38 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2013-01-10 07:42:56 +08:00
|
|
|
|
2011-11-04 05:55:38 +08:00
|
|
|
res
|
|
|
|
end
|
2012-11-17 06:00:05 +08:00
|
|
|
|
|
|
|
def destroy_stream_item_instances
|
2013-03-19 03:07:47 +08:00
|
|
|
# bare scoped call avoid Rails 2 HasManyAssociation loading all objects
|
|
|
|
self.stream_item_instances.with_each_shard { |scope| scope.scoped.delete_all; nil }
|
2012-11-17 06:00:05 +08:00
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|