2013-02-05 14:16:51 -05:00
class TopicUser < ActiveRecord :: Base
belongs_to :user
belongs_to :topic
2013-02-07 16:45:24 +01:00
2013-05-24 09:13:31 -07:00
scope :starred_since , lambda { | sinceDaysAgo | where ( 'starred_at > ?' , sinceDaysAgo . days . ago ) }
scope :by_date_starred , group ( 'date(starred_at)' ) . order ( 'date(starred_at)' )
2013-05-29 18:11:04 +10:00
scope :tracking , lambda { | topic_id |
where ( topic_id : topic_id )
. where ( " COALESCE(topic_users.notification_level, :regular) >= :tracking " ,
regular : TopicUser . notification_levels [ :regular ] , tracking : TopicUser . notification_levels [ :tracking ] )
}
2013-03-06 15:17:07 -05:00
# Class methods
class << self
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
# Enums
def notification_levels
@notification_levels || = Enum . new ( :muted , :regular , :tracking , :watching , start : 0 )
end
2013-02-07 16:45:24 +01:00
2013-03-06 15:17:07 -05:00
def notification_reasons
@notification_reasons || = Enum . new ( :created_topic , :user_changed , :user_interacted , :created_post )
end
def auto_track ( user_id , topic_id , reason )
if TopicUser . where ( user_id : user_id , topic_id : topic_id , notifications_reason_id : nil ) . exists?
change ( user_id , topic_id ,
notification_level : notification_levels [ :tracking ] ,
2013-02-05 14:16:51 -05:00
notifications_reason_id : reason
2013-03-06 15:17:07 -05:00
)
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
MessageBus . publish ( " /topic/ #{ topic_id } " , {
notification_level_change : notification_levels [ :tracking ] ,
notifications_reason_id : reason
} , user_ids : [ user_id ] )
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
# Find the information specific to a user in a forum topic
def lookup_for ( user , topics )
# If the user isn't logged in, there's no last read posts
return { } if user . blank? || topics . blank?
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
topic_ids = topics . map ( & :id )
create_lookup ( TopicUser . where ( topic_id : topic_ids , user_id : user . id ) )
end
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
def create_lookup ( topic_users )
topic_users = topic_users . to_a
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
result = { }
return result if topic_users . blank?
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
topic_users . each do | ftu |
result [ ftu . topic_id ] = ftu
end
result
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
def get ( topic , user )
topic = topic . id if Topic === topic
user = user . id if User === user
TopicUser . where ( 'topic_id = ? and user_id = ?' , topic , user ) . first
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
# Change attributes for a user (creates a record when none is present). First it tries an update
# since there's more likely to be an existing record than not. If the update returns 0 rows affected
# it then creates the row instead.
def change ( user_id , topic_id , attrs )
# Sometimes people pass objs instead of the ids. We can handle that.
2013-06-28 12:18:04 +10:00
topic_id = topic_id . id if topic_id . is_a? ( :: Topic )
user_id = user_id . id if user_id . is_a? ( :: User )
topic_id = topic_id . to_i
user_id = user_id . to_i
2013-03-06 15:17:07 -05:00
TopicUser . transaction do
attrs = attrs . dup
attrs [ :starred_at ] = DateTime . now if attrs [ :starred_at ] . nil? && attrs [ :starred ]
if attrs [ :notification_level ]
attrs [ :notifications_changed_at ] || = DateTime . now
attrs [ :notifications_reason_id ] || = TopicUser . notification_reasons [ :user_changed ]
end
attrs_array = attrs . to_a
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
attrs_sql = attrs_array . map { | t | " #{ t [ 0 ] } = ? " } . join ( " , " )
vals = attrs_array . map { | t | t [ 1 ] }
2013-07-01 20:45:52 +02:00
rows = TopicUser . where ( topic_id : topic_id , user_id : user_id ) . update_all ( [ attrs_sql , * vals ] )
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
if rows == 0
now = DateTime . now
auto_track_after = User . select ( :auto_track_topics_after_msecs ) . where ( id : user_id ) . first . auto_track_topics_after_msecs
auto_track_after || = SiteSetting . auto_track_topics_after
2013-02-07 16:45:24 +01:00
2013-03-06 15:17:07 -05:00
if auto_track_after > = 0 && auto_track_after < = ( attrs [ :total_msecs_viewed ] || 0 )
attrs [ :notification_level ] || = notification_levels [ :tracking ]
end
2013-02-05 14:16:51 -05:00
2013-06-28 12:18:04 +10:00
TopicUser . create ( attrs . merge! ( user_id : user_id , topic_id : topic_id , first_visited_at : now , last_visited_at : now ) )
2013-05-21 20:43:43 -07:00
else
observe_after_save_callbacks_for topic_id , user_id
2013-02-05 14:16:51 -05:00
end
end
2013-03-06 15:17:07 -05:00
rescue ActiveRecord :: RecordNotUnique
# In case of a race condition to insert, do nothing
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
def track_visit! ( topic , user )
now = DateTime . now
2013-07-01 20:45:52 +02:00
rows = TopicUser . where ( { topic_id : topic . id , user_id : user . id } ) . update_all ( { last_visited_at : now } )
2013-03-06 15:17:07 -05:00
if rows == 0
TopicUser . create ( topic_id : topic . id , user_id : user . id , last_visited_at : now , first_visited_at : now )
2013-05-21 20:45:03 -07:00
else
observe_after_save_callbacks_for topic . id , user . id
2013-03-06 15:17:07 -05:00
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
# Update the last read and the last seen post count, but only if it doesn't exist.
# This would be a lot easier if psql supported some kind of upsert
def update_last_read ( user , topic_id , post_number , msecs )
return if post_number . blank?
msecs = 0 if msecs . to_i < 0
args = {
user_id : user . id ,
topic_id : topic_id ,
post_number : post_number ,
now : DateTime . now ,
msecs : msecs ,
tracking : notification_levels [ :tracking ] ,
threshold : SiteSetting . auto_track_topics_after
}
2013-04-08 11:12:52 +10:00
# In case anyone seens "seen_post_count" and gets confused, like I do.
# seen_post_count represents the highest_post_number of the topic when
# the user visited it. It may be out of alignement with last_read, meaning
# ... user visited the topic but did not read the posts
2013-03-06 15:17:07 -05:00
rows = exec_sql ( " UPDATE topic_users
SET
last_read_post_number = greatest ( :post_number , tu . last_read_post_number ) ,
seen_post_count = t . highest_post_number ,
total_msecs_viewed = tu . total_msecs_viewed + :msecs ,
notification_level =
case when tu . notifications_reason_id is null and ( tu . total_msecs_viewed + :msecs ) >
coalesce ( u . auto_track_topics_after_msecs , :threshold ) and
coalesce ( u . auto_track_topics_after_msecs , :threshold ) > = 0 then
:tracking
else
tu . notification_level
end
FROM topic_users tu
join topics t on t . id = tu . topic_id
join users u on u . id = :user_id
WHERE
tu . topic_id = topic_users . topic_id AND
tu . user_id = topic_users . user_id AND
tu . topic_id = :topic_id AND
tu . user_id = :user_id
RETURNING
2013-05-30 16:19:12 +10:00
topic_users . notification_level , tu . notification_level old_level , tu . last_read_post_number
2013-03-06 15:17:07 -05:00
" ,
args ) . values
if rows . length == 1
before = rows [ 0 ] [ 1 ] . to_i
after = rows [ 0 ] [ 0 ] . to_i
2013-05-30 16:19:12 +10:00
before_last_read = rows [ 0 ] [ 2 ] . to_i
if before_last_read < post_number
TopicTrackingState . publish_read ( topic_id , post_number , user . id )
end
2013-03-06 15:17:07 -05:00
if before != after
MessageBus . publish ( " /topic/ #{ topic_id } " , { notification_level_change : after } , user_ids : [ user . id ] )
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
if rows . length == 0
2013-05-30 16:19:12 +10:00
TopicTrackingState . publish_read ( topic_id , post_number , user . id )
2013-03-06 15:17:07 -05:00
args [ :tracking ] = notification_levels [ :tracking ]
args [ :regular ] = notification_levels [ :regular ]
args [ :site_setting ] = SiteSetting . auto_track_topics_after
exec_sql ( " INSERT INTO topic_users (user_id, topic_id, last_read_post_number, seen_post_count, last_visited_at, first_visited_at, notification_level)
SELECT :user_id , :topic_id , :post_number , ft . highest_post_number , :now , :now ,
case when coalesce ( u . auto_track_topics_after_msecs , :site_setting ) = 0 then :tracking else :regular end
FROM topics AS ft
JOIN users u on u . id = :user_id
WHERE ft . id = :topic_id
AND NOT EXISTS ( SELECT 1
FROM topic_users AS ftu
WHERE ftu . user_id = :user_id and ftu . topic_id = :topic_id ) " ,
args )
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
2013-05-21 20:43:43 -07:00
def observe_after_save_callbacks_for ( topic_id , user_id )
TopicUser . where ( topic_id : topic_id , user_id : user_id ) . each do | topic_user |
UserActionObserver . instance . after_save topic_user
end
end
2013-02-05 14:16:51 -05:00
end
2013-03-06 15:17:07 -05:00
2013-04-05 15:29:46 +11:00
def self . ensure_consistency!
exec_sql <<SQL
UPDATE topic_users t
SET
last_read_post_number = last_read ,
2013-04-08 13:01:58 +10:00
seen_post_count = LEAST ( max_post_number , GREATEST ( t . seen_post_count , last_read ) )
2013-04-05 15:29:46 +11:00
FROM (
2013-04-08 11:12:52 +10:00
SELECT topic_id , user_id , MAX ( post_number ) last_read
2013-04-05 15:29:46 +11:00
FROM post_timings
GROUP BY topic_id , user_id
) as X
2013-04-08 13:01:58 +10:00
JOIN (
SELECT p . topic_id , MAX ( p . post_number ) max_post_number from posts p
GROUP BY p . topic_id
) as Y on Y . topic_id = X . topic_id
2013-04-05 15:29:46 +11:00
WHERE X . topic_id = t . topic_id AND
X . user_id = t . user_id AND
(
2013-04-08 13:01:58 +10:00
last_read_post_number < > last_read OR
seen_post_count < > LEAST ( max_post_number , GREATEST ( t . seen_post_count , last_read ) )
2013-04-05 15:29:46 +11:00
)
SQL
end
2013-02-05 14:16:51 -05:00
end
2013-05-24 12:48:32 +10:00
# == Schema Information
#
# Table name: topic_users
#
# user_id :integer not null
# topic_id :integer not null
# starred :boolean default(FALSE), not null
# posted :boolean default(FALSE), not null
# last_read_post_number :integer
# seen_post_count :integer
# starred_at :datetime
# last_visited_at :datetime
# first_visited_at :datetime
# notification_level :integer default(1), not null
# notifications_changed_at :datetime
# notifications_reason_id :integer
# total_msecs_viewed :integer default(0), not null
# cleared_pinned_at :datetime
# unstarred_at :datetime
#
# Indexes
#
# index_forum_thread_users_on_forum_thread_id_and_user_id (topic_id,user_id) UNIQUE
#