Cleaned up TopicUserSpec, introduces clearing of pinned topics

This commit is contained in:
Robin Ward 2013-03-06 15:17:07 -05:00
parent 3af2ab9022
commit f8d8272406
27 changed files with 2663 additions and 342 deletions

View File

@ -1,4 +1,4 @@
source 'https://rubygems.org' source 'http://rubygems.org'
gem 'active_model_serializers', git: 'git://github.com/rails-api/active_model_serializers.git' gem 'active_model_serializers', git: 'git://github.com/rails-api/active_model_serializers.git'
gem 'ember-rails', git: 'git://github.com/emberjs/ember-rails.git' # so we get the pre version gem 'ember-rails', git: 'git://github.com/emberjs/ember-rails.git' # so we get the pre version

View File

@ -71,7 +71,7 @@ PATH
rails (~> 3.1) rails (~> 3.1)
GEM GEM
remote: https://rubygems.org/ remote: http://rubygems.org/
specs: specs:
actionmailer (3.2.12) actionmailer (3.2.12)
actionpack (= 3.2.12) actionpack (= 3.2.12)

View File

@ -255,6 +255,15 @@ Discourse.TopicController = Discourse.ObjectController.extend({
this.get('content').toggleStar(); this.get('content').toggleStar();
}, },
/**
Clears the pin from a topic for the currentUser
@method clearPin
**/
clearPin: function() {
this.get('content').clearPin();
},
// Receive notifications for this topic // Receive notifications for this topic
subscribe: function() { subscribe: function() {
var bus, var bus,

View File

@ -329,6 +329,27 @@ Discourse.Topic = Discourse.Model.extend({
}); });
}, },
/**
Clears the pin from a topic for the currentUser
@method clearPin
**/
clearPin: function() {
var topic = this;
// Clear the pin optimistically from the object
topic.set('pinned', false);
$.ajax("/t/" + this.get('id') + "/clear-pin", {
type: 'PUT',
error: function() {
// On error, put the pin back
topic.set('pinned', true);
}
});
},
// Is the reply to a post directly below it? // Is the reply to a post directly below it?
isReplyDirectlyBelow: function(post) { isReplyDirectlyBelow: function(post) {
var postBelow, posts; var postBelow, posts;

View File

@ -68,6 +68,30 @@ Discourse.TopicFooterButtonsView = Ember.ContainerView.extend({
buffer.push("<i class='icon icon-share'></i>"); buffer.push("<i class='icon icon-share'></i>");
} }
})); }));
// Add our clear pin button
this.addObject(Discourse.ButtonView.createWithMixins({
textKey: 'topic.clear_pin.title',
helpKey: 'topic.clear_pin.help',
classNameBindings: ['unpinned'],
// Hide the button if it becomes unpinned
unpinned: function() {
// When not logged in don't show the button
if (!Discourse.get('currentUser')) return 'hidden'
return this.get('controller.pinned') ? null : 'hidden';
}.property('controller.pinned'),
click: function(buffer) {
this.get('controller').clearPin();
},
renderIcon: function(buffer) {
buffer.push("<i class='icon icon-pushpin'></i>");
}
}));
} }
this.addObject(Discourse.ButtonView.createWithMixins({ this.addObject(Discourse.ButtonView.createWithMixins({

View File

@ -14,7 +14,9 @@ class TopicsController < ApplicationController
:mute, :mute,
:unmute, :unmute,
:set_notifications, :set_notifications,
:move_posts] :move_posts,
:clear_pin]
before_filter :consider_user_for_promotion, only: :show before_filter :consider_user_for_promotion, only: :show
skip_before_filter :check_xhr, only: [:avatar, :show, :feed] skip_before_filter :check_xhr, only: [:avatar, :show, :feed]
@ -127,16 +129,21 @@ class TopicsController < ApplicationController
end end
end end
def clear_pin
topic = Topic.where(id: params[:topic_id].to_i).first
guardian.ensure_can_see!(topic)
topic.clear_pin_for(current_user)
render nothing: true
end
def timings def timings
PostTiming.process_timings( PostTiming.process_timings(
current_user, current_user,
params[:topic_id].to_i, params[:topic_id].to_i,
params[:highest_seen].to_i, params[:highest_seen].to_i,
params[:topic_time].to_i, params[:topic_time].to_i,
(params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]} (params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]}
) )
render nothing: true render nothing: true
end end

View File

@ -54,7 +54,7 @@ class Post < ActiveRecord::Base
after_commit :store_unique_post_key, on: :create after_commit :store_unique_post_key, on: :create
after_create do after_create do
TopicUser.auto_track(user_id, topic_id, TopicUser::NotificationReasons::CREATED_POST) TopicUser.auto_track(user_id, topic_id, TopicUser.notification_reasons[:created_post])
end end
scope :by_newest, order('created_at desc, id desc') scope :by_newest, order('created_at desc, id desc')

View File

@ -80,7 +80,7 @@ class PostAlertObserver < ActiveRecord::Observer
return unless Guardian.new(user).can_see?(post) return unless Guardian.new(user).can_see?(post)
# skip if muted on the topic # skip if muted on the topic
return if TopicUser.get(post.topic, user).try(:notification_level) == TopicUser::NotificationLevel::MUTED return if TopicUser.get(post.topic, user).try(:notification_level) == TopicUser.notification_levels[:muted]
# Don't notify the same user about the same notification on the same post # Don't notify the same user about the same notification on the same post
return if user.notifications.exists?(notification_type: type, topic_id: post.topic_id, post_number: post.post_number) return if user.notifications.exists?(notification_type: type, topic_id: post.topic_id, post_number: post.post_number)
@ -132,7 +132,7 @@ class PostAlertObserver < ActiveRecord::Observer
exclude_user_ids << extract_mentioned_users(post).map(&:id) exclude_user_ids << extract_mentioned_users(post).map(&:id)
exclude_user_ids << extract_quoted_users(post).map(&:id) exclude_user_ids << extract_quoted_users(post).map(&:id)
exclude_user_ids.flatten! exclude_user_ids.flatten!
TopicUser.where(topic_id: post.topic_id, notification_level: TopicUser::NotificationLevel::WATCHING).includes(:user).each do |tu| TopicUser.where(topic_id: post.topic_id, notification_level: TopicUser.notification_levels[:watching]).includes(:user).each do |tu|
create_notification(tu.user, Notification.types[:posted], post) unless exclude_user_ids.include?(tu.user_id) create_notification(tu.user, Notification.types[:posted], post) unless exclude_user_ids.include?(tu.user_id)
end end
end end

View File

@ -83,11 +83,9 @@ class Topic < ActiveRecord::Base
after_create do after_create do
changed_to_category(category) changed_to_category(category)
TopicUser.change( TopicUser.change(user_id, id,
user_id, id, notification_level: TopicUser.notification_levels[:watching],
notification_level: TopicUser::NotificationLevel::WATCHING, notifications_reason_id: TopicUser.notification_reasons[:created_topic])
notifications_reason_id: TopicUser::NotificationReasons::CREATED_TOPIC
)
if archetype == Archetype.private_message if archetype == Archetype.private_message
DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE) DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE)
else else
@ -206,8 +204,17 @@ class Topic < ActiveRecord::Base
end end
def update_status(property, status, user) def update_status(property, status, user)
Topic.transaction do Topic.transaction do
update_column(property, status)
# Special case: if it's pinned, update that
if property.to_sym == :pinned
update_pinned(status)
else
# otherwise update the column
update_column(property, status)
end
key = "topic_statuses.#{property}_" key = "topic_statuses.#{property}_"
key << (status ? 'enabled' : 'disabled') key << (status ? 'enabled' : 'disabled')
@ -506,7 +513,7 @@ class Topic < ActiveRecord::Base
# Enable/disable the mute on the topic # Enable/disable the mute on the topic
def toggle_mute(user, muted) def toggle_mute(user, muted)
TopicUser.change(user, self.id, notification_level: muted?(user) ? TopicUser::NotificationLevel::REGULAR : TopicUser::NotificationLevel::MUTED ) TopicUser.change(user, self.id, notification_level: muted?(user) ? TopicUser.notification_levels[:regular] : TopicUser.notification_levels[:muted] )
end end
def slug def slug
@ -526,7 +533,17 @@ class Topic < ActiveRecord::Base
def muted?(user) def muted?(user)
return false unless user && user.id return false unless user && user.id
tu = topic_users.where(user_id: user.id).first tu = topic_users.where(user_id: user.id).first
tu && tu.notification_level == TopicUser::NotificationLevel::MUTED tu && tu.notification_level == TopicUser.notification_levels[:muted]
end
def clear_pin_for(user)
return unless user.present?
TopicUser.change(user.id, id, cleared_pinned_at: Time.now)
end
def update_pinned(status)
update_column(:pinned_at, status ? Time.now : nil)
end end
def draft_key def draft_key
@ -535,18 +552,18 @@ class Topic < ActiveRecord::Base
# notification stuff # notification stuff
def notify_watch!(user) def notify_watch!(user)
TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::WATCHING) TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:watching])
end end
def notify_tracking!(user) def notify_tracking!(user)
TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::TRACKING) TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:tracking])
end end
def notify_regular!(user) def notify_regular!(user)
TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::REGULAR) TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:regular])
end end
def notify_muted!(user) def notify_muted!(user)
TopicUser.change(user, id, notification_level: TopicUser::NotificationLevel::MUTED) TopicUser.change(user, id, notification_level: TopicUser.notification_levels[:muted])
end end
end end

