2013-02-06 03:16:51 +08:00
|
|
|
require_dependency 'guardian'
|
|
|
|
require_dependency 'topic_query'
|
|
|
|
|
|
|
|
class TopicView
|
|
|
|
|
|
|
|
attr_accessor :topic, :min, :max, :draft, :draft_key, :draft_sequence
|
|
|
|
|
|
|
|
def initialize(topic_id, user=nil, options={})
|
2013-02-11 02:50:26 +08:00
|
|
|
@topic = find_topic(topic_id)
|
2013-02-06 03:16:51 +08:00
|
|
|
raise Discourse::NotFound if @topic.blank?
|
|
|
|
|
|
|
|
# Special case: If the topic is private and the user isn't logged in, ask them
|
|
|
|
# to log in!
|
|
|
|
if @topic.present? and @topic.private_message? and user.blank?
|
|
|
|
raise Discourse::NotLoggedIn.new
|
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
Guardian.new(user).ensure_can_see!(@topic)
|
2013-02-06 03:16:51 +08:00
|
|
|
@min, @max = 1, SiteSetting.posts_per_page
|
2013-02-11 02:50:26 +08:00
|
|
|
@post_number, @page = options[:post_number], options[:page]
|
2013-02-06 03:16:51 +08:00
|
|
|
@posts = @topic.posts
|
|
|
|
|
|
|
|
@posts = @posts.with_deleted if user.try(:admin?)
|
|
|
|
@posts = @posts.best_of if options[:best_of].present?
|
|
|
|
|
|
|
|
if options[:username_filters].present?
|
|
|
|
usernames = options[:username_filters].map{|u| u.downcase}
|
|
|
|
@posts = @posts.where('post_number = 1 or user_id in (select u.id from users u where username_lower in (?))', usernames)
|
|
|
|
end
|
|
|
|
|
|
|
|
@user = user
|
|
|
|
@initial_load = true
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
filter_posts(options)
|
|
|
|
|
|
|
|
@draft_key = @topic.draft_key
|
|
|
|
@draft_sequence = DraftSequence.current(user, @draft_key)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def canonical_path
|
|
|
|
path = @topic.relative_url
|
|
|
|
path << if @post_number
|
|
|
|
page = ((@post_number.to_i - 1) / SiteSetting.posts_per_page) + 1
|
|
|
|
(page > 1) ? "?page=#{page}" : ""
|
|
|
|
else
|
|
|
|
(@page && @page.to_i > 1) ? "?page=#{@page}" : ""
|
|
|
|
end
|
|
|
|
path
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def next_page
|
|
|
|
last_post = @posts.last
|
|
|
|
if last_post.present? and (@topic.highest_post_number > last_post.post_number)
|
|
|
|
(@posts[0].post_number / SiteSetting.posts_per_page) + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def next_page_path
|
|
|
|
"#{@topic.relative_url}?page=#{next_page}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def relative_url
|
|
|
|
@topic.relative_url
|
|
|
|
end
|
|
|
|
|
|
|
|
def title
|
|
|
|
@topic.title
|
|
|
|
end
|
|
|
|
|
|
|
|
def filter_posts(opts = {})
|
|
|
|
if opts[:post_number].present?
|
|
|
|
# Get posts near a post
|
|
|
|
filter_posts_near(opts[:post_number].to_i)
|
|
|
|
elsif opts[:posts_before].present?
|
|
|
|
filter_posts_before(opts[:posts_before].to_i)
|
|
|
|
elsif opts[:posts_after].present?
|
|
|
|
filter_posts_after(opts[:posts_after].to_i)
|
|
|
|
else
|
|
|
|
# No filter? Consider it a paged view, default to page 0 which is the first segment
|
|
|
|
filter_posts_paged(opts[:page].to_i)
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
# Filter to all posts near a particular post number
|
|
|
|
def filter_posts_near(post_number)
|
|
|
|
@min, @max = post_range(post_number)
|
|
|
|
filter_posts_in_range(@min, @max)
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
def post_numbers
|
|
|
|
@post_numbers ||= @posts.order(:post_number).pluck(:post_number)
|
|
|
|
end
|
|
|
|
|
|
|
|
def filter_posts_paged(page)
|
|
|
|
page ||= 0
|
|
|
|
min = (SiteSetting.posts_per_page * page)
|
|
|
|
max = min + SiteSetting.posts_per_page
|
|
|
|
|
|
|
|
max_val = (post_numbers.length - 1)
|
2013-02-11 02:50:26 +08:00
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
# If we're off the charts, return nil
|
|
|
|
return nil if min > max_val
|
|
|
|
|
|
|
|
# Pin max to the last post
|
|
|
|
max = max_val if max > max_val
|
|
|
|
|
|
|
|
filter_posts_in_range(post_numbers[min], post_numbers[max])
|
|
|
|
end
|
|
|
|
|
|
|
|
# Filter to all posts before a particular post number
|
|
|
|
def filter_posts_before(post_number)
|
|
|
|
@initial_load = false
|
|
|
|
@max = post_number - 1
|
|
|
|
|
|
|
|
@posts = @posts.reverse_order.where("post_number < ?", post_number)
|
2013-02-11 02:50:26 +08:00
|
|
|
@posts = @posts.includes(:topic).joins(:user).limit(SiteSetting.posts_per_page)
|
2013-02-06 03:16:51 +08:00
|
|
|
@min = @max - @posts.size
|
2013-02-11 02:50:26 +08:00
|
|
|
@min = 1 if @min < 1
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Filter to all posts after a particular post number
|
|
|
|
def filter_posts_after(post_number)
|
|
|
|
@initial_load = false
|
|
|
|
@min = post_number
|
|
|
|
@posts = @posts.regular_order.where("post_number > ?", post_number)
|
2013-02-11 02:50:26 +08:00
|
|
|
@posts = @posts.includes(:topic).joins(:user).limit(SiteSetting.posts_per_page)
|
|
|
|
@max = @min + @posts.size
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def posts
|
2013-02-11 02:50:26 +08:00
|
|
|
@post_number.present? ? find_post_by_post_number : @posts
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def find_post_by_post_number
|
|
|
|
@posts.select {|post| post.post_number == @post_number.to_i }
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def read?(post_number)
|
|
|
|
read_posts_set.include?(post_number)
|
|
|
|
end
|
|
|
|
|
|
|
|
def topic_user
|
|
|
|
@topic_user ||= begin
|
|
|
|
return nil if @user.blank?
|
|
|
|
@topic.topic_users.where(user_id: @user.id).first
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def posts_count
|
|
|
|
@posts_count ||= Post.where(topic_id: @topic.id).group(:user_id).order('count_all desc').limit(24).count
|
|
|
|
end
|
|
|
|
|
|
|
|
def participants
|
|
|
|
@participants ||= begin
|
|
|
|
participants = {}
|
|
|
|
User.where(id: posts_count.map {|k,v| k}).each {|u| participants[u.id] = u}
|
|
|
|
participants
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def all_post_actions
|
|
|
|
@all_post_actions ||= PostAction.counts_for(posts, @user)
|
|
|
|
end
|
|
|
|
|
|
|
|
def voted_in_topic?
|
2013-02-11 02:50:26 +08:00
|
|
|
return false
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
# all post_actions is not the way to do this, cut down on the query, roll it up into topic if we need it
|
|
|
|
|
|
|
|
@voted_in_topic ||= begin
|
|
|
|
return false unless all_post_actions.present?
|
|
|
|
all_post_actions.values.flatten.map {|ac| ac.keys}.flatten.include?(PostActionType.Types[:vote])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def post_action_visibility
|
|
|
|
@post_action_visibility ||= begin
|
|
|
|
result = []
|
|
|
|
PostActionType.Types.each do |k, v|
|
|
|
|
result << v if Guardian.new(@user).can_see_post_actors?(@topic, v)
|
|
|
|
end
|
|
|
|
result
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def links
|
|
|
|
@links ||= @topic.links_grouped
|
|
|
|
end
|
|
|
|
|
|
|
|
def link_counts
|
|
|
|
@link_counts ||= TopicLinkClick.counts_for(@topic, posts)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Binary search for closest value
|
|
|
|
def self.closest(array, target, min, max)
|
|
|
|
return min if max <= min
|
|
|
|
return max if (max - min) == 1
|
|
|
|
|
|
|
|
middle_idx = ((min + max) / 2).floor
|
|
|
|
middle_val = array[middle_idx]
|
|
|
|
|
|
|
|
return middle_idx if target == middle_val
|
|
|
|
return closest(array, target, min, middle_idx) if middle_val > target
|
|
|
|
return closest(array, target, middle_idx, max)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Find a range of posts, allowing for gaps by deleted posts.
|
|
|
|
def post_range(post_number)
|
|
|
|
closest_index = TopicView.closest(post_numbers, post_number, 0, post_numbers.size - 1)
|
|
|
|
|
|
|
|
min_idx = closest_index - (SiteSetting.posts_per_page.to_f / 4).floor
|
|
|
|
min_idx = 0 if min_idx < 0
|
|
|
|
max_idx = min_idx + (SiteSetting.posts_per_page - 1)
|
|
|
|
if max_idx > (post_numbers.length - 1)
|
|
|
|
max_idx = post_numbers.length - 1
|
|
|
|
min_idx = max_idx - SiteSetting.posts_per_page
|
|
|
|
min_idx = 0 if min_idx < 0
|
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
[post_numbers[min_idx], post_numbers[max_idx]]
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Are we the initial page load? If so, we can return extra information like
|
|
|
|
# user post counts, etc.
|
|
|
|
def initial_load?
|
|
|
|
@initial_load
|
|
|
|
end
|
|
|
|
|
|
|
|
def suggested_topics
|
|
|
|
return nil if topic.private_message?
|
|
|
|
@suggested_topics ||= TopicQuery.new(@user).list_suggested_for(topic)
|
|
|
|
end
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def read_posts_set
|
|
|
|
@read_posts_set ||= begin
|
|
|
|
result = Set.new
|
|
|
|
return result unless @user.present?
|
|
|
|
return result unless topic_user.present?
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
posts_max = @max > (topic_user.last_read_post_number || 1 ) ? (topic_user.last_read_post_number || 1) : @max
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
PostTiming.select(:post_number)
|
|
|
|
.where("topic_id = ? AND user_id = ? AND post_number BETWEEN ? AND ?",
|
|
|
|
@topic.id, @user.id, @min, posts_max)
|
|
|
|
.each {|t| result << t.post_number}
|
|
|
|
result
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def filter_posts_in_range(min, max)
|
|
|
|
@min, @max = min, max
|
|
|
|
@posts = @posts.where("post_number between ? and ?", @min, @max).includes(:user).regular_order
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def find_topic(topic_id)
|
|
|
|
Topic.where(id: topic_id).includes(:category).first
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|