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.
- data/.gitignore +4 -0
- data/.travis.yml +9 -0
- data/Gemfile +40 -0
- data/LICENSE +20 -0
- data/README.md +137 -0
- data/Rakefile +19 -0
- data/lib/ransack/adapters/active_record/3.0/compat.rb +166 -0
- data/lib/ransack/adapters/active_record/3.0/context.rb +161 -0
- data/lib/ransack/adapters/active_record/3.1/context.rb +166 -0
- data/lib/ransack/adapters/active_record/base.rb +33 -0
- data/lib/ransack/adapters/active_record/context.rb +41 -0
- data/lib/ransack/adapters/active_record.rb +12 -0
- data/lib/ransack/configuration.rb +35 -0
- data/lib/ransack/constants.rb +23 -0
- data/lib/ransack/context.rb +124 -0
- data/lib/ransack/helpers/form_builder.rb +203 -0
- data/lib/ransack/helpers/form_helper.rb +75 -0
- data/lib/ransack/helpers.rb +2 -0
- data/lib/ransack/locale/en.yml +70 -0
- data/lib/ransack/naming.rb +53 -0
- data/lib/ransack/nodes/attribute.rb +49 -0
- data/lib/ransack/nodes/bindable.rb +30 -0
- data/lib/ransack/nodes/condition.rb +212 -0
- data/lib/ransack/nodes/grouping.rb +183 -0
- data/lib/ransack/nodes/node.rb +34 -0
- data/lib/ransack/nodes/sort.rb +41 -0
- data/lib/ransack/nodes/value.rb +108 -0
- data/lib/ransack/nodes.rb +7 -0
- data/lib/ransack/predicate.rb +70 -0
- data/lib/ransack/ransacker.rb +24 -0
- data/lib/ransack/search.rb +123 -0
- data/lib/ransack/translate.rb +92 -0
- data/lib/ransack/version.rb +3 -0
- data/lib/ransack/visitor.rb +68 -0
- data/lib/ransack.rb +27 -0
- data/ransack_ffcrm.gemspec +30 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/notes.rb +3 -0
- data/spec/blueprints/people.rb +4 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/console.rb +21 -0
- data/spec/helpers/ransack_helper.rb +2 -0
- data/spec/ransack/adapters/active_record/base_spec.rb +67 -0
- data/spec/ransack/adapters/active_record/context_spec.rb +45 -0
- data/spec/ransack/configuration_spec.rb +31 -0
- data/spec/ransack/helpers/form_builder_spec.rb +137 -0
- data/spec/ransack/helpers/form_helper_spec.rb +38 -0
- data/spec/ransack/nodes/condition_spec.rb +15 -0
- data/spec/ransack/nodes/grouping_spec.rb +13 -0
- data/spec/ransack/predicate_spec.rb +55 -0
- data/spec/ransack/search_spec.rb +225 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/en.yml +5 -0
- data/spec/support/schema.rb +111 -0
- 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
|