diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index ee2c9786c2a..6aeb5285c31 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -127,6 +127,8 @@ class DiscoursePluginRegistry define_filtered_register :flag_applies_to_types + define_filtered_register :custom_filter_mappings + def self.register_auth_provider(auth_provider) self.auth_providers << auth_provider end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 10bbd34c802..916248efcf7 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -239,6 +239,17 @@ class Plugin::Instance DiscoursePluginRegistry.register_editable_group_custom_field(field, self) end + # Allows to define custom filter utilizing the user's input. + # Ensure proper input sanitization before using it in a query. + # + # Example usage: + # add_filter_custom_filter("word_count") do |scope, value| + # scope.where(word_count: value) + # end + def add_filter_custom_filter(name, &block) + DiscoursePluginRegistry.register_custom_filter_mapping({ name => block }, self) + end + # Allows to define custom "status:" filter. Example usage: # register_custom_filter_by_status("foobar") do |scope| # scope.where("word_count = 42") diff --git a/lib/topics_filter.rb b/lib/topics_filter.rb index ae1ec5bb580..8a308041e7b 100644 --- a/lib/topics_filter.rb +++ b/lib/topics_filter.rb @@ -82,6 +82,11 @@ class TopicsFilter filter_by_number_of_views(min: filter_values) when "views-max" filter_by_number_of_views(max: filter_values) + else + if custom_filter = + DiscoursePluginRegistry.custom_filter_mappings.find { |hash| hash.key?(filter) } + @scope = custom_filter[filter].call(@scope, filter_values) + end end end diff --git a/spec/lib/topics_filter_spec.rb b/spec/lib/topics_filter_spec.rb index 5fa0a0b9ead..83c7bb8cbfa 100644 --- a/spec/lib/topics_filter_spec.rb +++ b/spec/lib/topics_filter_spec.rb @@ -212,6 +212,43 @@ RSpec.describe TopicsFilter do end end + describe "when filtering with custom filters" do + fab!(:topic) + fab!(:word_count_topic) { Fabricate(:topic, word_count: 42) } + fab!(:word_count_topic_2) { Fabricate(:topic, word_count: 42) } + + let(:word_count_block) { Proc.new { |scope, value| scope.where(word_count: value) } } + let(:id_block) { Proc.new { |scope, value| scope.where(id: value) } } + let(:plugin) { Plugin::Instance.new } + + it "supports a custom filter" do + plugin.add_filter_custom_filter("word_count", &word_count_block) + + expect( + TopicsFilter + .new(guardian: Guardian.new) + .filter_from_query_string("word_count:42") + .pluck(:id), + ).to contain_exactly(word_count_topic.id, word_count_topic_2.id) + ensure + DiscoursePluginRegistry.reset_register!(:custom_filter_mappings) + end + + it "supports multiple custom filters" do + plugin.add_filter_custom_filter("word_count", &word_count_block) + plugin.add_filter_custom_filter("id", &id_block) + + expect( + TopicsFilter + .new(guardian: Guardian.new) + .filter_from_query_string("word_count:42 id:#{word_count_topic.id}") + .pluck(:id), + ).to contain_exactly(word_count_topic.id) + ensure + DiscoursePluginRegistry.reset_register!(:custom_filter_mappings) + end + end + describe "when filtering by categories" do fab!(:category) { Fabricate(:category, name: "category") }