ransack_ffcrm 0.6.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 (56) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +40 -0
  4. data/LICENSE +20 -0
  5. data/README.md +137 -0
  6. data/Rakefile +19 -0
  7. data/lib/ransack/adapters/active_record/3.0/compat.rb +166 -0
  8. data/lib/ransack/adapters/active_record/3.0/context.rb +161 -0
  9. data/lib/ransack/adapters/active_record/3.1/context.rb +166 -0
  10. data/lib/ransack/adapters/active_record/base.rb +33 -0
  11. data/lib/ransack/adapters/active_record/context.rb +41 -0
  12. data/lib/ransack/adapters/active_record.rb +12 -0
  13. data/lib/ransack/configuration.rb +35 -0
  14. data/lib/ransack/constants.rb +23 -0
  15. data/lib/ransack/context.rb +124 -0
  16. data/lib/ransack/helpers/form_builder.rb +203 -0
  17. data/lib/ransack/helpers/form_helper.rb +75 -0
  18. data/lib/ransack/helpers.rb +2 -0
  19. data/lib/ransack/locale/en.yml +70 -0
  20. data/lib/ransack/naming.rb +53 -0
  21. data/lib/ransack/nodes/attribute.rb +49 -0
  22. data/lib/ransack/nodes/bindable.rb +30 -0
  23. data/lib/ransack/nodes/condition.rb +212 -0
  24. data/lib/ransack/nodes/grouping.rb +183 -0
  25. data/lib/ransack/nodes/node.rb +34 -0
  26. data/lib/ransack/nodes/sort.rb +41 -0
  27. data/lib/ransack/nodes/value.rb +108 -0
  28. data/lib/ransack/nodes.rb +7 -0
  29. data/lib/ransack/predicate.rb +70 -0
  30. data/lib/ransack/ransacker.rb +24 -0
  31. data/lib/ransack/search.rb +123 -0
  32. data/lib/ransack/translate.rb +92 -0
  33. data/lib/ransack/version.rb +3 -0
  34. data/lib/ransack/visitor.rb +68 -0
  35. data/lib/ransack.rb +27 -0
  36. data/ransack_ffcrm.gemspec +30 -0
  37. data/spec/blueprints/articles.rb +5 -0
  38. data/spec/blueprints/comments.rb +5 -0
  39. data/spec/blueprints/notes.rb +3 -0
  40. data/spec/blueprints/people.rb +4 -0
  41. data/spec/blueprints/tags.rb +3 -0
  42. data/spec/console.rb +21 -0
  43. data/spec/helpers/ransack_helper.rb +2 -0
  44. data/spec/ransack/adapters/active_record/base_spec.rb +67 -0
  45. data/spec/ransack/adapters/active_record/context_spec.rb +45 -0
  46. data/spec/ransack/configuration_spec.rb +31 -0
  47. data/spec/ransack/helpers/form_builder_spec.rb +137 -0
  48. data/spec/ransack/helpers/form_helper_spec.rb +38 -0
  49. data/spec/ransack/nodes/condition_spec.rb +15 -0
  50. data/spec/ransack/nodes/grouping_spec.rb +13 -0
  51. data/spec/ransack/predicate_spec.rb +55 -0
  52. data/spec/ransack/search_spec.rb +225 -0
  53. data/spec/spec_helper.rb +47 -0
  54. data/spec/support/en.yml +5 -0
  55. data/spec/support/schema.rb +111 -0
  56. metadata +229 -0
