FEATURE: Ability to exclude category from search results. (#7194)
This commit also adds `Category#search_priority` which sets the ground work to enable prioritizing of posts for certain categories when searching.
This commit is contained in:
parent
0a8f950281
commit
5e410dc5e0
|
@ -0,0 +1 @@
|
|||
export const searchPriorities = <%= Searchable::PRIORITIES.to_json %>;
|
|
@ -1,6 +1,7 @@
|
|||
import { setting } from "discourse/lib/computed";
|
||||
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { searchPriorities } from "discourse/components/concerns/category_search_priorities";
|
||||
|
||||
const categorySortCriteria = [];
|
||||
export function addCategorySortCriteria(criteria) {
|
||||
|
@ -57,6 +58,20 @@ export default buildCategoryPanel("settings", {
|
|||
);
|
||||
},
|
||||
|
||||
@computed
|
||||
searchPrioritiesOptions() {
|
||||
const options = [];
|
||||
|
||||
for (const [name, value] of Object.entries(searchPriorities)) {
|
||||
options.push({
|
||||
name: I18n.t(`category.search_priority.options.${name}`),
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
return options.sort((a, b) => a.value <= b.value);
|
||||
},
|
||||
|
||||
@computed
|
||||
availableSorts() {
|
||||
return [
|
||||
|
|
|
@ -129,7 +129,8 @@ const Category = RestModel.extend({
|
|||
minimum_required_tags: this.get("minimum_required_tags"),
|
||||
navigate_to_first_post_after_read: this.get(
|
||||
"navigate_to_first_post_after_read"
|
||||
)
|
||||
),
|
||||
search_priority: this.get("search_priority")
|
||||
},
|
||||
type: id ? "PUT" : "POST"
|
||||
});
|
||||
|
|
|
@ -43,6 +43,17 @@
|
|||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<label for="category-search-priority">
|
||||
{{i18n "category.search_priority.label"}}
|
||||
</label>
|
||||
|
||||
{{combo-box valueAttribute="value"
|
||||
id="category-search-priority"
|
||||
content=searchPrioritiesOptions
|
||||
value=category.search_priority}}
|
||||
</section>
|
||||
|
||||
{{#if isParentCategory}}
|
||||
<section class="field show-subcategory-list-field">
|
||||
<label>
|
||||
|
|
|
@ -293,6 +293,7 @@ class CategoriesController < ApplicationController
|
|||
:default_top_period,
|
||||
:minimum_required_tags,
|
||||
:navigate_to_first_post_after_read,
|
||||
:search_priority,
|
||||
custom_fields: [params[:custom_fields].try(:keys)],
|
||||
permissions: [*p.try(:keys)],
|
||||
allowed_tags: [],
|
||||
|
|
|
@ -44,17 +44,18 @@ class Category < ActiveRecord::Base
|
|||
has_and_belongs_to_many :web_hooks
|
||||
|
||||
validates :user_id, presence: true
|
||||
|
||||
validates :name, if: Proc.new { |c| c.new_record? || c.will_save_change_to_name? },
|
||||
presence: true,
|
||||
uniqueness: { scope: :parent_category_id, case_sensitive: false },
|
||||
length: { in: 1..50 }
|
||||
|
||||
validates :num_featured_topics, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :search_priority, inclusion: { in: Searchable::PRIORITIES.values }
|
||||
|
||||
validate :parent_category_validator
|
||||
|
||||
validate :email_in_validator
|
||||
|
||||
validate :ensure_slug
|
||||
|
||||
validate :permissions_compatibility_validator
|
||||
|
||||
validates :auto_close_hours, numericality: { greater_than: 0, less_than_or_equal_to: 87600 }, allow_nil: true
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
module Searchable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
PRIORITIES = Enum.new(
|
||||
normal: 0,
|
||||
ignore: 1
|
||||
)
|
||||
|
||||
included do
|
||||
has_one "#{self.name.underscore}_search_data".to_sym, dependent: :destroy
|
||||
end
|
||||
|
|
|
@ -18,7 +18,8 @@ class CategorySerializer < BasicCategorySerializer
|
|||
:custom_fields,
|
||||
:allowed_tags,
|
||||
:allowed_tag_groups,
|
||||
:topic_featured_link_allowed
|
||||
:topic_featured_link_allowed,
|
||||
:search_priority
|
||||
|
||||
def group_permissions
|
||||
@group_permissions ||= begin
|
||||
|
|
|
@ -2453,6 +2453,11 @@ en:
|
|||
muted:
|
||||
title: "Muted"
|
||||
description: "You will never be notified of anything about new topics in these categories, and they will not appear in latest."
|
||||
search_priority:
|
||||
label: "Search Priority"
|
||||
options:
|
||||
normal: "Normal"
|
||||
ignore: "Ignore"
|
||||
sort_options:
|
||||
default: "default"
|
||||
likes: "Likes"
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class AddSearchPriorityToCategories < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :categories, :search_priority, :integer, default: 0
|
||||
add_index :categories, :search_priority
|
||||
end
|
||||
end
|
|
@ -243,7 +243,7 @@ class Search
|
|||
end
|
||||
end
|
||||
|
||||
find_grouped_results unless @results.posts.present?
|
||||
find_grouped_results if @results.posts.blank?
|
||||
|
||||
if preloaded_topic_custom_fields.present? && @results.posts.present?
|
||||
topics = @results.posts.map(&:topic)
|
||||
|
@ -608,7 +608,6 @@ class Search
|
|||
end
|
||||
|
||||
def find_grouped_results
|
||||
|
||||
if @results.type_filter.present?
|
||||
raise Discourse::InvalidAccess.new("invalid type filter") unless Search.facets.include?(@results.type_filter)
|
||||
send("#{@results.type_filter}_search")
|
||||
|
@ -727,6 +726,7 @@ class Search
|
|||
|
||||
def posts_query(limit, opts = nil)
|
||||
opts ||= {}
|
||||
|
||||
posts = Post.where(post_type: Topic.visible_post_types(@guardian.user))
|
||||
.joins(:post_search_data, :topic)
|
||||
.joins("LEFT JOIN categories ON categories.id = topics.category_id")
|
||||
|
@ -768,6 +768,7 @@ class Search
|
|||
weights = @in_title ? 'A' : (SiteSetting.tagging_enabled ? 'ABCD' : 'ABD')
|
||||
posts = posts.where("post_search_data.search_data @@ #{ts_query(weight_filter: weights)}")
|
||||
exact_terms = @term.scan(/"([^"]+)"/).flatten
|
||||
|
||||
exact_terms.each do |exact|
|
||||
posts = posts.where("posts.raw ilike :exact OR topics.title ilike :exact", exact: "%#{exact}%")
|
||||
end
|
||||
|
@ -783,24 +784,27 @@ class Search
|
|||
end if @filters
|
||||
|
||||
# If we have a search context, prioritize those posts first
|
||||
posts =
|
||||
if @search_context.present?
|
||||
|
||||
if @search_context.is_a?(User)
|
||||
|
||||
if opts[:private_messages]
|
||||
posts = posts.private_posts_for_user(@search_context)
|
||||
posts.private_posts_for_user(@search_context)
|
||||
else
|
||||
posts = posts.where("posts.user_id = #{@search_context.id}")
|
||||
posts.where("posts.user_id = #{@search_context.id}")
|
||||
end
|
||||
|
||||
elsif @search_context.is_a?(Category)
|
||||
category_ids = [@search_context.id] + Category.where(parent_category_id: @search_context.id).pluck(:id)
|
||||
posts = posts.where("topics.category_id in (?)", category_ids)
|
||||
category_ids = Category
|
||||
.where(parent_category_id: @search_context.id)
|
||||
.pluck(:id)
|
||||
.push(@search_context.id)
|
||||
|
||||
posts.where("topics.category_id in (?)", category_ids)
|
||||
elsif @search_context.is_a?(Topic)
|
||||
posts = posts.where("topics.id = #{@search_context.id}")
|
||||
posts.where("topics.id = #{@search_context.id}")
|
||||
.order("posts.post_number #{@order == :latest ? "DESC" : ""}")
|
||||
end
|
||||
|
||||
else
|
||||
categories_ignored(posts)
|
||||
end
|
||||
|
||||
if @order == :latest || (@term.blank? && !@order)
|
||||
|
@ -847,6 +851,14 @@ class Search
|
|||
posts.limit(limit)
|
||||
end
|
||||
|
||||
def categories_ignored(posts)
|
||||
posts.where(<<~SQL, Searchable::PRIORITIES[:ignore])
|
||||
categories.id NOT IN (
|
||||
SELECT categories.id WHERE categories.search_priority = ?
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
||||
def self.default_ts_config
|
||||
"'#{Search.ts_config}'"
|
||||
end
|
||||
|
|
|
@ -413,19 +413,72 @@ describe Search do
|
|||
end
|
||||
|
||||
context 'categories' do
|
||||
let(:category) { Fabricate(:category, name: "monkey Category 2") }
|
||||
let(:topic) { Fabricate(:topic, category: category) }
|
||||
let!(:post) { Fabricate(:post, topic: topic, raw: "snow monkey") }
|
||||
|
||||
let!(:category) { Fabricate(:category) }
|
||||
def search
|
||||
Search.execute(category.name)
|
||||
let!(:ignored_category) do
|
||||
Fabricate(:category,
|
||||
name: "monkey Category 1",
|
||||
search_priority: Searchable::PRIORITIES[:ignore]
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the correct result' do
|
||||
expect(search.categories).to be_present
|
||||
it "should return the right categories" do
|
||||
search = Search.execute("monkey")
|
||||
|
||||
expect(search.categories).to contain_exactly(
|
||||
category, ignored_category
|
||||
)
|
||||
|
||||
expect(search.posts).to contain_exactly(category.topic.first_post, post)
|
||||
end
|
||||
|
||||
describe "with child categories" do
|
||||
let!(:child_of_ignored_category) do
|
||||
Fabricate(:category,
|
||||
name: "monkey Category 3",
|
||||
parent_category: ignored_category
|
||||
)
|
||||
end
|
||||
|
||||
let!(:post2) do
|
||||
Fabricate(:post,
|
||||
topic: Fabricate(:topic, category: child_of_ignored_category),
|
||||
raw: "snow monkey park"
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the right results' do
|
||||
search = Search.execute("monkey")
|
||||
|
||||
expect(search.categories).to contain_exactly(
|
||||
category, ignored_category, child_of_ignored_category
|
||||
)
|
||||
|
||||
expect(search.posts).to contain_exactly(
|
||||
category.topic.first_post,
|
||||
post,
|
||||
child_of_ignored_category.topic.first_post,
|
||||
post2
|
||||
)
|
||||
|
||||
search = Search.execute("snow")
|
||||
expect(search.posts).to contain_exactly(post, post2)
|
||||
|
||||
category.set_permissions({})
|
||||
category.save
|
||||
search = Search.execute("monkey")
|
||||
|
||||
expect(search.categories).not_to be_present
|
||||
expect(search.categories).to contain_exactly(
|
||||
ignored_category, child_of_ignored_category
|
||||
)
|
||||
|
||||
expect(search.posts).to contain_exactly(
|
||||
child_of_ignored_category.topic.first_post,
|
||||
post2
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -576,7 +629,10 @@ describe Search do
|
|||
end
|
||||
|
||||
it 'can use category as a search context' do
|
||||
category = Fabricate(:category)
|
||||
category = Fabricate(:category,
|
||||
search_priority: Searchable::PRIORITIES[:ignore]
|
||||
)
|
||||
|
||||
topic = Fabricate(:topic, category: category)
|
||||
topic_no_cat = Fabricate(:topic)
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ require 'rails_helper'
|
|||
require_dependency 'post_creator'
|
||||
|
||||
describe Category do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
it { is_expected.to validate_presence_of :user_id }
|
||||
it { is_expected.to validate_presence_of :name }
|
||||
|
||||
|
@ -12,6 +14,17 @@ describe Category do
|
|||
is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_category_id)
|
||||
end
|
||||
|
||||
it 'validates inclusion of search_priority' do
|
||||
category = Fabricate.build(:category, user: user)
|
||||
|
||||
expect(category.valid?).to eq(true)
|
||||
|
||||
category.search_priority = Searchable::PRIORITIES.values.last + 1
|
||||
|
||||
expect(category.valid?).to eq(false)
|
||||
expect(category.errors.keys).to contain_exactly(:search_priority)
|
||||
end
|
||||
|
||||
it 'validates uniqueness in case insensitive way' do
|
||||
Fabricate(:category, name: "Cats")
|
||||
cats = Fabricate.build(:category, name: "cats")
|
||||
|
|
|
@ -141,6 +141,7 @@ describe CategoriesController do
|
|||
text_color: "fff",
|
||||
slug: "hello-cat",
|
||||
auto_close_hours: 72,
|
||||
search_priority: Searchable::PRIORITIES[:ignore],
|
||||
permissions: {
|
||||
"everyone" => readonly,
|
||||
"staff" => create_post
|
||||
|
@ -156,6 +157,7 @@ describe CategoriesController do
|
|||
expect(category.slug).to eq("hello-cat")
|
||||
expect(category.color).to eq("ff0")
|
||||
expect(category.auto_close_hours).to eq(72)
|
||||
expect(category.search_priority).to eq(Searchable::PRIORITIES[:ignore])
|
||||
expect(UserHistory.count).to eq(4) # 1 + 3 (bootstrap mode)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,27 +16,22 @@ QUnit.test("Can open the category modal", async assert => {
|
|||
assert.ok(!visible(".d-modal"), "it closes the modal");
|
||||
});
|
||||
|
||||
QUnit.test("Change the category color", async assert => {
|
||||
QUnit.test("Editing the category", async assert => {
|
||||
await visit("/c/bug");
|
||||
|
||||
await click(".edit-category");
|
||||
await fillIn("#edit-text-color", "#ff0000");
|
||||
await click("#save-category");
|
||||
assert.ok(!visible(".d-modal"), "it closes the modal");
|
||||
assert.equal(
|
||||
DiscourseURL.redirectedTo,
|
||||
"/c/bug",
|
||||
"it does one of the rare full page redirects"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Change the topic template", async assert => {
|
||||
await visit("/c/bug");
|
||||
|
||||
await click(".edit-category");
|
||||
await click(".edit-category-topic-template");
|
||||
await fillIn(".d-editor-input", "this is the new topic template");
|
||||
|
||||
await click(".edit-category-settings");
|
||||
const searchPriorityChooser = selectKit("#category-search-priority");
|
||||
await searchPriorityChooser.expand();
|
||||
await searchPriorityChooser.selectRowByValue(1);
|
||||
|
||||
await click("#save-category");
|
||||
|
||||
assert.ok(!visible(".d-modal"), "it closes the modal");
|
||||
assert.equal(
|
||||
DiscourseURL.redirectedTo,
|
||||
|
|
Loading…
Reference in New Issue