View File

@ -2,185 +2,173 @@ class TopicUser < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :topic belongs_to :topic
module NotificationLevel # Class methods
WATCHING = 3 class << self
TRACKING = 2
REGULAR = 1
MUTED = 0
end
module NotificationReasons # Enums
CREATED_TOPIC = 1 def notification_levels
USER_CHANGED = 2 @notification_levels ||= Enum.new(:muted, :regular, :tracking, :watching, start: 0)
USER_INTERACTED = 3 end
CREATED_POST = 4
end
def self.auto_track(user_id, topic_id, reason) def notification_reasons
if TopicUser.where(user_id: user_id, topic_id: topic_id, notifications_reason_id: nil).exists? @notification_reasons ||= Enum.new(:created_topic, :user_changed, :user_interacted, :created_post)
change(user_id, topic_id, end
notification_level: NotificationLevel::TRACKING,
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],
notifications_reason_id: reason notifications_reason_id: reason
) )
MessageBus.publish("/topic/#{topic_id}", { MessageBus.publish("/topic/#{topic_id}", {
notification_level_change: NotificationLevel::TRACKING, notification_level_change: notification_levels[:tracking],
notifications_reason_id: reason notifications_reason_id: reason
}, user_ids: [user_id]) }, user_ids: [user_id])
end
end
# Find the information specific to a user in a forum topic
def self.lookup_for(user, topics)
# If the user isn't logged in, there's no last read posts
return {} if user.blank? || topics.blank?
topic_ids = topics.map(&:id)
create_lookup(TopicUser.where(topic_id: topic_ids, user_id: user.id))
end
def self.create_lookup(topic_users)
topic_users = topic_users.to_a
result = {}
return result if topic_users.blank?
topic_users.each do |ftu|
result[ftu.topic_id] = ftu
end
result
end
def self.get(topic,user)
if Topic === topic
topic = topic.id
end
if User === user
user = user.id
end
TopicUser.where('topic_id = ? and user_id = ?', topic, user).first
end
# 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 self.change(user_id, topic_id, attrs)
# Sometimes people pass objs instead of the ids. We can handle that.
topic_id = topic_id.id if topic_id.is_a?(Topic)
user_id = user_id.id if user_id.is_a?(User)
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::NotificationReasons::USER_CHANGED
end end
attrs_array = attrs.to_a end
attrs_sql = attrs_array.map { |t| "#{t[0]} = ?" }.join(", ") # Find the information specific to a user in a forum topic
vals = attrs_array.map { |t| t[1] } def lookup_for(user, topics)
rows = TopicUser.update_all([attrs_sql, *vals], topic_id: topic_id.to_i, user_id: user_id) # If the user isn't logged in, there's no last read posts
return {} if user.blank? || topics.blank?
if rows == 0 topic_ids = topics.map(&:id)
now = DateTime.now create_lookup(TopicUser.where(topic_id: topic_ids, user_id: user.id))
auto_track_after = self.exec_sql("select auto_track_topics_after_msecs from users where id = ?", user_id).values[0][0] end
auto_track_after ||= SiteSetting.auto_track_topics_after
auto_track_after = auto_track_after.to_i
if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed] || 0) def create_lookup(topic_users)
attrs[:notification_level] ||= TopicUser::NotificationLevel::TRACKING topic_users = topic_users.to_a
result = {}
return result if topic_users.blank?
topic_users.each do |ftu|
result[ftu.topic_id] = ftu
end
result
end
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
end
# 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.
topic_id = topic_id.id if topic_id.is_a?(Topic)
user_id = user_id.id if user_id.is_a?(User)
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 end
attrs_array = attrs.to_a
TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id.to_i, first_visited_at: now ,last_visited_at: now)) attrs_sql = attrs_array.map { |t| "#{t[0]} = ?" }.join(", ")
vals = attrs_array.map { |t| t[1] }
rows = TopicUser.update_all([attrs_sql, *vals], topic_id: topic_id.to_i, user_id: user_id)
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
if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed] || 0)
attrs[:notification_level] ||= notification_levels[:tracking]
end
TopicUser.create(attrs.merge!(user_id: user_id, topic_id: topic_id.to_i, first_visited_at: now ,last_visited_at: now))
end
end end
rescue ActiveRecord::RecordNotUnique
# In case of a race condition to insert, do nothing
end end
rescue ActiveRecord::RecordNotUnique
# In case of a race condition to insert, do nothing
end
def self.track_visit!(topic,user) def track_visit!(topic,user)
now = DateTime.now now = DateTime.now
rows = exec_sql_row_count( rows = TopicUser.update_all({last_visited_at: now}, {topic_id: topic.id, user_id: user.id})
"update topic_users set last_visited_at=? where topic_id=? and user_id=?", if rows == 0
now, topic.id, user.id TopicUser.create(topic_id: topic.id, user_id: user.id, last_visited_at: now, first_visited_at: now)
)
if rows == 0
exec_sql('insert into topic_users(topic_id, user_id, last_visited_at, first_visited_at)
values(?,?,?,?)',
topic.id, user.id, now, now)
end
end
# 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 self.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: TopicUser::NotificationLevel::TRACKING,
threshold: SiteSetting.auto_track_topics_after
}
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
topic_users.notification_level, tu.notification_level old_level
",
args).values
if rows.length == 1
before = rows[0][1].to_i
after = rows[0][0].to_i
if before != after
MessageBus.publish("/topic/#{topic_id}", {notification_level_change: after}, user_ids: [user.id])
end end
end end
if rows.length == 0 # Update the last read and the last seen post count, but only if it doesn't exist.
args[:tracking] = TopicUser::NotificationLevel::TRACKING # This would be a lot easier if psql supported some kind of upsert
args[:regular] = TopicUser::NotificationLevel::REGULAR def update_last_read(user, topic_id, post_number, msecs)
args[:site_setting] = SiteSetting.auto_track_topics_after return if post_number.blank?
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) msecs = 0 if msecs.to_i < 0
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 args = {
FROM topics AS ft user_id: user.id,
JOIN users u on u.id = :user_id topic_id: topic_id,
WHERE ft.id = :topic_id post_number: post_number,
AND NOT EXISTS(SELECT 1 now: DateTime.now,
FROM topic_users AS ftu msecs: msecs,
WHERE ftu.user_id = :user_id and ftu.topic_id = :topic_id)", tracking: notification_levels[:tracking],
args) threshold: SiteSetting.auto_track_topics_after
}
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
topic_users.notification_level, tu.notification_level old_level
",
args).values
if rows.length == 1
before = rows[0][1].to_i
after = rows[0][0].to_i
if before != after
MessageBus.publish("/topic/#{topic_id}", {notification_level_change: after}, user_ids: [user.id])
end
end
if rows.length == 0
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
end end
end end
end end

