scoped_search 2.3.6 → 2.3.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -149,9 +149,9 @@ module ScopedSearch
149
149
  register_complete_for! unless klass.respond_to?(:complete_for)
150
150
 
151
151
  end
152
-
152
+
153
153
  attr_accessor :profile, :default_order
154
-
154
+
155
155
  def fields
156
156
  @profile ||= :default
157
157
  @profile_fields[@profile] ||= {}
@@ -176,11 +176,11 @@ module ScopedSearch
176
176
  def operator_by_field_name(name)
177
177
  field = field_by_name(name)
178
178
  return [] if field.nil?
179
- return field.operators if field.operators
180
- return ['= ', '!= '] if field.set?
181
- return ['= ', '> ', '< ', '<= ', '>= ','!= '] if field.numerical?
182
- return ['= ', '!= ', '~ ', '!~ '] if field.textual?
183
- return ['= ', '> ', '< '] if field.temporal?
179
+ return field.operators if field.operators
180
+ return ['= ', '!= '] if field.set?
181
+ return ['= ', '> ', '< ', '<= ', '>= ','!= ', '^ ', '!^ '] if field.numerical?
182
+ return ['= ', '!= ', '~ ', '!~ ', '^ ', '!^ '] if field.textual?
183
+ return ['= ', '> ', '< '] if field.temporal?
184
184
  raise ScopedSearch::QueryNotSupported, "could not verify '#{name}' type, this can be a result of a definition error"
185
185
  end
186
186
 
@@ -230,8 +230,8 @@ module ScopedSearch
230
230
  when 2
231
231
  @klass.named_scope(:search_for, lambda { |*args| ScopedSearch::QueryBuilder.build_query(self, args[0], args[1]) })
232
232
  when 3
