ransack 1.2.3 → 1.3.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.
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