mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Update experimental /filter
route with categories support (#20911)
On the `/filter` route, the categories filtering query language is now supported in the input per the example provided below: ``` category:bug => topics in the bug category AND all subcategories =category:bug => topics in the bug category excluding subcategories category:bug,feature => allow for categories either in bug or feature =category:bug,feature => allow for exact categories match excluding sub cats categories: => alias for category ``` Currently composing multiple category filters is not supported as we have yet to determine what behaviour it should result in. For example, `category:bug category:feature` would now return topics that are in both the `bug` and `feature` category but it is not possible for a topic to belong to two categories.
This commit is contained in:
parent
c86d772277
commit
0162f0ccb0
@ -9,14 +9,18 @@ class TopicsFilter
|
|||||||
def filter_from_query_string(query_string)
|
def filter_from_query_string(query_string)
|
||||||
return @scope if query_string.blank?
|
return @scope if query_string.blank?
|
||||||
|
|
||||||
query_string.scan(/(?<exclude>-)?(?<key>\w+):(?<value>[^:\s]+)/) do |exclude, key, value|
|
query_string.scan(
|
||||||
|
/(?<key_prefix>[-=])?(?<key>\w+):(?<value>[^:\s]+)/,
|
||||||
|
) do |key_prefix, key, value|
|
||||||
case key
|
case key
|
||||||
when "status"
|
when "status"
|
||||||
@scope = filter_status(status: value)
|
@scope = filter_status(status: value)
|
||||||
when "tags"
|
when "tags"
|
||||||
value.scan(
|
value.scan(
|
||||||
/^(?<tags>([a-zA-Z0-9\-]+)(?<delimiter>[,+])?([a-zA-Z0-9\-]+)?(\k<delimiter>[a-zA-Z0-9\-]+)*)$/,
|
/^(?<tag_names>([a-zA-Z0-9\-]+)(?<delimiter>[,+])?([a-zA-Z0-9\-]+)?(\k<delimiter>[a-zA-Z0-9\-]+)*)$/,
|
||||||
) do |value, delimiter|
|
) do |tag_names, delimiter|
|
||||||
|
break if key_prefix && key_prefix != "-"
|
||||||
|
|
||||||
match_all =
|
match_all =
|
||||||
if delimiter == ","
|
if delimiter == ","
|
||||||
false
|
false
|
||||||
@ -25,7 +29,23 @@ class TopicsFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
@scope =
|
@scope =
|
||||||
filter_tags(tag_names: value.split(delimiter), exclude: exclude, match_all: match_all)
|
filter_tags(
|
||||||
|
tag_names: tag_names.split(delimiter),
|
||||||
|
exclude: key_prefix.presence,
|
||||||
|
match_all: match_all,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
when "category", "categories"
|
||||||
|
value.scan(
|
||||||
|
/^(?<category_slugs>([a-zA-Z0-9\-]+)(?<delimiter>[,])?([a-zA-Z0-9\-]+)?(\k<delimiter>[a-zA-Z0-9\-]+)*)$/,
|
||||||
|
) do |category_slugs, delimiter|
|
||||||
|
break if key_prefix && key_prefix != "="
|
||||||
|
|
||||||
|
@scope =
|
||||||
|
filter_categories(
|
||||||
|
category_slugs: category_slugs.split(delimiter),
|
||||||
|
exclude_subcategories: key_prefix.presence,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -60,6 +80,22 @@ class TopicsFilter
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_categories(category_slugs:, exclude_subcategories: false)
|
||||||
|
category_ids =
|
||||||
|
Category
|
||||||
|
.where(slug: category_slugs)
|
||||||
|
.filter { |category| @guardian.can_see_category?(category) }
|
||||||
|
.map(&:id)
|
||||||
|
|
||||||
|
return @scope.none if category_ids.length != category_slugs.length
|
||||||
|
|
||||||
|
if !exclude_subcategories
|
||||||
|
category_ids = category_ids.flat_map { |category_id| Category.subcategory_ids(category_id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
@scope = @scope.joins(:category).where("categories.id IN (?)", category_ids)
|
||||||
|
end
|
||||||
|
|
||||||
def filter_tags(tag_names:, match_all: true, exclude: false)
|
def filter_tags(tag_names:, match_all: true, exclude: false)
|
||||||
return @scope if !SiteSetting.tagging_enabled?
|
return @scope if !SiteSetting.tagging_enabled?
|
||||||
return @scope if tag_names.blank?
|
return @scope if tag_names.blank?
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe TopicsFilter do
|
RSpec.describe TopicsFilter do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
fab!(:admin) { Fabricate(:admin) }
|
fab!(:admin) { Fabricate(:admin) }
|
||||||
fab!(:group) { Fabricate(:group) }
|
fab!(:group) { Fabricate(:group) }
|
||||||
|
|
||||||
@ -23,6 +24,143 @@ RSpec.describe TopicsFilter do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "when filtering by categories" do
|
||||||
|
fab!(:category) { Fabricate(:category, name: "category") }
|
||||||
|
|
||||||
|
fab!(:category_subcategory) do
|
||||||
|
Fabricate(:category, parent_category: category, name: "category subcategory")
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:category2) { Fabricate(:category, name: "category2") }
|
||||||
|
|
||||||
|
fab!(:category2_subcategory) do
|
||||||
|
Fabricate(:category, parent_category: category2, name: "category2 subcategory")
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:private_category) do
|
||||||
|
Fabricate(:private_category, group: group, slug: "private-category")
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:topic_in_category) { Fabricate(:topic, category: category) }
|
||||||
|
fab!(:topic_in_category_subcategory) { Fabricate(:topic, category: category_subcategory) }
|
||||||
|
fab!(:topic_in_category2) { Fabricate(:topic, category: category2) }
|
||||||
|
fab!(:topic_in_category2_subcategory) { Fabricate(:topic, category: category2_subcategory) }
|
||||||
|
fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category) }
|
||||||
|
|
||||||
|
describe "when query string is `-category:category`" do
|
||||||
|
it "ignores the filter because the prefix is invalid" do
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("-category:category")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(
|
||||||
|
topic_in_category.id,
|
||||||
|
topic_in_category_subcategory.id,
|
||||||
|
topic_in_category2.id,
|
||||||
|
topic_in_category2_subcategory.id,
|
||||||
|
topic_in_private_category.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when query string is `category:private-category`" do
|
||||||
|
it "should not return any topics when user does not have access to specified category" do
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("category:private-category")
|
||||||
|
.pluck(:id),
|
||||||
|
).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return topics from specified category when user has access to specified category" do
|
||||||
|
group.add(user)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new(user))
|
||||||
|
.filter_from_query_string("category:private-category")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(topic_in_private_category.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when query string is `category:category`" do
|
||||||
|
it "should return topics from specified category and its subcategories" do
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("category:category")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(topic_in_category.id, topic_in_category_subcategory.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return topics from specified category, its subcategories and sub-subcategories" do
|
||||||
|
SiteSetting.max_category_nesting = 3
|
||||||
|
|
||||||
|
category_subcategory_subcategory =
|
||||||
|
Fabricate(
|
||||||
|
:category,
|
||||||
|
parent_category: category_subcategory,
|
||||||
|
name: "category subcategory subcategory",
|
||||||
|
)
|
||||||
|
|
||||||
|
topic_in_category_subcategory_subcategory =
|
||||||
|
Fabricate(:topic, category: category_subcategory_subcategory)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("category:category")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(
|
||||||
|
topic_in_category.id,
|
||||||
|
topic_in_category_subcategory.id,
|
||||||
|
topic_in_category_subcategory_subcategory.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when query string is `category:category,category2`" do
|
||||||
|
it "should return topics from any of the specified categories and its subcategories" do
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("category:category,category2")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(
|
||||||
|
topic_in_category.id,
|
||||||
|
topic_in_category_subcategory.id,
|
||||||
|
topic_in_category2.id,
|
||||||
|
topic_in_category2_subcategory.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when query string is `=category:category`" do
|
||||||
|
it "should not return topics from subcategories`" do
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("=category:category")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(topic_in_category.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when query string is `=category:category,category2`" do
|
||||||
|
it "should not return topics from subcategories" do
|
||||||
|
expect(
|
||||||
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("=category:category,category2")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(topic_in_category.id, topic_in_category2.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "when filtering by status" do
|
describe "when filtering by status" do
|
||||||
fab!(:topic) { Fabricate(:topic) }
|
fab!(:topic) { Fabricate(:topic) }
|
||||||
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
|
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
|
||||||
|
Loading…
Reference in New Issue
Block a user