2013-02-25 11:42:20 -05:00
#
2013-02-05 14:16:51 -05:00
# Helps us find topics. Returns a TopicList object containing the topics
# found.
#
require_dependency 'topic_list'
2013-07-12 14:38:20 -04:00
require_dependency 'suggested_topics_builder'
2013-11-13 12:26:32 -05:00
require_dependency 'topic_query_sql'
2013-02-05 14:16:51 -05:00
class TopicQuery
2013-07-16 15:20:18 -04:00
# Could be rewritten to %i if Ruby 1.9 is no longer supported
2013-12-23 18:50:36 -05:00
VALID_OPTIONS = %w( except_topic_ids
2013-11-11 19:35:57 -05:00
exclude_category
limit
page
per_page
2014-06-05 09:30:24 -04:00
min_posts
max_posts
2013-11-11 19:35:57 -05:00
topic_ids
visible
category
2014-04-16 12:05:54 -04:00
order
ascending
2013-12-13 17:18:28 -05:00
no_subcategories
2014-02-07 17:01:31 -05:00
no_definitions
2014-05-15 10:31:45 -04:00
status
2014-07-16 19:29:09 -04:00
state
2014-05-15 10:31:45 -04:00
search
2014-12-15 11:54:26 -05:00
slow_platform
2014-05-15 10:31:45 -04:00
) . map ( & :to_sym )
2013-02-05 14:16:51 -05:00
2014-04-16 12:05:54 -04:00
# Maps `order` to a columns in `topics`
2013-11-13 14:17:06 -05:00
SORTABLE_MAPPING = {
'likes' = > 'like_count' ,
2014-10-02 23:16:53 -04:00
'op_likes' = > 'op_likes' ,
2013-11-13 14:17:06 -05:00
'views' = > 'views' ,
'posts' = > 'posts_count' ,
2014-08-18 13:26:12 -04:00
'activity' = > 'bumped_at' ,
2013-11-14 15:50:36 -05:00
'posters' = > 'participant_count' ,
2014-08-27 12:42:54 -04:00
'category' = > 'category_id' ,
'created' = > 'created_at'
2013-11-13 14:17:06 -05:00
}
2013-07-16 15:20:18 -04:00
def initialize ( user = nil , options = { } )
options . assert_valid_keys ( VALID_OPTIONS )
@options = options
@user = user
2013-02-05 14:16:51 -05:00
end
2014-02-21 14:17:45 -05:00
def joined_topic_user ( list = nil )
( list || Topic ) . joins ( " LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{ @user . id . to_i } ) " )
end
2013-02-05 14:16:51 -05:00
# Return a list of suggested topics for a topic
2013-02-25 11:42:20 -05:00
def list_suggested_for ( topic )
2013-07-12 14:38:20 -04:00
builder = SuggestedTopicsBuilder . new ( topic )
2013-02-05 14:16:51 -05:00
2013-07-12 14:38:20 -04:00
# When logged in we start with different results
2013-07-16 15:20:18 -04:00
if @user
2013-08-27 20:51:49 -04:00
builder . add_results ( unread_results ( topic : topic , per_page : builder . results_left ) , :high )
2014-12-03 18:04:23 -05:00
builder . add_results ( new_results ( topic : topic , per_page : builder . category_results_left ) ) unless builder . full?
2013-02-05 14:16:51 -05:00
end
2014-12-03 18:04:23 -05:00
builder . add_results ( random_suggested ( topic , builder . results_left , builder . excluded_topic_ids ) ) unless builder . full?
2013-02-05 14:16:51 -05:00
2013-07-16 15:20:18 -04:00
create_list ( :suggested , { } , builder . results )
2013-02-05 14:16:51 -05:00
end
2013-03-27 16:17:49 -04:00
# The latest view of topics
def list_latest
2014-10-08 12:44:47 -04:00
create_list ( :latest , { } , latest_results )
2013-02-05 14:16:51 -05:00
end
2014-01-09 16:22:54 -05:00
# The starred topics
def list_starred
create_list ( :starred ) { | topics | topics . where ( 'tu.starred' ) }
2013-02-05 14:16:51 -05:00
end
def list_read
2013-04-02 16:52:51 -04:00
create_list ( :read , unordered : true ) do | topics |
topics . order ( 'COALESCE(tu.last_visited_at, topics.bumped_at) DESC' )
2013-02-05 14:16:51 -05:00
end
end
def list_new
2014-10-08 12:44:47 -04:00
create_list ( :new , { } , new_results )
2013-02-05 14:16:51 -05:00
end
def list_unread
2014-10-09 15:45:06 -04:00
create_list ( :unread , { } , unread_results )
2013-02-05 14:16:51 -05:00
end
def list_posted
2014-03-29 18:26:13 -04:00
create_list ( :posted ) { | l | l . where ( 'tu.posted' ) }
2013-02-05 14:16:51 -05:00
end
2013-12-27 12:10:35 -05:00
def list_top_for ( period )
score = " #{ period } _score "
2013-12-23 18:50:36 -05:00
create_list ( :top , unordered : true ) do | topics |
2014-01-18 13:03:09 -05:00
topics = topics . joins ( :top_topic ) . where ( " top_topics. #{ score } > 0 " )
2014-09-05 01:20:39 -04:00
if period == :yearly && @user . try ( :trust_level ) == TrustLevel [ 0 ]
2014-01-18 13:03:09 -05:00
topics . order ( TopicQuerySQL . order_top_with_pinned_category_for ( score ) )
else
topics . order ( TopicQuerySQL . order_top_for ( score ) )
end
2013-12-23 18:50:36 -05:00
end
2014-01-13 19:02:14 -05:00
end
2013-07-24 17:15:21 -04:00
def list_topics_by ( user )
create_list ( :user_topics ) do | topics |
topics . where ( user_id : user . id )
end
end
2013-08-24 16:58:16 -04:00
def list_private_messages ( user )
list = private_messages_for ( user )
2014-10-08 12:44:47 -04:00
create_list ( :private_messages , { } , list )
2013-08-24 16:58:16 -04:00
end
def list_private_messages_sent ( user )
list = private_messages_for ( user )
list = list . where ( user_id : user . id )
2014-10-08 12:44:47 -04:00
create_list ( :private_messages , { } , list )
2013-08-24 16:58:16 -04:00
end
2013-08-30 12:32:05 -04:00
def list_private_messages_unread ( user )
list = private_messages_for ( user )
2014-05-02 16:36:52 -04:00
list = list . where ( " tu.last_read_post_number IS NULL OR tu.last_read_post_number < topics.highest_post_number " )
2014-10-08 12:44:47 -04:00
create_list ( :private_messages , { } , list )
2013-08-30 12:32:05 -04:00
end
2013-07-24 17:15:21 -04:00
2013-02-05 14:16:51 -05:00
def list_category ( category )
2014-02-04 15:55:30 -05:00
create_list ( :category , unordered : true , category : category . id ) do | list |
2013-07-16 15:20:18 -04:00
if @user
2013-11-13 12:26:32 -05:00
list . order ( TopicQuerySQL . order_with_pinned_sql )
2013-03-06 15:17:07 -05:00
else
2013-11-13 12:26:32 -05:00
list . order ( TopicQuerySQL . order_basic_bumped )
2013-03-06 15:17:07 -05:00
end
end
2013-02-05 14:16:51 -05:00
end
2013-02-27 22:36:12 -05:00
def list_new_in_category ( category )
2014-02-04 15:55:30 -05:00
create_list ( :new_in_category , unordered : true , category : category . id ) do | list |
list . by_newest . first ( 25 )
2014-01-17 17:52:06 -05:00
end
2013-02-27 22:36:12 -05:00
end
2013-07-16 15:20:18 -04:00
def self . new_filter ( list , treat_as_new_topic_start_date )
2013-05-23 01:21:07 -04:00
list . where ( " topics.created_at >= :created_at " , created_at : treat_as_new_topic_start_date )
. where ( " tu.last_read_post_number IS NULL " )
. where ( " COALESCE(tu.notification_level, :tracking) >= :tracking " , tracking : TopicUser . notification_levels [ :tracking ] )
end
def self . unread_filter ( list )
2014-08-18 16:46:16 -04:00
list . where ( " tu.last_read_post_number < topics.highest_post_number " )
2013-05-23 01:21:07 -04:00
. where ( " COALESCE(tu.notification_level, :regular) >= :tracking " , regular : TopicUser . notification_levels [ :regular ] , tracking : TopicUser . notification_levels [ :tracking ] )
2013-05-21 02:39:51 -04:00
end
2013-02-05 14:16:51 -05:00
protected
2014-12-15 11:54:26 -05:00
def per_page_setting
@options [ :slow_platform ] ? SiteSetting . slow_topics_per_page : SiteSetting . topics_per_page
end
2013-07-16 15:20:18 -04:00
def create_list ( filter , options = { } , topics = nil )
topics || = default_results ( options )
2013-04-02 16:52:51 -04:00
topics = yield ( topics ) if block_given?
2014-12-15 11:54:26 -05:00
list = TopicList . new ( filter , @user , topics , options . merge ( @options ) )
list . per_page = per_page_setting
list
2013-02-05 14:16:51 -05:00
end
2013-08-24 16:58:16 -04:00
def private_messages_for ( user )
options = @options
2014-12-15 11:54:26 -05:00
options . reverse_merge! ( per_page : per_page_setting )
2013-08-24 16:58:16 -04:00
# Start with a list of all topics
2014-09-08 13:23:40 -04:00
result = Topic . includes ( :allowed_users )
2014-05-12 03:32:49 -04:00
. where ( " topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{ user . id . to_i } ) " )
2013-11-13 12:26:32 -05:00
. joins ( " LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{ user . id . to_i } ) " )
. order ( TopicQuerySQL . order_nocategory_basic_bumped )
. private_messages
2013-08-24 16:58:16 -04:00
result = result . limit ( options [ :per_page ] ) unless options [ :limit ] == false
result = result . visible if options [ :visible ] || @user . nil? || @user . regular?
result = result . offset ( options [ :page ] . to_i * options [ :per_page ] ) if options [ :page ]
result
end
2013-11-13 14:17:06 -05:00
def default_ordering ( result , options )
# If we're logged in, we have to pay attention to our pinned settings
if @user
result = options [ :category ] . blank? ? result . order ( TopicQuerySQL . order_nocategory_with_pinned_sql ) :
2013-11-18 10:52:01 -05:00
result . order ( TopicQuerySQL . order_with_pinned_sql )
2013-11-13 14:17:06 -05:00
else
2013-11-18 10:52:01 -05:00
result = options [ :category ] . blank? ? result . order ( TopicQuerySQL . order_nocategory_basic_bumped ) :
result . order ( TopicQuerySQL . order_basic_bumped )
2013-11-13 14:17:06 -05:00
end
result
end
def apply_ordering ( result , options )
2014-04-16 12:05:54 -04:00
sort_column = SORTABLE_MAPPING [ options [ :order ] ] || 'default'
sort_dir = ( options [ :ascending ] == " true " ) ? " ASC " : " DESC "
2013-11-13 14:17:06 -05:00
# If we are sorting in the default order desc, we should consider including pinned
# topics. Otherwise, just use bumped_at.
if sort_column == 'default'
2013-11-14 15:50:36 -05:00
if sort_dir == 'DESC'
# If something requires a custom order, for example "unread" which sorts the least read
# to the top, do nothing
return result if options [ :unordered ]
# Otherwise apply our default ordering
return default_ordering ( result , options )
end
2013-11-13 14:17:06 -05:00
sort_column = 'bumped_at'
end
2013-11-14 15:50:36 -05:00
# If we are sorting by category, actually use the name
if sort_column == 'category_id'
return result . references ( :categories ) . order ( TopicQuerySQL . order_by_category_sql ( sort_dir ) )
end
2014-10-02 23:16:53 -04:00
if sort_column == 'op_likes'
return result . order ( " (SELECT like_count FROM posts p3 WHERE p3.topic_id = topics.id AND p3.post_number = 1) #{ sort_dir } " )
end
2013-11-13 14:17:06 -05:00
result . order ( " topics. #{ sort_column } #{ sort_dir } " )
end
2014-06-17 21:23:31 -04:00
def get_category_id ( category_id_or_slug )
return nil unless category_id_or_slug
category_id = category_id_or_slug . to_i
category_id = Category . where ( slug : category_id_or_slug ) . pluck ( :id ) . first if category_id == 0
category_id
end
2014-02-21 14:17:45 -05:00
2013-07-16 15:20:18 -04:00
# Create results based on a bunch of default options
def default_results ( options = { } )
options . reverse_merge! ( @options )
2014-12-15 11:54:26 -05:00
options . reverse_merge! ( per_page : per_page_setting )
2013-02-05 14:16:51 -05:00
2013-03-06 15:17:07 -05:00
# Start with a list of all topics
2014-11-19 17:46:55 -05:00
result = Topic . unscoped
2013-03-06 15:17:07 -05:00
2013-07-16 15:20:18 -04:00
if @user
result = result . joins ( " LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{ @user . id . to_i } ) " )
2014-02-26 11:09:02 -05:00
. references ( 'tu' )
2013-03-06 15:17:07 -05:00
end
2014-06-17 21:23:31 -04:00
category_id = get_category_id ( options [ :category ] )
2014-10-08 12:44:47 -04:00
@options [ :category_id ] = category_id
2014-06-17 21:23:31 -04:00
if category_id
if options [ :no_subcategories ]
result = result . where ( 'categories.id = ?' , category_id )
else
2014-06-30 15:22:40 -04:00
result = result . where ( 'categories.id = ? or (categories.parent_category_id = ? AND categories.topic_id <> topics.id)' , category_id , category_id )
2013-11-08 15:05:14 -05:00
end
2014-06-17 21:23:31 -04:00
result = result . references ( :categories )
2013-11-08 15:05:14 -05:00
end
2013-11-14 15:50:36 -05:00
result = apply_ordering ( result , options )
2014-09-10 22:55:10 -04:00
result = result . listable_topics . includes ( :category )
2013-08-16 08:53:40 -04:00
result = result . where ( 'categories.name is null or categories.name <> ?' , options [ :exclude_category ] ) . references ( :categories ) if options [ :exclude_category ]
2013-10-31 16:10:54 -04:00
2014-02-10 18:06:20 -05:00
# Don't include the category topics if excluded
if options [ :no_definitions ]
2014-02-04 15:55:30 -05:00
result = result . where ( 'COALESCE(categories.topic_id, 0) <> topics.id' )
end
2013-07-16 15:20:18 -04:00
result = result . limit ( options [ :per_page ] ) unless options [ :limit ] == false
result = result . visible if options [ :visible ] || @user . nil? || @user . regular?
2013-12-23 18:50:36 -05:00
result = result . where . not ( topics : { id : options [ :except_topic_ids ] } ) . references ( :topics ) if options [ :except_topic_ids ]
2013-07-16 15:20:18 -04:00
result = result . offset ( options [ :page ] . to_i * options [ :per_page ] ) if options [ :page ]
if options [ :topic_ids ]
2013-08-16 08:53:40 -04:00
result = result . where ( 'topics.id in (?)' , options [ :topic_ids ] ) . references ( :topics )
2013-05-28 03:52:52 -04:00
end
2014-05-15 10:31:45 -04:00
if search = options [ :search ]
result = result . where ( " topics.id in (select pp.topic_id from post_search_data pd join posts pp on pp.id = pd.post_id where pd.search_data @@ #{ Search . ts_query ( search . to_s ) } ) " )
end
2014-07-16 19:29:09 -04:00
# NOTE protect against SYM attack can be removed with Ruby 2.2
#
state = options [ :state ]
if @user && state &&
TopicUser . notification_levels . keys . map ( & :to_s ) . include? ( state )
level = TopicUser . notification_levels [ state . to_sym ]
result = result . where ( ' topics . id IN (
SELECT topic_id
FROM topic_users
WHERE user_id = ? AND
notification_level = ?) ' , @user . id , level )
end
2014-11-19 17:46:55 -05:00
require_deleted_clause = true
2014-01-12 22:40:21 -05:00
if status = options [ :status ]
case status
when 'open'
result = result . where ( 'NOT topics.closed AND NOT topics.archived' )
when 'closed'
result = result . where ( 'topics.closed' )
when 'archived'
result = result . where ( 'topics.archived' )
2014-09-11 19:17:16 -04:00
when 'visible'
result = result . where ( 'topics.visible' )
2014-09-11 19:21:25 -04:00
when 'invisible'
result = result . where ( 'NOT topics.visible' )
2014-11-19 17:46:55 -05:00
when 'deleted'
guardian = Guardian . new ( @user )
if guardian . is_staff?
result = result . where ( 'topics.deleted_at IS NOT NULL' )
require_deleted_clause = false
end
2014-01-12 22:40:21 -05:00
end
end
2014-11-19 17:46:55 -05:00
result = result . where ( 'topics.deleted_at IS NULL' ) if require_deleted_clause
2014-06-05 09:30:24 -04:00
result = result . where ( 'topics.posts_count <= ?' , options [ :max_posts ] ) if options [ :max_posts ] . present?
result = result . where ( 'topics.posts_count >= ?' , options [ :min_posts ] ) if options [ :min_posts ] . present?
2013-09-10 02:02:54 -04:00
guardian = Guardian . new ( @user )
2014-02-07 10:08:56 -05:00
if ! guardian . is_admin?
2013-09-10 02:02:54 -04:00
allowed_ids = guardian . allowed_category_ids
if allowed_ids . length > 0
result = result . where ( 'topics.category_id IS NULL or topics.category_id IN (?)' , allowed_ids )
2013-04-29 02:33:24 -04:00
else
2013-09-10 02:02:54 -04:00
result = result . where ( 'topics.category_id IS NULL' )
2013-04-29 02:33:24 -04:00
end
2013-12-13 12:06:49 -05:00
result = result . references ( :categories )
2013-04-29 02:33:24 -04:00
end
2013-02-25 11:42:20 -05:00
result
2013-02-05 14:16:51 -05:00
end
2014-02-03 00:05:49 -05:00
def latest_results ( options = { } )
result = default_results ( options )
2014-06-17 21:23:31 -04:00
result = remove_muted_categories ( result , @user , exclude : options [ :category ] )
2014-02-12 01:01:13 -05:00
result
2014-02-03 00:05:49 -05:00
end
2014-07-29 00:34:54 -04:00
def remove_muted_categories ( list , user , opts = nil )
2014-06-17 21:23:31 -04:00
category_id = get_category_id ( opts [ :exclude ] ) if opts
2014-02-03 00:05:49 -05:00
if user
list = list . where ( " NOT EXISTS(
SELECT 1 FROM category_users cu
WHERE cu . user_id = ? AND
cu . category_id = topics . category_id AND
2014-06-17 21:23:31 -04:00
cu . notification_level = ? AND
cu . category_id < > ?
) " ,
user . id ,
CategoryUser . notification_levels [ :muted ] ,
category_id || - 1
)
. references ( 'cu' )
2014-02-03 00:05:49 -05:00
end
list
end
2013-07-16 15:20:18 -04:00
def unread_results ( options = { } )
2013-07-18 14:47:59 -04:00
result = TopicQuery . unread_filter ( default_results ( options . reverse_merge ( :unordered = > true ) ) )
. order ( 'CASE WHEN topics.user_id = tu.user_id THEN 1 ELSE 2 END' )
2013-08-08 13:18:52 -04:00
suggested_ordering ( result , options )
2013-07-16 15:20:18 -04:00
end
2014-03-14 18:13:22 -04:00
def new_results ( options = { } )
result = TopicQuery . new_filter ( default_results ( options . reverse_merge ( :unordered = > true ) ) , @user . treat_as_new_topic_start_date )
2014-06-17 21:23:31 -04:00
result = remove_muted_categories ( result , @user , exclude : options [ :category ] )
2014-03-14 18:13:22 -04:00
suggested_ordering ( result , options )
end
2014-01-28 18:15:36 -05:00
def random_suggested ( topic , count , excluded_topic_ids = [ ] )
2014-02-04 12:26:38 -05:00
result = default_results ( unordered : true , per_page : count ) . where ( closed : false , archived : false )
2014-01-28 18:15:36 -05:00
excluded_topic_ids += Category . pluck ( :topic_id ) . compact
result = result . where ( " topics.id NOT IN (?) " , excluded_topic_ids ) unless excluded_topic_ids . empty?
2013-02-05 14:16:51 -05:00
2014-07-29 00:34:54 -04:00
result = remove_muted_categories ( result , @user )
2013-07-12 14:38:20 -04:00
# If we are in a category, prefer it for the random results
2013-07-16 15:20:18 -04:00
if topic . category_id
2013-07-18 14:47:59 -04:00
result = result . order ( " CASE WHEN topics.category_id = #{ topic . category_id . to_i } THEN 0 ELSE 1 END " )
2013-02-27 18:30:14 -05:00
end
2013-07-12 14:38:20 -04:00
result . order ( " RANDOM() " )
2013-02-05 14:16:51 -05:00
end
2013-08-08 13:18:52 -04:00
def suggested_ordering ( result , options )
# Prefer unread in the same category
if options [ :topic ] && options [ :topic ] . category_id
result = result . order ( " CASE WHEN topics.category_id = #{ options [ :topic ] . category_id . to_i } THEN 0 ELSE 1 END " )
end
2013-11-13 12:26:32 -05:00
result . order ( TopicQuerySQL . order_nocategory_with_pinned_sql )
2013-08-08 13:18:52 -04:00
end
2013-02-05 14:16:51 -05:00
end