DEV: Update experimental /filter route with tags support (#20874)

The following are the changes being introduced in this commit:

1. Instead of mapping the query language to various query params on the
client side, we've decided that the benefits of having a more robust
query language far outweighs the benefits of having a more human readable query params in the URL.
As such, the `/filter` route will just accept a single `q` query param
and the query string will be parsed on the server side.

1. On the `/filter` route, the tags filtering query language is now
   supported in the input per the example provided below:

   ```
   tags:bug+feature tagged both bug and feature
   tags:bug,feature tagged either bug or feature
   -tags:bug+feature excluding topics tagged bug and feature
   -tags:bug,feature excluding topics tagged bug or feature
   ```

   The `tags` filter can also be specified multiple
times in the query string like so `tags:bug tags:feature` which will
filter topics that contain both the `bug` tag and `feature` tag. More
complex query like `tags:bug+feature -tags:experimental` will also work.
This commit is contained in:
Alan Guo Xiang Tan 2023-03-30 09:00:42 +08:00 committed by GitHub
parent afe3e36363
commit 49e7e639cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 435 additions and 254 deletions

View File

@ -3,31 +3,12 @@ import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default class extends Controller {
@tracked status = "";
@tracked q = "";
queryParams = ["status"];
get queryString() {
let paramStrings = [];
this.queryParams.forEach((key) => {
if (this[key]) {
paramStrings.push(`${key}:${this[key]}`);
}
});
return paramStrings.join(" ");
}
queryParams = ["q"];
@action
updateTopicsListQueryParams(queryString) {
for (const match of queryString.matchAll(/(\w+):([^:\s]+)/g)) {
const key = match[1];
const value = match[2];
if (this.queryParams.includes(key)) {
this.set(key, value);
}
}
this.q = queryString;
}
}

View File

@ -7,6 +7,6 @@ export default class extends Controller {
constructor() {
super(...arguments);
this.queryString = this.discoveryFilter.queryString;
this.queryString = this.discoveryFilter.q;
}
}

View File

@ -1,18 +1,17 @@
import I18n from "I18n";
import DiscourseRoute from "discourse/routes/discourse";
import { isEmpty } from "@ember/utils";
import { action } from "@ember/object";
export default class extends DiscourseRoute {
queryParams = {
status: { replace: true, refreshModel: true },
q: { replace: true, refreshModel: true },
};
model(data) {
return this.store.findFiltered("topicList", {
filter: "filter",
params: this.#filterQueryParams(data),
params: { q: data.q },
});
}
@ -38,16 +37,4 @@ export default class extends DiscourseRoute {
// Figure out a way to remove this.
@action
changeSort() {}
#filterQueryParams(data) {
const params = {};
Object.keys(this.queryParams).forEach((key) => {
if (!isEmpty(data[key])) {
params[key] = data[key];
}
});
return params;
}
}

View File

@ -83,6 +83,7 @@ class ListController < ApplicationController
list.more_topics_url = construct_url_with(:next, list_opts)
list.prev_topics_url = construct_url_with(:prev, list_opts)
if Discourse.anonymous_filters.include?(filter)
@description = SiteSetting.site_description
@rss = filter
@ -91,12 +92,14 @@ class ListController < ApplicationController
# Note the first is the default and we don't add a title
if (filter.to_s != current_homepage) && use_crawler_layout?
filter_title = I18n.t("js.filters.#{filter.to_s}.title", count: 0)
if list_opts[:category] && @category
@title =
I18n.t("js.filters.with_category", filter: filter_title, category: @category.name)
else
@title = I18n.t("js.filters.with_topics", filter: filter_title)
end
@title << " - #{SiteSetting.title}"
elsif @category.blank? && (filter.to_s == current_homepage) &&
SiteSetting.short_site_description.present?
@ -119,7 +122,23 @@ class ListController < ApplicationController
def filter
raise Discourse::NotFound if !SiteSetting.experimental_topics_filter
latest
topic_query_opts = { no_definitions: !SiteSetting.show_category_definitions_in_topic_lists }
%i[page q].each do |key|
if params.key?(key.to_s)
value = params[key]
raise Discourse::InvalidParameters.new(key) if !TopicQuery.validate?(key, value)
topic_query_opts[key] = value
end
end
user = list_target_user
list = TopicQuery.new(user, topic_query_opts).list_filter
list.more_topics_url = construct_url_with(:next, topic_query_opts)
list.prev_topics_url = construct_url_with(:prev, topic_query_opts)
respond_with_list(list)
end
def category_default

View File

@ -269,7 +269,13 @@ class TopicQuery
end
def list_filter
list_latest
create_list(
:filter,
{},
TopicsFilter.new(guardian: @guardian, scope: latest_results).filter_from_query_string(
@options[:q],
),
)
end
def list_read
@ -796,11 +802,10 @@ class TopicQuery
if status = options[:status]
result =
TopicsFilter.new(
scope: result,
guardian: @guardian,
TopicsFilter.new(scope: result, guardian: @guardian).filter_status(
status: options[:status],
category_id: options[:category],
).filter_status(status: options[:status])
)
end
if (filter = (options[:filter] || options[:f])) && @user

View File

@ -1,12 +1,63 @@
# frozen_string_literal: true
class TopicsFilter
def initialize(guardian:, scope: Topic, category_id: nil)
def initialize(guardian:, scope: Topic)
@guardian = guardian
@scope = scope
@category = category_id.present? ? Category.find_by(id: category_id) : nil
end
def filter_from_query_string(query_string)
return @scope if query_string.blank?
query_string.scan(/(?<exclude>-)?(?<key>\w+):(?<value>[^:\s]+)/) do |exclude, key, value|
case key
when "status"
@scope = filter_status(status: value)
when "tags"
value.scan(
/^(?<tags>([a-zA-Z0-9\-]+)(?<delimiter>[,+])?([a-zA-Z0-9\-]+)?(\k<delimiter>[a-zA-Z0-9\-]+)*)$/,
) do |value, delimiter|
match_all =
if delimiter == ","
false
else
true
end
@scope =
filter_tags(tag_names: value.split(delimiter), exclude: exclude, match_all: match_all)
end
end
end
@scope
end
def filter_status(status:, category_id: nil)
case status
when "open"
@scope = @scope.where("NOT topics.closed AND NOT topics.archived")
when "closed"
@scope = @scope.where("topics.closed")
when "archived"
@scope = @scope.where("topics.archived")
when "listed"
@scope = @scope.where("topics.visible")
when "unlisted"
@scope = @scope.where("NOT topics.visible")
when "deleted"
category = category_id.present? ? Category.find_by(id: category_id) : nil
if @guardian.can_see_deleted_topics?(category)
@scope = @scope.unscope(where: :deleted_at).where("topics.deleted_at IS NOT NULL")
end
end
@scope
end
private
def filter_tags(tag_names:, match_all: true, exclude: false)
return @scope if !SiteSetting.tagging_enabled?
return @scope if tag_names.blank?
@ -32,34 +83,16 @@ class TopicsFilter
@scope
end
def filter_status(status:)
case status
when "open"
@scope = @scope.where("NOT topics.closed AND NOT topics.archived")
when "closed"
@scope = @scope.where("topics.closed")
when "archived"
@scope = @scope.where("topics.archived")
when "listed"
@scope = @scope.where("topics.visible")
when "unlisted"
@scope = @scope.where("NOT topics.visible")
when "deleted"
if @guardian.can_see_deleted_topics?(@category)
@scope = @scope.unscope(where: :deleted_at).where("topics.deleted_at IS NOT NULL")
end
end
@scope
def topic_tags_alias
@topic_tags_alias ||= 0
"tt#{@topic_tags_alias += 1}"
end
private
def exclude_topics_with_all_tags(tag_ids)
where_clause = []
tag_ids.each_with_index do |tag_id, index|
sql_alias = "tt#{index}"
tag_ids.each do |tag_id|
sql_alias = "tt#{topic_tags_alias}"
@scope =
@scope.joins(
@ -81,10 +114,12 @@ class TopicsFilter
end
def include_topics_with_all_tags(tag_ids)
tag_ids.each_with_index do |tag_id, index|
tag_ids.each do |tag_id|
sql_alias = "tt#{topic_tags_alias}"
@scope =
@scope.joins(
"INNER JOIN topic_tags tt#{index} ON tt#{index}.topic_id = topics.id AND tt#{index}.tag_id = #{tag_id}",
"INNER JOIN topic_tags #{sql_alias} ON #{sql_alias}.topic_id = topics.id AND #{sql_alias}.tag_id = #{tag_id}",
)
end
end

View File

@ -3,190 +3,242 @@
RSpec.describe TopicsFilter do
fab!(:admin) { Fabricate(:admin) }
describe "#filter_status" do
fab!(:topic) { Fabricate(:topic) }
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
fab!(:archived_topic) { Fabricate(:topic, archived: true) }
fab!(:deleted_topic_id) { Fabricate(:topic, deleted_at: Time.zone.now).id }
describe "#filter_from_query_string" do
describe "when filtering with multiple filters" do
fab!(:tag) { Fabricate(:tag, name: "tag1") }
fab!(:tag2) { Fabricate(:tag, name: "tag2") }
fab!(:topic_with_tag) { Fabricate(:topic, tags: [tag]) }
fab!(:closed_topic_with_tag) { Fabricate(:topic, tags: [tag], closed: true) }
fab!(:topic_with_tag2) { Fabricate(:topic, tags: [tag2]) }
fab!(:closed_topic_with_tag2) { Fabricate(:topic, tags: [tag2], closed: true) }
it "should only return topics that have not been closed or archived when status is `open`" do
expect(
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "open").pluck(:id),
).to contain_exactly(topic.id)
it "should return the right topics when query string is `status:closed tags:tag1,tag2`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:closed tags:tag1,tag2")
.pluck(:id),
).to contain_exactly(closed_topic_with_tag.id, closed_topic_with_tag2.id)
end
end
it "should only return topics that have been deleted when status is `deleted` and user can see deleted topics" do
expect(
TopicsFilter.new(guardian: Guardian.new(admin)).filter_status(status: "deleted").pluck(:id),
).to contain_exactly(deleted_topic_id)
describe "when filtering by status" do
fab!(:topic) { Fabricate(:topic) }
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
fab!(:archived_topic) { Fabricate(:topic, archived: true) }
fab!(:deleted_topic_id) { Fabricate(:topic, deleted_at: Time.zone.now).id }
it "should only return topics that have not been closed or archived when query string is `status:open`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:open")
.pluck(:id),
).to contain_exactly(topic.id)
end
it "should only return topics that have been deleted when query string is `status:deleted` and user can see deleted topics" do
expect(
TopicsFilter
.new(guardian: Guardian.new(admin))
.filter_from_query_string("status:deleted")
.pluck(:id),
).to contain_exactly(deleted_topic_id)
end
it "should ignore status filter when query string is `status:deleted` and user cannot see deleted topics" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:deleted")
.pluck(:id),
).to contain_exactly(topic.id, closed_topic.id, archived_topic.id)
end
it "should only return topics that have been archived when query string is `status:archived`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:archived")
.pluck(:id),
).to contain_exactly(archived_topic.id)
end
it "should only return topics that are visible when query string is `status:listed`" do
Topic.update_all(visible: false)
topic.update!(visible: true)
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:listed")
.pluck(:id),
).to contain_exactly(topic.id)
end
it "should only return topics that are not visible when query string is `status:unlisted`" do
Topic.update_all(visible: true)
topic.update!(visible: false)
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:unlisted")
.pluck(:id),
).to contain_exactly(topic.id)
end
end
it "should status filter when status is `deleted` and user cannot see deleted topics" do
expect(
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "deleted").pluck(:id),
).to contain_exactly(topic.id, closed_topic.id, archived_topic.id)
end
describe "when filtering by tags" do
fab!(:tag) { Fabricate(:tag, name: "tag1") }
fab!(:tag2) { Fabricate(:tag, name: "tag2") }
fab!(:tag3) { Fabricate(:tag, name: "tag3") }
it "should only return topics that have been archived when status is `archived`" do
expect(
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "archived").pluck(:id),
).to contain_exactly(archived_topic.id)
end
fab!(:group_only_tag) { Fabricate(:tag, name: "group-only-tag") }
fab!(:group) { Fabricate(:group) }
it "should only return topics that are visible when status is `listed`" do
Topic.update_all(visible: false)
topic.update!(visible: true)
let!(:staff_tag_group) do
Fabricate(
:tag_group,
permissions: {
group.name => TagGroupPermission.permission_types[:full],
},
tag_names: [group_only_tag.name],
)
end
expect(
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "listed").pluck(:id),
).to contain_exactly(topic.id)
end
fab!(:topic_without_tag) { Fabricate(:topic) }
fab!(:topic_with_tag) { Fabricate(:topic, tags: [tag]) }
fab!(:topic_with_tag_and_tag2) { Fabricate(:topic, tags: [tag, tag2]) }
fab!(:topic_with_tag2) { Fabricate(:topic, tags: [tag2]) }
fab!(:topic_with_group_only_tag) { Fabricate(:topic, tags: [group_only_tag]) }
it "should only return topics that are not visible when status is `unlisted`" do
Topic.update_all(visible: true)
topic.update!(visible: false)
it "should not filter any topics by tags when tagging is disabled" do
SiteSetting.tagging_enabled = false
expect(
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "unlisted").pluck(:id),
).to contain_exactly(topic.id)
end
end
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:#{tag.name}+#{tag2.name}")
.pluck(:id),
).to contain_exactly(
topic_without_tag.id,
topic_with_tag.id,
topic_with_tag_and_tag2.id,
topic_with_tag2.id,
topic_with_group_only_tag.id,
)
end
describe "#filter_tags" do
fab!(:tag) { Fabricate(:tag) }
fab!(:tag2) { Fabricate(:tag) }
it "should only return topics that are tagged with all of the specified tags when query string is `tags:tag1+tag2`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:#{tag.name}+#{tag2.name}")
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id)
end
fab!(:group_only_tag) { Fabricate(:tag) }
fab!(:group) { Fabricate(:group) }
it "should only return topics that are tagged with tag1 and tag2 when query string is `tags:tag1 tags:tag2`" do
topic_with_tag_and_tag2_and_tag3 = Fabricate(:topic, tags: [tag, tag2, tag3])
let!(:staff_tag_group) do
Fabricate(
:tag_group,
permissions: {
group.name => TagGroupPermission.permission_types[:full],
},
tag_names: [group_only_tag.name],
)
end
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:#{tag.name} tags:#{tag2.name}")
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag_and_tag2_and_tag3.id)
end
fab!(:topic_without_tag) { Fabricate(:topic) }
fab!(:topic_with_tag) { Fabricate(:topic, tags: [tag]) }
fab!(:topic_with_tag_and_tag2) { Fabricate(:topic, tags: [tag, tag2]) }
fab!(:topic_with_tag2) { Fabricate(:topic, tags: [tag2]) }
fab!(:topic_with_group_only_tag) { Fabricate(:topic, tags: [group_only_tag]) }
it "should only return topics that are tagged with tag1 and tag2 but not tag3 when query string is `tags:tag1 tags:tag2 -tags:tag3`" do
topic_with_tag_and_tag2_and_tag3 = Fabricate(:topic, tags: [tag, tag2, tag3])
it "should not filter any topics by tags when tagging is disabled" do
SiteSetting.tagging_enabled = false
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:#{tag.name} tags:#{tag2.name} -tags:tag3")
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id)
end
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag.name, tag2.name], match_all: true, exclude: false)
.pluck(:id),
).to contain_exactly(
topic_without_tag.id,
topic_with_tag.id,
topic_with_tag_and_tag2.id,
topic_with_tag2.id,
topic_with_group_only_tag.id,
)
end
it "should only return topics that are tagged with any of the specified tags when query string is `tags:tag1,tag2`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:#{tag.name},#{tag2.name}")
.pluck(:id),
).to contain_exactly(topic_with_tag.id, topic_with_tag_and_tag2.id, topic_with_tag2.id)
end
it "should only return topics that are tagged with all of the specified tags when `match_all` is `true`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag.name, tag2.name], match_all: true, exclude: false)
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id)
end
it "should not return any topics when query string is `tags:tag1+tag2+invalid`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:tag1+tag2+invalid")
.pluck(:id),
).to eq([])
end
it "should only return topics that are tagged with any of the specified tags when `match_all` is `false`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag2.name], match_all: false, exclude: false)
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag2.id)
end
it "should still filter topics by specificed tags when query string is `tags:tag1,tag2,invalid`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:tag1,tag2,invalid")
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag.id, topic_with_tag2.id)
end
it "should not return any topics when `match_all` is `true` and one of specified tags is invalid" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: ["invalid", tag.name, tag2.name], match_all: true, exclude: false)
.pluck(:id),
).to eq([])
end
it "should not return any topics when query string is `tags:group-only-tag` because specified tag is hidden to user" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("tags:group-only-tag")
.pluck(:id),
).to eq([])
end
it "should still filter topics by specificed tags when `match_all` is `false` even if one of the tags is invalid" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(
tag_names: ["invalid", tag.name, tag2.name],
match_all: false,
exclude: false,
)
.pluck(:id),
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag.id, topic_with_tag2.id)
end
it "should return the right topics when query string is `tags:group-only-tag` and user has access to specified tag" do
group.add(admin)
it "should not return any topics when user tries to filter topics by tags that are hidden" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [group_only_tag.name], match_all: true, exclude: false)
.pluck(:id),
).to eq([])
end
expect(
TopicsFilter
.new(guardian: Guardian.new(admin))
.filter_from_query_string("tags:group-only-tag")
.pluck(:id),
).to contain_exactly(topic_with_group_only_tag.id)
end
it "should allow user with permission to filter topics by tags that are hidden" do
group.add(admin)
it "should only return topics that are not tagged with specified tag when query string is `-tags:tag1`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("-tags:tag1")
.pluck(:id),
).to contain_exactly(topic_without_tag.id, topic_with_tag2.id, topic_with_group_only_tag.id)
end
expect(
TopicsFilter
.new(guardian: Guardian.new(admin))
.filter_tags(tag_names: [group_only_tag.name])
.pluck(:id),
).to contain_exactly(topic_with_group_only_tag.id)
end
it "should only return topics that are not tagged with all of the specified tags when query string is `-tags:tag1+tag2`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("-tags:tag1+tag2")
.pluck(:id),
).to contain_exactly(
topic_without_tag.id,
topic_with_tag.id,
topic_with_tag2.id,
topic_with_group_only_tag.id,
)
end
it "should only return topics that are not tagged with all of the specified tags when `match_all` is `true` and `exclude` is `true`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag.name], match_all: true, exclude: true)
.pluck(:id),
).to contain_exactly(topic_without_tag.id, topic_with_tag2.id, topic_with_group_only_tag.id)
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag.name, tag2.name], match_all: true, exclude: true)
.pluck(:id),
).to contain_exactly(
topic_without_tag.id,
topic_with_tag.id,
topic_with_tag2.id,
topic_with_group_only_tag.id,
)
end
it "should only return topics that are not tagged with any of the specified tags when `match_all` is `false` and `exclude` is `true`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag.name], match_all: false, exclude: true)
.pluck(:id),
).to contain_exactly(topic_without_tag.id, topic_with_group_only_tag.id, topic_with_tag2.id)
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_tags(tag_names: [tag.name, tag2.name], match_all: false, exclude: true)
.pluck(:id),
).to contain_exactly(topic_without_tag.id, topic_with_group_only_tag.id)
it "should only return topics that are not tagged with any of the specified tags when query string is `-tags:tag1,tag2`" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("-tags:tag1,tag2")
.pluck(:id),
).to contain_exactly(topic_without_tag.id, topic_with_group_only_tag.id)
end
end
end
end

