Class: TopicView

Inherits:
Object
  • Object
show all
Defined in:
lib/topic_view.rb

Constant Summary collapse

MEGA_TOPIC_POSTS_COUNT =
10_000
MIN_POST_READ_TIME =
4.0
CHUNK_SIZE =
20
MAX_PARTICIPANTS =
24
MAX_POSTS_COUNT_PARTICIPANTS =

if a topic has more that N posts no longer attempt to get accurate participant count, instead grab cached count from topic

500

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(topic_or_topic_id, user = nil, options = {}) ⇒ TopicView

Returns a new instance of TopicView.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/topic_view.rb', line 106

def initialize(topic_or_topic_id, user = nil, options = {})
  @topic = find_topic(topic_or_topic_id)
  @user = user
  @guardian = Guardian.new(@user)

  check_and_raise_exceptions(options[:skip_staff_action])

  @message_bus_last_id = MessageBus.last_id("/topic/#{@topic.id}")

  options.each { |key, value| self.instance_variable_set("@#{key}".to_sym, value) }

  @post_number = [@post_number.to_i, 1].max

  @include_suggested = options.fetch(:include_suggested) { true }
  @include_related = options.fetch(:include_related) { true }

  @chunk_size =
    case
    when @print
      TopicView.print_chunk_size
    else
      TopicView.chunk_size
    end

  @limit ||= @chunk_size

  @page = @page.to_i > 1 ? @page.to_i : calculate_page

  setup_filtered_posts
  @filtered_posts = apply_default_scope(@filtered_posts)
  filter_posts(options)

  if @posts && !@skip_custom_fields
    if (added_fields = User.allowed_user_custom_fields(@guardian)).present?
      @user_custom_fields = User.custom_fields_for_ids(@posts.map(&:user_id), added_fields)
    end

    if (allowed_fields = TopicView.allowed_post_custom_fields(@user, @topic)).present?
      @post_custom_fields = Post.custom_fields_for_ids(@posts.map(&:id), allowed_fields)
    end
  end

  TopicView.preload(self)

  @draft_key = @topic.draft_key
  @draft_sequence = DraftSequence.current(@user, @draft_key)

  @can_review_topic = @guardian.can_review_topic?(@topic)
  @queued_posts_enabled = NewPostManager.queue_enabled? || category_require_reply_approval?
  @personal_message = @topic.private_message?
end

Instance Attribute Details

#can_review_topicObject (readonly)

Returns the value of attribute can_review_topic.



22
23
24
# File 'lib/topic_view.rb', line 22

def can_review_topic
  @can_review_topic
end

#chunk_sizeObject (readonly)

Returns the value of attribute chunk_size.



22
23
24
# File 'lib/topic_view.rb', line 22

def chunk_size
  @chunk_size
end

#draftObject

Returns the value of attribute draft.



37
38
39
# File 'lib/topic_view.rb', line 37

def draft
  @draft
end

#draft_keyObject

Returns the value of attribute draft_key.



37
38
39
# File 'lib/topic_view.rb', line 37

def draft_key
  @draft_key
end

#draft_sequenceObject

Returns the value of attribute draft_sequence.



37
38
39
# File 'lib/topic_view.rb', line 37

def draft_sequence
  @draft_sequence
end

#filtered_postsObject (readonly)

Returns the value of attribute filtered_posts.



22
23
24
# File 'lib/topic_view.rb', line 22

def filtered_posts
  @filtered_posts
end

#guardianObject (readonly)

Returns the value of attribute guardian.



22
23
24
# File 'lib/topic_view.rb', line 22

def guardian
  @guardian
end

#include_suggestedObject

Returns the value of attribute include_suggested.



37
38
39
# File 'lib/topic_view.rb', line 37

def include_suggested
  @include_suggested
end

#message_bus_last_idObject (readonly)

Returns the value of attribute message_bus_last_id.



22
23
24
# File 'lib/topic_view.rb', line 22

