scoped_search 2.7.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,17 +1,16 @@
1
1
  module ScopedSearch
2
2
 
3
-
4
- LOGICAL_INFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::LOGICAL_INFIX_OPERATORS
5
- LOGICAL_PREFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::LOGICAL_PREFIX_OPERATORS
6
- NULL_PREFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::NULL_PREFIX_OPERATORS
7
- NULL_PREFIX_COMPLETER = ['has']
8
- COMPARISON_OPERATORS = ScopedSearch::QueryLanguage::Parser::COMPARISON_OPERATORS
9
- PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_OPERATORS
10
-
11
3
  # The AutoCompleteBuilder class builds suggestions to complete query based on
12
4
  # the query language syntax.
13
5
  class AutoCompleteBuilder
14
6
 
7
+ LOGICAL_INFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::LOGICAL_INFIX_OPERATORS
8
+ LOGICAL_PREFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::LOGICAL_PREFIX_OPERATORS
9
+ NULL_PREFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::NULL_PREFIX_OPERATORS
10
+ NULL_PREFIX_COMPLETER = ['has']
11
+ COMPARISON_OPERATORS = ScopedSearch::QueryLanguage::Parser::COMPARISON_OPERATORS
12
+ PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_OPERATORS
13
+
15
14
  attr_reader :ast, :definition, :query, :tokens
16
15
 
17
16
  # This method will parse the query string and build suggestion list using the
@@ -174,10 +173,14 @@ module ScopedSearch
174
173
  quoted_table = field.key_klass.connection.quote_table_name(field.key_klass.table_name)
175
174
  quoted_field = field.key_klass.connection.quote_column_name(field.key_field)
176
175
  field_name = "#{quoted_table}.#{quoted_field}"
177
- select_clause = "DISTINCT #{field_name}"
178
- opts = value_conditions(field_name, val).merge(:select => select_clause, :limit => 20)
179
176
 
180
- field.key_klass.all(opts).map(&field.key_field).compact.map{ |f| "#{name}.#{f} "}
177
+ field.key_klass
178
+ .where(value_conditions(field_name, val))
179
+ .select("DISTINCT #{field_name}")
180
+ .limit(20)
181
+ .map(&field.key_field)
182
+ .compact
183
+ .map { |f| "#{name}.#{f} " }
181
184
  end
182
185
 
183
186
  # this method auto-completes values of fields that have a :complete_value marker
@@ -197,10 +200,13 @@ module ScopedSearch
197
200
  return complete_date_value if field.temporal?
198
201
  return complete_key_value(field, token, val) if field.key_field
199
202
 
200
- opts = value_conditions(field.quoted_field, val)
201
- opts.merge!(:limit => 20, :select => "DISTINCT #{field.quoted_field}")
202
-
203
- return completer_scope(field).all(opts).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
203
+ completer_scope(field)
204
+ .where(value_conditions(field.quoted_field, val))
205
+ .select("DISTINCT #{field.quoted_field}")
206
+ .limit(20)
207
+ .map(&field.field)
208
+ .compact
209
+ .map { |v| v.to_s =~ /\s/ ? "\"#{v}\"" : v }
204
210
  end
205
211
 
206
212
  def completer_scope(field)
@@ -215,7 +221,7 @@ module ScopedSearch
215
221
  end
216
222
  # date value completer
217
223
  def complete_date_value
218
- options =[]
224
+ options = []
219
225
  options << '"30 minutes ago"'
220
226
  options << '"1 hour ago"'
221
227
  options << '"2 hours ago"'
@@ -233,34 +239,34 @@ module ScopedSearch
233
239
  # complete values in a key-value schema
234
240
  def complete_key_value(field, token, val)
235
241
  key_name = token.sub(/^.*\./,"")
236
- key_opts = value_conditions(field.quoted_field,val).merge(:conditions => {field.key_field => key_name})
237
- key_klass = field.key_klass.first(key_opts)
242
+ key_klass = field.key_klass.where(field.key_field => key_name).first
238
243
  raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil?
239
244
 