View File

@ -1086,9 +1086,14 @@ RSpec.describe ListController do
end
describe "#filter" do
it "should respond with 403 response code for an anonymous user" do
SiteSetting.experimental_topics_filter = true
fab!(:category) { Fabricate(:category) }
fab!(:tag) { Fabricate(:tag, name: "tag1") }
fab!(:topic_with_tag) { Fabricate(:topic, tags: [tag]) }
fab!(:topic2_with_tag) { Fabricate(:topic, tags: [tag]) }
before { SiteSetting.experimental_topics_filter = true }
it "should respond with 403 response code for an anonymous user" do
get "/filter.json"
expect(response.status).to eq(403)
@ -1096,11 +1101,83 @@ RSpec.describe ListController do
it "should respond with 404 response code when `experimental_topics_filter` site setting has not been enabled" do
SiteSetting.experimental_topics_filter = false
sign_in(user)
get "/filter.json"
expect(response.status).to eq(404)
end
it "returns category definition topics if `show_category_definitions_in_topic_lists` site setting is enabled" do
category_topic = Fabricate(:topic, category: category)
category.update!(topic: category_topic)
SiteSetting.show_category_definitions_in_topic_lists = true
sign_in(user)
get "/filter.json"
expect(response.status).to eq(200)
parsed = response.parsed_body
expect(parsed["topic_list"]["topics"].length).to eq(4)
expect(parsed["topic_list"]["topics"].map { |topic| topic["id"] }).to contain_exactly(
topic.id,
topic_with_tag.id,
topic2_with_tag.id,
category_topic.id,
)
end
it "does not return category definition topics if `show_category_definitions_in_topic_lists` site setting is disabled" do
category_topic = Fabricate(:topic, category: category)
category.update!(topic: category_topic)
SiteSetting.show_category_definitions_in_topic_lists = false
sign_in(user)
get "/filter.json"
expect(response.status).to eq(200)
parsed = response.parsed_body
expect(parsed["topic_list"]["topics"].length).to eq(3)
expect(parsed["topic_list"]["topics"].map { |topic| topic["id"] }).to contain_exactly(
topic.id,
topic_with_tag.id,
topic2_with_tag.id,
)
end
it "should accept the `page` query parameter" do
stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 1) do
sign_in(user)
get "/filter.json?q=tags:tag1"
expect(response.status).to eq(200)
parsed = response.parsed_body
expect(parsed["topic_list"]["topics"].length).to eq(1)
expect(parsed["topic_list"]["topics"].first["id"]).to eq(topic2_with_tag.id)
get "/filter.json?q=tags:tag1&page=1"
expect(response.status).to eq(200)
parsed = response.parsed_body
expect(parsed["topic_list"]["topics"].length).to eq(1)
expect(parsed["topic_list"]["topics"].first["id"]).to eq(topic_with_tag.id)
end
end
end
end