def message_bus_last_id
  @message_bus_last_id
end

#personal_messageObject (readonly)

Returns the value of attribute personal_message.



22
23
24
# File 'lib/topic_view.rb', line 22

def personal_message
  @personal_message
end

#post_custom_fieldsObject

Returns the value of attribute post_custom_fields.



37
38
39
# File 'lib/topic_view.rb', line 37

def post_custom_fields
  @post_custom_fields
end

#post_numberObject

Returns the value of attribute post_number.



37
38
39
# File 'lib/topic_view.rb', line 37

def post_number
  @post_number
end

#postsObject (readonly)

Returns the value of attribute posts.



22
23
24
# File 'lib/topic_view.rb', line 22

def posts
  @posts
end

Returns the value of attribute print.



22
23
24
# File 'lib/topic_view.rb', line 22

def print
  @print
end

#queued_posts_enabledObject (readonly) Also known as: queued_posts_enabled?

Returns the value of attribute queued_posts_enabled.



22
23
24
# File 'lib/topic_view.rb', line 22

def queued_posts_enabled
  @queued_posts_enabled
end

#topicObject (readonly)

Returns the value of attribute topic.



22
23
24
# File 'lib/topic_view.rb', line 22

def topic
  @topic
end

#user_custom_fieldsObject

Returns the value of attribute user_custom_fields.



37
38
39
# File 'lib/topic_view.rb', line 37

def user_custom_fields
  @user_custom_fields
end

Class Method Details

.add_custom_filter(key, &blk) ⇒ Object



79
80
81
# File 'lib/topic_view.rb', line 79

def self.add_custom_filter(key, &blk)
  custom_filters[key] = blk
end

.add_post_custom_fields_allowlister(&block) ⇒ Object



69
70
71
# File 'lib/topic_view.rb', line 69

def self.add_post_custom_fields_allowlister(&block)
  post_custom_fields_allowlisters << block
end

.allowed_post_custom_fields(user, topic) ⇒ Object



73
74
75
76
77
# File 'lib/topic_view.rb', line 73

def self.allowed_post_custom_fields(user, topic)
  wpcf =
    default_post_custom_fields + post_custom_fields_allowlisters.map { |w| w.call(user, topic) }
  wpcf.flatten.uniq
end

.apply_custom_default_scope(&block) ⇒ Object

Configure a default scope to be applied to @filtered_posts. The registered block is called with @filtered_posts and an instance of ‘TopicView`.

This API should be considered experimental until it is exposed in ‘Plugin::Instance`.



93
94
95
# File 'lib/topic_view.rb', line 93

def self.apply_custom_default_scope(&block)
  custom_default_scopes << block
end

.cancel_preload(&blk) ⇒ Object



11
12
13
14
15
16
# File 'lib/topic_view.rb', line 11

def self.cancel_preload(&blk)
  if @preload
    @preload.delete blk
    @preload = nil if @preload.length == 0
  end
end

.chunk_sizeObject



57
58
59
# File 'lib/topic_view.rb', line 57

def self.chunk_size
  CHUNK_SIZE
end

.custom_default_scopesObject



97
98
99
# File 'lib/topic_view.rb', line 97

def self.custom_default_scopes
  @custom_default_scopes ||= []
end

.custom_filtersObject



83
84
85
# File 'lib/topic_view.rb', line 83

def self.custom_filters
  @custom_filters ||= {}
end

.default_post_custom_fieldsObject



61
62
63
# File 'lib/topic_view.rb', line 61

def self.default_post_custom_fields
  @default_post_custom_fields ||= [Post::NOTICE, "action_code_who", "action_code_path"]
end

.on_preload(&blk) ⇒ Object



7
8
9
# File 'lib/topic_view.rb', line 7

def self.on_preload(&blk)
  (@preload ||= Set.new) << blk
end

.post_custom_fields_allowlistersObject



65
66
67
# File 'lib/topic_view.rb', line 65