@@ -0,0 +1,166 @@
1
+ require 'ransack/context'
2
+ require 'polyamorous'
3
+
4
+ module Ransack
5
+ module Adapters
6
+ module ActiveRecord
7
+ class Context < ::Ransack::Context
8
+ # Because the AR::Associations namespace is insane
9
+ JoinDependency = ::ActiveRecord::Associations::JoinDependency
10
+ JoinPart = JoinDependency::JoinPart
11
+
12
+ def initialize(object, options = {})
13
+ super
14
+ @arel_visitor = Arel::Visitors.visitor_for @engine
15
+ end
16
+
17
+ def evaluate(search, opts = {})
18
+ viz = Visitor.new
19
+ relation = @object.where(viz.accept(search.base))
20
+ if search.sorts.any?
21
+ relation = relation.except(:order).order(viz.accept(search.sorts))
22
+ end
23
+ opts[:distinct] ? relation.select("DISTINCT #{@klass.quoted_table_name}.*") : relation
24
+ end
25
+
26
+ def attribute_method?(str, klass = @klass)
27
+ exists = false
28
+
29
+ if ransackable_attribute?(str, klass)
30
+ exists = true
31
+ elsif (segments = str.split(/_/)).size > 1
32
+ remainder = []
33
+ found_assoc = nil
34
+ while !found_assoc && remainder.unshift(segments.pop) && segments.size > 0 do
35
+ assoc, poly_class = unpolymorphize_association(segments.join('_'))
36
+ if found_assoc = get_association(assoc, klass)
37
+ exists = attribute_method?(remainder.join('_'), poly_class || found_assoc.klass)
38
+ end
39
+ end
40
+ end
41
+
42
+ exists
43
+ end
44
+
45
+ def table_for(parent)
46
+ parent.table
47
+ end
48
+
49
+ def klassify(obj)
50
+ if Class === obj && ::ActiveRecord::Base > obj
51
+ obj
52
+ elsif obj.respond_to? :klass
53
+ obj.klass
54
+ elsif obj.respond_to? :active_record
55
+ obj.active_record
56
+ else
57
+ raise ArgumentError, "Don't know how to klassify #{obj}"
58
+ end
59
+ end
60
+
61
+ def type_for(attr)
62
+ return nil unless attr && attr.valid?
63
+ name = attr.arel_attribute.name.to_s
64
+ table = attr.arel_attribute.relation.table_name
65
+
66
+ unless @engine.connection_pool.table_exists?(table)
67
+ raise "No table named #{table} exists"
68
+ end
69
+
70
+ @engine.connection_pool.columns_hash[table][name].type
71
+ end
72
+
73
+ private
74
+
75
+ def get_parent_and_attribute_name(str, parent = @base)
76
+ attr_name = nil
77
+
78
+ if ransackable_attribute?(str, klassify(parent))
79
+ attr_name = str
80
+ elsif (segments = str.split(/_/)).size > 1
81
+ remainder = []
82
+ found_assoc = nil
83
+ while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
84
+ assoc, klass = unpolymorphize_association(segments.join('_'))
85
+ if found_assoc = get_association(assoc, parent)
86
+ join = build_or_find_association(found_assoc.name, parent, klass)
87
+ parent, attr_name = get_parent_and_attribute_name(remainder.join('_'), join)
88
+ end
89
+ end
90
+ end
91
+
92
+ [parent, attr_name]
93
+ end
94
+
95
+ def get_association(str, parent = @base)
96
+ klass = klassify parent
97
+ ransackable_association?(str, klass) &&
98
+ klass.reflect_on_all_associations.detect {|a| a.name.to_s == str}
99
+ end
100
+
101
+ def join_dependency(relation)
102
+ if relation.respond_to?(:join_dependency) # Squeel will enable this
103
+ relation.join_dependency
104
+ else
105
+ build_join_dependency(relation)
106
+ end
107
+ end
108
+
109
+ def build_join_dependency(relation)
110
+ buckets = relation.joins_values.group_by do |join|
111
+ case join
112
+ when String
113
+ 'string_join'
114
+ when Hash, Symbol, Array
115
+ 'association_join'
116
+ when ::ActiveRecord::Associations::JoinDependency::JoinAssociation
117
+ 'stashed_join'
118
+ when Arel::Nodes::Join
119
+ 'join_node'
120
+ else
121
+ raise 'unknown class: %s' % join.class.name
122
+ end
123
+ end
124
+
125
+ association_joins = buckets['association_join'] || []
126
+ stashed_association_joins = buckets['stashed_join'] || []
127
+ join_nodes = buckets['join_node'] || []
128
+ string_joins = (buckets['string_join'] || []).map { |x|
129
+ x.strip
130
+ }.uniq
131
+
132
+ join_list = relation.send :custom_join_ast, relation.table.from(relation.table), string_joins
133
+
134
+ join_dependency = JoinDependency.new(
135
+ relation.klass,
136
+ association_joins,
137
+ join_list
138
+ )
139
+
140
+ join_nodes.each do |join|
141
+ join_dependency.table_aliases[join.left.name.downcase] = 1
142
+ end
143
+
144
+ join_dependency.graft(*stashed_association_joins)
145
+ end
146
+
147
+ def build_or_find_association(name, parent = @base, klass = nil)
148
+ found_association = @join_dependency.join_associations.detect do |assoc|
149
+ assoc.reflection.name == name &&
150
+ assoc.parent == parent &&
151
+ (!klass || assoc.reflection.klass == klass)
152
+ end
153
+ unless found_association
154
+ @join_dependency.send(:build, Polyamorous::Join.new(name, @join_type, klass), parent)
155
+ found_association = @join_dependency.join_associations.last
156
+ # Leverage the stashed association functionality in AR
157
+ @object = @object.joins(found_association)
158
+ end
159
+
160
+ found_association
161
+ end
162
+
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,33 @@
1
+ module Ransack
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Base
5
+
6
+ def self.extended(base)
7
+ alias :search :ransack unless base.method_defined? :search
8
+ base.class_eval do
9
+ class_attribute :_ransackers
10
+ self._ransackers ||= {}
11
+ end
12
+ end
13
+
14
+ def ransack(params = {}, options = {})
15
+ Search.new(self, params, options)
16
+ end
17
+
18
+ def ransacker(name, opts = {}, &block)
19
+ self._ransackers = _ransackers.merge name.to_s => Ransacker.new(self, name, opts, &block)
20
+ end
21
+
22
+ def ransackable_attributes(auth_object = nil)
23
+ column_names + _ransackers.keys
24
+ end
25
+
26
+ def ransackable_associations(auth_object = nil)
27
+ reflect_on_all_associations.map {|a| a.name.to_s}
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ require 'ransack/context'
2
+ require 'ransack/adapters/active_record/3.1/context'
3
+ require 'polyamorous'
4
+
5
+ module Ransack
6
+ module Adapters
7
+ module ActiveRecord
8
+ class Context < ::Ransack::Context
9
+
10
+ # Redefine a few things that have changed with 3.2.
11
+
12
+ def initialize(object, options = {})
13
+ super
14
+ @arel_visitor = @engine.connection.visitor
15
+ end
16
+
17
+ def type_for(attr)
18
+ return nil unless attr && attr.valid?
19
+ name = attr.arel_attribute.name.to_s
20
+ table = attr.arel_attribute.relation.table_name
21
+
22
+ unless @engine.connection.table_exists?(table)
23
+ raise "No table named #{table} exists"
24
+ end
25
+
26
+ @engine.connection.schema_cache.columns_hash[table][name].type
27
+ end
28
+
29
+ def evaluate(search, opts = {})
30
+ viz = Visitor.new
31
+ relation = @object.where(viz.accept(search.base))
32
+ if search.sorts.any?
33
+ relation = relation.except(:order).order(viz.accept(search.sorts))
34
+ end
35
+ opts[:distinct] ? relation.uniq : relation
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_record'
2
+ require 'ransack/adapters/active_record/base'
3
+ ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base
4
+
5
+ case ActiveRecord::VERSION::STRING
6
+ when /^3\.0\./
7
+ require 'ransack/adapters/active_record/3.0/context'
8
+ when /^3\.1\./
9
+ require 'ransack/adapters/active_record/3.1/context'
10
+ else
11
+ require 'ransack/adapters/active_record/context'
12
+ end
@@ -0,0 +1,35 @@
1
+ require 'ransack/constants'
2
+ require 'ransack/predicate'
3
+
4
+ module Ransack
5
+ module Configuration
6
+
7
+ mattr_accessor :predicates
8
+ self.predicates = {}
9
+
10
+ def configure
11
+ yield self
12
+ end
13
+
14
+ def add_predicate(name, opts = {})
15
+ name = name.to_s
16
+ opts[:name] = name
17
+ compounds = opts.delete(:compounds)
18
+ compounds = true if compounds.nil?
19
+ opts[:arel_predicate] = opts[:arel_predicate].to_s
20
+
21
+ self.predicates[name] = Predicate.new(opts)
22
+
23
+ ['_any', '_all'].each do |suffix|
24
+ self.predicates[name + suffix] = Predicate.new(
25
+ opts.merge(
26
+ :name => name + suffix,
27
+ :arel_predicate => opts[:arel_predicate] + suffix,
28
+ :compound => true
29
+ )
30
+ )
31
+ end if compounds
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ module Ransack
2
+ module Constants
3
+ TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
4
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
5
+
6
+ AREL_PREDICATES = %w(eq not_eq matches does_not_match lt lteq gt gteq in not_in)
7
+
8
+ DERIVED_PREDICATES = [
9
+ ['cont', {:arel_predicate => 'matches', :formatter => proc {|v| "%#{v}%"}}],
10
+ ['not_cont', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "%#{v}%"}}],
11
+ ['start', {:arel_predicate => 'matches', :formatter => proc {|v| "#{v}%"}}],
12
+ ['not_start', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "#{v}%"}}],
13
+ ['end', {:arel_predicate => 'matches', :formatter => proc {|v| "%#{v}"}}],
14
+ ['not_end', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "%#{v}"}}],
15
+ ['true', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}}],
16
+ ['false', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| !v}}],
17
+ ['present', {:arel_predicate => 'not_eq_all', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| [nil, '']}}],
18
+ ['blank', {:arel_predicate => 'eq_any', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| [nil, '']}}],
19
+ ['null', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| nil}}],
20
+ ['not_null', {:arel_predicate => 'not_eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| nil}}]
21
+ ]
22
+ end
23
+ end
@@ -0,0 +1,124 @@
1
+ require 'ransack/visitor'
2
+
3
+ module Ransack
4
+ class Context
5
+ attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
6
+ attr_accessor :auth_object
7
+
8
+ class << self
9
+
10
+ def for(object, options = {})
11
+ context = Class === object ? for_class(object, options) : for_object(object, options)
12
+ context or raise ArgumentError, "Don't know what context to use for #{object}"
13
+ end
14
+
15
+ def for_class(klass, options = {})
16
+ if klass < ActiveRecord::Base
17
+ Adapters::ActiveRecord::Context.new(klass, options)
18
+ end
19
+ end
20
+
21
+ def for_object(object, options = {})
22
+ case object
23
+ when ActiveRecord::Relation
24
+ Adapters::ActiveRecord::Context.new(object.klass, options)
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ def initialize(object, options = {})
31
+ @object = object.scoped
32
+ @klass = @object.klass
33
+ @join_dependency = join_dependency(@object)
34
+ @join_type = options[:join_type] || Arel::OuterJoin
35
+ @base = @join_dependency.join_base
36
+ @engine = @base.arel_engine
37
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
38
+ @bind_pairs = Hash.new do |hash, key|
39
+ parent, attr_name = get_parent_and_attribute_name(key.to_s)
40
+ if parent && attr_name
41
+ hash[key] = [parent, attr_name]
42
+ end
43
+ end
44
+ end
45
+
46
+ # Convert a string representing a chain of associations and an attribute
47
+ # into the attribute itself
48
+ def contextualize(str)
49
+ parent, attr_name = @bind_pairs[str]
50
+ table_for(parent)[attr_name]
51
+ end
52
+
53
+ def bind(object, str)
54
+ object.parent, object.attr_name = @bind_pairs[str]
55
+ end
56
+
57
+ def traverse(str, base = @base)
58
+ str ||= ''
59
+
60
+ if (segments = str.split(/_/)).size > 0
61
+ remainder = []
62
+ found_assoc = nil
63
+ while !found_assoc && segments.size > 0 do
64
+ # Strip the _of_Model_type text from the association name, but hold
65
+ # onto it in klass, for use as the next base
66
+ assoc, klass = unpolymorphize_association(segments.join('_'))
67
+ if found_assoc = get_association(assoc, base)
68
+ base = traverse(remainder.join('_'), klass || found_assoc.klass)
69
+ end
70
+
71
+ remainder.unshift segments.pop
72
+ end
73
+ raise UntraversableAssociationError, "No association matches #{str}" unless found_assoc
74
+ end
75
+
76
+ klassify(base)
77
+ end
78
+
79
+ def association_path(str, base = @base)
80
+ base = klassify(base)
81
+ str ||= ''
82
+ path = []
83
+ segments = str.split(/_/)
84
+ association_parts = []
85
+ if (segments = str.split(/_/)).size > 0
86
+ while segments.size > 0 && !base.columns_hash[segments.join('_')] && association_parts << segments.shift do
87
+ assoc, klass = unpolymorphize_association(association_parts.join('_'))
88
+ if found_assoc = get_association(assoc, base)
89
+ path += association_parts
90
+ association_parts = []
91
+ base = klassify(klass || found_assoc)
92
+ end
93
+ end
94
+ end
95
+
96
+ path.join('_')
97
+ end
98
+
99
+ def unpolymorphize_association(str)
100
+ if (match = str.match(/_of_([^_]+?)_type$/))
101
+ [match.pre_match, Kernel.const_get(match.captures.first)]
102
+ else
103
+ [str, nil]
104
+ end
105
+ end
106
+
107
+ def ransackable_attribute?(str, klass)
108
+ klass.ransackable_attributes(auth_object).include? str
109
+ end
110
+
111
+ def ransackable_association?(str, klass)
112
+ klass.ransackable_associations(auth_object).include? str
113
+ end
114
+
115
+ def searchable_attributes(str = '')
116
+ traverse(str).ransackable_attributes(auth_object)
117
+ end
118
+
119
+ def searchable_associations(str = '')
120
+ traverse(str).ransackable_associations(auth_object)
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,203 @@
1
+ require 'action_view'
2
+
3
+ module Ransack
4
+ module Helpers
5
+ class FormBuilder < ::ActionView::Helpers::FormBuilder
6
+ def label(method, *args, &block)
7
+ options = args.extract_options!
8
+ text = args.first
9
+ i18n = options[:i18n] || {}
10
+ text ||= object.translate(method, i18n.reverse_merge(:include_associations => true)) if object.respond_to? :translate
11
+ super(method, text, options, &block)
12
+ end
13
+
14
+ def submit(value=nil, options={})
15
+ value, options = nil, value if value.is_a?(Hash)
16
+ value ||= Translate.word(:search).titleize
17
+ super(value, options)
18
+ end
19
+
20
+ def attribute_select(options = {}, html_options = {})
21
+ raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
22
+ options[:include_blank] = true unless options.has_key?(:include_blank)
23
+ bases = [''] + association_array(options[:associations])
24
+ if bases.size > 1
25
+ @template.grouped_collection_select(
26
+ @object_name, :name, attribute_collection_for_bases(bases), :last, :first, :first, :last,
27
+ objectify_options(options), @default_options.merge(html_options)
28
+ )
29
+ else
30
+ collection = object.context.searchable_attributes(bases.first).map do |c|
31
+ [
32
+ attr_from_base_and_column(bases.first, c),
33
+ Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
34
+ ]
35
+ end
36
+ @template.collection_select(
37
+ @object_name, :name, collection, :first, :last,
38
+ objectify_options(options), @default_options.merge(html_options)
39
+ )
40
+ end
41
+ end
42
+
43
+ def sort_select(options = {}, html_options = {})
44
+ raise ArgumentError, "sort_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
45
+ options[:include_blank] = true unless options.has_key?(:include_blank)
46
+ bases = [''] + association_array(options[:associations])
47
+ if bases.size > 1
48
+ @template.grouped_collection_select(
49
+ @object_name, :name, attribute_collection_for_bases(bases), :last, :first, :first, :last,
50
+ objectify_options(options), @default_options.merge(html_options)
51
+ ) + @template.collection_select(
52
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
53
+ objectify_options(options), @default_options.merge(html_options)
54
+ )
55
+ else
56
+ collection = object.context.searchable_attributes(bases.first).map do |c|
57
+ [
58
+ attr_from_base_and_column(bases.first, c),
59
+ Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
60
+ ]
61
+ end
62
+ @template.collection_select(
63
+ @object_name, :name, collection, :first, :last,
64
+ objectify_options(options), @default_options.merge(html_options)
65
+ ) + @template.collection_select(
66
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
67
+ objectify_options(options), @default_options.merge(html_options)
68
+ )
69
+ end
70
+ end
71
+
72
+ def sort_fields(*args, &block)
73
+ search_fields(:s, args, block)
74
+ end
75
+
76
+ def sort_link(attribute, *args)
77
+ @template.sort_link @object, attribute, *args
78
+ end
79
+
80
+ def condition_fields(*args, &block)
81
+ search_fields(:c, args, block)
82
+ end
83
+
84
+ def grouping_fields(*args, &block)
85
+ search_fields(:g, args, block)
86
+ end
87
+
88
+ def attribute_fields(*args, &block)
89
+ search_fields(:a, args, block)
90
+ end
91
+
92
+ def predicate_fields(*args, &block)
93
+ search_fields(:p, args, block)
94
+ end
95
+
96
+ def value_fields(*args, &block)
97
+ search_fields(:v, args, block)
98
+ end
99
+
100
+ def search_fields(name, args, block)
101
+ args << {} unless args.last.is_a?(Hash)
102
+ args.last[:builder] ||= options[:builder]
103
+ args.last[:parent_builder] = self
104
+ options = args.extract_options!
105
+ objects = args.shift
106
+ objects ||= @object.send(name)
107
+ objects = [objects] unless Array === objects
108
+ name = "#{options[:object_name] || object_name}[#{name}]"
109
+ output = ActiveSupport::SafeBuffer.new
110
+ objects.each do |child|
111
+ output << @template.fields_for("#{name}[#{options[:child_index] || nested_child_index(name)}]", child, options, &block)
112
+ end
113
+ output
114
+ end
115
+
116
+ def predicate_select(options = {}, html_options = {})
117
+ options[:compounds] = true if options[:compounds].nil?
118
+ keys = predicate_keys(options)
119
+ # If condition is newly built with build_condition(),
120
+ # then replace the default predicate with the first in the ordered list
121
+ @object.predicate_name = keys.first if @object.default?
122
+ @template.collection_select(
123
+ @object_name, :p, keys.map {|k| [k, Translate.predicate(k)]}, :first, :last,
124
+ objectify_options(options), @default_options.merge(html_options)
125
+ )
126
+ end
127
+
128
+ def combinator_select(options = {}, html_options = {})
129
+ @template.collection_select(
130
+ @object_name, :m, combinator_choices, :first, :last,
131
+ objectify_options(options), @default_options.merge(html_options)
132
+ )
133
+ end
134
+
135
+ private
136
+
137
+ def predicate_keys(options)
138
+ keys = options[:compounds] ? Predicate.names : Predicate.names.reject {|k| k.match(/_(any|all)$/)}
139
+ if only = options[:only]
140
+ if only.respond_to? :call
141
+ keys = keys.select {|k| only.call(k)}
142
+ else
143
+ only = Array.wrap(only).map(&:to_s)
144
+ # Create compounds hash, e.g. {"eq" => ["eq", "eq_any", "eq_all"], "blank" => ["blank"]}
145
+ key_groups = keys.inject(Hash.new([])){ |h,k| h[k.sub(/_(any|all)$/, '')] += [k]; h }
146
+ # Order compounds hash by 'only' keys
147
+ keys = only.map {|k| key_groups[k] }.flatten.compact
148
+ end
149
+ end
150
+ keys
151
+ end
152
+
153
+ def combinator_choices
154
+ if Nodes::Condition === object
155
+ [['or', Translate.word(:any)], ['and', Translate.word(:all)]]
156
+ else
157
+ [['and', Translate.word(:all)], ['or', Translate.word(:any)]]
158
+ end
159
+ end
160
+
161
+ def association_array(obj, prefix = nil)
162
+ ([prefix] + case obj
163
+ when Array
164
+ obj
165
+ when Hash
166
+ obj.map do |key, value|
167
+ case value
168
+ when Array, Hash
169
+ association_array(value, key.to_s)
170
+ else
171
+ [key.to_s, [key, value].join('_')]
172
+ end
173
+ end
174
+ else
175
+ [obj]
176
+ end).compact.flatten.map {|v| [prefix, v].compact.join('_')}
177
+ end
178
+
179
+ def attr_from_base_and_column(base, column)
180
+ [base, column].reject {|v| v.blank?}.join('_')
181
+ end
182
+
183
+ def attribute_collection_for_bases(bases)
184
+ bases.map do |base|
185
+ begin
186
+ [
187
+ Translate.association(base, :context => object.context),
188
+ object.context.searchable_attributes(base).map do |c|
189
+ [
190
+ attr_from_base_and_column(base, c),
191
+ Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
192
+ ]
193
+ end
194
+ ]
195
+ rescue UntraversableAssociationError => e
196
+ nil
197
+ end
198
+ end.compact
199
+ end
200
+
201
+ end
202
+ end
203
+ end