View File

@ -443,11 +443,8 @@ class User < ActiveRecord::Base
end end
def readable_name def readable_name
if name.present? && name != username return "#{name} (#{username})" if name.present? && name != username
"#{name} (#{username})" username
else
username
end
end end
protected protected
@ -461,25 +458,14 @@ class User < ActiveRecord::Base
end end
def update_tracked_topics def update_tracked_topics
if auto_track_topics_after_msecs_changed? return unless auto_track_topics_after_msecs_changed?
if auto_track_topics_after_msecs < 0 where_conditions = {notifications_reason_id: nil, user_id: id}
if auto_track_topics_after_msecs < 0
User.exec_sql('update topic_users set notification_level = ? TopicUser.update_all({notification_level: TopicUser.notification_levels[:regular]}, where_conditions)
where notifications_reason_id is null and else
user_id = ?' , TopicUser::NotificationLevel::REGULAR , id) TopicUser.update_all(["notification_level = CASE WHEN total_msecs_viewed < ? THEN ? ELSE ? END",
else auto_track_topics_after_msecs, TopicUser.notification_levels[:regular], TopicUser.notification_levels[:tracking]], where_conditions)
User.exec_sql('update topic_users set notification_level = ?
where notifications_reason_id is null and
user_id = ? and
total_msecs_viewed < ?' , TopicUser::NotificationLevel::REGULAR , id, auto_track_topics_after_msecs)
User.exec_sql('update topic_users set notification_level = ?
where notifications_reason_id is null and
user_id = ? and
total_msecs_viewed >= ?' , TopicUser::NotificationLevel::TRACKING , id, auto_track_topics_after_msecs)
end
end end
end end

View File

@ -2,7 +2,6 @@ class CategoryTopicSerializer < BasicTopicSerializer
attributes :slug, attributes :slug,
:visible, :visible,
:pinned,
:closed, :closed,
:archived :archived

View File

@ -1,3 +1,5 @@
require_dependency 'pinned_check'
class TopicListItemSerializer < BasicTopicSerializer class TopicListItemSerializer < BasicTopicSerializer
attributes :views, attributes :views,
@ -29,4 +31,8 @@ class TopicListItemSerializer < BasicTopicSerializer
object.posters || [] object.posters || []
end end
def pinned
PinnedCheck.new(object, object.user_data).pinned?
end
end end

View File

@ -1,3 +1,5 @@
require_dependency 'pinned_check'
class TopicViewSerializer < ApplicationSerializer class TopicViewSerializer < ApplicationSerializer
# These attributes will be delegated to the topic # These attributes will be delegated to the topic
@ -12,7 +14,6 @@ class TopicViewSerializer < ApplicationSerializer
:last_posted_at, :last_posted_at,
:visible, :visible,
:closed, :closed,
:pinned,
:archived, :archived,
:moderator_posts_count, :moderator_posts_count,
:has_best_of, :has_best_of,
@ -42,7 +43,8 @@ class TopicViewSerializer < ApplicationSerializer
:notifications_reason_id, :notifications_reason_id,
:posts, :posts,
:at_bottom, :at_bottom,
:highest_post_number :highest_post_number,
:pinned
has_one :created_by, serializer: BasicUserSerializer, embed: :objects has_one :created_by, serializer: BasicUserSerializer, embed: :objects
has_one :last_poster, serializer: BasicUserSerializer, embed: :objects has_one :last_poster, serializer: BasicUserSerializer, embed: :objects
@ -193,6 +195,10 @@ class TopicViewSerializer < ApplicationSerializer
object.highest_post_number object.highest_post_number
end end
def pinned
PinnedCheck.new(object.topic, object.topic_user).pinned?
end
def posts def posts
return @posts if @posts.present? return @posts if @posts.present?
@posts = [] @posts = []

View File

@ -452,6 +452,10 @@ en:
title: 'Reply' title: 'Reply'
help: 'begin composing a reply to this topic' help: 'begin composing a reply to this topic'
clear_pin:
title: "Clear pin"
help: "Clear the pinned status of this topic so it no longer appears at the top of your topic list"
share: share:
title: 'Share' title: 'Share'
help: 'share a link to this topic' help: 'share a link to this topic'

View File

@ -175,6 +175,7 @@ Discourse::Application.routes.draw do
put 't/:topic_id/star' => 'topics#star', :constraints => {:topic_id => /\d+/} put 't/:topic_id/star' => 'topics#star', :constraints => {:topic_id => /\d+/}
put 't/:slug/:topic_id/status' => 'topics#status', :constraints => {:topic_id => /\d+/} put 't/:slug/:topic_id/status' => 'topics#status', :constraints => {:topic_id => /\d+/}
put 't/:topic_id/status' => 'topics#status', :constraints => {:topic_id => /\d+/} put 't/:topic_id/status' => 'topics#status', :constraints => {:topic_id => /\d+/}
put 't/:topic_id/clear-pin' => 'topics#clear_pin', :constraints => {:topic_id => /\d+/}
put 't/:topic_id/mute' => 'topics#mute', :constraints => {:topic_id => /\d+/} put 't/:topic_id/mute' => 'topics#mute', :constraints => {:topic_id => /\d+/}
put 't/:topic_id/unmute' => 'topics#unmute', :constraints => {:topic_id => /\d+/} put 't/:topic_id/unmute' => 'topics#unmute', :constraints => {:topic_id => /\d+/}

View File

@ -0,0 +1,9 @@
class AddClearedPinnedToTopicUsers < ActiveRecord::Migration
def change
add_column :topic_users, :cleared_pinned_at, :datetime, null: true
add_column :topics, :pinned_at, :datetime, null: true
execute "UPDATE topics SET pinned_at = created_at WHERE pinned"
remove_column :topics, :pinned
end
end

File diff suppressed because it is too large Load Diff

24
lib/pinned_check.rb Normal file
View File

@ -0,0 +1,24 @@
# Helps us determine whether a topic should be displayed as pinned or not,
# taking into account anonymous users and users who have dismissed it
class PinnedCheck
def initialize(topic, topic_user=nil)
@topic, @topic_user = topic, topic_user
end
def pinned?
# If the topic isn't pinned the answer is false
return false if @topic.pinned_at.blank?
# If the user is anonymous or hasn't entered the topic, the value is always true
return true if @topic_user.blank?
# If the user hasn't cleared the pin, it's true
return true if @topic_user.cleared_pinned_at.blank?
# The final check is to see whether the cleared the pin before or after it was last pinned
@topic_user.cleared_pinned_at < @topic.pinned_at
end
end

View File

@ -6,6 +6,47 @@ require_dependency 'topic_list'
class TopicQuery class TopicQuery
class << self
# use the constants in conjuction with COALESCE to determine the order with regard to pinned
# topics that have been cleared by the user. There
# might be a cleaner way to do this.
def lowest_date
"2010-01-01"
end
def highest_date
"3000-01-01"
end
# If you've clearned the pin, use bumped_at, otherwise put it at the top
def order_with_pinned_sql
"CASE
WHEN (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}'))
THEN '#{highest_date}'
ELSE topics.bumped_at
END DESC"
end
# If you've clearned the pin, use bumped_at, otherwise put it at the top
def order_nocategory_with_pinned_sql
"CASE
WHEN topics.category_id IS NULL and (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}'))
THEN '#{highest_date}'
ELSE topics.bumped_at
END DESC"
end
# For anonymous users
def order_nocategory_basic_bumped
"CASE WHEN topics.category_id IS NULL and (topics.pinned_at IS NOT NULL) THEN 0 ELSE 1 END, topics.bumped_at DESC"
end
def order_basic_bumped
"CASE WHEN (topics.pinned_at IS NOT NULL) THEN 0 ELSE 1 END, topics.bumped_at DESC"
end
end
def initialize(user=nil, opts={}) def initialize(user=nil, opts={})
@user = user @user = user
@ -64,23 +105,19 @@ class TopicQuery
# The popular view of topics # The popular view of topics
def list_popular def list_popular
return_list(unordered: true) do |list| TopicList.new(@user, default_list)
list.order('CASE WHEN topics.category_id IS NULL and topics.pinned THEN 0 ELSE 1 END, topics.bumped_at DESC')
end
end end
# The favorited topics # The favorited topics
def list_favorited def list_favorited
return_list do |list| return_list do |list|
list.joins("INNER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.starred AND tu.user_id = #{@user_id})") list.where('tu.starred')
end end
end end
def list_read def list_read
return_list(unordered: true) do |list| return_list(unordered: true) do |list|
list list.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC')
.joins("INNER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})")
.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC')
end end
end end
@ -93,17 +130,30 @@ class TopicQuery
end end
def list_posted def list_posted
return_list do |list| return_list {|l| l.where('tu.user_id IS NOT NULL') }
list.joins("INNER JOIN topic_users AS tu ON (tu.topic_id = topics.id AND tu.posted AND tu.user_id = #{@user_id})")
end
end end
def list_uncategorized def list_uncategorized
return_list {|l| l.where(category_id: nil).order('topics.pinned desc')} return_list(unordered: true) do |list|
list = list.where(category_id: nil)
if @user_id.present?
list.order(TopicQuery.order_with_pinned_sql)
else
list.order(TopicQuery.order_nocategory_basic_bumped)
end
end
end end
def list_category(category) def list_category(category)
return_list {|l| l.where(category_id: category.id).order('topics.pinned desc')} return_list(unordered: true) do |list|
list = list.where(category_id: category.id)
if @user_id.present?
list.order(TopicQuery.order_with_pinned_sql)
else
list.order(TopicQuery.order_basic_bumped)
end
end
end end
def unread_count def unread_count
@ -130,8 +180,22 @@ class TopicQuery
query_opts = @opts.merge(list_opts) query_opts = @opts.merge(list_opts)
page_size = query_opts[:per_page] || SiteSetting.topics_per_page page_size = query_opts[:per_page] || SiteSetting.topics_per_page
# Start with a list of all topics
result = Topic result = Topic
result = result.topic_list_order unless query_opts[:unordered]
if @user_id.present?
result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})")
end
unless query_opts[:unordered]
# If we're logged in, we have to pay attention to our pinned settings
if @user_id.present?
result = result.order(TopicQuery.order_nocategory_with_pinned_sql)
else
result = result.order(TopicQuery.order_basic_bumped)
end
end
result = result.listable_topics.includes(:category) result = result.listable_topics.includes(:category)
result = result.where('categories.name is null or categories.name <> ?', query_opts[:exclude_category]) if query_opts[:exclude_category] result = result.where('categories.name is null or categories.name <> ?', query_opts[:exclude_category]) if query_opts[:exclude_category]
result = result.where('categories.name = ?', query_opts[:only_category]) if query_opts[:only_category] result = result.where('categories.name = ?', query_opts[:only_category]) if query_opts[:only_category]
@ -145,16 +209,15 @@ class TopicQuery
def new_results(list_opts={}) def new_results(list_opts={})
default_list(list_opts) default_list(list_opts)
.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})")
.where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date) .where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date)
.where("tu.last_read_post_number IS NULL") .where("tu.last_read_post_number IS NULL")
.where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser::NotificationLevel::TRACKING) .where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser.notification_levels[:tracking])
end end
def unread_results(list_opts={}) def unread_results(list_opts={})
default_list(list_opts) default_list(list_opts)
.joins("INNER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id} AND tu.last_read_post_number < topics.highest_post_number)") .where("tu.last_read_post_number < topics.highest_post_number")
.where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser::NotificationLevel::REGULAR, tracking: TopicUser::NotificationLevel::TRACKING) .where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking])
end end
def random_suggested_results_for(topic, count, exclude_topic_ids) def random_suggested_results_for(topic, count, exclude_topic_ids)