def self.post_custom_fields_allowlisters
  @post_custom_fields_allowlisters ||= Set.new
end

.preload(topic_view) ⇒ Object



18
19
20
# File 'lib/topic_view.rb', line 18

def self.preload(topic_view)
  @preload.each { |preload| preload.call(topic_view) } if @preload
end


51
52
53
# File 'lib/topic_view.rb', line 51

def self.print_chunk_size
  1000
end

.reset_custom_default_scopesObject

For testing



102
103
104
# File 'lib/topic_view.rb', line 102

def self.reset_custom_default_scopes
  @custom_default_scopes = nil
end

Instance Method Details

#absolute_urlObject



296
297
298
# File 'lib/topic_view.rb', line 296

def absolute_url
  "#{Discourse.base_url_no_prefix}#{relative_url}"
end

#actions_summaryObject



681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/topic_view.rb', line 681

def actions_summary
  return @actions_summary unless @actions_summary.nil?

  @actions_summary = []
  return @actions_summary unless post = posts&.first
  post_action_type_view.topic_flag_types.each do |sym, id|
    @actions_summary << {
      id: id,
      count: 0,
      hidden: false,
      can_act:
        @guardian.post_can_act?(
          post,
          sym,
          opts: {
            post_action_type_view: post_action_type_view,
          },
        ),
    }
  end

  @actions_summary
end

#all_post_actionsObject



627
628
629
# File 'lib/topic_view.rb', line 627

def all_post_actions
  @all_post_actions ||= PostAction.counts_for(@posts, @user)
end

#bookmarksObject



505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/topic_view.rb', line 505

def bookmarks
  return [] if @user.blank?
  return [] if @topic.trashed?

  @bookmarks ||=
    Bookmark.for_user_in_topic(@user, @topic.id).select(
      :id,
      :bookmarkable_id,
      :bookmarkable_type,
      :reminder_at,
      :name,
      :auto_delete_preference,
    )
end

#canonical_pathObject



233
234
235
236
237
238
239
# File 'lib/topic_view.rb', line 233

def canonical_path
  if SiteSetting.embed_set_canonical_url
    topic_embed = topic.topic_embed
    return topic_embed.embed_url if topic_embed
  end
  current_page_path
end

#categoriesObject



843
844
845
846
847
# File 'lib/topic_view.rb', line 843

def categories
  @categories ||= [category&.parent_category, category, suggested_topics&.categories].flatten
    .uniq
    .compact
end

#category_group_moderator_user_idsObject



604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'lib/topic_view.rb', line 604

def category_group_moderator_user_ids
  @category_group_moderator_user_ids ||=
    begin
      if SiteSetting.enable_category_group_moderation? && @topic.category.present?
        posts_user_ids = Set.new(@posts.map(&:user_id))
        Set.new(
          GroupUser
            .joins(
              "INNER JOIN category_moderation_groups ON category_moderation_groups.group_id = group_users.group_id",
            )
            .where(
              "category_moderation_groups.category_id": @topic.category.id,
              user_id: posts_user_ids,
            )
            .distinct
            .pluck(:user_id),
        )
      else
        Set.new
      end
    end
end

#contains_gaps?Boolean

Returns:

  • (Boolean)


249
250
251
# File 'lib/topic_view.rb', line 249

def contains_gaps?
  @contains_gaps
end

#crawler_postsObject



349
350
351
352
353
354
355
# File 'lib/topic_view.rb', line 349

def crawler_posts
  if single_post_request?
    [desired_post]
  else
    posts
  end
end

#current_page_pathObject



241
242
243
244
245
246
247
# File 'lib/topic_view.rb', line 241

def current_page_path
  if @page > 1
    "#{relative_url}?page=#{@page}"
  else
    relative_url
  end
end

#current_post_numberObject



810
811
812
813
814
# File 'lib/topic_view.rb', line 810

def current_post_number
  if highest_post_number.present?
    post_number > highest_post_number ? highest_post_number : post_number
  end
