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-05-07 14:39:01 +10:00
require_dependency 'trashable'
2013-02-05 14:16:51 -05:00
class Topic < ActiveRecord :: Base
2013-02-19 16:08:23 -05:00
include ActionView :: Helpers
2013-02-05 14:16:51 -05:00
include RateLimiter :: OnCreateRecord
2013-05-07 14:39:01 +10:00
include Trashable
2013-02-05 14:16:51 -05:00
2013-03-12 12:33:42 -04:00
def self . max_sort_order
2 ** 31 - 1
end
def self . featured_users_count
4
end
2013-02-05 14:16:51 -05:00
2013-02-25 22:13:36 +03:00
versioned if : :new_version_required?
2013-05-07 14:39:01 +10:00
def trash!
super
update_flagged_posts_count
end
def recover!
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
2013-02-07 16:45:24 +01:00
validate :title_quality
2013-02-05 14:16:51 -05:00
validates_presence_of :title
2013-04-10 14:54:10 +02:00
validate :title , - > { SiteSetting . topic_title_length . include? :length }
2013-02-05 14:16:51 -05:00
serialize :meta_data , ActiveRecord :: Coders :: Hstore
2013-04-22 13:48:05 +10:00
before_validation :sanitize_title
2013-02-05 14:16:51 -05:00
validate :unique_title
2013-02-07 16:45:24 +01:00
2013-02-05 14:16:51 -05:00
belongs_to :category
has_many :posts
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
2013-03-28 13:02:59 -04:00
has_one :hot_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
# When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
2013-04-02 16:52:51 -04:00
attr_accessor :topic_list
2013-02-05 14:16:51 -05:00
# The regular order
scope :topic_list_order , lambda { order ( 'topics.bumped_at desc' ) }
# Return private message topics
scope :private_messages , lambda {
where ( archetype : Archetype :: private_message )
}
scope :listable_topics , lambda { where ( 'topics.archetype <> ?' , [ Archetype . private_message ] ) }
2013-04-29 16:33:24 +10:00
scope :by_newest , order ( 'topics.created_at desc, topics.id desc' )
2013-02-27 19:36:12 -08:00
2013-02-05 14:16:51 -05:00
# Helps us limit how many favorites can be made in a day
class FavoriteLimiter < RateLimiter
def initialize ( user )
2013-02-07 16:45:24 +01:00
super ( user , " favorited: #{ Date . today . to_s } " , SiteSetting . max_favorites_per_day , 1 . day . to_i )
2013-02-05 14:16:51 -05:00
end
end
before_create do
self . bumped_at || = Time . now
2013-02-28 21:54:12 +03:00
self . last_post_user_id || = user_id
2013-05-15 15:19:41 -04:00
if ! @ignore_category_auto_close and self . category and self . category . auto_close_days and self . auto_close_at . nil?
self . auto_close_at = self . category . auto_close_days . days . from_now
self . auto_close_user = ( self . user . staff? ? self . user : Discourse . system_user )
end
2013-02-05 14:16:51 -05:00
end
after_create do
changed_to_category ( category )
2013-03-06 15:17:07 -05:00
TopicUser . change ( user_id , id ,
notification_level : TopicUser . notification_levels [ :watching ] ,
notifications_reason_id : TopicUser . notification_reasons [ :created_topic ] )
2013-02-28 21:54:12 +03:00
if archetype == Archetype . private_message
DraftSequence . next! ( user , Draft :: NEW_PRIVATE_MESSAGE )
2013-02-05 14:16:51 -05:00
else
2013-02-28 21:54:12 +03:00
DraftSequence . next! ( user , Draft :: NEW_TOPIC )
2013-02-05 14:16:51 -05:00
end
end
2013-05-07 14:25:41 -04:00
before_save do
if ( auto_close_at_changed? and ! auto_close_at_was . nil? ) or ( auto_close_user_id_changed? and auto_close_at )
Jobs . cancel_scheduled_job ( :close_topic , { topic_id : id } )
2013-05-15 15:19:41 -04:00
true
2013-05-07 14:25:41 -04:00
end
end
after_save do
if auto_close_at and ( auto_close_at_changed? or auto_close_user_id_changed? )
Jobs . enqueue_at ( auto_close_at , :close_topic , { topic_id : id , user_id : auto_close_user_id || user_id } )
end
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
# TODO we should probably change this from 3 queries to 1
User . where ( 'id in (?)' , allowed_users . select ( 'users.id' ) . to_a + allowed_group_users . select ( 'users.id' ) . to_a )
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
2013-02-07 16:45:24 +01:00
RateLimiter . new ( user , " topics-per-day: #{ Date . today . to_s } " , SiteSetting . max_topics_per_day , 1 . day . to_i )
2013-02-05 14:16:51 -05:00
end
def limit_private_messages_per_day
return unless private_message?
2013-02-07 16:45:24 +01:00
RateLimiter . new ( user , " pms-per-day: #{ Date . today . to_s } " , SiteSetting . max_private_messages_per_day , 1 . day . to_i )
2013-02-05 14:16:51 -05:00
end
# Validate unique titles if a site setting is set
def unique_title
return if SiteSetting . allow_duplicate_topic_titles?
# Let presence validation catch it if it's blank
return if title . blank?
# Private messages can be called whatever they want
return if private_message?
finder = Topic . listable_topics . where ( " lower(title) = ? " , title . downcase )
finder = finder . where ( " id != ? " , self . id ) if self . id . present?
errors . add ( :title , I18n . t ( :has_already_been_used ) ) if finder . exists?
end
2013-02-25 19:42:20 +03:00
def fancy_title
2013-02-19 16:08:23 -05:00
return title unless SiteSetting . title_fancy_entities?
# We don't always have to require this, if fancy is disabled
2013-03-12 12:33:42 -04:00
# see: http://meta.discourse.org/t/pattern-for-defer-loading-gems-and-profiling-with-perftools-rb/4629
2013-03-10 07:13:52 -07:00
require 'redcarpet' unless defined? Redcarpet
2013-02-19 16:08:23 -05:00
Redcarpet :: Render :: SmartyPants . render ( title )
end
2013-02-06 15:47:36 -05:00
2013-02-06 20:09:31 -05:00
def title_quality
# We don't care about quality on private messages
return if private_message?
2013-02-25 19:42:20 +03:00
sentinel = TextSentinel . title_sentinel ( title )
2013-02-06 20:09:31 -05:00
if sentinel . valid?
2013-04-10 11:00:50 +02:00
# clean up the title
self . title = TextCleaner . clean_title ( sentinel . text )
2013-02-06 20:09:31 -05:00
else
2013-02-26 19:27:59 +03:00
errors . add ( :title , I18n . t ( :is_invalid ) )
2013-02-06 15:47:36 -05:00
end
end
2013-04-22 13:48:05 +10:00
def sanitize_title
if self . title . present?
self . title = sanitize ( title , tags : [ ] , attributes : [ ] )
self . title . strip!
end
end
2013-02-05 14:16:51 -05:00
def new_version_required?
2013-02-26 19:27:59 +03:00
title_changed? || category_id_changed?
2013-02-05 14:16:51 -05:00
end
2013-02-21 21:22:02 -06:00
# Returns new topics since a date for display in email digest.
2013-02-05 14:16:51 -05:00
def self . new_topics ( since )
Topic
. visible
2013-03-05 17:21:32 -05:00
. where ( closed : false , archived : false )
2013-02-21 21:22:02 -06:00
. created_since ( since )
2013-02-05 14:16:51 -05:00
. listable_topics
. topic_list_order
. includes ( :user )
2013-02-07 16:45:24 +01:00
. limit ( 5 )
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
def update_meta_data ( data )
self . meta_data = ( self . meta_data || { } ) . merge ( data . stringify_keys )
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-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 )
2013-02-28 21:54:12 +03:00
return unless meta_data . present?
2013-02-07 16:45:24 +01:00
meta_data [ key . to_s ]
2013-02-05 14:16:51 -05:00
end
def self . visible
where ( visible : true )
end
2013-02-25 19:42:20 +03:00
2013-02-16 14:57:16 -06:00
def self . created_since ( time_ago )
where ( " created_at > ? " , time_ago )
end
2013-02-25 19:42:20 +03:00
2013-04-03 13:25:52 -04:00
def self . listable_count_per_day ( sinceDaysAgo = 30 )
listable_topics . where ( 'created_at > ?' , sinceDaysAgo . days . ago ) . 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-02-05 14:16:51 -05:00
self . archetype == Archetype . private_message
end
def links_grouped
2013-02-07 16:45:24 +01:00
exec_sql ( " SELECT ftl.url,
2013-02-05 14:16:51 -05:00
ft . title ,
2013-02-07 16:45:24 +01:00
ftl . link_topic_id ,
2013-02-05 14:16:51 -05:00
ftl . reflection ,
ftl . internal ,
MIN ( ftl . user_id ) AS user_id ,
SUM ( clicks ) AS clicks
FROM topic_links AS ftl
LEFT OUTER JOIN topics AS ft ON ftl . link_topic_id = ft . id
WHERE ftl . topic_id = ?
GROUP BY ftl . url , ft . title , ftl . link_topic_id , ftl . reflection , ftl . internal
2013-02-07 16:45:24 +01:00
ORDER BY clicks DESC " ,
2013-02-28 21:54:12 +03:00
id ) . to_a
2013-02-05 14:16:51 -05:00
end
2013-03-14 14:45:29 -04:00
# Search for similar topics
def self . similar_to ( title , raw )
return [ ] unless title . present?
return [ ] unless raw . present?
# For now, we only match on title. We'll probably add body later on, hence the API hook
Topic . select ( sanitize_sql_array ( [ " topics.*, similarity(topics.title, :title) AS similarity " , title : title ] ) )
. visible
. where ( closed : false , archived : false )
. listable_topics
2013-03-19 13:51:25 -04:00
. limit ( SiteSetting . max_similar_results )
2013-03-14 14:45:29 -04:00
. order ( 'similarity desc' )
. all
end
2013-03-06 15:17:07 -05:00
2013-03-14 14:45:29 -04:00
def update_status ( property , status , user )
2013-02-05 14:16:51 -05:00
Topic . transaction do
2013-03-06 15:17:07 -05:00
# Special case: if it's pinned, update that
if property . to_sym == :pinned
update_pinned ( status )
else
# otherwise update the column
2013-05-07 14:25:41 -04:00
update_column ( property == 'autoclosed' ? 'closed' : property , status )
2013-03-06 15:17:07 -05:00
end
2013-02-05 14:16:51 -05:00
key = " topic_statuses. #{ property } _ "
key << ( status ? 'enabled' : 'disabled' )
opts = { }
# We don't bump moderator posts except for the re-open post.
2013-05-07 14:25:41 -04:00
opts [ :bump ] = true if ( property == 'closed' or property == 'autoclosed' ) and ( ! status )
message = property != 'autoclosed' ? I18n . t ( key ) : I18n . t ( key , count : ( ( ( self . auto_close_at || Time . zone . now ) - self . created_at ) / 86_400 ) . round )
2013-02-05 14:16:51 -05:00
2013-05-07 14:25:41 -04:00
add_moderator_post ( user , message , opts )
2013-02-05 14:16:51 -05:00
end
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 ) ,
posts_count = ( SELECT count ( * ) FROM posts WHERE deleted_at IS NULL AND topic_id = :topic_id )
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 ,
seen_post_count = CASE
WHEN seen_post_count > :highest THEN :highest
ELSE seen_post_count
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
def self . calculate_avg_time
exec_sql ( " UPDATE topics
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
GROUP BY topic_id ) AS x
2013-02-07 16:45:24 +01:00
WHERE x . topic_id = topics . id " )
2013-02-05 14:16:51 -05:00
end
def changed_to_category ( cat )
return if cat . blank?
2013-02-28 21:54:12 +03:00
return if Category . where ( topic_id : id ) . first . present?
2013-02-05 14:16:51 -05:00
Topic . transaction do
old_category = category
2013-03-05 01:42:44 +01:00
if category_id . present? && category_id != cat . id
2013-02-05 14:16:51 -05:00
Category . update_all 'topic_count = topic_count - 1' , [ 'id = ?' , category_id ]
end
self . category_id = cat . id
2013-02-28 21:54:12 +03:00
save
2013-02-05 14:16:51 -05:00
2013-02-07 16:45:24 +01:00
CategoryFeaturedTopic . feature_topics_for ( old_category )
2013-02-28 21:54:12 +03:00
Category . update_all 'topic_count = topic_count + 1' , id : cat . id
2013-02-05 14:16:51 -05:00
CategoryFeaturedTopic . feature_topics_for ( cat ) unless old_category . try ( :id ) == cat . try ( :id )
2013-02-07 16:45:24 +01:00
end
2013-02-05 14:16:51 -05:00
end
def add_moderator_post ( user , text , opts = { } )
new_post = nil
Topic . transaction do
2013-03-28 16:40:54 -04:00
creator = PostCreator . new ( user ,
raw : text ,
post_type : Post . types [ :moderator_action ] ,
no_bump : opts [ :bump ] . blank? ,
topic_id : self . id )
new_post = creator . create
2013-02-07 16:45:24 +01:00
increment! ( :moderator_posts_count )
2013-02-05 14:16:51 -05:00
new_post
end
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 )
end
2013-02-07 16:45:24 +01:00
2013-02-05 14:16:51 -05:00
new_post
end
# Changes the category to a new name
def change_category ( name )
# If the category name is blank, reset the attribute
if name . blank?
if category_id . present?
CategoryFeaturedTopic . feature_topics_for ( category )
2013-02-28 21:54:12 +03:00
Category . update_all 'topic_count = topic_count - 1' , id : category_id
2013-02-05 14:16:51 -05:00
end
self . category_id = nil
2013-02-28 21:54:12 +03:00
save
2013-02-05 14:16:51 -05:00
return
end
cat = Category . where ( name : name ) . first
return if cat == category
changed_to_category ( cat )
end
def featured_user_ids
[ featured_user1_id , featured_user2_id , featured_user3_id , featured_user4_id ] . uniq . compact
end
# Invite a user to the topic by username or email. Returns success/failure
def invite ( invited_by , username_or_email )
if private_message?
# If the user exists, add them to the topic.
2013-02-26 19:27:59 +03:00
user = User . find_by_username_or_email ( username_or_email ) . first
2013-02-05 14:16:51 -05:00
if user . present?
if topic_allowed_users . create! ( user_id : user . id )
# Notify the user they've been invited
2013-03-01 15:07:44 +03:00
user . notifications . create ( notification_type : Notification . types [ :invited_to_private_message ] ,
2013-02-28 21:54:12 +03:00
topic_id : id ,
2013-02-05 14:16:51 -05:00
post_number : 1 ,
2013-02-28 21:54:12 +03:00
data : { topic_title : title ,
display_username : invited_by . username } . to_json )
2013-02-07 16:45:24 +01:00
return true
2013-02-05 14:16:51 -05:00
end
elsif username_or_email =~ / ^.+@.+$ /
# If the user doesn't exist, but it looks like an email, invite the user by email.
return invite_by_email ( invited_by , username_or_email )
end
else
# Success is whether the invite was created
return invite_by_email ( invited_by , username_or_email ) . present?
end
2013-02-07 16:45:24 +01:00
2013-02-05 14:16:51 -05:00
false
end
# Invite a user by email and return the invite. Return the previously existing invite
2013-02-07 16:45:24 +01:00
# if already exists. Returns nil if the invite can't be created.
2013-02-05 14:16:51 -05:00
def invite_by_email ( invited_by , email )
2013-04-15 02:20:33 +02:00
lower_email = Email . downcase ( email )
2013-02-05 14:16:51 -05:00
invite = Invite . with_deleted . where ( 'invited_by_id = ? and email = ?' , invited_by . id , lower_email ) . first
2013-02-07 16:45:24 +01:00
2013-02-05 14:16:51 -05:00
if invite . blank?
2013-02-07 16:45:24 +01:00
invite = Invite . create ( invited_by : invited_by , email : lower_email )
2013-02-05 14:16:51 -05:00
unless invite . valid?
# If the email already exists, grant permission to that user
if invite . email_already_exists and private_message?
user = User . where ( email : lower_email ) . first
topic_allowed_users . create! ( user_id : user . id )
end
2013-02-28 21:54:12 +03:00
return
2013-02-05 14:16:51 -05:00
end
end
# Recover deleted invites if we invite them again
invite . recover if invite . deleted_at . present?
topic_invites . create ( invite_id : invite . id )
Jobs . enqueue ( :invite_email , invite_id : invite . id )
2013-02-07 16:45:24 +01:00
invite
2013-02-05 14:16:51 -05:00
end
2013-05-13 14:06:16 -04:00
def move_posts_to_topic ( moved_by , post_ids , destination_topic )
2013-05-08 13:33:58 -04:00
to_move = posts . where ( id : post_ids ) . order ( :created_at )
raise Discourse :: InvalidParameters . new ( :post_ids ) if to_move . blank?
2013-02-05 14:16:51 -05:00
first_post_number = nil
Topic . transaction do
2013-05-08 13:33:58 -04:00
# Find the max post number in the topic
max_post_number = destination_topic . posts . maximum ( :post_number ) || 0
2013-02-05 14:16:51 -05:00
to_move . each_with_index do | post , i |
2013-05-13 14:06:16 -04:00
if post . post_number == 1
# We have a special case for the OP, we copy it instead of deleting it.
result = PostCreator . new ( post . user ,
raw : post . raw ,
topic_id : destination_topic . id ,
acting_user : moved_by ) . create
else
first_post_number || = post . post_number
# Move the post and raise an error if it couldn't be moved
row_count = Post . update_all [ " post_number = :post_number, topic_id = :topic_id, sort_order = :post_number " , post_number : max_post_number + i + 1 , topic_id : destination_topic . id ] , id : post . id , topic_id : id
raise Discourse :: InvalidParameters . new ( :post_ids ) if row_count == 0
end
2013-02-05 14:16:51 -05:00
end
2013-05-08 13:33:58 -04:00
end
first_post_number
end
def move_posts ( moved_by , post_ids , opts )
topic = nil
first_post_number = nil
if opts [ :title ] . present?
# If we're moving to a new topic...
Topic . transaction do
topic = Topic . create ( user : moved_by , title : opts [ :title ] , category : category )
2013-05-13 14:06:16 -04:00
first_post_number = move_posts_to_topic ( moved_by , post_ids , topic )
2013-05-08 13:33:58 -04:00
end
elsif opts [ :destination_topic_id ] . present?
# If we're moving to an existing topic...
topic = Topic . where ( id : opts [ :destination_topic_id ] ) . first
Guardian . new ( moved_by ) . ensure_can_see! ( topic )
2013-05-13 14:06:16 -04:00
first_post_number = move_posts_to_topic ( moved_by , post_ids , topic )
2013-02-05 14:16:51 -05:00
end
# Add a moderator post explaining that the post was moved
if topic . present?
topic_url = " #{ Discourse . base_url } #{ topic . relative_url } "
2013-05-08 13:33:58 -04:00
topic_link = " [ #{ topic . title } ]( #{ topic_url } ) "
2013-02-05 14:16:51 -05:00
2013-02-25 22:13:36 +03:00
add_moderator_post ( moved_by , I18n . t ( " move_posts.moderator_post " , count : post_ids . size , topic_link : topic_link ) , post_number : first_post_number )
2013-02-05 14:16:51 -05:00
Jobs . enqueue ( :notify_moved_posts , post_ids : post_ids , moved_by_id : moved_by . id )
2013-03-12 12:33:42 -04:00
topic . update_statistics
update_statistics
2013-02-05 14:16:51 -05:00
end
2013-03-12 12:33:42 -04:00
2013-02-05 14:16:51 -05:00
topic
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
PostActionType . types . keys . each do | type |
count_field = " #{ type } _count "
update_column ( count_field , Post . where ( topic_id : id ) . sum ( count_field ) )
end
end
# Chooses which topic users to feature
def feature_topic_users ( args = { } )
reload
to_feature = posts
# Don't include the OP or the last poster
to_feature = to_feature . where ( 'user_id NOT IN (?, ?)' , user_id , last_post_user_id )
# Exclude a given post if supplied (in the case of deletes)
to_feature = to_feature . where ( " id <> ? " , args [ :except_post_id ] ) if args [ :except_post_id ] . present?
# Clear the featured users by default
Topic . featured_users_count . times do | i |
send ( " featured_user #{ i + 1 } _id= " , nil )
end
# Assign the featured_user{x} columns
to_feature = to_feature . group ( :user_id ) . order ( 'count_all desc' ) . limit ( Topic . featured_users_count )
to_feature . count . keys . each_with_index do | user_id , i |
send ( " featured_user #{ i + 1 } _id= " , user_id )
end
save
end
2013-02-05 14:16:51 -05:00
# Create the summary of the interesting posters in a topic. Cheats to avoid
# many queries.
2013-02-28 21:54:12 +03:00
def posters_summary ( topic_user = nil , current_user = nil , opts = { } )
2013-02-05 14:16:51 -05:00
return @posters_summary if @posters_summary . present?
descriptions = { }
# Use an avatar lookup object if we have it, otherwise create one just for this forum topic
al = opts [ :avatar_lookup ]
if al . blank?
al = AvatarLookup . new ( [ user_id , last_post_user_id , featured_user1_id , featured_user2_id , featured_user3_id ] )
end
# Helps us add a description to a poster
add_description = lambda do | u , desc |
if u . present?
descriptions [ u . id ] || = [ ]
descriptions [ u . id ] << I18n . t ( desc )
end
end
add_description . call ( al [ user_id ] , :original_poster )
add_description . call ( al [ featured_user1_id ] , :most_posts )
add_description . call ( al [ featured_user2_id ] , :frequent_poster )
add_description . call ( al [ featured_user3_id ] , :frequent_poster )
add_description . call ( al [ featured_user4_id ] , :frequent_poster )
2013-02-07 16:45:24 +01:00
add_description . call ( al [ last_post_user_id ] , :most_recent_poster )
2013-02-05 14:16:51 -05:00
2013-02-07 16:45:24 +01:00
@posters_summary = [ al [ user_id ] ,
al [ last_post_user_id ] ,
al [ featured_user1_id ] ,
al [ featured_user2_id ] ,
2013-02-05 14:16:51 -05:00
al [ featured_user3_id ] ,
al [ featured_user4_id ]
] . compact . uniq [ 0 .. 4 ]
unless @posters_summary [ 0 ] == al [ last_post_user_id ]
2013-02-07 16:45:24 +01:00
# shuffle last_poster to back
2013-02-05 14:16:51 -05:00
@posters_summary . reject! { | u | u == al [ last_post_user_id ] }
@posters_summary << al [ last_post_user_id ]
end
2013-02-07 16:45:24 +01:00
@posters_summary . map! do | p |
2013-04-11 16:04:20 -04:00
if p
result = TopicPoster . new
result . user = p
result . description = descriptions [ p . id ] . join ( ', ' )
result . extras = " latest " if al [ last_post_user_id ] == p
result
else
nil
end
end . compact!
2013-02-05 14:16:51 -05:00
@posters_summary
end
# Enable/disable the star on the topic
def toggle_star ( user , starred )
Topic . transaction do
2013-04-28 16:58:14 -04:00
TopicUser . change ( user , id , { starred : starred } . merge ( starred ? { starred_at : DateTime . now , unstarred_at : nil } : { unstarred_at : DateTime . now } ) )
2013-02-05 14:16:51 -05:00
# Update the star count
2013-02-07 16:45:24 +01:00
exec_sql " UPDATE topics
SET star_count = ( SELECT COUNT ( * )
FROM topic_users AS ftu
2013-02-05 14:16:51 -05:00
WHERE ftu . topic_id = topics . id
AND ftu . starred = true )
2013-02-28 21:54:12 +03:00
WHERE id = ?" , id
2013-02-05 14:16:51 -05:00
if starred
FavoriteLimiter . new ( user ) . performed!
else
FavoriteLimiter . new ( user ) . rollback!
end
end
end
2013-02-07 16:45:24 +01:00
2013-04-18 14:27:22 -04:00
def self . starred_counts_per_day ( sinceDaysAgo = 30 )
TopicUser . where ( 'starred_at > ?' , sinceDaysAgo . days . ago ) . group ( 'date(starred_at)' ) . order ( 'date(starred_at)' ) . count
end
2013-02-05 14:16:51 -05:00
# Enable/disable the mute on the topic
def toggle_mute ( user , muted )
2013-03-06 15:17:07 -05:00
TopicUser . change ( user , self . id , notification_level : muted? ( user ) ? TopicUser . notification_levels [ :regular ] : TopicUser . notification_levels [ :muted ] )
2013-02-05 14:16:51 -05:00
end
def slug
2013-04-24 12:46:43 +10:00
unless slug = read_attribute ( :slug )
return '' unless title . present?
slug = Slug . for ( title ) . presence || " topic "
if new_record?
write_attribute ( :slug , slug )
else
update_column ( :slug , slug )
end
end
slug
end
def title = ( t )
slug = " "
slug = ( Slug . for ( t ) . presence || " topic " ) if t . present?
write_attribute ( :slug , slug )
write_attribute ( :title , t )
2013-02-05 14:16:51 -05:00
end
def last_post_url
" /t/ #{ slug } / #{ id } / #{ posts_count } "
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-02-05 14:16:51 -05:00
def relative_url ( post_number = nil )
url = " /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
def muted? ( user )
return false unless user && user . id
tu = topic_users . where ( user_id : user . id ) . first
2013-03-06 15:17:07 -05:00
tu && tu . notification_level == TopicUser . notification_levels [ :muted ]
end
def clear_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : Time . now )
end
def update_pinned ( status )
update_column ( :pinned_at , status ? Time . now : nil )
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
# notification stuff
def notify_watch! ( user )
2013-03-06 15:17:07 -05:00
TopicUser . change ( user , id , notification_level : TopicUser . notification_levels [ :watching ] )
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
def notify_tracking! ( user )
2013-03-06 15:17:07 -05:00
TopicUser . change ( user , id , notification_level : TopicUser . notification_levels [ :tracking ] )
2013-02-05 14:16:51 -05:00
end
def notify_regular! ( user )
2013-03-06 15:17:07 -05:00
TopicUser . change ( user , id , notification_level : TopicUser . notification_levels [ :regular ] )
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
def notify_muted! ( user )
2013-03-06 15:17:07 -05:00
TopicUser . change ( user , id , notification_level : TopicUser . notification_levels [ :muted ] )
2013-02-05 14:16:51 -05:00
end
2013-05-07 14:25:41 -04:00
def auto_close_days = ( num_days )
2013-05-15 15:19:41 -04:00
@ignore_category_auto_close = true
2013-05-07 14:25:41 -04:00
self . auto_close_at = ( num_days and num_days . to_i > 0 . 0 ? num_days . to_i . days . from_now : nil )
end
2013-05-19 23:04:53 -07:00
def secure_category?
category && category . secure
end
2013-02-05 14:16:51 -05:00
end