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-05-22 21:52:12 -07:00
validates :title , :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
2013-05-31 15:22:34 -04:00
before_validation do
2013-07-02 12:22:56 +10:00
self . sanitize_title
2013-05-22 21:52:12 -07:00
self . title = TextCleaner . clean_title ( TextSentinel . title_sentinel ( title ) . text ) if errors [ :title ] . empty?
end
serialize :meta_data , ActiveRecord :: Coders :: Hstore
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-05-30 13:23:40 +02:00
scope :visible , where ( visible : true )
scope :created_since , lambda { | time_ago | where ( 'created_at > ?' , time_ago ) }
2013-06-13 10:27:17 +10: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
2013-06-08 23:52:06 +10:00
condition =
if ids . present?
[ " NOT c.secure or c.id in (:cats) " , cats : ids ]
else
[ " NOT c.secure " ]
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
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?
2013-06-06 17:04:10 -04:00
set_auto_close ( self . category . auto_close_days )
2013-05-15 15:19:41 -04:00
end
2013-02-05 14:16:51 -05:00
end
after_create do
changed_to_category ( category )
2013-05-23 23:06:38 -07:00
notifier . created_topic! user_id
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 )
2013-06-06 17:04:10 -04:00
self . auto_close_started_at || = Time . zone . now
2013-05-07 14:25:41 -04:00
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-06-03 16:12:24 -04:00
def best_post
posts . order ( 'score desc' ) . limit ( 1 ) . first
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
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-04-22 13:48:05 +10:00
def sanitize_title
2013-05-23 23:06:38 -07:00
self . title = sanitize ( title . to_s , tags : [ ] , attributes : [ ] ) . strip . presence
2013-04-22 13:48:05 +10:00
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-06-03 16:12:24 -04:00
# Returns hot topics since a date for display in email digest.
def self . for_digest ( user , since )
2013-02-05 14:16:51 -05:00
Topic
. visible
2013-06-08 23:52:06 +10:00
. secured ( Guardian . new ( user ) )
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
2013-06-03 16:12:24 -04:00
. order ( :percent_rank )
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-05-27 17:58:57 -07:00
def age_in_days
( ( Time . zone . now - created_at ) / 1 . day ) . round
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
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-05-24 09:13:31 -07:00
archetype == Archetype . private_message
2013-02-05 14:16:51 -05:00
end
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?
# 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 )
2013-06-12 13:43:59 -04:00
. secured ( Guardian . new ( user ) )
2013-03-14 14:45:29 -04:00
. 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-05-27 18:00:53 -07:00
def update_status ( status , enabled , user )
TopicStatusUpdate . new ( self , user ) . update! status , enabled
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 ,
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
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
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-07-01 20:45:52 +02:00
Category . where ( [ 'id = ?' , category_id ] ) . update_all 'topic_count = topic_count - 1'
2013-02-05 14:16:51 -05:00
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-07-01 20:45:52 +02:00
Category . where ( id : cat . id ) . update_all 'topic_count = topic_count + 1'
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-07-01 20:45:52 +02:00
Category . where ( id : category_id ) . update_all 'topic_count = topic_count - 1'
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
2013-06-18 17:17:01 +10:00
def remove_allowed_user ( username )
user = User . where ( username : username ) . first
if user
topic_allowed_users . where ( user_id : user . id ) . first . destroy
end
end
2013-02-05 14:16:51 -05:00
# 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-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
2013-06-19 10:31:19 +10:00
if username_or_email =~ / ^.+@.+$ /
# NOTE callers expect an invite object if an invite was sent via email
invite_by_email ( invited_by , username_or_email )
else
false
end
2013-02-05 14:16:51 -05:00
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-25 17:37:23 -07:00
def max_post_number
posts . maximum ( :post_number ) . to_i
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 ]
post_mover . to_new_topic opts [ :title ]
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
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
# Don't include the OP or the last poster
2013-05-24 09:13:31 -07:00
to_feature = posts . where ( 'user_id NOT IN (?, ?)' , user_id , last_post_user_id )
2013-03-12 12:33:42 -04:00
# 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
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
# 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 )
2013-05-24 09:13:31 -07:00
TopicUser . starred_since ( sinceDaysAgo ) . by_date_starred . count
2013-02-05 14:16:51 -05: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?
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 )
2013-05-23 23:06:38 -07:00
slug = ( Slug . for ( t . to_s ) . presence || " topic " )
2013-04-24 12:46:43 +10:00
write_attribute ( :slug , slug )
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
" /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-05-25 17:38:15 -07:00
def url ( post_number = nil )
self . class . url id , slug , post_number
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
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
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
2013-05-23 23:06:38 -07:00
def notifier
@topic_notifier || = TopicNotifier . new ( self )
end
2013-02-05 14:16:51 -05:00
# notification stuff
def notify_watch! ( user )
2013-05-23 23:06:38 -07:00
notifier . watch! user
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-05-23 23:06:38 -07:00
notifier . tracking! user
2013-02-05 14:16:51 -05:00
end
def notify_regular! ( user )
2013-05-23 23:06:38 -07:00
notifier . regular! user
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-05-23 23:06:38 -07:00
notifier . muted! user
end
def muted? ( user )
if user && user . id
notifier . muted? ( user . id )
end
end
# Enable/disable the mute on the topic
def toggle_mute ( user_id )
notifier . toggle_mute user_id
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-06-06 17:04:10 -04:00
set_auto_close ( num_days )
end
def set_auto_close ( num_days , by_user = nil )
2013-05-24 09:13:31 -07:00
num_days = num_days . to_i
self . auto_close_at = ( num_days > 0 ? num_days . days . from_now : nil )
2013-06-06 17:04:10 -04:00
if num_days > 0
self . auto_close_started_at || = Time . zone . now
if by_user and by_user . staff?
self . auto_close_user = by_user
else
self . auto_close_user || = ( self . user . staff? ? self . user : Discourse . system_user )
end
else
self . auto_close_started_at = nil
end
self
2013-05-07 14:25:41 -04:00
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
2013-05-24 12:48:32 +10:00
# == Schema Information
#
# Table name: topics
#
# 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 not null
# 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
# star_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_best_of :boolean default(FALSE), not null
# meta_data :hstore
# 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
2013-06-06 17:04:10 -04:00
# auto_close_started_at :datetime
2013-05-24 12:48:32 +10:00
#
# Indexes
#
# idx_topics_user_id_deleted_at (user_id)
# index_forum_threads_on_bumped_at (bumped_at)
#