ransack 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -4
  3. data/CONTRIBUTING.md +12 -4
  4. data/Gemfile +4 -5
  5. data/README.md +160 -55
  6. data/lib/ransack.rb +1 -1
  7. data/lib/ransack/adapters/active_record/3.0/context.rb +16 -0
  8. data/lib/ransack/adapters/active_record/3.1/context.rb +24 -0
  9. data/lib/ransack/adapters/active_record/base.rb +6 -0
  10. data/lib/ransack/adapters/active_record/context.rb +49 -1
  11. data/lib/ransack/configuration.rb +23 -6
  12. data/lib/ransack/constants.rb +46 -45
  13. data/lib/ransack/context.rb +19 -2
  14. data/lib/ransack/helpers/form_builder.rb +5 -4
  15. data/lib/ransack/helpers/form_helper.rb +34 -14
  16. data/lib/ransack/locale/hu.yml +70 -0
  17. data/lib/ransack/locale/nl.yml +70 -0
  18. data/lib/ransack/nodes/attribute.rb +2 -2
  19. data/lib/ransack/nodes/condition.rb +29 -12
  20. data/lib/ransack/nodes/grouping.rb +6 -6
  21. data/lib/ransack/nodes/node.rb +1 -1
  22. data/lib/ransack/nodes/value.rb +1 -1
  23. data/lib/ransack/predicate.rb +4 -5
  24. data/lib/ransack/ransacker.rb +1 -1
  25. data/lib/ransack/search.rb +39 -13
  26. data/lib/ransack/translate.rb +7 -8
  27. data/lib/ransack/version.rb +1 -1
  28. data/ransack.gemspec +5 -5
  29. data/spec/ransack/adapters/active_record/base_spec.rb +78 -35
  30. data/spec/ransack/adapters/active_record/context_spec.rb +58 -15
  31. data/spec/ransack/configuration_spec.rb +18 -18
  32. data/spec/ransack/dependencies_spec.rb +1 -1
  33. data/spec/ransack/helpers/form_builder_spec.rb +29 -29
  34. data/spec/ransack/helpers/form_helper_spec.rb +14 -1
  35. data/spec/ransack/nodes/condition_spec.rb +21 -2
  36. data/spec/ransack/predicate_spec.rb +49 -9
  37. data/spec/ransack/search_spec.rb +178 -143
  38. data/spec/ransack/translate_spec.rb +1 -1
  39. data/spec/spec_helper.rb +1 -0
  40. data/spec/support/schema.rb +26 -21
  41. metadata +15 -11
@@ -73,6 +73,30 @@ module Ransack
73
73
  @engine.connection_pool.columns_hash[table][name].type
74
74
  end
75
75
 
76
+ def join_associations
77
+ @join_dependency.join_associations
78
+ end
79
+
80
+ # All dependent Arel::Join nodes used in the search query
81
+ #
82
+ # This could otherwise be done as `@object.arel.join_sources`, except
83
+ # that ActiveRecord's build_joins sets up its own JoinDependency.
84
+ # This extracts what we need to access the joins using our existing
85
+ # JoinDependency to track table aliases.
86
+ #
87
+ def join_sources
88
+ base = Arel::SelectManager.new(@object.engine, @object.table)
89
+ joins = @object.joins_values
90
+ joins.each do |assoc|
91
+ assoc.join_to(base)
92
+ end
93
+ base.join_sources
94
+ end
95
+
96
+ def alias_tracker
97
+ @join_dependency.alias_tracker
98
+ end
99
+
76
100
  private
77
101
 
78
102
  def get_parent_and_attribute_name(str, parent = @base)
@@ -12,6 +12,7 @@ module Ransack
12
12
  end
13
13
 
14
14
  def ransack(params = {}, options = {})
15
+ params = params.presence || {}
15
16
  Search.new(self, params ? params.delete_if {
16
17
  |k, v| v.blank? && v != false } : params, options)
17
18
  end
@@ -35,6 +36,11 @@ module Ransack
35
36
  reflect_on_all_associations.map { |a| a.name.to_s }
36
37
  end
37
38
 
39
+ # For overriding with a whitelist of symbols
40
+ def ransackable_scopes(auth_object = nil)
41
+ []
42
+ end
43
+
38
44
  end
39
45
  end
40
46
  end
