discourse/spec/lib/topic_query_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1723 lines
57 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
require "topic_view"
RSpec.describe TopicQuery do
# TODO:
# This fab! here has impact on all tests.
#
# It happens first, but is not obvious later in the tests that we depend on
# the user being created so early otherwise finding new topics does not
# work.
#
# We should use be more explicit in communicating how the clock moves
fab!(:user) { Fabricate(:user) }
fab!(:creator) { Fabricate(:user) }
2013-02-06 03:16:51 +08:00
let(:topic_query) { TopicQuery.new(user) }
fab!(:moderator) { Fabricate(:moderator) }
fab!(:admin) { Fabricate(:admin) }
2013-02-06 03:16:51 +08:00
describe "secure category" do
it "filters categories out correctly" do
category = Fabricate(:category_with_definition)
group = Fabricate(:group)
category.set_permissions(group => :full)
category.save
Fabricate(:topic, category: category)
Fabricate(:topic, visible: false)
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(nil).list_latest.topics.count).to eq(0)
expect(TopicQuery.new(user).list_latest.topics.count).to eq(0)
2015-01-10 00:34:37 +08:00
expect(Topic.top_viewed(10).count).to eq(0)
expect(Topic.recent(10).count).to eq(0)
# mods can see hidden topics
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(moderator).list_latest.topics.count).to eq(1)
# admins can see all the topics
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(admin).list_latest.topics.count).to eq(3)
group.add(user)
group.save
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(user).list_latest.topics.count).to eq(2)
end
end
describe "custom filters" do
it "allows custom filters to be applied" do
topic1 = Fabricate(:topic)
_topic2 = Fabricate(:topic)
TopicQuery.add_custom_filter(:only_topic_id) do |results, topic_query|
results = results.where("topics.id = ?", topic_query.options[:only_topic_id])
end
expect(TopicQuery.new(nil, only_topic_id: topic1.id).list_latest.topics.map(&:id)).to eq(
[topic1.id],
)
TopicQuery.remove_custom_filter(:only_topic_id)
end
end
describe "#list_topics_by" do
it "allows users to view their own invisible topics" do
2015-06-22 16:09:08 +08:00
_topic = Fabricate(:topic, user: user)
_invisible_topic = Fabricate(:topic, user: user, visible: false)
expect(TopicQuery.new(nil).list_topics_by(user).topics.count).to eq(1)
expect(TopicQuery.new(user).list_topics_by(user).topics.count).to eq(2)
end
end
describe "#prioritize_pinned_topics" do
2017-07-11 06:25:29 +08:00
it "does the pagination correctly" do
num_topics = 15
per_page = 3
topics = []
(num_topics - 1)
.downto(0)
.each { |i| topics[i] = freeze_time(i.seconds.ago) { Fabricate(:topic) } }
2017-07-11 06:25:29 +08:00
topic_query = TopicQuery.new(user)
results = topic_query.send(:default_results)
expect(topic_query.prioritize_pinned_topics(results, per_page: per_page, page: 0)).to eq(
topics[0...per_page],
)
expect(topic_query.prioritize_pinned_topics(results, per_page: per_page, page: 1)).to eq(
topics[per_page...num_topics],
)
2017-07-11 06:25:29 +08:00
end
it "orders globally pinned topics by pinned_at rather than bumped_at" do
pinned1 =
Fabricate(
:topic,
bumped_at: 3.hour.ago,
pinned_at: 1.hours.ago,
pinned_until: 10.days.from_now,
pinned_globally: true,
)
pinned2 =
Fabricate(
:topic,
bumped_at: 2.hour.ago,
pinned_at: 4.hours.ago,
pinned_until: 10.days.from_now,
pinned_globally: true,
)
unpinned1 = Fabricate(:topic, bumped_at: 2.hour.ago)
unpinned2 = Fabricate(:topic, bumped_at: 3.hour.ago)
topic_query = TopicQuery.new(user)
results = topic_query.send(:default_results)
expected_order = [pinned1, pinned2, unpinned1, unpinned2].map(&:id)
expect(topic_query.prioritize_pinned_topics(results, per_page: 10, page: 0).pluck(:id)).to eq(
expected_order,
)
end
it "orders pinned topics within a category by pinned_at rather than bumped_at" do
cat = Fabricate(:category)
pinned1 =
Fabricate(
:topic,
category: cat,
bumped_at: 3.hour.ago,
pinned_at: 1.hours.ago,
pinned_until: 10.days.from_now,
)
pinned2 =
Fabricate(
:topic,
category: cat,
bumped_at: 2.hour.ago,
pinned_at: 4.hours.ago,
pinned_until: 10.days.from_now,
)
unpinned1 = Fabricate(:topic, category: cat, bumped_at: 2.hour.ago)
unpinned2 = Fabricate(:topic, category: cat, bumped_at: 3.hour.ago)
topic_query = TopicQuery.new(user)
results = topic_query.send(:default_results)
expected_order = [pinned1, pinned2, unpinned1, unpinned2].map(&:id)
expect(
topic_query.prioritize_pinned_topics(
results,
per_page: 10,
page: 0,
category_id: cat.id,
).pluck(:id),
).to eq(expected_order)
end
2017-07-11 06:25:29 +08:00
end
describe "tracked" do
it "filters tracked topics correctly" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
topic = Fabricate(:topic, tags: [tag])
topic2 = Fabricate(:topic)
query = TopicQuery.new(user, filter: "tracked").list_latest
expect(query.topics.length).to eq(0)
TagUser.create!(
tag_id: tag.id,
user_id: user.id,
notification_level: NotificationLevels.all[:watching],
)
cu =
CategoryUser.create!(
category_id: topic2.category_id,
user_id: user.id,
notification_level: NotificationLevels.all[:regular],
)
query = TopicQuery.new(user, filter: "tracked").list_latest
expect(query.topics.map(&:id)).to contain_exactly(topic.id)
cu.update!(notification_level: NotificationLevels.all[:tracking])
query = TopicQuery.new(user, filter: "tracked").list_latest
expect(query.topics.map(&:id)).to contain_exactly(topic.id, topic2.id)
# includes subcategories of tracked categories
parent_category = Fabricate(:category)
sub_category = Fabricate(:category, parent_category_id: parent_category.id)
topic3 = Fabricate(:topic, category_id: sub_category.id)
parent_category_2 = Fabricate(:category)
sub_category_2 = Fabricate(:category, parent_category: parent_category_2)
topic4 = Fabricate(:topic, category: sub_category_2)
CategoryUser.create!(
category_id: parent_category.id,
user_id: user.id,
notification_level: NotificationLevels.all[:tracking],
)
CategoryUser.create!(
category_id: sub_category_2.id,
user_id: user.id,
notification_level: NotificationLevels.all[:tracking],
)
query = TopicQuery.new(user, filter: "tracked").list_latest
expect(query.topics.map(&:id)).to contain_exactly(topic.id, topic2.id, topic3.id, topic4.id)
# includes sub-subcategories of tracked categories
SiteSetting.max_category_nesting = 3
sub_sub_category = Fabricate(:category, parent_category_id: sub_category.id)
topic5 = Fabricate(:topic, category_id: sub_sub_category.id)
query = TopicQuery.new(user, filter: "tracked").list_latest
expect(query.topics.map(&:id)).to contain_exactly(
topic.id,
topic2.id,
topic3.id,
topic4.id,
topic5.id,
)
end
end
describe "deleted filter" do
it "filters deleted topics correctly" do
SiteSetting.enable_category_group_moderation = true
group_moderator = Fabricate(:user)
group = Fabricate(:group)
group.add(group_moderator)
category = Fabricate(:category, reviewable_by_group: group)
topic = Fabricate(:topic, category: category, deleted_at: 1.year.ago)
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(admin, status: "deleted").list_latest.topics.size).to eq(1)
expect(TopicQuery.new(moderator, status: "deleted").list_latest.topics.size).to eq(1)
expect(
TopicQuery
.new(group_moderator, status: "deleted", category: category.id)
.list_latest
.topics
.size,
).to eq(1)
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(user, status: "deleted").list_latest.topics.size).to eq(0)
expect(TopicQuery.new(nil, status: "deleted").list_latest.topics.size).to eq(0)
end
end
describe "include_pms option" do
it "includes users own pms in regular topic lists" do
topic = Fabricate(:topic)
own_pm = Fabricate(:private_message_topic, user: user)
other_pm = Fabricate(:private_message_topic, user: Fabricate(:user))
expect(TopicQuery.new(user).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(admin).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(user, include_pms: true).list_latest.topics).to contain_exactly(
topic,
own_pm,
)
end
end
describe "include_all_pms option" do
it "includes all pms in regular topic lists for admins" do
topic = Fabricate(:topic)
own_pm = Fabricate(:private_message_topic, user: user)
other_pm = Fabricate(:private_message_topic, user: Fabricate(:user))
expect(TopicQuery.new(user).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(admin).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(user, include_all_pms: true).list_latest.topics).to contain_exactly(
topic,
own_pm,
)
expect(TopicQuery.new(admin, include_all_pms: true).list_latest.topics).to contain_exactly(
topic,
own_pm,
other_pm,
)
end
end
describe "category filter" do
let(:category) { Fabricate(:category_with_definition) }
let(:diff_category) { Fabricate(:category_with_definition, name: "Different Category") }
it "returns topics in the category when we filter to it" do
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(moderator).list_latest.topics.size).to eq(0)
# Filter by slug
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(moderator, category: category.slug).list_latest.topics.size).to eq(1)
expect(
TopicQuery.new(moderator, category: "#{category.id}-category").list_latest.topics.size,
).to eq(1)
list = TopicQuery.new(moderator, category: diff_category.slug).list_latest
2015-01-10 00:34:37 +08:00
expect(list.topics.size).to eq(1)
expect(list.preload_key).to eq("topic_list")
2013-12-14 06:18:28 +08:00
# Defaults to no category filter when slug does not exist
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(moderator, category: "made up slug").list_latest.topics.size).to eq(2)
end
context "with subcategories" do
let!(:subcategory) { Fabricate(:category_with_definition, parent_category_id: category.id) }
let(:subsubcategory) do
Fabricate(:category_with_definition, parent_category_id: subcategory.id)
end
2013-12-14 06:18:28 +08:00
PERF: Improve database query perf when loading topics for a category. (#14416) * PERF: Improve database query perf when loading topics for a category. Instead of left joining the `topics` table against `categories` by filtering with `categories.id`, we can improve the query plan by filtering against `topics.category_id` first before joining which helps to reduce the number of rows in the topics table that has to be joined against the other tables and also make better use of our existing index. The following is a before and after of the query plan for a category with many subcategories. Before: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------- Limit (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Nested Loop (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1) Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1 1))) Rows Removed by Join Filter: 13938306 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..100480.05 rows=715549 width=24) (actual ti me=0.010..633.015 rows=464623 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) Rows Removed by Filter: 105321 -> Materialize (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623) -> Index Scan using categories_pkey on categories (cost=0.14..35.89 rows=30 width=8) (actual time=0.006. .0.040 rows=30 loops=1) Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1 13,104,98,100,96,108,109,110,111}'::integer[])) -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..0.53 rows=1 width=16) (a ctual time=0.004..0.004 rows=0 loops=31) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width =8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) Planning Time: 1.359 ms Execution Time: 2453.765 ms (23 rows) ``` After: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1) Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11)) Rows Removed by Filter: 29 -> Nested Loop Left Join (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[]))) Rows Removed by Filter: 569895 -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) -> Index Scan using categories_pkey on categories (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59) Index Cond: (id = topics.category_id) Planning Time: 1.633 ms Execution Time: 657.255 ms (22 rows) ``` * PERF: Optimize index on topics bumped_at. Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
# Not used in assertions but fabricated to ensure we're not leaking topics
# across categories
let!(:_category) { Fabricate(:category_with_definition) }
let!(:_subcategory) { Fabricate(:category_with_definition, parent_category_id: _category.id) }
2013-12-14 06:18:28 +08:00
it "works with subcategories" do
PERF: Improve database query perf when loading topics for a category. (#14416) * PERF: Improve database query perf when loading topics for a category. Instead of left joining the `topics` table against `categories` by filtering with `categories.id`, we can improve the query plan by filtering against `topics.category_id` first before joining which helps to reduce the number of rows in the topics table that has to be joined against the other tables and also make better use of our existing index. The following is a before and after of the query plan for a category with many subcategories. Before: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------- Limit (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Nested Loop (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1) Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1 1))) Rows Removed by Join Filter: 13938306 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..100480.05 rows=715549 width=24) (actual ti me=0.010..633.015 rows=464623 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) Rows Removed by Filter: 105321 -> Materialize (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623) -> Index Scan using categories_pkey on categories (cost=0.14..35.89 rows=30 width=8) (actual time=0.006. .0.040 rows=30 loops=1) Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1 13,104,98,100,96,108,109,110,111}'::integer[])) -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..0.53 rows=1 width=16) (a ctual time=0.004..0.004 rows=0 loops=31) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width =8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) Planning Time: 1.359 ms Execution Time: 2453.765 ms (23 rows) ``` After: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1) Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11)) Rows Removed by Filter: 29 -> Nested Loop Left Join (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[]))) Rows Removed by Filter: 569895 -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) -> Index Scan using categories_pkey on categories (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59) Index Cond: (id = topics.category_id) Planning Time: 1.633 ms Execution Time: 657.255 ms (22 rows) ``` * PERF: Optimize index on topics bumped_at. Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
expect(
TopicQuery.new(moderator, category: category.id).list_latest.topics,
).to contain_exactly(category.topic)
expect(
TopicQuery.new(moderator, category: subcategory.id).list_latest.topics,
).to contain_exactly(subcategory.topic)
expect(
TopicQuery
.new(moderator, category: category.id, no_subcategories: true)
.list_latest
.topics,
).to contain_exactly(category.topic)
2013-12-14 06:18:28 +08:00
end
it "shows a subcategory definition topic in its parent list with the right site setting" do
SiteSetting.show_category_definitions_in_topic_lists = true
PERF: Improve database query perf when loading topics for a category. (#14416) * PERF: Improve database query perf when loading topics for a category. Instead of left joining the `topics` table against `categories` by filtering with `categories.id`, we can improve the query plan by filtering against `topics.category_id` first before joining which helps to reduce the number of rows in the topics table that has to be joined against the other tables and also make better use of our existing index. The following is a before and after of the query plan for a category with many subcategories. Before: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------- Limit (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Nested Loop (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1) Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1 1))) Rows Removed by Join Filter: 13938306 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..100480.05 rows=715549 width=24) (actual ti me=0.010..633.015 rows=464623 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) Rows Removed by Filter: 105321 -> Materialize (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623) -> Index Scan using categories_pkey on categories (cost=0.14..35.89 rows=30 width=8) (actual time=0.006. .0.040 rows=30 loops=1) Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1 13,104,98,100,96,108,109,110,111}'::integer[])) -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..0.53 rows=1 width=16) (a ctual time=0.004..0.004 rows=0 loops=31) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width =8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) Planning Time: 1.359 ms Execution Time: 2453.765 ms (23 rows) ``` After: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1) Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11)) Rows Removed by Filter: 29 -> Nested Loop Left Join (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[]))) Rows Removed by Filter: 569895 -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) -> Index Scan using categories_pkey on categories (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59) Index Cond: (id = topics.category_id) Planning Time: 1.633 ms Execution Time: 657.255 ms (22 rows) ``` * PERF: Optimize index on topics bumped_at. Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
expect(
TopicQuery.new(moderator, category: category.id).list_latest.topics,
).to contain_exactly(category.topic, subcategory.topic)
end
it "works with subsubcategories" do
SiteSetting.max_category_nesting = 3
PERF: Improve database query perf when loading topics for a category. (#14416) * PERF: Improve database query perf when loading topics for a category. Instead of left joining the `topics` table against `categories` by filtering with `categories.id`, we can improve the query plan by filtering against `topics.category_id` first before joining which helps to reduce the number of rows in the topics table that has to be joined against the other tables and also make better use of our existing index. The following is a before and after of the query plan for a category with many subcategories. Before: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------- Limit (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Nested Loop (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1) Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1 1))) Rows Removed by Join Filter: 13938306 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..100480.05 rows=715549 width=24) (actual ti me=0.010..633.015 rows=464623 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) Rows Removed by Filter: 105321 -> Materialize (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623) -> Index Scan using categories_pkey on categories (cost=0.14..35.89 rows=30 width=8) (actual time=0.006. .0.040 rows=30 loops=1) Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1 13,104,98,100,96,108,109,110,111}'::integer[])) -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..0.53 rows=1 width=16) (a ctual time=0.004..0.004 rows=0 loops=31) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width =8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) Planning Time: 1.359 ms Execution Time: 2453.765 ms (23 rows) ``` After: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1) Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11)) Rows Removed by Filter: 29 -> Nested Loop Left Join (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[]))) Rows Removed by Filter: 569895 -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) -> Index Scan using categories_pkey on categories (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59) Index Cond: (id = topics.category_id) Planning Time: 1.633 ms Execution Time: 657.255 ms (22 rows) ``` * PERF: Optimize index on topics bumped_at. Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
category_topic = Fabricate(:topic, category: category)
subcategory_topic = Fabricate(:topic, category: subcategory)
subsubcategory_topic = Fabricate(:topic, category: subsubcategory)
SiteSetting.max_category_nesting = 2
PERF: Improve database query perf when loading topics for a category. (#14416) * PERF: Improve database query perf when loading topics for a category. Instead of left joining the `topics` table against `categories` by filtering with `categories.id`, we can improve the query plan by filtering against `topics.category_id` first before joining which helps to reduce the number of rows in the topics table that has to be joined against the other tables and also make better use of our existing index. The following is a before and after of the query plan for a category with many subcategories. Before: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------- Limit (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Nested Loop (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1) Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1 1))) Rows Removed by Join Filter: 13938306 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..100480.05 rows=715549 width=24) (actual ti me=0.010..633.015 rows=464623 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) Rows Removed by Filter: 105321 -> Materialize (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623) -> Index Scan using categories_pkey on categories (cost=0.14..35.89 rows=30 width=8) (actual time=0.006. .0.040 rows=30 loops=1) Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1 13,104,98,100,96,108,109,110,111}'::integer[])) -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..0.53 rows=1 width=16) (a ctual time=0.004..0.004 rows=0 loops=31) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width =8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) Planning Time: 1.359 ms Execution Time: 2453.765 ms (23 rows) ``` After: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1) Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11)) Rows Removed by Filter: 29 -> Nested Loop Left Join (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[]))) Rows Removed by Filter: 569895 -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) -> Index Scan using categories_pkey on categories (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59) Index Cond: (id = topics.category_id) Planning Time: 1.633 ms Execution Time: 657.255 ms (22 rows) ``` * PERF: Optimize index on topics bumped_at. Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
expect(
TopicQuery.new(moderator, category: category.id).list_latest.topics,
).to contain_exactly(category.topic, category_topic, subcategory_topic)
expect(
TopicQuery.new(moderator, category: subcategory.id).list_latest.topics,
).to contain_exactly(subcategory.topic, subcategory_topic, subsubcategory_topic)
expect(
TopicQuery.new(moderator, category: subsubcategory.id).list_latest.topics,
).to contain_exactly(subsubcategory.topic, subsubcategory_topic)
SiteSetting.max_category_nesting = 3
PERF: Improve database query perf when loading topics for a category. (#14416) * PERF: Improve database query perf when loading topics for a category. Instead of left joining the `topics` table against `categories` by filtering with `categories.id`, we can improve the query plan by filtering against `topics.category_id` first before joining which helps to reduce the number of rows in the topics table that has to be joined against the other tables and also make better use of our existing index. The following is a before and after of the query plan for a category with many subcategories. Before: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------- Limit (cost=1.28..747.09 rows=30 width=12) (actual time=85.502..2453.727 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..566518.36 rows=22788 width=12) (actual time=85.501..2453.722 rows=30 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=1.00..566001.58 rows=22866 width=20) (actual time=85.494..2453.702 rows=30 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((t opics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Nested Loop (cost=0.57..528561.75 rows=68606 width=24) (actual time=85.472..2453.562 rows=31 loops=1) Join Filter: ((topics.category_id = categories.id) AND ((categories.topic_id <> topics.id) OR (categories.id = 1 1))) Rows Removed by Join Filter: 13938306 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..100480.05 rows=715549 width=24) (actual ti me=0.010..633.015 rows=464623 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text)) Rows Removed by Filter: 105321 -> Materialize (cost=0.14..36.04 rows=30 width=8) (actual time=0.000..0.002 rows=30 loops=464623) -> Index Scan using categories_pkey on categories (cost=0.14..35.89 rows=30 width=8) (actual time=0.006. .0.040 rows=30 loops=1) Index Cond: (id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,1 13,104,98,100,96,108,109,110,111}'::integer[])) -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..0.53 rows=1 width=16) (a ctual time=0.004..0.004 rows=0 loops=31) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=30) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width =8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) Planning Time: 1.359 ms Execution Time: 2453.765 ms (23 rows) ``` After: ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=1.28..438.55 rows=30 width=12) (actual time=38.297..657.215 rows=30 loops=1) -> Nested Loop Left Join (cost=1.28..195944.68 rows=13443 width=12) (actual time=38.296..657.211 rows=30 loops=1) Filter: ((categories.topic_id <> topics.id) OR (topics.category_id = 11)) Rows Removed by Filter: 29 -> Nested Loop Left Join (cost=1.13..193462.59 rows=13443 width=16) (actual time=38.289..657.092 rows=59 loops=1) Join Filter: (category_users.category_id = topics.category_id) Filter: ((topics.category_id = 11) OR (COALESCE(category_users.notification_level, 1) <> 0) OR (tu.notification_level > 1)) -> Nested Loop Left Join (cost=0.85..193156.79 rows=13489 width=20) (actual time=38.282..657.059 rows=59 loops=1) Filter: ((COALESCE(tu.notification_level, 1) > 0) AND ((topics.category_id <> 11) OR (topics.pinned_at IS NULL) OR ((topics.pinned_at <= tu.cleared_pinned_at) AND (tu.cleared_pinned_at IS NOT NULL)))) Rows Removed by Filter: 1 -> Index Scan using index_topics_on_bumped_at on topics (cost=0.42..134521.06 rows=40470 width=24) (actual time=38.267..656.850 rows=60 loops=1) Filter: ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text) AND (category_id = ANY ('{11,53,57,55,54,56,112,94,107,115,116,117,97,95,102,103,101,105,99,114,106,113,104,98,100,96,108,109,110,111}'::integer[]))) Rows Removed by Filter: 569895 -> Index Scan using index_topic_users_on_topic_id_and_user_id on topic_users tu (cost=0.43..1.43 rows=1 width=16) (actual time=0.003..0.003 rows=0 loops=60) Index Cond: ((topic_id = topics.id) AND (user_id = 1103877)) -> Materialize (cost=0.28..2.30 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=59) -> Index Scan using index_category_users_on_user_id_and_last_seen_at on category_users (cost=0.28..2.29 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1) Index Cond: (user_id = 1103877) -> Index Scan using categories_pkey on categories (cost=0.14..0.17 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=59) Index Cond: (id = topics.category_id) Planning Time: 1.633 ms Execution Time: 657.255 ms (22 rows) ``` * PERF: Optimize index on topics bumped_at. Replace `index_topics_on_bumped_at` index with a partial index on `Topic#bumped_at` filtered by archetype since there is already another index that covers private topics.
2021-09-28 10:05:00 +08:00
expect(
TopicQuery.new(moderator, category: category.id).list_latest.topics,
).to contain_exactly(
category.topic,
category_topic,
subcategory_topic,
subsubcategory_topic,
)
expect(
TopicQuery.new(moderator, category: subcategory.id).list_latest.topics,
).to contain_exactly(subcategory.topic, subcategory_topic, subsubcategory_topic)
expect(
TopicQuery.new(moderator, category: subsubcategory.id).list_latest.topics,
).to contain_exactly(subsubcategory.topic, subsubcategory_topic)
end
2013-12-14 06:18:28 +08:00
end
end
describe "tag filter" do
fab!(:tag) { Fabricate(:tag) }
fab!(:other_tag) { Fabricate(:tag) }
fab!(:uppercase_tag) { Fabricate(:tag, name: "HeLlO") }
before { SiteSetting.tagging_enabled = true }
context "with no category filter" do
fab!(:tagged_topic1) { Fabricate(:topic, tags: [tag]) }
fab!(:tagged_topic2) { Fabricate(:topic, tags: [other_tag]) }
fab!(:tagged_topic3) { Fabricate(:topic, tags: [tag, other_tag]) }
fab!(:tagged_topic4) { Fabricate(:topic, tags: [uppercase_tag]) }
fab!(:no_tags_topic) { Fabricate(:topic) }
let(:synonym) { Fabricate(:tag, target_tag: tag, name: "synonym") }
it "excludes a tag if desired" do
topics = TopicQuery.new(moderator, exclude_tag: tag.name).list_latest.topics
expect(topics.any? { |t| t.tags.include?(tag) }).to eq(false)
end
it "returns topics with the tag when filtered to it" do
expect(TopicQuery.new(moderator, tags: tag.name).list_latest.topics).to contain_exactly(
2018-03-19 13:39:29 +08:00
tagged_topic1,
tagged_topic3,
)
2018-03-19 13:39:29 +08:00
expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics).to contain_exactly(
tagged_topic1,
tagged_topic3,
)
2018-03-19 13:39:29 +08:00
expect(
TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics,
).to contain_exactly(tagged_topic1, tagged_topic2, tagged_topic3)
expect(
TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics,
).to contain_exactly(tagged_topic1, tagged_topic2, tagged_topic3)
expect(TopicQuery.new(moderator, tags: ["hElLo"]).list_latest.topics).to contain_exactly(
tagged_topic4,
)
end
2016-08-11 13:38:16 +08:00
it "can return topics with all specified tags" do
expect(
TopicQuery
.new(moderator, tags: [tag.name, other_tag.name], match_all_tags: true)
.list_latest
.topics
.map(&:id),
).to eq([tagged_topic3.id])
end
it "can return topics with tag intersections using truthy/falsey values" do
expect(
TopicQuery
.new(moderator, tags: [tag.name, other_tag.name], match_all_tags: "false")
.list_latest
.topics
.map(&:id)
.sort,
).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort)
end
2016-08-16 03:42:06 +08:00
it "returns an empty relation when an invalid tag is passed" do
expect(
TopicQuery
.new(moderator, tags: [tag.name, "notatag"], match_all_tags: true)
.list_latest
.topics,
).to be_empty
end
it "can return topics with no tags" do
expect(TopicQuery.new(moderator, no_tags: true).list_latest.topics.map(&:id)).to eq(
[no_tags_topic.id],
)
end
it "can filter using a synonym" do
expect(TopicQuery.new(moderator, tags: synonym.name).list_latest.topics).to contain_exactly(
tagged_topic1,
tagged_topic3,
)
expect(TopicQuery.new(moderator, tags: [synonym.id]).list_latest.topics).to contain_exactly(
tagged_topic1,
tagged_topic3,
)
expect(
TopicQuery.new(moderator, tags: [synonym.name, other_tag.name]).list_latest.topics,
).to contain_exactly(tagged_topic1, tagged_topic2, tagged_topic3)
expect(
TopicQuery.new(moderator, tags: [synonym.id, other_tag.id]).list_latest.topics,
).to contain_exactly(tagged_topic1, tagged_topic2, tagged_topic3)
expect(TopicQuery.new(moderator, tags: ["SYnonYM"]).list_latest.topics).to contain_exactly(
tagged_topic1,
tagged_topic3,
)
end
end
context "when remove_muted_tags is enabled" do
fab!(:topic) { Fabricate(:topic, tags: [tag]) }
before do
SiteSetting.remove_muted_tags_from_latest = "always"
SiteSetting.default_tags_muted = tag.name
end
it "removes default muted tag topics for anonymous users" do
expect(TopicQuery.new(nil).list_latest.topics.map(&:id)).not_to include(topic.id)
end
end
context "with categories too" do
let(:category1) { Fabricate(:category_with_definition) }
let(:category2) { Fabricate(:category_with_definition) }
it "returns topics in the given category with the given tag" do
tagged_topic1 = Fabricate(:topic, category: category1, tags: [tag])
_tagged_topic2 = Fabricate(:topic, category: category2, tags: [tag])
tagged_topic3 = Fabricate(:topic, category: category1, tags: [tag, other_tag])
_no_tags_topic = Fabricate(:topic, category: category1)
expect(
TopicQuery
.new(moderator, category: category1.id, tags: [tag.name])
.list_latest
.topics
.map(&:id)
.sort,
).to eq([tagged_topic1.id, tagged_topic3.id].sort)
expect(
TopicQuery
.new(moderator, category: category2.id, tags: [other_tag.name])
.list_latest
.topics
.size,
).to eq(0)
end
end
end
describe "muted categories" do
it "is removed from top, new and latest lists" do
category = Fabricate(:category_with_definition)
topic = Fabricate(:topic, category: category)
CategoryUser.create!(
user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
2015-01-10 00:34:37 +08:00
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).not_to include(topic.id)
TopTopic.create!(topic: topic, all_score: 1)
expect(topic_query.list_top_for(:all).topics.map(&:id)).not_to include(topic.id)
end
end
describe "#list_top_for" do
it "lists top for the week" do
Fabricate(:topic, like_count: 1000, posts_count: 100)
TopTopic.refresh!
expect(topic_query.list_top_for(:weekly).topics.count).to eq(1)
end
it "only allows periods defined by TopTopic.periods" do
expect { topic_query.list_top_for(:all) }.not_to raise_error
expect { topic_query.list_top_for(:yearly) }.not_to raise_error
expect { topic_query.list_top_for(:quarterly) }.not_to raise_error
expect { topic_query.list_top_for(:monthly) }.not_to raise_error
expect { topic_query.list_top_for(:weekly) }.not_to raise_error
expect { topic_query.list_top_for(:daily) }.not_to raise_error
expect { topic_query.list_top_for("some bad input") }.to raise_error(
Discourse::InvalidParameters,
)
end
end
describe "mute_all_categories_by_default" do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:topic) { Fabricate(:topic, category: category) }
before { SiteSetting.mute_all_categories_by_default = true }
it "should remove all topics from new and latest lists by default" do
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).not_to include(topic.id)
end
it "should include tracked category topics in new and latest lists" do
topic = Fabricate(:topic, category: category)
CategoryUser.create!(
user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:tracking],
)
expect(topic_query.list_new.topics.map(&:id)).to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
it "should include default watched category topics in latest list for anonymous users" do
SiteSetting.default_categories_watching = category.id.to_s
expect(TopicQuery.new.list_latest.topics.map(&:id)).to include(topic.id)
end
it "should include default regular category topics in latest list for anonymous users" do
SiteSetting.default_categories_normal = category.id.to_s
expect(TopicQuery.new.list_latest.topics.map(&:id)).to include(topic.id)
end
it "should include topics when filtered by category" do
topic_query = TopicQuery.new(user, category: topic.category_id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
end
describe "already seen topics" do
it "is removed from new and visible on latest lists" do
category = Fabricate(:category_with_definition)
topic = Fabricate(:topic, category: category)
DismissedTopicUser.create!(user_id: user.id, topic_id: topic.id, created_at: Time.zone.now)
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
end
describe "muted tags" do
it "is removed from new and latest lists" do
SiteSetting.tagging_enabled = true
SiteSetting.remove_muted_tags_from_latest = "always"
muted_tag, other_tag = Fabricate(:tag), Fabricate(:tag)
muted_topic = Fabricate(:topic, tags: [muted_tag])
tagged_topic = Fabricate(:topic, tags: [other_tag])
muted_tagged_topic = Fabricate(:topic, tags: [muted_tag, other_tag])
untagged_topic = Fabricate(:topic)
TagUser.create!(
user_id: user.id,
tag_id: muted_tag.id,
notification_level: CategoryUser.notification_levels[:muted],
)
topic_ids = topic_query.list_latest.topics.map(&:id)
expect(topic_ids).to contain_exactly(tagged_topic.id, untagged_topic.id)
topic_ids = topic_query.list_new.topics.map(&:id)
expect(topic_ids).to contain_exactly(tagged_topic.id, untagged_topic.id)
SiteSetting.remove_muted_tags_from_latest = "only_muted"
topic_ids = topic_query.list_latest.topics.map(&:id)
expect(topic_ids).to contain_exactly(
tagged_topic.id,
muted_tagged_topic.id,
untagged_topic.id,
)
topic_ids = topic_query.list_new.topics.map(&:id)
expect(topic_ids).to contain_exactly(
tagged_topic.id,
muted_tagged_topic.id,
untagged_topic.id,
)
SiteSetting.remove_muted_tags_from_latest = "never"
topic_ids = topic_query.list_latest.topics.map(&:id)
expect(topic_ids).to contain_exactly(
muted_topic.id,
tagged_topic.id,
muted_tagged_topic.id,
untagged_topic.id,
)
topic_ids = topic_query.list_new.topics.map(&:id)
expect(topic_ids).to contain_exactly(
muted_topic.id,
tagged_topic.id,
muted_tagged_topic.id,
untagged_topic.id,
)
end
it "is not removed from the tag page itself" do
muted_tag = Fabricate(:tag)
TagUser.create!(
user_id: user.id,
tag_id: muted_tag.id,
notification_level: CategoryUser.notification_levels[:muted],
)
muted_topic = Fabricate(:topic, tags: [muted_tag])
topic_ids = topic_query.latest_results(tags: [muted_tag.name]).map(&:id)
expect(topic_ids).to contain_exactly(muted_topic.id)
muted_tag.update(name: "mixedCaseName")
topic_ids = topic_query.latest_results(tags: [muted_tag.name.downcase]).map(&:id)
expect(topic_ids).to contain_exactly(muted_topic.id)
end
end
describe "a bunch of topics" do
fab!(:regular_topic) do
Fabricate(
:topic,
title: "this is a regular topic",
user: creator,
views: 100,
like_count: 66,
posts_count: 3,
2013-11-15 04:50:36 +08:00
participant_count: 11,
bumped_at: 15.minutes.ago,
)
end
fab!(:pinned_topic) do
Fabricate(
:topic,
title: "this is a pinned topic",
user: creator,
views: 10,
like_count: 100,
posts_count: 5,
2013-11-15 04:50:36 +08:00
participant_count: 12,
pinned_at: 10.minutes.ago,
pinned_globally: true,
bumped_at: 10.minutes.ago,
)
end
fab!(:archived_topic) do
Fabricate(
:topic,
title: "this is an archived topic",
user: creator,
views: 50,
like_count: 30,
posts_count: 4,
archived: true,
2013-11-15 04:50:36 +08:00
participant_count: 1,
bumped_at: 6.minutes.ago,
)
end
fab!(:invisible_topic) do
Fabricate(
:topic,
title: "this is an invisible topic",
user: creator,
views: 1,
like_count: 5,
posts_count: 2,
visible: false,
2013-11-15 04:50:36 +08:00
participant_count: 3,
bumped_at: 5.minutes.ago,
)
end
fab!(:closed_topic) do
Fabricate(
:topic,
title: "this is a closed topic",
user: creator,
views: 2,
like_count: 1,
posts_count: 1,
closed: true,
2013-11-15 04:50:36 +08:00
participant_count: 2,
bumped_at: 1.minute.ago,
)
end
fab!(:future_topic) do
Fabricate(
:topic,
title: "this is a topic in far future",
user: creator,
views: 30,
like_count: 11,
posts_count: 6,
participant_count: 5,
bumped_at: 1000.years.from_now,
)
end
let(:topics) { topic_query.list_latest.topics }
2013-02-06 03:16:51 +08:00
context "with list_latest" do
2013-02-06 03:16:51 +08:00
it "returns the topics in the correct order" do
2015-01-10 00:34:37 +08:00
expect(topics.map(&:id)).to eq(
[pinned_topic, future_topic, closed_topic, archived_topic, regular_topic].map(&:id),
)
2013-02-06 03:16:51 +08:00
# includes the invisible topic if you're a moderator
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(moderator).list_latest.topics.include?(invisible_topic)).to eq(true)
2013-02-06 03:16:51 +08:00
# includes the invisible topic if you're an admin" do
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new(admin).list_latest.topics.include?(invisible_topic)).to eq(true)
2013-02-06 03:16:51 +08:00
end
context "with sort_order" do
def ids_in_order(order, descending = true)
TopicQuery
.new(admin, order: order, ascending: descending ? "false" : "true")
.list_latest
.topics
.map(&:id)
end
it "returns the topics in correct order" do
# returns the topics in likes order if requested
2015-01-10 00:34:37 +08:00
expect(ids_in_order("posts")).to eq(
[
2015-01-10 00:34:37 +08:00
future_topic,
pinned_topic,
archived_topic,
regular_topic,
invisible_topic,
closed_topic,
].map(&:id),
)
# returns the topics in reverse likes order if requested
2015-01-10 00:34:37 +08:00
expect(ids_in_order("posts", false)).to eq(
[
2015-01-10 00:34:37 +08:00
closed_topic,
invisible_topic,
regular_topic,
archived_topic,
pinned_topic,
future_topic,
].map(&:id),
)
# returns the topics in likes order if requested
2015-01-10 00:34:37 +08:00
expect(ids_in_order("likes")).to eq(
[
2015-01-10 00:34:37 +08:00
pinned_topic,
regular_topic,
archived_topic,
future_topic,
invisible_topic,
closed_topic,
].map(&:id),
)
# returns the topics in reverse likes order if requested
2015-01-10 00:34:37 +08:00
expect(ids_in_order("likes", false)).to eq(
[
2015-01-10 00:34:37 +08:00
closed_topic,
invisible_topic,
future_topic,
archived_topic,
regular_topic,
pinned_topic,
].map(&:id),
)
# returns the topics in views order if requested
2015-01-10 00:34:37 +08:00
expect(ids_in_order("views")).to eq(
[
2015-01-10 00:34:37 +08:00
regular_topic,
archived_topic,
future_topic,
pinned_topic,
closed_topic,
invisible_topic,
].map(&:id),
)
# returns the topics in reverse views order if requested" do
2015-01-10 00:34:37 +08:00
expect(ids_in_order("views", false)).to eq(
[
2015-01-10 00:34:37 +08:00
invisible_topic,
closed_topic,
pinned_topic,
future_topic,
archived_topic,
regular_topic,
].map(&:id),
)
# returns the topics in posters order if requested" do
2015-01-10 00:34:37 +08:00
expect(ids_in_order("posters")).to eq(
[
2015-01-10 00:34:37 +08:00
pinned_topic,
regular_topic,
future_topic,
invisible_topic,
closed_topic,
archived_topic,
].map(&:id),
)
2013-11-15 04:50:36 +08:00
# returns the topics in reverse posters order if requested" do
2015-01-10 00:34:37 +08:00
expect(ids_in_order("posters", false)).to eq(
[
2015-01-10 00:34:37 +08:00
archived_topic,
closed_topic,
invisible_topic,
future_topic,
regular_topic,
pinned_topic,
].map(&:id),
)
# sets a custom field for each topic to emulate a plugin
regular_topic.custom_fields["sheep"] = 26
pinned_topic.custom_fields["sheep"] = 47
archived_topic.custom_fields["sheep"] = 69
invisible_topic.custom_fields["sheep"] = 12
closed_topic.custom_fields["sheep"] = 31
future_topic.custom_fields["sheep"] = 53
regular_topic.save
pinned_topic.save
archived_topic.save
invisible_topic.save
closed_topic.save
future_topic.save
# adds the custom field as a viable sort option
class ::TopicQuery
SORTABLE_MAPPING["sheep"] = "custom_fields.sheep"
end
# returns the topics in the sheep order if requested" do
expect(ids_in_order("sheep")).to eq(
[
archived_topic,
future_topic,
pinned_topic,
closed_topic,
regular_topic,
invisible_topic,
].map(&:id),
)
# returns the topics in reverse sheep order if requested" do
expect(ids_in_order("sheep", false)).to eq(
[
invisible_topic,
regular_topic,
closed_topic,
pinned_topic,
future_topic,
archived_topic,
].map(&:id),
)
2013-11-15 04:50:36 +08:00
end
end
2013-02-06 03:16:51 +08:00
end
context "after clearing a pinned topic" do
before { pinned_topic.clear_pin_for(user) }
it "no longer shows the pinned topic at the top" do
2015-01-10 00:34:37 +08:00
expect(topics).to eq(
[future_topic, closed_topic, archived_topic, pinned_topic, regular_topic],
)
end
end
2013-02-06 03:16:51 +08:00
end
describe "categorized" do
fab!(:category) { Fabricate(:category_with_definition) }
let(:topic_category) { category.topic }
fab!(:topic_no_cat) { Fabricate(:topic) }
fab!(:topic_in_cat1) do
Fabricate(:topic, category: category, bumped_at: 10.minutes.ago, created_at: 10.minutes.ago)
end
fab!(:topic_in_cat2) { Fabricate(:topic, category: category) }
describe "#list_new_in_category" do
it "returns the topic category and the categorized topic" do
2015-02-25 11:39:50 +08:00
expect(topic_query.list_new_in_category(category).topics.map(&:id)).to eq(
[topic_in_cat2.id, topic_category.id, topic_in_cat1.id],
)
end
end
describe "category default sort order" do
it "can use category's default sort order" do
category.update!(sort_order: "created", sort_ascending: true)
topic_ids = TopicQuery.new(user, category: category.id).list_latest.topics.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat1.id, topic_in_cat2.id])
end
it "should apply default sort order to latest and unseen filters only" do
category.update!(sort_order: "created", sort_ascending: true)
topic1 =
Fabricate(
:topic,
category: category,
like_count: 1000,
posts_count: 100,
created_at: 1.day.ago,
)
topic2 =
Fabricate(
:topic,
category: category,
like_count: 5200,
posts_count: 500,
created_at: 1.hour.ago,
)
TopTopic.refresh!
topic_ids =
TopicQuery.new(user, category: category.id).list_top_for(:monthly).topics.map(&:id)
expect(topic_ids).to eq([topic2.id, topic1.id])
end
it "ignores invalid order value" do
category.update!(sort_order: "funny")
topic_ids = TopicQuery.new(user, category: category.id).list_latest.topics.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat2.id, topic_in_cat1.id])
end
it "can be overridden" do
category.update!(sort_order: "created", sort_ascending: true)
topic_ids =
TopicQuery
.new(user, category: category.id, order: "activity")
.list_latest
.topics
.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat2.id, topic_in_cat1.id])
end
end
2013-02-06 03:16:51 +08:00
end
describe "unread / read topics" do
2013-02-06 03:16:51 +08:00
context "with no data" do
it "has no unread topics" do
2015-01-10 00:34:37 +08:00
expect(topic_query.list_unread.topics).to be_blank
2013-02-06 03:16:51 +08:00
end
end
context "with whispers" do
before { SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" }
it "correctly shows up in unread for staff" do
first = create_post(raw: "this is the first post", title: "super amazing title")
_whisper =
create_post(
topic_id: first.topic.id,
post_type: Post.types[:whisper],
raw: "this is a whispered reply",
)
topic_id = first.topic.id
TopicUser.update_last_read(user, topic_id, first.post_number, 1, 1)
TopicUser.update_last_read(admin, topic_id, first.post_number, 1, 1)
TopicUser.change(
user.id,
topic_id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.change(
admin.id,
topic_id,
notification_level: TopicUser.notification_levels[:tracking],
)
expect(TopicQuery.new(user).list_unread.topics).to eq([])
expect(TopicQuery.new(admin).list_unread.topics).to eq([first.topic])
end
end
2013-02-06 03:16:51 +08:00
context "with read data" do
fab!(:partially_read) { Fabricate(:post, user: creator).topic }
fab!(:fully_read) { Fabricate(:post, user: creator).topic }
2013-02-06 03:16:51 +08:00
before do
TopicUser.update_last_read(user, partially_read.id, 0, 0, 0)
TopicUser.update_last_read(user, fully_read.id, 1, 1, 0)
2013-02-06 03:16:51 +08:00
end
context "with list_unread" do
it "lists topics correctly" do
_new_topic = Fabricate(:post, user: creator).topic
expect(topic_query.list_unread.topics).to eq([])
expect(topic_query.list_read.topics).to match_array([fully_read, partially_read])
2013-02-06 03:16:51 +08:00
end
end
context "with user with auto_track_topics list_unread" do
2013-02-26 00:42:20 +08:00
before do
user.user_option.auto_track_topics_after_msecs = 0
user.user_option.save
2013-02-06 03:16:51 +08:00
end
2013-02-26 00:42:20 +08:00
2013-02-06 03:16:51 +08:00
it "only contains the partially read topic" do
expect(topic_query.list_unread.topics).to eq([partially_read])
2013-02-06 03:16:51 +08:00
end
end
end
end
describe "#list_new" do
2013-02-06 03:16:51 +08:00
context "without a new topic" do
it "has no new topics" do
2015-01-10 00:34:37 +08:00
expect(topic_query.list_new.topics).to be_blank
2013-02-06 03:16:51 +08:00
end
end
context "when preloading api" do
it "preloads data correctly" do
TopicList.preloaded_custom_fields << "tag"
TopicList.preloaded_custom_fields << "age"
TopicList.preloaded_custom_fields << "foo"
topic = Fabricate.build(:topic, user: creator, bumped_at: 10.minutes.ago)
topic.custom_fields["tag"] = %w[a b c]
topic.custom_fields["age"] = 22
topic.save
new_topic = topic_query.list_new.topics.first
expect(new_topic.custom_fields["tag"].sort).to eq(%w[a b c])
expect(new_topic.custom_fields["age"]).to eq("22")
expect(new_topic.custom_field_preloaded?("tag")).to eq(true)
expect(new_topic.custom_field_preloaded?("age")).to eq(true)
expect(new_topic.custom_field_preloaded?("foo")).to eq(true)
expect(new_topic.custom_field_preloaded?("bar")).to eq(false)
TopicList.preloaded_custom_fields.clear
# if we attempt to access non preloaded fields explode
2016-05-30 11:38:04 +08:00
expect { new_topic.custom_fields["boom"] }.to raise_error(StandardError)
end
end
context "when preloading associations" do
it "preloads associations" do
DiscoursePluginRegistry.register_topic_preloader_association(
:first_post,
Plugin::Instance.new,
)
topic = Fabricate(:topic)
Fabricate(:post, topic: topic)
new_topic = topic_query.list_new.topics.first
expect(new_topic.association(:image_upload).loaded?).to eq(true) # Preloaded by default
expect(new_topic.association(:first_post).loaded?).to eq(true) # Testing a user-defined preloaded association
expect(new_topic.association(:user).loaded?).to eq(false) # Testing the negative
DiscoursePluginRegistry.reset_register!(:topic_preloader_associations)
end
end
2013-02-06 03:16:51 +08:00
context "with a new topic" do
2013-02-26 00:42:20 +08:00
let!(:new_topic) { Fabricate(:topic, user: creator, bumped_at: 10.minutes.ago) }
2013-02-06 03:16:51 +08:00
let(:topics) { topic_query.list_new.topics }
it "contains no new topics for a user that has missed the window" do
expect(topic_query.list_new.topics).to eq([new_topic])
user.user_option.new_topic_duration_minutes = 5
user.user_option.save
new_topic.created_at = 10.minutes.ago
new_topic.save
expect(topic_query.list_new.topics).to eq([])
end
context "with muted topics" do
2013-02-06 03:16:51 +08:00
before { new_topic.notify_muted!(user) }
2013-02-26 00:42:20 +08:00
it "returns an empty set" do
2015-01-10 00:34:37 +08:00
expect(topics).to be_blank
2015-11-02 12:05:08 +08:00
expect(topic_query.list_latest.topics).to be_blank
2013-02-06 03:16:51 +08:00
end
context "when un-muted" do
2013-02-06 03:16:51 +08:00
before { new_topic.notify_tracking!(user) }
it "returns the topic again" do
2015-01-10 00:34:37 +08:00
expect(topics).to eq([new_topic])
2015-11-02 12:05:08 +08:00
expect(topic_query.list_latest.topics).not_to be_blank
2013-02-06 03:16:51 +08:00
end
end
2013-02-26 00:42:20 +08:00
end
2013-02-06 03:16:51 +08:00
end
end
describe "#list_posted" do
2013-02-06 03:16:51 +08:00
let(:topics) { topic_query.list_posted.topics }
it "returns blank when there are no posted topics" do
2015-01-10 00:34:37 +08:00
expect(topics).to be_blank
2013-02-06 03:16:51 +08:00
end
context "with created topics" do
let!(:created_topic) { create_post(user: user).topic }
2013-02-06 03:16:51 +08:00
it "includes the created topic" do
2015-01-10 00:34:37 +08:00
expect(topics.include?(created_topic)).to eq(true)
2013-02-06 03:16:51 +08:00
end
end
context "with topic you've posted in" do
let(:other_users_topic) { create_post(user: creator).topic }
let!(:your_post) { create_post(user: user, topic: other_users_topic) }
2013-02-06 03:16:51 +08:00
it "includes the posted topic" do
2015-01-10 00:34:37 +08:00
expect(topics.include?(other_users_topic)).to eq(true)
2013-02-06 03:16:51 +08:00
end
end
context "with topic you haven't posted in" do
let(:other_users_topic) { create_post(user: creator).topic }
it "does not include the topic" do
2015-01-10 00:34:37 +08:00
expect(topics).to be_blank
end
context "with topic you interacted with" do
it "is not included if read" do
TopicUser.update_last_read(user, other_users_topic.id, 0, 0, 0)
2015-01-10 00:34:37 +08:00
expect(topics).to be_blank
end
it "is not included if muted" do
other_users_topic.notify_muted!(user)
2015-01-10 00:34:37 +08:00
expect(topics).to be_blank
end
it "is not included if tracking" do
other_users_topic.notify_tracking!(user)
2015-01-10 00:34:37 +08:00
expect(topics).to be_blank
end
end
2013-02-06 03:16:51 +08:00
end
end
describe "#list_unseen" do
it "returns an empty list when there aren't topics" do
expect(topic_query.list_unseen.topics).to be_blank
end
it "doesn't return topics that were bumped last time before user joined the forum" do
user.first_seen_at = 10.minutes.ago
create_topic_with_three_posts(bumped_at: 15.minutes.ago)
expect(topic_query.list_unseen.topics).to be_blank
end
it "returns only topics that contain unseen posts" do
user.first_seen_at = 10.minutes.ago
topic_with_unseen_posts = create_topic_with_three_posts(bumped_at: 5.minutes.ago)
read_to_post(topic_with_unseen_posts, user, 1)
fully_read_topic = create_topic_with_three_posts(bumped_at: 5.minutes.ago)
read_to_the_end(fully_read_topic, user)
expect(topic_query.list_unseen.topics).to eq([topic_with_unseen_posts])
end
it "ignores staff posts if user is not staff" do
user.first_seen_at = 10.minutes.ago
topic = create_topic_with_three_posts(bumped_at: 5.minutes.ago)
read_to_the_end(topic, user)
create_post(topic: topic, post_type: Post.types[:whisper])
expect(topic_query.list_unseen.topics).to be_blank
end
def create_topic_with_three_posts(bumped_at:)
topic = Fabricate(:topic, bumped_at: bumped_at)
Fabricate(:post, topic: topic)
Fabricate(:post, topic: topic)
Fabricate(:post, topic: topic)
topic.highest_staff_post_number = 3
topic.highest_post_number = 3
topic
end
def read_to_post(topic, user, post_number)
TopicUser.update_last_read(user, topic.id, post_number, 0, 0)
end
def read_to_the_end(topic, user)
read_to_post topic, user, topic.highest_post_number
end
end
describe "#list_related_for" do
let(:user) { Fabricate(:user) }
let(:sender) { Fabricate(:user) }
let(:group_with_user) do
group = Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone])
group.add(user)
group.save
group
end
def create_pm(user, opts = nil)
unless opts
opts = user
user = nil
end
create_post(opts.merge(user: user, archetype: Archetype.private_message)).topic
end
def read(user, topic, post_number)
TopicUser.update_last_read(user, topic, post_number, post_number, 10_000)
end
2013-02-06 03:16:51 +08:00
before do
user.change_trust_level!(4)
sender.change_trust_level!(4)
end
it "returns the correct suggestions" do
pm_to_group = create_pm(sender, target_group_names: [group_with_user.name])
pm_to_user = create_pm(sender, target_usernames: [user.username])
other_user = Fabricate(:user)
other_user.change_trust_level!(1)
old_unrelated_pm = create_pm(other_user, target_usernames: [user.username])
read(user, old_unrelated_pm, 1)
related_by_user_pm = create_pm(sender, target_usernames: [user.username])
read(user, related_by_user_pm, 1)
related_by_group_pm = create_pm(sender, target_group_names: [group_with_user.name])
read(user, related_by_group_pm, 1)
expect(TopicQuery.new(user).list_related_for(pm_to_group).topics.map(&:id)).to(
eq([related_by_group_pm.id]),
)
expect(TopicQuery.new(user).list_related_for(pm_to_user).topics.map(&:id)).to(
eq([related_by_user_pm.id]),
)
SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:staff]
expect(TopicQuery.new(user).list_related_for(pm_to_group)).to be_blank
expect(TopicQuery.new(user).list_related_for(pm_to_user)).to be_blank
end
end
describe "suggested_for" do
def clear_cache!
Discourse.redis.keys("random_topic_cache*").each { |k| Discourse.redis.del k }
end
2015-02-25 15:09:45 +08:00
before { clear_cache! }
2015-02-25 15:09:45 +08:00
2013-02-06 03:16:51 +08:00
context "when anonymous" do
let(:topic) { Fabricate(:topic) }
let!(:new_topic) { Fabricate(:post, user: creator).topic }
it "should return the new topic" do
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new.list_suggested_for(topic).topics).to eq([new_topic])
2013-02-26 00:42:20 +08:00
end
end
2013-02-06 03:16:51 +08:00
context "when anonymously browsing with invisible, closed and archived" do
let!(:topic) { Fabricate(:topic) }
let!(:regular_topic) { Fabricate(:post, user: creator).topic }
let!(:closed_topic) { Fabricate(:topic, user: creator, closed: true) }
let!(:archived_topic) { Fabricate(:topic, user: creator, archived: true) }
let!(:invisible_topic) { Fabricate(:topic, user: creator, visible: false) }
it "should omit the closed/archived/invisible topics from suggested" do
2015-01-10 00:34:37 +08:00
expect(TopicQuery.new.list_suggested_for(topic).topics).to eq([regular_topic])
end
2013-02-06 03:16:51 +08:00
end
context "when logged in" do
def suggested_for(topic)
topic_query.list_suggested_for(topic)&.topics&.map { |t| t.id }
end
2013-02-06 03:16:51 +08:00
let(:topic) { Fabricate(:topic) }
2015-03-05 14:47:34 +08:00
let(:suggested_topics) do
tt = topic
# lets clear cache once category is created - working around caching is hard
clear_cache!
suggested_for(tt)
end
2013-02-06 03:16:51 +08:00
it "should return empty results when there is nothing to find" do
2015-01-10 00:34:37 +08:00
expect(suggested_topics).to be_blank
2013-02-06 03:16:51 +08:00
end
context "with random suggested" do
let!(:new_topic) { Fabricate(:topic, created_at: 2.days.ago) }
let!(:old_topic) { Fabricate(:topic, created_at: 3.years.ago) }
it "respects suggested_topics_max_days_old" do
SiteSetting.suggested_topics_max_days_old = 1365
tt = topic
clear_cache!
expect(topic_query.list_suggested_for(tt).topics.length).to eq(2)
SiteSetting.suggested_topics_max_days_old = 365
clear_cache!
expect(topic_query.list_suggested_for(tt).topics.length).to eq(1)
end
it "removes muted topics" do
SiteSetting.suggested_topics_max_days_old = 1365
tt = topic
TopicNotifier.new(old_topic).mute!(user)
clear_cache!
topics = topic_query.list_suggested_for(tt).topics
expect(topics.length).to eq(1)
expect(topics).not_to include(old_topic)
end
end
context "with private messages" do
let(:group_user) { Fabricate(:user) }
let(:group) { Fabricate(:group) }
let(:another_group) { Fabricate(:group) }
let!(:topic) do
Fabricate(
:private_message_topic,
topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)],
topic_allowed_groups: [Fabricate.build(:topic_allowed_group, group: group)],
)
end
let!(:private_message) do
Fabricate(
:private_message_topic,
topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)],
topic_allowed_groups: [
Fabricate.build(:topic_allowed_group, group: group),
Fabricate.build(:topic_allowed_group, group: another_group),
],
)
end
let!(:private_group_topic) do
Fabricate(
:private_message_topic,
user: Fabricate(:user),
topic_allowed_groups: [Fabricate.build(:topic_allowed_group, group: group)],
)
end
before do
group.add(group_user)
another_group.add(user)
Group.user_trust_level_change!(user.id, user.trust_level)
Group.user_trust_level_change!(group_user.id, group_user.trust_level)
end
context "as user not part of group" do
let!(:user) { Fabricate(:user) }
it "should not return topics by the group user" do
expect(suggested_topics).to eq([private_message.id])
end
end
context "as user part of group" do
let!(:user) { group_user }
it "should return the group topics" do
expect(suggested_topics).to match_array([private_group_topic.id, private_message.id])
end
context "when user is not in personal_message_enabled_groups" do
before do
SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:trust_level_4]
end
it "should not return topics by the group user" do
expect(suggested_topics).to eq(nil)
end
end
end
context "with tag filter" do
let(:tag) { Fabricate(:tag) }
let!(:user) { group_user }
it "should return only tagged topics" do
Fabricate(:topic_tag, topic: private_message, tag: tag)
Fabricate(:topic_tag, topic: private_group_topic)
expect(
TopicQuery.new(user, tags: [tag.name]).list_private_messages_tag(user).topics,
).to eq([private_message])
end
end
end
2013-02-06 03:16:51 +08:00
context "with some existing topics" do
let!(:old_partially_read) do
topic = Fabricate(:post, user: creator).topic
Fabricate(:post, user: creator, topic: topic)
topic
end
let!(:partially_read) do
topic = Fabricate(:post, user: creator).topic
Fabricate(:post, user: creator, topic: topic)
topic
end
2013-02-06 03:16:51 +08:00
let!(:new_topic) { Fabricate(:post, user: creator).topic }
2013-02-26 00:42:20 +08:00
let!(:fully_read) { Fabricate(:post, user: creator).topic }
let!(:closed_topic) { Fabricate(:topic, user: creator, closed: true) }
let!(:archived_topic) { Fabricate(:topic, user: creator, archived: true) }
let!(:invisible_topic) { Fabricate(:topic, user: creator, visible: false) }
let!(:fully_read_closed) { Fabricate(:post, user: creator).topic }
let!(:fully_read_archived) { Fabricate(:post, user: creator).topic }
2013-02-06 03:16:51 +08:00
before do
user.user_option.update!(
auto_track_topics_after_msecs: 0,
new_topic_duration_minutes: User::NewTopicDuration::ALWAYS,
)
freeze_time 3.weeks.from_now
TopicUser.update_last_read(user, old_partially_read.id, 1, 1, 0)
TopicUser.update_last_read(user, partially_read.id, 1, 1, 0)
TopicUser.update_last_read(user, fully_read.id, 1, 1, 0)
TopicUser.update_last_read(user, fully_read_closed.id, 1, 1, 0)
TopicUser.update_last_read(user, fully_read_archived.id, 1, 1, 0)
fully_read_closed.closed = true
fully_read_closed.save
fully_read_archived.archived = true
fully_read_archived.save
2013-02-06 03:16:51 +08:00
old_partially_read.update!(updated_at: 2.weeks.ago)
partially_read.update!(updated_at: Time.now)
end
it "operates correctly" do
# Note, this is a pretty slow integration test
# it tests that suggested is returned in the expected order
# hence we run suggested_for twice here to save on all the setup
2013-02-06 03:16:51 +08:00
2015-02-27 06:40:10 +08:00
SiteSetting.suggested_topics = 4
SiteSetting.suggested_topics_unread_max_days_old = 7
2015-01-10 00:34:37 +08:00
expect(suggested_topics[0]).to eq(partially_read.id)
expect(suggested_topics[1, 3]).to contain_exactly(
new_topic.id,
closed_topic.id,
archived_topic.id,
)
expect(suggested_topics.length).to eq(4)
SiteSetting.suggested_topics = 2
SiteSetting.suggested_topics_unread_max_days_old = 15
expect(suggested_for(topic)).to contain_exactly(partially_read.id, old_partially_read.id)
2013-02-06 03:16:51 +08:00
end
2013-02-26 00:42:20 +08:00
end
2013-02-06 03:16:51 +08:00
end
end
describe "#list_group_topics" do
fab!(:group) { Fabricate(:group) }
let(:user) do
user = Fabricate(:user)
group.add(user)
user
end
let(:user2) do
user = Fabricate(:user)
group.add(user)
user
end
fab!(:user3) { Fabricate(:user) }
fab!(:private_category) { Fabricate(:private_category_with_definition, group: group) }
let!(:private_message_topic) { Fabricate(:private_message_post, user: user).topic }
let!(:topic1) { Fabricate(:topic, user: user) }
let!(:topic2) { Fabricate(:topic, user: user, category: Fabricate(:category_with_definition)) }
let!(:topic3) { Fabricate(:topic, user: user, category: private_category) }
let!(:topic4) { Fabricate(:topic) }
let!(:topic5) { Fabricate(:topic, user: user, visible: false) }
let!(:topic6) { Fabricate(:topic, user: user2) }
it "should return the right lists for anon user" do
topics = TopicQuery.new.list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic6)
end
it "should return the right list for users in the same group" do
topics = TopicQuery.new(user).list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic3, topic6)
topics = TopicQuery.new(user2).list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic3, topic6)
end
it "should return the right list for user no in the group" do
topics = TopicQuery.new(user3).list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic6)
end
end
describe "shared drafts" do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:shared_drafts_category) { Fabricate(:category_with_definition) }
fab!(:topic) { Fabricate(:topic, category: shared_drafts_category) }
fab!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: category) }
fab!(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group) }
before do
shared_drafts_category.set_permissions(group => :full)
shared_drafts_category.save
SiteSetting.shared_drafts_category = shared_drafts_category.id
SiteSetting.shared_drafts_min_trust_level = TrustLevel[3]
end
context "with destination_category_id" do
it "doesn't allow regular users to query destination_category_id" do
list = TopicQuery.new(user, destination_category_id: category.id).list_latest
expect(list.topics).not_to include(topic)
end
it "allows staff users to query destination_category_id" do
list = TopicQuery.new(admin, destination_category_id: category.id).list_latest
expect(list.topics).to include(topic)
end
it "allow group members with enough trust level to query destination_category_id" do
member = Fabricate(:user, trust_level: TrustLevel[3])
group.add(member)
list = TopicQuery.new(member, destination_category_id: category.id).list_latest
expect(list.topics).to include(topic)
end
it "doesn't allow group members without enough trust level to query destination_category_id" do
member = Fabricate(:user, trust_level: TrustLevel[2])
group.add(member)
list = TopicQuery.new(member, destination_category_id: category.id).list_latest
expect(list.topics).not_to include(topic)
end
end
context "with latest" do
it "doesn't include shared topics unless filtering by category" do
list = TopicQuery.new(moderator).list_latest
expect(list.topics).not_to include(topic)
end
it "doesn't include shared draft topics for regular users" do
group.add(user)
SiteSetting.shared_drafts_category = nil
list = TopicQuery.new(user).list_latest
expect(list.topics).to include(topic)
SiteSetting.shared_drafts_category = shared_drafts_category.id
list = TopicQuery.new(user).list_latest
expect(list.topics).not_to include(topic)
end
it "doesn't include shared draft topics for group members with access to shared drafts" do
member = Fabricate(:user, trust_level: TrustLevel[3])
group.add(member)
list = TopicQuery.new(member).list_latest
expect(list.topics).not_to include(topic)
end
end
context "with unread" do
let!(:partially_read) do
topic = Fabricate(:topic, category: shared_drafts_category)
Fabricate(:post, user: creator, topic: topic).topic
TopicUser.update_last_read(admin, topic.id, 0, 0, 0)
TopicUser.change(
admin.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
topic
end
it "does not remove topics from unread" do
expect(TopicQuery.new(admin).list_latest.topics).not_to include(partially_read) # Check we set up the topic/category correctly
expect(TopicQuery.new(admin).list_unread.topics).to include(partially_read)
end
end
end
2013-02-06 03:16:51 +08:00
end