233
- @klass.scope(:search_for, lambda { |*args|
234
- find_options = ScopedSearch::QueryBuilder.build_query(self, args[0], args[1])
233
+ @klass.scope(:search_for, lambda { |*args|
234
+ find_options = ScopedSearch::QueryBuilder.build_query(self, args[0], args[1])
235
235
  search_scope = @klass.scoped
236
236
  search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
237
237
  search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
@@ -101,7 +101,8 @@ module ScopedSearch
101
101
 
102
102
  # A hash that maps the operators of the query language with the corresponding SQL operator.
103
103
  SQL_OPERATORS = { :eq =>'=', :ne => '<>', :like => 'LIKE', :unlike => 'NOT LIKE',
104
- :gt => '>', :lt =>'<', :lte => '<=', :gte => '>=' }
104
+ :gt => '>', :lt =>'<', :lte => '<=', :gte => '>=',
105
+ :in => 'IN',:notin => 'NOT IN' }
105
106
 
106
107
  # Return the SQL operator to use given an operator symbol and field definition.
107
108
  #
@@ -170,9 +171,16 @@ module ScopedSearch
170
171
  end
171
172
 
172
173
  # Validate the key name is in the set and translate the value to the set value.
174
+ def translate_value(field, value)
175
+ translated_value = field.complete_value[value.to_sym]
176
+ raise ScopedSearch::QueryNotSupported, "'#{field.field}' should be one of '#{field.complete_value.keys.join(', ')}', but the query was '#{value}'" if translated_value.nil?
177
+ translated_value
178
+ end
179
+
180
+ # A 'set' is group of possible values, for example a status might be "on", "off" or "unknown" and the database representation
181
+ # could be for example a numeric value. This method will validate the input and translate it into the database representation.
173
182
  def set_test(field, operator,value, &block)
174
- set_value = field.complete_value[value.to_sym]
175
- raise ScopedSearch::QueryNotSupported, "'#{field.field}' should be one of '#{field.complete_value.keys.join(', ')}', but the query was '#{value}'" if set_value.nil?
183
+ set_value = translate_value(field, value)
176
184
  raise ScopedSearch::QueryNotSupported, "Operator '#{operator}' not supported for '#{field.field}'" unless [:eq,:ne].include?(operator)
177
185
  negate = ''
178
186
  if [true,false].include?(set_value)
@@ -205,6 +213,10 @@ module ScopedSearch
205
213
  if [:like, :unlike].include?(operator)
206
214
  yield(:parameter, (value !~ /^\%|\*/ && value !~ /\%|\*$/) ? "%#{value}%" : value.tr_s('%*', '%'))
207
215
  return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
216
+ elsif [:in, :notin].include?(operator)
217
+ value.split(',').collect { |v| yield(:parameter, field.set? ? translate_value(field, v) : v.strip) }
218
+ value = value.split(',').collect { "?" }.join(",")
219
+ return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} (#{value})"
208
220
  elsif field.temporal?
209
221
  return datetime_test(field, operator, value, &block)
210
222
  elsif field.set?
@@ -408,7 +420,7 @@ module ScopedSearch
408
420
  # Defines the to_sql method for AST AND/OR operators
409
421
  module LogicalOperatorNode
410
422
  def to_sql(builder, definition, &block)
411
- fragments = children.map { |c| c.to_sql(builder, definition, &block) }.compact.map { |sql| "(#{sql})" }
423
+ fragments = children.map { |c| c.to_sql(builder, definition, &block) }.map { |sql| "(#{sql})" unless sql.blank? }.compact
412
424
  fragments.empty? ? nil : "#{fragments.join(" #{operator.to_s.upcase} ")}"
413
425
  end
414
426
  end
@@ -11,7 +11,7 @@ module ScopedSearch::QueryLanguage::Parser
11
11
  LOGICAL_INFIX_OPERATORS = [:and, :or]
12
12
  LOGICAL_PREFIX_OPERATORS = [:not]
13
13
  NULL_PREFIX_OPERATORS = [:null, :notnull]
14
- COMPARISON_OPERATORS = [:eq, :ne, :gt, :gte, :lt, :lte, :like, :unlike]
14
+ COMPARISON_OPERATORS = [:eq, :ne, :gt, :gte, :lt, :lte, :like, :unlike, :in, :notin]
15
15
  ALL_INFIX_OPERATORS = LOGICAL_INFIX_OPERATORS + COMPARISON_OPERATORS
16
16
  ALL_PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + COMPARISON_OPERATORS + NULL_PREFIX_OPERATORS
17
17
 
@@ -99,11 +99,25 @@ module ScopedSearch::QueryLanguage::Parser
99
99
  end
100
100
  end
101
101
 
102
- # Parses a single value.
102
+ # Parse values in the format (val, val, val)
103
+ def parse_multiple_values
104
+ next_token if peek_token == :lparen #skip :lparen
105
+ value = []
106
+ value << current_token if String === next_token until peek_token.nil? || peek_token == :rparen
107
+ next_token if peek_token == :rparen # consume the :rparen
108
+ value.join(',')
109
+ end
110
+
103
111
  # This can either be a constant value or a field name.
104
112
  def parse_value
105
- raise ScopedSearch::QueryNotSupported, "Value expected but found #{peek_token.inspect}" unless String === peek_token
106
- ScopedSearch::QueryLanguage::AST::LeafNode.new(next_token)
113
+ if String === peek_token
114
+ ScopedSearch::QueryLanguage::AST::LeafNode.new(next_token)
115
+ elsif ([:in, :notin].include? current_token)
116
+ value = parse_multiple_values()
117
+ ScopedSearch::QueryLanguage::AST::LeafNode.new(value)
118
+ else
119
+ raise ScopedSearch::QueryNotSupported, "Value expected but found #{peek_token.inspect}"
120
+ end
107
121
  end
108
122
 
109
123
  protected
@@ -7,7 +7,7 @@ module ScopedSearch::QueryLanguage::Tokenizer
7
7
 
8
8
  # Every operator the language supports.
9
9
  OPERATORS = { '&' => :and, '|' => :or, '&&' => :and, '||' => :or, '-'=> :not, '!' => :not, '~' => :like, '!~' => :unlike,
10
- '=' => :eq, '==' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
10
+ '=' => :eq, '==' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte, '^' => :in, '!^' => :notin }
11
11
 
12
12
  # Tokenizes the string and returns the result as an array of tokens.
13
13
  def tokenize
@@ -41,7 +41,7 @@ module ScopedSearch::QueryLanguage::Tokenizer
41
41
  when '('; yield(:lparen)
42
42
  when ')'; yield(:rparen)
43
43
  when ','; yield(:comma)
44
- when /\&|\||=|<|>|!|~|-/; tokenize_operator(&block)
44
+ when /\&|\||=|<|>|\^|!|~|-/; tokenize_operator(&block)
45
45
  when '"'; tokenize_quoted_keyword(&block)
46
46
  else; tokenize_keyword(&block)
47
47
  end
data/lib/scoped_search.rb CHANGED
@@ -14,7 +14,7 @@ module ScopedSearch
14
14
 
15
15
  # The current scoped_search version. Do not change thisvalue by hand,
16
16
  # because it will be updated automatically by the gem release script.
17
- VERSION = "2.3.6"
17
+ VERSION = "2.3.7"
18
18
 
19
19
  # The ClassMethods module will be included into the ActiveRecord::Base class
20
20
  # to add the <tt>ActiveRecord::Base.scoped_search</tt> method and the
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
 
4
4
  # Do not change the version and date fields by hand. This will be done
5
5
  # automatically by the gem release script.
6
- s.version = "2.3.6"
7
- s.date = "2011-11-13"
6
+ s.version = "2.3.7"
7
+ s.date = "2012-04-30"
8
8
 
9
9
  s.summary = "Easily search you ActiveRecord models with a simple query language using a named scope."
10
10
  s.description = <<-EOS
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
26
26
 
27
27
  s.add_runtime_dependency('activerecord', '>= 2.1.0')
28
28
  s.add_development_dependency('rspec', '~> 2.0')
29
+ s.add_development_dependency('rake')
29
30
 
30
31
  s.add_development_dependency('sqlite3-ruby')
31
32
 
@@ -47,11 +47,11 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
47
47
  end
48
48
 
49
49
  it "should complete the string comparators" do
50
- Foo.complete_for('string ').should =~ (["string != ", "string !~ ", "string = ", "string ~ "])
50
+ Foo.complete_for('string ').should =~ (["string != ", "string !^ ", "string !~ ", "string = ", "string ^ ", "string ~ "])
51
51
  end
52
52
 
53
53
  it "should complete the numerical comparators" do
54
- Foo.complete_for('int ').should =~ (["int != ", "int < ", "int <= ", "int = ", "int > ", "int >= "])
54
+ Foo.complete_for('int ').should =~ (["int != ", "int !^ ", "int < ", "int <= ", "int = ", "int > ", "int >= ", "int ^ "])
55
55
  end
56
56
 
57
57
  it "should complete the temporal (date) comparators" do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15
4
+ hash: 13
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
8
  - 3
9
- - 6
10
- version: 2.3.6
9
+ - 7
10
+ version: 2.3.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - Willem van Bergen
@@ -16,12 +16,10 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2011-11-13 00:00:00 +02:00
19
+ date: 2012-04-30 00:00:00 +03:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency
23
- name: activerecord
24
- prerelease: false
25
23
  requirement: &id001 !ruby/object:Gem::Requirement
26
24
  none: false
27
25
  requirements:
@@ -34,10 +32,10 @@ dependencies:
34
32
  - 0
35
33
  version: 2.1.0
36
34
  type: :runtime
35
+ name: activerecord
36
+ prerelease: false
37
37
  version_requirements: *id001
38
38
  - !ruby/object:Gem::Dependency
39
- name: rspec
40
- prerelease: false
41
39
  requirement: &id002 !ruby/object:Gem::Requirement
42
40
  none: false
43
41
  requirements:
@@ -49,10 +47,10 @@ dependencies:
49
47
  - 0
50
48
  version: "2.0"
51
49
  type: :development
50
+ name: rspec
51
+ prerelease: false
52
52
  version_requirements: *id002
53
53
  - !ruby/object:Gem::Dependency
54
- name: sqlite3-ruby
55
- prerelease: false
56
54
  requirement: &id003 !ruby/object:Gem::Requirement
57
55
  none: false
58
56
  requirements:
@@ -63,7 +61,23 @@ dependencies:
63
61
  - 0
64
62
  version: "0"
65
63
  type: :development
64
+ name: rake
65
+ prerelease: false
66
66
  version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ type: :development
78
+ name: sqlite3-ruby
79
+ prerelease: false
80
+ version_requirements: *id004
67
81
  description: " Scoped search makes it easy to search your ActiveRecord-based models.\n \n It will create a named scope :search_for that can be called with a query string. It will build an SQL query using\n the provided query string and a definition that specifies on what fields to search. Because the functionality is\n built on named_scope, the result of the search_for call can be used like any other named_scope, so it can be\n chained with another scope or combined with will_paginate.\n \n Because it uses standard SQL, it does not require any setup, indexers or daemons. This makes scoped_search\n suitable to quickly add basic search functionality to your application with little hassle. On the other hand,\n it may not be the best choice if it is going to be used on very large datasets or by a large user base.\n"
68
82
  email:
69
83
  - willem@railsdoctors.com