DEV: Support ordering filters on /filter route ()

This commit adds support for the following ordering filters:

1. `order:activity` which orders the topics by `Topic#bumped_at` in descending order
2. `order:activity-asc` which orders the topics by `Topic#bumped_at` in ascending order
3. `order:latest-post` which orders the topics by `Topic#last_posted_at` in descending order
4. `order:latest-post-asc` which orders the topics by `Topic#last_posted_at` in ascending order
5. `order:created` which orders the topics by `Topic#created_at` in descending order
6. `order:created-asc` which orders the topics by `Topic#created_at` in ascending order
7. `order:views` which orders the topics by `Topic#views` in descending order
8. `order:views-asc` which orders the topics by `Topic#views` in ascending order
9. `order:likes` which orders the topics by `Topic#likes` in descending order
10. `order:likes-asc` which orders the topics by `Topic#likes` in ascending order
11. `order:likes-op` which orders the topics by `Post#like_count` of the first post in the topic in descending order
12. `order:likes-op-asc` which orders the topics by `Post#like_count` of the first post in the topic in ascending order
13. `order:posters` which orders the topics by `Topic#participant_count` in descending order
14. `order:posters-asc` which orders the topics by `Topic#participant_count` in ascending order
15. `order:category` which orders the topics by `Category#name` of the topic's category in descending order
16. `order:category-asc` which orders the topics by `Category#name` of the topic's category in ascending order

Multiple order filters can be composed together and the order of ordering is applied based on the position of the filter
in the query string. For example, `order:views order:created` will order the topics by `Topic#views` in descending order
and then order the topics by `Topics#created_at` in descending order.
This commit is contained in:
Alan Guo Xiang Tan 2023-04-27 14:44:58 +07:00 committed by GitHub
parent 141555136a
commit 6e5e607072
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 203 additions and 4 deletions

