mirror of
https://github.com/discourse/discourse.git
synced 2025-01-18 10:42:45 +08:00
DEV: Add support for more filters for /filter
route (#21097)
* DEV: Support `likes-(min:max):<count>` on `/filter` route This commit adds support for the following filters: 1. `likes-min` 2. `likes-max` 3. `views-min` 4. `views-max` 5. `likes-op-min` 6. `likes-op-max` If the filter has an invalid value, i.e string that cannot be converted into an integer, the filter will be ignored. If either of each filter is specify multiple times, only the last occurrence of each filter will be taken into consideration.
This commit is contained in:
parent
4b9061ae3f
commit
1f0207ba06
|
@ -33,33 +33,39 @@ class TopicsFilter
|
|||
key_prefixes = hash["key_prefixes"]
|
||||
values = hash["values"]
|
||||
|
||||
filter_values = extract_and_validate_value_for(filter, values)
|
||||
|
||||
case filter
|
||||
when "category"
|
||||
filter_categories(values: key_prefixes.zip(values))
|
||||
filter_categories(values: key_prefixes.zip(filter_values))
|
||||
when "created-by"
|
||||
filter_created_by_user(usernames: values.flat_map { |value| value.split(",") })
|
||||
filter_created_by_user(usernames: filter_values.flat_map { |value| value.split(",") })
|
||||
when "in"
|
||||
filter_in(values: values)
|
||||
filter_in(values: filter_values)
|
||||
when "likes-min"
|
||||
filter_by_number_of_likes(min: filter_values)
|
||||
when "likes-max"
|
||||
filter_by_number_of_likes(max: filter_values)
|
||||
when "likes-op-min"
|
||||
filter_by_number_of_likes_in_first_post(min: filter_values)
|
||||
when "likes-op-max"
|
||||
filter_by_number_of_likes_in_first_post(max: filter_values)
|
||||
when "posts-min"
|
||||
min = values.last
|
||||
break if !integer_string?(min)
|
||||
filter_by_number_of_posts(min: min)
|
||||
filter_by_number_of_posts(min: filter_values)
|
||||
when "posts-max"
|
||||
max = values.last
|
||||
break if !integer_string?(max)
|
||||
filter_by_number_of_posts(max: max)
|
||||
filter_by_number_of_posts(max: filter_values)
|
||||
when "posters-min"
|
||||
min = values.last
|
||||
break if !integer_string?(min)
|
||||
filter_by_number_of_posters(min: min)
|
||||
filter_by_number_of_posters(min: filter_values)
|
||||
when "posters-max"
|
||||
max = values.last
|
||||
break if !integer_string?(max)
|
||||
filter_by_number_of_posters(max: max)
|
||||
filter_by_number_of_posters(max: filter_values)
|
||||
when "status"
|
||||
values.each { |status| @scope = filter_status(status: status) }
|
||||
filter_values.each { |status| @scope = filter_status(status: status) }
|
||||
when "tags"
|
||||
filter_tags(values: key_prefixes.zip(values))
|
||||
filter_tags(values: key_prefixes.zip(filter_values))
|
||||
when "views-min"
|
||||
filter_by_number_of_views(min: filter_values)
|
||||
when "views-max"
|
||||
filter_by_number_of_views(max: filter_values)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -93,19 +99,50 @@ class TopicsFilter
|
|||
|
||||
private
|
||||
|
||||
def filter_by_topic_range(column_name:, min: nil, max: nil)
|
||||
def extract_and_validate_value_for(filter, values)
|
||||
case filter
|
||||
when "likes-min", "likes-max", "likes-op-min", "likes-op-max", "posts-min", "posts-max",
|
||||
"posters-min", "posters-max", "views-min", "views-max"
|
||||
value = values.last
|
||||
value if value =~ /\A\d+\z/
|
||||
else
|
||||
values
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_topic_range(column_name:, min: nil, max: nil, scope: nil)
|
||||
{ min => ">=", max => "<=" }.each do |value, operator|
|
||||
next if !value
|
||||
@scope = @scope.where("topics.#{column_name} #{operator} ?", value)
|
||||
@scope = (scope || @scope).where("#{column_name} #{operator} ?", value)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_number_of_posts(min: nil, max: nil)
|
||||
filter_by_topic_range(column_name: "posts_count", min:, max:)
|
||||
filter_by_topic_range(column_name: "topics.posts_count", min:, max:)
|
||||
end
|
||||
|
||||
def filter_by_number_of_posters(min: nil, max: nil)
|
||||
filter_by_topic_range(column_name: "participant_count", min:, max:)
|
||||
filter_by_topic_range(column_name: "topics.participant_count", min:, max:)
|
||||
end
|
||||
|
||||
def filter_by_number_of_likes(min: nil, max: nil)
|
||||
filter_by_topic_range(column_name: "topics.like_count", min:, max:)
|
||||
end
|
||||
|
||||
def filter_by_number_of_likes_in_first_post(min: nil, max: nil)
|
||||
filter_by_topic_range(
|
||||
column_name: "first_posts.like_count",
|
||||
min:,
|
||||
max:,
|
||||
scope:
|
||||
@scope.joins(
|
||||
"INNER JOIN posts AS first_posts ON first_posts.topic_id = topics.id AND first_posts.post_number = 1",
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def filter_by_number_of_views(min: nil, max: nil)
|
||||
filter_by_topic_range(column_name: "views", min:, max:)
|
||||
end
|
||||
|
||||
def filter_categories(values:)
|
||||
|
@ -352,8 +389,4 @@ class TopicsFilter
|
|||
def include_topics_with_any_tags(tag_ids)
|
||||
@scope = @scope.joins(:topic_tags).where("topic_tags.tag_id IN (?)", tag_ids).distinct(:id)
|
||||
end
|
||||
|
||||
def integer_string?(string)
|
||||
string =~ /\A\d+\z/
|
||||
end
|
||||
end
|
||||
|
|
|
@ -847,182 +847,136 @@ RSpec.describe TopicsFilter do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when filtering by number of posts in a topic" do
|
||||
fab!(:topic_with_1_post) { Fabricate(:topic, posts_count: 1) }
|
||||
fab!(:topic_with_2_posts) { Fabricate(:topic, posts_count: 2) }
|
||||
fab!(:topic_with_3_posts) { Fabricate(:topic, posts_count: 3) }
|
||||
|
||||
describe "when query string is `posts-min:1`" do
|
||||
it "should only return topics with at least 1 post" do
|
||||
shared_examples "filtering for topics by range" do |filter|
|
||||
describe "when query string is `#{filter}-min:1`" do
|
||||
it "should only return topics with at least 1 #{filter}" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-min:1")
|
||||
.filter_from_query_string("#{filter}-min:1")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_post.id, topic_with_2_posts.id, topic_with_3_posts.id)
|
||||
).to contain_exactly(topic_with_1_count.id, topic_with_2_count.id, topic_with_3_count.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posts-min:3`" do
|
||||
it "should only return topics with at least 3 posts" do
|
||||
describe "when query string is `#{filter}-min:3`" do
|
||||
it "should only return topics with at least 3 #{filter}" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-min:3")
|
||||
.filter_from_query_string("#{filter}-min:3")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_3_posts.id)
|
||||
).to contain_exactly(topic_with_3_count.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posts-max:1`" do
|
||||
it "should only return topics with at most 1 post" do
|
||||
describe "when query string is `#{filter}-max:1`" do
|
||||
it "should only return topics with at most 1 #{filter}" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-max:1")
|
||||
.filter_from_query_string("#{filter}-max:1")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_post.id)
|
||||
).to contain_exactly(topic_with_1_count.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posts-max:3`" do
|
||||
it "should only return topics with at most 3 posts" do
|
||||
describe "when query string is `#{filter}-max:3`" do
|
||||
it "should only return topics with at most 3 #{filter}" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-max:3")
|
||||
.filter_from_query_string("#{filter}-max:3")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_post.id, topic_with_2_posts.id, topic_with_3_posts.id)
|
||||
).to contain_exactly(topic_with_1_count.id, topic_with_2_count.id, topic_with_3_count.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posts-min:1 posts-max:2`" do
|
||||
it "should only return topics with at least a post and at most 2 posts" do
|
||||
describe "when query string is `#{filter}-min:1 #{filter}-max:2`" do
|
||||
it "should only return topics with at least 1 like and at most 2 #{filter}" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-min:1 posts-max:2")
|
||||
.filter_from_query_string("#{filter}-min:1 #{filter}-max:2")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_post.id, topic_with_2_posts.id)
|
||||
).to contain_exactly(topic_with_1_count.id, topic_with_2_count.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posts-min:3 posts-min:2 posts-max:1 posts-max:3`" do
|
||||
it "should only return topics with at least 2 posts and at most 3 posts as it ignores earlier filters which are duplicated" do
|
||||
describe "when query string is `#{filter}-min:3 #{filter}-min:2 #{filter}-max:1 #{filter}-max:3`" do
|
||||
it "should only return topics with at least 2 #{filter} and at most 3 #{filter} as it ignores earlier filters which are duplicated" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-min:3 posts-min:2 posts-max:1 posts-max:3")
|
||||
.filter_from_query_string(
|
||||
"#{filter}-min:3 #{filter}-min:2 #{filter}-max:1 #{filter}-max:3",
|
||||
)
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_2_posts.id, topic_with_3_posts.id)
|
||||
).to contain_exactly(topic_with_2_count.id, topic_with_3_count.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posts-min:invalid posts-max:invalid`" do
|
||||
describe "when query string is `#{filter}-min:invalid #{filter}-max:invalid`" do
|
||||
it "should ignore the filters with invalid values" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posts-min:invalid posts-max:invalid")
|
||||
.filter_from_query_string("#{filter}-min:invalid #{filter}-max:invalid")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_post.id, topic_with_2_posts.id, topic_with_3_posts.id)
|
||||
).to contain_exactly(topic_with_1_count.id, topic_with_2_count.id, topic_with_3_count.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "when filtering by number of likes in a topic" do
|
||||
fab!(:topic_with_1_count) { Fabricate(:topic, like_count: 1) }
|
||||
fab!(:topic_with_2_count) { Fabricate(:topic, like_count: 2) }
|
||||
fab!(:topic_with_3_count) { Fabricate(:topic, like_count: 3) }
|
||||
|
||||
include_examples("filtering for topics by range", "likes")
|
||||
end
|
||||
|
||||
describe "when filtering by number of posters in a topic" do
|
||||
fab!(:topic_with_1_participant) { Fabricate(:topic, participant_count: 1) }
|
||||
fab!(:topic_with_2_participants) { Fabricate(:topic, participant_count: 2) }
|
||||
fab!(:topic_with_3_participants) { Fabricate(:topic, participant_count: 3) }
|
||||
fab!(:topic_with_1_count) { Fabricate(:topic, participant_count: 1) }
|
||||
fab!(:topic_with_2_count) { Fabricate(:topic, participant_count: 2) }
|
||||
fab!(:topic_with_3_count) { Fabricate(:topic, participant_count: 3) }
|
||||
|
||||
describe "when query string is `posters-min:1`" do
|
||||
it "should only return topics with at least 1 participant" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-min:1")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(
|
||||
topic_with_1_participant.id,
|
||||
topic_with_2_participants.id,
|
||||
topic_with_3_participants.id,
|
||||
)
|
||||
end
|
||||
include_examples("filtering for topics by range", "posters")
|
||||
end
|
||||
|
||||
describe "when filtering by number of posts in a topic" do
|
||||
fab!(:topic_with_1_count) { Fabricate(:topic, posts_count: 1) }
|
||||
fab!(:topic_with_2_count) { Fabricate(:topic, posts_count: 2) }
|
||||
fab!(:topic_with_3_count) { Fabricate(:topic, posts_count: 3) }
|
||||
|
||||
include_examples("filtering for topics by range", "posts")
|
||||
end
|
||||
|
||||
describe "when filtering by number of views in a topic" do
|
||||
fab!(:topic_with_1_count) { Fabricate(:topic, views: 1) }
|
||||
fab!(:topic_with_2_count) { Fabricate(:topic, views: 2) }
|
||||
fab!(:topic_with_3_count) { Fabricate(:topic, views: 3) }
|
||||
|
||||
include_examples("filtering for topics by range", "views")
|
||||
end
|
||||
|
||||
describe "when filtering by number of likes in the first post of a topic" do
|
||||
fab!(:topic_with_1_count) do
|
||||
post = Fabricate(:post, like_count: 1)
|
||||
post.topic
|
||||
end
|
||||
|
||||
describe "when query string is `posters-min:3`" do
|
||||
it "should only return topics with at least 3 participants" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-min:3")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_3_participants.id)
|
||||
end
|
||||
fab!(:topic_with_2_count) do
|
||||
post = Fabricate(:post, like_count: 2)
|
||||
post.topic
|
||||
end
|
||||
|
||||
describe "when query string is `posters-max:1`" do
|
||||
it "should only return topics with at most 1 participant" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-max:1")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_participant.id)
|
||||
end
|
||||
fab!(:topic_with_3_count) do
|
||||
post = Fabricate(:post, like_count: 3)
|
||||
post.topic
|
||||
end
|
||||
|
||||
describe "when query string is `posters-max:3`" do
|
||||
it "should only return topics with at most 3 participants" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-max:3")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(
|
||||
topic_with_1_participant.id,
|
||||
topic_with_2_participants.id,
|
||||
topic_with_3_participants.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posters-min:1 posters-max:2`" do
|
||||
it "should only return topics with at least 1 participant and at most 2 participants" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-min:1 posters-max:2")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_1_participant.id, topic_with_2_participants.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posters-min:3 posters-min:2 posters-max:1 posters-max:3`" do
|
||||
it "should only return topics with at least 2 participants and at most 3 participants as it ignores earlier filters which are duplicated" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-min:3 posters-min:2 posters-max:1 posters-max:3")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(topic_with_2_participants.id, topic_with_3_participants.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when query string is `posters-min:invalid posters-max:invalid`" do
|
||||
it "should ignore the filters with invalid values" do
|
||||
expect(
|
||||
TopicsFilter
|
||||
.new(guardian: Guardian.new)
|
||||
.filter_from_query_string("posters-min:invalid posters-max:invalid")
|
||||
.pluck(:id),
|
||||
).to contain_exactly(
|
||||
topic_with_1_participant.id,
|
||||
topic_with_2_participants.id,
|
||||
topic_with_3_participants.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
include_examples("filtering for topics by range", "likes-op")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user