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 11:00:50 +02:00
require_dependency 'text_cleaner'
2013-11-06 15:05:06 -05:00
require_dependency 'archetype'
2015-09-24 13:37:53 +10:00
require_dependency 'html_prettify'
2013-02-05 14:16:51 -05:00
class Topic < ActiveRecord :: Base
2013-07-03 21:43:29 +02:00
include ActionView :: Helpers :: SanitizeHelper
2013-02-05 14:16:51 -05:00
include RateLimiter :: OnCreateRecord
2014-04-28 10:31:51 +02:00
include HasCustomFields
2013-05-07 14:39:01 +10:00
include Trashable
2015-02-25 20:53:21 +01:00
include LimitedEdit
2013-08-30 09:12:44 +00: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!
def_delegator :notifier , :tracking! , :notify_tracking!
def_delegator :notifier , :regular! , :notify_regular!
def_delegator :notifier , :muted! , :notify_muted!
def_delegator :notifier , :toggle_mute , :toggle_mute
2013-02-05 14:16:51 -05:00
2014-05-12 09:32:49 +02:00
attr_accessor :allowed_user_ids
2013-03-12 12:33:42 -04:00
def self . max_sort_order
2 ** 31 - 1
end
2013-08-26 10:41:56 +00: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
2013-07-09 15:20:18 -04:00
def trash! ( trashed_by = nil )
2013-07-08 15:23:20 -04:00
update_category_topic_count_by ( - 1 ) if deleted_at . nil?
2013-07-09 15:20:18 -04:00
super ( trashed_by )
2013-05-07 14:39:01 +10:00
update_flagged_posts_count
end
def recover!
2013-07-08 15:23:20 -04:00
update_category_topic_count_by ( 1 ) unless deleted_at . nil?
2013-05-07 14:39:01 +10:00
super
update_flagged_posts_count
end
2013-02-06 12:13:41 +11: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
2014-08-11 16:55:26 -04:00
validates :title , :if = > Proc . new { | t | t . new_record? || t . title_changed? } ,
2014-08-01 17:28:00 -04:00
:presence = > true ,
2013-06-04 17:58:25 -04:00
:topic_title_length = > true ,
2013-05-22 21:52:12 -07:00
:quality_title = > { :unless = > :private_message? } ,
: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-24 09:19:59 +10:00
validates :category_id ,
:presence = > true ,
:exclusion = > {
:in = > Proc . new { [ SiteSetting . uncategorized_category_id ] }
} ,
:if = > Proc . new { | t |
( t . new_record? || t . category_id_changed? ) &&
! SiteSetting . allow_uncategorized_topics &&
( t . archetype . nil? || t . archetype == Archetype . default ) &&
( ! t . user_id || ! t . user . staff? )
}
2013-10-24 10:05:51 +11:00
2013-10-08 14:40:31 -04:00
2013-05-31 15:22:34 -04:00
before_validation do
2013-05-22 21:52:12 -07:00
self . title = TextCleaner . clean_title ( TextSentinel . title_sentinel ( title ) . text ) if errors [ :title ] . empty?
end
2013-02-05 14:16:51 -05:00
belongs_to :category
has_many :posts
2014-07-28 22:50:49 +02: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 15:15:17 +10:00
has_many :topic_allowed_groups
has_many :allowed_group_users , through : :allowed_groups , source : :users
has_many :allowed_groups , through : :topic_allowed_groups , source : :group
2013-02-05 14:16:51 -05:00
has_many :allowed_users , through : :topic_allowed_users , source : :user
2015-04-27 15:55:10 -04:00
has_many :queued_posts
2013-03-28 13:02:59 -04:00
2013-12-24 00:50:36 +01:00
has_one :top_topic
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
2013-05-07 14:25:41 -04:00
belongs_to :auto_close_user , class_name : 'User' , foreign_key : :auto_close_user_id
2013-02-05 14:16:51 -05:00
has_many :topic_users
has_many :topic_links
has_many :topic_invites
has_many :invites , through : :topic_invites , source : :invite
2014-09-08 11:11:56 -04:00
has_one :warning
2013-12-12 03:41:34 +01:00
2015-01-05 17:39:49 +11:00
has_one :first_post , - > { where post_number : 1 } , class_name : Post
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 18:09:08 +10: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 09:32:49 +02:00
attr_accessor :participants
2013-04-02 16:52:51 -04:00
attr_accessor :topic_list
2014-04-25 18:24:22 +02:00
attr_accessor :meta_data
2013-10-17 17:44:56 +11: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 19:04:39 +02: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 19:04:39 +02:00
scope :private_messages , - > { where ( archetype : Archetype . private_message ) }
2013-02-05 14:16:51 -05:00
2014-05-07 19:04:39 +02:00
scope :listable_topics , - > { where ( 'topics.archetype <> ?' , [ Archetype . private_message ] ) }
2013-02-05 14:16:51 -05:00
2014-02-28 15:07:55 +11:00
scope :by_newest , - > { order ( 'topics.created_at desc, topics.id desc' ) }
2013-02-27 19:36:12 -08:00
2013-07-03 22:04:22 +02:00
scope :visible , - > { where ( visible : true ) }
2013-05-30 13:23:40 +02:00
2015-01-29 21:10:26 +05:30
scope :created_since , lambda { | time_ago | where ( 'topics.created_at > ?' , time_ago ) }
2013-05-30 13:23:40 +02:00
2014-05-07 19:04:39 +02:00
scope :secured , lambda { | guardian = nil |
2013-06-08 23:52:06 +10:00
ids = guardian . secure_category_ids if guardian
2013-06-12 13:43:59 -04:00
# Query conditions
2014-05-07 19:04:39 +02:00
condition = if ids . present?
[ " NOT c.read_restricted or c.id in (:cats) " , cats : ids ]
else
[ " NOT c.read_restricted " ]
end
2013-06-12 13:43:59 -04:00
where ( " category_id IS NULL OR category_id IN (
SELECT c . id FROM categories c
WHERE #{condition[0]})", condition[1])
}
2013-06-08 23:52:06 +10:00
2014-10-10 18:21:44 +02:00
attr_accessor :ignore_category_auto_close
2014-10-27 22:06:43 +01:00
attr_accessor :skip_callbacks
2014-10-10 18:21:44 +02:00
2013-02-05 14:16:51 -05:00
before_create do
2014-10-27 22:06:43 +01:00
initialize_default_values
inherit_auto_close_from_category
2013-02-05 14:16:51 -05:00
end
after_create do
2013-12-13 17:04:45 +11:00
unless skip_callbacks
changed_to_category ( category )
2014-10-27 22:06:43 +01: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 17:04:45 +11:00
unless skip_callbacks
2014-10-27 22:06:43 +01:00
cancel_auto_close_job
ensure_topic_has_a_category
2013-10-24 10:05:51 +11:00
end
2015-09-24 13:37:53 +10:00
if title_changed?
write_attribute :fancy_title , Topic . fancy_title ( title )
end
2013-05-07 14:25:41 -04:00
end
after_save do
2013-12-13 17:04:45 +11:00
unless skip_callbacks
2014-10-27 22:06:43 +01:00
schedule_auto_close_job
2013-05-07 14:25:41 -04:00
end
2014-11-14 15:39:17 +11:00
banner = " banner " . freeze
if archetype_was == banner || archetype == banner
ApplicationController . banner_json_cache . clear
end
2013-05-07 14:25:41 -04:00
end
2014-10-27 22:06:43 +01:00
def initialize_default_values
self . bumped_at || = Time . now
self . last_post_user_id || = user_id
end
2014-03-07 18:59:47 +11:00
2014-10-27 22:06:43 +01:00
def inherit_auto_close_from_category
if ! @ignore_category_auto_close && self . category && self . category . auto_close_hours && self . auto_close_at . nil?
self . auto_close_based_on_last_post = self . category . auto_close_based_on_last_post
set_auto_close ( self . category . auto_close_hours )
end
end
2014-03-07 18:59:47 +11:00
2014-10-27 22:06:43 +01:00
def advance_draft_sequence
if archetype == Archetype . private_message
DraftSequence . next! ( user , Draft :: NEW_PRIVATE_MESSAGE )
else
DraftSequence . next! ( user , Draft :: NEW_TOPIC )
end
end
def cancel_auto_close_job
if ( auto_close_at_changed? && ! auto_close_at_was . nil? ) || ( auto_close_user_id_changed? && auto_close_at )
self . auto_close_started_at || = Time . zone . now if auto_close_at
Jobs . cancel_scheduled_job ( :close_topic , { topic_id : id } )
end
end
def schedule_auto_close_job
if auto_close_at && ( auto_close_at_changed? || auto_close_user_id_changed? )
options = { topic_id : id , user_id : auto_close_user_id || user_id }
Jobs . enqueue_at ( auto_close_at , :close_topic , options )
2014-03-07 18:59:47 +11:00
end
2013-12-12 03:41:34 +01:00
end
2014-10-27 22:06:43 +01:00
def ensure_topic_has_a_category
if category_id . nil? && ( archetype . nil? || archetype == Archetype . default )
self . category_id = SiteSetting . uncategorized_category_id
end
2013-12-12 03:41:34 +01:00
end
2015-09-22 00:50:52 +02: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 ] ]
result << types [ :whisper ] if viewed_by . try ( :staff? )
result
end
2013-11-13 12:26:32 -05:00
def self . top_viewed ( max = 10 )
2014-02-28 15:07:55 +11: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-28 15:07:55 +11:00
Topic . listable_topics . visible . secured . order ( 'created_at desc' ) . limit ( max )
2013-11-13 12:26:32 -05:00
end
2013-10-28 11:42:07 +05:30
def self . count_exceeds_minimum?
count > SiteSetting . minimum_topics_similar
end
2013-06-03 16:12:24 -04:00
def best_post
2014-02-28 15:07:55 +11:00
posts . order ( 'score desc' ) . limit ( 1 ) . first
2013-06-03 16:12:24 -04:00
end
2015-02-16 13:03:04 +01:00
def has_flags?
FlagQuery . flagged_post_actions ( " active " )
. where ( " topics.id " = > id )
. exists?
end
2013-05-02 15:15:17 +10:00
# all users (in groups or directly targetted) that are going to get the pm
def all_allowed_users
2015-02-16 13:03:04 +01:00
# TODO we should probably change this to 1 query
allowed_user_ids = allowed_users . select ( 'users.id' ) . to_a
allowed_group_user_ids = allowed_group_users . select ( 'users.id' ) . to_a
allowed_staff_ids = private_message? && has_flags? ? User . where ( moderator : true ) . pluck ( :id ) . to_a : [ ]
User . where ( 'id IN (?)' , allowed_user_ids + allowed_group_user_ids + allowed_staff_ids )
2013-05-02 15:15:17 +10: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
2015-09-24 12:04:41 -04:00
if user && user . first_day_user?
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?
2013-10-16 14:58:18 +05:30
apply_per_day_rate_limit_for ( " pms " , :max_private_messages_per_day )
2013-02-05 14:16:51 -05:00
end
2015-09-24 13:37:53 +10:00
def self . fancy_title ( title )
escaped = ERB :: Util . html_escape ( title )
return unless escaped
HtmlPrettify . render ( escaped )
end
2013-02-25 19:42:20 +03:00
def fancy_title
2015-09-24 13:37:53 +10:00
return ERB :: Util . html_escape ( title ) unless SiteSetting . title_fancy_entities?
2014-04-18 10:18:38 +05:30
2015-09-24 13:37:53 +10:00
unless fancy_title = read_attribute ( :fancy_title )
2013-02-19 16:08:23 -05:00
2015-09-24 13:37:53 +10:00
fancy_title = Topic . fancy_title ( title )
write_attribute ( :fancy_title , fancy_title )
unless new_record?
# make sure data is set in table, this also allows us to change algorithm
# by simply nulling this column
exec_sql ( " UPDATE topics SET fancy_title = :fancy_title where id = :id " , id : self . id , fancy_title : fancy_title )
end
end
2013-02-19 16:08:23 -05:00
2015-09-24 13:37:53 +10:00
fancy_title
2013-04-22 13:48:05 +10:00
end
2015-04-27 15:55:10 -04:00
def pending_posts_count
queued_posts . new_count
end
2013-06-03 16:12:24 -04:00
# Returns hot topics since a date for display in email digest.
2014-04-17 16:42:40 -04:00
def self . for_digest ( user , since , opts = nil )
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
. visible
. secured ( Guardian . new ( user ) )
2014-04-17 15:21:55 -04:00
. joins ( " LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{ user . id . to_i } " )
2015-01-29 21:10:26 +05:30
. joins ( " LEFT OUTER JOIN users ON users.id = topics.user_id " )
2013-11-06 15:05:06 -05:00
. where ( closed : false , archived : false )
2014-04-17 15:21:55 -04:00
. where ( " COALESCE(topic_users.notification_level, 1) <> ? " , TopicUser . notification_levels [ :muted ] )
2015-01-29 21:10:26 +05:30
. where ( " COALESCE(users.trust_level, 0) > 0 " )
2013-11-06 15:05:06 -05:00
. created_since ( since )
. listable_topics
2014-04-17 15:43:24 -04:00
. includes ( :category )
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 " )
. order ( TopicQuerySQL . order_top_for ( score ) )
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 )
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
topics
2013-02-05 14:16:51 -05:00
end
2013-02-07 16:45:24 +01:00
2014-04-17 16:42:40 -04:00
# Using the digest query, figure out what's new for a user since last seen
def self . new_since_last_seen ( user , since , featured_topic_ids )
topics = Topic . for_digest ( user , since )
topics . where ( " topics.id NOT IN (?) " , featured_topic_ids )
end
2014-04-25 18:24:22 +02: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 18:24:22 +02:00
custom_fields . update ( data )
2013-02-05 14:16:51 -05:00
save
end
2013-04-22 13:48:05 +10:00
def reload ( options = nil )
@post_numbers = nil
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 17:58:57 -07:00
end
2013-02-07 16:45:24 +01:00
def has_meta_data_boolean? ( key )
2013-02-05 14:16:51 -05:00
meta_data_string ( key ) == 'true'
end
def meta_data_string ( key )
2014-04-25 18:24:22 +02:00
custom_fields [ key . to_s ]
2013-02-05 14:16:51 -05:00
end
2015-06-24 15:19:39 +02:00
def self . listable_count_per_day ( start_date , end_date , category_id = nil )
result = listable_topics . where ( 'created_at >= ? and created_at <= ?' , start_date , end_date )
result = result . where ( category_id : category_id ) if category_id
result . group ( 'date(created_at)' ) . order ( 'date(created_at)' ) . count
2013-03-07 11:07:59 -05:00
end
2013-02-07 16:45:24 +01:00
def private_message?
2013-05-24 09:13:31 -07:00
archetype == Archetype . private_message
2013-02-05 14:16:51 -05:00
end
2014-08-08 15:50:26 +10:00
MAX_SIMILAR_BODY_LENGTH = 200
2013-03-14 14:45:29 -04:00
# Search for similar topics
2013-06-12 13:43:59 -04:00
def self . similar_to ( title , raw , user = nil )
2013-03-14 14:45:29 -04:00
return [ ] unless title . present?
return [ ] unless raw . present?
2014-08-08 15:50:26 +10:00
filter_words = Search . prepare_data ( title + " " + raw [ 0 ... MAX_SIMILAR_BODY_LENGTH ] ) ;
2014-08-08 12:12:53 +10:00
ts_query = Search . ts_query ( filter_words , nil , " | " )
2014-04-14 15:20:41 -04:00
# Exclude category definitions from similar topic suggestions
2014-08-08 12:12:53 +10:00
candidates = Topic . visible
. secured ( Guardian . new ( user ) )
. listable_topics
2014-08-08 15:50:26 +10:00
. joins ( 'JOIN topic_search_data s ON topics.id = s.topic_id' )
2014-08-08 12:12:53 +10:00
. where ( " search_data @@ #{ ts_query } " )
. order ( " ts_rank(search_data, #{ ts_query } ) DESC " )
. limit ( SiteSetting . max_similar_results * 3 )
2014-04-14 15:20:41 -04:00
exclude_topic_ids = Category . pluck ( :topic_id ) . compact!
if exclude_topic_ids . present?
2014-08-08 12:12:53 +10:00
candidates = candidates . where ( " topics.id NOT IN (?) " , exclude_topic_ids )
2014-04-14 15:20:41 -04:00
end
2014-08-08 12:12:53 +10:00
candidate_ids = candidates . pluck ( :id )
return [ ] unless candidate_ids . present?
2015-06-24 15:08:22 -04:00
similar = Topic . select ( sanitize_sql_array ( [ " topics.*, similarity(topics.title, :title) + similarity(topics.title, :raw) AS similarity, p.cooked as blurb " , title : title , raw : raw ] ) )
2014-08-08 12:12:53 +10:00
. joins ( " JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1 " )
. limit ( SiteSetting . max_similar_results )
. where ( " topics.id IN (?) " , candidate_ids )
. where ( " similarity(topics.title, :title) + similarity(topics.title, :raw) > 0.2 " , raw : raw , title : title )
. order ( 'similarity desc' )
2014-04-14 15:20:41 -04:00
similar
2013-03-14 14:45:29 -04:00
end
2013-03-06 15:17:07 -05:00
2015-07-29 16:34:21 +02:00
def update_status ( status , enabled , user , opts = { } )
TopicStatusUpdate . new ( self , user ) . update! ( status , enabled , opts )
2013-02-05 14:16:51 -05:00
end
# Atomically creates the next post number
2013-02-28 21:54:12 +03:00
def self . next_post_number ( topic_id , reply = false )
2013-02-05 14:16:51 -05:00
highest = exec_sql ( " select coalesce(max(post_number),0) as max from posts where topic_id = ? " , topic_id ) . first [ 'max' ] . to_i
reply_sql = reply ? " , reply_count = reply_count + 1 " : " "
2013-02-07 16:45:24 +01:00
result = exec_sql ( " UPDATE topics SET highest_post_number = ? + 1 #{ reply_sql }
2013-02-05 14:16:51 -05:00
WHERE id = ? RETURNING highest_post_number " , highest, topic_id)
result . first [ 'highest_post_number' ] . to_i
end
# If a post is deleted we have to update our highest post counters
def self . reset_highest ( topic_id )
2013-02-07 16:45:24 +01:00
result = exec_sql " UPDATE topics
2013-02-05 14:16:51 -05:00
SET highest_post_number = ( SELECT COALESCE ( MAX ( post_number ) , 0 ) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL ) ,
2013-06-17 13:00:34 -04:00
posts_count = ( SELECT count ( * ) FROM posts WHERE deleted_at IS NULL AND topic_id = :topic_id ) ,
last_posted_at = ( SELECT MAX ( created_at ) FROM POSTS WHERE topic_id = :topic_id AND deleted_at IS NULL )
2013-02-05 14:16:51 -05:00
WHERE id = :topic_id
2013-02-07 16:45:24 +01:00
RETURNING highest_post_number " , topic_id: topic_id
2013-02-05 14:16:51 -05:00
highest_post_number = result . first [ 'highest_post_number' ] . to_i
# Update the forum topic user records
exec_sql " UPDATE topic_users
SET last_read_post_number = CASE
WHEN last_read_post_number > :highest THEN :highest
ELSE last_read_post_number
END ,
2014-10-31 09:40:35 +11:00
highest_seen_post_number = CASE
WHEN highest_seen_post_number > :highest THEN :highest
ELSE highest_seen_post_number
2013-02-05 14:16:51 -05:00
END
WHERE topic_id = :topic_id " ,
highest : highest_post_number ,
topic_id : topic_id
end
# This calculates the geometric mean of the posts and stores it with the topic
2014-02-27 11:45:20 +11:00
def self . calculate_avg_time ( min_topic_age = nil )
builder = SqlBuilder . new ( " UPDATE topics
2013-02-05 14:16:51 -05:00
SET avg_time = x . gmean
2013-02-07 16:45:24 +01:00
FROM ( SELECT topic_id ,
2013-02-05 14:16:51 -05:00
round ( exp ( avg ( ln ( avg_time ) ) ) ) AS gmean
FROM posts
2013-06-30 01:30:26 +10:00
WHERE avg_time > 0 AND avg_time IS NOT NULL
2013-02-05 14:16:51 -05:00
GROUP BY topic_id ) AS x
2014-02-27 11:45:20 +11:00
/ *where* / " )
builder . where ( " x.topic_id = topics.id AND
( topics . avg_time < > x . gmean OR topics . avg_time IS NULL ) " )
if min_topic_age
2014-10-27 22:06:43 +01:00
builder . where ( " topics.bumped_at > :bumped_at " , bumped_at : min_topic_age )
2014-02-27 11:45:20 +11:00
end
builder . exec
2013-02-05 14:16:51 -05:00
end
2014-10-27 22:06:43 +01:00
def changed_to_category ( new_category )
return true if new_category . blank? || Category . find_by ( topic_id : id ) . present?
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 22:06:43 +01:00
if self . category_id != new_category . id
self . category_id = new_category . id
self . update_column ( :category_id , new_category . id )
Category . where ( id : old_category . id ) . update_all ( " topic_count = topic_count - 1 " ) if old_category
2013-02-05 14:16:51 -05:00
end
2014-10-27 22:06:43 +01:00
Category . where ( id : new_category . id ) . update_all ( " topic_count = topic_count + 1 " )
CategoryFeaturedTopic . feature_topics_for ( old_category ) unless @import_mode
CategoryFeaturedTopic . feature_topics_for ( new_category ) unless @import_mode || old_category . id == new_category . id
2015-08-19 22:40:20 +02:00
CategoryUser . auto_watch_new_topic ( self , new_category )
CategoryUser . auto_track_new_topic ( self , new_category )
2013-02-07 16:45:24 +01:00
end
2014-10-27 22:06:43 +01:00
2013-10-08 14:40:31 -04:00
true
2013-02-05 14:16:51 -05:00
end
2015-07-24 16:39:03 -04:00
def add_moderator_post ( user , text , opts = nil )
opts || = { }
2013-02-05 14:16:51 -05:00
new_post = nil
2015-10-15 08:56:10 +08: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? ,
skip_notifications : opts [ :skip_notifications ] ,
topic_id : self . id ,
skip_validations : true )
new_post = creator . create
increment! ( :moderator_posts_count ) if new_post . persisted?
2013-02-05 14:16:51 -05:00
if new_post . present?
# If we are moving posts, we want to insert the moderator post where the previous posts were
# in the stream, not at the end.
new_post . update_attributes ( post_number : opts [ :post_number ] , sort_order : opts [ :post_number ] ) if opts [ :post_number ] . present?
# Grab any links that are present
TopicLink . extract_from ( new_post )
2014-07-15 17:47:24 +10:00
QuotedPost . extract_from ( new_post )
2013-02-05 14:16:51 -05:00
end
2013-02-07 16:45:24 +01: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 17:39:20 +10:00
return false if private_message?
2014-10-27 22:06:43 +01: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 22:06:43 +01:00
return true if self . category_id == new_category_id
cat = Category . find_by ( id : new_category_id )
2013-10-24 10:05:51 +11:00
return false unless cat
2014-10-27 22:06:43 +01:00
2013-02-05 14:16:51 -05:00
changed_to_category ( cat )
end
2013-06-18 17:17:01 +10:00
def remove_allowed_user ( username )
2014-09-25 17:44:48 +02:00
if user = User . find_by ( username : username )
2014-05-06 14:41:59 +01:00
topic_user = topic_allowed_users . find_by ( user_id : user . id )
2013-10-02 19:11:48 +02:00
if topic_user
topic_user . destroy
2014-09-25 17:44:48 +02:00
return true
2013-10-02 19:11:48 +02:00
end
2013-06-18 17:17:01 +10:00
end
2014-09-25 17:44:48 +02:00
false
2013-06-18 17:17:01 +10:00
end
2013-02-05 14:16:51 -05:00
# Invite a user to the topic by username or email. Returns success/failure
2014-05-09 11:45:18 +10:00
def invite ( invited_by , username_or_email , group_ids = nil )
2013-02-05 14:16:51 -05:00
if private_message?
2015-03-31 00:06:47 +05:30
# If the user exists, add them to the message.
2013-06-19 10:31:19 +10:00
user = User . find_by_username_or_email ( username_or_email )
if user && topic_allowed_users . create! ( user_id : user . id )
# Notify the user they've been invited
user . notifications . create ( notification_type : Notification . types [ :invited_to_private_message ] ,
topic_id : id ,
post_number : 1 ,
data : { topic_title : title ,
display_username : invited_by . username } . to_json )
return true
2013-02-05 14:16:51 -05:00
end
end
2013-02-07 16:45:24 +01:00
2015-04-05 13:32:43 +05:30
if username_or_email =~ / ^.+@.+$ / && ! SiteSetting . enable_sso
2015-05-13 12:34:05 +05:30
# rate limit topic invite
RateLimiter . new ( invited_by , " topic-invitations-per-day " , SiteSetting . max_topic_invitations_per_day , 1 . day . to_i ) . performed!
2013-06-19 10:31:19 +10:00
# NOTE callers expect an invite object if an invite was sent via email
2014-05-09 11:45:18 +10:00
invite_by_email ( invited_by , username_or_email , group_ids )
2013-06-19 10:31:19 +10:00
else
2015-03-31 00:06:47 +05:30
# invite existing member to a topic
2015-04-05 13:32:43 +05:30
user = User . find_by_username ( username_or_email )
2015-03-31 00:06:47 +05:30
if user && topic_allowed_users . create! ( user_id : user . id )
2015-05-13 12:34:05 +05:30
# rate limit topic invite
RateLimiter . new ( invited_by , " topic-invitations-per-day " , SiteSetting . max_topic_invitations_per_day , 1 . day . to_i ) . performed!
2015-03-31 00:06:47 +05:30
# Notify the user they've been invited
user . notifications . create ( notification_type : Notification . types [ :invited_to_topic ] ,
topic_id : id ,
post_number : 1 ,
data : { topic_title : title ,
display_username : invited_by . username } . to_json )
return true
else
false
end
2013-06-19 10:31:19 +10:00
end
2013-02-05 14:16:51 -05:00
end
2014-05-09 11:45:18 +10:00
def invite_by_email ( invited_by , email , group_ids = nil )
Invite . invite_by_email ( email , invited_by , self , group_ids )
2013-02-05 14:16:51 -05:00
end
2013-10-03 17:06:14 -04:00
def email_already_exists_for? ( invite )
invite . email_already_exists and private_message?
end
def grant_permission_to_user ( lower_email )
2014-05-06 14:41:59 +01:00
user = User . find_by ( email : lower_email )
2013-10-03 17:06:14 -04:00
topic_allowed_users . create! ( user_id : user . id )
end
2013-05-25 17:37:23 -07:00
def max_post_number
2014-08-20 12:28:34 -04:00
posts . with_deleted . maximum ( :post_number ) . to_i
2013-05-25 17:37:23 -07:00
end
2013-05-08 13:33:58 -04:00
def move_posts ( moved_by , post_ids , opts )
2013-05-25 17:40:33 -07:00
post_mover = PostMover . new ( self , moved_by , post_ids )
2013-05-08 13:33:58 -04:00
2013-05-25 17:40:33 -07:00
if opts [ :destination_topic_id ]
post_mover . to_topic opts [ :destination_topic_id ]
elsif opts [ :title ]
2013-10-29 15:30:06 -04:00
post_mover . to_new_topic ( opts [ :title ] , opts [ :category_id ] )
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
2013-02-06 12:13:41 +11:00
def update_flagged_posts_count
PostAction . update_flagged_posts_count
end
2013-03-12 12:33:42 -04:00
def update_action_counts
2015-04-18 21:53:53 +10:00
PostActionType . types . each_key do | type |
2013-03-12 12:33:42 -04:00
count_field = " #{ type } _count "
update_column ( count_field , Post . where ( topic_id : id ) . sum ( count_field ) )
end
end
2013-05-22 23:21:19 -07:00
def posters_summary ( options = { } )
@posters_summary || = TopicPostersSummary . new ( self , options ) . summary
2013-02-05 14:16:51 -05:00
end
2014-05-12 09:32:49 +02:00
def participants_summary ( options = { } )
@participants_summary || = TopicParticipantsSummary . new ( self , options ) . summary
end
2014-06-16 19:21:21 +02: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?
self . archetype = Archetype . banner
self . add_moderator_post ( user , I18n . t ( " archetypes.banner.message.make " ) )
self . save
2014-06-18 20:04:10 +02:00
2015-05-04 12:21:00 +10:00
MessageBus . publish ( '/site/banner' , banner )
2014-06-16 19:21:21 +02:00
end
def remove_banner! ( user )
self . archetype = Archetype . default
self . add_moderator_post ( user , I18n . t ( " archetypes.banner.message.remove " ) )
self . save
2014-06-18 20:04:10 +02:00
2015-05-04 12:21:00 +10:00
MessageBus . publish ( '/site/banner' , nil )
2014-06-18 20:04:10 +02:00
end
def banner
post = self . posts . order ( :post_number ) . limit ( 1 ) . first
{
html : post . cooked ,
2015-06-09 19:31:14 +02:00
key : self . id ,
url : self . url
2014-06-18 20:04:10 +02:00
}
2014-06-16 19:21:21 +02: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-24 12:46:43 +10:00
unless slug = read_attribute ( :slug )
return '' unless title . present?
2015-05-04 19:48:37 +08:00
slug = Slug . for ( title )
2013-04-24 12:46:43 +10:00
if new_record?
write_attribute ( :slug , slug )
else
update_column ( :slug , slug )
end
end
slug
end
def title = ( t )
2015-05-04 19:48:37 +08:00
slug = Slug . for ( t . to_s )
2013-04-24 12:46:43 +10:00
write_attribute ( :slug , slug )
2015-09-24 13:37:53 +10:00
write_attribute ( :fancy_title , nil )
2013-04-24 12:46:43 +10:00
write_attribute ( :title , t )
2013-02-05 14:16:51 -05:00
end
2013-05-23 23:06:38 -07: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
2013-05-09 17:37:34 +10:00
def self . url ( id , slug , post_number = nil )
url = " #{ Discourse . base_url } /t/ #{ slug } / #{ id } "
url << " / #{ post_number } " if post_number . to_i > 1
url
end
2013-05-25 17:38:15 -07:00
def url ( post_number = nil )
self . class . url id , slug , post_number
end
2015-09-28 16:43:38 +10:00
def self . relative_url ( id , slug , post_number = nil )
2015-04-30 12:46:19 -04:00
url = " #{ Discourse . base_uri } /t/ #{ slug } / #{ id } "
2013-05-09 17:37:34 +10:00
url << " / #{ post_number } " if post_number . to_i > 1
2013-02-05 14:16:51 -05:00
url
end
2015-09-28 16:43:38 +10:00
def relative_url ( post_number = nil )
Topic . relative_url ( id , slug , post_number )
end
2015-08-12 23:00:16 +02:00
def unsubscribe_url
" #{ url } /unsubscribe "
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-10 10:56:56 +10:00
def re_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : nil )
end
2015-07-29 16:34:21 +02:00
def update_pinned ( status , global = false , pinned_until = nil )
pinned_until = Time . parse ( pinned_until ) rescue nil
update_columns (
pinned_at : status ? Time . now : nil ,
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 21:54:12 +03:00
" #{ Draft :: EXISTING_TOPIC } #{ id } "
2013-02-05 14:16:51 -05:00
end
2013-05-23 23:06:38 -07: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 16:34:21 +02: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
2013-11-11 10:52:44 +11:00
def self . auto_close
2013-12-18 14:09:49 -05:00
Topic . where ( " NOT closed AND auto_close_at < ? AND auto_close_user_id IS NOT NULL " , 1 . minute . ago ) . each do | t |
2013-11-11 10:52:44 +11:00
t . auto_close
end
end
def auto_close ( closer = nil )
if auto_close_at && ! closed? && ! deleted_at && auto_close_at < 5 . minutes . from_now
closer || = auto_close_user
if Guardian . new ( closer ) . can_moderate? ( self )
update_status ( 'autoclosed' , true , closer )
end
end
end
2013-11-26 19:06:20 -05:00
# Valid arguments for the auto close time:
# * An integer, which is the number of hours from now to close the topic.
# * A time, like "12:00", which is the time at which the topic will close in the current day
# or the next day if that time has already passed today.
# * A timestamp, like "2013-11-25 13:00", when the topic should close.
# * A timestamp with timezone in JSON format. (e.g., "2013-11-26T21:00:00.000Z")
# * nil, to prevent the topic from automatically closing.
2015-05-27 12:22:34 -04:00
# Options:
# * by_user: User who is setting the auto close time
# * timezone_offset: (Integer) offset from UTC in minutes of the given argument. Default 0.
def set_auto_close ( arg , opts = { } )
2014-10-10 18:21:44 +02:00
self . auto_close_hours = nil
2015-05-27 12:22:34 -04:00
by_user = opts [ :by_user ]
offset_minutes = opts [ :timezone_offset ]
2014-10-10 18:21:44 +02:00
if self . auto_close_based_on_last_post
num_hours = arg . to_f
if num_hours > 0
last_post_created_at = self . ordered_posts . last . try ( :created_at ) || Time . zone . now
self . auto_close_at = last_post_created_at + num_hours . hours
self . auto_close_hours = num_hours
else
self . auto_close_at = nil
end
2013-11-26 19:06:20 -05:00
else
2015-05-27 12:22:34 -04:00
utc = Time . find_zone ( " UTC " )
2014-10-10 18:21:44 +02:00
if arg . is_a? ( String ) && m = / ^( \ d{1,2}):( \ d{2})(?: \ s*[AP]M)?$ /i . match ( arg . strip )
2015-05-27 12:22:34 -04:00
# a time of day in client's time zone, like "15:00"
now = utc . now
self . auto_close_at = utc . local ( now . year , now . month , now . day , m [ 1 ] . to_i , m [ 2 ] . to_i )
self . auto_close_at += offset_minutes * 60 if offset_minutes
2014-10-10 18:21:44 +02:00
self . auto_close_at += 1 . day if self . auto_close_at < now
2015-05-27 12:22:34 -04:00
elsif arg . is_a? ( String ) && arg . include? ( " - " ) && timestamp = utc . parse ( arg )
# a timestamp in client's time zone, like "2015-5-27 12:00"
2014-10-10 18:21:44 +02:00
self . auto_close_at = timestamp
2015-05-27 12:22:34 -04:00
self . auto_close_at += offset_minutes * 60 if offset_minutes
2014-10-10 18:21:44 +02:00
self . errors . add ( :auto_close_at , :invalid ) if timestamp < Time . zone . now
else
num_hours = arg . to_f
if num_hours > 0
self . auto_close_at = num_hours . hours . from_now
self . auto_close_hours = num_hours
else
self . auto_close_at = nil
end
end
2013-11-26 19:06:20 -05:00
end
2014-10-10 18:21:44 +02:00
if self . auto_close_at . nil?
self . auto_close_started_at = nil
else
if self . auto_close_based_on_last_post
self . auto_close_started_at = Time . zone . now
else
self . auto_close_started_at || = Time . zone . now
end
2014-11-26 19:51:07 +01:00
if by_user . try ( :staff? ) || by_user . try ( :trust_level ) == TrustLevel [ 4 ]
2013-06-06 17:04:10 -04:00
self . auto_close_user = by_user
else
2014-11-26 19:51:07 +01:00
self . auto_close_user || = ( self . user . staff? || self . user . trust_level == TrustLevel [ 4 ] ? self . user : Discourse . system_user )
2013-06-06 17:04:10 -04:00
end
end
2014-10-10 18:21:44 +02:00
2013-06-06 17:04:10 -04:00
self
2013-05-07 14:25:41 -04:00
end
2013-07-14 11:24:16 +10:00
def read_restricted_category?
category && category . read_restricted
2013-05-19 23:04:53 -07:00
end
2013-07-08 15:23:20 -04:00
2013-12-12 03:41:34 +01:00
def acting_user
@acting_user || user
end
def acting_user = ( u )
@acting_user = u
end
2014-03-24 12:19:08 +11: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-06-22 19:46:51 +02: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
2015-06-24 15:19:39 +02:00
def self . time_to_first_response ( sql , opts = nil )
opts || = { }
2015-06-22 19:46:51 +02:00
builder = SqlBuilder . new ( sql )
2015-06-24 15:19:39 +02: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 ]
2015-06-24 15:19:39 +02:00
builder . where ( " t.category_id = :category_id " , category_id : opts [ :category_id ] ) if opts [ :category_id ]
2015-06-22 19:46:51 +02: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 " )
2015-09-14 13:36:41 -04:00
builder . where ( " p.user_id in (:user_ids) " , { user_ids : opts [ :user_ids ] } ) if opts [ :user_ids ]
2015-06-22 19:46:51 +02:00
builder . where ( " EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0 " )
builder . exec
end
2015-09-14 13:36:41 -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 19:46:51 +02:00
end
2015-06-24 15:19:39 +02:00
def self . time_to_first_response_total ( opts = nil )
total = time_to_first_response ( TIME_TO_FIRST_RESPONSE_TOTAL_SQL , opts )
total . first [ " hours " ] . to_f . round ( 2 )
2015-06-22 19:46:51 +02: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
LEFT JOIN posts p ON p . topic_id = t . id AND p . user_id != t . user_id AND p . deleted_at IS NULL
/ *where* /
GROUP BY t . id
) tt
WHERE tt . first_reply IS NULL
GROUP BY tt . created_at
ORDER BY tt . created_at
SQL
2015-06-24 15:19:39 +02:00
def self . with_no_response_per_day ( start_date , end_date , category_id = nil )
2015-06-25 18:45:11 -04:00
builder = SqlBuilder . new ( WITH_NO_RESPONSE_SQL )
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
builder . where ( " t.category_id = :category_id " , category_id : category_id ) if category_id
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
builder . exec
2015-06-22 19:46:51 +02: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
LEFT JOIN posts p ON p . topic_id = t . id AND p . user_id != t . user_id AND p . deleted_at IS NULL
/ *where* /
GROUP BY t . id
) tt
WHERE tt . first_reply IS NULL
SQL
def self . with_no_response_total ( opts = { } )
builder = SqlBuilder . new ( WITH_NO_RESPONSE_TOTAL_SQL )
builder . where ( " t.category_id = :category_id " , category_id : opts [ :category_id ] ) if opts [ :category_id ]
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
2015-06-30 15:42:38 -04:00
builder . exec . first [ " count " ] . to_i
2015-06-22 19:46:51 +02:00
end
2013-07-08 15:23:20 -04:00
private
2013-10-16 14:58:18 +05:30
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 } " )
2013-07-08 15:23:20 -04:00
end
2013-10-16 14:58:18 +05:30
end
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 )
2015-02-10 22:45:46 -08:00
RateLimiter . new ( user , " #{ key } -per-day " , SiteSetting . send ( method_name ) , 1 . day . to_i )
2013-10-16 14:58:18 +05:30
end
2013-08-13 23:14:56 -05:00
2013-02-05 14:16:51 -05:00
end
2013-05-24 12:48:32 +10:00
2015-01-27 12:13:45 -05:00
2013-05-24 12:48:32 +10:00
# == Schema Information
#
# Table name: topics
#
2014-11-20 14:53:15 +11:00
# id :integer not null, primary key
# title :string(255) not null
# 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
# image_url :string(255)
# off_topic_count :integer default(0), not null
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# bookmark_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
# vote_count :integer default(0), not null
# archetype :string(255) default("regular"), not null
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# illegal_count :integer default(0), not null
# inappropriate_count :integer default(0), not null
# pinned_at :datetime
# score :float
# percent_rank :float default(1.0), not null
# notify_user_count :integer default(0), not null
# subtype :string(255)
# slug :string(255)
# auto_close_at :datetime
# auto_close_user_id :integer
# auto_close_started_at :datetime
# deleted_by_id :integer
# participant_count :integer default(1)
# word_count :integer
# excerpt :string(1000)
# pinned_globally :boolean default(FALSE), not null
# auto_close_based_on_last_post :boolean default(FALSE)
# auto_close_hours :float
2015-09-18 10:41:10 +10:00
# pinned_until :datetime
2013-05-24 12:48:32 +10:00
#
# Indexes
#
2015-09-18 10:41:10 +10:00
# idx_topics_front_page (deleted_at,visible,archetype,category_id,id)
# idx_topics_user_id_deleted_at (user_id)
# index_topics_on_bumped_at (bumped_at)
# index_topics_on_created_at_and_visible (created_at,visible)
# index_topics_on_id_and_deleted_at (id,deleted_at)
# index_topics_on_pinned_at (pinned_at)
# index_topics_on_pinned_globally (pinned_globally)
2013-05-24 12:48:32 +10:00
#