@ -62,6 +62,8 @@ class TopicsFilter
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 "order"
order_by(values: filter_values)
when "posts-min"
filter_by_number_of_posts(min: filter_values)
when "posts-max"
@ -171,10 +173,7 @@ class TopicsFilter
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",
),
scope: self.joins_first_posts(@scope),
)
end
@ -426,4 +425,57 @@ 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
ORDER_BY_MAPPINGS = {
"activity" => {
column: "topics.bumped_at",
},
"category" => {
column: "categories.name",
scope: -> { @scope.joins(:category) },
},
"created" => {
column: "topics.created_at",
},
"latest-post" => {
column: "topics.last_posted_at",
},
"likes" => {
column: "topics.like_count",
},
"likes-op" => {
column: "first_posts.like_count",
scope: -> { joins_first_posts(@scope) },
},
"posters" => {
column: "topics.participant_count",
},
"views" => {
column: "topics.views",
},
}
private_constant :ORDER_BY_MAPPINGS
ORDER_BY_REGEXP = /^(?<order_by>#{ORDER_BY_MAPPINGS.keys.join("|")})(?<asc>-asc)?$/
private_constant :ORDER_BY_REGEXP
def order_by(values:)
values.each do |value|
match_data = value.match(ORDER_BY_REGEXP)
if match_data && column_name = ORDER_BY_MAPPINGS.dig(match_data[:order_by], :column)
if scope = ORDER_BY_MAPPINGS.dig(match_data[:order_by], :scope)
@scope = instance_exec(&scope)
end
@scope = @scope.order(column_name => match_data[:asc] ? :asc : :desc)
end
end
end
def joins_first_posts(scope)
scope.joins(
"INNER JOIN posts AS first_posts ON first_posts.topic_id = topics.id AND first_posts.post_number = 1",
)
end
end

@ -1075,5 +1075,152 @@ RSpec.describe TopicsFilter do
:last_posted_at,
"last posted date"
end
describe "ordering topics filter" do
# Requires the fabrication of `topic`, `topic2` and `topic3` such that the order of the topics is `topic2`, `topic1`, `topic3`
# when ordered by the given filter in descending order.
shared_examples "ordering topics filters" do |order, order_description|
describe "when query string is `order:#{order}`" do
it "should return topics ordered by #{order_description} in descending order" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("order:#{order}")
.pluck(:id),
).to eq([topic2.id, topic.id, topic3.id])
end
end
describe "when query string is `order:#{order}-asc`" do
it "should return topics ordered by #{order_description} in ascending order" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("order:#{order}-asc")
.pluck(:id),
).to eq([topic3.id, topic.id, topic2.id])
end
end
describe "when query string is `order:#{order}-invalid`" do
it "should return topics ordered by the default order" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("order:#{order}-invalid")
.pluck(:id),
).to eq(Topic.all.order(:id).pluck(:id))
end
end
end
describe "when ordering topics by creation date" do
fab!(:topic) { Fabricate(:topic, created_at: Time.zone.local(2023, 1, 1)) }
fab!(:topic2) { Fabricate(:topic, created_at: Time.zone.local(2024, 1, 1)) }
fab!(:topic3) { Fabricate(:topic, created_at: Time.zone.local(2022, 1, 1)) }
include_examples "ordering topics filters", "created", "creation date"
end
describe "when ordering topics by last activity date" do
fab!(:topic) { Fabricate(:topic, bumped_at: Time.zone.local(2023, 1, 1)) }
fab!(:topic2) { Fabricate(:topic, bumped_at: Time.zone.local(2024, 1, 1)) }
fab!(:topic3) { Fabricate(:topic, bumped_at: Time.zone.local(2022, 1, 1)) }
include_examples "ordering topics filters", "activity", "bumped date"
end
describe "when ordering topics by number of likes in the topic" do
fab!(:topic) { Fabricate(:topic, like_count: 2) }
fab!(:topic2) { Fabricate(:topic, like_count: 3) }
fab!(:topic3) { Fabricate(:topic, like_count: 1) }
include_examples "ordering topics filters", "likes", "number of likes in the topic"
end
describe "when ordering topics by number of participants in the topic" do
fab!(:topic) { Fabricate(:topic, participant_count: 2) }
fab!(:topic2) { Fabricate(:topic, participant_count: 3) }
fab!(:topic3) { Fabricate(:topic, participant_count: 1) }
include_examples "ordering topics filters", "posters", "number of participants in the topic"
end
describe "when ordering topics by number of topics views" do
fab!(:topic) { Fabricate(:topic, views: 2) }
fab!(:topic2) { Fabricate(:topic, views: 3) }
fab!(:topic3) { Fabricate(:topic, views: 1) }
include_examples "ordering topics filters", "views", "number of views"
end
describe "when ordering topics by latest post creation date" do
fab!(:topic) { Fabricate(:topic, last_posted_at: Time.zone.local(2023, 1, 1)) }
fab!(:topic2) { Fabricate(:topic, last_posted_at: Time.zone.local(2024, 1, 1)) }
fab!(:topic3) { Fabricate(:topic, last_posted_at: Time.zone.local(2022, 1, 1)) }
include_examples "ordering topics filters", "latest-post", "latest post creation date"
end
describe "when ordering topics by number of likes in the first post" do
fab!(:topic) do
post = Fabricate(:post, like_count: 2)
post.topic
end
fab!(:topic2) do
post = Fabricate(:post, like_count: 3)
post.topic
end
fab!(:topic3) do
post = Fabricate(:post, like_count: 1)
post.topic
end
include_examples "ordering topics filters", "likes-op", "number of likes in the first post"
end
describe "when ordering by topics's category name" do
fab!(:category) { Fabricate(:category, name: "Category 1") }
fab!(:category2) { Fabricate(:category, name: "Category 2") }
fab!(:category3) { Fabricate(:category, name: "Category 3") }
fab!(:topic) { Fabricate(:topic, category: category2) }
fab!(:topic2) { Fabricate(:topic, category: category3) }
fab!(:topic3) { Fabricate(:topic, category: category) }
include_examples "ordering topics filters", "category", "category name"
describe "when query string is `order:category` and there are multiple topics in a category" do
fab!(:topic4) { Fabricate(:topic, category: category) }
fab!(:topic5) { Fabricate(:topic, category: category2) }
it "should return topics ordered by category name in descending order and then topic id in ascending order" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("order:category")
.pluck(:id),
).to eq([topic2.id, topic.id, topic5.id, topic3.id, topic4.id])
end
end
end
describe "when query string is `order:created order:views`" do
fab!(:topic) { Fabricate(:topic, created_at: Time.zone.local(2023, 1, 1), views: 2) }
fab!(:topic2) { Fabricate(:topic, created_at: Time.zone.local(2024, 1, 1), views: 2) }
fab!(:topic3) { Fabricate(:topic, created_at: Time.zone.local(2024, 1, 1), views: 1) }
it "should return topics ordered by creation date in descending order and then number of views in descending order" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("order:created order:views")
.pluck(:id),
).to eq([topic2.id, topic3.id, topic.id])
end
end
end
end
end