View File

@ -27,7 +27,7 @@ class Unread
protected protected
def do_not_notify?(notification_level) def do_not_notify?(notification_level)
[TopicUser::NotificationLevel::MUTED, TopicUser::NotificationLevel::REGULAR].include?(notification_level) [TopicUser.notification_levels[:muted], TopicUser.notification_levels[:regular]].include?(notification_level)
end end
end end

View File

@ -0,0 +1,58 @@
require 'pinned_check'
describe PinnedCheck do
#let(:topic) { Fabricate.build(:topic) }
let(:pinned_at) { 12.hours.ago }
let(:unpinned_topic) { Fabricate.build(:topic) }
let(:pinned_topic) { Fabricate.build(:topic, pinned_at: pinned_at) }
context "without a topic_user record (either anonymous or never been in the topic)" do
it "returns false if the topic is not pinned" do
PinnedCheck.new(unpinned_topic).should_not be_pinned
end
it "returns true if the topic is pinned" do
PinnedCheck.new(unpinned_topic).should_not be_pinned
end
end
context "with a topic_user record" do
let(:user) { Fabricate.build(:user) }
let(:unpinned_topic_user) { Fabricate.build(:topic_user, user: user, topic: unpinned_topic) }
describe "unpinned topic" do
let(:topic_user) { TopicUser.new(topic: unpinned_topic, user: user) }
it "returns false" do
PinnedCheck.new(unpinned_topic, topic_user).should_not be_pinned
end
end
describe "pinned topic" do
let(:topic_user) { TopicUser.new(topic: pinned_topic, user: user) }
it "is pinned if the topic_user's cleared_pinned_at is blank" do
PinnedCheck.new(pinned_topic, topic_user).should be_pinned
end
it "is not pinned if the topic_user's cleared_pinned_at is later than when it was pinned_at" do
topic_user.cleared_pinned_at = (pinned_at + 1.hour)
PinnedCheck.new(pinned_topic, topic_user).should_not be_pinned
end
it "is pinned if the topic_user's cleared_pinned_at is earlier than when it was pinned_at" do
topic_user.cleared_pinned_at = (pinned_at - 3.hours)
PinnedCheck.new(pinned_topic, topic_user).should be_pinned
end
end
end
end

