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 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