240
- opts = {:limit => 20, :select => "DISTINCT #{field.quoted_field}"}
241
- if(field.key_klass != field.klass)
242
- key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym
243
- fk = field.klass.reflections[key].association_foreign_key.to_sym
244
- opts.merge!(:conditions => {fk => key_klass.id})
245
- else
246
- opts.merge!(key_opts)
245
+ query = completer_scope(field)
246
+
247
+ if field.key_klass != field.klass
248
+ key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym
249
+ fk = field.klass.reflections[key].association_foreign_key.to_sym
250
+ query = query.where(fk => key_klass.id)
247
251
  end
248
- return completer_scope(field).all(opts).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
252
+
253
+ query
254
+ .where(value_conditions(field, val))
255
+ .select("DISTINCT #{field.quoted_field}")
256
+ .limit(20)
257
+ .map(&field.field)
258
+ .compact
259
+ .map { |v| v.to_s =~ /\s/ ? "\"#{v}\"" : v }
249
260
  end
250
261
 
251
- #this method returns conditions for selecting completion from partial value
252
- def value_conditions(field_name, val)
253
- return val.blank? ? {} : {:conditions => "#{field_name} LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%')}
262
+ # This method returns conditions for selecting completion from partial value
263
+ def value_conditions(field, val)
264
+ val.blank? ? nil : "#{field.quoted_field} LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%')
254
265
  end
255
266
 
256
267
  # This method complete infix operators by field type
257
268
  def complete_operator(node)
258
269
  definition.operator_by_field_name(node.value)
259
270
  end
260
-
261
271
  end
262
-
263
272
  end
264
-
265
- # Load lib files
266
- require 'scoped_search/query_builder'
@@ -84,7 +84,11 @@ module ScopedSearch
84
84
  if klass.columns_hash.has_key?(field.to_s)
85
85
  klass.columns_hash[field.to_s]
86
86
  else
87
- raise ActiveRecord::UnknownAttributeError, "#{klass.inspect} doesn't have column #{field.inspect}."
87
+ if "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}".to_f < 4.1
88
+ raise ActiveRecord::UnknownAttributeError, "#{klass.inspect} doesn't have column #{field.inspect}."
89
+ else
90
+ raise ActiveRecord::UnknownAttributeError.new( klass, field )
91
+ end
88
92
  end
89
93
  end
90
94
  end
@@ -243,8 +247,6 @@ module ScopedSearch
243
247
  definition = self
244
248
  if @klass.ancestors.include?(ActiveRecord::Base)
245
249
  case ActiveRecord::VERSION::MAJOR
246
- when 2
247
- @klass.named_scope(:search_for, lambda { |*args| ScopedSearch::QueryBuilder.build_query(definition, args[0], args[1]) })
248
250
  when 3
249
251
  @klass.scope(:search_for, lambda { |*args|
250
252
  find_options = ScopedSearch::QueryBuilder.build_query(definition, args[0], args[1])
@@ -258,7 +260,7 @@ module ScopedSearch
258
260
  when 4
259
261
  @klass.scope(:search_for, lambda { |*args|
260
262
  find_options = ScopedSearch::QueryBuilder.build_query(definition, args[0], args[1])
261
- search_scope = @klass.all
263
+ search_scope = @klass
262
264
  search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
263
265
  search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
264
266
  search_scope = search_scope.references(find_options[:include]) if find_options[:include]
@@ -270,7 +272,7 @@ module ScopedSearch
270
272
  raise "This ActiveRecord version is currently not supported!"
271
273
  end
272
274
  else
273
- raise "Currently, only ActiveRecord 2.1 or higher is supported!"
275
+ raise "Currently, only ActiveRecord 3 or newer is supported!"
274
276
  end
275
277
  end
276
278
 
@@ -479,20 +479,6 @@ module ScopedSearch
479
479
  # The MysqlAdapter makes sure that case sensitive comparisons are used
480
480
  # when using the (not) equals operator, regardless of the field's
481
481
  # collation setting.
482
- class MysqlAdapter < ScopedSearch::QueryBuilder
483
-
484
- # Patches the default <tt>sql_operator</tt> method to add
485
- # <tt>BINARY</tt> after the equals and not equals operator to force
486
- # case-sensitive comparisons.
487
- def sql_operator(operator, field)
488
- if [:ne, :eq].include?(operator) && field.textual?
489
- "#{SQL_OPERATORS[operator]} BINARY"
490
- else
491
- super(operator, field)
492
- end
493
- end
494
- end
495
-
496
482
  class Mysql2Adapter < ScopedSearch::QueryBuilder
497
483
  # Patches the default <tt>sql_operator</tt> method to add
498
484
  # <tt>BINARY</tt> after the equals and not equals operator to force
@@ -506,6 +492,8 @@ module ScopedSearch
506
492
  end
507
493
  end
508
494
 
495
+ MysqlAdapter = Mysql2Adapter
496
+
509
497
  # The PostgreSQLAdapter make sure that searches are case sensitive when
510
498
  # using the like/unlike operators, by using the PostrgeSQL-specific
511
499
  # <tt>ILIKE operator</tt> instead of <tt>LIKE</tt>.
@@ -525,7 +513,7 @@ module ScopedSearch
525
513
  end
526
514
  end
527
515
 
528
- # Switches out the default LIKE operator in the default <tt>sql_operator</tt>
516
+ # Switches out the default LIKE operator in the default <tt>sql_operator</tt>
529
517
  # method for ILIKE or @@ if full text searching is enabled.
530
518
  def sql_operator(operator, field)
531
519
  raise ScopedSearch::QueryNotSupported, "the operator '#{operator}' is not supported for field type '#{field.type}'" if [:like, :unlike].include?(operator) and !field.textual?
@@ -548,25 +536,6 @@ module ScopedSearch
548
536
  sql
549
537
  end
550
538
  end
551
-
552
- # The Oracle adapter also requires some tweaks to make the case insensitive LIKE work.
553
- class OracleEnhancedAdapter < ScopedSearch::QueryBuilder
554
-
555
- def sql_test(field, operator, value, lhs, &block) # :yields: finder_option_type, value
556
- if field.key_field
557
- yield(:parameter, lhs.sub(/^.*\./,''))
558
- end
559
- if field.textual? && [:like, :unlike].include?(operator)
560
- yield(:parameter, (value !~ /^\%|\*/ && value !~ /\%|\*$/) ? "%#{value}%" : value.to_s.tr_s('%*', '%'))
561
- return "LOWER(#{field.to_sql(operator, &block)}) #{self.sql_operator(operator, field)} LOWER(?)"
562
- elsif field.temporal?
563
- return datetime_test(field, operator, value, &block)
564
- else
565
- yield(:parameter, value)
566
- return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
567
- end
568
- end
569
- end
570
539
  end
571
540
 
572
541
  # Include the modules into the corresponding classes
@@ -31,8 +31,5 @@ module ScopedSearch::QueryLanguage
31
31
  compiler = self.new(str)
32
32
  compiler.tokenize
33
33
  end
34
-
35
34
  end
36
35
  end
37
-
38
-
@@ -1,4 +1,3 @@
1
-
2
1
  module ScopedSearch
3
2
  module RailsHelper
4
3
  # Creates a link that alternates between ascending and descending.
@@ -36,7 +35,7 @@ module ScopedSearch
36
35
 
37
36
  if selected
38
37
  css_classes = html_options[:class] ? html_options[:class].split(" ") : []
39
- if new_sort == ascend
38
+ if new_sort == ascend
40
39
  options[:as] = "&#9650;&nbsp;#{options[:as]}"
41
40
  css_classes << "ascending"
42
41
  else
@@ -124,52 +123,51 @@ module ScopedSearch
124
123
  end
125
124
 
126
125
  def auto_complete_field_jquery(method, url, options = {})
127
- function = <<-EOF
128
- $.widget( "custom.catcomplete", $.ui.autocomplete, {
129
- _renderMenu: function( ul, items ) {
130
- var self = this,
131
- currentCategory = "";
132
- $.each( items, function( index, item ) {
133
- if ( item.category != undefined && item.category != currentCategory ) {
134
- ul.append( "<li class='ui-autocomplete-category'>" + item.category + "</li>" );
135
- currentCategory = item.category;
136
- }
137
- if ( item.error != undefined ) {
138
- ul.append( "<li class='ui-autocomplete-error'>" + item.error + "</li>" );
139
- }
140
- if( item.completed != undefined ) {
141
- $( "<li></li>" ).data( "item.autocomplete", item )
142
- .append( "<a>" + "<strong class='ui-autocomplete-completed'>" + item.completed + "</strong>" + item.part + "</a>" )
143
- .appendTo( ul );
144
- } else {
145
- if(typeof(self._renderItemData) === "function") {
146
- self._renderItemData( ul, item );
126
+ function = <<-JAVASCRIPT
127
+ $.widget( "custom.catcomplete", $.ui.autocomplete, {
128
+ _renderMenu: function( ul, items ) {
129
+ var self = this,
130
+ currentCategory = "";
131
+ $.each( items, function( index, item ) {
132
+ if ( item.category != undefined && item.category != currentCategory ) {
133
+ ul.append( "<li class='ui-autocomplete-category'>" + item.category + "</li>" );
134
+ currentCategory = item.category;
135
+ }
136
+ if ( item.error != undefined ) {
137
+ ul.append( "<li class='ui-autocomplete-error'>" + item.error + "</li>" );
138
+ }
139
+ if( item.completed != undefined ) {
140
+ $( "<li></li>" ).data( "item.autocomplete", item )
141
+ .append( "<a>" + "<strong class='ui-autocomplete-completed'>" + item.completed + "</strong>" + item.part + "</a>" )
142
+ .appendTo( ul );
147
143
  } else {
148
- self._renderItem( ul, item );
144
+ if(typeof(self._renderItemData) === "function") {
145
+ self._renderItemData( ul, item );
146
+ } else {
147
+ self._renderItem( ul, item );
148
+ }
149
149
  }
150
- }
151
- });
152
- }
153
- });
154
-
155
- $("##{method}")
156
- .catcomplete({
157
- source: function( request, response ) { $.getJSON( "#{url}", { #{method}: request.term }, response ); },
158
- minLength: #{options[:min_length] || 0},
159
- delay: #{options[:delay] || 200},
160
- select: function(event, ui) { $( this ).catcomplete( "search" , ui.item.value); },
161
- search: function(event, ui) { $(".auto_complete_clear").hide(); },
162
- open: function(event, ui) { $(".auto_complete_clear").show(); }
163
- });
164
-
165
- $("##{method}").bind( "focus", function( event ) {
166
- if( $( this )[0].value == "" ) {
167
- $( this ).catcomplete( "search" );
168
- }
169
- });
170
-
171
- EOF
172
-
150
+ });
151
+ }
152
+ });
153
+
154
+ $("##{method}")
155
+ .catcomplete({
156
+ source: function( request, response ) { $.getJSON( "#{url}", { #{method}: request.term }, response ); },
157
+ minLength: #{options[:min_length] || 0},
158
+ delay: #{options[:delay] || 200},
159
+ select: function(event, ui) { $( this ).catcomplete( "search" , ui.item.value); },
160
+ search: function(event, ui) { $(".auto_complete_clear").hide(); },
161
+ open: function(event, ui) { $(".auto_complete_clear").show(); }
162
+ });
163
+
164
+ $("##{method}").bind( "focus", function( event ) {
165
+ if( $( this )[0].value == "" ) {
166
+ $( this ).catcomplete( "search" );
167
+ }
168
+ });
169
+
170
+ JAVASCRIPT
173
171
 
174
172
  javascript_tag(function)
175
173
  end
@@ -218,6 +216,7 @@ module ScopedSearch
218
216
  text_field_tag(method, val, options) + auto_complete_clear_value_button(method) +
219
217
  auto_complete_field_jquery(method, url, completion_options)
220
218
  end
219
+ deprecate :auto_complete_field_tag_jquery, :auto_complete_field_tag, :auto_complete_result
221
220
 
222
221
  end
223
222
  end
@@ -1,8 +1,13 @@
1
- if defined?(ActionController)
2
- require "scoped_search/rails_helper"
3
- ActionController::Base.helper(ScopedSearch::RailsHelper)
4
- end
1
+ require 'scoped_search/engine'
2
+
3
+ module ScopedSearch
4
+ class Railtie < ::Rails::Railtie
5
5
 
6
- if defined?(::Rails::Engine)
7
- require 'scoped_search/engine'
6
+ initializer "scoped_search.setup_rails_helper" do |app|
7
+ ActiveSupport.on_load :action_controller do
8
+ require "scoped_search/rails_helper"
9
+ ActionController::Base.helper(ScopedSearch::RailsHelper)
10
+ end
11
+ end
12
+ end
8
13
  end
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "2.7.1"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -12,26 +12,27 @@ Gem::Specification.new do |gem|
12
12
  gem.summary = %q{Easily search you ActiveRecord models with a simple query language using a named scope}
13
13
  gem.description = <<-EOS
14
14
  Scoped search makes it easy to search your ActiveRecord-based models.
15
-
15
+
16
16
  It will create a named scope :search_for that can be called with a query string. It will build an SQL query using
17
17
  the provided query string and a definition that specifies on what fields to search. Because the functionality is
18
18
  built on named_scope, the result of the search_for call can be used like any other named_scope, so it can be
19
19
  chained with another scope or combined with will_paginate.
20
-
20
+
21
21
  Because it uses standard SQL, it does not require any setup, indexers or daemons. This makes scoped_search
22
22
  suitable to quickly add basic search functionality to your application with little hassle. On the other hand,
23
23
  it may not be the best choice if it is going to be used on very large datasets or by a large user base.
24
24
  EOS
25
-
25
+
26
26
  gem.files = `git ls-files`.split($/)
27
27
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
28
28
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
29
29
  gem.require_paths = ["lib"]
30
30
 
31
- gem.add_runtime_dependency('activerecord', '>= 2.1.0')
32
- gem.add_development_dependency('rspec', '~> 2.0')
33
- gem.add_development_dependency('rake')
31
+ gem.required_ruby_version = '>= 1.9.3'
32
+ gem.add_runtime_dependency('activerecord', '>= 3.0.0')
33
+ gem.add_development_dependency('rspec', '~> 3.0')
34
+ gem.add_development_dependency('rake')
34
35
 
35
36
  gem.rdoc_options << '--title' << gem.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
36
- gem.extra_rdoc_files = ['README.rdoc']
37
+ gem.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.rdoc', 'CONTRIBUTING.rdoc', 'LICENSE']
37
38
  end
@@ -44,54 +44,15 @@ describe ScopedSearch, "API" do
44
44
  @class.should respond_to(:search_for)
45
45
  end
46
46
 
47
- if ActiveRecord::VERSION::MAJOR == 2
48
-
49
- it "should return a ActiveRecord::NamedScope::Scope instance" do
50
- @class.search_for('query').class.should eql(ActiveRecord::NamedScope::Scope)
51
- end
52
-
53
- elsif ActiveRecord::VERSION::MAJOR == 3
54
-
47
+ if ActiveRecord::VERSION::MAJOR == 3
55
48
  it "should return a ActiveRecord::Relation instance" do
56
49
  @class.search_for('query').class.should eql(ActiveRecord::Relation)
57
50
  end
58
-
51
+
59
52
  elsif ActiveRecord::VERSION::MAJOR == 4
60
-
61
53
  it "should return a ActiveRecord::Relation instance" do
62
54
  @class.search_for('query').class.superclass.should eql(ActiveRecord::Relation)
63
55
  end
64
-
65
56
  end
66
57
  end
67
-
68
- context 'having backwards compatibility' do
69
-
70
- before(:each) do
71
- class ::Foo < ActiveRecord::Base
72
- belongs_to :bar
73
- end
74
- end
75
-
76
- after(:each) do
77
- Object.send :remove_const, :Foo
78
- end
79
-
80
- it "should respond to :searchable_on" do
81
- Foo.should respond_to(:searchable_on)
82
- end
83
-
84
- it "should create a Field instance for every argument" do
85
- ScopedSearch::Definition::Field.should_receive(:new).exactly(3).times
86
- Foo.searchable_on(:field_1, :field_2, :field_3)
87
- end
88
-
89
- it "should create a Field with a valid relation when using the underscore notation" do
90
- ScopedSearch::Definition::Field.should_receive(:new).with(
91
- instance_of(ScopedSearch::Definition), hash_including(:in => :bar, :on => :related_field))
92
- Foo.searchable_on(:bar_related_field)
93
- end
94
-
95
- end
96
-
97
58
  end