View File

@ -2,40 +2,65 @@
describe "Filtering topics", type: :system, js: true do
fab!(:user) { Fabricate(:user) }
fab!(:topic) { Fabricate(:topic) }
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
let(:topic_list) { PageObjects::Components::TopicList.new }
let(:topic_query_filter) { PageObjects::Components::TopicQueryFilter.new }
before { SiteSetting.experimental_topics_filter = true }
it "should allow users to input a custom query string to filter through topics" do
sign_in(user)
describe "when filtering by status" do
fab!(:topic) { Fabricate(:topic) }
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
visit("/filter")
it "should display the right topics when the status filter is used in the query string" do
sign_in(user)
expect(topic_list).to have_topic(topic)
expect(topic_list).to have_topic(closed_topic)
visit("/filter")
topic_query_filter = PageObjects::Components::TopicQueryFilter.new
topic_query_filter.fill_in("status:open")
expect(topic_list).to have_topic(topic)
expect(topic_list).to have_topic(closed_topic)
expect(topic_list).to have_topic(topic)
expect(topic_list).to have_no_topic(closed_topic)
expect(page).to have_current_path("/filter?status=open")
topic_query_filter.fill_in("status:open")
topic_query_filter.fill_in("status:closed")
expect(topic_list).to have_topic(topic)
expect(topic_list).to have_no_topic(closed_topic)
expect(topic_list).to have_no_topic(topic)
expect(topic_list).to have_topic(closed_topic)
expect(page).to have_current_path("/filter?status=closed")
topic_query_filter.fill_in("status:closed")
expect(topic_list).to have_no_topic(topic)
expect(topic_list).to have_topic(closed_topic)
end
end
it "should filter topics when 'status' query params is present" do
sign_in(user)
describe "when filtering by tags" do
fab!(:tag) { Fabricate(:tag, name: "tag1") }
fab!(:tag2) { Fabricate(:tag, name: "tag2") }
fab!(:topic_with_tag) { Fabricate(:topic, tags: [tag]) }
fab!(:topic_with_tag2) { Fabricate(:topic, tags: [tag2]) }
fab!(:topic_with_tag_and_tag2) { Fabricate(:topic, tags: [tag, tag2]) }
visit("/filter?status=open")
it "should display the right topics when tags filter is used in the query string" do
sign_in(user)
expect(topic_list).to have_topic(topic)
expect(topic_list).to have_no_topic(closed_topic)
visit("/filter")
expect(topic_list).to have_topics(count: 3)
expect(topic_list).to have_topic(topic_with_tag)
expect(topic_list).to have_topic(topic_with_tag2)
expect(topic_list).to have_topic(topic_with_tag_and_tag2)
topic_query_filter.fill_in("tags:tag1")
expect(topic_list).to have_topics(count: 2)
expect(topic_list).to have_topic(topic_with_tag)
expect(topic_list).to have_topic(topic_with_tag_and_tag2)
expect(topic_list).to have_no_topic(topic_with_tag2)
topic_query_filter.fill_in("tags:tag1+tag2")
expect(topic_list).to have_topics(count: 1)
expect(topic_list).to have_no_topic(topic_with_tag)
expect(topic_list).to have_no_topic(topic_with_tag2)
expect(topic_list).to have_topic(topic_with_tag_and_tag2)
end
end
end