require_dependency 'guardian' require_dependency 'topic_query' require_dependency 'summarize' class TopicView attr_reader :topic, :posts, :index_offset, :index_reverse attr_accessor :draft, :draft_key, :draft_sequence def initialize(topic_id, user=nil, options={}) @topic = find_topic(topic_id) 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? && @topic.private_message? && user.blank? raise Discourse::NotLoggedIn.new end Guardian.new(user).ensure_can_see!(@topic) @post_number, @page = options[:post_number], options[:page] @filtered_posts = @topic.posts @filtered_posts = @filtered_posts.with_deleted if user.try(:admin?) @filtered_posts = @filtered_posts.best_of if options[:best_of].present? if options[:username_filters].present? usernames = options[:username_filters].map{|u| u.downcase} @filtered_posts = @filtered_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 @index_reverse = false filter_posts(options) @draft_key = @topic.draft_key @draft_sequence = DraftSequence.current(user, @draft_key) end 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 end def next_page last_post = @filtered_posts.last if last_post.present? && (@topic.highest_post_number > last_post.post_number) (@filtered_posts[0].post_number / SiteSetting.posts_per_page) + 1 end end def next_page_path "#{@topic.relative_url}?page=#{next_page}" end def absolute_url "#{Discourse.base_url}#{@topic.relative_url}" end def relative_url @topic.relative_url end def title @topic.title end def filtered_posts_count @filtered_posts_count ||= @filtered_posts.count end def summary return nil if posts.blank? Summarize.new(posts.first.cooked).summary end def image_url return nil if posts.blank? posts.first.user.small_avatar_url end def filter_posts(opts = {}) return filter_posts_near(opts[:post_number].to_i) if opts[:post_number].present? return filter_posts_before(opts[:posts_before].to_i) if opts[:posts_before].present? return filter_posts_after(opts[:posts_after].to_i) if opts[:posts_after].present? filter_posts_paged(opts[:page].to_i) end # Find the sort order for a post in the topic def sort_order_for_post_number(post_number) Post.where(topic_id: @topic.id, post_number: post_number) .with_deleted .first .try(:sort_order) end # Filter to all posts near a particular post number def filter_posts_near(post_number) # Find the closest number we have closest_post_id = @filtered_posts.order("@(post_number - #{post_number})").first.try(:id) return nil if closest_post_id.blank? closest_index = filtered_post_ids.index(closest_post_id) return nil if closest_index.blank? # Make sure to get at least one post before, even with rounding posts_before = (SiteSetting.posts_per_page.to_f / 4).floor posts_before = 1 if posts_before == 0 min_idx = closest_index - posts_before min_idx = 0 if min_idx < 0 max_idx = min_idx + (SiteSetting.posts_per_page - 1) # Get a full page even if at the end upper_limit = (filtered_post_ids.length - 1) if max_idx >= upper_limit max_idx = upper_limit min_idx = (upper_limit - SiteSetting.posts_per_page) + 1 end filter_posts_in_range(min_idx, max_idx) end def filtered_post_ids @filtered_post_ids ||= @filtered_posts.order(:sort_order).pluck(:id) end def filter_posts_paged(page) page ||= 0 min = (SiteSetting.posts_per_page * page) max = min + SiteSetting.posts_per_page filter_posts_in_range(min, max) end # Filter to all posts before a particular post number def filter_posts_before(post_number) @initial_load = false sort_order = sort_order_for_post_number(post_number) return nil unless sort_order # Find posts before the `sort_order` @posts = @filtered_posts.order('sort_order desc').where("sort_order < ?", sort_order) @index_offset = @posts.count @index_reverse = true @posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(SiteSetting.posts_per_page) end # Filter to all posts after a particular post number def filter_posts_after(post_number) @initial_load = false sort_order = sort_order_for_post_number(post_number) return nil unless sort_order @index_offset = @filtered_posts.where("sort_order <= ?", sort_order).count @posts = @filtered_posts.order('sort_order').where("sort_order > ?", sort_order) @posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(SiteSetting.posts_per_page) 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 post_counts_by_user @post_counts_by_user ||= 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: post_counts_by_user.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 # 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 # This is pending a larger refactor, that allows custom orders # for now we need to look for the highest_post_number in the stream # the cache on topics is not correct if there are deleted posts at # the end of the stream (for mods), nor is it correct for filtered # streams def highest_post_number @highest_post_number ||= @filtered_posts.maximum(:post_number) end def recent_posts @filtered_posts.by_newest.with_user.first(25) end protected def read_posts_set @read_posts_set ||= begin result = Set.new return result unless @user.present? return result unless topic_user.present? post_numbers = PostTiming.select(:post_number) .where(topic_id: @topic.id, user_id: @user.id) .where(post_number: @posts.pluck(:post_number)) .pluck(:post_number) post_numbers.each {|pn| result << pn} result end end private def filter_posts_in_range(min, max) max_index = (filtered_post_ids.length - 1) # If we're off the charts, return nil return nil if min > max_index # Pin max to the last post max = max_index if max > max_index min = 0 if min < 0 @index_offset = min # TODO: Sort might be off @posts = Post.where(id: filtered_post_ids[min..max]) .includes(:user) .includes(:reply_to_user) .order('sort_order') @posts = @posts.with_deleted if @user.try(:admin?) @posts end def find_topic(topic_id) Topic.where(id: topic_id).includes(:category).first end end