View File

@ -12,14 +12,13 @@ describe TopicQuery do
context 'a bunch of topics' do context 'a bunch of topics' do
let!(:regular_topic) { Fabricate(:topic, title: 'this is a regular topic', user: creator, bumped_at: 15.minutes.ago) } let!(:regular_topic) { Fabricate(:topic, title: 'this is a regular topic', user: creator, bumped_at: 15.minutes.ago) }
let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned: true, bumped_at: 10.minutes.ago) } let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) }
let!(:archived_topic) { Fabricate(:topic, title: 'this is an archived topic', user: creator, archived: true, bumped_at: 6.minutes.ago) } let!(:archived_topic) { Fabricate(:topic, title: 'this is an archived topic', user: creator, archived: true, bumped_at: 6.minutes.ago) }
let!(:invisible_topic) { Fabricate(:topic, title: 'this is an invisible topic', user: creator, visible: false, bumped_at: 5.minutes.ago) } let!(:invisible_topic) { Fabricate(:topic, title: 'this is an invisible topic', user: creator, visible: false, bumped_at: 5.minutes.ago) }
let!(:closed_topic) { Fabricate(:topic, title: 'this is a closed topic', user: creator, closed: true, bumped_at: 1.minute.ago) } let!(:closed_topic) { Fabricate(:topic, title: 'this is a closed topic', user: creator, closed: true, bumped_at: 1.minute.ago) }
let(:topics) { topic_query.list_popular.topics }
context 'list_popular' do context 'list_popular' do
let(:topics) { topic_query.list_popular.topics }
it "returns the topics in the correct order" do it "returns the topics in the correct order" do
topics.should == [pinned_topic, closed_topic, archived_topic, regular_topic] topics.should == [pinned_topic, closed_topic, archived_topic, regular_topic]
end end
@ -33,6 +32,17 @@ describe TopicQuery do
end end
end end
context 'after clearring a pinned topic' do
before do
pinned_topic.clear_pin_for(user)
end
it "no longer shows the pinned topic at the top" do
topics.should == [closed_topic, archived_topic, pinned_topic, regular_topic]
end
end
end end
context 'categorized' do context 'categorized' do