@@ -78,7 +78,55 @@ module Ransack
78
78
  end
79
79
  end
80
80
 
81
- private
81
+ if ::ActiveRecord::VERSION::STRING >= '4.1'
82
+
83
+ def join_associations
84
+ raise NotImplementedError,
85
+ "ActiveRecord 4.1 and later does not use join_associations. Use join_sources."
86
+ end
87
+
88
+ # All dependent Arel::Join nodes used in the search query
89
+ #
90
+ # This could otherwise be done as `@object.arel.join_sources`, except
91
+ # that ActiveRecord's build_joins sets up its own JoinDependency.
92
+ # This extracts what we need to access the joins using our existing
93
+ # JoinDependency to track table aliases.
94
+ #
95
+ def join_sources
96
+ base = Arel::SelectManager.new(@object.engine, @object.table)
97
+ joins = @join_dependency.join_constraints(@object.joins_values)
98
+ joins.each do |aliased_join|
99
+ base.from(aliased_join)
100
+ end
101
+ base.join_sources
102
+ end
103
+
104
+ else
105
+
106
+ # All dependent JoinAssociation items used in the search query
107
+ #
108
+ # Deprecated: this goes away in ActiveRecord 4.1. Use join_sources.
109
+ #
110
+ def join_associations
111
+ @join_dependency.join_associations
112
+ end
113
+
114
+ def join_sources
115
+ base = Arel::SelectManager.new(@object.engine, @object.table)
116
+ joins = @object.joins_values
117
+ joins.each do |assoc|
118
+ assoc.join_to(base)
119
+ end
120
+ base.join_sources
121
+ end
122
+
123
+ end
124
+
125
+ def alias_tracker
126
+ @join_dependency.alias_tracker
127
+ end
128
+
129
+ private
82
130
 
83
131
  def get_parent_and_attribute_name(str, parent = @base)
84
132
  attr_name = nil
@@ -7,7 +7,8 @@ module Ransack
7
7
  mattr_accessor :predicates, :options
8
8
  self.predicates = {}
9
9
  self.options = {
10
- search_key: :q
10
+ :search_key => :q,
11
+ :ignore_unknown_conditions => true
11
12
  }
12
13
 
13
14
  def configure
@@ -20,16 +21,18 @@ module Ransack
20
21
  compounds = opts.delete(:compounds)
21
22
  compounds = true if compounds.nil?
22
23
  compounds = false if opts[:wants_array]
23
- opts[:arel_predicate] = opts[:arel_predicate].to_s
24
24
 
25
25
  self.predicates[name] = Predicate.new(opts)
26
26
 
27
27
  ['_any', '_all'].each do |suffix|
