scoped_search 4.0.0 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.rdoc +11 -0
- data/Gemfile +3 -0
- data/Gemfile.activerecord42 +3 -0
- data/Gemfile.activerecord50 +1 -0
- data/Gemfile.activerecord51 +17 -0
- data/lib/scoped_search.rb +6 -1
- data/lib/scoped_search/auto_complete_builder.rb +2 -0
- data/lib/scoped_search/definition.rb +27 -9
- data/lib/scoped_search/rails_helper.rb +3 -9
- data/lib/scoped_search/version.rb +1 -1
- data/spec/integration/api_spec.rb +15 -8
- data/spec/integration/auto_complete_spec.rb +1 -1
- data/spec/integration/ext_method_spec.rb +49 -0
- data/spec/integration/ordinal_querying_spec.rb +23 -7
- data/spec/{unit → integration}/rails_helper_spec.rb +9 -23
- data/spec/integration/scope_spec.rb +30 -0
- data/spec/integration/sti_querying_spec.rb +83 -0
- data/spec/lib/database.rb +7 -0
- data/spec/lib/mocks.rb +7 -0
- data/spec/unit/definition_spec.rb +54 -2
- data/spec/unit/query_builder_spec.rb +34 -1
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b15c18415944dcdf767c8ab154ac00186290c2b
|
4
|
+
data.tar.gz: ca8384a89a6887ae0335b02fe7d8e0341654c063
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3534f6ce7a7c2d0edc4c3d5ca1fe5c43bf21ef51491861d298da837dcb949d86a237df26680f1273a9f8ec88a0bf6b139efd8e801747e9c994435628f66dd343
|
7
|
+
data.tar.gz: 7fedaf4f620b8f77b5dd3594a77810f51632ac7a8d0d5ac33ea946c34ae4b085eb85ca7502ebe2c8955b93d1f9c5940ba76422ef180fe4177709d1290878d13e
|
data/.travis.yml
CHANGED
@@ -15,6 +15,7 @@ rvm:
|
|
15
15
|
- "2.1"
|
16
16
|
- "2.2.2"
|
17
17
|
- "2.3.1"
|
18
|
+
- "2.4.0"
|
18
19
|
- ruby-head
|
19
20
|
- jruby-19mode
|
20
21
|
- jruby-head
|
@@ -22,6 +23,7 @@ rvm:
|
|
22
23
|
gemfile:
|
23
24
|
- Gemfile.activerecord42
|
24
25
|
- Gemfile.activerecord50
|
26
|
+
- Gemfile.activerecord51
|
25
27
|
|
26
28
|
matrix:
|
27
29
|
allow_failures:
|
@@ -33,3 +35,7 @@ matrix:
|
|
33
35
|
gemfile: Gemfile.activerecord50
|
34
36
|
- rvm: "2.1"
|
35
37
|
gemfile: Gemfile.activerecord50
|
38
|
+
- rvm: "2.0"
|
39
|
+
gemfile: Gemfile.activerecord51
|
40
|
+
- rvm: "2.1"
|
41
|
+
gemfile: Gemfile.activerecord51
|
data/CHANGELOG.rdoc
CHANGED
@@ -8,6 +8,17 @@ Please add an entry to the "Unreleased changes" section in your pull requests.
|
|
8
8
|
|
9
9
|
*Nothing yet*
|
10
10
|
|
11
|
+
=== Version 4.1.0
|
12
|
+
|
13
|
+
- Add support for ActiveRecord and ActionView 5.1
|
14
|
+
- Add support for Ruby 2.4
|
15
|
+
- Support calling `search_for` on an STI subclass, returning only records of the
|
16
|
+
subclass type. (#112)
|
17
|
+
- Inherited search definitions: when defining search fields on both STI parents
|
18
|
+
and subclasses, the subclass can now be searched on all fields, including
|
19
|
+
those inherited from the parent. Only works for STI classes. (#135)
|
20
|
+
- Add 'tomorrow' and 'from now' keywords for searching future dates (#162)
|
21
|
+
|
11
22
|
=== Version 4.0.0
|
12
23
|
|
13
24
|
- Drop support for Ruby 1.9
|
data/Gemfile
CHANGED
data/Gemfile.activerecord42
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
gemspec
|
3
3
|
|
4
|
+
gem 'actionview', '~> 4.2.0'
|
4
5
|
gem 'activerecord', '~> 4.2.0'
|
5
6
|
|
7
|
+
gem 'nokogiri', '~> 1.6.0' if RUBY_VERSION.start_with?('2.0')
|
8
|
+
|
6
9
|
platforms :jruby do
|
7
10
|
gem 'activerecord-jdbcsqlite3-adapter'
|
8
11
|
gem 'activerecord-jdbcmysql-adapter'
|
data/Gemfile.activerecord50
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
gemspec
|
3
|
+
|
4
|
+
gem 'actionview', '>= 5.1.0.beta1', '< 5.2'
|
5
|
+
gem 'activerecord', '>= 5.1.0.beta1', '< 5.2'
|
6
|
+
|
7
|
+
platforms :jruby do
|
8
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
9
|
+
gem 'activerecord-jdbcmysql-adapter'
|
10
|
+
gem 'activerecord-jdbcpostgresql-adapter'
|
11
|
+
end
|
12
|
+
|
13
|
+
platforms :ruby do
|
14
|
+
gem 'sqlite3', '~> 1.3.6'
|
15
|
+
gem 'mysql2', '>= 0.3.18', '< 0.5'
|
16
|
+
gem 'pg', '~> 0.18'
|
17
|
+
end
|
data/lib/scoped_search.rb
CHANGED
@@ -24,9 +24,14 @@ module ScopedSearch
|
|
24
24
|
|
25
25
|
# Export the scoped_search method fo defining the search options.
|
26
26
|
# This method will create a definition instance for the class if it does not yet exist,
|
27
|
-
#
|
27
|
+
# or if a parent definition exists then it will create a new one inheriting it,
|
28
|
+
# and use the object as block argument and return value.
|
28
29
|
def scoped_search(*definitions)
|
29
30
|
self.scoped_search_definition ||= ScopedSearch::Definition.new(self)
|
31
|
+
unless self.scoped_search_definition.klass == self # inheriting the parent
|
32
|
+
self.scoped_search_definition = ScopedSearch::Definition.new(self)
|
33
|
+
end
|
34
|
+
|
30
35
|
definitions.each do |definition|
|
31
36
|
if definition[:on].kind_of?(Array)
|
32
37
|
definition[:on].each { |field| self.scoped_search_definition.define(definition.merge(:on => field)) }
|
@@ -229,12 +229,14 @@ module ScopedSearch
|
|
229
229
|
options << '"2 hours ago"'
|
230
230
|
options << 'Today'
|
231
231
|
options << 'Yesterday'
|
232
|
+
options << 'Tomorrow'
|
232
233
|
options << 2.days.ago.strftime('%A')
|
233
234
|
options << 3.days.ago.strftime('%A')
|
234
235
|
options << 4.days.ago.strftime('%A')
|
235
236
|
options << 5.days.ago.strftime('%A')
|
236
237
|
options << '"6 days ago"'
|
237
238
|
options << 7.days.ago.strftime('"%b %d,%Y"')
|
239
|
+
options << '2 weeks from now'
|
238
240
|
options
|
239
241
|
end
|
240
242
|
|
@@ -81,11 +81,10 @@ module ScopedSearch
|
|
81
81
|
@word_size = word_size
|
82
82
|
|
83
83
|
# Store this field in the field array
|
84
|
-
definition.
|
85
|
-
definition.unique_fields << self
|
84
|
+
definition.define_field(rename || @field, self)
|
86
85
|
|
87
86
|
# Store definition for aliases as well
|
88
|
-
aliases.each { |al| definition.
|
87
|
+
aliases.each { |al| definition.define_field(al, self) }
|
89
88
|
end
|
90
89
|
|
91
90
|
# The ActiveRecord-based class that belongs to this field.
|
@@ -193,14 +192,29 @@ module ScopedSearch
|
|
193
192
|
|
194
193
|
attr_accessor :profile, :default_order
|
195
194
|
|
195
|
+
def super_definition
|
196
|
+
klass.superclass.try(:scoped_search_definition)
|
197
|
+
end
|
198
|
+
|
199
|
+
def define_field(name, field)
|
200
|
+
@profile ||= :default
|
201
|
+
@profile_fields[@profile] ||= {}
|
202
|
+
@profile_fields[@profile][name.to_sym] ||= field
|
203
|
+
@profile_unique_fields[@profile] ||= []
|
204
|
+
@profile_unique_fields[@profile] = (@profile_unique_fields[@profile] + [field]).uniq
|
205
|
+
field
|
206
|
+
end
|
207
|
+
|
196
208
|
def fields
|
197
209
|
@profile ||= :default
|
198
210
|
@profile_fields[@profile] ||= {}
|
211
|
+
super_definition ? super_definition.fields.merge(@profile_fields[@profile]) : @profile_fields[@profile]
|
199
212
|
end
|
200
213
|
|
201
214
|
def unique_fields
|
202
215
|
@profile ||= :default
|
203
216
|
@profile_unique_fields[@profile] ||= []
|
217
|
+
super_definition ? (super_definition.unique_fields + @profile_unique_fields[@profile]).uniq : @profile_unique_fields[@profile]
|
204
218
|
end
|
205
219
|
|
206
220
|
# this method return definitions::field object from string
|
@@ -241,13 +255,16 @@ module ScopedSearch
|
|
241
255
|
end
|
242
256
|
|
243
257
|
# Try to parse a string as a datetime.
|
244
|
-
# Supported formats are Today, Yesterday, Sunday, '1 day ago', '2 hours ago', '3 months ago','Jan 23, 2004'
|
258
|
+
# Supported formats are Today, Yesterday, Sunday, '1 day ago', '2 hours ago', '3 months ago', '4 weeks from now', 'Jan 23, 2004'
|
245
259
|
# And many more formats that are documented in Ruby DateTime API Doc.
|
246
260
|
def parse_temporal(value)
|
247
261
|
return Date.current if value =~ /\btoday\b/i
|
248
262
|
return 1.day.ago.to_date if value =~ /\byesterday\b/i
|
263
|
+
return 1.day.from_now.to_date if value =~ /\btomorrow\b/i
|
249
264
|
return (eval($1.strip.gsub(/\s+/,'.').downcase)).to_datetime if value =~ /\A\s*(\d+\s+\b(?:hours?|minutes?)\b\s+\bago)\b\s*\z/i
|
250
265
|
return (eval($1.strip.gsub(/\s+/,'.').downcase)).to_date if value =~ /\A\s*(\d+\s+\b(?:days?|weeks?|months?|years?)\b\s+\bago)\b\s*\z/i
|
266
|
+
return (eval($1.strip.gsub(/from\s+now/i,'from_now').gsub(/\s+/,'.').downcase)).to_datetime if value =~ /\A\s*(\d+\s+\b(?:hours?|minutes?)\b\s+\bfrom\s+now)\b\s*\z/i
|
267
|
+
return (eval($1.strip.gsub(/from\s+now/i,'from_now').gsub(/\s+/,'.').downcase)).to_date if value =~ /\A\s*(\d+\s+\b(?:days?|weeks?|months?|years?)\b\s+\bfrom\s+now)\b\s*\z/i
|
251
268
|
DateTime.parse(value, true) rescue nil
|
252
269
|
end
|
253
270
|
|
@@ -274,12 +291,13 @@ module ScopedSearch
|
|
274
291
|
|
275
292
|
# Registers the search_for named scope within the class that is used for searching.
|
276
293
|
def register_named_scope! # :nodoc
|
277
|
-
|
278
|
-
|
279
|
-
klass =
|
294
|
+
@klass.define_singleton_method(:search_for) do |query = '', options = {}|
|
295
|
+
# klass may be different to @klass if the scope is called on a subclass
|
296
|
+
klass = self
|
297
|
+
definition = klass.scoped_search_definition
|
280
298
|
|
281
299
|
search_scope = klass.all
|
282
|
-
find_options = ScopedSearch::QueryBuilder.build_query(definition, query || '', options
|
300
|
+
find_options = ScopedSearch::QueryBuilder.build_query(definition, query || '', options)
|
283
301
|
search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
|
284
302
|
search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
|
285
303
|
search_scope = search_scope.joins(find_options[:joins]) if find_options[:joins]
|
@@ -287,7 +305,7 @@ module ScopedSearch
|
|
287
305
|
search_scope = search_scope.references(find_options[:include]) if find_options[:include]
|
288
306
|
|
289
307
|
search_scope
|
290
|
-
|
308
|
+
end
|
291
309
|
end
|
292
310
|
|
293
311
|
# Registers the complete_for method within the class that is used for searching.
|
@@ -47,10 +47,10 @@ module ScopedSearch
|
|
47
47
|
unless selected_sort.nil?
|
48
48
|
css_classes = html_options[:class] ? html_options[:class].split(" ") : []
|
49
49
|
if selected_sort == ascend
|
50
|
-
as = "▲ 
|
50
|
+
as = "▲ ".html_safe + as
|
51
51
|
css_classes << "ascending"
|
52
52
|
else
|
53
|
-
as = "▼ 
|
53
|
+
as = "▼ ".html_safe + as
|
54
54
|
css_classes << "descending"
|
55
55
|
end
|
56
56
|
html_options[:class] = css_classes.join(" ")
|
@@ -61,13 +61,7 @@ module ScopedSearch
|
|
61
61
|
|
62
62
|
as = raw(as) if defined?(RailsXss)
|
63
63
|
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
def a_link(name, href, html_options)
|
68
|
-
tag_options = tag_options(html_options)
|
69
|
-
link = "<a href=\"#{href}\"#{tag_options}>#{name}</a>"
|
70
|
-
return link.respond_to?(:html_safe) ? link.html_safe : link
|
64
|
+
content_tag(:a, as, html_options.merge(href: url_for(url_options)))
|
71
65
|
end
|
72
66
|
end
|
73
67
|
end
|
@@ -21,13 +21,6 @@ describe ScopedSearch, "API" do
|
|
21
21
|
ScopedSearch::RSpec::Database.close_connection
|
22
22
|
end
|
23
23
|
|
24
|
-
context 'for unprepared ActiveRecord model' do
|
25
|
-
|
26
|
-
it "should respond to :scoped_search to setup scoped_search for the model" do
|
27
|
-
Class.new(ActiveRecord::Base).should respond_to(:scoped_search)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
24
|
context 'for a prepared ActiveRecord model' do
|
32
25
|
|
33
26
|
before(:all) do
|
@@ -44,8 +37,22 @@ describe ScopedSearch, "API" do
|
|
44
37
|
@class.should respond_to(:search_for)
|
45
38
|
end
|
46
39
|
|
47
|
-
it "should return a ActiveRecord::Relation instance" do
|
40
|
+
it "should return a ActiveRecord::Relation instance with no arguments" do
|
41
|
+
@class.search_for.should be_a(ActiveRecord::Relation)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return a ActiveRecord::Relation instance with one argument" do
|
48
45
|
@class.search_for('query').should be_a(ActiveRecord::Relation)
|
49
46
|
end
|
47
|
+
|
48
|
+
it "should return a ActiveRecord::Relation instance with two arguments" do
|
49
|
+
@class.search_for('query', {}).should be_a(ActiveRecord::Relation)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should respect existing scope" do
|
53
|
+
@class.create! field: 'a'
|
54
|
+
record = @class.create! field: 'ab'
|
55
|
+
@class.where(field: 'ab').search_for('field ~ a').should eq([record])
|
56
|
+
end
|
50
57
|
end
|
51
58
|
end
|
@@ -91,7 +91,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
91
91
|
end
|
92
92
|
|
93
93
|
it "should complete when query is already distinct" do
|
94
|
-
Foo.
|
94
|
+
Foo.distinct.complete_for('int =').length.should > 0
|
95
95
|
end
|
96
96
|
|
97
97
|
it "should raise error for unindexed field" do
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
# These specs will run on all databases that are defined in the spec/database.yml file.
|
4
|
+
# Comment out any databases that you do not have available for testing purposes if needed.
|
5
|
+
ScopedSearch::RSpec::Database.test_databases.each do |db|
|
6
|
+
|
7
|
+
describe ScopedSearch, "using a #{db} database" do
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
ScopedSearch::RSpec::Database.establish_named_connection(db)
|
11
|
+
|
12
|
+
@class = ScopedSearch::RSpec::Database.create_model(alpha: :integer, beta_id: :integer) do |klass|
|
13
|
+
klass.send(:define_singleton_method, :test_ext_alpha) do |key, operator, value|
|
14
|
+
{ conditions: "#{key} = ?", parameter: [value.to_i * 2] }
|
15
|
+
end
|
16
|
+
klass.scoped_search on: :alpha, ext_method: :test_ext_alpha
|
17
|
+
end
|
18
|
+
|
19
|
+
@class2 = ScopedSearch::RSpec::Database.create_model(int: :integer) do |klass|
|
20
|
+
klass.has_one @class.table_name.to_sym, foreign_key: :beta_id
|
21
|
+
end
|
22
|
+
c2table = @class2.table_name.to_sym
|
23
|
+
@class.belongs_to c2table, foreign_key: :beta_id
|
24
|
+
|
25
|
+
@class.send(:define_singleton_method, :test_ext_beta) do |key, operator, value|
|
26
|
+
{ joins: c2table, conditions: "#{c2table}.int = ?", parameter: [value.to_i] }
|
27
|
+
end
|
28
|
+
@class.scoped_search relation: c2table, on: :int, rename: :beta, ext_method: :test_ext_beta
|
29
|
+
|
30
|
+
@class.create!(alpha: 1)
|
31
|
+
@beta = @class2.create!(int: 42)
|
32
|
+
@two = @class.create!(alpha: 2, beta_id: @beta.id)
|
33
|
+
end
|
34
|
+
|
35
|
+
after(:all) do
|
36
|
+
ScopedSearch::RSpec::Database.drop_model(@class)
|
37
|
+
ScopedSearch::RSpec::Database.drop_model(@class2)
|
38
|
+
ScopedSearch::RSpec::Database.close_connection
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should find record via conditions + parameter' do
|
42
|
+
@class.search_for('alpha = 1').should == [@two]
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should find record via joins + conditions + parameter' do
|
46
|
+
@class.search_for('beta = 42').should == [@two]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -166,6 +166,8 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
166
166
|
@curent_record = @class.create!(:timestamp => Time.current, :date => Date.current)
|
167
167
|
@hour_ago_record = @class.create!(:timestamp => Time.current - 1.hour, :date => Date.current)
|
168
168
|
@day_ago_record = @class.create!(:timestamp => Time.current - 1.day, :date => Date.current - 1.day)
|
169
|
+
@tomorrow_record = @class.create!(:timestamp => Time.current + 1.day, :date => Date.current + 1.day)
|
170
|
+
@week_from_now_record = @class.create!(:timestamp => Time.current + 1.week, :date => Date.current + 1.week)
|
169
171
|
@month_ago_record = @class.create!(:timestamp => Time.current - 1.month, :date => Date.current - 1.month)
|
170
172
|
@year_ago_record = @class.create!(:timestamp => Time.current - 1.year, :date => Date.current - 1.year)
|
171
173
|
end
|
@@ -174,6 +176,8 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
174
176
|
@curent_record.destroy
|
175
177
|
@hour_ago_record.destroy
|
176
178
|
@day_ago_record.destroy
|
179
|
+
@tomorrow_record.destroy
|
180
|
+
@week_from_now_record.destroy
|
177
181
|
@month_ago_record.destroy
|
178
182
|
@year_ago_record.destroy
|
179
183
|
end
|
@@ -186,6 +190,10 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
186
190
|
@class.search_for('date = yesterday').length.should == 1
|
187
191
|
end
|
188
192
|
|
193
|
+
it "should accept Tomorrow as date format" do
|
194
|
+
@class.search_for('date = tomorrow').length.should == 1
|
195
|
+
end
|
196
|
+
|
189
197
|
it "should find all timestamps and date from today using the = operator" do
|
190
198
|
@class.search_for('= Today').length.should == 2
|
191
199
|
end
|
@@ -198,16 +206,24 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
198
206
|
@class.search_for('date < "2 days ago"').length.should == 2
|
199
207
|
end
|
200
208
|
|
201
|
-
|
202
|
-
@class.search_for('timestamp > "3 hours ago"').length.should ==
|
203
|
-
|
209
|
+
it "should accept 3 hours ago as date format" do
|
210
|
+
@class.search_for('timestamp > "3 hours ago"').length.should == 4
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should accept 1 week from now as date format" do
|
214
|
+
@class.search_for('date < "1 week from now"').length.should == 6
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should accept 1 month ago as date format" do
|
218
|
+
@class.search_for('date > "1 month ago"').length.should == 5
|
219
|
+
end
|
204
220
|
|
205
|
-
|
206
|
-
@class.search_for('date
|
207
|
-
|
221
|
+
it "should accept 1 month from now as date format" do
|
222
|
+
@class.search_for('date < "1 month from now"').length.should == 7
|
223
|
+
end
|
208
224
|
|
209
225
|
it "should accept 1 year ago as date format" do
|
210
|
-
@class.search_for('date > "1 year ago"').length.should ==
|
226
|
+
@class.search_for('date > "1 year ago"').length.should == 6
|
211
227
|
end
|
212
228
|
|
213
229
|
end
|
@@ -1,19 +1,10 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require "action_view"
|
2
3
|
require "scoped_search/rails_helper"
|
3
4
|
|
4
|
-
module ActionViewHelperStubs
|
5
|
-
def html_escape(str)
|
6
|
-
str
|
7
|
-
end
|
8
|
-
|
9
|
-
def tag_options(options)
|
10
|
-
""
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
5
|
describe ScopedSearch::RailsHelper do
|
15
6
|
include ScopedSearch::RailsHelper
|
16
|
-
include
|
7
|
+
include ActionView::Helpers
|
17
8
|
|
18
9
|
let(:params) { HashWithIndifferentAccess.new(:controller => "resources", :action => "search") }
|
19
10
|
|
@@ -71,26 +62,21 @@ describe ScopedSearch::RailsHelper do
|
|
71
62
|
sort("other")
|
72
63
|
end
|
73
64
|
|
74
|
-
it "should
|
75
|
-
should_receive(:url_for)
|
76
|
-
|
77
|
-
sort("field")
|
65
|
+
it "should set :href and no :class on anchor" do
|
66
|
+
should_receive(:url_for).and_return('/example')
|
67
|
+
sort("field").should == '<a href="/example">Field</a>'
|
78
68
|
end
|
79
69
|
|
80
70
|
it "should add ascending style for current ascending sort order " do
|
81
|
-
should_receive(:url_for)
|
82
|
-
should_receive(:a_link).with('▲ Field', anything, hash_including(:class => 'ascending'))
|
83
|
-
|
71
|
+
should_receive(:url_for).and_return('/example')
|
84
72
|
params[:order] = "field ASC"
|
85
|
-
sort("field")
|
73
|
+
sort("field").should == '<a class="ascending" href="/example">▲ Field</a>'
|
86
74
|
end
|
87
75
|
|
88
76
|
it "should add descending style for current descending sort order " do
|
89
|
-
should_receive(:url_for)
|
90
|
-
should_receive(:a_link).with('▼ Field', anything, hash_including(:class => 'descending'))
|
91
|
-
|
77
|
+
should_receive(:url_for).and_return('/example')
|
92
78
|
params[:order] = "field DESC"
|
93
|
-
sort("field")
|
79
|
+
sort("field").should == '<a class="descending" href="/example">▼ Field</a>'
|
94
80
|
end
|
95
81
|
|
96
82
|
context 'with ActionController::Parameters' do
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
# These specs will run on all databases that are defined in the spec/database.yml file.
|
4
|
+
# Comment out any databases that you do not have available for testing purposes if needed.
|
5
|
+
ScopedSearch::RSpec::Database.test_databases.each do |db|
|
6
|
+
|
7
|
+
describe ScopedSearch, "using a #{db} database" do
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
ScopedSearch::RSpec::Database.establish_named_connection(db)
|
11
|
+
|
12
|
+
@class = ScopedSearch::RSpec::Database.create_model(:field => :string) do |klass|
|
13
|
+
klass.scoped_search :on => :field
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:all) do
|
18
|
+
ScopedSearch::RSpec::Database.drop_model(@class)
|
19
|
+
ScopedSearch::RSpec::Database.close_connection
|
20
|
+
end
|
21
|
+
|
22
|
+
context '.search_for' do
|
23
|
+
it "should respect existing scope" do
|
24
|
+
@class.create! field: 'a'
|
25
|
+
record = @class.create! field: 'ab'
|
26
|
+
@class.where(field: 'ab').search_for('field ~ a').should eq([record])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
# These specs will run on all databases that are defined in the spec/database.yml file.
|
4
|
+
# Comment out any databases that you do not have available for testing purposes if needed.
|
5
|
+
ScopedSearch::RSpec::Database.test_databases.each do |db|
|
6
|
+
|
7
|
+
describe ScopedSearch, "using a #{db} database" do
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
ScopedSearch::RSpec::Database.establish_named_connection(db)
|
11
|
+
|
12
|
+
@related_class = ScopedSearch::RSpec::Database.create_model(int: :integer)
|
13
|
+
|
14
|
+
@parent_class = ScopedSearch::RSpec::Database.create_model(int: :integer, type: :string, related_id: :integer) do |klass|
|
15
|
+
klass.scoped_search on: :int
|
16
|
+
end
|
17
|
+
@subclass1 = ScopedSearch::RSpec::Database.create_sti_model(@parent_class)
|
18
|
+
@subclass2 = ScopedSearch::RSpec::Database.create_sti_model(@parent_class) do |klass|
|
19
|
+
klass.belongs_to @related_class.table_name.to_sym, foreign_key: :related_id
|
20
|
+
klass.scoped_search on: :int, rename: :other_int
|
21
|
+
klass.scoped_search relation: @related_class.table_name, on: :int, rename: :related_int
|
22
|
+
end
|
23
|
+
|
24
|
+
@related_class.has_many @subclass1.table_name.to_sym
|
25
|
+
|
26
|
+
@record1 = @subclass1.create!(int: 7)
|
27
|
+
@related_record1 = @related_class.create!(int: 42)
|
28
|
+
@record2 = @subclass2.create!(int: 9, related_id: @related_record1.id)
|
29
|
+
end
|
30
|
+
|
31
|
+
after(:all) do
|
32
|
+
@record1.destroy
|
33
|
+
@record2.destroy
|
34
|
+
|
35
|
+
ScopedSearch::RSpec::Database.drop_model(@parent_class)
|
36
|
+
ScopedSearch::RSpec::Database.drop_model(@related_class)
|
37
|
+
ScopedSearch::RSpec::Database.close_connection
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'querying STI parent and subclasses' do
|
41
|
+
it "should find a record using the parent class" do
|
42
|
+
@parent_class.search_for('int = 7').should eq([@record1])
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should find a record using the subclass" do
|
46
|
+
@subclass1.search_for('int = 7').should eq([@record1])
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not find a record using the wrong subclass" do
|
50
|
+
@subclass2.search_for('int = 7').should eq([])
|
51
|
+
@subclass2.search_for('int = 9').should eq([@record2])
|
52
|
+
end
|
53
|
+
|
54
|
+
it "parent should not recognize field from subclass" do
|
55
|
+
lambda { @parent_class.search_for('related_int = 9') }.should raise_error(ScopedSearch::QueryNotSupported, "Field 'related_int' not recognized for searching!")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should autocomplete int field on parent" do
|
59
|
+
@parent_class.complete_for('').should contain(' int ')
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should autocomplete int field on subclass" do
|
63
|
+
@subclass1.complete_for('').should contain(' int ')
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should autocomplete int, other_int, related_int fields on subclass" do
|
67
|
+
@subclass2.complete_for('').should contain(' int ')
|
68
|
+
@subclass2.complete_for('').should contain(' other_int ')
|
69
|
+
@subclass2.complete_for('').should contain(' related_int ')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'querying definition on STI subclass' do
|
74
|
+
it "should find a record using subclass definition" do
|
75
|
+
@subclass2.search_for('other_int = 9').should eq([@record2])
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should find a record via relation" do
|
79
|
+
@subclass2.search_for('related_int = 42').should eq([@record2])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/spec/lib/database.rb
CHANGED
@@ -58,6 +58,13 @@ module ScopedSearch::RSpec::Database
|
|
58
58
|
return klass
|
59
59
|
end
|
60
60
|
|
61
|
+
def self.create_sti_model(parent)
|
62
|
+
klass_name = "#{parent.table_name}_#{rand}".gsub(/\W/, '')
|
63
|
+
klass = ScopedSearch::RSpec::Database.const_set(klass_name.classify, Class.new(parent))
|
64
|
+
yield(klass) if block_given?
|
65
|
+
return klass
|
66
|
+
end
|
67
|
+
|
61
68
|
def self.drop_model(klass)
|
62
69
|
klass.constants.grep(/\AHABTM_/).each do |habtm_class|
|
63
70
|
ActiveRecord::Migration.drop_table(klass.const_get(habtm_class).table_name)
|
data/spec/lib/mocks.rb
CHANGED
@@ -10,10 +10,17 @@ module ScopedSearch::RSpec::Mocks
|
|
10
10
|
ar_mock.stub(:scope).with(:search_for, anything)
|
11
11
|
ar_mock.stub(:connection).and_return(mock_database_connection)
|
12
12
|
ar_mock.stub(:ancestors).and_return([ActiveRecord::Base])
|
13
|
+
ar_mock.stub(:superclass).and_return(ActiveRecord::Base)
|
13
14
|
ar_mock.stub(:columns_hash).and_return({'existing' => double('column')})
|
14
15
|
return ar_mock
|
15
16
|
end
|
16
17
|
|
18
|
+
def mock_activerecord_subclass(parent)
|
19
|
+
ar_mock = mock_activerecord_class
|
20
|
+
ar_mock.stub(:superclass).and_return(parent)
|
21
|
+
return ar_mock
|
22
|
+
end
|
23
|
+
|
17
24
|
def mock_database_connection
|
18
25
|
c_mock = double('ActiveRecord::Base.connection')
|
19
26
|
return c_mock
|
@@ -6,6 +6,7 @@ describe ScopedSearch::Definition do
|
|
6
6
|
@klass = mock_activerecord_class
|
7
7
|
@definition = ScopedSearch::Definition.new(@klass)
|
8
8
|
@definition.stub(:setup_adapter)
|
9
|
+
@klass.stub(:scoped_search_definition).and_return(@definition)
|
9
10
|
end
|
10
11
|
|
11
12
|
describe ScopedSearch::Definition::Field do
|
@@ -62,14 +63,65 @@ describe ScopedSearch::Definition do
|
|
62
63
|
|
63
64
|
describe '#initialize' do
|
64
65
|
it "should create the named scope when" do
|
65
|
-
@klass.should_receive(:scope).with(:search_for, instance_of(Proc))
|
66
66
|
ScopedSearch::Definition.new(@klass)
|
67
|
+
@klass.should respond_to(:search_for)
|
67
68
|
end
|
68
69
|
|
69
70
|
it "should not create the named scope if it already exists" do
|
70
71
|
@klass.stub(:search_for)
|
71
|
-
@klass.should_not_receive(:
|
72
|
+
@klass.should_not_receive(:define_singleton_method)
|
72
73
|
ScopedSearch::Definition.new(@klass)
|
73
74
|
end
|
74
75
|
end
|
76
|
+
|
77
|
+
describe '#define_field' do
|
78
|
+
it "should add to fields" do
|
79
|
+
field = instance_double('ScopedSearch::Definition::Field')
|
80
|
+
@definition.define_field('test', field)
|
81
|
+
@definition.fields.should eq(test: field)
|
82
|
+
@definition.unique_fields.should eq([field])
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should add aliases" do
|
86
|
+
field = instance_double('ScopedSearch::Definition::Field')
|
87
|
+
@definition.define_field('test', field)
|
88
|
+
@definition.define_field('alias', field)
|
89
|
+
@definition.fields.should eq(test: field, alias: field)
|
90
|
+
@definition.unique_fields.should eq([field])
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should ignore duplicate field names" do
|
94
|
+
field1 = instance_double('ScopedSearch::Definition::Field')
|
95
|
+
field2 = instance_double('ScopedSearch::Definition::Field')
|
96
|
+
@definition.define_field('test', field1)
|
97
|
+
@definition.define_field('test', field2)
|
98
|
+
@definition.fields.should eq(test: field1)
|
99
|
+
@definition.unique_fields.should eq([field1, field2])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#fields' do
|
104
|
+
before(:each) do
|
105
|
+
@subklass = mock_activerecord_subclass(@klass)
|
106
|
+
@subdefinition = ScopedSearch::Definition.new(@subklass)
|
107
|
+
@subdefinition.stub(:setup_adapter)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should return fields from class" do
|
111
|
+
field = @definition.define(on: 'foo')
|
112
|
+
@definition.fields.should eq(foo: field)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should return fields from parent class" do
|
116
|
+
field = @definition.define(on: 'foo')
|
117
|
+
@subdefinition.fields.should eq(foo: field)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should combine fields from class and parent class" do
|
121
|
+
field1 = @definition.define(on: 'foo')
|
122
|
+
field2 = @subdefinition.define(on: 'foo', only_explicit: true)
|
123
|
+
field3 = @subdefinition.define(on: 'bar')
|
124
|
+
@subdefinition.fields.should eq(foo: field2, bar: field3)
|
125
|
+
end
|
126
|
+
end
|
75
127
|
end
|
@@ -2,9 +2,11 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe ScopedSearch::QueryBuilder do
|
4
4
|
|
5
|
+
let(:klass) { Class.new(ActiveRecord::Base) }
|
6
|
+
|
5
7
|
before(:each) do
|
6
8
|
@definition = double('ScopedSearch::Definition')
|
7
|
-
@definition.stub(:klass).and_return(
|
9
|
+
@definition.stub(:klass).and_return(klass)
|
8
10
|
@definition.stub(:profile).and_return(:default)
|
9
11
|
@definition.stub(:default_order).and_return(nil)
|
10
12
|
@definition.stub(:profile=).and_return(true)
|
@@ -55,4 +57,35 @@ describe ScopedSearch::QueryBuilder do
|
|
55
57
|
|
56
58
|
lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error('my custom message')
|
57
59
|
end
|
60
|
+
|
61
|
+
context "with ext_method" do
|
62
|
+
before do
|
63
|
+
@definition = ScopedSearch::Definition.new(klass)
|
64
|
+
@definition.define(:test_field, ext_method: :ext_test)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should return combined :conditions and :parameter" do
|
68
|
+
klass.should_receive(:ext_test).with('test_field', '=', 'test_val').and_return(conditions: 'field = ?', parameter: ['test_val'])
|
69
|
+
ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val').should eq(conditions: ['(field = ?)', 'test_val'])
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should return :joins and :include" do
|
73
|
+
klass.should_receive(:ext_test).with('test_field', '=', 'test_val').and_return(include: 'test1', joins: 'test2')
|
74
|
+
ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val').should eq(include: ['test1'], joins: ['test2'])
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should raise error when non-hash returned" do
|
78
|
+
klass.should_receive(:ext_test).and_return('test')
|
79
|
+
lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported, /should return hash/)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should raise error when method doesn't exist" do
|
83
|
+
lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported, /doesn't respond to 'ext_test'/)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should ignore exceptions" do
|
87
|
+
klass.should_receive(:ext_test).and_raise('test')
|
88
|
+
ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val').should eq({})
|
89
|
+
end
|
90
|
+
end
|
58
91
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scoped_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Amos Benari
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2017-03-29 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -84,6 +84,7 @@ files:
|
|
84
84
|
- Gemfile
|
85
85
|
- Gemfile.activerecord42
|
86
86
|
- Gemfile.activerecord50
|
87
|
+
- Gemfile.activerecord51
|
87
88
|
- LICENSE
|
88
89
|
- README.rdoc
|
89
90
|
- Rakefile
|
@@ -109,11 +110,15 @@ files:
|
|
109
110
|
- spec/database.ruby.yml
|
110
111
|
- spec/integration/api_spec.rb
|
111
112
|
- spec/integration/auto_complete_spec.rb
|
113
|
+
- spec/integration/ext_method_spec.rb
|
112
114
|
- spec/integration/key_value_querying_spec.rb
|
113
115
|
- spec/integration/ordinal_querying_spec.rb
|
114
116
|
- spec/integration/profile_querying_spec.rb
|
117
|
+
- spec/integration/rails_helper_spec.rb
|
115
118
|
- spec/integration/relation_querying_spec.rb
|
119
|
+
- spec/integration/scope_spec.rb
|
116
120
|
- spec/integration/set_query_spec.rb
|
121
|
+
- spec/integration/sti_querying_spec.rb
|
117
122
|
- spec/integration/string_querying_spec.rb
|
118
123
|
- spec/lib/database.rb
|
119
124
|
- spec/lib/matchers.rb
|
@@ -124,7 +129,6 @@ files:
|
|
124
129
|
- spec/unit/definition_spec.rb
|
125
130
|
- spec/unit/parser_spec.rb
|
126
131
|
- spec/unit/query_builder_spec.rb
|
127
|
-
- spec/unit/rails_helper_spec.rb
|
128
132
|
- spec/unit/tokenizer_spec.rb
|
129
133
|
- spec/unit/validators_spec.rb
|
130
134
|
homepage: https://github.com/wvanbergen/scoped_search/wiki
|
@@ -163,11 +167,15 @@ test_files:
|
|
163
167
|
- spec/database.ruby.yml
|
164
168
|
- spec/integration/api_spec.rb
|
165
169
|
- spec/integration/auto_complete_spec.rb
|
170
|
+
- spec/integration/ext_method_spec.rb
|
166
171
|
- spec/integration/key_value_querying_spec.rb
|
167
172
|
- spec/integration/ordinal_querying_spec.rb
|
168
173
|
- spec/integration/profile_querying_spec.rb
|
174
|
+
- spec/integration/rails_helper_spec.rb
|
169
175
|
- spec/integration/relation_querying_spec.rb
|
176
|
+
- spec/integration/scope_spec.rb
|
170
177
|
- spec/integration/set_query_spec.rb
|
178
|
+
- spec/integration/sti_querying_spec.rb
|
171
179
|
- spec/integration/string_querying_spec.rb
|
172
180
|
- spec/lib/database.rb
|
173
181
|
- spec/lib/matchers.rb
|
@@ -178,6 +186,5 @@ test_files:
|
|
178
186
|
- spec/unit/definition_spec.rb
|
179
187
|
- spec/unit/parser_spec.rb
|
180
188
|
- spec/unit/query_builder_spec.rb
|
181
|
-
- spec/unit/rails_helper_spec.rb
|
182
189
|
- spec/unit/tokenizer_spec.rb
|
183
190
|
- spec/unit/validators_spec.rb
|