mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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:
parent
afe3e36363
commit
49e7e639cc
@ -3,31 +3,12 @@ import { action } from "@ember/object";
|
|||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
@tracked status = "";
|
@tracked q = "";
|
||||||
|
|
||||||
queryParams = ["status"];
|
queryParams = ["q"];
|
||||||
|
|
||||||
get queryString() {
|
|
||||||
let paramStrings = [];
|
|
||||||
|
|
||||||
this.queryParams.forEach((key) => {
|
|
||||||
if (this[key]) {
|
|
||||||
paramStrings.push(`${key}:${this[key]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return paramStrings.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateTopicsListQueryParams(queryString) {
|
updateTopicsListQueryParams(queryString) {
|
||||||
for (const match of queryString.matchAll(/(\w+):([^:\s]+)/g)) {
|
this.q = queryString;
|
||||||
const key = match[1];
|
|
||||||
const value = match[2];
|
|
||||||
|
|
||||||
if (this.queryParams.includes(key)) {
|
|
||||||
this.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,6 @@ export default class extends Controller {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.queryString = this.discoveryFilter.queryString;
|
this.queryString = this.discoveryFilter.q;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import { isEmpty } from "@ember/utils";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
export default class extends DiscourseRoute {
|
export default class extends DiscourseRoute {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
status: { replace: true, refreshModel: true },
|
q: { replace: true, refreshModel: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
model(data) {
|
model(data) {
|
||||||
return this.store.findFiltered("topicList", {
|
return this.store.findFiltered("topicList", {
|
||||||
filter: "filter",
|
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.
|
// Figure out a way to remove this.
|
||||||
@action
|
@action
|
||||||
changeSort() {}
|
changeSort() {}
|
||||||
|
|
||||||
#filterQueryParams(data) {
|
|
||||||
const params = {};
|
|
||||||
|
|
||||||
Object.keys(this.queryParams).forEach((key) => {
|
|
||||||
if (!isEmpty(data[key])) {
|
|
||||||
params[key] = data[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,7 @@ class ListController < ApplicationController
|
|||||||
|
|
||||||
list.more_topics_url = construct_url_with(:next, list_opts)
|
list.more_topics_url = construct_url_with(:next, list_opts)
|
||||||
list.prev_topics_url = construct_url_with(:prev, list_opts)
|
list.prev_topics_url = construct_url_with(:prev, list_opts)
|
||||||
|
|
||||||
if Discourse.anonymous_filters.include?(filter)
|
if Discourse.anonymous_filters.include?(filter)
|
||||||
@description = SiteSetting.site_description
|
@description = SiteSetting.site_description
|
||||||
@rss = filter
|
@rss = filter
|
||||||
@ -91,12 +92,14 @@ class ListController < ApplicationController
|
|||||||
# Note the first is the default and we don't add a title
|
# Note the first is the default and we don't add a title
|
||||||
if (filter.to_s != current_homepage) && use_crawler_layout?
|
if (filter.to_s != current_homepage) && use_crawler_layout?
|
||||||
filter_title = I18n.t("js.filters.#{filter.to_s}.title", count: 0)
|
filter_title = I18n.t("js.filters.#{filter.to_s}.title", count: 0)
|
||||||
|
|
||||||
if list_opts[:category] && @category
|
if list_opts[:category] && @category
|
||||||
@title =
|
@title =
|
||||||
I18n.t("js.filters.with_category", filter: filter_title, category: @category.name)
|
I18n.t("js.filters.with_category", filter: filter_title, category: @category.name)
|
||||||
else
|
else
|
||||||
@title = I18n.t("js.filters.with_topics", filter: filter_title)
|
@title = I18n.t("js.filters.with_topics", filter: filter_title)
|
||||||
end
|
end
|
||||||
|
|
||||||
@title << " - #{SiteSetting.title}"
|
@title << " - #{SiteSetting.title}"
|
||||||
elsif @category.blank? && (filter.to_s == current_homepage) &&
|
elsif @category.blank? && (filter.to_s == current_homepage) &&
|
||||||
SiteSetting.short_site_description.present?
|
SiteSetting.short_site_description.present?
|
||||||
@ -119,7 +122,23 @@ class ListController < ApplicationController
|
|||||||
|
|
||||||
def filter
|
def filter
|
||||||
raise Discourse::NotFound if !SiteSetting.experimental_topics_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
|
end
|
||||||
|
|
||||||
def category_default
|
def category_default
|
||||||
|
@ -269,7 +269,13 @@ class TopicQuery
|
|||||||
end
|
end
|
||||||
|
|
||||||
def list_filter
|
def list_filter
|
||||||
list_latest
|
create_list(
|
||||||
|
:filter,
|
||||||
|
{},
|
||||||
|
TopicsFilter.new(guardian: @guardian, scope: latest_results).filter_from_query_string(
|
||||||
|
@options[:q],
|
||||||
|
),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_read
|
def list_read
|
||||||
@ -796,11 +802,10 @@ class TopicQuery
|
|||||||
|
|
||||||
if status = options[:status]
|
if status = options[:status]
|
||||||
result =
|
result =
|
||||||
TopicsFilter.new(
|
TopicsFilter.new(scope: result, guardian: @guardian).filter_status(
|
||||||
scope: result,
|
status: options[:status],
|
||||||
guardian: @guardian,
|
|
||||||
category_id: options[:category],
|
category_id: options[:category],
|
||||||
).filter_status(status: options[:status])
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
if (filter = (options[:filter] || options[:f])) && @user
|
if (filter = (options[:filter] || options[:f])) && @user
|
||||||
|
@ -1,12 +1,63 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TopicsFilter
|
class TopicsFilter
|
||||||
def initialize(guardian:, scope: Topic, category_id: nil)
|
def initialize(guardian:, scope: Topic)
|
||||||
@guardian = guardian
|
@guardian = guardian
|
||||||
@scope = scope
|
@scope = scope
|
||||||
@category = category_id.present? ? Category.find_by(id: category_id) : nil
|
|
||||||
end
|
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)
|
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?
|
||||||
@ -32,34 +83,16 @@ class TopicsFilter
|
|||||||
@scope
|
@scope
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_status(status:)
|
def topic_tags_alias
|
||||||
case status
|
@topic_tags_alias ||= 0
|
||||||
when "open"
|
"tt#{@topic_tags_alias += 1}"
|
||||||
@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
|
||||||
end
|
|
||||||
|
|
||||||
@scope
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def exclude_topics_with_all_tags(tag_ids)
|
def exclude_topics_with_all_tags(tag_ids)
|
||||||
where_clause = []
|
where_clause = []
|
||||||
|
|
||||||
tag_ids.each_with_index do |tag_id, index|
|
tag_ids.each do |tag_id|
|
||||||
sql_alias = "tt#{index}"
|
sql_alias = "tt#{topic_tags_alias}"
|
||||||
|
|
||||||
@scope =
|
@scope =
|
||||||
@scope.joins(
|
@scope.joins(
|
||||||
@ -81,10 +114,12 @@ class TopicsFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def include_topics_with_all_tags(tag_ids)
|
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 =
|
||||||
@scope.joins(
|
@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
|
||||||
end
|
end
|
||||||
|
@ -3,60 +3,98 @@
|
|||||||
RSpec.describe TopicsFilter do
|
RSpec.describe TopicsFilter do
|
||||||
fab!(:admin) { Fabricate(:admin) }
|
fab!(:admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
describe "#filter_status" do
|
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 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
|
||||||
|
|
||||||
|
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) }
|
||||||
fab!(:archived_topic) { Fabricate(:topic, archived: true) }
|
fab!(:archived_topic) { Fabricate(:topic, archived: true) }
|
||||||
fab!(:deleted_topic_id) { Fabricate(:topic, deleted_at: Time.zone.now).id }
|
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 status is `open`" do
|
it "should only return topics that have not been closed or archived when query string is `status:open`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "open").pluck(:id),
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("status:open")
|
||||||
|
.pluck(:id),
|
||||||
).to contain_exactly(topic.id)
|
).to contain_exactly(topic.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only return topics that have been deleted when status is `deleted` and user can see deleted topics" do
|
it "should only return topics that have been deleted when query string is `status:deleted` and user can see deleted topics" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter.new(guardian: Guardian.new(admin)).filter_status(status: "deleted").pluck(:id),
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new(admin))
|
||||||
|
.filter_from_query_string("status:deleted")
|
||||||
|
.pluck(:id),
|
||||||
).to contain_exactly(deleted_topic_id)
|
).to contain_exactly(deleted_topic_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should status filter when status is `deleted` and user cannot see deleted topics" do
|
it "should ignore status filter when query string is `status:deleted` and user cannot see deleted topics" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "deleted").pluck(:id),
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("status:deleted")
|
||||||
|
.pluck(:id),
|
||||||
).to contain_exactly(topic.id, closed_topic.id, archived_topic.id)
|
).to contain_exactly(topic.id, closed_topic.id, archived_topic.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only return topics that have been archived when status is `archived`" do
|
it "should only return topics that have been archived when query string is `status:archived`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "archived").pluck(:id),
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("status:archived")
|
||||||
|
.pluck(:id),
|
||||||
).to contain_exactly(archived_topic.id)
|
).to contain_exactly(archived_topic.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only return topics that are visible when status is `listed`" do
|
it "should only return topics that are visible when query string is `status:listed`" do
|
||||||
Topic.update_all(visible: false)
|
Topic.update_all(visible: false)
|
||||||
topic.update!(visible: true)
|
topic.update!(visible: true)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "listed").pluck(:id),
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("status:listed")
|
||||||
|
.pluck(:id),
|
||||||
).to contain_exactly(topic.id)
|
).to contain_exactly(topic.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only return topics that are not visible when status is `unlisted`" do
|
it "should only return topics that are not visible when query string is `status:unlisted`" do
|
||||||
Topic.update_all(visible: true)
|
Topic.update_all(visible: true)
|
||||||
topic.update!(visible: false)
|
topic.update!(visible: false)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter.new(guardian: Guardian.new).filter_status(status: "unlisted").pluck(:id),
|
TopicsFilter
|
||||||
|
.new(guardian: Guardian.new)
|
||||||
|
.filter_from_query_string("status:unlisted")
|
||||||
|
.pluck(:id),
|
||||||
).to contain_exactly(topic.id)
|
).to contain_exactly(topic.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#filter_tags" do
|
describe "when filtering by tags" do
|
||||||
fab!(:tag) { Fabricate(:tag) }
|
fab!(:tag) { Fabricate(:tag, name: "tag1") }
|
||||||
fab!(:tag2) { Fabricate(:tag) }
|
fab!(:tag2) { Fabricate(:tag, name: "tag2") }
|
||||||
|
fab!(:tag3) { Fabricate(:tag, name: "tag3") }
|
||||||
|
|
||||||
fab!(:group_only_tag) { Fabricate(:tag) }
|
fab!(:group_only_tag) { Fabricate(:tag, name: "group-only-tag") }
|
||||||
fab!(:group) { Fabricate(:group) }
|
fab!(:group) { Fabricate(:group) }
|
||||||
|
|
||||||
let!(:staff_tag_group) do
|
let!(:staff_tag_group) do
|
||||||
@ -81,7 +119,7 @@ RSpec.describe TopicsFilter do
|
|||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [tag.name, tag2.name], match_all: true, exclude: false)
|
.filter_from_query_string("tags:#{tag.name}+#{tag2.name}")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(
|
).to contain_exactly(
|
||||||
topic_without_tag.id,
|
topic_without_tag.id,
|
||||||
@ -92,78 +130,98 @@ RSpec.describe TopicsFilter do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only return topics that are tagged with all of the specified tags when `match_all` is `true`" do
|
it "should only return topics that are tagged with all of the specified tags when query string is `tags:tag1+tag2`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [tag.name, tag2.name], match_all: true, exclude: false)
|
.filter_from_query_string("tags:#{tag.name}+#{tag2.name}")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(topic_with_tag_and_tag2.id)
|
).to contain_exactly(topic_with_tag_and_tag2.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only return topics that are tagged with any of the specified tags when `match_all` is `false`" do
|
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])
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [tag2.name], match_all: false, exclude: false)
|
.filter_from_query_string("tags:#{tag.name} tags:#{tag2.name}")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag2.id)
|
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag_and_tag2_and_tag3.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not return any topics when `match_all` is `true` and one of specified tags is invalid" do
|
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])
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: ["invalid", tag.name, tag2.name], match_all: true, exclude: false)
|
.filter_from_query_string("tags:#{tag.name} tags:#{tag2.name} -tags:tag3")
|
||||||
|
.pluck(:id),
|
||||||
|
).to contain_exactly(topic_with_tag_and_tag2.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 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),
|
.pluck(:id),
|
||||||
).to eq([])
|
).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should still filter topics by specificed tags when `match_all` is `false` even if one of the tags is invalid" do
|
it "should still filter topics by specificed tags when query string is `tags:tag1,tag2,invalid`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(
|
.filter_from_query_string("tags:tag1,tag2,invalid")
|
||||||
tag_names: ["invalid", tag.name, tag2.name],
|
|
||||||
match_all: false,
|
|
||||||
exclude: false,
|
|
||||||
)
|
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag.id, topic_with_tag2.id)
|
).to contain_exactly(topic_with_tag_and_tag2.id, topic_with_tag.id, topic_with_tag2.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not return any topics when user tries to filter topics by tags that are hidden" do
|
it "should not return any topics when query string is `tags:group-only-tag` because specified tag is hidden to user" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [group_only_tag.name], match_all: true, exclude: false)
|
.filter_from_query_string("tags:group-only-tag")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to eq([])
|
).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should allow user with permission to filter topics by tags that are hidden" do
|
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)
|
group.add(admin)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new(admin))
|
.new(guardian: Guardian.new(admin))
|
||||||
.filter_tags(tag_names: [group_only_tag.name])
|
.filter_from_query_string("tags:group-only-tag")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(topic_with_group_only_tag.id)
|
).to contain_exactly(topic_with_group_only_tag.id)
|
||||||
end
|
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
|
it "should only return topics that are not tagged with specified tag when query string is `-tags:tag1`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [tag.name], match_all: true, exclude: true)
|
.filter_from_query_string("-tags:tag1")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(topic_without_tag.id, topic_with_tag2.id, topic_with_group_only_tag.id)
|
).to contain_exactly(topic_without_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 query string is `-tags:tag1+tag2`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [tag.name, tag2.name], match_all: true, exclude: true)
|
.filter_from_query_string("-tags:tag1+tag2")
|
||||||
.pluck(:id),
|
.pluck(:id),
|
||||||
).to contain_exactly(
|
).to contain_exactly(
|
||||||
topic_without_tag.id,
|
topic_without_tag.id,
|
||||||
@ -173,20 +231,14 @@ RSpec.describe TopicsFilter do
|
|||||||
)
|
)
|
||||||
end
|
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
|
it "should only return topics that are not tagged with any of the specified tags when query string is `-tags:tag1,tag2`" do
|
||||||
expect(
|
expect(
|
||||||
TopicsFilter
|
TopicsFilter
|
||||||
.new(guardian: Guardian.new)
|
.new(guardian: Guardian.new)
|
||||||
.filter_tags(tag_names: [tag.name], match_all: false, exclude: true)
|
.filter_from_query_string("-tags:tag1,tag2")
|
||||||
.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),
|
.pluck(:id),
|
||||||
).to contain_exactly(topic_without_tag.id, topic_with_group_only_tag.id)
|
).to contain_exactly(topic_without_tag.id, topic_with_group_only_tag.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
@ -1086,9 +1086,14 @@ RSpec.describe ListController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "#filter" do
|
describe "#filter" do
|
||||||
it "should respond with 403 response code for an anonymous user" do
|
fab!(:category) { Fabricate(:category) }
|
||||||
SiteSetting.experimental_topics_filter = true
|
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"
|
get "/filter.json"
|
||||||
|
|
||||||
expect(response.status).to eq(403)
|
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
|
it "should respond with 404 response code when `experimental_topics_filter` site setting has not been enabled" do
|
||||||
SiteSetting.experimental_topics_filter = false
|
SiteSetting.experimental_topics_filter = false
|
||||||
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
||||||
get "/filter.json"
|
get "/filter.json"
|
||||||
|
|
||||||
expect(response.status).to eq(404)
|
expect(response.status).to eq(404)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
describe "Filtering topics", type: :system, js: true do
|
describe "Filtering topics", type: :system, js: true do
|
||||||
fab!(:user) { Fabricate(:user) }
|
fab!(:user) { Fabricate(:user) }
|
||||||
fab!(:topic) { Fabricate(:topic) }
|
|
||||||
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
|
|
||||||
let(:topic_list) { PageObjects::Components::TopicList.new }
|
let(:topic_list) { PageObjects::Components::TopicList.new }
|
||||||
|
let(:topic_query_filter) { PageObjects::Components::TopicQueryFilter.new }
|
||||||
|
|
||||||
before { SiteSetting.experimental_topics_filter = true }
|
before { SiteSetting.experimental_topics_filter = true }
|
||||||
|
|
||||||
it "should allow users to input a custom query string to filter through topics" do
|
describe "when filtering by status" do
|
||||||
|
fab!(:topic) { Fabricate(:topic) }
|
||||||
|
fab!(:closed_topic) { Fabricate(:topic, closed: true) }
|
||||||
|
|
||||||
|
it "should display the right topics when the status filter is used in the query string" do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
||||||
visit("/filter")
|
visit("/filter")
|
||||||
@ -16,26 +19,48 @@ describe "Filtering topics", type: :system, js: true do
|
|||||||
expect(topic_list).to have_topic(topic)
|
expect(topic_list).to have_topic(topic)
|
||||||
expect(topic_list).to have_topic(closed_topic)
|
expect(topic_list).to have_topic(closed_topic)
|
||||||
|
|
||||||
topic_query_filter = PageObjects::Components::TopicQueryFilter.new
|
|
||||||
topic_query_filter.fill_in("status:open")
|
topic_query_filter.fill_in("status:open")
|
||||||
|
|
||||||
expect(topic_list).to have_topic(topic)
|
expect(topic_list).to have_topic(topic)
|
||||||
expect(topic_list).to have_no_topic(closed_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:closed")
|
topic_query_filter.fill_in("status:closed")
|
||||||
|
|
||||||
expect(topic_list).to have_no_topic(topic)
|
expect(topic_list).to have_no_topic(topic)
|
||||||
expect(topic_list).to have_topic(closed_topic)
|
expect(topic_list).to have_topic(closed_topic)
|
||||||
expect(page).to have_current_path("/filter?status=closed")
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should filter topics when 'status' query params is present" do
|
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]) }
|
||||||
|
|
||||||
|
it "should display the right topics when tags filter is used in the query string" do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
||||||
visit("/filter?status=open")
|
visit("/filter")
|
||||||
|
|
||||||
expect(topic_list).to have_topic(topic)
|
expect(topic_list).to have_topics(count: 3)
|
||||||
expect(topic_list).to have_no_topic(closed_topic)
|
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
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user