28
- self.predicates[name + suffix] = Predicate.new(
28
+ compound_name = name + suffix
29
+ self.predicates[compound_name] = Predicate.new(
29
30
  opts.merge(
30
- name: name + suffix,
31
- arel_predicate: opts[:arel_predicate] + suffix,
32
- compound: true
31
+ :name => compound_name,
32
+ :arel_predicate => arel_predicate_with_suffix(
33
+ opts[:arel_predicate], suffix
34
+ ),
35
+ :compound => true
33
36
  )
34
37
  )
35
38
  end if compounds
@@ -40,5 +43,19 @@ module Ransack
40
43
  self.options[:search_key] = name
41
44
  end
42
45
 
46
+ # raise an error if an unknown predicate, condition or attribute is passed
47
+ # into a search
48
+ def ignore_unknown_conditions=(boolean)
49
+ self.options[:ignore_unknown_conditions] = boolean
50
+ end
51
+
52
+ def arel_predicate_with_suffix(arel_predicate, suffix)
53
+ if arel_predicate === Proc
54
+ proc { |v| "#{arel_predicate.call(v)}#{suffix}" }
55
+ else
56
+ "#{arel_predicate}#{suffix}"
57
+ end
58
+ end
59
+
43
60
  end
44
61
  end
@@ -2,97 +2,98 @@ module Ransack
2
2
  module Constants
3
3
  TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
4
4
  FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
5
+ BOOLEAN_VALUES = TRUE_VALUES + FALSE_VALUES
5
6
 
6
7
  AREL_PREDICATES = %w(eq not_eq matches does_not_match lt lteq gt gteq in not_in)
7
8
 
8
9
  DERIVED_PREDICATES = [
9
10
  ['cont', {
10
- arel_predicate: 'matches',
11
- formatter: proc { |v| "%#{escape_wildcards(v)}%" }
11
+ :arel_predicate => 'matches',
12
+ :formatter => proc { |v| "%#{escape_wildcards(v)}%" }
12
13
  }
13
14
  ],
14
15
  ['not_cont', {
15
- arel_predicate: 'does_not_match',
16
- formatter: proc { |v| "%#{escape_wildcards(v)}%" }
16
+ :arel_predicate => 'does_not_match',
17
+ :formatter => proc { |v| "%#{escape_wildcards(v)}%" }
17
18
  }
18
19
  ],
19
20
  ['start', {
20
- arel_predicate: 'matches',
21
- formatter: proc { |v| "#{escape_wildcards(v)}%" }
21
+ :arel_predicate => 'matches',
22
+ :formatter => proc { |v| "#{escape_wildcards(v)}%" }
22
23
  }
23
24
  ],
24
25
  ['not_start', {
25
- arel_predicate: 'does_not_match',
26
- formatter: proc { |v| "#{escape_wildcards(v)}%" }
26
+ :arel_predicate => 'does_not_match',
27
+ :formatter => proc { |v| "#{escape_wildcards(v)}%" }
27
28
  }
28
29
  ],
29
30
  ['end', {
30
- arel_predicate: 'matches',
31
- formatter: proc { |v| "%#{escape_wildcards(v)}" }
31
+ :arel_predicate => 'matches',
32
+ :formatter => proc { |v| "%#{escape_wildcards(v)}" }
32
33
  }
33
34
  ],
34
35
  ['not_end', {
35
- arel_predicate: 'does_not_match',
36
- formatter: proc { |v| "%#{escape_wildcards(v)}" }
36
+ :arel_predicate => 'does_not_match',
37
+ :formatter => proc { |v| "%#{escape_wildcards(v)}" }
37
38
  }
38
39
  ],
39
40
  ['true', {
40
- arel_predicate: 'eq',
41
- compounds: false,
42
- type: :boolean,
43
- validator: proc { |v| TRUE_VALUES.include?(v) }
41
+ :arel_predicate => 'eq',
42
+ :compounds => false,
43
+ :type => :boolean,
44
+ :validator => proc { |v| TRUE_VALUES.include?(v) }
44
45
  }
45
46
  ],
46
47
  ['false', {
47
- arel_predicate: 'eq',
48
- compounds: false,
49
- type: :boolean,
50
- validator: proc { |v| TRUE_VALUES.include?(v) },
51
- formatter: proc { |v| !v }
48
+ :arel_predicate => 'eq',
49
+ :compounds => false,
50
+ :type => :boolean,
51
+ :validator => proc { |v| TRUE_VALUES.include?(v) },
52
+ :formatter => proc { |v| !v }
52
53
  }
53
54
  ],
54
55
  ['present', {
55
- arel_predicate: 'not_eq_all',
56
- compounds: false,
57
- type: :boolean,
58
- validator: proc { |v| TRUE_VALUES.include?(v) },
59
- formatter: proc { |v| [nil, ''] }
56
+ :arel_predicate => proc { |v| v ? 'not_eq_all' : 'eq_any' },
57
+ :compounds => false,
58
+ :type => :boolean,
59
+ :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
60
+ :formatter => proc { |v| [nil, ''] }
60
61
  }
61
62
  ],
62
63
  ['blank', {
63
- arel_predicate: 'eq_any',
64
- compounds: false,
65
- type: :boolean,
66
- validator: proc { |v| TRUE_VALUES.include?(v) },
67
- formatter: proc { |v| [nil, ''] }
64
+ :arel_predicate => proc { |v| v ? 'eq_any' : 'not_eq_all' },
65
+ :compounds => false,
66
+ :type => :boolean,
67
+ :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
68
+ :formatter => proc { |v| [nil, ''] }
68
69
  }
69
70
  ],
70
71
  ['null', {
71
- arel_predicate: 'eq',
72
- compounds: false,
73
- type: :boolean,
74
- validator: proc { |v| TRUE_VALUES.include?(v)},
75
- formatter: proc { |v| nil }
72
+ :arel_predicate => proc { |v| v ? 'eq' : 'not_eq' },
73
+ :compounds => false,
74
+ :type => :boolean,
75
+ :validator => proc { |v| BOOLEAN_VALUES.include?(v)},
76
+ :formatter => proc { |v| nil }
76
77
  }
77
78
  ],
78
79
  ['not_null', {
79
- arel_predicate: 'not_eq',
80
- compounds: false,
81
- type: :boolean,
82
- validator: proc { |v| TRUE_VALUES.include?(v) },
83
- formatter: proc { |v| nil } }
80
+ :arel_predicate => proc { |v| v ? 'not_eq' : 'eq' },
81
+ :compounds => false,
82
+ :type => :boolean,
83
+ :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
84
+ :formatter => proc { |v| nil } }
84
85
  ]
85
86
  ]
