scoped_search 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +0 -8
- data/CHANGELOG.rdoc +20 -0
- data/Gemfile.activerecord42 +3 -3
- data/Gemfile.activerecord50 +4 -4
- data/README.rdoc +3 -0
- data/lib/scoped_search.rb +1 -0
- data/lib/scoped_search/auto_complete_builder.rb +3 -3
- data/lib/scoped_search/definition.rb +73 -58
- data/lib/scoped_search/query_builder.rb +17 -3
- data/lib/scoped_search/rails_helper.rb +27 -176
- data/lib/scoped_search/validators.rb +7 -0
- data/lib/scoped_search/version.rb +1 -1
- data/scoped_search.gemspec +2 -2
- data/spec/integration/auto_complete_spec.rb +10 -6
- data/spec/integration/key_value_querying_spec.rb +1 -1
- data/spec/integration/ordinal_querying_spec.rb +0 -10
- data/spec/integration/relation_querying_spec.rb +63 -10
- data/spec/integration/string_querying_spec.rb +1 -1
- data/spec/unit/ast_spec.rb +3 -3
- data/spec/unit/definition_spec.rb +36 -0
- data/spec/unit/query_builder_spec.rb +22 -0
- data/spec/unit/rails_helper_spec.rb +20 -0
- data/spec/unit/validators_spec.rb +27 -0
- metadata +23 -23
- data/Gemfile.activerecord32 +0 -16
- data/Gemfile.activerecord40 +0 -16
- data/Gemfile.activerecord41 +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df5ac5265b984d6208df025b20dd336950d9bdbb
|
4
|
+
data.tar.gz: 3643c8835b43e6333e24d44473b1b29c3ec7ac91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d760759a261eda13744cad755263a056213315859bc925e5ea814cd0e214e19941dc6b3e07572f80b0bf10058359d6895f663a9249b894fa53297c9f81a7efa
|
7
|
+
data.tar.gz: 4b9580f1b55aeaf6f74b64847318361d58f1e3a7477376b1db4921861ec95db3ba8594f7a6f5915f097fdd9313b2fb9bd30b26afc53c2199d3f367d8fa54c336
|
data/.travis.yml
CHANGED
@@ -11,7 +11,6 @@ script:
|
|
11
11
|
- bundle exec rake
|
12
12
|
|
13
13
|
rvm:
|
14
|
-
- "1.9"
|
15
14
|
- "2.0"
|
16
15
|
- "2.1"
|
17
16
|
- "2.2.2"
|
@@ -21,9 +20,6 @@ rvm:
|
|
21
20
|
- jruby-head
|
22
21
|
|
23
22
|
gemfile:
|
24
|
-
- Gemfile.activerecord32
|
25
|
-
- Gemfile.activerecord40
|
26
|
-
- Gemfile.activerecord41
|
27
23
|
- Gemfile.activerecord42
|
28
24
|
- Gemfile.activerecord50
|
29
25
|
|
@@ -33,11 +29,7 @@ matrix:
|
|
33
29
|
- rvm: jruby-head
|
34
30
|
- rvm: jruby-19mode
|
35
31
|
exclude:
|
36
|
-
- rvm: "1.9"
|
37
|
-
gemfile: Gemfile.activerecord50
|
38
32
|
- rvm: "2.0"
|
39
33
|
gemfile: Gemfile.activerecord50
|
40
34
|
- rvm: "2.1"
|
41
35
|
gemfile: Gemfile.activerecord50
|
42
|
-
- rvm: "2.3.1"
|
43
|
-
gemfile: Gemfile.activerecord32
|
data/CHANGELOG.rdoc
CHANGED
@@ -8,6 +8,26 @@ Please add an entry to the "Unreleased changes" section in your pull requests.
|
|
8
8
|
|
9
9
|
*Nothing yet*
|
10
10
|
|
11
|
+
=== Version 4.0.0
|
12
|
+
|
13
|
+
- Drop support for Ruby 1.9
|
14
|
+
- Drop support for ActiveRecord 3, 4.0, and 4.1
|
15
|
+
- `scoped_search` registration deprecates `:in` in favor of `:relation`, and
|
16
|
+
`:alias` in favor of `aliases: [..]` to avoid clash with Ruby reserved words.
|
17
|
+
- `sort` helper: any HTML options must now be given as a keyword argument,
|
18
|
+
instead of the third argument, i.e. `html_options: {..}`
|
19
|
+
- `sort` helper now takes hash of `url_options`, defaulting to an unfiltered
|
20
|
+
`params`. Call `params.permit(..)` and pass in the result to safely generate
|
21
|
+
URLs and prevent non-sanitized errors under Rails 5.
|
22
|
+
- Auto completion: remove autocomplete_* JavaScript helpers in favor of jQuery
|
23
|
+
method provided via asset pipeline. Call `$(..).scopedSearch` on autocomplete
|
24
|
+
text boxes to activate.
|
25
|
+
- Auto completion: escape quotes in listed values (#138)
|
26
|
+
- Add :validator option to definitions to validate user's search inputs
|
27
|
+
- Fix incorrect association conditions being used on through relations
|
28
|
+
- Use `#distinct` on Rails 4+
|
29
|
+
- Test Rails 5.0.x releases, test latest AR adapters
|
30
|
+
|
11
31
|
=== Version 3.3.0
|
12
32
|
|
13
33
|
- Support for ActiveRecord 5.0
|
data/Gemfile.activerecord42
CHANGED
data/Gemfile.activerecord50
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
gemspec
|
3
3
|
|
4
|
-
gem 'activerecord', '5.0.0
|
4
|
+
gem 'activerecord', '~> 5.0.0'
|
5
5
|
|
6
6
|
platforms :jruby do
|
7
7
|
gem 'activerecord-jdbcsqlite3-adapter'
|
@@ -10,7 +10,7 @@ platforms :jruby do
|
|
10
10
|
end
|
11
11
|
|
12
12
|
platforms :ruby do
|
13
|
-
gem 'sqlite3'
|
14
|
-
gem 'mysql2', '
|
15
|
-
gem 'pg'
|
13
|
+
gem 'sqlite3', '~> 1.3.6'
|
14
|
+
gem 'mysql2', '>= 0.3.18', '< 0.5'
|
15
|
+
gem 'pg', '~> 0.18'
|
16
16
|
end
|
data/README.rdoc
CHANGED
@@ -22,6 +22,9 @@ Add the following line in your Gemfile, and run <tt>bundle install</tt>:
|
|
22
22
|
|
23
23
|
gem "scoped_search"
|
24
24
|
|
25
|
+
Scoped search 4.x supports Rails 4.2 to 5.0, with Ruby 2.0.0 or higher. Use
|
26
|
+
previous versions, e.g. 3.x to support older versions of Rails or Ruby.
|
27
|
+
|
25
28
|
== Usage
|
26
29
|
|
27
30
|
Scoped search requires you to define the fields you want to search in:
|
data/lib/scoped_search.rb
CHANGED
@@ -65,6 +65,7 @@ require 'scoped_search/definition'
|
|
65
65
|
require 'scoped_search/query_language'
|
66
66
|
require 'scoped_search/query_builder'
|
67
67
|
require 'scoped_search/auto_complete_builder'
|
68
|
+
require 'scoped_search/validators'
|
68
69
|
|
69
70
|
# Import the search_on method in the ActiveReocrd::Base class
|
70
71
|
ActiveRecord::Base.send(:extend, ScopedSearch::ClassMethods)
|
@@ -178,7 +178,7 @@ module ScopedSearch
|
|
178
178
|
.where(value_conditions(field_name, val))
|
179
179
|
.select(field_name)
|
180
180
|
.limit(20)
|
181
|
-
.
|
181
|
+
.distinct
|
182
182
|
.map(&field.key_field)
|
183
183
|
.compact
|
184
184
|
.map { |f| "#{name}.#{f} " }
|
@@ -205,10 +205,10 @@ module ScopedSearch
|
|
205
205
|
.where(value_conditions(field.quoted_field, val))
|
206
206
|
.select(field.quoted_field)
|
207
207
|
.limit(20)
|
208
|
-
.
|
208
|
+
.distinct
|
209
209
|
.map(&field.field)
|
210
210
|
.compact
|
211
|
-
.map { |v| v.to_s =~ /\s/ ? "\"#{v}\"" : v }
|
211
|
+
.map { |v| v.to_s =~ /\s/ ? "\"#{v.gsub('"', '\"')}\"" : v }
|
212
212
|
end
|
213
213
|
|
214
214
|
def completer_scope(field)
|
@@ -16,44 +16,76 @@ module ScopedSearch
|
|
16
16
|
class Field
|
17
17
|
|
18
18
|
attr_reader :definition, :field, :only_explicit, :relation, :key_relation, :full_text_search,
|
19
|
-
:key_field, :complete_value, :complete_enabled, :offset, :word_size, :ext_method, :operators
|
19
|
+
:key_field, :complete_value, :complete_enabled, :offset, :word_size, :ext_method, :operators,
|
20
|
+
:validator
|
20
21
|
|
21
22
|
# Initializes a Field instance given the definition passed to the
|
22
23
|
# scoped_search call on the ActiveRecord-based model class.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
24
|
+
#
|
25
|
+
# Field name may be given in positional 'field' argument or 'on' named
|
26
|
+
# argument.
|
27
|
+
def initialize(definition,
|
28
|
+
field = nil,
|
29
|
+
aliases: [],
|
30
|
+
complete_enabled: true,
|
31
|
+
complete_value: nil,
|
32
|
+
default_operator: nil,
|
33
|
+
default_order: nil,
|
34
|
+
ext_method: nil,
|
35
|
+
full_text_search: nil,
|
36
|
+
in_key: nil,
|
37
|
+
offset: nil,
|
38
|
+
on: field,
|
39
|
+
on_key: nil,
|
40
|
+
only_explicit: nil,
|
41
|
+
operators: nil,
|
42
|
+
profile: nil,
|
43
|
+
relation: nil,
|
44
|
+
rename: nil,
|
45
|
+
validator: nil,
|
46
|
+
word_size: 1,
|
47
|
+
**kwargs)
|
48
|
+
|
49
|
+
# Prefer 'on' kw arg if given, defaults to the 'field' positional to allow either syntax
|
50
|
+
raise ArgumentError, "Missing field or 'on' keyword argument" if on.nil?
|
51
|
+
@field = on.to_sym
|
52
|
+
|
53
|
+
# Reserved Ruby keywords so access via kwargs instead, but deprecate them for future versions
|
54
|
+
if kwargs.key?(:in)
|
55
|
+
relation = kwargs.delete(:in)
|
56
|
+
ActiveSupport::Deprecation.warn("'in' argument deprecated, prefer 'relation' since scoped_search 4.0.0", caller(6))
|
47
57
|
end
|
58
|
+
if kwargs.key?(:alias)
|
59
|
+
aliases += [kwargs.delete(:alias)]
|
60
|
+
ActiveSupport::Deprecation.warn("'alias' argument deprecated, prefer aliases: [..] since scoped_search 4.0.0", caller(6))
|
61
|
+
end
|
62
|
+
raise ArgumentError, "Unknown arguments to scoped_search: #{kwargs.keys.join(', ')}" unless kwargs.empty?
|
48
63
|
|
49
|
-
|
50
|
-
definition.
|
51
|
-
definition.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
64
|
+
@definition = definition
|
65
|
+
@definition.profile = profile if profile
|
66
|
+
@definition.default_order ||= generate_default_order(default_order, rename || @field) if default_order
|
67
|
+
|
68
|
+
# Set attributes from keyword arguments
|
69
|
+
@complete_enabled = complete_enabled
|
70
|
+
@complete_value = complete_value
|
71
|
+
@default_operator = default_operator
|
72
|
+
@ext_method = ext_method
|
73
|
+
@full_text_search = full_text_search
|
74
|
+
@key_field = on_key
|
75
|
+
@key_relation = in_key
|
76
|
+
@offset = offset
|
77
|
+
@only_explicit = !!only_explicit
|
78
|
+
@operators = operators
|
79
|
+
@relation = relation
|
80
|
+
@validator = validator
|
81
|
+
@word_size = word_size
|
82
|
+
|
83
|
+
# Store this field in the field array
|
84
|
+
definition.fields[rename ? rename.to_sym : @field] ||= self
|
85
|
+
definition.unique_fields << self
|
86
|
+
|
87
|
+
# Store definition for aliases as well
|
88
|
+
aliases.each { |al| definition.fields[al.to_sym] ||= self }
|
57
89
|
end
|
58
90
|
|
59
91
|
# The ActiveRecord-based class that belongs to this field.
|
@@ -82,11 +114,7 @@ module ScopedSearch
|
|
82
114
|
if klass.columns_hash.has_key?(field.to_s)
|
83
115
|
klass.columns_hash[field.to_s]
|
84
116
|
else
|
85
|
-
|
86
|
-
raise ActiveRecord::UnknownAttributeError, "#{klass.inspect} doesn't have column #{field.inspect}."
|
87
|
-
else
|
88
|
-
raise ActiveRecord::UnknownAttributeError.new(klass, field)
|
89
|
-
end
|
117
|
+
raise ActiveRecord::UnknownAttributeError.new(klass, field)
|
90
118
|
end
|
91
119
|
end
|
92
120
|
end
|
@@ -135,11 +163,9 @@ module ScopedSearch
|
|
135
163
|
end
|
136
164
|
end
|
137
165
|
|
138
|
-
def default_order
|
139
|
-
|
140
|
-
|
141
|
-
order = (options[:default_order].to_s.downcase.include?('desc')) ? "DESC" : "ASC"
|
142
|
-
return "#{field_name} #{order}"
|
166
|
+
def generate_default_order(default_order, field)
|
167
|
+
order = (default_order.to_s.downcase.include?('desc')) ? "DESC" : "ASC"
|
168
|
+
return "#{field} #{order}"
|
143
169
|
end
|
144
170
|
|
145
171
|
# Return 'table'.'column' with the correct database quotes
|
@@ -234,8 +260,8 @@ module ScopedSearch
|
|
234
260
|
end
|
235
261
|
|
236
262
|
# Defines a new search field for this search definition.
|
237
|
-
def define(
|
238
|
-
Field.new(self,
|
263
|
+
def define(*args)
|
264
|
+
Field.new(self, *args)
|
239
265
|
end
|
240
266
|
|
241
267
|
# Returns a reflection for a given klass and name
|
@@ -252,24 +278,13 @@ module ScopedSearch
|
|
252
278
|
@klass.scope(:search_for, proc { |query, options|
|
253
279
|
klass = definition.klass
|
254
280
|
|
255
|
-
search_scope =
|
256
|
-
when 3
|
257
|
-
klass.scoped
|
258
|
-
when 4
|
259
|
-
(ActiveRecord::VERSION::MINOR < 1) ? klass.where(nil) : klass.all
|
260
|
-
when 5
|
261
|
-
klass.all
|
262
|
-
else
|
263
|
-
raise ScopedSearch::DefinitionError, 'version ' \
|
264
|
-
"#{ActiveRecord::VERSION::MAJOR} of activerecord is not supported"
|
265
|
-
end
|
266
|
-
|
281
|
+
search_scope = klass.all
|
267
282
|
find_options = ScopedSearch::QueryBuilder.build_query(definition, query || '', options || {})
|
268
283
|
search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
|
269
284
|
search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
|
270
285
|
search_scope = search_scope.joins(find_options[:joins]) if find_options[:joins]
|
271
286
|
search_scope = search_scope.reorder(find_options[:order]) if find_options[:order]
|
272
|
-
search_scope = search_scope.references(find_options[:include]) if find_options[:include]
|
287
|
+
search_scope = search_scope.references(find_options[:include]) if find_options[:include]
|
273
288
|
|
274
289
|
search_scope
|
275
290
|
})
|
@@ -251,9 +251,9 @@ module ScopedSearch
|
|
251
251
|
|
252
252
|
def find_has_many_through_association(field, through)
|
253
253
|
middle_table_association = nil
|
254
|
-
field.klass.reflect_on_all_associations(:has_many).each do |reflection|
|
254
|
+
field.klass.reflect_on_all_associations(:has_many).each do |reflection|
|
255
255
|
class_name = reflection.options[:class_name].constantize.table_name if reflection.options[:class_name]
|
256
|
-
middle_table_association = reflection.name if class_name == through.to_s
|
256
|
+
middle_table_association = reflection.name if class_name == through.to_s
|
257
257
|
middle_table_association = reflection.plural_name if reflection.plural_name == through.to_s
|
258
258
|
end
|
259
259
|
middle_table_association
|
@@ -271,7 +271,7 @@ module ScopedSearch
|
|
271
271
|
|
272
272
|
# primary and foreign keys + optional condition for the many to middle join
|
273
273
|
pk1, fk1 = field.reflection_keys(definition.reflection_by_name(many_class, through))
|
274
|
-
condition1 = field.reflection_conditions(definition.reflection_by_name(field.klass,
|
274
|
+
condition1 = field.reflection_conditions(definition.reflection_by_name(field.klass, middle_table_name))
|
275
275
|
|
276
276
|
# primary and foreign keys + optional condition for the endpoint to middle join
|
277
277
|
middle_table_association = find_has_many_through_association(field, through) || middle_table_name
|
@@ -468,6 +468,10 @@ module ScopedSearch
|
|
468
468
|
# Search only on the given field.
|
469
469
|
field = definition.field_by_name(lhs.value)
|
470
470
|
raise ScopedSearch::QueryNotSupported, "Field '#{lhs.value}' not recognized for searching!" unless field
|
471
|
+
|
472
|
+
# see if the value passes user defined validation
|
473
|
+
validate_value(field, rhs.value)
|
474
|
+
|
471
475
|
builder.sql_test(field, operator, rhs.value,lhs.value, &block)
|
472
476
|
end
|
473
477
|
|
@@ -485,6 +489,16 @@ module ScopedSearch
|
|
485
489
|
raise ScopedSearch::QueryNotSupported, "Don't know how to handle this operator node: #{operator.inspect} with #{children.inspect}!"
|
486
490
|
end
|
487
491
|
end
|
492
|
+
|
493
|
+
private
|
494
|
+
|
495
|
+
def validate_value(field, value)
|
496
|
+
validator = field.validator
|
497
|
+
if validator
|
498
|
+
valid = validator.call(value)
|
499
|
+
raise ScopedSearch::QueryNotSupported, "Value '#{value}' is not valid for field '#{field.field}'" unless valid
|
500
|
+
end
|
501
|
+
end
|
488
502
|
end
|
489
503
|
|
490
504
|
# Defines the to_sql method for AST AND/OR operators
|
@@ -4,20 +4,31 @@ module ScopedSearch
|
|
4
4
|
#
|
5
5
|
# Examples:
|
6
6
|
#
|
7
|
-
# sort
|
8
|
-
# sort
|
9
|
-
# sort
|
7
|
+
# sort :username
|
8
|
+
# sort :created_at, as: "Created"
|
9
|
+
# sort :created_at, default: "DESC"
|
10
|
+
#
|
11
|
+
# * <tt>field</tt> - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
|
10
12
|
#
|
11
13
|
# This helper accepts the following options:
|
12
14
|
#
|
13
|
-
# * <tt>:
|
14
|
-
# * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to :by
|
15
|
+
# * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to `field`
|
15
16
|
# * <tt>:default</tt> - default sorting order, DESC or ASC
|
16
|
-
|
17
|
+
# * <tt>:html_options</tt> - is a hash of HTML options for the anchor tag
|
18
|
+
# * <tt>:url_options</tt> - is a hash of URL parameters, defaulting to `params`, to preserve the current URL
|
19
|
+
# parameters.
|
20
|
+
#
|
21
|
+
# On Rails 5 or higher, parameter whitelisting prevents any parameter being used in a link by
|
22
|
+
# default, so `params.permit(..)` should be passed for `url_options` for all known and
|
23
|
+
# permitted URL parameters, e.g.
|
24
|
+
#
|
25
|
+
# sort :username, url_options: params.permit(:search)
|
26
|
+
#
|
27
|
+
def sort(field, as: nil, default: "ASC", html_options: {}, url_options: params)
|
17
28
|
|
18
|
-
unless
|
19
|
-
id
|
20
|
-
|
29
|
+
unless as
|
30
|
+
id = field.to_s.downcase == "id"
|
31
|
+
as = id ? field.to_s.upcase : field.to_s.humanize
|
21
32
|
end
|
22
33
|
|
23
34
|
ascend = "#{field} ASC"
|
@@ -30,151 +41,27 @@ module ScopedSearch
|
|
30
41
|
when descend
|
31
42
|
new_sort = ascend
|
32
43
|
else
|
33
|
-
new_sort = ["ASC", "DESC"].include?(
|
44
|
+
new_sort = ["ASC", "DESC"].include?(default) ? "#{field} #{default}" : ascend
|
34
45
|
end
|
35
46
|
|
36
47
|
unless selected_sort.nil?
|
37
48
|
css_classes = html_options[:class] ? html_options[:class].split(" ") : []
|
38
49
|
if selected_sort == ascend
|
39
|
-
|
50
|
+
as = "▲ #{as}"
|
40
51
|
css_classes << "ascending"
|
41
52
|
else
|
42
|
-
|
53
|
+
as = "▼ #{as}"
|
43
54
|
css_classes << "descending"
|
44
55
|
end
|
45
56
|
html_options[:class] = css_classes.join(" ")
|
46
57
|
end
|
47
58
|
|
48
|
-
url_options =
|
49
|
-
|
50
|
-
options[:as] = raw(options[:as]) if defined?(RailsXss)
|
51
|
-
|
52
|
-
a_link(options[:as], html_escape(url_for(url_options)),html_options)
|
53
|
-
end
|
54
|
-
|
55
|
-
# Adds AJAX auto complete functionality to the text input field with the
|
56
|
-
# DOM ID specified by +field_id+.
|
57
|
-
#
|
58
|
-
# Required +options+ is:
|
59
|
-
# <tt>:url</tt>:: URL to call for auto completion results
|
60
|
-
# in url_for format.
|
61
|
-
#
|
62
|
-
# Additional +options+ are:
|
63
|
-
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
|
64
|
-
# innerHTML should be updated with the auto complete
|
65
|
-
# entries returned by the AJAX request.
|
66
|
-
# Defaults to <tt>field_id</tt> + '_auto_complete'
|
67
|
-
# <tt>:with</tt>:: A JavaScript expression specifying the
|
68
|
-
# parameters for the XMLHttpRequest. This defaults
|
69
|
-
# to 'fieldname=value'.
|
70
|
-
# <tt>:frequency</tt>:: Determines the time to wait after the last keystroke
|
71
|
-
# for the AJAX request to be initiated.
|
72
|
-
# <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be
|
73
|
-
# displayed while auto complete is running.
|
74
|
-
# <tt>:tokens</tt>:: A string or an array of strings containing
|
75
|
-
# separator tokens for tokenized incremental
|
76
|
-
# auto completion. Example: <tt>:tokens => ','</tt> would
|
77
|
-
# allow multiple auto completion entries, separated
|
78
|
-
# by commas.
|
79
|
-
# <tt>:min_chars</tt>:: The minimum number of characters that should be
|
80
|
-
# in the input field before an Ajax call is made
|
81
|
-
# to the server.
|
82
|
-
# <tt>:on_hide</tt>:: A Javascript expression that is called when the
|
83
|
-
# auto completion div is hidden. The expression
|
84
|
-
# should take two variables: element and update.
|
85
|
-
# Element is a DOM element for the field, update
|
86
|
-
# is a DOM element for the div from which the
|
87
|
-
# innerHTML is replaced.
|
88
|
-
# <tt>:on_show</tt>:: Like on_hide, only now the expression is called
|
89
|
-
# then the div is shown.
|
90
|
-
# <tt>:after_update_element</tt>:: A Javascript expression that is called when the
|
91
|
-
# user has selected one of the proposed values.
|
92
|
-
# The expression should take two variables: element and value.
|
93
|
-
# Element is a DOM element for the field, value
|
94
|
-
# is the value selected by the user.
|
95
|
-
# <tt>:select</tt>:: Pick the class of the element from which the value for
|
96
|
-
# insertion should be extracted. If this is not specified,
|
97
|
-
# the entire element is used.
|
98
|
-
# <tt>:method</tt>:: Specifies the HTTP verb to use when the auto completion
|
99
|
-
# request is made. Defaults to POST.
|
100
|
-
def auto_complete_field(field_id, options = {})
|
101
|
-
function = "var #{field_id}_auto_completer = new Ajax.Autocompleter("
|
102
|
-
function << "'#{field_id}', "
|
103
|
-
function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', "
|
104
|
-
function << "'#{url_for(options[:url])}'"
|
105
|
-
|
106
|
-
js_options = {}
|
107
|
-
js_options[:tokens] = array_or_string_for_javascript(options[:tokens]) if options[:tokens]
|
108
|
-
js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
|
109
|
-
js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
|
110
|
-
js_options[:select] = "'#{options[:select]}'" if options[:select]
|
111
|
-
js_options[:paramName] = "'#{options[:param_name]}'" if options[:param_name]
|
112
|
-
js_options[:frequency] = "#{options[:frequency]}" if options[:frequency]
|
113
|
-
js_options[:method] = "'#{options[:method].to_s}'" if options[:method]
|
114
|
-
|
115
|
-
{ :after_update_element => :afterUpdateElement,
|
116
|
-
:on_show => :onShow, :on_hide => :onHide, :min_chars => :minChars }.each do |k,v|
|
117
|
-
js_options[v] = options[k] if options[k]
|
118
|
-
end
|
59
|
+
url_options = url_options.to_h if url_options.respond_to?(:permit) # convert ActionController::Parameters if given
|
60
|
+
url_options = url_options.merge(:order => new_sort)
|
119
61
|
|
120
|
-
|
62
|
+
as = raw(as) if defined?(RailsXss)
|
121
63
|
|
122
|
-
|
123
|
-
end
|
124
|
-
|
125
|
-
def auto_complete_field_jquery(method, url, options = {})
|
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 );
|
143
|
-
} else {
|
144
|
-
if(typeof(self._renderItemData) === "function") {
|
145
|
-
self._renderItemData( ul, item );
|
146
|
-
} else {
|
147
|
-
self._renderItem( ul, item );
|
148
|
-
}
|
149
|
-
}
|
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
|
171
|
-
|
172
|
-
javascript_tag(function)
|
173
|
-
end
|
174
|
-
|
175
|
-
def auto_complete_clear_value_button(field_id)
|
176
|
-
html_options = {:tabindex => '-1',:class=>"auto_complete_clear",:title =>'Clear Search', :onclick=>"document.getElementById('#{field_id}').value = '';"}
|
177
|
-
a_link("", "#", html_options)
|
64
|
+
a_link(as, html_escape(url_for(url_options)),html_options)
|
178
65
|
end
|
179
66
|
|
180
67
|
def a_link(name, href, html_options)
|
@@ -182,41 +69,5 @@ module ScopedSearch
|
|
182
69
|
link = "<a href=\"#{href}\"#{tag_options}>#{name}</a>"
|
183
70
|
return link.respond_to?(:html_safe) ? link.html_safe : link
|
184
71
|
end
|
185
|
-
|
186
|
-
# Use this method in your view to generate a return for the AJAX auto complete requests.
|
187
|
-
#
|
188
|
-
# The auto_complete_result can of course also be called from a view belonging to the
|
189
|
-
# auto_complete action if you need to decorate it further.
|
190
|
-
def auto_complete_result(entries, phrase = nil)
|
191
|
-
return unless entries
|
192
|
-
items = entries.map { |entry| content_tag("li", phrase ? highlight(entry, phrase) : h(entry)) }
|
193
|
-
content_tag("ul", items)
|
194
|
-
end
|
195
|
-
|
196
|
-
# Wrapper for text_field with added AJAX auto completion functionality.
|
197
|
-
#
|
198
|
-
# In your controller, you'll need to define an action called
|
199
|
-
# auto_complete_method to respond the AJAX calls,
|
200
|
-
def auto_complete_field_tag(method, val,tag_options = {}, completion_options = {})
|
201
|
-
auto_completer_options = { :url => { :action => "auto_complete_#{method}" } }.update(completion_options)
|
202
|
-
options = tag_options.merge(:class => "auto_complete_input " << tag_options[:class].to_s)
|
203
|
-
text_field_tag(method, val,options) +
|
204
|
-
auto_complete_clear_value_button(method) +
|
205
|
-
content_tag("div", "", :id => "#{method}_auto_complete", :class => "auto_complete") +
|
206
|
-
auto_complete_field(method, auto_completer_options)
|
207
|
-
end
|
208
|
-
|
209
|
-
# Wrapper for text_field with added JQuery auto completion functionality.
|
210
|
-
#
|
211
|
-
# In your controller, you'll need to define an action called
|
212
|
-
# auto_complete_method to respond the JQuery calls,
|
213
|
-
def auto_complete_field_tag_jquery(method, val,tag_options = {}, completion_options = {})
|
214
|
-
url = url_for(:action => "auto_complete_#{method}", :filter => completion_options[:filter])
|
215
|
-
options = tag_options.merge(:class => "auto_complete_input " << tag_options[:class].to_s)
|
216
|
-
text_field_tag(method, val, options) + auto_complete_clear_value_button(method) +
|
217
|
-
auto_complete_field_jquery(method, url, completion_options)
|
218
|
-
end
|
219
|
-
deprecate :auto_complete_field_tag_jquery, :auto_complete_field_tag, :auto_complete_result
|
220
|
-
|
221
72
|
end
|
222
73
|
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module ScopedSearch
|
2
|
+
# This class will be used to store standard validators.
|
3
|
+
class Validators
|
4
|
+
NUMERIC = ->(value) { !!(value =~ ScopedSearch::Definition::NUMERICAL_REGXP) }
|
5
|
+
INTEGER = ->(value) { !!(value =~ ScopedSearch::Definition::INTEGER_REGXP) }
|
6
|
+
end
|
7
|
+
end
|
data/scoped_search.gemspec
CHANGED
@@ -29,8 +29,8 @@ Gem::Specification.new do |gem|
|
|
29
29
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
30
30
|
gem.require_paths = ["lib"]
|
31
31
|
|
32
|
-
gem.required_ruby_version = '>=
|
33
|
-
gem.add_runtime_dependency('activerecord', '>=
|
32
|
+
gem.required_ruby_version = '>= 2.0.0'
|
33
|
+
gem.add_runtime_dependency('activerecord', '>= 4.2.0')
|
34
34
|
gem.add_development_dependency('rspec', '~> 3.0')
|
35
35
|
gem.add_development_dependency('rake')
|
36
36
|
|
@@ -37,20 +37,20 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
37
37
|
|
38
38
|
scoped_search :on => [:string, :date]
|
39
39
|
scoped_search :on => [:int], :complete_value => true
|
40
|
-
scoped_search :on => :another, :default_operator => :eq, :
|
40
|
+
scoped_search :on => :another, :default_operator => :eq, :aliases => [:alias], :complete_value => true
|
41
41
|
scoped_search :on => :explicit, :only_explicit => true, :complete_value => true
|
42
42
|
scoped_search :on => :deprecated, :complete_enabled => false
|
43
|
-
scoped_search :on => :related, :
|
44
|
-
scoped_search :on => :other_a, :
|
45
|
-
scoped_search :on => :other_b, :
|
46
|
-
scoped_search :on => :other_c, :
|
43
|
+
scoped_search :on => :related, :relation => :bars, :rename => 'bars.related'.to_sym
|
44
|
+
scoped_search :on => :other_a, :relation => :bars, :rename => 'bars.other_a'.to_sym
|
45
|
+
scoped_search :on => :other_b, :relation => :bars, :rename => 'bars.other_b'.to_sym
|
46
|
+
scoped_search :on => :other_c, :relation => :bars, :rename => 'bars.other_c'.to_sym
|
47
47
|
end
|
48
48
|
|
49
49
|
class ::Infoo < ::Foo
|
50
50
|
end
|
51
51
|
|
52
52
|
@foo_1 = Foo.create!(:string => 'foo', :another => 'temp 1', :explicit => 'baz', :int => 9 , :date => 'February 8, 2011' , :unindexed => 10)
|
53
|
-
Foo.create!(:string => 'bar', :another => 'temp 2', :explicit => 'baz', :int => 22 , :date => 'February 10, 2011', :unindexed => 10)
|
53
|
+
Foo.create!(:string => 'bar', :another => 'temp "2"', :explicit => 'baz', :int => 22 , :date => 'February 10, 2011', :unindexed => 10)
|
54
54
|
Foo.create!(:string => 'baz', :another => nil, :explicit => nil , :int => nil, :date => nil , :unindexed => nil)
|
55
55
|
20.times { Foo.create!(:explicit => "aaa") }
|
56
56
|
|
@@ -143,6 +143,10 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
143
143
|
it "should complete values should contain baz" do
|
144
144
|
Foo.complete_for('explicit = ').should contain('explicit = baz')
|
145
145
|
end
|
146
|
+
|
147
|
+
it "should complete values with quotes where required" do
|
148
|
+
Foo.complete_for('alias = ').should contain('alias = "temp \"2\""')
|
149
|
+
end
|
146
150
|
end
|
147
151
|
|
148
152
|
context 'auto complete relations' do
|
@@ -32,7 +32,7 @@ require "spec_helper"
|
|
32
32
|
has_many :facts
|
33
33
|
has_many :keys, :through => :facts
|
34
34
|
|
35
|
-
scoped_search :
|
35
|
+
scoped_search :relation => :facts, :on => :value, :rename => :facts, :in_key => :keys, :on_key => :name, :complete_value => true
|
36
36
|
end
|
37
37
|
class ::MyItem < ::Item
|
38
38
|
end
|
@@ -114,16 +114,6 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
114
114
|
@class.search_for('date = 09-01-02').length.should == 1
|
115
115
|
end
|
116
116
|
|
117
|
-
if RUBY_VERSION.to_f == 1.8
|
118
|
-
it "should accept MM/DD/YY as date format" do
|
119
|
-
@class.search_for('date = 01/02/09').length.should == 1
|
120
|
-
end
|
121
|
-
|
122
|
-
it "should accept MM/DD/YYYY as date format" do
|
123
|
-
@class.search_for('date = 01/02/2009').length.should == 1
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
117
|
it "should accept YYYY/MM/DD as date format" do
|
128
118
|
@class.search_for('date = 2009/01/02').length.should == 1
|
129
119
|
end
|
@@ -46,7 +46,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
46
46
|
ActiveRecord::Migration.create_table(:loos) { |t| t.string :foo; t.integer :har_id }
|
47
47
|
class Loo < ActiveRecord::Base
|
48
48
|
belongs_to :har
|
49
|
-
scoped_search :
|
49
|
+
scoped_search :relation => :har, :on => :related
|
50
50
|
end
|
51
51
|
|
52
52
|
@har_record = Har.create!(:related => 'bar')
|
@@ -93,7 +93,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
93
93
|
class Goo < ActiveRecord::Base
|
94
94
|
has_many :jars
|
95
95
|
scoped_search :on => :foo
|
96
|
-
scoped_search :
|
96
|
+
scoped_search :relation => :jars, :on => :related
|
97
97
|
end
|
98
98
|
|
99
99
|
@foo_1 = Goo.create!(:foo => 'foo')
|
@@ -158,7 +158,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
158
158
|
class Hoo < ActiveRecord::Base
|
159
159
|
has_one :car
|
160
160
|
scoped_search :on => :foo
|
161
|
-
scoped_search :
|
161
|
+
scoped_search :relation => :car, :on => :related
|
162
162
|
end
|
163
163
|
|
164
164
|
@hoo_1 = Hoo.create!(:foo => 'foo')
|
@@ -206,7 +206,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
206
206
|
# The class on which to call search_for
|
207
207
|
class Joo < ActiveRecord::Base
|
208
208
|
has_and_belongs_to_many :dars
|
209
|
-
scoped_search :
|
209
|
+
scoped_search :relation => :dars, :on => :related
|
210
210
|
end
|
211
211
|
|
212
212
|
@joo_1 = Joo.create!(:foo => 'foo')
|
@@ -265,7 +265,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
265
265
|
# as an indication for a polymorphic relation.
|
266
266
|
has_many :bazs, :through => :mars, :source => :baz
|
267
267
|
|
268
|
-
scoped_search :
|
268
|
+
scoped_search :relation => :bazs, :on => :related
|
269
269
|
end
|
270
270
|
|
271
271
|
@koo_1 = Koo.create!(:foo => 'foo')
|
@@ -316,7 +316,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
316
316
|
belongs_to :zar
|
317
317
|
has_many :bazs, :through => :zar
|
318
318
|
|
319
|
-
scoped_search :
|
319
|
+
scoped_search :relation => :bazs, :on => :related
|
320
320
|
end
|
321
321
|
|
322
322
|
baz_1 = Baz.create(:related => 'baz')
|
@@ -362,7 +362,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
362
362
|
has_many :taggables
|
363
363
|
has_many :dogs, :through => :taggables, :source => :taggable, :source_type => 'Dog'
|
364
364
|
|
365
|
-
scoped_search :
|
365
|
+
scoped_search :relation => :dogs, :on => :related, :rename => :dog
|
366
366
|
end
|
367
367
|
|
368
368
|
# The class on which to call search_for
|
@@ -370,7 +370,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
370
370
|
has_many :taggables, :as => :taggable
|
371
371
|
has_many :tags, :through => :taggables
|
372
372
|
|
373
|
-
scoped_search :
|
373
|
+
scoped_search :relation => :tags, :on => :foo
|
374
374
|
end
|
375
375
|
|
376
376
|
class Cat < ActiveRecord::Base
|
@@ -438,7 +438,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
438
438
|
has_many :zaps
|
439
439
|
has_many :pazs, :through => :zaps
|
440
440
|
|
441
|
-
scoped_search :
|
441
|
+
scoped_search :relation => :pazs, :on => :related
|
442
442
|
end
|
443
443
|
|
444
444
|
@moo_1 = Moo.create!(:foo => 'foo')
|
@@ -471,6 +471,59 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
471
471
|
end
|
472
472
|
end
|
473
473
|
|
474
|
+
context 'querying a :has_many => :through relation with same name on target class with custom condition' do
|
475
|
+
|
476
|
+
before do
|
477
|
+
|
478
|
+
# Create some tables
|
479
|
+
ActiveRecord::Migration.create_table(:user_groups) { |t| t.integer :user_id; t.integer :group_id }
|
480
|
+
ActiveRecord::Migration.create_table(:conflicts) { |t| t.integer :group_id; t.integer :user_id }
|
481
|
+
ActiveRecord::Migration.create_table(:groups) { |t| t.string :related; t.integer :user_id }
|
482
|
+
ActiveRecord::Migration.create_table(:users) { |t| t.string :foo }
|
483
|
+
|
484
|
+
# The related classes
|
485
|
+
class UserGroup < ActiveRecord::Base; belongs_to :user; belongs_to :group; end
|
486
|
+
class Conflict < ActiveRecord::Base; belongs_to :user; belongs_to :group; end
|
487
|
+
class Group < ActiveRecord::Base
|
488
|
+
has_many :user_groups
|
489
|
+
has_many :users, :through => :conflicts, :source_type => 'User', :source => :user
|
490
|
+
end
|
491
|
+
|
492
|
+
# The class on which to call search_for
|
493
|
+
class User < ActiveRecord::Base
|
494
|
+
has_many :user_groups
|
495
|
+
has_many :groups, :through => :user_groups
|
496
|
+
|
497
|
+
scoped_search :relation => :groups, :on => :related
|
498
|
+
end
|
499
|
+
|
500
|
+
@user_1 = User.create!(:foo => 'foo')
|
501
|
+
@user_2 = User.create!(:foo => 'foo too')
|
502
|
+
@user_3 = User.create!(:foo => 'foo three')
|
503
|
+
|
504
|
+
@group_1 = Group.create(:related => 'value')
|
505
|
+
@group_2 = Group.create(:related => 'value too!')
|
506
|
+
|
507
|
+
@bar_1 = UserGroup.create!(:user => @user_1, :group => @group_1)
|
508
|
+
@bar_2 = UserGroup.create!(:user => @user_1)
|
509
|
+
@bar_3 = UserGroup.create!(:user => @user_2, :group => @group_1)
|
510
|
+
@bar_3 = UserGroup.create!(:user => @user_2, :group => @group_2)
|
511
|
+
@bar_3 = UserGroup.create!(:user => @user_2, :group => @group_2)
|
512
|
+
@bar_4 = UserGroup.create!(:user => @user_3)
|
513
|
+
end
|
514
|
+
|
515
|
+
after do
|
516
|
+
ActiveRecord::Migration.drop_table(:user_groups)
|
517
|
+
ActiveRecord::Migration.drop_table(:users)
|
518
|
+
ActiveRecord::Migration.drop_table(:groups)
|
519
|
+
ActiveRecord::Migration.drop_table(:conflicts)
|
520
|
+
end
|
521
|
+
|
522
|
+
it "should find the one record that is related based on forward groups relation" do
|
523
|
+
User.search_for('related=value AND related="value too!"').length.should == 1
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
474
527
|
|
475
528
|
context 'querying a :has_many => :through relation with modules' do
|
476
529
|
|
@@ -492,7 +545,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
492
545
|
has_many :bazs, :through => :mars
|
493
546
|
self.table_name = "zan_koos"
|
494
547
|
|
495
|
-
scoped_search :
|
548
|
+
scoped_search :relation => :bazs, :on => :related
|
496
549
|
end
|
497
550
|
end
|
498
551
|
|
@@ -16,7 +16,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
|
|
16
16
|
:description => :string
|
17
17
|
) do |klass|
|
18
18
|
klass.scoped_search :on => :string
|
19
|
-
klass.scoped_search :on => :another, :default_operator => :eq, :
|
19
|
+
klass.scoped_search :on => :another, :default_operator => :eq, :aliases => [:alias], :default_order => :desc
|
20
20
|
klass.scoped_search :on => :explicit, :only_explicit => true
|
21
21
|
klass.scoped_search :on => :description
|
22
22
|
end
|
data/spec/unit/ast_spec.rb
CHANGED
@@ -150,7 +150,7 @@ describe ScopedSearch::QueryLanguage::AST::OperatorNode do
|
|
150
150
|
end
|
151
151
|
|
152
152
|
it "should raise an error of the LHS is requested" do
|
153
|
-
lambda { @node.lhs }.should raise_error
|
153
|
+
lambda { @node.lhs }.should raise_error(ScopedSearch::Exception, "Operator does not have a LHS")
|
154
154
|
end
|
155
155
|
end
|
156
156
|
|
@@ -187,11 +187,11 @@ describe ScopedSearch::QueryLanguage::AST::OperatorNode do
|
|
187
187
|
end
|
188
188
|
|
189
189
|
it "should raise an error of the LHS is requested" do
|
190
|
-
lambda { @node.lhs }.should raise_error
|
190
|
+
lambda { @node.lhs }.should raise_error(ScopedSearch::Exception, "Operators with more than 2 children do not have LHS/RHS")
|
191
191
|
end
|
192
192
|
|
193
193
|
it "should raise an error of the RHS is requested" do
|
194
|
-
lambda { @node.rhs }.should raise_error
|
194
|
+
lambda { @node.rhs }.should raise_error(ScopedSearch::Exception, "Operators with more than 2 children do not have LHS/RHS")
|
195
195
|
end
|
196
196
|
end
|
197
197
|
end
|
@@ -9,6 +9,42 @@ describe ScopedSearch::Definition do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
describe ScopedSearch::Definition::Field do
|
12
|
+
describe '#initialize' do
|
13
|
+
it "should raise an exception with missing field or 'on' keyword" do
|
14
|
+
lambda {
|
15
|
+
@definition.define
|
16
|
+
}.should raise_error(ArgumentError, "Missing field or 'on' keyword argument")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should raise an exception with unknown keyword arguments" do
|
20
|
+
lambda {
|
21
|
+
@definition.define(:field, :nonexisting => 'foo')
|
22
|
+
}.should raise_error(ArgumentError, "Unknown arguments to scoped_search: nonexisting")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should alias :in to :relation" do
|
26
|
+
ActiveSupport::Deprecation.should_receive(:warn).with("'in' argument deprecated, prefer 'relation' since scoped_search 4.0.0", anything)
|
27
|
+
@definition.define(:field, :in => 'foo').relation.should eq('foo')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should accept :relation" do
|
31
|
+
ActiveSupport::Deprecation.should_not_receive(:warn)
|
32
|
+
@definition.define(:field, :relation => 'foo').relation.should eq('foo')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should alias :alias to :aliases" do
|
36
|
+
ActiveSupport::Deprecation.should_receive(:warn).with("'alias' argument deprecated, prefer aliases: [..] since scoped_search 4.0.0", anything)
|
37
|
+
@definition.define(:field, :alias => 'foo')
|
38
|
+
@definition.fields.keys.should eq([:field, :foo])
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should accept :relation" do
|
42
|
+
ActiveSupport::Deprecation.should_not_receive(:warn)
|
43
|
+
@definition.define(:field, :aliases => ['foo'])
|
44
|
+
@definition.fields.keys.should eq([:field, :foo])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
12
48
|
describe '#column' do
|
13
49
|
it "should raise an exception when using an unknown field" do
|
14
50
|
lambda {
|
@@ -33,4 +33,26 @@ describe ScopedSearch::QueryBuilder do
|
|
33
33
|
@definition.klass.connection.stub("class").and_return(connection)
|
34
34
|
ScopedSearch::QueryBuilder.class_for(@definition).should == ScopedSearch::QueryBuilder::PostgreSQLAdapter
|
35
35
|
end
|
36
|
+
|
37
|
+
it "should validate value if validator selected" do
|
38
|
+
field = double('field')
|
39
|
+
field.stub(:only_explicit).and_return(true)
|
40
|
+
field.stub(:field).and_return(:test_field)
|
41
|
+
field.stub(:validator).and_return(->(_value) { false })
|
42
|
+
|
43
|
+
@definition.stub(:field_by_name).and_return(field)
|
44
|
+
|
45
|
+
lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error(ScopedSearch::QueryNotSupported)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should display custom error from validator" do
|
49
|
+
field = double('field')
|
50
|
+
field.stub(:only_explicit).and_return(true)
|
51
|
+
field.stub(:field).and_return(:test_field)
|
52
|
+
field.stub(:validator).and_return(->(_value) { raise ScopedSearch::QueryNotSupported, 'my custom message' })
|
53
|
+
|
54
|
+
@definition.stub(:field_by_name).and_return(field)
|
55
|
+
|
56
|
+
lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error('my custom message')
|
57
|
+
end
|
36
58
|
end
|
@@ -92,4 +92,24 @@ describe ScopedSearch::RailsHelper do
|
|
92
92
|
params[:order] = "field DESC"
|
93
93
|
sort("field")
|
94
94
|
end
|
95
|
+
|
96
|
+
context 'with ActionController::Parameters' do
|
97
|
+
let(:ac_params) { double('ActionController::Parameters') }
|
98
|
+
|
99
|
+
it "should call to_h on passed params object" do
|
100
|
+
should_receive(:url_for).with(
|
101
|
+
"controller" => "resources",
|
102
|
+
"action" => "search",
|
103
|
+
"walrus" => "unicorns",
|
104
|
+
"order" => "field ASC"
|
105
|
+
).and_return("/example")
|
106
|
+
|
107
|
+
params[:walrus] = "unicorns"
|
108
|
+
|
109
|
+
ac_params.should_receive(:respond_to?).with(:permit).and_return(true)
|
110
|
+
ac_params.should_receive(:to_h).and_return(params)
|
111
|
+
|
112
|
+
sort("field", url_options: ac_params)
|
113
|
+
end
|
114
|
+
end
|
95
115
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe ScopedSearch::Validators do
|
4
|
+
describe 'NUMERIC' do
|
5
|
+
it 'should accept integer value' do
|
6
|
+
ScopedSearch::Validators::NUMERIC.call('123').should eq(true)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should accept float value' do
|
10
|
+
ScopedSearch::Validators::NUMERIC.call('123.5').should eq(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should reject string value' do
|
14
|
+
ScopedSearch::Validators::NUMERIC.call('abc').should eq(false)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'INTEGER' do
|
19
|
+
it 'should accept numeric value' do
|
20
|
+
ScopedSearch::Validators::INTEGER.call('123').should eq(true)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should reject string value' do
|
24
|
+
ScopedSearch::Validators::INTEGER.call('abc').should eq(false)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
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
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Amos Benari
|
@@ -10,48 +10,48 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2016-
|
13
|
+
date: 2016-12-05 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
17
17
|
requirement: !ruby/object:Gem::Requirement
|
18
18
|
requirements:
|
19
|
-
- -
|
19
|
+
- - ">="
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
21
|
+
version: 4.2.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
25
|
requirements:
|
26
|
-
- -
|
26
|
+
- - ">="
|
27
27
|
- !ruby/object:Gem::Version
|
28
|
-
version:
|
28
|
+
version: 4.2.0
|
29
29
|
- !ruby/object:Gem::Dependency
|
30
30
|
name: rspec
|
31
31
|
requirement: !ruby/object:Gem::Requirement
|
32
32
|
requirements:
|
33
|
-
- - ~>
|
33
|
+
- - "~>"
|
34
34
|
- !ruby/object:Gem::Version
|
35
35
|
version: '3.0'
|
36
36
|
type: :development
|
37
37
|
prerelease: false
|
38
38
|
version_requirements: !ruby/object:Gem::Requirement
|
39
39
|
requirements:
|
40
|
-
- - ~>
|
40
|
+
- - "~>"
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
version: '3.0'
|
43
43
|
- !ruby/object:Gem::Dependency
|
44
44
|
name: rake
|
45
45
|
requirement: !ruby/object:Gem::Requirement
|
46
46
|
requirements:
|
47
|
-
- -
|
47
|
+
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
49
|
version: '0'
|
50
50
|
type: :development
|
51
51
|
prerelease: false
|
52
52
|
version_requirements: !ruby/object:Gem::Requirement
|
53
53
|
requirements:
|
54
|
-
- -
|
54
|
+
- - ">="
|
55
55
|
- !ruby/object:Gem::Version
|
56
56
|
version: '0'
|
57
57
|
description: |2
|
@@ -77,14 +77,11 @@ extra_rdoc_files:
|
|
77
77
|
- CONTRIBUTING.rdoc
|
78
78
|
- LICENSE
|
79
79
|
files:
|
80
|
-
- .gitignore
|
81
|
-
- .travis.yml
|
80
|
+
- ".gitignore"
|
81
|
+
- ".travis.yml"
|
82
82
|
- CHANGELOG.rdoc
|
83
83
|
- CONTRIBUTING.rdoc
|
84
84
|
- Gemfile
|
85
|
-
- Gemfile.activerecord32
|
86
|
-
- Gemfile.activerecord40
|
87
|
-
- Gemfile.activerecord41
|
88
85
|
- Gemfile.activerecord42
|
89
86
|
- Gemfile.activerecord50
|
90
87
|
- LICENSE
|
@@ -105,6 +102,7 @@ files:
|
|
105
102
|
- lib/scoped_search/query_language/tokenizer.rb
|
106
103
|
- lib/scoped_search/rails_helper.rb
|
107
104
|
- lib/scoped_search/railtie.rb
|
105
|
+
- lib/scoped_search/validators.rb
|
108
106
|
- lib/scoped_search/version.rb
|
109
107
|
- scoped_search.gemspec
|
110
108
|
- spec/database.jruby.yml
|
@@ -128,33 +126,34 @@ files:
|
|
128
126
|
- spec/unit/query_builder_spec.rb
|
129
127
|
- spec/unit/rails_helper_spec.rb
|
130
128
|
- spec/unit/tokenizer_spec.rb
|
129
|
+
- spec/unit/validators_spec.rb
|
131
130
|
homepage: https://github.com/wvanbergen/scoped_search/wiki
|
132
131
|
licenses:
|
133
132
|
- MIT
|
134
133
|
metadata: {}
|
135
134
|
post_install_message:
|
136
135
|
rdoc_options:
|
137
|
-
- --title
|
136
|
+
- "--title"
|
138
137
|
- scoped_search
|
139
|
-
- --main
|
138
|
+
- "--main"
|
140
139
|
- README.rdoc
|
141
|
-
- --line-numbers
|
142
|
-
- --inline-source
|
140
|
+
- "--line-numbers"
|
141
|
+
- "--inline-source"
|
143
142
|
require_paths:
|
144
143
|
- lib
|
145
144
|
required_ruby_version: !ruby/object:Gem::Requirement
|
146
145
|
requirements:
|
147
|
-
- -
|
146
|
+
- - ">="
|
148
147
|
- !ruby/object:Gem::Version
|
149
|
-
version:
|
148
|
+
version: 2.0.0
|
150
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
150
|
requirements:
|
152
|
-
- -
|
151
|
+
- - ">="
|
153
152
|
- !ruby/object:Gem::Version
|
154
153
|
version: '0'
|
155
154
|
requirements: []
|
156
155
|
rubyforge_project:
|
157
|
-
rubygems_version: 2.
|
156
|
+
rubygems_version: 2.6.8
|
158
157
|
signing_key:
|
159
158
|
specification_version: 4
|
160
159
|
summary: Easily search you ActiveRecord models with a simple query language using
|
@@ -181,3 +180,4 @@ test_files:
|
|
181
180
|
- spec/unit/query_builder_spec.rb
|
182
181
|
- spec/unit/rails_helper_spec.rb
|
183
182
|
- spec/unit/tokenizer_spec.rb
|
183
|
+
- spec/unit/validators_spec.rb
|
data/Gemfile.activerecord32
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
source 'https://rubygems.org'
|
2
|
-
gemspec
|
3
|
-
|
4
|
-
gem 'activerecord', '~> 3.2.0'
|
5
|
-
|
6
|
-
platforms :jruby do
|
7
|
-
gem 'activerecord-jdbcsqlite3-adapter'
|
8
|
-
gem 'activerecord-jdbcmysql-adapter'
|
9
|
-
gem 'activerecord-jdbcpostgresql-adapter'
|
10
|
-
end
|
11
|
-
|
12
|
-
platforms :ruby do
|
13
|
-
gem 'sqlite3'
|
14
|
-
gem 'mysql2', '~> 0.3.11'
|
15
|
-
gem 'pg'
|
16
|
-
end
|
data/Gemfile.activerecord40
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
source 'https://rubygems.org'
|
2
|
-
gemspec
|
3
|
-
|
4
|
-
gem 'activerecord', '~> 4.0.0'
|
5
|
-
|
6
|
-
platforms :jruby do
|
7
|
-
gem 'activerecord-jdbcsqlite3-adapter'
|
8
|
-
gem 'activerecord-jdbcmysql-adapter'
|
9
|
-
gem 'activerecord-jdbcpostgresql-adapter'
|
10
|
-
end
|
11
|
-
|
12
|
-
platforms :ruby do
|
13
|
-
gem 'sqlite3'
|
14
|
-
gem 'mysql2', '~> 0.3.11'
|
15
|
-
gem 'pg'
|
16
|
-
end
|
data/Gemfile.activerecord41
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
source 'https://rubygems.org'
|
2
|
-
gemspec
|
3
|
-
|
4
|
-
gem 'activerecord', '~> 4.1.0'
|
5
|
-
|
6
|
-
platforms :jruby do
|
7
|
-
gem 'activerecord-jdbcsqlite3-adapter'
|
8
|
-
gem 'activerecord-jdbcmysql-adapter'
|
9
|
-
gem 'activerecord-jdbcpostgresql-adapter'
|
10
|
-
end
|
11
|
-
|
12
|
-
platforms :ruby do
|
13
|
-
gem 'sqlite3'
|
14
|
-
gem 'mysql2', '~> 0.3.11'
|
15
|
-
gem 'pg'
|
16
|
-
end
|