From 9abc7f929444049c1ac10b890e4aeb694e4c8e95 Mon Sep 17 00:00:00 2001 From: "M.Shibuya" Date: Sat, 1 Jun 2019 19:44:11 +0900 Subject: [PATCH] Add custom search feature. Closes #343, Closes #3019 --- lib/rails_admin/adapters/active_record.rb | 16 +++++++---- lib/rails_admin/adapters/mongoid.rb | 28 +++++++++++-------- lib/rails_admin/config/sections/list.rb | 4 +++ spec/dummy_app/app/active_record/player.rb | 2 ++ spec/dummy_app/app/mongoid/player.rb | 2 ++ .../basic/list/rails_admin_basic_list_spec.rb | 22 +++++++++++++++ .../adapters/active_record_spec.rb | 4 +-- spec/rails_admin/adapters/mongoid_spec.rb | 20 ++++++------- 8 files changed, 68 insertions(+), 30 deletions(-) diff --git a/lib/rails_admin/adapters/active_record.rb b/lib/rails_admin/adapters/active_record.rb index 469621fb28..eb90cf78d4 100644 --- a/lib/rails_admin/adapters/active_record.rb +++ b/lib/rails_admin/adapters/active_record.rb @@ -118,13 +118,17 @@ def build end def query_scope(scope, query, fields = config.list.fields.select(&:queryable?)) - wb = WhereBuilder.new(scope) - fields.each do |field| - value = parse_field_value(field, query) - wb.add(field, value, field.search_operator) + if config.list.search_by + scope.send(config.list.search_by, query) + else + wb = WhereBuilder.new(scope) + fields.each do |field| + value = parse_field_value(field, query) + wb.add(field, value, field.search_operator) + end + # OR all query statements + wb.build end - # OR all query statements - wb.build end # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...} diff --git a/lib/rails_admin/adapters/mongoid.rb b/lib/rails_admin/adapters/mongoid.rb index 5a7fccfd16..8c7cc520f6 100644 --- a/lib/rails_admin/adapters/mongoid.rb +++ b/lib/rails_admin/adapters/mongoid.rb @@ -42,8 +42,8 @@ def all(options = {}, scope = nil) scope = scope.includes(*options[:include]) if options[:include] scope = scope.limit(options[:limit]) if options[:limit] scope = scope.any_in(_id: options[:bulk_ids]) if options[:bulk_ids] - scope = scope.where(query_conditions(options[:query])) if options[:query] - scope = scope.where(filter_conditions(options[:filters])) if options[:filters] + scope = query_scope(scope, options[:query]) if options[:query] + scope = filter_scope(scope, options[:filters]) if options[:filters] if options[:page] && options[:per] scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) end @@ -113,21 +113,25 @@ def make_field_conditions(field, value, operator) conditions_per_collection end - def query_conditions(query, fields = config.list.fields.select(&:queryable?)) - statements = [] + def query_scope(scope, query, fields = config.list.fields.select(&:queryable?)) + if config.list.search_by + scope.send(config.list.search_by, query) + else + statements = [] - fields.each do |field| - value = parse_field_value(field, query) - conditions_per_collection = make_field_conditions(field, value, field.search_operator) - statements.concat make_condition_for_current_collection(field, conditions_per_collection) - end + fields.each do |field| + value = parse_field_value(field, query) + conditions_per_collection = make_field_conditions(field, value, field.search_operator) + statements.concat make_condition_for_current_collection(field, conditions_per_collection) + end - statements.any? ? {'$or' => statements} : {} + scope.where(statements.any? ? {'$or' => statements} : {}) + end end # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...} # "0055" is the filter index, no use here. o is the operator, v the value - def filter_conditions(filters, fields = config.list.fields.select(&:filterable?)) + def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?)) statements = [] filters.each_pair do |field_name, filters_dump| @@ -145,7 +149,7 @@ def filter_conditions(filters, fields = config.list.fields.select(&:filterable?) end end - statements.any? ? {'$and' => statements} : {} + scope.where(statements.any? ? {'$and' => statements} : {}) end def parse_collection_name(column) diff --git a/lib/rails_admin/config/sections/list.rb b/lib/rails_admin/config/sections/list.rb index 132f6351e4..1d8f1db186 100644 --- a/lib/rails_admin/config/sections/list.rb +++ b/lib/rails_admin/config/sections/list.rb @@ -24,6 +24,10 @@ class List < RailsAdmin::Config::Sections::Base false end + register_instance_option :search_by do + nil + end + register_instance_option :sort_by do parent.abstract_model.primary_key end diff --git a/spec/dummy_app/app/active_record/player.rb b/spec/dummy_app/app/active_record/player.rb index 32d81379dd..e71300b681 100644 --- a/spec/dummy_app/app/active_record/player.rb +++ b/spec/dummy_app/app/active_record/player.rb @@ -14,6 +14,8 @@ class Player < ActiveRecord::Base before_destroy :destroy_hook + scope :rails_admin_search, ->(query) { where(name: query.reverse) } + def destroy_hook; end def draft_id diff --git a/spec/dummy_app/app/mongoid/player.rb b/spec/dummy_app/app/mongoid/player.rb index 0d26dbc79b..618014139b 100644 --- a/spec/dummy_app/app/mongoid/player.rb +++ b/spec/dummy_app/app/mongoid/player.rb @@ -28,6 +28,8 @@ class Player before_destroy :destroy_hook + scope :rails_admin_search, ->(query) { where(name: query.reverse) } + def destroy_hook; end def draft_id diff --git a/spec/integration/basic/list/rails_admin_basic_list_spec.rb b/spec/integration/basic/list/rails_admin_basic_list_spec.rb index 9f5863ea42..139cbc3d1d 100644 --- a/spec/integration/basic/list/rails_admin_basic_list_spec.rb +++ b/spec/integration/basic/list/rails_admin_basic_list_spec.rb @@ -505,6 +505,28 @@ def visit_page(page) end end + describe 'Custom search' do + before do + RailsAdmin.config do |config| + config.model Player do + list do + search_by :rails_admin_search + end + end + end + end + let!(:players) do + [FactoryBot.create(:player, name: 'Joe'), + FactoryBot.create(:player, name: 'George')] + end + + it 'performs search using given scope' do + visit index_path(model_name: 'player', query: 'eoJ') + is_expected.to have_content(players[0].name) + is_expected.to have_no_content(players[1].name) + end + end + describe 'list for objects with overridden to_param' do before do @ball = FactoryBot.create :ball diff --git a/spec/rails_admin/adapters/active_record_spec.rb b/spec/rails_admin/adapters/active_record_spec.rb index ebc34f548e..33e4514799 100644 --- a/spec/rails_admin/adapters/active_record_spec.rb +++ b/spec/rails_admin/adapters/active_record_spec.rb @@ -128,7 +128,7 @@ class PlayerWithDefaultScope < Player end end - describe '#query_conditions' do + describe '#query_scope' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Team') } before do @@ -155,7 +155,7 @@ class PlayerWithDefaultScope < Player end end - describe '#filter_conditions' do + describe '#filter_scope' do let(:abstract_model) { RailsAdmin::AbstractModel.new('Team') } before do diff --git a/spec/rails_admin/adapters/mongoid_spec.rb b/spec/rails_admin/adapters/mongoid_spec.rb index 8f493cebae..a74afe74c1 100644 --- a/spec/rails_admin/adapters/mongoid_spec.rb +++ b/spec/rails_admin/adapters/mongoid_spec.rb @@ -188,7 +188,7 @@ end end - describe '#query_conditions' do + describe '#query_scope' do before do @abstract_model = RailsAdmin::AbstractModel.new('Player') @players = [{}, {name: 'Many foos'}, {position: 'foo shortage'}]. @@ -200,7 +200,7 @@ end end - describe '#filter_conditions' do + describe '#filter_scope' do before do @abstract_model = RailsAdmin::AbstractModel.new('Player') @team = FactoryBot.create :team, name: 'king of bar' @@ -341,17 +341,17 @@ end it 'supports date type query' do - expect(@abstract_model.send(:filter_conditions, 'date_field' => {'1' => {v: ['', 'January 02, 2012', 'January 03, 2012'], o: 'between'}})).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 3)}}]) - expect(@abstract_model.send(:filter_conditions, 'date_field' => {'1' => {v: ['', 'January 03, 2012', ''], o: 'between'}})).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 3)}}]) - expect(@abstract_model.send(:filter_conditions, 'date_field' => {'1' => {v: ['', '', 'January 02, 2012'], o: 'between'}})).to eq('$and' => [{'date_field' => {'$lte' => Date.new(2012, 1, 2)}}]) - expect(@abstract_model.send(:filter_conditions, 'date_field' => {'1' => {v: ['January 02, 2012'], o: 'default'}})).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 2)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', 'January 02, 2012', 'January 03, 2012'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 3)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', 'January 03, 2012', ''], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 3)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['', '', 'January 02, 2012'], o: 'between'}}).selector).to eq('$and' => [{'date_field' => {'$lte' => Date.new(2012, 1, 2)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'date_field' => {'1' => {v: ['January 02, 2012'], o: 'default'}}).selector).to eq('$and' => [{'date_field' => {'$gte' => Date.new(2012, 1, 2), '$lte' => Date.new(2012, 1, 2)}}]) end it 'supports datetime type query' do - expect(@abstract_model.send(:filter_conditions, 'datetime_field' => {'1' => {v: ['', 'January 02, 2012 00:00', 'January 03, 2012 00:00'], o: 'between'}})).to eq('$and' => [{'datetime_field' => {'$gte' => Time.local(2012, 1, 2), '$lte' => Time.local(2012, 1, 3).end_of_day}}]) - expect(@abstract_model.send(:filter_conditions, 'datetime_field' => {'1' => {v: ['', 'January 03, 2012 00:00', ''], o: 'between'}})).to eq('$and' => [{'datetime_field' => {'$gte' => Time.local(2012, 1, 3)}}]) - expect(@abstract_model.send(:filter_conditions, 'datetime_field' => {'1' => {v: ['', '', 'January 02, 2012 00:00'], o: 'between'}})).to eq('$and' => [{'datetime_field' => {'$lte' => Time.local(2012, 1, 2).end_of_day}}]) - expect(@abstract_model.send(:filter_conditions, 'datetime_field' => {'1' => {v: ['January 02, 2012 00:00'], o: 'default'}})).to eq('$and' => [{'datetime_field' => {'$gte' => Time.local(2012, 1, 2), '$lte' => Time.local(2012, 1, 2).end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', 'January 02, 2012 00:00', 'January 03, 2012 00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.local(2012, 1, 2), '$lte' => Time.local(2012, 1, 3).end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', 'January 03, 2012 00:00', ''], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.local(2012, 1, 3)}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['', '', 'January 02, 2012 00:00'], o: 'between'}}).selector).to eq('$and' => [{'datetime_field' => {'$lte' => Time.local(2012, 1, 2).end_of_day}}]) + expect(@abstract_model.send(:filter_scope, FieldTest, 'datetime_field' => {'1' => {v: ['January 02, 2012 00:00'], o: 'default'}}).selector).to eq('$and' => [{'datetime_field' => {'$gte' => Time.local(2012, 1, 2), '$lte' => Time.local(2012, 1, 2).end_of_day}}]) end it 'supports enum type query' do