View File

@ -7,8 +7,8 @@ describe Unread do
before do before do
@topic = Fabricate(:topic, posts_count: 13, highest_post_number: 13) @topic = Fabricate(:topic, posts_count: 13, highest_post_number: 13)
@topic_user = TopicUser.get(@topic, @topic.user) @topic_user = TopicUser.get(@topic, @topic.user)
@topic_user.stubs(:notification_level).returns(TopicUser::NotificationLevel::TRACKING) @topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:tracking])
@topic_user.notification_level = TopicUser::NotificationLevel::TRACKING @topic_user.notification_level = TopicUser.notification_levels[:tracking]
@unread = Unread.new(@topic, @topic_user) @unread = Unread.new(@topic, @topic_user)
end end
@ -51,7 +51,7 @@ describe Unread do
it 'has 0 new posts if the user has read 10 posts but is not tracking' do it 'has 0 new posts if the user has read 10 posts but is not tracking' do
@topic_user.stubs(:seen_post_count).returns(10) @topic_user.stubs(:seen_post_count).returns(10)
@topic_user.stubs(:notification_level).returns(TopicUser::NotificationLevel::REGULAR) @topic_user.stubs(:notification_level).returns(TopicUser.notification_levels[:regular])
@unread.new_posts.should == 0 @unread.new_posts.should == 0
end end

View File

@ -74,6 +74,37 @@ describe TopicsController do
end end
context 'clear_pin' do
it 'needs you to be logged in' do
lambda { xhr :put, :clear_pin, topic_id: 1 }.should raise_error(Discourse::NotLoggedIn)
end
context 'when logged in' do
let(:topic) { Fabricate(:topic) }
let!(:user) { log_in }
it "fails when the user can't see the topic" do
Guardian.any_instance.expects(:can_see?).with(topic).returns(false)
xhr :put, :clear_pin, topic_id: topic.id
response.should_not be_success
end
describe 'when the user can see the topic' do
it "calls clear_pin_for if the user can see the topic" do
Topic.any_instance.expects(:clear_pin_for).with(user).once
xhr :put, :clear_pin, topic_id: topic.id
end
it "succeeds" do
xhr :put, :clear_pin, topic_id: topic.id
response.should be_success
end
end
end
end
context 'status' do context 'status' do
it 'needs you to be logged in' do it 'needs you to be logged in' do
lambda { xhr :put, :status, topic_id: 1, status: 'visible', enabled: true }.should raise_error(Discourse::NotLoggedIn) lambda { xhr :put, :status, topic_id: 1, status: 'visible', enabled: true }.should raise_error(Discourse::NotLoggedIn)

View File

@ -547,8 +547,12 @@ describe Topic do
@topic.reload @topic.reload
end end
it "doesn't have a pinned_at" do
@topic.pinned_at.should be_blank
end
it 'should not be pinned' do it 'should not be pinned' do
@topic.should_not be_pinned @topic.pinned_at.should be_blank
end end
it 'adds a moderator post' do it 'adds a moderator post' do
@ -562,13 +566,13 @@ describe Topic do
context 'enable' do context 'enable' do
before do before do
@topic.update_attribute :pinned, false @topic.update_attribute :pinned_at, nil
@topic.update_status('pinned', true, @user) @topic.update_status('pinned', true, @user)
@topic.reload @topic.reload
end end
it 'should be pinned' do it 'should be pinned' do
@topic.should be_pinned @topic.pinned_at.should be_present
end end
it 'adds a moderator post' do it 'adds a moderator post' do
@ -588,7 +592,7 @@ describe Topic do
@topic.reload @topic.reload
end end
it 'should not be pinned' do it 'should not be archived' do
@topic.should_not be_archived @topic.should_not be_archived
end end
@ -866,8 +870,12 @@ describe Topic do
topic.should be_visible topic.should be_visible
end end
it "has an empty pinned_at" do
topic.pinned_at.should be_blank
end
it 'is not pinned' do it 'is not pinned' do
topic.should_not be_pinned topic.pinned_at.should be_blank
end end
it 'is not closed' do it 'is not closed' do

View File