86
87
 
87
- module_function
88
+ module_function
88
89
  # replace % \ to \% \\
89
90
  def escape_wildcards(unescaped)
90
91
  case ActiveRecord::Base.connection.adapter_name
91
- when "SQLite"
92
- unescaped
93
- else
92
+ when "Mysql2", "PostgreSQL"
94
93
  # Necessary for PostgreSQL and MySQL
95
94
  unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
95
+ else
96
+ unescaped
96
97
  end
97
98
  end
98
99
  end
@@ -2,7 +2,7 @@ require 'ransack/visitor'
2
2
 
3
3
  module Ransack
4
4
  class Context
5
- attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
5
+ attr_reader :object, :klass, :base, :engine, :arel_visitor
6
6
  attr_accessor :auth_object, :search_key
7
7
 
8
8
  class << self
@@ -46,7 +46,7 @@ module Ransack
46
46
  end
47
47
 
48
48
  @default_table = Arel::Table.new(
49
- @base.table_name, as: @base.aliased_table_name, engine: @engine
49
+ @base.table_name, :as => @base.aliased_table_name, :engine => @engine
50
50
  )
51
51
  @bind_pairs = Hash.new do |hash, key|
52
52
  parent, attr_name = get_parent_and_attribute_name(key.to_s)
@@ -77,6 +77,19 @@ module Ransack
77
77
  table_for(parent)[attr_name]
78
78
  end
79
79
 
80
+ def chain_scope(scope, args)
81
+ return unless @klass.method(scope) && args != false
82
+ @object = if scope_arity(scope) < 1 && args == true
83
+ @object.public_send(scope)
84
+ else
85
+ @object.public_send(scope, *args)
86
+ end
87
+ end
88
+
89
+ def scope_arity(scope)
90
+ @klass.method(scope).arity
91
+ end
92
+
80
93
  def bind(object, str)
81
94
  object.parent, object.attr_name = @bind_pairs[str]
82
95
  end
@@ -143,6 +156,10 @@ module Ransack
143
156
  klass.ransackable_associations(auth_object).include? str
144
157
  end
145
158
 
159
+ def ransackable_scope?(str, klass)
160
+ klass.ransackable_scopes(auth_object).any? { |s| s.to_s == str }
161
+ end
162
+
146
163
  def searchable_attributes(str = '')
147
164
  traverse(str).ransackable_attributes(auth_object)
148
165
  end
@@ -13,7 +13,7 @@ module Ransack
13
13
  text = args.first
14
14
  i18n = options[:i18n] || {}
15
15
  text ||= object.translate(
16
- method, i18n.reverse_merge(include_associations: true)
16
+ method, i18n.reverse_merge(:include_associations => true)
17
17
  ) if object.respond_to? :translate
18
18
  super(method, text, options, &block)
19
19
  end
@@ -127,7 +127,8 @@ module Ransack
127
127
  end
128
128
 
129
129
  def combinator_select(options = {}, html_options = {})
130
- template_collection_select(:m, combinator_choices, options, html_options)
130
+ template_collection_select(
131
+ :m, combinator_choices, options, html_options)
131
132
  end
132
133
 
133
134
  private
@@ -202,7 +203,7 @@ module Ransack
202
203
 
203
204
  def get_attribute_element(action, base)
204
205
  begin