end

#desired_postObject



340
341
342
343
344
345
346
347
# File 'lib/topic_view.rb', line 340

def desired_post
  return @desired_post if @desired_post.present?
  return nil if posts.blank?

  @desired_post = posts.detect { |p| p.post_number == @post_number }
  @desired_post ||= posts.first
  @desired_post
end

#filter_best(max, opts = {}) ⇒ Object



474
475
476
477
478
# File 'lib/topic_view.rb', line 474

def filter_best(max, opts = {})
  filter = FilterBestPosts.new(@topic, @filtered_posts, max, opts)
  @posts = filter.posts
  @filtered_posts = filter.filtered_posts
end

#filter_posts(opts = {}) ⇒ Object



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/topic_view.rb', line 398

def filter_posts(opts = {})
  if opts[:post_number].present?
    filter_posts_near(opts[:post_number].to_i)
  elsif opts[:post_ids].present?
    filter_posts_by_ids(opts[:post_ids])
  elsif opts[:filter_post_number].present?
    # Only used for megatopics where we do not load the entire post stream
    filter_posts_by_post_number(opts[:filter_post_number], opts[:asc])
  elsif opts[:best].present?
    # Only used for wordpress
    filter_best(opts[:best], opts)
  else
    filter_posts_paged(@page)
  end
end

#filter_posts_near(post_number) ⇒ Object

Filter to all posts near a particular post number



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/topic_view.rb', line 431

def filter_posts_near(post_number)
  posts_before = (@limit.to_f / 4).floor
  posts_before = 1 if posts_before.zero?
  sort_order = get_sort_order(post_number)

  before_post_ids =
    @filtered_posts
      .reverse_order
      .where("posts.sort_order < ?", sort_order)
      .limit(posts_before)
      .pluck(:id)

  post_ids =
    before_post_ids +
      @filtered_posts
        .where("posts.sort_order >= ?", sort_order)
        .limit(@limit - before_post_ids.length)
        .pluck(:id)

  if post_ids.length < @limit
    post_ids =
      post_ids +
        @filtered_posts
          .reverse_order
          .where("posts.sort_order < ?", sort_order)
          .offset(before_post_ids.length)
          .limit(@limit - post_ids.length)
          .pluck(:id)
  end

  filter_posts_by_ids(post_ids)
end

#filter_posts_paged(page) ⇒ Object



464
465
466
467
468
469
470
471
472
# File 'lib/topic_view.rb', line 464

def filter_posts_paged(page)
  page = [page, 1].max
  min = @limit * (page - 1)

  # Sometimes we don't care about the OP, for example when embedding comments
  min = 1 if min == 0 && @exclude_first

  filter_posts_by_ids(@filtered_posts.offset(min).limit(@limit).pluck(:id))
end

#filtered_post_id(post_number) ⇒ Object



798
799
800
# File 'lib/topic_view.rb', line 798

def filtered_post_id(post_number)
  @filtered_posts.where(post_number: post_number).pick(:id)
end

#filtered_post_idsObject



776
777
778
779
780
781
782
783
784
785
# File 'lib/topic_view.rb', line 776

def filtered_post_ids
  @filtered_post_ids ||=
    filtered_post_stream.map do |tuple|
      if is_mega_topic?
        tuple
      else
        tuple[0]
      end
    end
end

#filtered_post_streamObject

