mirror of
https://github.com/discourse/discourse.git
synced 2025-03-23 12:35:44 +08:00
User tracking state implementation progress for live unread / new counts
This commit is contained in:
parent
cdbe6f64c7
commit
fcc7192fd2
@ -7,6 +7,8 @@ class UserTrackingState
|
|||||||
|
|
||||||
CHANNEL = "/user-tracking"
|
CHANNEL = "/user-tracking"
|
||||||
|
|
||||||
|
attr_accessor :user_id, :topic_id, :highest_post_number, :last_read_post_number, :created_at
|
||||||
|
|
||||||
MessageBus.client_filter(CHANNEL) do |user_id, message|
|
MessageBus.client_filter(CHANNEL) do |user_id, message|
|
||||||
if user_id
|
if user_id
|
||||||
UserTrackingState.new(User.find(user_id)).filter(message)
|
UserTrackingState.new(User.find(user_id)).filter(message)
|
||||||
@ -19,23 +21,61 @@ class UserTrackingState
|
|||||||
MessageBus.publish(CHANNEL, "CHANGE", user_ids: [user_id].compact)
|
MessageBus.publish(CHANNEL, "CHANGE", user_ids: [user_id].compact)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(user)
|
def self.treat_as_new_topic_clause
|
||||||
@user = user
|
User.where("CASE
|
||||||
@query = TopicQuery.new(@user)
|
WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :always THEN u.created_at
|
||||||
|
WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(u.previous_visit_at,u.created_at)
|
||||||
|
ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(u.new_topic_duration_minutes, :default_duration))
|
||||||
|
END",
|
||||||
|
now: DateTime.now,
|
||||||
|
last_visit: User::NewTopicDuration::LAST_VISIT,
|
||||||
|
always: User::NewTopicDuration::ALWAYS,
|
||||||
|
default_duration: SiteSetting.new_topic_duration_minutes
|
||||||
|
).where_values[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_list
|
def self.report(user_ids, topic_id = nil)
|
||||||
@query
|
|
||||||
.new_results(limit: false)
|
|
||||||
.select(topics: [:id, :created_at])
|
|
||||||
.map{|t| [t.id, t.created_at]}
|
|
||||||
end
|
|
||||||
|
|
||||||
def unread_list
|
# Sam: this is a hairy report, in particular I need custom joins and fancy conditions
|
||||||
[]
|
# Dropping to sql_builder so I can make sense of it.
|
||||||
end
|
#
|
||||||
|
# Keep in mind, we need to be able to filter on a GROUP of users, and zero in on topic
|
||||||
|
# all our existing scope work does not do this
|
||||||
|
#
|
||||||
|
# This code needs to be VERY efficient as it is triggered via the message bus and may steal
|
||||||
|
# cycles from usual requests
|
||||||
|
#
|
||||||
|
|
||||||
|
unread = TopicQuery.unread_filter(Topic).where_values.join(" AND ")
|
||||||
|
new = TopicQuery.new_filter(Topic, "xxx").where_values.join(" AND ").gsub!("'xxx'", treat_as_new_topic_clause)
|
||||||
|
|
||||||
|
sql = <<SQL
|
||||||
|
SELECT u.id AS user_id, topics.id AS topic_id, topics.created_at, highest_post_number, last_read_post_number
|
||||||
|
FROM users u
|
||||||
|
FULL OUTER JOIN topics ON 1=1
|
||||||
|
LEFT JOIN topic_users tu ON tu.topic_id = topics.id AND tu.user_id = u.id
|
||||||
|
LEFT JOIN categories c ON c.id = topics.id
|
||||||
|
WHERE u.id IN (:user_ids) AND
|
||||||
|
topics.archetype <> 'private_message' AND
|
||||||
|
((#{unread}) OR (#{new})) AND
|
||||||
|
(topics.visible OR u.admin OR u.moderator) AND
|
||||||
|
topics.deleted_at IS NULL AND
|
||||||
|
( category_id IS NULL OR NOT c.secure OR category_id IN (
|
||||||
|
SELECT c2.id FROM categories c2
|
||||||
|
JOIN category_groups cg ON cg.category_id = c2.id
|
||||||
|
JOIN group_users gu ON gu.user_id = u.id AND cg.group_id = gu.group_id
|
||||||
|
WHERE c2.secure )
|
||||||
|
)
|
||||||
|
|
||||||
|
SQL
|
||||||
|
|
||||||
|
if topic_id
|
||||||
|
sql << " AND topics.id = :topic_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
SqlBuilder.new(sql)
|
||||||
|
.map_exec(UserTrackingState, user_ids: user_ids, topic_id: topic_id)
|
||||||
|
|
||||||
def filter(message)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -25,7 +25,7 @@ class SqlBuilder
|
|||||||
when :select
|
when :select
|
||||||
joined = "SELECT " << v.join(" , ")
|
joined = "SELECT " << v.join(" , ")
|
||||||
when :where, :where2
|
when :where, :where2
|
||||||
joined = "WHERE " << v.join(" AND ")
|
joined = "WHERE " << v.map{|c| "(" << c << ")" }.join(" AND ")
|
||||||
when :join
|
when :join
|
||||||
joined = v.map{|v| "JOIN " << v }.join("\n")
|
joined = v.map{|v| "JOIN " << v }.join("\n")
|
||||||
when :left_join
|
when :left_join
|
||||||
@ -55,6 +55,35 @@ class SqlBuilder
|
|||||||
ActiveRecord::Base.exec_sql(sql,@args)
|
ActiveRecord::Base.exec_sql(sql,@args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#weird AS reloading
|
||||||
|
unless defined? FTYPE_MAP
|
||||||
|
FTYPE_MAP = {
|
||||||
|
23 => :value_to_integer,
|
||||||
|
1114 => :string_to_time
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_exec(klass, args = {})
|
||||||
|
results = exec(args)
|
||||||
|
|
||||||
|
setters = results.fields.each_with_index.map do |f, index|
|
||||||
|
[(f.dup << "=").to_sym, FTYPE_MAP[results.ftype(index)]]
|
||||||
|
end
|
||||||
|
values = results.values
|
||||||
|
values.map! do |row|
|
||||||
|
mapped = klass.new
|
||||||
|
setters.each_with_index do |mapper, index|
|
||||||
|
translated = row[index]
|
||||||
|
if mapper[1] && !translated.nil?
|
||||||
|
translated = ActiveRecord::ConnectionAdapters::Column.send mapper[1], translated
|
||||||
|
end
|
||||||
|
mapped.send mapper[0], translated
|
||||||
|
end
|
||||||
|
mapped
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class ActiveRecord::Base
|
class ActiveRecord::Base
|
||||||
|
@ -190,17 +190,23 @@ class TopicQuery
|
|||||||
create_list(:new_in_category) {|l| l.where(category_id: category.id).by_newest.first(25)}
|
create_list(:new_in_category) {|l| l.where(category_id: category.id).by_newest.first(25)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.new_filter(list,treat_as_new_topic_start_date)
|
||||||
|
list.where("topics.created_at >= :created_at", created_at: treat_as_new_topic_start_date)
|
||||||
|
.where("tu.last_read_post_number IS NULL")
|
||||||
|
.where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser.notification_levels[:tracking])
|
||||||
|
end
|
||||||
|
|
||||||
def new_results(list_opts={})
|
def new_results(list_opts={})
|
||||||
default_list(list_opts)
|
TopicQuery.new_filter(default_list(list_opts),@user.treat_as_new_topic_start_date)
|
||||||
.where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date)
|
end
|
||||||
.where("tu.last_read_post_number IS NULL")
|
|
||||||
.where("COALESCE(tu.notification_level, :tracking) >= :tracking", tracking: TopicUser.notification_levels[:tracking])
|
def self.unread_filter(list)
|
||||||
|
list.where("tu.last_read_post_number < topics.highest_post_number")
|
||||||
|
.where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking])
|
||||||
end
|
end
|
||||||
|
|
||||||
def unread_results(list_opts={})
|
def unread_results(list_opts={})
|
||||||
default_list(list_opts)
|
TopicQuery.unread_filter(default_list(list_opts))
|
||||||
.where("tu.last_read_post_number < topics.highest_post_number")
|
|
||||||
.where("COALESCE(tu.notification_level, :regular) >= :tracking", regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
@ -18,6 +18,24 @@ describe SqlBuilder do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "map" do
|
||||||
|
class SqlBuilder::TestClass
|
||||||
|
attr_accessor :int, :string, :date, :text
|
||||||
|
end
|
||||||
|
|
||||||
|
it "correctly maps to a klass" do
|
||||||
|
rows = SqlBuilder.new("SELECT 1 AS int, 'string' AS string, CAST(NOW() at time zone 'utc' AS timestamp without time zone) AS date, 'text'::text AS text")
|
||||||
|
.map_exec(SqlBuilder::TestClass)
|
||||||
|
|
||||||
|
rows.count.should == 1
|
||||||
|
row = rows[0]
|
||||||
|
row.int.should == 1
|
||||||
|
row.string.should == "string"
|
||||||
|
row.text.should == "text"
|
||||||
|
row.date.should be_within(10.seconds).of(DateTime.now)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "detached" do
|
describe "detached" do
|
||||||
before do
|
before do
|
||||||
@builder = SqlBuilder.new("select * from (select :a A union all select :b) as X /*where*/ /*order_by*/ /*limit*/ /*offset*/")
|
@builder = SqlBuilder.new("select * from (select :a A union all select :b) as X /*where*/ /*order_by*/ /*limit*/ /*offset*/")
|
||||||
|
@ -10,25 +10,40 @@ describe UserTrackingState do
|
|||||||
Fabricate(:post)
|
Fabricate(:post)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:state) do
|
it "correctly gets the tracking state" do
|
||||||
UserTrackingState.new(user)
|
report = UserTrackingState.report([user.id])
|
||||||
end
|
report.length.should == 0
|
||||||
|
|
||||||
it "correctly gets the list of new topics" do
|
|
||||||
state.new_list.should == []
|
|
||||||
state.unread_list.should == []
|
|
||||||
|
|
||||||
new_post = post
|
new_post = post
|
||||||
|
|
||||||
new_list = state.new_list
|
report = UserTrackingState.report([user.id])
|
||||||
|
|
||||||
new_list.length.should == 1
|
report.length.should == 1
|
||||||
new_list[0][0].should == post.topic.id
|
row = report[0]
|
||||||
new_list[0][1].should be_within(1.second).of(post.topic.created_at)
|
|
||||||
|
|
||||||
state.unread_list.should == []
|
row.topic_id.should == post.topic_id
|
||||||
|
row.highest_post_number.should == 1
|
||||||
|
row.last_read_post_number.should be_nil
|
||||||
|
row.user_id.should == user.id
|
||||||
|
|
||||||
# read it
|
# lets not leak out random users
|
||||||
|
UserTrackingState.report([post.user_id]).should be_empty
|
||||||
|
|
||||||
|
# lets not return anything if we scope on non-existing topic
|
||||||
|
UserTrackingState.report([user.id], post.topic_id + 1).should be_empty
|
||||||
|
|
||||||
|
# when we reply the poster should have an unread row
|
||||||
|
Fabricate(:post, user: user, topic: post.topic)
|
||||||
|
|
||||||
|
report = UserTrackingState.report([post.user_id, user.id])
|
||||||
|
report.length.should == 1
|
||||||
|
|
||||||
|
row = report[0]
|
||||||
|
|
||||||
|
row.topic_id.should == post.topic_id
|
||||||
|
row.highest_post_number.should == 2
|
||||||
|
row.last_read_post_number.should == 1
|
||||||
|
row.user_id.should == post.user_id
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user