205
- [Translate.association(base, context: object.context),
206
+ [Translate.association(base, :context => object.context),
206
207
  collection_for_base(action, base)]
207
208
  rescue UntraversableAssociationError => e
208
209
  nil
@@ -214,7 +215,7 @@ module Ransack
214
215
  [attr_from_base_and_column(base, c),
215
216
  Translate.attribute(
216
217
  attr_from_base_and_column(base, c),
217
- context: object.context
218
+ :context => object.context
218
219
  )
219
220
  ]
220
221
  end
@@ -2,6 +2,26 @@ module Ransack
2
2
  module Helpers
3
3
  module FormHelper
4
4
 
5
+ def asc
6
+ 'asc'.freeze
7
+ end
8
+
9
+ def desc
10
+ 'desc'.freeze
11
+ end
12
+
13
+ def asc_arrow
14
+ '&#9650;'.freeze
15
+ end
16
+
17
+ def desc_arrow
18
+ '&#9660;'.freeze
19
+ end
20
+
21
+ def non_breaking_space
22
+ '&nbsp;'.freeze
23
+ end
24
+
5
25
  def search_form_for(record, options = {}, &proc)
6
26
  if record.is_a?(Ransack::Search)
7
27
  search = record
@@ -20,15 +40,15 @@ module Ransack
20
40
  end
21
41
  options[:html] ||= {}
22
42
  html_options = {
23
- class: options[:class].present? ?
43
+ :class => options[:class].present? ?
24
44
  "#{options[:class]}" :
25
45
  "#{search.klass.to_s.underscore}_search",
26
- id: options[:id].present? ?
46
+ :id => options[:id].present? ?
27
47
  "#{options[:id]}" :
28
48
  "#{search.klass.to_s.underscore}_search",
29
- method: :get
49
+ :method => :get
30
50
  }
31
- options[:as] ||= 'q'
51
+ options[:as] ||= 'q'.freeze
32
52
  options[:html].reverse_merge!(html_options)
33
53
  options[:builder] ||= FormBuilder
34
54
 
@@ -45,7 +65,7 @@ module Ransack
45
65
  raise TypeError, "First argument must be a Ransack::Search!" unless
46
66
  Search === search
47
67
 
48
- search_params = params[search.context.search_key] ||
68
+ search_params = params[search.context.search_key].presence ||
49
69
  {}.with_indifferent_access
50
70
 
51
71
  attr_name = attribute.to_s
@@ -54,7 +74,7 @@ module Ransack
54
74
  if args.size > 0 && !args.first.is_a?(Hash)
55
75
  args.shift.to_s
56
76
  else
57
- Translate.attribute(attr_name, context: search.context)
77
+ Translate.attribute(attr_name, :context => search.context)
58
78
  end
59
79
  )
60
80
 
@@ -67,9 +87,9 @@ module Ransack
67
87
  current_dir = prev_attr == attr_name ? prev_dir : nil
68
88
 
69
89
  if current_dir
70
- new_dir = current_dir == 'desc' ? 'asc' : 'desc'
90
+ new_dir = current_dir == desc ? asc : desc
71
91
  else
72
- new_dir = default_order || 'asc'
92
+ new_dir = default_order || asc
73
93
  end
74
94
 
75
95
  html_options = args.first.is_a?(Hash) ? args.shift.dup : {}
@@ -77,7 +97,7 @@ module Ransack
77
97
  html_options[:class] = [css, html_options[:class]].compact.join(' ')
78
98
  query_hash = {}
79
99
  query_hash[search.context.search_key] = search_params
80
- .merge(s: "#{attr_name} #{new_dir}")
100
+ .merge(:s => "#{attr_name} #{new_dir}")
81
101
  options.merge!(query_hash)
82
102
  options_for_url = params.merge options
83
103
 
@@ -90,7 +110,7 @@ module Ransack
90
110
  link_to(
91
111
  [ERB::Util.h(name), order_indicator_for(current_dir)]
92
112
  .compact
93
- .join(' ')
113
+ .join(non_breaking_space)
94
114
  .html_safe,
95
115
  url,
96
116
  html_options
@@ -100,10 +120,10 @@ module Ransack
100
120
  private
101
121
 
102
122
  def order_indicator_for(order)
103
- if order == 'asc'
104
- '&#9650;'
105
- elsif order == 'desc'
106
- '&#9660;'
123
+ if order == asc
124
+ asc_arrow
125
+ elsif order == desc
126
+ desc_arrow
107
127
  else
108
128
  nil
109
129
  end