@ -5,155 +5,165 @@ describe TopicUser do
it { should belong_to :user } it { should belong_to :user }
it { should belong_to :topic } it { should belong_to :topic }
let!(:yesterday) { DateTime.now.yesterday }
before do before do
#mock time so we can test dates DateTime.expects(:now).at_least_once.returns(yesterday)
@now = DateTime.now.yesterday end
DateTime.expects(:now).at_least_once.returns(@now)
@topic = Fabricate(:topic) let!(:topic) { Fabricate(:topic) }
@user = Fabricate(:coding_horror) let!(:user) { Fabricate(:coding_horror) }
let(:topic_user) { TopicUser.get(topic,user) }
let(:topic_creator_user) { TopicUser.get(topic, topic.user) }
let(:post) { Fabricate(:post, topic: topic, user: user) }
let(:new_user) { Fabricate(:user, auto_track_topics_after_msecs: 1000) }
let(:topic_new_user) { TopicUser.get(topic, new_user)}
describe "unpinned" do
before do
TopicUser.change(user, topic, {:starred_at => yesterday})
end
it "defaults to blank" do
topic_user.cleared_pinned_at.should be_blank
end
end end
describe 'notifications' do describe 'notifications' do
it 'should be set to tracking if auto_track_topics is enabled' do it 'should be set to tracking if auto_track_topics is enabled' do
@user.auto_track_topics_after_msecs = 0 user.update_column(:auto_track_topics_after_msecs, 0)
@user.save TopicUser.change(user, topic, {:starred_at => yesterday})
TopicUser.change(@user, @topic, {:starred_at => DateTime.now}) TopicUser.get(topic, user).notification_level.should == TopicUser.notification_levels[:tracking]
TopicUser.get(@topic,@user).notification_level.should == TopicUser::NotificationLevel::TRACKING
end end
it 'should reset regular topics to tracking topics if auto track is changed' do it 'should reset regular topics to tracking topics if auto track is changed' do
TopicUser.change(@user, @topic, {:starred_at => DateTime.now}) TopicUser.change(user, topic, {:starred_at => yesterday})
@user.auto_track_topics_after_msecs = 0 user.auto_track_topics_after_msecs = 0
@user.save user.save
TopicUser.get(@topic,@user).notification_level.should == TopicUser::NotificationLevel::TRACKING topic_user.notification_level.should == TopicUser.notification_levels[:tracking]
end end
it 'should be set to "regular" notifications, by default on non creators' do it 'should be set to "regular" notifications, by default on non creators' do
TopicUser.change(@user, @topic, {:starred_at => DateTime.now}) TopicUser.change(user, topic, {:starred_at => yesterday})
TopicUser.get(@topic,@user).notification_level.should == TopicUser::NotificationLevel::REGULAR TopicUser.get(topic,user).notification_level.should == TopicUser.notification_levels[:regular]
end end
it 'reason should reset when changed' do it 'reason should reset when changed' do
@topic.notify_muted!(@topic.user) topic.notify_muted!(topic.user)
TopicUser.get(@topic,@topic.user).notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED TopicUser.get(topic,topic.user).notifications_reason_id.should == TopicUser.notification_reasons[:user_changed]
end end
it 'should have the correct reason for a user change when watched' do it 'should have the correct reason for a user change when watched' do
@topic.notify_watch!(@user) topic.notify_watch!(user)
tu = TopicUser.get(@topic,@user) topic_user.notification_level.should == TopicUser.notification_levels[:watching]
tu.notification_level.should == TopicUser::NotificationLevel::WATCHING topic_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed]
tu.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED topic_user.notifications_changed_at.should_not be_nil
tu.notifications_changed_at.should_not be_nil
end end
it 'should have the correct reason for a user change when set to regular' do it 'should have the correct reason for a user change when set to regular' do
@topic.notify_regular!(@user) topic.notify_regular!(user)
tu = TopicUser.get(@topic,@user) topic_user.notification_level.should == TopicUser.notification_levels[:regular]
tu.notification_level.should == TopicUser::NotificationLevel::REGULAR topic_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed]
tu.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED topic_user.notifications_changed_at.should_not be_nil
tu.notifications_changed_at.should_not be_nil
end end
it 'should have the correct reason for a user change when set to regular' do it 'should have the correct reason for a user change when set to regular' do
@topic.notify_muted!(@user) topic.notify_muted!(user)
tu = TopicUser.get(@topic,@user) topic_user.notification_level.should == TopicUser.notification_levels[:muted]
tu.notification_level.should == TopicUser::NotificationLevel::MUTED topic_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed]
tu.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED topic_user.notifications_changed_at.should_not be_nil
tu.notifications_changed_at.should_not be_nil
end end
it 'should watch topics a user created' do it 'should watch topics a user created' do
tu = TopicUser.get(@topic,@topic.user) topic_creator_user.notification_level.should == TopicUser.notification_levels[:watching]
tu.notification_level.should == TopicUser::NotificationLevel::WATCHING topic_creator_user.notifications_reason_id.should == TopicUser.notification_reasons[:created_topic]
tu.notifications_reason_id.should == TopicUser::NotificationReasons::CREATED_TOPIC
end end
end end
describe 'visited at' do describe 'visited at' do
before do
TopicUser.track_visit!(@topic, @user)
@topic_user = TopicUser.get(@topic,@user)
before do
TopicUser.track_visit!(topic, user)
end end
it 'set upon initial visit' do it 'set upon initial visit' do
@topic_user.first_visited_at.to_i.should == @now.to_i topic_user.first_visited_at.to_i.should == yesterday.to_i
@topic_user.last_visited_at.to_i.should == @now.to_i topic_user.last_visited_at.to_i.should == yesterday.to_i
end end
it 'updates upon repeat visit' do it 'updates upon repeat visit' do
tomorrow = @now.tomorrow today = yesterday.tomorrow
DateTime.expects(:now).returns(tomorrow) DateTime.expects(:now).returns(today)
TopicUser.track_visit!(@topic,@user) TopicUser.track_visit!(topic,user)
# reload is a no go # reload is a no go
@topic_user = TopicUser.get(@topic,@user) topic_user = TopicUser.get(topic,user)
@topic_user.first_visited_at.to_i.should == @now.to_i topic_user.first_visited_at.to_i.should == yesterday.to_i
@topic_user.last_visited_at.to_i.should == tomorrow.to_i topic_user.last_visited_at.to_i.should == today.to_i
end end
end end
describe 'read tracking' do describe 'read tracking' do
before do
@post = Fabricate(:post, topic: @topic, user: @topic.user)
TopicUser.update_last_read(@user, @topic.id, 1, 0)
@topic_user = TopicUser.get(@topic,@user)
end
it 'should create a new record for a visit' do context "without auto tracking" do
@topic_user.last_read_post_number.should == 1
@topic_user.last_visited_at.to_i.should == @now.to_i
@topic_user.first_visited_at.to_i.should == @now.to_i
end
it 'should update the record for repeat visit' do before do
Fabricate(:post, topic: @topic, user: @user) TopicUser.update_last_read(user, topic.id, 1, 0)
TopicUser.update_last_read(@user, @topic.id, 2, 0) end
@topic_user = TopicUser.get(@topic,@user)
@topic_user.last_read_post_number.should == 2 let(:topic_user) { TopicUser.get(topic,user) }
@topic_user.last_visited_at.to_i.should == @now.to_i
@topic_user.first_visited_at.to_i.should == @now.to_i it 'should create a new record for a visit' do
topic_user.last_read_post_number.should == 1
topic_user.last_visited_at.to_i.should == yesterday.to_i
topic_user.first_visited_at.to_i.should == yesterday.to_i
end
it 'should update the record for repeat visit' do
Fabricate(:post, topic: topic, user: user)
TopicUser.update_last_read(user, topic.id, 2, 0)
topic_user = TopicUser.get(topic,user)
topic_user.last_read_post_number.should == 2
topic_user.last_visited_at.to_i.should == yesterday.to_i
topic_user.first_visited_at.to_i.should == yesterday.to_i
end
end end
context 'auto tracking' do context 'auto tracking' do
before do before do
Fabricate(:post, topic: @topic, user: @user) TopicUser.update_last_read(new_user, topic.id, 2, 0)
@new_user = Fabricate(:user, auto_track_topics_after_msecs: 1000)
TopicUser.update_last_read(@new_user, @topic.id, 2, 0)
@topic_user = TopicUser.get(@topic,@new_user)
end end
it 'should automatically track topics you reply to' do it 'should automatically track topics you reply to' do
post = Fabricate(:post, topic: @topic, user: @new_user) post = Fabricate(:post, topic: topic, user: new_user)
@topic_user = TopicUser.get(@topic,@new_user) topic_new_user.notification_level.should == TopicUser.notification_levels[:tracking]
@topic_user.notification_level.should == TopicUser::NotificationLevel::TRACKING topic_new_user.notifications_reason_id.should == TopicUser.notification_reasons[:created_post]
@topic_user.notifications_reason_id.should == TopicUser::NotificationReasons::CREATED_POST
end end
it 'should not automatically track topics you reply to and have set state manually' do it 'should not automatically track topics you reply to and have set state manually' do
Fabricate(:post, topic: @topic, user: @new_user) Fabricate(:post, topic: topic, user: new_user)
TopicUser.change(@new_user, @topic, notification_level: TopicUser::NotificationLevel::REGULAR) TopicUser.change(new_user, topic, notification_level: TopicUser.notification_levels[:regular])
@topic_user = TopicUser.get(@topic,@new_user) topic_new_user.notification_level.should == TopicUser.notification_levels[:regular]
@topic_user.notification_level.should == TopicUser::NotificationLevel::REGULAR topic_new_user.notifications_reason_id.should == TopicUser.notification_reasons[:user_changed]
@topic_user.notifications_reason_id.should == TopicUser::NotificationReasons::USER_CHANGED
end end
it 'should automatically track topics after they are read for long enough' do it 'should automatically track topics after they are read for long enough' do
@topic_user.notification_level.should == TopicUser::NotificationLevel::REGULAR topic_new_user.notification_level.should ==TopicUser.notification_levels[:regular]
TopicUser.update_last_read(@new_user, @topic.id, 2, 1001) TopicUser.update_last_read(new_user, topic.id, 2, 1001)
@topic_user = TopicUser.get(@topic,@new_user) TopicUser.get(topic, new_user).notification_level.should == TopicUser.notification_levels[:tracking]
@topic_user.notification_level.should == TopicUser::NotificationLevel::TRACKING
end end
it 'should not automatically track topics after they are read for long enough if changed manually' do it 'should not automatically track topics after they are read for long enough if changed manually' do
TopicUser.change(@new_user, @topic, notification_level: TopicUser::NotificationLevel::REGULAR) TopicUser.change(new_user, topic, notification_level: TopicUser.notification_levels[:regular])
@topic_user = TopicUser.get(@topic,@new_user) TopicUser.update_last_read(new_user, topic, 2, 1001)
topic_new_user.notification_level.should == TopicUser.notification_levels[:regular]
TopicUser.update_last_read(@new_user, @topic, 2, 1001)
@topic_user = TopicUser.get(@topic,@new_user)
@topic_user.notification_level.should == TopicUser::NotificationLevel::REGULAR
end end
end end
end end
@ -162,34 +172,33 @@ describe TopicUser do
it 'creates a forum topic user record' do it 'creates a forum topic user record' do
lambda { lambda {
TopicUser.change(@user, @topic.id, starred: true) TopicUser.change(user, topic.id, starred: true)
}.should change(TopicUser, :count).by(1) }.should change(TopicUser, :count).by(1)
end end
it "only inserts a row once, even on repeated calls" do it "only inserts a row once, even on repeated calls" do
lambda { lambda {
TopicUser.change(@user, @topic.id, starred: true) TopicUser.change(user, topic.id, starred: true)
TopicUser.change(@user, @topic.id, starred: false) TopicUser.change(user, topic.id, starred: false)
TopicUser.change(@user, @topic.id, starred: true) TopicUser.change(user, topic.id, starred: true)
}.should change(TopicUser, :count).by(1) }.should change(TopicUser, :count).by(1)
end end
describe 'after creating a row' do describe 'after creating a row' do
before do before do
TopicUser.change(@user, @topic.id, starred: true) TopicUser.change(user, topic.id, starred: true)
@topic_user = TopicUser.where(user_id: @user.id, topic_id: @topic.id).first
end end
it 'has the correct starred value' do it 'has the correct starred value' do
@topic_user.should be_starred TopicUser.get(topic, user).should be_starred
end end
it 'has a lookup' do it 'has a lookup' do
TopicUser.lookup_for(@user, [@topic]).should be_present TopicUser.lookup_for(user, [topic]).should be_present
end end
it 'has a key in the lookup for this forum topic' do it 'has a key in the lookup for this forum topic' do
TopicUser.lookup_for(@user, [@topic]).has_key?(@topic.id).should be_true TopicUser.lookup_for(user, [topic]).has_key?(topic.id).should be_true
end end
end end