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={}) @topic = Topic.where(id: topic_id).includes(:category).first 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 Guardian.new(user).ensure_can_see!(@topic) @min, @max = 1, SiteSetting.posts_per_page @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 end # 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 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 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) # 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) @posts = @posts.includes(:topic).joins(:user).limit(SiteSetting.posts_per_page) @min = @max - @posts.size @min = 1 if @min < 1 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) @posts = @posts.includes(:topic).joins(:user).limit(SiteSetting.posts_per_page) @max = @min + @posts.size end def posts @posts end 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? return false # 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 [post_numbers[min_idx], post_numbers[max_idx]] 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 def read_posts_set @read_posts_set ||= begin result = Set.new return result unless @user.present? return result unless topic_user.present? posts_max = @max > (topic_user.last_read_post_number || 1 ) ? (topic_user.last_read_post_number || 1) : @max 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 end end end