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