2019-05-02 18:17:27 -04:00
# frozen_string_literal: true
2013-02-05 14:16:51 -05:00
require_dependency 'slug'
require_dependency 'avatar_lookup'
require_dependency 'topic_view'
require_dependency 'rate_limiter'
2013-02-06 20:09:31 -05:00
require_dependency 'text_sentinel'
2013-04-10 05:00:50 -04:00
require_dependency 'text_cleaner'
2013-11-06 15:05:06 -05:00
require_dependency 'archetype'
2015-09-23 23:37:53 -04:00
require_dependency 'html_prettify'
2016-04-25 15:55:15 -04:00
require_dependency 'discourse_tagging'
2017-09-26 05:25:32 -04:00
require_dependency 'search_indexer'
2017-09-15 01:02:11 -04:00
require_dependency 'list_controller'
require_dependency 'topic_posters_summary'
require_dependency 'topic_featured_users'
2013-02-05 14:16:51 -05:00
class Topic < ActiveRecord :: Base
2017-05-02 05:43:33 -04:00
class UserExists < StandardError ; end
2013-07-03 15:43:29 -04:00
include ActionView :: Helpers :: SanitizeHelper
2013-02-05 14:16:51 -05:00
include RateLimiter :: OnCreateRecord
2014-04-28 04:31:51 -04:00
include HasCustomFields
2013-05-07 00:39:01 -04:00
include Trashable
2017-08-15 11:46:57 -04:00
include Searchable
2015-02-25 14:53:21 -05:00
include LimitedEdit
2013-08-30 05:12:44 -04:00
extend Forwardable
def_delegator :featured_users , :user_ids , :featured_user_ids
def_delegator :featured_users , :choose , :feature_topic_users
def_delegator :notifier , :watch! , :notify_watch!
2015-12-14 17:17:09 -05:00
def_delegator :notifier , :track! , :notify_tracking!
2013-08-30 05:12:44 -04:00
def_delegator :notifier , :regular! , :notify_regular!
2015-12-14 17:17:09 -05:00
def_delegator :notifier , :mute! , :notify_muted!
2013-08-30 05:12:44 -04:00
def_delegator :notifier , :toggle_mute , :toggle_mute
2013-02-05 14:16:51 -05:00
2018-03-28 15:36:12 -04:00
attr_accessor :allowed_user_ids , :tags_changed , :includes_destination_category
2014-05-12 03:32:49 -04:00
2017-10-17 16:37:13 -04:00
def self . max_fancy_title_length
400
end
2013-08-26 06:41:56 -04:00
def featured_users
@featured_users || = TopicFeaturedUsers . new ( self )
2013-03-12 12:33:42 -04:00
end
2013-02-05 14:16:51 -05:00
2017-07-27 21:20:09 -04:00
def trash! ( trashed_by = nil )
2018-02-08 16:36:39 -05:00
if deleted_at . nil?
update_category_topic_count_by ( - 1 )
CategoryTagStat . topic_deleted ( self ) if self . tags . present?
end
2013-07-09 15:20:18 -04:00
super ( trashed_by )
2017-04-24 14:29:04 -04:00
self . topic_embed . trash! if has_topic_embed?
2013-05-07 00:39:01 -04:00
end
2019-01-03 12:03:01 -05:00
def recover! ( recovered_by = nil )
2018-02-08 16:36:39 -05:00
unless deleted_at . nil?
update_category_topic_count_by ( 1 )
CategoryTagStat . topic_recovered ( self ) if self . tags . present?
end
2019-01-03 12:03:01 -05:00
# Note parens are required because superclass doesn't take `recovered_by`
super ( )
2017-04-24 14:29:04 -04:00
unless ( topic_embed = TopicEmbed . with_deleted . find_by_topic_id ( id ) ) . nil?
topic_embed . recover!
end
2013-05-07 00:39:01 -04:00
end
2013-02-05 20:13:41 -05:00
2013-02-05 14:16:51 -05:00
rate_limit :default_rate_limiter
rate_limit :limit_topics_per_day
rate_limit :limit_private_messages_per_day
2017-07-27 21:20:09 -04:00
validates :title , if : Proc . new { | t | t . new_record? || t . title_changed? } ,
presence : true ,
topic_title_length : true ,
censored_words : true ,
quality_title : { unless : :private_message? } ,
2018-02-17 00:10:30 -05:00
max_emojis : true ,
2017-07-27 21:20:09 -04:00
unique_among : { unless : Proc . new { | t | ( SiteSetting . allow_duplicate_topic_titles? || t . private_message? ) } ,
message : :has_already_been_used ,
allow_blank : true ,
case_sensitive : false ,
collection : Proc . new { Topic . listable_topics } }
2013-02-05 14:16:51 -05:00
2014-04-23 19:19:59 -04:00
validates :category_id ,
2017-07-27 21:20:09 -04:00
presence : true ,
exclusion : {
in : Proc . new { [ SiteSetting . uncategorized_category_id ] }
2014-04-23 19:19:59 -04:00
} ,
2017-07-27 21:20:09 -04:00
if : Proc . new { | t |
( t . new_record? || t . category_id_changed? ) &&
! SiteSetting . allow_uncategorized_topics &&
2019-02-28 09:51:13 -05:00
( t . archetype . nil? || t . regular? )
2014-04-23 19:19:59 -04:00
}
2013-10-23 19:05:51 -04:00
2017-12-11 03:27:33 -05:00
validates :featured_link , allow_nil : true , url : true
2016-12-05 07:31:43 -05:00
validate if : :featured_link do
2017-08-31 00:06:56 -04:00
errors . add ( :featured_link , :invalid_category ) unless ! featured_link_changed? ||
Guardian . new . can_edit_featured_link? ( category_id )
2016-12-05 07:31:43 -05:00
end
2013-10-08 14:40:31 -04:00
2013-05-31 15:22:34 -04:00
before_validation do
2013-05-23 00:52:12 -04:00
self . title = TextCleaner . clean_title ( TextSentinel . title_sentinel ( title ) . text ) if errors [ :title ] . empty?
2017-11-22 14:53:35 -05:00
self . featured_link = self . featured_link . strip . presence if self . featured_link
2013-05-23 00:52:12 -04:00
end
2013-02-05 14:16:51 -05:00
belongs_to :category
2016-07-06 15:56:40 -04:00
has_many :category_users , through : :category
2013-02-05 14:16:51 -05:00
has_many :posts
2014-07-28 16:50:49 -04:00
has_many :ordered_posts , - > { order ( post_number : :asc ) } , class_name : " Post "
2013-02-05 14:16:51 -05:00
has_many :topic_allowed_users
2013-05-02 01:15:17 -04:00
has_many :topic_allowed_groups
2015-12-22 19:09:17 -05:00
has_many :group_archived_messages , dependent : :destroy
has_many :user_archived_messages , dependent : :destroy
2013-05-02 01:15:17 -04:00
has_many :allowed_groups , through : :topic_allowed_groups , source : :group
2017-08-31 00:06:56 -04:00
has_many :allowed_group_users , through : :allowed_groups , source : :users
2013-02-05 14:16:51 -05:00
has_many :allowed_users , through : :topic_allowed_users , source : :user
2013-03-28 13:02:59 -04:00
2018-02-08 16:36:39 -05:00
has_many :topic_tags
has_many :tags , through : :topic_tags , dependent : :destroy # dependent destroy applies to the topic_tags records
2016-07-06 15:56:40 -04:00
has_many :tag_users , through : :tags
2016-05-04 14:02:47 -04:00
2013-12-23 18:50:36 -05:00
has_one :top_topic
2018-03-13 15:59:12 -04:00
has_one :shared_draft , dependent : :destroy
2013-02-05 14:16:51 -05:00
belongs_to :user
belongs_to :last_poster , class_name : 'User' , foreign_key : :last_post_user_id
belongs_to :featured_user1 , class_name : 'User' , foreign_key : :featured_user1_id
belongs_to :featured_user2 , class_name : 'User' , foreign_key : :featured_user2_id
belongs_to :featured_user3 , class_name : 'User' , foreign_key : :featured_user3_id
belongs_to :featured_user4 , class_name : 'User' , foreign_key : :featured_user4_id
has_many :topic_users
has_many :topic_links
has_many :topic_invites
has_many :invites , through : :topic_invites , source : :invite
2017-05-11 18:23:18 -04:00
has_many :topic_timers , dependent : :destroy
2019-01-03 12:03:01 -05:00
has_many :reviewables
2013-02-05 14:16:51 -05:00
2017-04-15 00:11:02 -04:00
has_one :user_warning
2017-08-31 00:06:56 -04:00
has_one :first_post , - > { where post_number : 1 } , class_name : 'Post'
2017-08-15 11:46:57 -04:00
has_one :topic_search_data
2017-04-24 14:29:04 -04:00
has_one :topic_embed , dependent : :destroy
2013-02-05 14:16:51 -05:00
# When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data
2015-06-22 04:09:08 -04:00
2013-02-05 14:16:51 -05:00
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
2014-05-12 03:32:49 -04:00
attr_accessor :participants
2013-04-02 16:52:51 -04:00
attr_accessor :topic_list
2014-04-25 12:24:22 -04:00
attr_accessor :meta_data
2013-10-17 02:44:56 -04:00
attr_accessor :include_last_poster
2014-07-03 14:43:24 -04:00
attr_accessor :import_mode # set to true to optimize creation and save for imports
2013-02-05 14:16:51 -05:00
# The regular order
2014-05-07 13:04:39 -04:00
scope :topic_list_order , - > { order ( 'topics.bumped_at desc' ) }
2013-02-05 14:16:51 -05:00
# Return private message topics
2014-05-07 13:04:39 -04:00
scope :private_messages , - > { where ( archetype : Archetype . private_message ) }
2013-02-05 14:16:51 -05:00
2016-05-30 13:48:46 -04:00
scope :listable_topics , - > { where ( 'topics.archetype <> ?' , Archetype . private_message ) }
2013-02-05 14:16:51 -05:00
2014-02-27 23:07:55 -05:00
scope :by_newest , - > { order ( 'topics.created_at desc, topics.id desc' ) }
2013-02-27 22:36:12 -05:00
2013-07-03 16:04:22 -04:00
scope :visible , - > { where ( visible : true ) }
2013-05-30 07:23:40 -04:00
2015-01-29 10:40:26 -05:00
scope :created_since , lambda { | time_ago | where ( 'topics.created_at > ?' , time_ago ) }
2013-05-30 07:23:40 -04:00
2017-07-27 21:20:09 -04:00
scope :secured , lambda { | guardian = nil |
2013-06-08 09:52:06 -04:00
ids = guardian . secure_category_ids if guardian
2013-06-12 13:43:59 -04:00
# Query conditions
2014-05-07 13:04:39 -04:00
condition = if ids . present?
2016-05-02 09:26:23 -04:00
[ " NOT read_restricted OR id IN (:cats) " , cats : ids ]
2014-05-07 13:04:39 -04:00
else
2016-05-02 09:26:23 -04:00
[ " NOT read_restricted " ]
2014-05-07 13:04:39 -04:00
end
2013-06-12 13:43:59 -04:00
2016-05-02 09:26:23 -04:00
where ( " topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{ condition [ 0 ] } ) " , condition [ 1 ] )
2013-06-12 13:43:59 -04:00
}
2013-06-08 09:52:06 -04:00
2018-08-09 20:50:05 -04:00
IN_CATEGORY_AND_SUBCATEGORIES_SQL = << ~ SQL
t . category_id = :category_id
OR t . category_id IN ( SELECT id FROM categories WHERE categories . parent_category_id = :category_id )
SQL
scope :in_category_and_subcategories , lambda { | category_id |
where ( " topics.category_id = ? OR topics.category_id IN (SELECT id FROM categories WHERE categories.parent_category_id = ?) " ,
category_id ,
category_id ) if category_id
}
2018-03-27 04:30:08 -04:00
scope :with_subtype , - > ( subtype ) { where ( 'topics.subtype = ?' , subtype ) }
2014-10-10 12:21:44 -04:00
attr_accessor :ignore_category_auto_close
2014-10-27 17:06:43 -04:00
attr_accessor :skip_callbacks
2014-10-10 12:21:44 -04:00
2013-02-05 14:16:51 -05:00
before_create do
2014-10-27 17:06:43 -04:00
initialize_default_values
2013-02-05 14:16:51 -05:00
end
after_create do
2013-12-13 01:04:45 -05:00
unless skip_callbacks
changed_to_category ( category )
2014-10-27 17:06:43 -04:00
advance_draft_sequence
2013-02-05 14:16:51 -05:00
end
end
2013-05-07 14:25:41 -04:00
before_save do
2013-12-13 01:04:45 -05:00
unless skip_callbacks
2014-10-27 17:06:43 -04:00
ensure_topic_has_a_category
2013-10-23 19:05:51 -04:00
end
2018-09-17 18:54:44 -04:00
2015-09-23 23:37:53 -04:00
if title_changed?
2018-09-17 18:54:44 -04:00
write_attribute ( :fancy_title , Topic . fancy_title ( title ) )
2015-09-23 23:37:53 -04:00
end
2017-05-31 04:40:21 -04:00
if category_id_changed? || new_record?
inherit_auto_close_from_category
end
2013-05-07 14:25:41 -04:00
end
after_save do
2014-11-13 23:39:17 -05:00
banner = " banner " . freeze
2017-08-31 00:06:56 -04:00
if archetype_before_last_save == banner || archetype == banner
2014-11-13 23:39:17 -05:00
ApplicationController . banner_json_cache . clear
end
2016-07-07 22:58:18 -04:00
2018-02-19 22:41:00 -05:00
if tags_changed || saved_change_to_attribute? ( :category_id )
SearchIndexer . queue_post_reindex ( self . id )
if tags_changed
TagUser . auto_watch ( topic_id : id )
TagUser . auto_track ( topic_id : id )
self . tags_changed = false
end
2016-07-07 22:58:18 -04:00
end
2016-12-21 21:13:14 -05:00
SearchIndexer . index ( self )
2013-05-07 14:25:41 -04:00
end
2018-02-08 16:36:39 -05:00
after_update do
if saved_changes [ :category_id ] && self . tags . present?
CategoryTagStat . topic_moved ( self , * saved_changes [ :category_id ] )
end
end
2014-10-27 17:06:43 -04:00
def initialize_default_values
self . bumped_at || = Time . now
self . last_post_user_id || = user_id
end
2014-03-07 02:59:47 -05:00
2014-10-27 17:06:43 -04:00
def inherit_auto_close_from_category
2017-08-21 23:54:11 -04:00
if ! self . closed &&
! @ignore_category_auto_close &&
2017-03-21 23:12:02 -04:00
self . category &&
self . category . auto_close_hours &&
2017-05-16 14:49:42 -04:00
! public_topic_timer & . execute_at
2017-03-21 23:12:02 -04:00
2017-05-11 18:23:18 -04:00
self . set_or_create_timer (
TopicTimer . types [ :close ] ,
2017-03-21 23:12:02 -04:00
self . category . auto_close_hours ,
based_on_last_post : self . category . auto_close_based_on_last_post
)
2014-10-27 17:06:43 -04:00
end
end
2014-03-07 02:59:47 -05:00
2014-10-27 17:06:43 -04:00
def advance_draft_sequence
2018-03-05 02:38:05 -05:00
if self . private_message?
2014-10-27 17:06:43 -04:00
DraftSequence . next! ( user , Draft :: NEW_PRIVATE_MESSAGE )
else
DraftSequence . next! ( user , Draft :: NEW_TOPIC )
end
end
def ensure_topic_has_a_category
2018-03-05 03:18:23 -05:00
if category_id . nil? && ( archetype . nil? || self . regular? )
2018-06-05 03:29:17 -04:00
self . category_id = category & . id || SiteSetting . uncategorized_category_id
2014-10-27 17:06:43 -04:00
end
2013-12-11 21:41:34 -05:00
end
2017-07-27 21:20:09 -04:00
def self . visible_post_types ( viewed_by = nil )
2015-09-10 16:01:23 -04:00
types = Post . types
result = [ types [ :regular ] , types [ :moderator_action ] , types [ :small_action ] ]
2017-09-08 01:07:22 -04:00
result << types [ :whisper ] if viewed_by & . staff?
2015-09-10 16:01:23 -04:00
result
end
2013-11-13 12:26:32 -05:00
def self . top_viewed ( max = 10 )
2014-02-27 23:07:55 -05:00
Topic . listable_topics . visible . secured . order ( 'views desc' ) . limit ( max )
2013-11-13 12:26:32 -05:00
end
def self . recent ( max = 10 )
2014-02-27 23:07:55 -05:00
Topic . listable_topics . visible . secured . order ( 'created_at desc' ) . limit ( max )
2013-11-13 12:26:32 -05:00
end
2013-10-28 02:12:07 -04:00
def self . count_exceeds_minimum?
count > SiteSetting . minimum_topics_similar
end
2013-06-03 16:12:24 -04:00
def best_post
2016-08-19 13:19:08 -04:00
posts . where ( post_type : Post . types [ :regular ] , user_deleted : false ) . order ( 'score desc nulls last' ) . limit ( 1 ) . first
2013-06-03 16:12:24 -04:00
end
2015-02-16 07:03:04 -05:00
def has_flags?
2019-01-03 12:03:01 -05:00
ReviewableFlaggedPost . pending . default_visible . where ( topic_id : id ) . exists?
2015-02-16 07:03:04 -05:00
end
2016-04-11 08:37:28 -04:00
def is_official_warning?
subtype == TopicSubtype . moderator_warning
end
2013-05-02 01:15:17 -04:00
# all users (in groups or directly targetted) that are going to get the pm
def all_allowed_users
2016-04-11 08:37:28 -04:00
moderators_sql = " UNION #{ User . moderators . to_sql } " if private_message? && ( has_flags? || is_official_warning? )
2015-10-29 13:39:30 -04:00
User . from ( " ( #{ allowed_users . to_sql } UNION #{ allowed_group_users . to_sql } #{ moderators_sql } ) as users " )
2013-05-02 01:15:17 -04:00
end
2013-02-05 14:16:51 -05:00
# Additional rate limits on topics: per day and private messages per day
def limit_topics_per_day
2016-06-20 16:38:15 -04:00
if user && user . new_user_posting_on_first_day?
2015-09-24 12:04:41 -04:00
limit_first_day_topics_per_day
else
apply_per_day_rate_limit_for ( " topics " , :max_topics_per_day )
end
2013-02-05 14:16:51 -05:00
end
def limit_private_messages_per_day
return unless private_message?
2018-01-31 01:16:25 -05:00
apply_per_day_rate_limit_for ( " pms " , :max_personal_messages_per_day )
2013-02-05 14:16:51 -05:00
end
2015-09-23 23:37:53 -04:00
def self . fancy_title ( title )
2018-09-17 18:54:44 -04:00
return unless escaped = ERB :: Util . html_escape ( title )
2017-10-17 16:37:13 -04:00
fancy_title = Emoji . unicode_unescape ( HtmlPrettify . render ( escaped ) )
2018-09-17 18:54:44 -04:00
fancy_title . length > Topic . max_fancy_title_length ? escaped : fancy_title
2015-09-23 23:37:53 -04:00
end
2013-02-25 11:42:20 -05:00
def fancy_title
2015-09-23 23:37:53 -04:00
return ERB :: Util . html_escape ( title ) unless SiteSetting . title_fancy_entities?
2014-04-18 00:48:38 -04:00
2015-09-23 23:37:53 -04:00
unless fancy_title = read_attribute ( :fancy_title )
fancy_title = Topic . fancy_title ( title )
write_attribute ( :fancy_title , fancy_title )
2017-10-19 03:41:03 -04:00
if ! new_record? && ! Discourse . readonly_mode?
2015-09-23 23:37:53 -04:00
# make sure data is set in table, this also allows us to change algorithm
# by simply nulling this column
2018-06-19 02:13:14 -04:00
DB . exec ( " UPDATE topics SET fancy_title = :fancy_title where id = :id " , id : self . id , fancy_title : fancy_title )
2015-09-23 23:37:53 -04:00
end
end
2013-02-19 16:08:23 -05:00
2015-09-23 23:37:53 -04:00
fancy_title
2013-04-21 23:48:05 -04:00
end
2013-06-03 16:12:24 -04:00
# Returns hot topics since a date for display in email digest.
2017-07-27 21:20:09 -04:00
def self . for_digest ( user , since , opts = nil )
2014-04-17 16:42:40 -04:00
opts = opts || { }
2014-04-17 15:14:54 -04:00
score = " #{ ListController . best_period_for ( since ) } _score "
2013-11-06 15:05:06 -05:00
topics = Topic
2017-07-27 21:20:09 -04:00
. visible
. secured ( Guardian . new ( user ) )
. joins ( " LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{ user . id . to_i } " )
. joins ( " LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{ user . id . to_i } " )
. joins ( " LEFT OUTER JOIN users ON users.id = topics.user_id " )
. where ( closed : false , archived : false )
. where ( " COALESCE(topic_users.notification_level, 1) <> ? " , TopicUser . notification_levels [ :muted ] )
. created_since ( since )
2017-08-14 12:47:33 -04:00
. where ( 'topics.created_at < ?' , ( SiteSetting . editing_grace_period || 0 ) . seconds . ago )
2017-07-27 21:20:09 -04:00
. listable_topics
. includes ( :category )
2014-04-17 16:42:40 -04:00
2016-12-19 14:53:53 -05:00
unless opts [ :include_tl0 ] || user . user_option . try ( :include_tl0_in_digests )
2016-03-17 17:35:23 -04:00
topics = topics . where ( " COALESCE(users.trust_level, 0) > 0 " )
end
2014-04-17 16:42:40 -04:00
if ! ! opts [ :top_order ]
topics = topics . joins ( " LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id " )
2017-07-27 21:20:09 -04:00
. order ( TopicQuerySQL . order_top_with_notification_levels ( score ) )
2014-04-17 16:42:40 -04:00
end
if opts [ :limit ]
topics = topics . limit ( opts [ :limit ] )
end
2013-11-06 15:05:06 -05:00
2014-04-17 15:14:54 -04:00
# Remove category topics
2013-11-06 15:05:06 -05:00
category_topic_ids = Category . pluck ( :topic_id ) . compact!
if category_topic_ids . present?
2014-04-17 15:14:54 -04:00
topics = topics . where ( " topics.id NOT IN (?) " , category_topic_ids )
end
# Remove muted categories
muted_category_ids = CategoryUser . where ( user_id : user . id , notification_level : CategoryUser . notification_levels [ :muted ] ) . pluck ( :category_id )
2016-03-25 15:12:00 -04:00
if SiteSetting . digest_suppress_categories . present?
muted_category_ids += SiteSetting . digest_suppress_categories . split ( " | " ) . map ( & :to_i )
muted_category_ids = muted_category_ids . uniq
end
2014-04-17 15:14:54 -04:00
if muted_category_ids . present?
topics = topics . where ( " topics.category_id NOT IN (?) " , muted_category_ids )
2013-11-06 15:05:06 -05:00
end
2016-08-18 17:16:52 -04:00
# Remove muted tags
2016-08-08 15:14:18 -04:00
muted_tag_ids = TagUser . lookup ( user , :muted ) . pluck ( :tag_id )
unless muted_tag_ids . empty?
2017-07-04 16:12:10 -04:00
# If multiple tags per topic, include topics with tags that aren't muted,
# and don't forget untagged topics.
topics = topics . where (
" EXISTS ( SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id AND tag_id NOT IN (?) )
OR NOT EXISTS ( SELECT 1 FROM topic_tags WHERE topic_tags . topic_id = topics . id ) " , muted_tag_ids)
2016-08-08 15:14:18 -04:00
end
2013-11-06 15:05:06 -05:00
topics
2013-02-05 14:16:51 -05:00
end
2013-02-07 10:45:24 -05:00
2014-04-25 12:24:22 -04:00
def meta_data = ( data )
custom_fields . replace ( data )
end
def meta_data
custom_fields
end
2013-02-05 14:16:51 -05:00
def update_meta_data ( data )
2014-04-25 12:24:22 -04:00
custom_fields . update ( data )
2013-02-05 14:16:51 -05:00
save
end
2017-07-27 21:20:09 -04:00
def reload ( options = nil )
2013-04-21 23:48:05 -04:00
@post_numbers = nil
2017-05-16 21:37:11 -04:00
@public_topic_timer = nil
2017-10-04 23:48:42 -04:00
@private_topic_timer = nil
2018-05-24 04:41:51 -04:00
@is_category_topic = nil
2013-04-21 23:48:05 -04:00
super ( options )
end
2013-02-05 14:16:51 -05:00
def post_numbers
@post_numbers || = posts . order ( :post_number ) . pluck ( :post_number )
end
2013-12-06 16:39:35 -05:00
def age_in_minutes
( ( Time . zone . now - created_at ) / 1 . minute ) . round
2013-05-27 20:58:57 -04:00
end
2017-07-27 21:20:09 -04:00
def self . listable_count_per_day ( start_date , end_date , category_id = nil )
2018-05-10 23:30:21 -04:00
result = listable_topics . where ( " topics.created_at >= ? AND topics.created_at <= ? " , start_date , end_date )
result = result . group ( 'date(topics.created_at)' ) . order ( 'date(topics.created_at)' )
2015-06-24 09:19:39 -04:00
result = result . where ( category_id : category_id ) if category_id
2018-04-26 08:49:41 -04:00
result . count
2013-03-07 11:07:59 -05:00
end
2013-02-07 10:45:24 -05:00
def private_message?
2013-05-24 12:13:31 -04:00
archetype == Archetype . private_message
2013-02-05 14:16:51 -05:00
end
2018-03-05 03:18:23 -05:00
def regular?
self . archetype == Archetype . default
end
2017-09-15 19:03:29 -04:00
MAX_SIMILAR_BODY_LENGTH || = 200
2017-07-27 21:20:09 -04:00
def self . similar_to ( title , raw , user = nil )
2017-09-15 19:03:29 -04:00
return [ ] if title . blank?
raw = raw . presence || " "
2013-03-14 14:45:29 -04:00
2017-09-15 19:03:29 -04:00
search_data = " #{ title } #{ raw [ 0 ... MAX_SIMILAR_BODY_LENGTH ] } " . strip
filter_words = Search . prepare_data ( search_data )
2018-02-19 22:41:00 -05:00
ts_query = Search . ts_query ( term : filter_words , joiner : " | " )
2014-04-14 15:20:41 -04:00
2017-09-15 19:03:29 -04:00
candidates = Topic
. visible
2017-07-27 21:20:09 -04:00
. listable_topics
2017-09-15 19:03:29 -04:00
. secured ( Guardian . new ( user ) )
. joins ( " JOIN topic_search_data s ON topics.id = s.topic_id " )
. joins ( " LEFT JOIN categories c ON topics.id = c.topic_id " )
2017-07-27 21:20:09 -04:00
. where ( " search_data @@ #{ ts_query } " )
2017-09-15 19:03:29 -04:00
. where ( " c.topic_id IS NULL " )
2017-07-27 21:20:09 -04:00
. order ( " ts_rank(search_data, #{ ts_query } ) DESC " )
. limit ( SiteSetting . max_similar_results * 3 )
2014-08-07 22:12:53 -04:00
candidate_ids = candidates . pluck ( :id )
2017-09-15 19:03:29 -04:00
return [ ] if candidate_ids . blank?
2014-08-07 22:12:53 -04:00
2017-09-15 19:03:29 -04:00
similars = Topic
2017-07-27 21:20:09 -04:00
. joins ( " JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1 " )
. where ( " topics.id IN (?) " , candidate_ids )
2017-09-15 19:03:29 -04:00
. order ( " similarity DESC " )
. limit ( SiteSetting . max_similar_results )
2014-08-07 22:12:53 -04:00
2017-09-15 19:03:29 -04:00
if raw . present?
similars
. select ( sanitize_sql_array ( [ " topics.*, similarity(topics.title, :title) + similarity(p.raw, :raw) AS similarity, p.cooked AS blurb " , title : title , raw : raw ] ) )
. where ( " similarity(topics.title, :title) + similarity(p.raw, :raw) > 0.2 " , title : title , raw : raw )
else
similars
. select ( sanitize_sql_array ( [ " topics.*, similarity(topics.title, :title) AS similarity, p.cooked AS blurb " , title : title ] ) )
. where ( " similarity(topics.title, :title) > 0.2 " , title : title )
end
2013-03-14 14:45:29 -04:00
end
2013-03-06 15:17:07 -05:00
2017-07-27 21:20:09 -04:00
def update_status ( status , enabled , user , opts = { } )
2017-03-21 23:12:02 -04:00
TopicStatusUpdater . new ( self , user ) . update! ( status , enabled , opts )
2018-02-26 22:07:37 -05:00
DiscourseEvent . trigger ( :topic_status_updated , self , status , enabled )
2013-02-05 14:16:51 -05:00
end
# Atomically creates the next post number
2019-03-08 03:49:34 -05:00
def self . next_post_number ( topic_id , opts = { } )
2018-06-19 02:13:14 -04:00
highest = DB . query_single ( " SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ? " , topic_id ) . first . to_i
2013-02-05 14:16:51 -05:00
2019-03-08 03:49:34 -05:00
if opts [ :whisper ]
2016-12-02 01:03:31 -05:00
2018-06-19 02:13:14 -04:00
result = DB . query_single ( << ~ SQL , highest , topic_id )
UPDATE topics
SET highest_staff_post_number = ? + 1
WHERE id = ?
RETURNING highest_staff_post_number
SQL
2016-12-02 01:03:31 -05:00
2018-06-19 02:13:14 -04:00
result . first . to_i
2016-12-02 01:03:31 -05:00
else
2019-03-08 03:49:34 -05:00
reply_sql = opts [ :reply ] ? " , reply_count = reply_count + 1 " : " "
posts_sql = opts [ :post ] ? " , posts_count = posts_count + 1 " : " "
2016-12-02 01:03:31 -05:00
2018-06-19 02:13:14 -04:00
result = DB . query_single ( << ~ SQL , highest : highest , topic_id : topic_id )
UPDATE topics
SET highest_staff_post_number = :highest + 1 ,
2019-03-08 03:49:34 -05:00
highest_post_number = :highest + 1
#{reply_sql}
#{posts_sql}
2018-06-19 02:13:14 -04:00
WHERE id = :topic_id
RETURNING highest_post_number
SQL
2016-12-02 01:03:31 -05:00
2018-06-19 02:13:14 -04:00
result . first . to_i
2016-12-02 01:03:31 -05:00
end
end
def self . reset_all_highest!
2018-06-19 02:13:14 -04:00
DB . exec << ~ SQL
WITH
X as (
SELECT topic_id ,
COALESCE ( MAX ( post_number ) , 0 ) highest_post_number
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
) ,
Y as (
SELECT topic_id ,
coalesce ( MAX ( post_number ) , 0 ) highest_post_number ,
count ( * ) posts_count ,
max ( created_at ) last_posted_at
FROM posts
WHERE deleted_at IS NULL AND post_type < > 4
GROUP BY topic_id
)
UPDATE topics
SET
highest_staff_post_number = X . highest_post_number ,
highest_post_number = Y . highest_post_number ,
last_posted_at = Y . last_posted_at ,
posts_count = Y . posts_count
FROM X , Y
WHERE
2019-03-08 03:49:34 -05:00
topics . archetype < > 'private_message' AND
X . topic_id = topics . id AND
Y . topic_id = topics . id AND (
topics . highest_staff_post_number < > X . highest_post_number OR
topics . highest_post_number < > Y . highest_post_number OR
topics . last_posted_at < > Y . last_posted_at OR
topics . posts_count < > Y . posts_count
)
SQL
DB . exec << ~ SQL
WITH
X as (
SELECT topic_id ,
COALESCE ( MAX ( post_number ) , 0 ) highest_post_number
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
) ,
Y as (
SELECT topic_id ,
coalesce ( MAX ( post_number ) , 0 ) highest_post_number ,
count ( * ) posts_count ,
max ( created_at ) last_posted_at
FROM posts
WHERE deleted_at IS NULL AND post_type < > 3 AND post_type < > 4
GROUP BY topic_id
)
UPDATE topics
SET
highest_staff_post_number = X . highest_post_number ,
highest_post_number = Y . highest_post_number ,
last_posted_at = Y . last_posted_at ,
posts_count = Y . posts_count
FROM X , Y
WHERE
topics . archetype = 'private_message' AND
2018-06-19 02:13:14 -04:00
X . topic_id = topics . id AND
Y . topic_id = topics . id AND (
topics . highest_staff_post_number < > X . highest_post_number OR
topics . highest_post_number < > Y . highest_post_number OR
topics . last_posted_at < > Y . last_posted_at OR
topics . posts_count < > Y . posts_count
)
SQL
2013-02-05 14:16:51 -05:00
end
# If a post is deleted we have to update our highest post counters
def self . reset_highest ( topic_id )
2019-03-08 03:49:34 -05:00
archetype = Topic . where ( id : topic_id ) . pluck ( :archetype ) . first
# ignore small_action replies for private messages
post_type = archetype == Archetype . private_message ? " AND post_type <> #{ Post . types [ :small_action ] } " : ''
2018-06-19 02:13:14 -04:00
result = DB . query_single ( << ~ SQL , topic_id : topic_id )
UPDATE topics
SET
2019-03-08 03:49:34 -05:00
highest_staff_post_number = (
2018-06-19 02:13:14 -04:00
SELECT COALESCE ( MAX ( post_number ) , 0 ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL
) ,
2019-03-08 03:49:34 -05:00
highest_post_number = (
2018-06-19 02:13:14 -04:00
SELECT COALESCE ( MAX ( post_number ) , 0 ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type < > 4
2019-03-08 03:49:34 -05:00
#{post_type}
2018-06-19 02:13:14 -04:00
) ,
posts_count = (
SELECT count ( * ) FROM posts
WHERE deleted_at IS NULL AND
topic_id = :topic_id AND
post_type < > 4
2019-03-08 03:49:34 -05:00
#{post_type}
2018-06-19 02:13:14 -04:00
) ,
last_posted_at = (
SELECT MAX ( created_at ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type < > 4
2019-03-08 03:49:34 -05:00
#{post_type}
2018-06-19 02:13:14 -04:00
)
WHERE id = :topic_id
RETURNING highest_post_number
SQL
highest_post_number = result . first . to_i
2013-02-05 14:16:51 -05:00
# Update the forum topic user records
2018-06-19 02:13:14 -04:00
DB . exec ( << ~ SQL , highest : highest_post_number , topic_id : topic_id )
UPDATE topic_users
SET last_read_post_number = CASE
WHEN last_read_post_number > :highest THEN :highest
ELSE last_read_post_number
END ,
highest_seen_post_number = CASE
WHEN highest_seen_post_number > :highest THEN :highest
ELSE highest_seen_post_number
END
WHERE topic_id = :topic_id
SQL
2013-02-05 14:16:51 -05:00
end
2019-05-09 21:37:37 -04:00
cattr_accessor :update_featured_topics
2014-10-27 17:06:43 -04:00
def changed_to_category ( new_category )
2018-05-31 21:44:14 -04:00
return true if new_category . blank? || Category . exists? ( topic_id : id )
2014-10-27 17:06:43 -04:00
return false if new_category . id == SiteSetting . uncategorized_category_id && ! SiteSetting . allow_uncategorized_topics
2013-02-05 14:16:51 -05:00
Topic . transaction do
old_category = category
2014-10-27 17:06:43 -04:00
if self . category_id != new_category . id
2018-08-14 10:06:52 -04:00
self . update_attribute ( :category_id , new_category . id )
2018-05-07 09:29:06 -04:00
if old_category
Category
. where ( id : old_category . id )
. update_all ( " topic_count = topic_count - 1 " )
end
2016-07-07 22:58:18 -04:00
# when a topic changes category we may have to start watching it
# if we happen to have read state for it
CategoryUser . auto_watch ( category_id : new_category . id , topic_id : self . id )
CategoryUser . auto_track ( category_id : new_category . id , topic_id : self . id )
2018-05-07 09:29:06 -04:00
2018-05-24 11:27:43 -04:00
if post = self . ordered_posts . first
notified_user_ids = [ post . user_id , post . last_editor_id ] . uniq
2018-07-24 04:41:55 -04:00
DB . after_commit do
2018-07-18 17:04:43 -04:00
Jobs . enqueue ( :notify_category_change , post_id : post . id , notified_user_ids : notified_user_ids )
end
2018-05-07 09:29:06 -04:00
end
2013-02-05 14:16:51 -05:00
end
2014-10-27 17:06:43 -04:00
Category . where ( id : new_category . id ) . update_all ( " topic_count = topic_count + 1 " )
2019-05-09 21:37:37 -04:00
if Topic . update_featured_topics != false
CategoryFeaturedTopic . feature_topics_for ( old_category ) unless @import_mode
CategoryFeaturedTopic . feature_topics_for ( new_category ) unless @import_mode || old_category . try ( :id ) == new_category . id
end
2013-02-07 10:45:24 -05:00
end
2014-10-27 17:06:43 -04:00
2013-10-08 14:40:31 -04:00
true
2013-02-05 14:16:51 -05:00
end
2018-07-17 20:17:33 -04:00
def add_small_action ( user , action_code , who = nil , opts = { } )
2016-01-18 18:57:55 -05:00
custom_fields = { }
custom_fields [ " action_code_who " ] = who if who . present?
2018-07-17 20:17:33 -04:00
opts = opts . merge (
post_type : Post . types [ :small_action ] ,
action_code : action_code ,
custom_fields : custom_fields
)
add_moderator_post ( user , nil , opts )
2016-01-18 18:57:55 -05:00
end
2017-07-27 21:20:09 -04:00
def add_moderator_post ( user , text , opts = nil )
2015-07-24 16:39:03 -04:00
opts || = { }
2013-02-05 14:16:51 -05:00
new_post = nil
2015-10-14 20:56:10 -04:00
creator = PostCreator . new ( user ,
raw : text ,
post_type : opts [ :post_type ] || Post . types [ :moderator_action ] ,
action_code : opts [ :action_code ] ,
no_bump : opts [ :bump ] . blank? ,
topic_id : self . id ,
2016-01-11 06:42:06 -05:00
skip_validations : true ,
custom_fields : opts [ :custom_fields ] )
2013-02-05 14:16:51 -05:00
2016-06-20 03:47:29 -04:00
if ( new_post = creator . create ) && new_post . present?
increment! ( :moderator_posts_count ) if new_post . persisted?
2013-02-05 14:16:51 -05:00
# If we are moving posts, we want to insert the moderator post where the previous posts were
# in the stream, not at the end.
2019-04-29 03:32:25 -04:00
new_post . update! ( post_number : opts [ :post_number ] , sort_order : opts [ :post_number ] ) if opts [ :post_number ] . present?
2013-02-05 14:16:51 -05:00
# Grab any links that are present
TopicLink . extract_from ( new_post )
2014-07-15 03:47:24 -04:00
QuotedPost . extract_from ( new_post )
2013-02-05 14:16:51 -05:00
end
2013-02-07 10:45:24 -05:00
2013-02-05 14:16:51 -05:00
new_post
end
2014-07-16 15:39:39 -04:00
def change_category_to_id ( category_id )
2014-09-11 03:39:20 -04:00
return false if private_message?
2014-10-27 17:06:43 -04:00
new_category_id = category_id . to_i
# if the category name is blank, reset the attribute
new_category_id = SiteSetting . uncategorized_category_id if new_category_id == 0
2013-02-05 14:16:51 -05:00
2014-10-27 17:06:43 -04:00
return true if self . category_id == new_category_id
cat = Category . find_by ( id : new_category_id )
2013-10-23 19:05:51 -04:00
return false unless cat
2014-10-27 17:06:43 -04:00
2019-01-03 12:03:01 -05:00
reviewables . update_all ( category_id : new_category_id )
2013-02-05 14:16:51 -05:00
changed_to_category ( cat )
end
2016-06-20 02:29:11 -04:00
def remove_allowed_group ( removed_by , name )
if group = Group . find_by ( name : name )
group_user = topic_allowed_groups . find_by ( group_id : group . id )
if group_user
group_user . destroy
add_small_action ( removed_by , " removed_group " , group . name )
return true
end
end
false
end
2016-01-11 06:42:06 -05:00
def remove_allowed_user ( removed_by , username )
2017-10-10 04:26:56 -04:00
user = username . is_a? ( User ) ? username : User . find_by ( username : username )
if user
2014-05-06 09:41:59 -04:00
topic_user = topic_allowed_users . find_by ( user_id : user . id )
2017-10-10 04:26:56 -04:00
2013-10-02 13:11:48 -04:00
if topic_user
topic_user . destroy
2017-10-10 04:26:56 -04:00
if user . id == removed_by & . id
removed_by = Discourse . system_user
add_small_action ( removed_by , " user_left " , user . username )
else
add_small_action ( removed_by , " removed_user " , user . username )
end
2014-09-25 11:44:48 -04:00
return true
2013-10-02 13:11:48 -04:00
end
2013-06-18 03:17:01 -04:00
end
2014-09-25 11:44:48 -04:00
false
2013-06-18 03:17:01 -04:00
end
2018-08-23 00:36:49 -04:00
def reached_recipients_limit?
return false unless private_message?
topic_allowed_users . count + topic_allowed_groups . count > = SiteSetting . max_allowed_message_recipients
end
2016-06-20 02:29:11 -04:00
def invite_group ( user , group )
TopicAllowedGroup . create! ( topic_id : id , group_id : group . id )
last_post = posts . order ( 'post_number desc' ) . where ( 'not hidden AND posts.deleted_at IS NULL' ) . first
if last_post
2018-05-24 11:27:43 -04:00
Jobs . enqueue ( :post_alert , post_id : last_post . id )
2016-06-20 02:29:11 -04:00
add_small_action ( user , " invited_group " , group . name )
2017-06-14 01:53:49 -04:00
2017-07-26 02:51:44 -04:00
group_id = group . id
2017-06-14 01:53:49 -04:00
group . users . where (
2018-03-05 00:49:11 -05:00
" group_users.notification_level > ? AND user_id != ? " ,
NotificationLevels . all [ :muted ] , user . id
2017-06-14 01:53:49 -04:00
) . find_each do | u |
u . notifications . create! (
notification_type : Notification . types [ :invited_to_private_message ] ,
topic_id : self . id ,
post_number : 1 ,
data : {
topic_title : self . title ,
2017-07-26 02:51:44 -04:00
display_username : user . username ,
group_id : group_id
2017-06-14 01:53:49 -04:00
} . to_json
)
end
2016-06-20 02:29:11 -04:00
end
true
end
2017-07-27 21:20:09 -04:00
def invite ( invited_by , username_or_email , group_ids = nil , custom_message = nil )
2018-02-26 00:19:52 -05:00
target_user = User . find_by_username_or_email ( username_or_email )
2018-03-07 15:04:17 -05:00
guardian = Guardian . new ( invited_by )
2018-12-05 10:43:07 -05:00
is_email = username_or_email =~ / ^.+@.+$ /
2017-07-12 06:01:10 -04:00
2018-12-05 10:43:07 -05:00
if target_user
if topic_allowed_users . exists? ( user_id : target_user . id )
raise UserExists . new ( I18n . t ( " topic_invite.user_exists " ) )
end
2018-02-28 23:41:36 -05:00
2018-12-05 10:43:07 -05:00
if invite_existing_muted? ( target_user , invited_by )
return true
end
2017-07-12 06:01:10 -04:00
2018-12-05 10:43:07 -05:00
if private_message?
! ! invite_to_private_message ( invited_by , target_user , guardian )
2017-07-12 06:01:10 -04:00
else
2018-12-05 10:43:07 -05:00
! ! invite_to_topic ( invited_by , target_user , group_ids , guardian )
2017-07-12 06:01:10 -04:00
end
2018-12-05 10:43:07 -05:00
elsif is_email && guardian . can_invite_via_email? ( self )
! ! Invite . invite_by_email (
username_or_email , invited_by , self , group_ids , custom_message
2018-02-25 21:42:06 -05:00
)
2013-06-18 20:31:19 -04:00
end
2013-02-05 14:16:51 -05:00
end
2018-02-28 14:15:01 -05:00
def invite_existing_muted? ( target_user , invited_by )
if invited_by . id &&
MutedUser . where ( user_id : target_user . id , muted_user_id : invited_by . id )
. joins ( :muted_user )
. where ( 'NOT admin AND NOT moderator' )
. exists?
return true
end
if TopicUser . where (
topic : self ,
user : target_user ,
notification_level : TopicUser . notification_levels [ :muted ]
) . exists?
return true
end
false
end
2013-10-03 17:06:14 -04:00
def email_already_exists_for? ( invite )
2017-07-27 21:20:09 -04:00
invite . email_already_exists && private_message?
2013-10-03 17:06:14 -04:00
end
def grant_permission_to_user ( lower_email )
2017-04-26 14:47:36 -04:00
user = User . find_by_email ( lower_email )
2019-03-29 12:03:33 -04:00
topic_allowed_users . create! ( user_id : user . id ) unless topic_allowed_users . exists? ( user_id : user . id )
2013-10-03 17:06:14 -04:00
end
2013-05-25 20:37:23 -04:00
def max_post_number
2014-08-20 12:28:34 -04:00
posts . with_deleted . maximum ( :post_number ) . to_i
2013-05-25 20:37:23 -04:00
end
2013-05-08 13:33:58 -04:00
def move_posts ( moved_by , post_ids , opts )
2018-12-31 06:47:22 -05:00
post_mover = PostMover . new ( self , moved_by , post_ids , move_to_pm : opts [ :archetype ] . present? && opts [ :archetype ] == " private_message " )
2013-05-08 13:33:58 -04:00
2013-05-25 20:40:33 -04:00
if opts [ :destination_topic_id ]
2018-12-31 06:47:22 -05:00
topic = post_mover . to_topic ( opts [ :destination_topic_id ] , participants : opts [ :participants ] )
2018-07-09 21:48:57 -04:00
DiscourseEvent . trigger ( :topic_merged ,
post_mover . original_topic ,
post_mover . destination_topic
)
topic
2013-05-25 20:40:33 -04:00
elsif opts [ :title ]
2018-07-06 12:21:32 -04:00
post_mover . to_new_topic ( opts [ :title ] , opts [ :category_id ] , opts [ :tags ] )
2013-02-05 14:16:51 -05:00
end
end
2013-03-12 12:33:42 -04:00
# Updates the denormalized statistics of a topic including featured posters. They shouldn't
# go out of sync unless you do something drastic live move posts from one topic to another.
# this recalculates everything.
def update_statistics
feature_topic_users
update_action_counts
Topic . reset_highest ( id )
end
def update_action_counts
2016-12-02 01:03:31 -05:00
update_column ( :like_count , Post . where ( topic_id : id ) . sum ( :like_count ) )
2013-03-12 12:33:42 -04:00
end
2017-02-17 17:54:43 -05:00
def posters_summary ( options = { } ) # avatar lookup in options
2013-05-23 02:21:19 -04:00
@posters_summary || = TopicPostersSummary . new ( self , options ) . summary
2013-02-05 14:16:51 -05:00
end
2014-05-12 03:32:49 -04:00
def participants_summary ( options = { } )
@participants_summary || = TopicParticipantsSummary . new ( self , options ) . summary
end
2014-06-16 13:21:21 -04:00
def make_banner! ( user )
# only one banner at the same time
previous_banner = Topic . where ( archetype : Archetype . banner ) . first
previous_banner . remove_banner! ( user ) if previous_banner . present?
2017-02-03 15:07:38 -05:00
UserProfile . where ( " dismissed_banner_key IS NOT NULL " )
. update_all ( dismissed_banner_key : nil )
2014-06-16 13:21:21 -04:00
self . archetype = Archetype . banner
2017-03-16 17:31:27 -04:00
self . add_small_action ( user , " banner.enabled " )
2014-06-16 13:21:21 -04:00
self . save
2014-06-18 14:04:10 -04:00
2015-05-03 22:21:00 -04:00
MessageBus . publish ( '/site/banner' , banner )
2014-06-16 13:21:21 -04:00
end
def remove_banner! ( user )
self . archetype = Archetype . default
2017-03-16 17:31:27 -04:00
self . add_small_action ( user , " banner.disabled " )
2014-06-16 13:21:21 -04:00
self . save
2014-06-18 14:04:10 -04:00
2015-05-03 22:21:00 -04:00
MessageBus . publish ( '/site/banner' , nil )
2014-06-18 14:04:10 -04:00
end
def banner
2017-02-24 06:56:13 -05:00
post = self . ordered_posts . first
2014-06-18 14:04:10 -04:00
{
html : post . cooked ,
2015-06-09 13:31:14 -04:00
key : self . id ,
url : self . url
2014-06-18 14:04:10 -04:00
}
2014-06-16 13:21:21 -04:00
end
2013-06-07 14:17:12 -04:00
# Even if the slug column in the database is null, topic.slug will return something:
2013-02-05 14:16:51 -05:00
def slug
2013-04-23 22:46:43 -04:00
unless slug = read_attribute ( :slug )
return '' unless title . present?
2015-05-04 07:48:37 -04:00
slug = Slug . for ( title )
2013-04-23 22:46:43 -04:00
if new_record?
write_attribute ( :slug , slug )
else
update_column ( :slug , slug )
end
end
slug
end
def title = ( t )
2015-05-04 07:48:37 -04:00
slug = Slug . for ( t . to_s )
2013-04-23 22:46:43 -04:00
write_attribute ( :slug , slug )
2015-09-23 23:37:53 -04:00
write_attribute ( :fancy_title , nil )
2017-07-27 21:20:09 -04:00
write_attribute ( :title , t )
2013-02-05 14:16:51 -05:00
end
2013-05-24 02:06:38 -04:00
# NOTE: These are probably better off somewhere else.
# Having a model know about URLs seems a bit strange.
2013-02-05 14:16:51 -05:00
def last_post_url
2015-04-30 12:46:19 -04:00
" #{ Discourse . base_uri } /t/ #{ slug } / #{ id } / #{ posts_count } "
2013-02-05 14:16:51 -05:00
end
2017-07-27 21:20:09 -04:00
def self . url ( id , slug , post_number = nil )
2019-05-13 08:51:45 -04:00
url = + " #{ Discourse . base_url } /t/ #{ slug } / #{ id } "
2013-05-09 03:37:34 -04:00
url << " / #{ post_number } " if post_number . to_i > 1
url
end
2013-05-25 20:38:15 -04:00
def url ( post_number = nil )
self . class . url id , slug , post_number
end
2017-07-27 21:20:09 -04:00
def self . relative_url ( id , slug , post_number = nil )
2019-05-02 18:17:27 -04:00
url = + " #{ Discourse . base_uri } /t/ "
2017-04-24 15:26:06 -04:00
url << " #{ slug } / " if slug . present?
url << id . to_s
2013-05-09 03:37:34 -04:00
url << " / #{ post_number } " if post_number . to_i > 1
2013-02-05 14:16:51 -05:00
url
end
2017-07-27 21:20:09 -04:00
def slugless_url ( post_number = nil )
2017-04-24 15:26:06 -04:00
Topic . relative_url ( id , nil , post_number )
end
2017-07-27 21:20:09 -04:00
def relative_url ( post_number = nil )
2015-09-28 02:43:38 -04:00
Topic . relative_url ( id , slug , post_number )
end
2013-03-06 15:17:07 -05:00
def clear_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : Time . now )
end
2014-04-09 20:56:56 -04:00
def re_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : nil )
end
2018-03-28 04:20:08 -04:00
def update_pinned ( status , global = false , pinned_until = " " )
pinned_until || = ''
pinned_until = begin
Time . parse ( pinned_until )
rescue ArgumentError
end
2015-07-29 10:34:21 -04:00
update_columns (
2018-03-28 04:20:08 -04:00
pinned_at : status ? Time . zone . now : nil ,
2015-07-29 10:34:21 -04:00
pinned_globally : global ,
pinned_until : pinned_until
)
Jobs . cancel_scheduled_job ( :unpin_topic , topic_id : self . id )
Jobs . enqueue_at ( pinned_until , :unpin_topic , topic_id : self . id ) if pinned_until
2013-02-05 14:16:51 -05:00
end
def draft_key
2013-02-28 13:54:12 -05:00
" #{ Draft :: EXISTING_TOPIC } #{ id } "
2013-02-05 14:16:51 -05:00
end
2013-05-24 02:06:38 -04:00
def notifier
@topic_notifier || = TopicNotifier . new ( self )
end
def muted? ( user )
if user && user . id
notifier . muted? ( user . id )
end
end
2015-07-29 10:34:21 -04:00
def self . ensure_consistency!
# unpin topics that might have been missed
Topic . where ( " pinned_until < now() " ) . update_all ( pinned_at : nil , pinned_globally : false , pinned_until : nil )
end
2017-05-16 14:49:42 -04:00
def public_topic_timer
2017-05-16 21:37:11 -04:00
@public_topic_timer || = topic_timers . find_by ( deleted_at : nil , public_type : true )
2013-11-10 18:52:44 -05:00
end
2017-10-04 23:48:42 -04:00
def private_topic_timer ( user )
@private_topic_Timer || = topic_timers . find_by ( deleted_at : nil , public_type : false , user_id : user . id )
end
2017-08-22 02:22:48 -04:00
def delete_topic_timer ( status_type , by_user : Discourse . system_user )
options = { status_type : status_type }
options . merge! ( user : by_user ) unless TopicTimer . public_types [ status_type ]
self . topic_timers . find_by ( options ) & . trash! ( by_user )
2017-08-22 02:52:16 -04:00
nil
2017-08-22 02:22:48 -04:00
end
2017-03-21 23:12:02 -04:00
# Valid arguments for the time:
# * An integer, which is the number of hours from now to update the topic's status.
# * A timestamp, like "2013-11-25 13:00", when the topic's status should update.
2013-11-26 19:06:20 -05:00
# * A timestamp with timezone in JSON format. (e.g., "2013-11-26T21:00:00.000Z")
2017-03-21 23:12:02 -04:00
# * `nil` to delete the topic's status update.
2015-05-27 12:22:34 -04:00
# Options:
2017-03-21 23:12:02 -04:00
# * by_user: User who is setting the topic's status update.
2017-04-03 05:28:41 -04:00
# * based_on_last_post: True if time should be based on timestamp of the last post.
# * category_id: Category that the update will apply to.
2017-12-07 08:42:58 -05:00
def set_or_create_timer ( status_type , time , by_user : nil , based_on_last_post : false , category_id : SiteSetting . uncategorized_category_id )
2017-08-22 02:22:48 -04:00
return delete_topic_timer ( status_type , by_user : by_user ) if time . blank?
2017-10-04 04:31:40 -04:00
public_topic_timer = ! ! TopicTimer . public_types [ status_type ]
topic_timer_options = { topic : self , public_type : public_topic_timer }
topic_timer_options . merge! ( user : by_user ) unless public_topic_timer
2017-05-16 21:37:11 -04:00
topic_timer = TopicTimer . find_or_initialize_by ( topic_timer_options )
2017-06-21 02:31:15 -04:00
topic_timer . status_type = status_type
2017-03-21 23:12:02 -04:00
time_now = Time . zone . now
2017-05-11 18:23:18 -04:00
topic_timer . based_on_last_post = ! based_on_last_post . blank?
2017-03-21 23:12:02 -04:00
2017-05-11 18:23:18 -04:00
if status_type == TopicTimer . types [ :publish_to_category ]
topic_timer . category = Category . find_by ( id : category_id )
2017-04-03 05:28:41 -04:00
end
2017-05-11 18:23:18 -04:00
if topic_timer . based_on_last_post
2017-03-21 23:12:02 -04:00
num_hours = time . to_f
2014-10-10 12:21:44 -04:00
if num_hours > 0
2017-03-31 08:27:46 -04:00
last_post_created_at = self . ordered_posts . last . present? ? self . ordered_posts . last . created_at : time_now
2017-05-11 18:23:18 -04:00
topic_timer . execute_at = last_post_created_at + num_hours . hours
topic_timer . created_at = last_post_created_at
2014-10-10 12:21:44 -04:00
end
2013-11-26 19:06:20 -05:00
else
2015-05-27 12:22:34 -04:00
utc = Time . find_zone ( " UTC " )
2018-03-25 23:32:52 -04:00
is_float = ( Float ( time ) rescue nil )
2017-03-21 23:12:02 -04:00
2018-03-25 23:32:52 -04:00
if is_float
num_hours = time . to_f
topic_timer . execute_at = num_hours . hours . from_now if num_hours > 0
else
timestamp = utc . parse ( time )
raise Discourse :: InvalidParameters unless timestamp
2015-05-27 12:22:34 -04:00
# a timestamp in client's time zone, like "2015-5-27 12:00"
2017-05-11 18:23:18 -04:00
topic_timer . execute_at = timestamp
2018-03-25 23:32:52 -04:00
topic_timer . errors . add ( :execute_at , :invalid ) if timestamp < utc . now
2014-10-10 12:21:44 -04:00
end
2013-11-26 19:06:20 -05:00
end
2017-05-11 18:23:18 -04:00
if topic_timer . execute_at
2017-03-21 23:12:02 -04:00
if by_user & . staff? || by_user & . trust_level == TrustLevel [ 4 ]
2017-05-11 18:23:18 -04:00
topic_timer . user = by_user
2014-10-10 12:21:44 -04:00
else
2017-05-11 18:23:18 -04:00
topic_timer . user || = ( self . user . staff? || self . user . trust_level == TrustLevel [ 4 ] ? self . user : Discourse . system_user )
2014-10-10 12:21:44 -04:00
end
2017-03-21 23:12:02 -04:00
if self . persisted?
2017-05-11 18:23:18 -04:00
topic_timer . save!
2013-06-06 17:04:10 -04:00
else
2017-05-11 18:23:18 -04:00
self . topic_timers << topic_timer
2015-12-08 07:43:23 -05:00
end
2017-05-11 18:23:18 -04:00
topic_timer
2013-06-06 17:04:10 -04:00
end
2013-05-07 14:25:41 -04:00
end
2013-07-13 21:24:16 -04:00
def read_restricted_category?
category && category . read_restricted
2013-05-20 02:04:53 -04:00
end
2013-07-08 15:23:20 -04:00
2013-12-11 21:41:34 -05:00
def acting_user
@acting_user || user
end
def acting_user = ( u )
@acting_user = u
end
2014-03-23 21:19:08 -04:00
def secure_group_ids
@secure_group_ids || = if self . category && self . category . read_restricted?
self . category . secure_group_ids
end
end
2014-04-01 15:29:15 -04:00
def has_topic_embed?
TopicEmbed . where ( topic_id : id ) . exists?
end
def expandable_first_post?
2015-08-18 17:15:46 -04:00
SiteSetting . embed_truncate? && has_topic_embed?
2014-04-01 15:29:15 -04:00
end
2015-12-29 21:26:21 -05:00
def message_archived? ( user )
return false unless user && user . id
2018-04-05 03:17:31 -04:00
# tricky query but this checks to see if message is archived for ALL groups you belong to
# OR if you have it archived as a user explicitly
sql = << ~ SQL
2018-06-19 02:13:14 -04:00
SELECT 1
WHERE
(
SELECT count ( * ) FROM topic_allowed_groups tg
JOIN group_archived_messages gm
ON gm . topic_id = tg . topic_id AND
gm . group_id = tg . group_id
WHERE tg . group_id IN ( SELECT g . group_id FROM group_users g WHERE g . user_id = :user_id )
AND tg . topic_id = :topic_id
) =
(
SELECT case when count ( * ) = 0 then - 1 else count ( * ) end FROM topic_allowed_groups tg
WHERE tg . group_id IN ( SELECT g . group_id FROM group_users g WHERE g . user_id = :user_id )
AND tg . topic_id = :topic_id
)
2018-04-05 03:17:31 -04:00
2018-06-19 02:13:14 -04:00
UNION ALL
2018-04-05 03:17:31 -04:00
2018-06-19 02:13:14 -04:00
SELECT 1 FROM topic_allowed_users tu
JOIN user_archived_messages um ON um . user_id = tu . user_id AND um . topic_id = tu . topic_id
WHERE tu . user_id = :user_id AND tu . topic_id = :topic_id
SQL
2015-12-29 21:26:21 -05:00
2018-06-19 02:13:14 -04:00
DB . exec ( sql , user_id : user . id , topic_id : id ) > 0
2015-12-29 21:26:21 -05:00
end
2015-06-22 13:46:51 -04:00
TIME_TO_FIRST_RESPONSE_SQL || = <<-SQL
SELECT AVG ( t . hours ) :: float AS " hours " , t . created_at AS " date "
FROM (
SELECT t . id , t . created_at :: date AS created_at , EXTRACT ( EPOCH FROM MIN ( p . created_at ) - t . created_at ) :: float / 3600 . 0 AS " hours "
FROM topics t
LEFT JOIN posts p ON p . topic_id = t . id
/ *where* /
GROUP BY t . id
) t
GROUP BY t . created_at
ORDER BY t . created_at
SQL
TIME_TO_FIRST_RESPONSE_TOTAL_SQL || = <<-SQL
SELECT AVG ( t . hours ) :: float AS " hours "
FROM (
SELECT t . id , EXTRACT ( EPOCH FROM MIN ( p . created_at ) - t . created_at ) :: float / 3600 . 0 AS " hours "
FROM topics t
LEFT JOIN posts p ON p . topic_id = t . id
/ *where* /
GROUP BY t . id
) t
SQL
2017-07-27 21:20:09 -04:00
def self . time_to_first_response ( sql , opts = nil )
2015-06-24 09:19:39 -04:00
opts || = { }
2018-06-20 03:48:02 -04:00
builder = DB . build ( sql )
2015-06-24 09:19:39 -04:00
builder . where ( " t.created_at >= :start_date " , start_date : opts [ :start_date ] ) if opts [ :start_date ]
2015-06-25 18:45:11 -04:00
builder . where ( " t.created_at < :end_date " , end_date : opts [ :end_date ] ) if opts [ :end_date ]
2018-08-09 20:50:05 -04:00
builder . where ( IN_CATEGORY_AND_SUBCATEGORIES_SQL , category_id : opts [ :category_id ] ) if opts [ :category_id ]
2015-06-22 13:46:51 -04:00
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
builder . where ( " p.deleted_at IS NULL " )
builder . where ( " p.post_number > 1 " )
2015-06-25 18:45:11 -04:00
builder . where ( " p.user_id != t.user_id " )
2017-07-27 21:20:09 -04:00
builder . where ( " p.user_id in (:user_ids) " , user_ids : opts [ :user_ids ] ) if opts [ :user_ids ]
2017-02-02 17:27:41 -05:00
builder . where ( " p.post_type = :post_type " , post_type : Post . types [ :regular ] )
2015-06-22 13:46:51 -04:00
builder . where ( " EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0 " )
2018-06-20 03:48:02 -04:00
builder . query_hash
2015-06-22 13:46:51 -04:00
end
2017-07-27 21:20:09 -04:00
def self . time_to_first_response_per_day ( start_date , end_date , opts = { } )
time_to_first_response ( TIME_TO_FIRST_RESPONSE_SQL , opts . merge ( start_date : start_date , end_date : end_date ) )
2015-06-22 13:46:51 -04:00
end
2017-07-27 21:20:09 -04:00
def self . time_to_first_response_total ( opts = nil )
2015-06-24 09:19:39 -04:00
total = time_to_first_response ( TIME_TO_FIRST_RESPONSE_TOTAL_SQL , opts )
total . first [ " hours " ] . to_f . round ( 2 )
2015-06-22 13:46:51 -04:00
end
2015-06-25 18:45:11 -04:00
WITH_NO_RESPONSE_SQL || = <<-SQL
SELECT COUNT ( * ) as count , tt . created_at AS " date "
FROM (
SELECT t . id , t . created_at :: date AS created_at , MIN ( p . post_number ) first_reply
FROM topics t
2017-02-02 17:27:41 -05:00
LEFT JOIN posts p ON p . topic_id = t . id AND p . user_id != t . user_id AND p . deleted_at IS NULL AND p . post_type = #{Post.types[:regular]}
2015-06-25 18:45:11 -04:00
/ *where* /
GROUP BY t . id
) tt
2017-02-02 17:27:41 -05:00
WHERE tt . first_reply IS NULL OR tt . first_reply < 2
2015-06-25 18:45:11 -04:00
GROUP BY tt . created_at
ORDER BY tt . created_at
SQL
2017-07-27 21:20:09 -04:00
def self . with_no_response_per_day ( start_date , end_date , category_id = nil )
2018-06-20 03:48:02 -04:00
builder = DB . build ( WITH_NO_RESPONSE_SQL )
2015-06-25 18:45:11 -04:00
builder . where ( " t.created_at >= :start_date " , start_date : start_date ) if start_date
builder . where ( " t.created_at < :end_date " , end_date : end_date ) if end_date
2018-08-09 20:50:05 -04:00
builder . where ( IN_CATEGORY_AND_SUBCATEGORIES_SQL , category_id : category_id ) if category_id
2015-06-25 18:45:11 -04:00
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
2018-06-20 03:48:02 -04:00
builder . query_hash
2015-06-22 13:46:51 -04:00
end
2015-06-25 18:45:11 -04:00
WITH_NO_RESPONSE_TOTAL_SQL || = <<-SQL
SELECT COUNT ( * ) as count
FROM (
SELECT t . id , MIN ( p . post_number ) first_reply
FROM topics t
2017-02-02 17:27:41 -05:00
LEFT JOIN posts p ON p . topic_id = t . id AND p . user_id != t . user_id AND p . deleted_at IS NULL AND p . post_type = #{Post.types[:regular]}
2015-06-25 18:45:11 -04:00
/ *where* /
GROUP BY t . id
) tt
2017-02-02 17:27:41 -05:00
WHERE tt . first_reply IS NULL OR tt . first_reply < 2
2015-06-25 18:45:11 -04:00
SQL
2017-07-27 21:20:09 -04:00
def self . with_no_response_total ( opts = { } )
2018-06-20 03:48:02 -04:00
builder = DB . build ( WITH_NO_RESPONSE_TOTAL_SQL )
2018-08-09 20:50:05 -04:00
builder . where ( IN_CATEGORY_AND_SUBCATEGORIES_SQL , category_id : opts [ :category_id ] ) if opts [ :category_id ]
2015-06-25 18:45:11 -04:00
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
2018-06-20 03:48:02 -04:00
builder . query_single . first . to_i
2015-06-22 13:46:51 -04:00
end
2016-05-01 07:48:43 -04:00
def convert_to_public_topic ( user )
public_topic = TopicConverter . new ( self , user ) . convert_to_public_topic
add_small_action ( user , " public_topic " ) if public_topic
public_topic
end
def convert_to_private_message ( user )
private_topic = TopicConverter . new ( self , user ) . convert_to_private_message
add_small_action ( user , " private_topic " ) if private_topic
private_topic
end
2017-04-26 23:53:53 -04:00
def pm_with_non_human_user?
2017-09-12 02:05:25 -04:00
sql = << ~ SQL
SELECT 1 FROM topics
LEFT JOIN topic_allowed_groups ON topics . id = topic_allowed_groups . topic_id
WHERE topic_allowed_groups . topic_id IS NULL
AND topics . archetype = :private_message
AND topics . id = :topic_id
AND (
SELECT COUNT ( * ) FROM topic_allowed_users
WHERE topic_allowed_users . topic_id = :topic_id
AND topic_allowed_users . user_id > 0
) = 1
SQL
2018-06-19 02:13:14 -04:00
result = DB . exec ( sql , private_message : Archetype . private_message , topic_id : self . id )
result != 0
2017-04-26 23:53:53 -04:00
end
2017-11-29 08:52:41 -05:00
def featured_link_root_domain
2018-01-06 16:01:35 -05:00
MiniSuffix . domain ( URI . parse ( URI . encode ( self . featured_link ) ) . hostname )
2017-11-29 08:52:41 -05:00
end
2018-03-27 04:30:08 -04:00
def self . private_message_topics_count_per_day ( start_date , end_date , topic_subtype )
2018-06-05 03:29:17 -04:00
private_messages
. with_subtype ( topic_subtype )
. where ( 'topics.created_at >= ? AND topics.created_at <= ?' , start_date , end_date )
. group ( 'date(topics.created_at)' )
. order ( 'date(topics.created_at)' )
. count
2018-03-27 04:30:08 -04:00
end
2018-05-24 04:41:51 -04:00
def is_category_topic?
@is_category_topic || = Category . exists? ( topic_id : self . id . to_i )
end
2018-08-09 20:51:03 -04:00
def reset_bumped_at
post = ordered_posts . where (
user_deleted : false ,
hidden : false ,
2018-11-14 12:56:22 -05:00
post_type : Post . types [ :regular ]
2018-12-21 11:37:32 -05:00
) . last || first_post
2018-08-09 20:51:03 -04:00
update! ( bumped_at : post . created_at )
end
2019-01-03 12:03:01 -05:00
def auto_close_threshold_reached?
return if user & . staff?
scores = ReviewableScore . pending
. joins ( :reviewable )
. where ( " reviewables.topic_id = ? " , self . id )
. pluck ( " COUNT(DISTINCT reviewable_scores.user_id), COALESCE(SUM(reviewable_scores.score), 0.0) " )
. first
2019-05-24 14:13:03 -04:00
scores [ 0 ] > = SiteSetting . num_flaggers_to_close_topic && scores [ 1 ] > = Reviewable . score_to_auto_close_topic
2019-01-03 12:03:01 -05:00
end
2019-04-16 03:16:23 -04:00
def update_category_topic_count_by ( num )
if category_id . present?
Category
. where ( [ 'id = ?' , category_id ] )
. update_all ( " topic_count = topic_count " + ( num > 0 ? '+' : '' ) + " #{ num } " )
end
end
2013-07-08 15:23:20 -04:00
private
2018-12-05 10:43:07 -05:00
def invite_to_private_message ( invited_by , target_user , guardian )
if ! guardian . can_send_private_message? ( target_user )
raise UserExists . new ( I18n . t (
" activerecord.errors.models.topic.attributes.base.cant_send_pm "
) )
end
Topic . transaction do
rate_limit_topic_invitation ( invited_by )
2019-03-29 12:03:33 -04:00
topic_allowed_users . create! ( user_id : target_user . id ) unless topic_allowed_users . exists? ( user_id : target_user . id )
2018-12-05 10:43:07 -05:00
add_small_action ( invited_by , " invited_user " , target_user . username )
create_invite_notification! (
target_user ,
Notification . types [ :invited_to_private_message ] ,
invited_by . username
)
end
end
def invite_to_topic ( invited_by , target_user , group_ids , guardian )
Topic . transaction do
rate_limit_topic_invitation ( invited_by )
if group_ids
(
self . category . groups . where ( id : group_ids ) . where ( automatic : false ) -
target_user . groups . where ( automatic : false )
) . each do | group |
if guardian . can_edit_group? ( group )
group . add ( target_user )
GroupActionLogger
. new ( invited_by , group )
. log_add_user_to_group ( target_user )
end
end
end
if Guardian . new ( target_user ) . can_see_topic? ( self )
create_invite_notification! (
target_user ,
Notification . types [ :invited_to_topic ] ,
invited_by . username
)
end
end
end
2013-10-16 05:28:18 -04:00
def limit_first_day_topics_per_day
apply_per_day_rate_limit_for ( " first-day-topics " , :max_topics_in_first_day )
end
def apply_per_day_rate_limit_for ( key , method_name )
2019-05-06 21:00:09 -04:00
RateLimiter . new ( user , " #{ key } -per-day " , SiteSetting . get ( method_name ) , 1 . day . to_i )
2013-10-16 05:28:18 -04:00
end
2013-08-14 00:14:56 -04:00
2018-02-26 00:19:52 -05:00
def create_invite_notification! ( target_user , notification_type , username )
target_user . notifications . create! (
2018-02-25 21:42:06 -05:00
notification_type : notification_type ,
topic_id : self . id ,
post_number : 1 ,
data : {
topic_title : self . title ,
display_username : username
} . to_json
)
end
2018-02-28 23:41:36 -05:00
def rate_limit_topic_invitation ( invited_by )
RateLimiter . new (
invited_by ,
" topic-invitations-per-day " ,
SiteSetting . max_topic_invitations_per_day ,
1 . day . to_i
) . performed!
true
end
2013-02-05 14:16:51 -05:00
end
2013-05-23 22:48:32 -04:00
# == Schema Information
#
# Table name: topics
#
2017-11-23 15:55:44 -05:00
# id :integer not null, primary key
2019-01-11 14:29:56 -05:00
# title :string not null
2017-11-23 15:55:44 -05:00
# last_posted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# views :integer default(0), not null
# posts_count :integer default(0), not null
# user_id :integer
# last_post_user_id :integer not null
# reply_count :integer default(0), not null
# featured_user1_id :integer
# featured_user2_id :integer
# featured_user3_id :integer
# avg_time :integer
# deleted_at :datetime
# highest_post_number :integer default(0), not null
2019-01-11 14:29:56 -05:00
# image_url :string
2017-11-23 15:55:44 -05:00
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# category_id :integer
# visible :boolean default(TRUE), not null
# moderator_posts_count :integer default(0), not null
# closed :boolean default(FALSE), not null
# archived :boolean default(FALSE), not null
# bumped_at :datetime not null
# has_summary :boolean default(FALSE), not null
2019-01-11 14:29:56 -05:00
# archetype :string default("regular"), not null
2017-11-23 15:55:44 -05:00
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# pinned_at :datetime
# score :float
2019-01-03 12:03:01 -05:00
# percent_rank :float default(1.0), not null
2019-01-11 14:29:56 -05:00
# subtype :string
# slug :string
2017-11-23 15:55:44 -05:00
# deleted_by_id :integer
# participant_count :integer default(1)
# word_count :integer
# excerpt :string(1000)
# pinned_globally :boolean default(FALSE), not null
# pinned_until :datetime
# fancy_title :string(400)
# highest_staff_post_number :integer default(0), not null
# featured_link :string
2019-01-03 12:03:01 -05:00
# reviewable_score :float default(0.0), not null
2013-05-23 22:48:32 -04:00
#
# Indexes
#
2015-09-17 20:41:10 -04:00
# idx_topics_front_page (deleted_at,visible,archetype,category_id,id)
2018-07-16 02:18:07 -04:00
# idx_topics_user_id_deleted_at (user_id) WHERE (deleted_at IS NULL)
# idxtopicslug (slug) WHERE ((deleted_at IS NULL) AND (slug IS NOT NULL))
2019-01-11 14:29:56 -05:00
# index_topics_on_bumped_at (bumped_at)
2018-07-16 02:18:07 -04:00
# index_topics_on_created_at_and_visible (created_at,visible) WHERE ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text))
2015-09-17 20:41:10 -04:00
# index_topics_on_id_and_deleted_at (id,deleted_at)
2017-10-05 23:13:01 -04:00
# index_topics_on_lower_title (lower((title)::text))
2018-07-16 02:18:07 -04:00
# index_topics_on_pinned_at (pinned_at) WHERE (pinned_at IS NOT NULL)
# index_topics_on_pinned_globally (pinned_globally) WHERE pinned_globally
2019-04-05 05:13:12 -04:00
# index_topics_on_updated_at_public (updated_at,visible,highest_staff_post_number,highest_post_number,category_id,created_at,id) WHERE (((archetype)::text <> 'private_message'::text) AND (deleted_at IS NULL))
2013-05-23 22:48:32 -04:00
#