discourse/spec/lib/topic_query_spec.rb

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

2296 lines
76 KiB
Ruby
Raw Permalink Normal View History

# frozen_string_literal: true
2013-02-05 14:16:51 -05: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, refresh_auto_groups: true) }
fab!(:creator) { Fabricate(:user, refresh_auto_groups: true) }
2013-02-05 14:16:51 -05:00
let(:topic_query) { TopicQuery.new(user) }
fab!(:tl4_user) { Fabricate(:trust_level_4) }
fab!(:moderator)
fab!(:admin)
2013-02-05 14:16:51 -05: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-09 11:34:37 -05: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-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(TopicQuery.new(moderator).list_latest.topics.count).to eq(1)
# admins can see all the topics
2015-01-09 11:34:37 -05:00
expect(TopicQuery.new(admin).list_latest.topics.count).to eq(3)
group.add(user)
group.save
2015-01-09 11:34:37 -05: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 04:09:08 -04: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 "#list_hot" do
it "keeps pinned topics on top" do
pinned_topic =
Fabricate(
:topic,
created_at: 1.hour.ago,
pinned_at: 1.hour.ago,
pinned_globally: true,
like_count: 1,
)
_topic = Fabricate(:topic, created_at: 5.minute.ago, like_count: 100)
topic = Fabricate(:topic, created_at: 1.minute.ago, like_count: 100)
# pinned topic is older so generally it would not hit the batch without
# extra special logic
TopicHotScore.update_scores(2)
expect(TopicQuery.new(nil).list_hot.topics.map(&:id)).to eq([pinned_topic.id, topic.id])
SiteSetting.tagging_enabled = true
user = Fabricate(:user)
tag = Fabricate(:tag)
TagUser.create!(
user_id: user.id,
tag_id: tag.id,
notification_level: NotificationLevels.all[:muted],
)
topic.update!(tags: [tag])
# even though it is muted, we should still show it cause we are filtered to it
expect(TopicQuery.new(user, { tags: [tag.name] }).list_hot.topics.map(&:id)).to eq([topic.id])
end
it "excludes muted categories and topics" do
muted_category = Fabricate(:category)
muted_topic = Fabricate(:topic, category: muted_category)
TopicHotScore.create!(topic_id: muted_topic.id, score: 1.0)
expect(TopicQuery.new(user).list_hot.topics.map(&:id)).to include(muted_topic.id)
tu =
TopicUser.create!(
user_id: user.id,
topic_id: muted_topic.id,
notification_level: TopicUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).list_hot.topics.map(&:id)).not_to include(muted_topic.id)
tu.destroy!
CategoryUser.create!(
user_id: user.id,
category_id: muted_category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).list_hot.topics.map(&:id)).not_to include(muted_topic.id)
end
end
describe "#prioritize_pinned_topics" do
2017-07-10 18:25:29 -04: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-10 18:25:29 -04: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-10 18:25:29 -04: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-10 18:25:29 -04: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)
Fabricate(:category_moderation_group, category:, group:)
_topic = Fabricate(:topic, category: category, deleted_at: 1.year.ago)
2015-01-09 11:34:37 -05: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-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(TopicQuery.new(moderator).list_latest.topics.size).to eq(0)
# Filter by slug
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(list.topics.size).to eq(1)
expect(list.preload_key).to eq("topic_list")
2013-12-13 17:18:28 -05:00
# Defaults to no category filter when slug does not exist
2015-01-09 11:34:37 -05: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-13 17:18:28 -05: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-27 22:05:00 -04: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-13 17:18:28 -05: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-27 22:05:00 -04: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-13 17:18:28 -05: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-27 22:05:00 -04: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-27 22:05:00 -04: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-27 22:05:00 -04: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-27 22:05:00 -04: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-13 17:18:28 -05:00
end
end
describe "tag filter" do
fab!(: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) }
fab!(:tag_group) do
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [other_tag.name])
end
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 "does not exclude a tagged topic without permission" do
topics = TopicQuery.new(user, exclude_tag: other_tag.name).list_latest.topics
expect(topics.map(&:id)).to include(tagged_topic2.id)
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 01:39:29 -04:00
tagged_topic1,
tagged_topic3,
)
2018-03-19 01:39:29 -04:00
expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics).to contain_exactly(
tagged_topic1,
tagged_topic3,
)
2018-03-19 01:39:29 -04: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 01:38:16 -04: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-15 15:42:06 -04: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-09 11:34:37 -05: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-14 15:50:36 -05: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-14 15:50:36 -05: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-14 15:50:36 -05: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-14 15:50:36 -05: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-14 15:50:36 -05: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-05 14:16:51 -05:00
context "with list_latest" do
2013-02-05 14:16:51 -05:00
it "returns the topics in the correct order" do
2015-01-09 11:34:37 -05:00
expect(topics.map(&:id)).to eq(
[pinned_topic, future_topic, closed_topic, archived_topic, regular_topic].map(&:id),
)
2013-02-05 14:16:51 -05:00
# includes the invisible topic if you're a moderator
2015-01-09 11:34:37 -05:00
expect(TopicQuery.new(moderator).list_latest.topics.include?(invisible_topic)).to eq(true)
2013-02-05 14:16:51 -05:00
# includes the invisible topic if you're an admin
2015-01-09 11:34:37 -05:00
expect(TopicQuery.new(admin).list_latest.topics.include?(invisible_topic)).to eq(true)
# includes the invisible topic if you're a TL4 user
expect(TopicQuery.new(tl4_user).list_latest.topics.include?(invisible_topic)).to eq(true)
2013-02-05 14:16:51 -05: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-09 11:34:37 -05:00
expect(ids_in_order("posts")).to eq(
[
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(ids_in_order("posts", false)).to eq(
[
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(ids_in_order("likes")).to eq(
[
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(ids_in_order("likes", false)).to eq(
[
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(ids_in_order("views")).to eq(
[
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(ids_in_order("views", false)).to eq(
[
2015-01-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(ids_in_order("posters")).to eq(
[
2015-01-09 11:34:37 -05:00
pinned_topic,
regular_topic,
future_topic,
invisible_topic,
closed_topic,
archived_topic,
].map(&:id),
)
2013-11-14 15:50:36 -05:00
# returns the topics in reverse posters order if requested" do
2015-01-09 11:34:37 -05:00
expect(ids_in_order("posters", false)).to eq(
[
2015-01-09 11:34:37 -05: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-14 15:50:36 -05:00
end
end
2013-02-05 14:16:51 -05: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-09 11:34:37 -05:00
expect(topics).to eq(
[future_topic, closed_topic, archived_topic, pinned_topic, regular_topic],
)
end
end
2013-02-05 14:16:51 -05: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-24 22:39:50 -05: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 "uses the category's default sort order when filter is passed as a string" do
category.update!(sort_order: "created", sort_ascending: true)
topic_ids =
TopicQuery.new(user, category: category.id, filter: "latest").list_latest.topics.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat1.id, topic_in_cat2.id])
end
it "uses the category's default sort order when filter=default is passed explicitly" do
category.update!(sort_order: "created", sort_ascending: true)
topic_ids =
TopicQuery
.new(user, category: category.id, filter: "default")
.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-05 14:16:51 -05:00
end
describe "unread / read topics" do
2013-02-05 14:16:51 -05:00
context "with no data" do
it "has no unread topics" do
2015-01-09 11:34:37 -05:00
expect(topic_query.list_unread.topics).to be_blank
2013-02-05 14:16:51 -05: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-05 14:16:51 -05:00
context "with read data" do
fab!(:partially_read) { Fabricate(:post, user: creator).topic }
fab!(:fully_read) { Fabricate(:post, user: creator).topic }
2013-02-05 14:16:51 -05: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-05 14:16:51 -05: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-05 14:16:51 -05:00
end
end
context "with user with auto_track_topics list_unread" do
2013-02-25 11:42:20 -05:00
before do
user.user_option.auto_track_topics_after_msecs = 0
user.user_option.save
2013-02-05 14:16:51 -05:00
end
2013-02-25 11:42:20 -05:00
2013-02-05 14:16:51 -05:00
it "only contains the partially read topic" do
expect(topic_query.list_unread.topics).to eq([partially_read])
2013-02-05 14:16:51 -05:00
end
end
end
end
describe "#list_new" do
2013-02-05 14:16:51 -05:00
context "without a new topic" do
it "has no new topics" do
2015-01-09 11:34:37 -05:00
expect(topic_query.list_new.topics).to be_blank
2013-02-05 14:16:51 -05: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-29 23:38:04 -04: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-05 14:16:51 -05:00
context "with a new topic" do
2013-02-25 11:42:20 -05:00
let!(:new_topic) { Fabricate(:topic, user: creator, bumped_at: 10.minutes.ago) }
2013-02-05 14:16:51 -05: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-05 14:16:51 -05:00
before { new_topic.notify_muted!(user) }
2013-02-25 11:42:20 -05:00
it "returns an empty set" do
2015-01-09 11:34:37 -05:00
expect(topics).to be_blank
2015-11-01 23:05:08 -05:00
expect(topic_query.list_latest.topics).to be_blank
2013-02-05 14:16:51 -05:00
end
context "when un-muted" do
2013-02-05 14:16:51 -05:00
before { new_topic.notify_tracking!(user) }
it "returns the topic again" do
2015-01-09 11:34:37 -05:00
expect(topics).to eq([new_topic])
2015-11-01 23:05:08 -05:00
expect(topic_query.list_latest.topics).not_to be_blank
2013-02-05 14:16:51 -05:00
end
end
2013-02-25 11:42:20 -05:00
end
2013-02-05 14:16:51 -05:00
end
end
describe "#list_posted" do
2013-02-05 14:16:51 -05:00
let(:topics) { topic_query.list_posted.topics }
it "returns blank when there are no posted topics" do
2015-01-09 11:34:37 -05:00
expect(topics).to be_blank
2013-02-05 14:16:51 -05:00
end
context "with created topics" do
let!(:created_topic) { create_post(user: user).topic }
2013-02-05 14:16:51 -05:00
it "includes the created topic" do
2015-01-09 11:34:37 -05:00
expect(topics.include?(created_topic)).to eq(true)
2013-02-05 14:16:51 -05: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-05 14:16:51 -05:00
it "includes the posted topic" do
2015-01-09 11:34:37 -05:00
expect(topics.include?(other_users_topic)).to eq(true)
2013-02-05 14:16:51 -05: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-09 11:34:37 -05: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-09 11:34:37 -05:00
expect(topics).to be_blank
end
it "is not included if muted" do
other_users_topic.notify_muted!(user)
2015-01-09 11:34:37 -05:00
expect(topics).to be_blank
end
it "is not included if tracking" do
other_users_topic.notify_tracking!(user)
2015-01-09 11:34:37 -05:00
expect(topics).to be_blank
end
end
2013-02-05 14:16:51 -05: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.id, post_number, post_number, 10_000)
end
2013-02-05 14:16:51 -05: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 "#list_suggested_for" do
use_redis_snapshotting
def clear_cache!
Discourse.redis.keys("random_topic_cache*").each { |k| Discourse.redis.del k }
end
2015-02-25 02:09:45 -05:00
2013-02-05 14:16:51 -05: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-09 11:34:37 -05:00
expect(TopicQuery.new.list_suggested_for(topic).topics).to eq([new_topic])
2013-02-25 11:42:20 -05:00
end
it "should return the nothing when random topics excluded" do
expect(TopicQuery.new.list_suggested_for(topic, include_random: false).topics).to eq([])
end
end
2013-02-05 14:16:51 -05: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-09 11:34:37 -05:00
expect(TopicQuery.new.list_suggested_for(topic).topics).to eq([regular_topic])
end
2013-02-05 14:16:51 -05:00
end
context "with a custom suggested provider registered" do
let!(:topic1) { Fabricate(:topic) }
let!(:topic2) { Fabricate(:topic) }
let!(:topic3) { Fabricate(:topic) }
let!(:topic4) { Fabricate(:topic) }
let!(:topic5) { Fabricate(:topic) }
let!(:topic6) { Fabricate(:topic) }
let!(:topic7) { Fabricate(:topic) }
let(:plugin_class) do
Class.new(Plugin::Instance) do
attr_accessor :enabled
def enabled?
true
end
def self.custom_suggested_topics(topic, pm_params, topic_query)
{ result: Topic.order("id desc").limit(1), params: {} }
end
end
end
let(:plugin) { plugin_class.new }
it "should return suggested defined by the custom provider" do
DiscoursePluginRegistry.register_list_suggested_for_provider(
plugin_class.method(:custom_suggested_topics),
plugin,
)
expect(TopicQuery.new.list_suggested_for(topic1).topics).to include(Topic.last)
DiscoursePluginRegistry.reset_register!(:list_suggested_for_providers)
end
end
context "when logged in and user is part of the `experimental_new_new_view_groups` site setting groups" do
fab!(:group)
fab!(:topic)
before do
SiteSetting.experimental_new_new_view_groups = group.name
group.add(user)
end
after { clear_cache! }
context "when there are no new topics for user" do
it "should return random topics excluding topics that are muted by user and not older than `suggested_topics_max_days_old` site setting" do
topic2 = Fabricate(:topic, user: user)
topic3 = Fabricate(:topic, user: user)
_topic4 = Fabricate(:topic, user: user, created_at: 8.days.ago)
_topic5 = Fabricate(:topic).tap { |t| TopicNotifier.new(t).mute!(user) }
SiteSetting.suggested_topics_max_days_old = 7
expect(topic_query.list_suggested_for(topic).topics.map(&:id)).to eq(
[topic3.id, topic2.id],
)
end
end
context "when there are new topics for user" do
fab!(:category)
fab!(:category2) { Fabricate(:category) }
fab!(:topic_in_category_that_user_created_and_has_partially_read) do
Fabricate(:topic, user: user, category:).tap do |t|
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
user.id,
t.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, t.id, second_post.post_number - 1, 1, 1)
end
end
fab!(:topic_in_category2_that_user_created_and_has_partially_read) do
Fabricate(:topic, user: user, category: category2).tap do |t|
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
user.id,
t.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, t.id, second_post.post_number - 1, 1, 1)
end
end
fab!(:topic_in_category_that_user_has_partially_read) do
Fabricate(:topic, category:).tap do |t|
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
user.id,
t.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, t.id, second_post.post_number - 1, 1, 1)
end
end
fab!(:topic_in_category2_that_user_has_partially_read) do
Fabricate(:topic, category: category2).tap do |t|
_first_post = Fabricate(:post, topic: t)
second_post = Fabricate(:post, topic: t)
TopicUser.change(
user.id,
t.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, t.id, second_post.post_number - 1, 1, 1)
end
end
fab!(:topic_in_category_that_user_has_not_read) { Fabricate(:topic, category:) }
fab!(:topic_in_category2_that_user_has_not_read) { Fabricate(:topic, category: category2) }
before { topic.update!(category:) }
it "should return new topics for user ordered by topics that user has created first, in the same category as the topic and then topic's bumped at" do
expect(
topic_query.list_suggested_for(topic, include_random: false).topics.map(&:id),
).to eq(
[
topic_in_category_that_user_created_and_has_partially_read.id,
topic_in_category2_that_user_created_and_has_partially_read.id,
topic_in_category_that_user_has_not_read.id,
topic_in_category_that_user_has_partially_read.id,
topic_in_category2_that_user_has_not_read.id,
],
)
SiteSetting.suggested_topics = 6
expect(
topic_query.list_suggested_for(topic, include_random: false).topics.map(&:id),
).to eq(
[
topic_in_category_that_user_created_and_has_partially_read.id,
topic_in_category2_that_user_created_and_has_partially_read.id,
topic_in_category_that_user_has_not_read.id,
topic_in_category_that_user_has_partially_read.id,
topic_in_category2_that_user_has_not_read.id,
topic_in_category2_that_user_has_partially_read.id,
],
)
end
end
end
2013-02-05 14:16:51 -05:00
context "when logged in" do
def suggested_for(topic)
topic_query.list_suggested_for(topic)&.topics&.map { |t| t.id }
end
2013-02-05 14:16:51 -05:00
let(:topic) { Fabricate(:topic) }
2015-03-05 01:47:34 -05: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-05 14:16:51 -05:00
it "should return empty results when there is nothing to find" do
2015-01-09 11:34:37 -05:00
expect(suggested_topics).to be_blank
2013-02-05 14:16:51 -05: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-05 14:16:51 -05: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-05 14:16:51 -05:00
let!(:new_topic) { Fabricate(:post, user: creator).topic }
2013-02-25 11:42:20 -05: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-05 14:16:51 -05: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-05 14:16:51 -05: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-05 14:16:51 -05:00
2015-02-26 17:40:10 -05:00
SiteSetting.suggested_topics = 4
SiteSetting.suggested_topics_unread_max_days_old = 7
2015-01-09 11:34:37 -05: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-05 14:16:51 -05:00
end
2013-02-25 11:42:20 -05:00
end
2013-02-05 14:16:51 -05:00
end
end
describe "#list_group_topics" do
fab!(: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)
fab!(:user)
fab!(: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_allowed_groups =
Group::AUTO_GROUPS[:trust_level_3].to_s + "|" + Group::AUTO_GROUPS[:staff].to_s
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
describe "#new_and_unread_results" do
fab!(:unread_topic) { Fabricate(:post).topic }
fab!(:new_topic) { Fabricate(:post).topic }
fab!(:read_topic) { Fabricate(:post).topic }
before do
unread_post = Fabricate(:post, topic: unread_topic)
read_post = Fabricate(:post, topic: read_topic)
TopicUser.change(
user.id,
unread_topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.change(
user.id,
read_topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, unread_topic.id, unread_post.post_number - 1, 1, 1)
TopicUser.update_last_read(user, read_topic.id, read_post.post_number, 1, 1)
end
it "includes unread and new topics for the user" do
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
new_topic.id,
)
end
it "doesn't include deleted topics" do
unread_topic.trash!
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
new_topic.id,
)
end
it "doesn't include muted topics with unread posts" do
TopicUser.change(
user.id,
unread_topic.id,
notification_level: TopicUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
new_topic.id,
)
end
it "doesn't include muted new topics" do
TopicUser.change(
user.id,
new_topic.id,
notification_level: TopicUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
it "doesn't include new topics in muted category" do
CategoryUser.create!(
user_id: user.id,
category_id: new_topic.category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
it "includes unread and trakced topics even if they're in a muted category" do
new_topic.update!(category: Fabricate(:category))
CategoryUser.create!(
user_id: user.id,
category_id: unread_topic.category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
new_topic.id,
)
end
it "doesn't include new topics that have a muted tag(s)" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
new_topic.tags << tag
new_topic.save!
TagUser.create!(
tag_id: tag.id,
user_id: user.id,
notification_level: NotificationLevels.all[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
it "includes unread and tracked topics even if they have a muted tag(s)" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
unread_topic.tags << tag
unread_topic.save!
TagUser.create!(
tag_id: tag.id,
user_id: user.id,
notification_level: NotificationLevels.all[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
new_topic.id,
)
end
it "doesn't include topics in restricted categories that user cannot access" do
category = Fabricate(:category_with_definition)
group = Fabricate(:group)
category.set_permissions(group => :full)
category.save!
unread_topic.update!(category: category)
new_topic.update!(category: category)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to be_blank
end
it "doesn't include dismissed topics" do
DismissedTopicUser.create!(
user_id: user.id,
topic_id: new_topic.id,
created_at: Time.zone.now,
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
end
describe "#apply_ordering" do
fab!(:topic1) { Fabricate(:topic, spam_count: 3, bumped_at: 3.hours.ago) }
fab!(:topic2) { Fabricate(:topic, spam_count: 2, bumped_at: 3.minutes.ago) }
fab!(:topic3) { Fabricate(:topic, spam_count: 3, bumped_at: 1.hour.ago) }
let(:modifier_block) do
Proc.new do |result, sort_column, sort_dir, options, topic_query|
if sort_column == "spam"
sort_column = "spam_count"
result.order("topics.#{sort_column} #{sort_dir}, bumped_at DESC")
end
end
end
it "returns the result of topic_query_apply_ordering_result modifier" do
plugin_instance = Plugin::Instance.new
plugin_instance.register_modifier(:topic_query_apply_ordering_result, &modifier_block)
topics = TopicQuery.new(nil, order: "spam", ascending: "false").list_latest.topics
expect(topics.map(&:id)).to eq([topic3.id, topic1.id, topic2.id])
ensure
DiscoursePluginRegistry.unregister_modifier(
plugin_instance,
:topic_query_apply_ordering_result,
&modifier_block
)
end
it "ignores the result of topic_query_apply_ordering_result if modifier not registered" do
topics = TopicQuery.new(nil, order: "spam", ascending: "false").list_latest.topics
expect(topics.map(&:id)).to eq([topic2.id, topic3.id, topic1.id])
end
end
describe "show_category_definitions_in_topic_lists setting" do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:subcategory) { Fabricate(:category_with_definition, parent_category: category) }
fab!(:subcategory_regular_topic) { Fabricate(:topic, category: subcategory) }
it "excludes subcategory definition topics by default" do
expect(
TopicQuery.new(nil, category: category.id).list_latest.topics.map(&:id),
).to contain_exactly(category.topic_id, subcategory_regular_topic.id)
end
it "works when topic_id is null" do
subcategory.topic.destroy!
subcategory.update!(topic_id: nil)
expect(
TopicQuery.new(nil, category: category.id).list_latest.topics.map(&:id),
).to contain_exactly(category.topic_id, subcategory_regular_topic.id)
end
it "includes subcategory definition when setting enabled" do
SiteSetting.show_category_definitions_in_topic_lists = true
expect(
TopicQuery.new(nil, category: category.id).list_latest.topics.map(&:id),
).to contain_exactly(category.topic_id, subcategory.topic_id, subcategory_regular_topic.id)
end
end
describe "with topic_query_create_list_topics modifier" do
fab!(:topic1) { Fabricate(:topic, created_at: 3.days.ago, bumped_at: 1.hour.ago) }
fab!(:topic2) { Fabricate(:topic, created_at: 2.days.ago, bumped_at: 3.hour.ago) }
after { DiscoursePluginRegistry.clear_modifiers! }
it "allows changing" do
original_topic_query = TopicQuery.new(user)
Plugin::Instance
.new
.register_modifier(:topic_query_create_list_topics) do |topics, options, topic_query|
expect(topic_query).to eq(topic_query)
topic_query.options[:order] = "created"
topics
end
expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic1, topic2].map(&:id))
DiscoursePluginRegistry.clear_modifiers!
expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic2, topic1].map(&:id))
end
end
describe "precedence of categories and tag setting" do
fab!(:watched_category) do
Fabricate(:category).tap do |category|
CategoryUser.create!(
user: user,
category: category,
notification_level: CategoryUser.notification_levels[:watching],
)
end
end
fab!(:muted_category) do
Fabricate(:category).tap do |category|
CategoryUser.create!(
user: user,
category: category,
notification_level: CategoryUser.notification_levels[:muted],
)
end
end
fab!(:watched_tag) do
Fabricate(:tag).tap do |tag|
TagUser.create!(
user: user,
tag: tag,
notification_level: TagUser.notification_levels[:watching],
)
end
end
fab!(:muted_tag) do
Fabricate(:tag).tap do |tag|
TagUser.create!(
user: user,
tag: tag,
notification_level: TagUser.notification_levels[:muted],
)
end
end
fab!(:topic)
fab!(:topic_in_watched_category_and_muted_tag) do
Fabricate(:topic, category: watched_category, tags: [muted_tag])
end
fab!(:topic_in_muted_category_and_watched_tag) do
Fabricate(:topic, category: muted_category, tags: [watched_tag])
end
fab!(:topic_in_watched_and_muted_tag) { Fabricate(:topic, tags: [watched_tag, muted_tag]) }
fab!(:topic_in_muted_category) { Fabricate(:topic, category: muted_category) }
fab!(:topic_in_muted_tag) { Fabricate(:topic, tags: [muted_tag]) }
context "when enabled" do
it "returns topics even if category or tag is muted but another tag or category is watched" do
SiteSetting.watched_precedence_over_muted = true
query = TopicQuery.new(user).list_latest
expect(query.topics.map(&:id)).to contain_exactly(
topic.id,
topic_in_watched_category_and_muted_tag.id,
topic_in_muted_category_and_watched_tag.id,
)
end
end
context "when disabled" do
it "returns topics without muted category or tag" do
SiteSetting.watched_precedence_over_muted = false
query = TopicQuery.new(user).list_latest
expect(query.topics.map(&:id)).to contain_exactly(topic.id)
end
end
context "when disabled but overridden by user" do
it "returns topics even if category or tag is muted but another tag or category is watched" do
SiteSetting.watched_precedence_over_muted = false
user.user_option.update!(watched_precedence_over_muted: true)
query = TopicQuery.new(user).list_latest
expect(query.topics.map(&:id)).to contain_exactly(
topic.id,
topic_in_watched_category_and_muted_tag.id,
topic_in_muted_category_and_watched_tag.id,
)
end
end
end
2013-02-05 14:16:51 -05:00
end