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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: df5ac5265b984d6208df025b20dd336950d9bdbb
4
- data.tar.gz: 3643c8835b43e6333e24d44473b1b29c3ec7ac91
3
+ metadata.gz: 3b15c18415944dcdf767c8ab154ac00186290c2b
4
+ data.tar.gz: ca8384a89a6887ae0335b02fe7d8e0341654c063
5
5
  SHA512:
6
- metadata.gz: 7d760759a261eda13744cad755263a056213315859bc925e5ea814cd0e214e19941dc6b3e07572f80b0bf10058359d6895f663a9249b894fa53297c9f81a7efa
7
- data.tar.gz: 4b9580f1b55aeaf6f74b64847318361d58f1e3a7477376b1db4921861ec95db3ba8594f7a6f5915f097fdd9313b2fb9bd30b26afc53c2199d3f367d8fa54c336
6
+ metadata.gz: 3534f6ce7a7c2d0edc4c3d5ca1fe5c43bf21ef51491861d298da837dcb949d86a237df26680f1273a9f8ec88a0bf6b139efd8e801747e9c994435628f66dd343
7
+ data.tar.gz: 7fedaf4f620b8f77b5dd3594a77810f51632ac7a8d0d5ac33ea946c34ae4b085eb85ca7502ebe2c8955b93d1f9c5940ba76422ef180fe4177709d1290878d13e
@@ -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
@@ -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
@@ -1,8 +1,11 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
+ gem 'actionview'
4
5
  gem 'activerecord'
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'
@@ -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'
@@ -1,6 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
+ gem 'actionview', '~> 5.0.0'
4
5
  gem 'activerecord', '~> 5.0.0'
5
6
 
6
7
  platforms :jruby do
@@ -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
@@ -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
- # and use the object as block argument and retun value.
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.fields[rename ? rename.to_sym : @field] ||= self
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.fields[al.to_sym] ||= self }
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
- definition = self
278
- @klass.scope(:search_for, proc { |query, options|
279
- klass = definition.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 = "&#9650;&nbsp;#{as}"
50
+ as = "&#9650;&nbsp;".html_safe + as
51
51
  css_classes << "ascending"
52
52
  else
53
- as = "&#9660;&nbsp;#{as}"
53
+ as = "&#9660;&nbsp;".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
- a_link(as, html_escape(url_for(url_options)),html_options)
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
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "4.0.0"
2
+ VERSION = "4.1.0"
3
3
  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.uniq.complete_for('int =').length.should > 0
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
- it "should accept 3 hours ago as date format" do
202
- @class.search_for('timestamp > "3 hours ago"').length.should == 2
203
- end
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
- it "should accept 1 month ago as date format" do
206
- @class.search_for('date > "1 month ago"').length.should == 3
207
- end
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 == 4
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 ActionViewHelperStubs
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 add no styling by default" do
75
- should_receive(:url_for)
76
- should_receive(:a_link).with('Field', anything, hash_excluding(:class))
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('&#9650;&nbsp;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">&#9650;&nbsp;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('&#9660;&nbsp;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">&#9660;&nbsp;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
@@ -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)
@@ -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(:scope)
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(Class.new(ActiveRecord::Base))
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.0.0
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: 2016-12-05 00:00:00.000000000 Z
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