discourse/spec/components/topic_query_spec.rb
Osama Sayegh ec352a1969
FEATURE: Order pinned topics by their pinned_at column (#14090)
Currently, pinned topics are ordered by the `bumped_at` column. This behavior is not desired because it gives admins no control over the order of pinned topics. This PR makes pinned topics ordered by the `pinned_at` column. A topic that is pinned last appears first in topic lists. If an admin wants an already pinned topic to appear first in the list of pinned topics, they'll have to unpin that topic and pin it again.

Meta topic: https://meta.discourse.org/t/how-do-i-set-the-order-of-pinned-topics/16935/23?u=osama.
2021-08-19 14:43:58 +03:00

1391 lines
50 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
require 'topic_view'
describe TopicQuery do
# TODO: this let! here has impact on all tests
# it indeed happens first, but is not obvious later in the tests we depend on the user being
# created so early otherwise finding new topics does not work
# we should remove the let! here and use freeze time to communicate how the clock moves
let!(:user) { Fabricate(:user) }
fab!(:creator) { Fabricate(:user) }
let(:topic_query) { TopicQuery.new(user) }
fab!(:moderator) { Fabricate(:moderator) }
fab!(:admin) { Fabricate(:admin) }
context 'secure category' do
it "filters categories out correctly" do
category = Fabricate(:category_with_definition)
group = Fabricate(:group)
category.set_permissions(group => :full)
category.save
Fabricate(:topic, category: category)
Fabricate(:topic, visible: false)
expect(TopicQuery.new(nil).list_latest.topics.count).to eq(0)
expect(TopicQuery.new(user).list_latest.topics.count).to eq(0)
expect(Topic.top_viewed(10).count).to eq(0)
expect(Topic.recent(10).count).to eq(0)
# mods can see hidden topics
expect(TopicQuery.new(moderator).list_latest.topics.count).to eq(1)
# admins can see all the topics
expect(TopicQuery.new(admin).list_latest.topics.count).to eq(3)
group.add(user)
group.save
expect(TopicQuery.new(user).list_latest.topics.count).to eq(2)
end
end
context "custom filters" do
it "allows custom filters to be applied" do
topic1 = Fabricate(:topic)
_topic2 = Fabricate(:topic)
TopicQuery.add_custom_filter(:only_topic_id) do |results, topic_query|
results = results.where('topics.id = ?', topic_query.options[:only_topic_id])
end
expect(TopicQuery.new(nil, only_topic_id: topic1.id).list_latest.topics.map(&:id)).to eq([topic1.id])
TopicQuery.remove_custom_filter(:only_topic_id)
end
end
context "#list_topics_by" do
it "allows users to view their own invisible topics" do
_topic = Fabricate(:topic, user: user)
_invisible_topic = Fabricate(:topic, user: user, visible: false)
expect(TopicQuery.new(nil).list_topics_by(user).topics.count).to eq(1)
expect(TopicQuery.new(user).list_topics_by(user).topics.count).to eq(2)
end
end
context "#prioritize_pinned_topics" do
it "does the pagination correctly" do
num_topics = 15
per_page = 3
topics = []
(num_topics - 1).downto(0).each do |i|
topics[i] = freeze_time(i.seconds.ago) { Fabricate(:topic) }
end
topic_query = TopicQuery.new(user)
results = topic_query.send(:default_results)
expect(topic_query.prioritize_pinned_topics(results,
per_page: per_page,
page: 0)
).to eq(topics[0...per_page])
expect(topic_query.prioritize_pinned_topics(results,
per_page: per_page,
page: 1)
).to eq(topics[per_page...num_topics])
end
it "orders globally pinned topics by pinned_at rather than bumped_at" do
pinned1 = Fabricate(
:topic,
bumped_at: 3.hour.ago,
pinned_at: 1.hours.ago,
pinned_until: 10.days.from_now,
pinned_globally: true
)
pinned2 = Fabricate(
:topic,
bumped_at: 2.hour.ago,
pinned_at: 4.hours.ago,
pinned_until: 10.days.from_now,
pinned_globally: true
)
unpinned1 = Fabricate(:topic, bumped_at: 2.hour.ago)
unpinned2 = Fabricate(:topic, bumped_at: 3.hour.ago)
topic_query = TopicQuery.new(user)
results = topic_query.send(:default_results)
expected_order = [pinned1, pinned2, unpinned1, unpinned2].map(&:id)
expect(topic_query
.prioritize_pinned_topics(results, per_page: 10, page: 0)
.pluck(:id)
).to eq(expected_order)
end
it "orders pinned topics within a category by pinned_at rather than bumped_at" do
cat = Fabricate(:category)
pinned1 = Fabricate(
:topic,
category: cat,
bumped_at: 3.hour.ago,
pinned_at: 1.hours.ago,
pinned_until: 10.days.from_now,
)
pinned2 = Fabricate(
:topic,
category: cat,
bumped_at: 2.hour.ago,
pinned_at: 4.hours.ago,
pinned_until: 10.days.from_now,
)
unpinned1 = Fabricate(:topic, category: cat, bumped_at: 2.hour.ago)
unpinned2 = Fabricate(:topic, category: cat, bumped_at: 3.hour.ago)
topic_query = TopicQuery.new(user)
results = topic_query.send(:default_results)
expected_order = [pinned1, pinned2, unpinned1, unpinned2].map(&:id)
expect(topic_query
.prioritize_pinned_topics(results, per_page: 10, page: 0, category_id: cat.id)
.pluck(:id)
).to eq(expected_order)
end
end
context 'bookmarks' do
it "filters and returns bookmarks correctly" do
post = Fabricate(:post)
reply = Fabricate(:post, topic: post.topic)
post2 = Fabricate(:post)
PostActionCreator.create(user, post, :bookmark)
PostActionCreator.create(user, reply, :bookmark)
TopicUser.change(user, post.topic, notification_level: 1)
TopicUser.change(user, post2.topic, notification_level: 1)
query = TopicQuery.new(user, filter: 'bookmarked').list_latest
expect(query.topics.length).to eq(1)
expect(query.topics.first.user_data.post_action_data).to eq(PostActionType.types[:bookmark] => [1, 2])
end
end
context 'tracked' do
it "filters tracked topics correctly" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
Fabricate(:topic, tags: [tag])
topic2 = Fabricate(:topic)
query = TopicQuery.new(user, filter: 'tracked').list_latest
expect(query.topics.length).to eq(0)
TagUser.create!(
tag_id: tag.id,
user_id: user.id,
notification_level: NotificationLevels.all[:watching]
)
cu = CategoryUser.create!(
category_id: topic2.category_id,
user_id: user.id,
notification_level: NotificationLevels.all[:regular]
)
query = TopicQuery.new(user, filter: 'tracked').list_latest
expect(query.topics.length).to eq(1)
cu.update!(notification_level: NotificationLevels.all[:tracking])
query = TopicQuery.new(user, filter: 'tracked').list_latest
expect(query.topics.length).to eq(2)
# includes subcategories of tracked categories
parentcat = Fabricate(:category)
subcat = Fabricate(:category, parent_category_id: parentcat.id)
topic3 = Fabricate(:topic, category_id: subcat.id)
CategoryUser.create!(
category_id: parentcat.id,
user_id: user.id,
notification_level: NotificationLevels.all[:tracking]
)
query = TopicQuery.new(user, filter: 'tracked').list_latest
expect(query.topics.length).to eq(3)
end
end
context 'deleted filter' do
it "filters deleted topics correctly" do
_topic = Fabricate(:topic, deleted_at: 1.year.ago)
expect(TopicQuery.new(admin, status: 'deleted').list_latest.topics.size).to eq(1)
expect(TopicQuery.new(moderator, status: 'deleted').list_latest.topics.size).to eq(1)
expect(TopicQuery.new(user, status: 'deleted').list_latest.topics.size).to eq(0)
expect(TopicQuery.new(nil, status: 'deleted').list_latest.topics.size).to eq(0)
end
end
describe 'include_pms option' do
it "includes users own pms in regular topic lists" do
topic = Fabricate(:topic)
own_pm = Fabricate(:private_message_topic, user: user)
other_pm = Fabricate(:private_message_topic, user: Fabricate(:user))
expect(TopicQuery.new(user).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(admin).list_latest.topics).to contain_exactly(topic)
expect(TopicQuery.new(user, include_pms: true).list_latest.topics).to contain_exactly(topic, own_pm)
end
end
context 'category filter' do
let(:category) { Fabricate(:category_with_definition) }
let(:diff_category) { Fabricate(:category_with_definition, name: "Different Category") }
it "returns topics in the category when we filter to it" do
expect(TopicQuery.new(moderator).list_latest.topics.size).to eq(0)
# Filter by slug
expect(TopicQuery.new(moderator, category: category.slug).list_latest.topics.size).to eq(1)
expect(TopicQuery.new(moderator, category: "#{category.id}-category").list_latest.topics.size).to eq(1)
list = TopicQuery.new(moderator, category: diff_category.slug).list_latest
expect(list.topics.size).to eq(1)
expect(list.preload_key).to eq("topic_list_c/different-category/#{diff_category.id}/l/latest")
# Defaults to no category filter when slug does not exist
expect(TopicQuery.new(moderator, category: 'made up slug').list_latest.topics.size).to eq(2)
end
context 'subcategories' do
let!(:subcategory) { Fabricate(:category_with_definition, parent_category_id: category.id) }
let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id) }
it "works with subcategories" do
expect(TopicQuery.new(moderator, category: category.id).list_latest.topics.size).to eq(1)
expect(TopicQuery.new(moderator, category: subcategory.id).list_latest.topics.size).to eq(1)
expect(TopicQuery.new(moderator, category: category.id, no_subcategories: true).list_latest.topics.size).to eq(1)
end
it "shows a subcategory definition topic in its parent list with the right site setting" do
SiteSetting.show_category_definitions_in_topic_lists = true
expect(TopicQuery.new(moderator, category: category.id).list_latest.topics.size).to eq(2)
end
it "works with subsubcategories" do
SiteSetting.max_category_nesting = 3
Fabricate(:topic, category: category)
Fabricate(:topic, category: subcategory)
Fabricate(:topic, category: subsubcategory)
SiteSetting.max_category_nesting = 2
expect(TopicQuery.new(moderator, category: category.id).list_latest.topics.size).to eq(3)
expect(TopicQuery.new(moderator, category: subcategory.id).list_latest.topics.size).to eq(3)
expect(TopicQuery.new(moderator, category: subsubcategory.id).list_latest.topics.size).to eq(2)
SiteSetting.max_category_nesting = 3
expect(TopicQuery.new(moderator, category: category.id).list_latest.topics.size).to eq(4)
expect(TopicQuery.new(moderator, category: subcategory.id).list_latest.topics.size).to eq(3)
expect(TopicQuery.new(moderator, category: subsubcategory.id).list_latest.topics.size).to eq(2)
end
end
end
context 'tag filter' do
fab!(:tag) { Fabricate(:tag) }
fab!(:other_tag) { Fabricate(:tag) }
fab!(:uppercase_tag) { Fabricate(:tag, name: "HeLlO") }
before do
SiteSetting.tagging_enabled = true
end
context "no category filter" do
fab!(:tagged_topic1) { Fabricate(:topic, tags: [tag]) }
fab!(:tagged_topic2) { Fabricate(:topic, tags: [other_tag]) }
fab!(:tagged_topic3) { Fabricate(:topic, tags: [tag, other_tag]) }
fab!(:tagged_topic4) { Fabricate(:topic, tags: [uppercase_tag]) }
fab!(:no_tags_topic) { Fabricate(:topic) }
let(:synonym) { Fabricate(:tag, target_tag: tag, name: 'synonym') }
it "returns topics with the tag when filtered to it" do
expect(TopicQuery.new(moderator, tags: tag.name).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic3)
expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic3)
expect(TopicQuery.new(
moderator, tags: [tag.name, other_tag.name]
).list_latest.topics).to contain_exactly(
tagged_topic1, tagged_topic2, tagged_topic3
)
expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic2, tagged_topic3)
expect(TopicQuery.new(moderator, tags: ["hElLo"]).list_latest.topics)
.to contain_exactly(tagged_topic4)
end
it "can return topics with all specified tags" do
expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name], match_all_tags: true).list_latest.topics.map(&:id)).to eq([tagged_topic3.id])
end
it "returns an empty relation when an invalid tag is passed" do
expect(TopicQuery.new(moderator, tags: [tag.name, 'notatag'], match_all_tags: true).list_latest.topics).to be_empty
end
it "can return topics with no tags" do
expect(TopicQuery.new(moderator, no_tags: true).list_latest.topics.map(&:id)).to eq([no_tags_topic.id])
end
it "can filter using a synonym" do
expect(TopicQuery.new(moderator, tags: synonym.name).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic3)
expect(TopicQuery.new(moderator, tags: [synonym.id]).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic3)
expect(TopicQuery.new(
moderator, tags: [synonym.name, other_tag.name]
).list_latest.topics).to contain_exactly(
tagged_topic1, tagged_topic2, tagged_topic3
)
expect(TopicQuery.new(moderator, tags: [synonym.id, other_tag.id]).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic2, tagged_topic3)
expect(TopicQuery.new(moderator, tags: ["SYnonYM"]).list_latest.topics)
.to contain_exactly(tagged_topic1, tagged_topic3)
end
end
context 'remove_muted_tags' do
fab!(:topic) { Fabricate(:topic, tags: [tag]) }
before do
SiteSetting.remove_muted_tags_from_latest = 'always'
SiteSetting.default_tags_muted = tag.name
end
it 'removes default muted tag topics for anonymous users' do
expect(TopicQuery.new(nil).list_latest.topics.map(&:id)).not_to include(topic.id)
end
end
context "and categories too" do
let(:category1) { Fabricate(:category_with_definition) }
let(:category2) { Fabricate(:category_with_definition) }
it "returns topics in the given category with the given tag" do
tagged_topic1 = Fabricate(:topic, category: category1, tags: [tag])
_tagged_topic2 = Fabricate(:topic, category: category2, tags: [tag])
tagged_topic3 = Fabricate(:topic, category: category1, tags: [tag, other_tag])
_no_tags_topic = Fabricate(:topic, category: category1)
expect(TopicQuery.new(moderator, category: category1.id, tags: [tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort)
expect(TopicQuery.new(moderator, category: category2.id, tags: [other_tag.name]).list_latest.topics.size).to eq(0)
end
end
end
context 'muted categories' do
it 'is removed from top, new and latest lists' do
category = Fabricate(:category_with_definition)
topic = Fabricate(:topic, category: category)
CategoryUser.create!(user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:muted])
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).not_to include(topic.id)
TopTopic.create!(topic: topic, all_score: 1)
expect(topic_query.list_top_for(:all).topics.map(&:id)).not_to include(topic.id)
end
end
context "#list_top_for" do
it "lists top for the week" do
Fabricate(:topic, like_count: 1000, posts_count: 100)
TopTopic.refresh!
expect(topic_query.list_top_for(:weekly).topics.count).to eq(1)
end
it "only allows periods defined by TopTopic.periods" do
expect { topic_query.list_top_for(:all) }.not_to raise_error
expect { topic_query.list_top_for(:yearly) }.not_to raise_error
expect { topic_query.list_top_for(:quarterly) }.not_to raise_error
expect { topic_query.list_top_for(:monthly) }.not_to raise_error
expect { topic_query.list_top_for(:weekly) }.not_to raise_error
expect { topic_query.list_top_for(:daily) }.not_to raise_error
expect { topic_query.list_top_for("some bad input") }.to raise_error(Discourse::InvalidParameters)
end
end
context 'mute_all_categories_by_default' do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:topic) { Fabricate(:topic, category: category) }
before do
SiteSetting.mute_all_categories_by_default = true
end
it 'should remove all topics from new and latest lists by default' do
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).not_to include(topic.id)
end
it 'should include tracked category topics in new and latest lists' do
topic = Fabricate(:topic, category: category)
CategoryUser.create!(user_id: user.id,
category_id: category.id,
notification_level: CategoryUser.notification_levels[:tracking])
expect(topic_query.list_new.topics.map(&:id)).to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
it 'should include default watched category topics in latest list for anonymous users' do
SiteSetting.default_categories_watching = category.id.to_s
expect(TopicQuery.new.list_latest.topics.map(&:id)).to include(topic.id)
end
it 'should include default regular category topics in latest list for anonymous users' do
SiteSetting.default_categories_regular = category.id.to_s
expect(TopicQuery.new.list_latest.topics.map(&:id)).to include(topic.id)
end
it 'should include topics when filtered by category' do
topic_query = TopicQuery.new(user, category: topic.category_id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
end
context 'already seen topics' do
it 'is removed from new and visible on latest lists' do
category = Fabricate(:category_with_definition)
topic = Fabricate(:topic, category: category)
DismissedTopicUser.create!(user_id: user.id,
topic_id: topic.id,
created_at: Time.zone.now
)
expect(topic_query.list_new.topics.map(&:id)).not_to include(topic.id)
expect(topic_query.list_latest.topics.map(&:id)).to include(topic.id)
end
end
context 'muted tags' do
it 'is removed from new and latest lists' do
SiteSetting.tagging_enabled = true
SiteSetting.remove_muted_tags_from_latest = 'always'
muted_tag, other_tag = Fabricate(:tag), Fabricate(:tag)
muted_topic = Fabricate(:topic, tags: [muted_tag])
tagged_topic = Fabricate(:topic, tags: [other_tag])
muted_tagged_topic = Fabricate(:topic, tags: [muted_tag, other_tag])
untagged_topic = Fabricate(:topic)
TagUser.create!(user_id: user.id,
tag_id: muted_tag.id,
notification_level: CategoryUser.notification_levels[:muted])
topic_ids = topic_query.list_latest.topics.map(&:id)
expect(topic_ids).to contain_exactly(tagged_topic.id, untagged_topic.id)
topic_ids = topic_query.list_new.topics.map(&:id)
expect(topic_ids).to contain_exactly(tagged_topic.id, untagged_topic.id)
SiteSetting.remove_muted_tags_from_latest = 'only_muted'
topic_ids = topic_query.list_latest.topics.map(&:id)
expect(topic_ids).to contain_exactly(tagged_topic.id, muted_tagged_topic.id, untagged_topic.id)
topic_ids = topic_query.list_new.topics.map(&:id)
expect(topic_ids).to contain_exactly(tagged_topic.id, muted_tagged_topic.id, untagged_topic.id)
SiteSetting.remove_muted_tags_from_latest = 'never'
topic_ids = topic_query.list_latest.topics.map(&:id)
expect(topic_ids).to contain_exactly(muted_topic.id, tagged_topic.id, muted_tagged_topic.id, untagged_topic.id)
topic_ids = topic_query.list_new.topics.map(&:id)
expect(topic_ids).to contain_exactly(muted_topic.id, tagged_topic.id, muted_tagged_topic.id, untagged_topic.id)
end
it 'is not removed from the tag page itself' do
muted_tag = Fabricate(:tag)
TagUser.create!(user_id: user.id,
tag_id: muted_tag.id,
notification_level: CategoryUser.notification_levels[:muted])
muted_topic = Fabricate(:topic, tags: [muted_tag])
topic_ids = topic_query.latest_results(tags: [muted_tag.name]).map(&:id)
expect(topic_ids).to contain_exactly(muted_topic.id)
muted_tag.update(name: "mixedCaseName")
topic_ids = topic_query.latest_results(tags: [muted_tag.name.downcase]).map(&:id)
expect(topic_ids).to contain_exactly(muted_topic.id)
end
end
context 'a bunch of topics' do
fab!(:regular_topic) do
Fabricate(:topic, title: 'this is a regular topic',
user: creator,
views: 100,
like_count: 66,
posts_count: 3,
participant_count: 11,
bumped_at: 15.minutes.ago)
end
fab!(:pinned_topic) do
Fabricate(:topic, title: 'this is a pinned topic',
user: creator,
views: 10,
like_count: 100,
posts_count: 5,
participant_count: 12,
pinned_at: 10.minutes.ago,
pinned_globally: true,
bumped_at: 10.minutes.ago)
end
fab!(:archived_topic) do
Fabricate(:topic, title: 'this is an archived topic',
user: creator,
views: 50,
like_count: 30,
posts_count: 4,
archived: true,
participant_count: 1,
bumped_at: 6.minutes.ago)
end
fab!(:invisible_topic) do
Fabricate(:topic, title: 'this is an invisible topic',
user: creator,
views: 1,
like_count: 5,
posts_count: 2,
visible: false,
participant_count: 3,
bumped_at: 5.minutes.ago)
end
fab!(:closed_topic) do
Fabricate(:topic, title: 'this is a closed topic',
user: creator,
views: 2,
like_count: 1,
posts_count: 1,
closed: true,
participant_count: 2,
bumped_at: 1.minute.ago)
end
fab!(:future_topic) do
Fabricate(:topic, title: 'this is a topic in far future',
user: creator,
views: 30,
like_count: 11,
posts_count: 6,
participant_count: 5,
bumped_at: 1000.years.from_now)
end
let(:topics) { topic_query.list_latest.topics }
context 'list_latest' do
it "returns the topics in the correct order" do
expect(topics.map(&:id)).to eq([pinned_topic, future_topic, closed_topic, archived_topic, regular_topic].map(&:id))
# includes the invisible topic if you're a moderator
expect(TopicQuery.new(moderator).list_latest.topics.include?(invisible_topic)).to eq(true)
# includes the invisible topic if you're an admin" do
expect(TopicQuery.new(admin).list_latest.topics.include?(invisible_topic)).to eq(true)
end
context 'sort_order' do
def ids_in_order(order, descending = true)
TopicQuery.new(admin, order: order, ascending: descending ? 'false' : 'true').list_latest.topics.map(&:id)
end
it "returns the topics in correct order" do
# returns the topics in likes order if requested
expect(ids_in_order('posts')).to eq([future_topic, pinned_topic, archived_topic, regular_topic, invisible_topic, closed_topic].map(&:id))
# returns the topics in reverse likes order if requested
expect(ids_in_order('posts', false)).to eq([closed_topic, invisible_topic, regular_topic, archived_topic, pinned_topic, future_topic].map(&:id))
# returns the topics in likes order if requested
expect(ids_in_order('likes')).to eq([pinned_topic, regular_topic, archived_topic, future_topic, invisible_topic, closed_topic].map(&:id))
# returns the topics in reverse likes order if requested
expect(ids_in_order('likes', false)).to eq([closed_topic, invisible_topic, future_topic, archived_topic, regular_topic, pinned_topic].map(&:id))
# returns the topics in views order if requested
expect(ids_in_order('views')).to eq([regular_topic, archived_topic, future_topic, pinned_topic, closed_topic, invisible_topic].map(&:id))
# returns the topics in reverse views order if requested" do
expect(ids_in_order('views', false)).to eq([invisible_topic, closed_topic, pinned_topic, future_topic, archived_topic, regular_topic].map(&:id))
# returns the topics in posters order if requested" do
expect(ids_in_order('posters')).to eq([pinned_topic, regular_topic, future_topic, invisible_topic, closed_topic, archived_topic].map(&:id))
# returns the topics in reverse posters order if requested" do
expect(ids_in_order('posters', false)).to eq([archived_topic, closed_topic, invisible_topic, future_topic, regular_topic, pinned_topic].map(&:id))
# sets a custom field for each topic to emulate a plugin
regular_topic.custom_fields["sheep"] = 26
pinned_topic.custom_fields["sheep"] = 47
archived_topic.custom_fields["sheep"] = 69
invisible_topic.custom_fields["sheep"] = 12
closed_topic.custom_fields["sheep"] = 31
future_topic.custom_fields["sheep"] = 53
regular_topic.save
pinned_topic.save
archived_topic.save
invisible_topic.save
closed_topic.save
future_topic.save
# adds the custom field as a viable sort option
class ::TopicQuery
SORTABLE_MAPPING["sheep"] = "custom_fields.sheep"
end
# returns the topics in the sheep order if requested" do
expect(ids_in_order('sheep')).to eq([archived_topic, future_topic, pinned_topic, closed_topic, regular_topic, invisible_topic].map(&:id))
# returns the topics in reverse sheep order if requested" do
expect(ids_in_order('sheep', false)).to eq([invisible_topic, regular_topic, closed_topic, pinned_topic, future_topic, archived_topic].map(&:id))
end
end
end
context 'after clearing a pinned topic' do
before do
pinned_topic.clear_pin_for(user)
end
it "no longer shows the pinned topic at the top" do
expect(topics).to eq([future_topic, closed_topic, archived_topic, pinned_topic, regular_topic])
end
end
end
context 'categorized' do
fab!(:category) { Fabricate(:category_with_definition) }
let(:topic_category) { category.topic }
fab!(:topic_no_cat) { Fabricate(:topic) }
fab!(:topic_in_cat1) { Fabricate(:topic, category: category,
bumped_at: 10.minutes.ago,
created_at: 10.minutes.ago) }
fab!(:topic_in_cat2) { Fabricate(:topic, category: category) }
describe '#list_new_in_category' do
it 'returns the topic category and the categorized topic' do
expect(
topic_query.list_new_in_category(category).topics.map(&:id)
).to eq([topic_in_cat2.id, topic_category.id, topic_in_cat1.id])
end
end
describe "category default sort order" do
it "can use category's default sort order" do
category.update!(sort_order: 'created', sort_ascending: true)
topic_ids = TopicQuery.new(user, category: category.id).list_latest.topics.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat1.id, topic_in_cat2.id])
end
it "ignores invalid order value" do
category.update!(sort_order: 'funny')
topic_ids = TopicQuery.new(user, category: category.id).list_latest.topics.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat2.id, topic_in_cat1.id])
end
it "can be overridden" do
category.update!(sort_order: 'created', sort_ascending: true)
topic_ids = TopicQuery.new(user, category: category.id, order: 'activity').list_latest.topics.map(&:id)
expect(topic_ids - [topic_category.id]).to eq([topic_in_cat2.id, topic_in_cat1.id])
end
end
end
context 'unread / read topics' do
context 'with no data' do
it "has no unread topics" do
expect(topic_query.list_unread.topics).to be_blank
end
end
context 'with whispers' do
it 'correctly shows up in unread for staff' do
first = create_post(raw: 'this is the first post', title: 'super amazing title')
_whisper = create_post(topic_id: first.topic.id,
post_type: Post.types[:whisper],
raw: 'this is a whispered reply')
topic_id = first.topic.id
TopicUser.update_last_read(user, topic_id, first.post_number, 1, 1)
TopicUser.update_last_read(admin, topic_id, first.post_number, 1, 1)
TopicUser.change(user.id, topic_id, notification_level: TopicUser.notification_levels[:tracking])
TopicUser.change(admin.id, topic_id, notification_level: TopicUser.notification_levels[:tracking])
expect(TopicQuery.new(user).list_unread.topics).to eq([])
expect(TopicQuery.new(admin).list_unread.topics).to eq([first.topic])
end
end
context 'with read data' do
fab!(:partially_read) { Fabricate(:post, user: creator).topic }
fab!(:fully_read) { Fabricate(:post, user: creator).topic }
before do
TopicUser.update_last_read(user, partially_read.id, 0, 0, 0)
TopicUser.update_last_read(user, fully_read.id, 1, 1, 0)
end
context 'list_unread' do
it 'lists topics correctly' do
_new_topic = Fabricate(:post, user: creator).topic
expect(topic_query.list_unread.topics).to eq([])
expect(topic_query.list_read.topics).to match_array([fully_read, partially_read])
end
end
context 'user with auto_track_topics list_unread' do
before do
user.user_option.auto_track_topics_after_msecs = 0
user.user_option.save
end
it 'only contains the partially read topic' do
expect(topic_query.list_unread.topics).to eq([partially_read])
end
end
end
end
context '#list_new' do
context 'without a new topic' do
it "has no new topics" do
expect(topic_query.list_new.topics).to be_blank
end
end
context 'preload api' do
it "preloads data correctly" do
TopicList.preloaded_custom_fields << "tag"
TopicList.preloaded_custom_fields << "age"
TopicList.preloaded_custom_fields << "foo"
topic = Fabricate.build(:topic, user: creator, bumped_at: 10.minutes.ago)
topic.custom_fields["tag"] = ["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(["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
expect { new_topic.custom_fields["boom"] }.to raise_error(StandardError)
end
end
context 'with a new topic' do
let!(:new_topic) { Fabricate(:topic, user: creator, bumped_at: 10.minutes.ago) }
let(:topics) { topic_query.list_new.topics }
it "contains no new topics for a user that has missed the window" do
expect(topic_query.list_new.topics).to eq([new_topic])
user.user_option.new_topic_duration_minutes = 5
user.user_option.save
new_topic.created_at = 10.minutes.ago
new_topic.save
expect(topic_query.list_new.topics).to eq([])
end
context "muted topics" do
before do
new_topic.notify_muted!(user)
end
it "returns an empty set" do
expect(topics).to be_blank
expect(topic_query.list_latest.topics).to be_blank
end
context 'un-muted' do
before do
new_topic.notify_tracking!(user)
end
it "returns the topic again" do
expect(topics).to eq([new_topic])
expect(topic_query.list_latest.topics).not_to be_blank
end
end
end
end
end
context '#list_posted' do
let(:topics) { topic_query.list_posted.topics }
it "returns blank when there are no posted topics" do
expect(topics).to be_blank
end
context 'created topics' do
let!(:created_topic) { create_post(user: user).topic }
it "includes the created topic" do
expect(topics.include?(created_topic)).to eq(true)
end
end
context "topic you've posted in" do
let(:other_users_topic) { create_post(user: creator).topic }
let!(:your_post) { create_post(user: user, topic: other_users_topic) }
it "includes the posted topic" do
expect(topics.include?(other_users_topic)).to eq(true)
end
end
context "topic you haven't posted in" do
let(:other_users_topic) { create_post(user: creator).topic }
it "does not include the topic" do
expect(topics).to be_blank
end
context "but interacted with" do
it "is not included if read" do
TopicUser.update_last_read(user, other_users_topic.id, 0, 0, 0)
expect(topics).to be_blank
end
it "is not included if muted" do
other_users_topic.notify_muted!(user)
expect(topics).to be_blank
end
it "is not included if tracking" do
other_users_topic.notify_tracking!(user)
expect(topics).to be_blank
end
end
end
end
context '#list_unseen' do
it "returns an empty list when there aren't topics" do
expect(topic_query.list_unseen.topics).to be_blank
end
it "doesn't return topics that were bumped last time before user joined the forum" do
user.first_seen_at = 10.minutes.ago
create_topic_with_three_posts(bumped_at: 15.minutes.ago)
expect(topic_query.list_unseen.topics).to be_blank
end
it "returns only topics that contain unseen posts" do
user.first_seen_at = 10.minutes.ago
topic_with_unseen_posts = create_topic_with_three_posts(bumped_at: 5.minutes.ago)
read_to_post(topic_with_unseen_posts, user, 1)
fully_read_topic = create_topic_with_three_posts(bumped_at: 5.minutes.ago)
read_to_the_end(fully_read_topic, user)
expect(topic_query.list_unseen.topics).to eq([topic_with_unseen_posts])
end
it "ignores staff posts if user is not staff" do
user.first_seen_at = 10.minutes.ago
topic = create_topic_with_three_posts(bumped_at: 5.minutes.ago)
read_to_the_end(topic, user)
create_post(topic: topic, post_type: Post.types[:whisper])
expect(topic_query.list_unseen.topics).to be_blank
end
def create_topic_with_three_posts(bumped_at:)
topic = Fabricate(:topic, bumped_at: bumped_at)
Fabricate(:post, topic: topic)
Fabricate(:post, topic: topic)
Fabricate(:post, topic: topic)
topic.highest_staff_post_number = 3
topic.highest_post_number = 3
topic
end
def read_to_post(topic, user, post_number)
TopicUser.update_last_read(user, topic.id, post_number, 0, 0)
end
def read_to_the_end(topic, user)
read_to_post topic, user, topic.highest_post_number
end
end
context '#list_related_for' do
let(:user) do
Fabricate(:admin)
end
let(:sender) do
Fabricate(:admin)
end
let(:group_with_user) do
group = Fabricate(:group)
group.add(user)
group.save
group
end
def create_pm(user, opts = nil)
unless opts
opts = user
user = nil
end
create_post(opts.merge(user: user, archetype: Archetype.private_message)).topic
end
def read(user, topic, post_number)
TopicUser.update_last_read(user, topic, post_number, post_number, 10000)
end
it 'returns the correct suggestions' do
pm_to_group = create_pm(sender, target_group_names: [group_with_user.name])
pm_to_user = create_pm(sender, target_usernames: [user.username])
old_unrelated_pm = create_pm(target_usernames: [user.username])
read(user, old_unrelated_pm, 1)
related_by_user_pm = create_pm(sender, target_usernames: [user.username])
read(user, related_by_user_pm, 1)
related_by_group_pm = create_pm(sender, target_group_names: [group_with_user.name])
read(user, related_by_group_pm, 1)
expect(TopicQuery.new(user).list_related_for(pm_to_group).topics.map(&:id)).to(
eq([related_by_group_pm.id])
)
expect(TopicQuery.new(user).list_related_for(pm_to_user).topics.map(&:id)).to(
eq([related_by_user_pm.id])
)
SiteSetting.enable_personal_messages = false
expect(TopicQuery.new(user).list_related_for(pm_to_group)).to be_blank
expect(TopicQuery.new(user).list_related_for(pm_to_user)).to be_blank
end
end
context 'suggested_for' do
def clear_cache!
Discourse.redis.keys('random_topic_cache*').each { |k| Discourse.redis.del k }
end
before do
clear_cache!
end
context 'when anonymous' do
let(:topic) { Fabricate(:topic) }
let!(:new_topic) { Fabricate(:post, user: creator).topic }
it "should return the new topic" do
expect(TopicQuery.new.list_suggested_for(topic).topics).to eq([new_topic])
end
end
context "anonymously browsing with invisible, closed and archived" do
let!(:topic) { Fabricate(:topic) }
let!(:regular_topic) { Fabricate(:post, user: creator).topic }
let!(:closed_topic) { Fabricate(:topic, user: creator, closed: true) }
let!(:archived_topic) { Fabricate(:topic, user: creator, archived: true) }
let!(:invisible_topic) { Fabricate(:topic, user: creator, visible: false) }
it "should omit the closed/archived/invisible topics from suggested" do
expect(TopicQuery.new.list_suggested_for(topic).topics).to eq([regular_topic])
end
end
context 'when logged in' do
def suggested_for(topic)
topic_query.list_suggested_for(topic).topics.map { |t| t.id }
end
let(:topic) { Fabricate(:topic) }
let(:suggested_topics) {
tt = topic
# lets clear cache once category is created - working around caching is hard
clear_cache!
suggested_for(tt)
}
it "should return empty results when there is nothing to find" do
expect(suggested_topics).to be_blank
end
context 'random suggested' do
let!(:new_topic) { Fabricate(:topic, created_at: 2.days.ago) }
let!(:old_topic) { Fabricate(:topic, created_at: 3.years.ago) }
it 'respects suggested_topics_max_days_old' do
SiteSetting.suggested_topics_max_days_old = 1365
tt = topic
clear_cache!
expect(topic_query.list_suggested_for(tt).topics.length).to eq(2)
SiteSetting.suggested_topics_max_days_old = 365
clear_cache!
expect(topic_query.list_suggested_for(tt).topics.length).to eq(1)
end
it 'removes muted topics' do
SiteSetting.suggested_topics_max_days_old = 1365
tt = topic
TopicNotifier.new(old_topic).mute!(user)
clear_cache!
topics = topic_query.list_suggested_for(tt).topics
expect(topics.length).to eq(1)
expect(topics).not_to include(old_topic)
end
end
context 'with private messages' do
let(:group_user) { Fabricate(:user) }
let(:group) { Fabricate(:group) }
let(:another_group) { Fabricate(:group) }
let!(:topic) do
Fabricate(:private_message_topic,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: user)
],
topic_allowed_groups: [
Fabricate.build(:topic_allowed_group, group: group)
]
)
end
let!(:private_message) do
Fabricate(:private_message_topic,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: user)
],
topic_allowed_groups: [
Fabricate.build(:topic_allowed_group, group: group),
Fabricate.build(:topic_allowed_group, group: another_group),
]
)
end
let!(:private_group_topic) do
Fabricate(:private_message_topic,
user: Fabricate(:user),
topic_allowed_groups: [
Fabricate.build(:topic_allowed_group, group: group)
]
)
end
before do
group.add(group_user)
another_group.add(user)
end
describe 'as user not part of group' do
let!(:user) { Fabricate(:user) }
it 'should not return topics by the group user' do
expect(suggested_topics).to eq([private_message.id])
end
end
describe 'as user part of group' do
let!(:user) { group_user }
it 'should return the group topics' do
expect(suggested_topics).to match_array([private_group_topic.id, private_message.id])
end
end
context "by tag filter" do
let(:tag) { Fabricate(:tag) }
let!(:user) { group_user }
it 'should return only tagged topics' do
Fabricate(:topic_tag, topic: private_message, tag: tag)
Fabricate(:topic_tag, topic: private_group_topic)
expect(TopicQuery.new(user, tags: [tag.name]).list_private_messages_tag(user).topics).to eq([private_message])
end
end
end
context 'with some existing topics' do
let!(:old_partially_read) {
topic = Fabricate(:post, user: creator).topic
Fabricate(:post, user: creator, topic: topic)
topic
}
let!(:partially_read) {
topic = Fabricate(:post, user: creator).topic
Fabricate(:post, user: creator, topic: topic)
topic
}
let!(:new_topic) { Fabricate(:post, user: creator).topic }
let!(:fully_read) { Fabricate(:post, user: creator).topic }
let!(:closed_topic) { Fabricate(:topic, user: creator, closed: true) }
let!(:archived_topic) { Fabricate(:topic, user: creator, archived: true) }
let!(:invisible_topic) { Fabricate(:topic, user: creator, visible: false) }
let!(:fully_read_closed) { Fabricate(:post, user: creator).topic }
let!(:fully_read_archived) { Fabricate(:post, user: creator).topic }
before do
user.user_option.update!(
auto_track_topics_after_msecs: 0,
new_topic_duration_minutes: User::NewTopicDuration::ALWAYS
)
freeze_time 3.weeks.from_now
TopicUser.update_last_read(user, old_partially_read.id, 1, 1, 0)
TopicUser.update_last_read(user, partially_read.id, 1, 1, 0)
TopicUser.update_last_read(user, fully_read.id, 1, 1, 0)
TopicUser.update_last_read(user, fully_read_closed.id, 1, 1, 0)
TopicUser.update_last_read(user, fully_read_archived.id, 1, 1, 0)
fully_read_closed.closed = true
fully_read_closed.save
fully_read_archived.archived = true
fully_read_archived.save
old_partially_read.update!(updated_at: 2.weeks.ago)
partially_read.update!(updated_at: Time.now)
end
it "operates correctly" do
# Note, this is a pretty slow integration test
# it tests that suggested is returned in the expected order
# hence we run suggested_for twice here to save on all the setup
SiteSetting.suggested_topics = 4
SiteSetting.suggested_topics_unread_max_days_old = 7
expect(suggested_topics[0]).to eq(partially_read.id)
expect(suggested_topics[1, 3]).to contain_exactly(new_topic.id, closed_topic.id, archived_topic.id)
expect(suggested_topics.length).to eq(4)
SiteSetting.suggested_topics = 2
SiteSetting.suggested_topics_unread_max_days_old = 15
expect(suggested_for(topic)).to contain_exactly(partially_read.id, old_partially_read.id)
end
end
end
end
describe '#list_group_topics' do
fab!(:group) { Fabricate(:group) }
let(:user) do
user = Fabricate(:user)
group.add(user)
user
end
let(:user2) do
user = Fabricate(:user)
group.add(user)
user
end
fab!(:user3) { Fabricate(:user) }
fab!(:private_category) do
Fabricate(:private_category_with_definition, group: group)
end
let!(:private_message_topic) { Fabricate(:private_message_post, user: user).topic }
let!(:topic1) { Fabricate(:topic, user: user) }
let!(:topic2) { Fabricate(:topic, user: user, category: Fabricate(:category_with_definition)) }
let!(:topic3) { Fabricate(:topic, user: user, category: private_category) }
let!(:topic4) { Fabricate(:topic) }
let!(:topic5) { Fabricate(:topic, user: user, visible: false) }
let!(:topic6) { Fabricate(:topic, user: user2) }
it 'should return the right lists for anon user' do
topics = TopicQuery.new.list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic6)
end
it 'should return the right list for users in the same group' do
topics = TopicQuery.new(user).list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic3, topic6)
topics = TopicQuery.new(user2).list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic3, topic6)
end
it 'should return the right list for user no in the group' do
topics = TopicQuery.new(user3).list_group_topics(group).topics
expect(topics).to contain_exactly(topic1, topic2, topic6)
end
end
context "shared drafts" do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:shared_drafts_category) { Fabricate(:category_with_definition) }
fab!(:topic) { Fabricate(:topic, category: shared_drafts_category) }
fab!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: category) }
fab!(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group) }
before do
shared_drafts_category.set_permissions(group => :full)
shared_drafts_category.save
SiteSetting.shared_drafts_category = shared_drafts_category.id
SiteSetting.shared_drafts_min_trust_level = TrustLevel[3]
end
context "destination_category_id" do
it "doesn't allow regular users to query destination_category_id" do
list = TopicQuery.new(user, destination_category_id: category.id).list_latest
expect(list.topics).not_to include(topic)
end
it "allows staff users to query destination_category_id" do
list = TopicQuery.new(admin, destination_category_id: category.id).list_latest
expect(list.topics).to include(topic)
end
it 'allow group members with enough trust level to query destination_category_id' do
member = Fabricate(:user, trust_level: TrustLevel[3])
group.add(member)
list = TopicQuery.new(member, destination_category_id: category.id).list_latest
expect(list.topics).to include(topic)
end
it "doesn't allow group members without enough trust level to query destination_category_id" do
member = Fabricate(:user, trust_level: TrustLevel[2])
group.add(member)
list = TopicQuery.new(member, destination_category_id: category.id).list_latest
expect(list.topics).not_to include(topic)
end
end
context "latest" do
it "doesn't include shared topics unless filtering by category" do
list = TopicQuery.new(moderator).list_latest
expect(list.topics).not_to include(topic)
end
it "doesn't include shared draft topics for regular users" do
group.add(user)
SiteSetting.shared_drafts_category = nil
list = TopicQuery.new(user).list_latest
expect(list.topics).to include(topic)
SiteSetting.shared_drafts_category = shared_drafts_category.id
list = TopicQuery.new(user).list_latest
expect(list.topics).not_to include(topic)
end
it "doesn't include shared draft topics for group members with access to shared drafts" do
member = Fabricate(:user, trust_level: TrustLevel[3])
group.add(member)
list = TopicQuery.new(member).list_latest
expect(list.topics).not_to include(topic)
end
end
context "unread" do
let!(:partially_read) do
topic = Fabricate(:topic, category: shared_drafts_category)
Fabricate(:post, user: creator, topic: topic).topic
TopicUser.update_last_read(admin, topic.id, 0, 0, 0)
TopicUser.change(admin.id, topic.id, notification_level: TopicUser.notification_levels[:tracking])
topic
end
it 'does not remove topics from unread' do
expect(TopicQuery.new(admin).list_latest.topics).not_to include(partially_read) # Check we set up the topic/category correctly
expect(TopicQuery.new(admin).list_unread.topics).to include(partially_read)
end
end
end
end