scoped_search 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|