Returns an array of [id, days_ago] tuples. ‘days_ago` is there for the timeline calculations.



762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'lib/topic_view.rb', line 762

def filtered_post_stream
  @filtered_post_stream ||=
    begin
      posts = @filtered_posts
      columns = [:id]

      if !is_mega_topic?
        columns << "(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP - posts.created_at) / 86400)::INT AS days_ago"
      end

      posts.pluck(*columns)
    end
end

#gapsObject



253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/topic_view.rb', line 253

def gaps
  return unless @contains_gaps

  @gaps ||=
    begin
      if is_mega_topic?
        nil
      else
        Gaps.new(filtered_post_ids, apply_default_scope(unfiltered_posts).pluck(:id))
      end
    end
end

#group_allowed_user_idsObject



597
598
599
600
601
602
# File 'lib/topic_view.rb', line 597

def group_allowed_user_ids
  return @group_allowed_user_ids unless @group_allowed_user_ids.nil?

  @group_allowed_user_ids =
    GroupUser.where(group_id: topic_allowed_group_ids).pluck("distinct user_id")
end

#has_bookmarks?Boolean

Returns:

  • (Boolean)


501
502
503
# File 'lib/topic_view.rb', line 501

def has_bookmarks?
  bookmarks.any?
end

#has_deleted?Boolean

Returns:

  • (Boolean)


485
486
487
488
489
490
491
# File 'lib/topic_view.rb', line 485

def has_deleted?
  @predelete_filtered_posts
    .with_deleted
    .where("posts.deleted_at IS NOT NULL")
    .where("posts.post_number > 1")
    .exists?
end

#highest_post_numberObject

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



752
753
754
# File 'lib/topic_view.rb', line 752

def highest_post_number
  @highest_post_number ||= @filtered_posts.maximum(:post_number)
end

#image_urlObject



393
394
395
396
# File 'lib/topic_view.rb', line 393

def image_url
  return @topic.image_url if @post_number == 1
  desired_post&.image_url
end

#is_mega_topic?Boolean

Returns:

  • (Boolean)


802
803
804
# File 'lib/topic_view.rb', line 802

def is_mega_topic?
  @is_mega_topic ||= (@topic.posts_count >= MEGA_TOPIC_POSTS_COUNT)
end

#last_postObject



266
267
268
269
# File 'lib/topic_view.rb', line 266

def last_post
  return nil if @posts.blank?
  @last_post ||= @posts.last
end

#last_post_idObject



806
807
808
# File 'lib/topic_view.rb', line 806

def last_post_id
  @filtered_posts.reverse_order.pick(:id)
end

#like_countObject



379
380
381
382
# File 'lib/topic_view.rb', line 379

def like_count
  return nil if @post_number > 1 # only show for topic URLs
  @topic.like_count
end


705
706
707
708
709
710
711
712
713
714
715
# File 'lib/topic_view.rb', line 705

def link_counts
  # Normal memoizations doesn't work in nil cases, so using the ol' `defined?` trick
  # to memoize more safely, as a modifier could nil this out.
  return @link_counts if defined?(@link_counts)

  @link_counts =
    DiscoursePluginRegistry.apply_modifier(
      :topic_view_link_counts,
      TopicLink.counts_for(@guardian, @topic, posts),
    )
end


631
632
633
# File 'lib/topic_view.rb', line 631

def links
  @links ||= TopicLink.topic_map(@guardian, @topic.id)
end

#mentioned_usersObject



824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
# File 'lib/topic_view.rb', line 824

def mentioned_users
  @mentioned_users ||=
    begin
      mentions = @posts.to_h { |p| [p.id, p.mentions] }.reject { |_, v| v.empty? }
      usernames = mentions.values
      usernames.flatten!
      usernames.uniq!

      users = User.where(username_lower: usernames)
      users = users.includes(:user_option, :user_status) if SiteSetting.enable_user_status
      users = users.index_by(&:username_lower)

      mentions.reduce({}) do |hash, (post_id, post_mentioned_usernames)|
        hash[post_id] = post_mentioned_usernames.map { |username| users[username] }.compact
        hash
      end
    end
end

#next_pageObject



275
276
277
278
279
280
281
282
# File 'lib/topic_view.rb', line 275

def next_page
  @next_page ||=
    begin
      if last_post && highest_post_number && (highest_post_number > last_post.post_number)
        @page + 1
      end
    end
end

#next_page_pathObject



292
293
294
# File 'lib/topic_view.rb', line 292

def next_page_path
  "#{relative_url}?page=#{next_page}"
end

#page_titleObject



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/topic_view.rb', line 304

def page_title
  title = @topic.title
  if @post_number > 1
    title += " - "
    post = @topic.posts.find_by(post_number: @post_number)
    author = post&.user
    if author && @guardian.can_see_post?(post)
      title +=
        I18n.t(
          "inline_oneboxer.topic_page_title_post_number_by_user",
          post_number: @post_number,
          username: author.username,
        )
    else
      title += I18n.t("inline_oneboxer.topic_page_title_post_number", post_number: @post_number)
    end
  elsif @page > 1
    title += " - #{I18n.t("page_num", num: @page)}"
  end

  if SiteSetting.topic_page_title_includes_category
    if @topic.category_id != SiteSetting.uncategorized_category_id && @topic.category_id &&
         @topic.category
      title += " - #{@topic.category.name}"
    elsif SiteSetting.tagging_enabled && visible_tags.exists?
      title +=
        " - #{visible_tags.order("tags.#{Tag.topic_count_column(@guardian)} DESC").first.name}"
    end
  end
  title
end

#participant_countObject



557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/topic_view.rb', line 557

def participant_count
  @participant_count ||=
    begin
      if participants.size == MAX_PARTICIPANTS
        if @topic.posts_count > MAX_POSTS_COUNT_PARTICIPANTS
          @topic.participant_count
        else
          sql = <<~SQL
            SELECT COUNT(DISTINCT user_id)
            FROM posts
            WHERE id IN (:post_ids)
            AND user_id IS NOT NULL
          SQL
          DB.query_single(sql, post_ids: unfiltered_post_ids).first.to_i
        end
      else
        participants.size
      end
    end
end

#participantsObject



578
579
580
581
582
583
584
585
586
587
588
# File 'lib/topic_view.rb', line 578

def participants
  @participants ||=
    begin
      participants = {}
      User
        .where(id: post_counts_by_user.keys)
        .includes(:primary_group, :flair_group)
        .each { |u| participants[u.id] = u }
      participants
    end
end

#pending_postsObject



672
673
674
675
# File 'lib/topic_view.rb', line 672

def pending_posts
  @pending_posts ||=
    ReviewableQueuedPost.pending.where(target_created_by: @user, topic: @topic).order(:created_at)
end

#pm_paramsObject



717
718
719
# File 'lib/topic_view.rb', line 717

def pm_params
  @pm_params ||= TopicQuery.new(@user).get_pm_params(topic)
end

#post_action_type_viewObject



677
678
679
# File 'lib/topic_view.rb', line 677

def post_action_type_view
  @post_action_type_view ||= PostActionTypeView.new
end

#post_counts_by_userObject



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'lib/topic_view.rb', line 522

def post_counts_by_user
  @post_counts_by_user ||=
    begin
      if is_mega_topic?
        {}
      else
        sql = <<~SQL
          SELECT user_id, count(*) AS count_all
            FROM posts
           WHERE topic_id = :topic_id
             AND post_type IN (:post_types)
             AND user_id IS NOT NULL
             AND posts.deleted_at IS NULL
             AND action_code IS NULL
        GROUP BY user_id
        ORDER BY count_all DESC
           LIMIT #{MAX_PARTICIPANTS}
      SQL

        Hash[
          *DB.query_single(
            sql,
            topic_id: @topic.id,
            post_types: Topic.visible_post_types(@guardian&.user),
          )
        ]
      end
    end
end

#post_user_badgesObject



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/topic_view.rb', line 208

def post_user_badges
  return [] unless SiteSetting.enable_badges && SiteSetting.show_badges_in_post_header

  @post_user_badges ||=
    begin
      UserBadge
        .for_post_header_badges(@posts)
        .reduce({}) do |hash, user_badge|
          hash[user_badge.post_id] ||= []
          hash[user_badge.post_id] << user_badge
          hash
        end
    end

  return [] unless @post_user_badges

  @post_user_badges
end

#prev_pageObject



271
272
273
# File 'lib/topic_view.rb', line 271

def prev_page
  @page > 1 && posts.size > 0 ? @page - 1 : nil
end

#prev_page_pathObject



284
285
286
287
288
289
290
# File 'lib/topic_view.rb', line 284

def prev_page_path
  if prev_page > 1
    "#{relative_url}?page=#{prev_page}"
  else
    relative_url
  end
end

#primary_group_namesObject



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/topic_view.rb', line 414

def primary_group_names
  return @group_names if @group_names

  primary_group_ids = Set.new
  @posts.each do |p|
    primary_group_ids << p.user.primary_group_id if p.user.try(:primary_group_id)
  end

  result = {}
  unless primary_group_ids.empty?
    Group.where(id: primary_group_ids.to_a).pluck(:id, :name).each { |g| result[g[0]] = g[1] }
  end

  @group_names = result
end

#published_pageObject



820
821
822
# File 'lib/topic_view.rb', line 820

def published_page
  @topic.published_page
end

#published_timeObject



384
385
386
387
388
389
390
391
# File 'lib/topic_view.rb', line 384

def published_time
  return nil if desired_post.blank?
  if desired_post.wiki && desired_post.post_number == 1 && desired_post.revisions.size > 0
    desired_post.revisions.last.updated_at.strftime("%FT%T%:z")
  else
    desired_post.created_at.strftime("%FT%T%:z")
  end
end

#queued_posts_countObject



816
817
818
# File 'lib/topic_view.rb', line 816

def queued_posts_count
  ReviewableQueuedPost.viewable_by(@user).where(topic_id: @topic.id).pending.count
end

#read?(post_number) ⇒ Boolean

Returns:

  • (Boolean)


480
481
482
483
# File 'lib/topic_view.rb', line 480

def read?(post_number)
  return true unless @user
  read_posts_set.include?(post_number)
end

#read_timeObject



368
369
370
371
372
373
374
375
376
377
# File 'lib/topic_view.rb', line 368

def read_time
  return nil if @post_number > 1 # only show for topic URLs

  if @topic.word_count && SiteSetting.read_time_word_count > 0
    [
      @topic.word_count / SiteSetting.read_time_word_count,
      @topic.posts_count * MIN_POST_READ_TIME / 60,
    ].max.ceil
  end
end

#recent_postsObject



756
757
758
# File 'lib/topic_view.rb', line 756

def recent_posts
  @filtered_posts.unscope(:order).by_newest.with_user.first(25)
end


739
740
741
742
743
744
745
# File 'lib/topic_view.rb', line 739

def related_messages
  if @include_related
    @related_messages ||= TopicQuery.new(@user).list_related_for(topic, pm_params: pm_params)
  else
    nil
  end
end

#relative_urlObject



300
301
302
# File 'lib/topic_view.rb', line 300

def relative_url
  "#{@topic.relative_url}#{@print ? "/print" : ""}"
end

#reviewable_countsObject



635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
# File 'lib/topic_view.rb', line 635

def reviewable_counts
  @reviewable_counts ||=
    begin
      sql = <<~SQL
      SELECT
        target_id,
        MAX(r.id) reviewable_id,
        COUNT(*) total,
        SUM(CASE WHEN s.status = :pending THEN 1 ELSE 0 END) pending
      FROM
        reviewables r
      JOIN
        reviewable_scores s ON reviewable_id = r.id
      WHERE
        r.target_id IN (:post_ids) AND
        r.target_type = 'Post' AND
        COALESCE(s.reason, '') != 'category'
      GROUP BY
        target_id
    SQL

      counts = {}

      DB
        .query(sql, pending: ReviewableScore.statuses[:pending], post_ids: @posts.map(&:id))
        .each do |row|
          counts[row.target_id] = {
            total: row.total,
            pending: row.pending,
            reviewable_id: row.reviewable_id,
          }
        end

      counts
    end
end

#show_read_indicator?Boolean

Returns:

  • (Boolean)


227
228
229
230
231
# File 'lib/topic_view.rb', line 227

def show_read_indicator?
  return false if !@user || !topic.private_message?

  topic.allowed_groups.any? { |group| group.publish_read_state? && group.users.include?(@user) }
end

#single_post_request?Boolean

Returns:

  • (Boolean)


357
358
359
# File 'lib/topic_view.rb', line 357

def single_post_request?
  @post_number && @post_number != 1
end

#suggested_topicsObject



721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# File 'lib/topic_view.rb', line 721

def suggested_topics
  if @include_suggested
    @suggested_topics ||=
      begin
        kwargs =
          DiscoursePluginRegistry.apply_modifier(
            :topic_view_suggested_topics_options,
            { include_random: true, pm_params: pm_params },
            self,
          )

        TopicQuery.new(@user).list_suggested_for(topic, **kwargs)
      end
  else
    nil
  end
end

#summary(opts = {}) ⇒ Object



361
362
363
364
365
366
# File 'lib/topic_view.rb', line 361

def summary(opts = {})
  return nil if desired_post.blank?
  # TODO, this is actually quite slow, should be cached in the post table
  excerpt = desired_post.excerpt(500, opts.merge(strip_links: true, text_entities: true))
  (excerpt || "").gsub(/\n/, " ").strip
end

#titleObject



336
337
338
# File 'lib/topic_view.rb', line 336

def title
  @topic.title
end

#topic_allowed_group_idsObject



590
591
592
593
594
595
# File 'lib/topic_view.rb', line 590

def topic_allowed_group_ids
  @topic_allowed_group_ids ||=
    begin
      @topic.allowed_groups.map(&:id)
    end
end

#topic_userObject



493
494
495
496
497
498
499
# File 'lib/topic_view.rb', line 493

def topic_user
  @topic_user ||=
    begin
      return nil if @user.blank?
      @topic.topic_users.find_by(user_id: @user.id)
    end
end

#unfiltered_post_idsObject



787
788
789
790
791
792
793
794
795
796
# File 'lib/topic_view.rb', line 787

def unfiltered_post_ids
  @unfiltered_post_ids ||=
    begin
      if @contains_gaps
        unfiltered_posts.pluck(:id)
      else
        filtered_post_ids
      end
    end
end

#user_badges(badge_names) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/topic_view.rb', line 158

def user_badges(badge_names)
  return if !badge_names.present?

  user_ids = Set.new
  posts.each { |post| user_ids << post.user_id if post.user_id }

  return if !user_ids.present?

  badges =
    Badge.where("LOWER(name) IN (?)", badge_names.map(&:downcase)).where(enabled: true).to_a

  sql = <<~SQL
   SELECT user_id, badge_id
   FROM user_badges
   WHERE user_id IN (:user_ids) AND badge_id IN (:badge_ids)
   GROUP BY user_id, badge_id
   ORDER BY user_id, badge_id
 SQL

  user_badges = DB.query(sql, user_ids: user_ids, badge_ids: badges.map(&:id))

  user_badge_mapping = {}
  user_badges.each do |user_badge|
    user_badge_mapping[user_badge.user_id] ||= []
    user_badge_mapping[user_badge.user_id] << user_badge.badge_id
  end

  indexed_badges = {}

  badges.each do |badge|
    indexed_badges[badge.id] = {
      id: badge.id,
      name: badge.name,
      slug: badge.slug,
      description: badge.description,
      icon: badge.icon,
      image_url: badge.image_url,
      badge_grouping_id: badge.badge_grouping_id,
      badge_type_id: badge.badge_type_id,
    }
  end

  user_badge_mapping =
    user_badge_mapping
      .map { |user_id, badge_ids| [user_id, { id: user_id, badge_ids: badge_ids }] }
      .to_h

  { users: user_badge_mapping, badges: indexed_badges }
end