meta_search_hub 1.1.3
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 +7 -0
- data/.document +5 -0
- data/.gitmodules +6 -0
- data/CHANGELOG +109 -0
- data/Gemfile +12 -0
- data/LICENSE +20 -0
- data/README.rdoc +438 -0
- data/Rakefile +69 -0
- data/VERSION +1 -0
- data/lib/meta_search/builder.rb +316 -0
- data/lib/meta_search/exceptions.rb +17 -0
- data/lib/meta_search/helpers/form_builder.rb +166 -0
- data/lib/meta_search/helpers/form_helper.rb +24 -0
- data/lib/meta_search/helpers/url_helper.rb +66 -0
- data/lib/meta_search/helpers.rb +3 -0
- data/lib/meta_search/locale/en.yml +24 -0
- data/lib/meta_search/method.rb +129 -0
- data/lib/meta_search/model_compatibility.rb +75 -0
- data/lib/meta_search/searches/active_record.rb +203 -0
- data/lib/meta_search/utility.rb +110 -0
- data/lib/meta_search/where.rb +264 -0
- data/lib/meta_search.rb +58 -0
- data/meta_search.gemspec +90 -0
- data/test/fixtures/companies.yml +17 -0
- data/test/fixtures/company.rb +22 -0
- data/test/fixtures/data_type.rb +5 -0
- data/test/fixtures/data_types.yml +15 -0
- data/test/fixtures/developer.rb +11 -0
- data/test/fixtures/developers.yml +62 -0
- data/test/fixtures/developers_projects.yml +25 -0
- data/test/fixtures/note.rb +3 -0
- data/test/fixtures/notes.yml +79 -0
- data/test/fixtures/project.rb +4 -0
- data/test/fixtures/projects.yml +34 -0
- data/test/fixtures/schema.rb +47 -0
- data/test/helper.rb +44 -0
- data/test/locales/es.yml +5 -0
- data/test/locales/flanders.yml +18 -0
- data/test/test_search.rb +969 -0
- data/test/test_view_helpers.rb +447 -0
- metadata +160 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
require 'polyamorous'
|
|
2
|
+
require 'meta_search/model_compatibility'
|
|
3
|
+
require 'meta_search/exceptions'
|
|
4
|
+
require 'meta_search/where'
|
|
5
|
+
require 'meta_search/utility'
|
|
6
|
+
|
|
7
|
+
module MetaSearch
|
|
8
|
+
# Builder is the workhorse of MetaSearch -- it is the class that handles dynamically generating
|
|
9
|
+
# methods based on a supplied model, and is what gets instantiated when you call your model's search
|
|
10
|
+
# method. Builder doesn't generate any methods until they're needed, using method_missing to compare
|
|
11
|
+
# requested method names against your model's attributes, associations, and the configured Where
|
|
12
|
+
# list.
|
|
13
|
+
#
|
|
14
|
+
# === Attributes
|
|
15
|
+
#
|
|
16
|
+
# * +base+ - The base model that Builder wraps.
|
|
17
|
+
# * +search_attributes+ - Attributes that have been assigned (search terms)
|
|
18
|
+
# * +relation+ - The ActiveRecord::Relation representing the current search.
|
|
19
|
+
# * +join_dependency+ - The JoinDependency object representing current association join
|
|
20
|
+
# dependencies. It's used internally to avoid joining association tables more than
|
|
21
|
+
# once when constructing search queries.
|
|
22
|
+
class Builder
|
|
23
|
+
include ModelCompatibility
|
|
24
|
+
include Utility
|
|
25
|
+
|
|
26
|
+
attr_reader :base, :relation, :search_key, :search_attributes, :join_dependency, :errors, :options
|
|
27
|
+
delegate *RELATION_METHODS + [:to => :relation]
|
|
28
|
+
|
|
29
|
+
# Initialize a new Builder. Requires a base model to wrap, and supports a couple of options
|
|
30
|
+
# for how it will expose this model and its associations to your controllers/views.
|
|
31
|
+
def initialize(base_or_relation, opts = {})
|
|
32
|
+
opts = opts.dup
|
|
33
|
+
@relation = base_or_relation.scoped
|
|
34
|
+
@base = @relation.klass
|
|
35
|
+
@search_key = (opts.delete(:search_key) || 'search').to_s
|
|
36
|
+
@options = opts # Let's just hang on to other options for use in authorization blocks
|
|
37
|
+
@join_type = opts[:join_type] || Arel::Nodes::OuterJoin
|
|
38
|
+
@join_type = get_join_type(@join_type)
|
|
39
|
+
@join_dependency = build_join_dependency(@relation)
|
|
40
|
+
@search_attributes = {}
|
|
41
|
+
@errors = ActiveModel::Errors.new(self)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_column(column, base = @base)
|
|
45
|
+
base.columns_hash[column.to_s] if base._metasearch_attribute_authorized?(column, self)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def get_association(assoc, base = @base)
|
|
49
|
+
base.reflect_on_association(assoc.to_sym) if base._metasearch_association_authorized?(assoc, self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get_attribute(name, parent = @join_dependency.join_base)
|
|
53
|
+
attribute = nil
|
|
54
|
+
if get_column(name, parent.active_record)
|
|
55
|
+
attribute = parent.table[name]
|
|
56
|
+
elsif (segments = name.to_s.split(/_/)).size > 1
|
|
57
|
+
remainder = []
|
|
58
|
+
found_assoc = nil
|
|
59
|
+
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
|
60
|
+
if found_assoc = get_association(segments.join('_'), parent.active_record)
|
|
61
|
+
if found_assoc.options[:polymorphic]
|
|
62
|
+
unless delimiter = remainder.index('type')
|
|
63
|
+
raise PolymorphicAssociationMissingTypeError, "Polymorphic association specified without a type"
|
|
64
|
+
end
|
|
65
|
+
polymorphic_class, attribute_name = remainder[0...delimiter].join('_'),
|
|
66
|
+
remainder[delimiter + 1...remainder.size].join('_')
|
|
67
|
+
polymorphic_class = polymorphic_class.classify.constantize
|
|
68
|
+
join = build_or_find_association(found_assoc.name, parent, polymorphic_class)
|
|
69
|
+
attribute = get_attribute(attribute_name, join)
|
|
70
|
+
else
|
|
71
|
+
join = build_or_find_association(found_assoc.name, parent, found_assoc.klass)
|
|
72
|
+
attribute = get_attribute(remainder.join('_'), join)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
attribute
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Build the search with the given search options. Options are in the form of a hash
|
|
81
|
+
# with keys matching the names creted by the Builder's "wheres" as outlined in
|
|
82
|
+
# MetaSearch::Where
|
|
83
|
+
def build(option_hash)
|
|
84
|
+
opts = option_hash.dup || {}
|
|
85
|
+
@relation = @base.scoped
|
|
86
|
+
opts.stringify_keys!
|
|
87
|
+
opts = collapse_multiparameter_options(opts)
|
|
88
|
+
assign_attributes(opts)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def respond_to?(method_id, include_private = false)
|
|
93
|
+
return true if super
|
|
94
|
+
|
|
95
|
+
method_name = method_id.to_s
|
|
96
|
+
if RELATION_METHODS.map(&:to_s).include?(method_name)
|
|
97
|
+
true
|
|
98
|
+
elsif method_name.match(/^meta_sort=?$/)
|
|
99
|
+
true
|
|
100
|
+
elsif match = method_name.match(/^(.*)\(([0-9]+).*\)$/)
|
|
101
|
+
method_name, index = match.captures
|
|
102
|
+
respond_to?(method_name)
|
|
103
|
+
elsif matches_named_method(method_name) || matches_attribute_method(method_name)
|
|
104
|
+
true
|
|
105
|
+
else
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def assign_attributes(opts)
|
|
113
|
+
opts.each_pair do |k, v|
|
|
114
|
+
self.send("#{k}=", v)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def gauge_depth_of_join_association(ja)
|
|
119
|
+
1 + (ja.respond_to?(:parent) ? gauge_depth_of_join_association(ja.parent) : 0)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def method_missing(method_id, *args, &block)
|
|
123
|
+
method_name = method_id.to_s
|
|
124
|
+
if method_name =~ /^meta_sort=?$/
|
|
125
|
+
(args.any? || method_name =~ /=$/) ? set_sort(args.first) : get_sort
|
|
126
|
+
elsif match = method_name.match(/^(.*)\(([0-9]+).*\)$/) # Multiparameter reader
|
|
127
|
+
method_name, index = match.captures
|
|
128
|
+
vals = self.send(method_name)
|
|
129
|
+
vals.is_a?(Array) ? vals[index.to_i - 1] : nil
|
|
130
|
+
elsif match = matches_named_method(method_name)
|
|
131
|
+
(args.any? || method_name =~ /=$/) ? set_named_method_value(match, args.first) : get_named_method_value(match)
|
|
132
|
+
elsif match = matches_attribute_method(method_id)
|
|
133
|
+
attribute, predicate = match.captures
|
|
134
|
+
(args.any? || method_name =~ /=$/) ? set_attribute_method_value(attribute, predicate, args.first) : get_attribute_method_value(attribute, predicate)
|
|
135
|
+
else
|
|
136
|
+
super
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def matches_named_method(name)
|
|
141
|
+
method_name = name.to_s.sub(/\=$/, '')
|
|
142
|
+
return method_name if @base._metasearch_method_authorized?(method_name, self)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def matches_attribute_method(method_id)
|
|
146
|
+
method_name = preferred_method_name(method_id)
|
|
147
|
+
where = Where.new(method_id) rescue nil
|
|
148
|
+
return nil unless method_name && where
|
|
149
|
+
match = method_name.match("^(.*)_(#{where.name})=?$")
|
|
150
|
+
attribute, predicate = match.captures
|
|
151
|
+
attributes = attribute.split(/_or_/)
|
|
152
|
+
if attributes.all? {|a| where.types.include?(column_type(a))}
|
|
153
|
+
return match
|
|
154
|
+
end
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def get_sort
|
|
159
|
+
search_attributes['meta_sort']
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def set_sort(val)
|
|
163
|
+
return if val.blank?
|
|
164
|
+
column, direction = val.split('.')
|
|
165
|
+
direction ||= 'asc'
|
|
166
|
+
if ['asc','desc'].include?(direction)
|
|
167
|
+
if @base.respond_to?("sort_by_#{column}_#{direction}")
|
|
168
|
+
search_attributes['meta_sort'] = val
|
|
169
|
+
@relation = @relation.send("sort_by_#{column}_#{direction}")
|
|
170
|
+
elsif attribute = get_attribute(column)
|
|
171
|
+
search_attributes['meta_sort'] = val
|
|
172
|
+
@relation = @relation.order(attribute.send(direction).to_sql)
|
|
173
|
+
elsif column.scan('_and_').present?
|
|
174
|
+
attribute_names = column.split('_and_')
|
|
175
|
+
attributes = attribute_names.map {|n| get_attribute(n)}
|
|
176
|
+
if attribute_names.size == attributes.compact.size # We found all attributes
|
|
177
|
+
search_attributes['meta_sort'] = val
|
|
178
|
+
attributes.each do |attribute|
|
|
179
|
+
@relation = @relation.order(attribute.send(direction).to_sql)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def get_named_method_value(name)
|
|
187
|
+
search_attributes[name]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def set_named_method_value(name, val)
|
|
191
|
+
meth = @base._metasearch_methods[name][:method]
|
|
192
|
+
search_attributes[name] = meth.cast_param(val)
|
|
193
|
+
if meth.validate(search_attributes[name])
|
|
194
|
+
return_value = meth.evaluate(@relation, search_attributes[name])
|
|
195
|
+
if return_value.is_a?(ActiveRecord::Relation)
|
|
196
|
+
@relation = return_value
|
|
197
|
+
else
|
|
198
|
+
raise NonRelationReturnedError, "Custom search methods must return an ActiveRecord::Relation. #{name} returned a #{return_value.class}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def get_attribute_method_value(attribute, predicate)
|
|
204
|
+
search_attributes["#{attribute}_#{predicate}"]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def set_attribute_method_value(attribute, predicate, val)
|
|
208
|
+
where = Where.new(predicate)
|
|
209
|
+
attributes = attribute.split(/_or_/)
|
|
210
|
+
search_attributes["#{attribute}_#{predicate}"] = cast_attributes(where.cast || column_type(attributes.first), val)
|
|
211
|
+
if where.validate(search_attributes["#{attribute}_#{predicate}"])
|
|
212
|
+
arel_attributes = attributes.map {|a| get_attribute(a)}
|
|
213
|
+
@relation = where.evaluate(@relation, arel_attributes, search_attributes["#{attribute}_#{predicate}"])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def column_type(name, base = @base, depth = 1)
|
|
218
|
+
type = nil
|
|
219
|
+
if column = get_column(name, base)
|
|
220
|
+
type = column.type
|
|
221
|
+
elsif (segments = name.split(/_/)).size > 1
|
|
222
|
+
type = type_from_association_segments(segments, base, depth)
|
|
223
|
+
end
|
|
224
|
+
type
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def type_from_association_segments(segments, base, depth)
|
|
228
|
+
remainder = []
|
|
229
|
+
found_assoc = nil
|
|
230
|
+
type = nil
|
|
231
|
+
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
|
232
|
+
if found_assoc = get_association(segments.join('_'), base)
|
|
233
|
+
depth += 1
|
|
234
|
+
raise JoinDepthError, "Maximum join depth of #{MAX_JOIN_DEPTH} exceeded." if depth > MAX_JOIN_DEPTH
|
|
235
|
+
if found_assoc.options[:polymorphic]
|
|
236
|
+
unless delimiter = remainder.index('type')
|
|
237
|
+
raise PolymorphicAssociationMissingTypeError, "Polymorphic association specified without a type"
|
|
238
|
+
end
|
|
239
|
+
polymorphic_class, attribute_name = remainder[0...delimiter].join('_'),
|
|
240
|
+
remainder[delimiter + 1...remainder.size].join('_')
|
|
241
|
+
polymorphic_class = polymorphic_class.classify.constantize
|
|
242
|
+
type = column_type(attribute_name, polymorphic_class, depth)
|
|
243
|
+
else
|
|
244
|
+
type = column_type(remainder.join('_'), found_assoc.klass, depth)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
type
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def build_or_find_association(name, parent = @join_dependency.join_base, klass = nil)
|
|
252
|
+
found_association = @join_dependency.join_associations.detect do |assoc|
|
|
253
|
+
assoc.reflection.name == name &&
|
|
254
|
+
assoc.parent == parent &&
|
|
255
|
+
(!klass || assoc.reflection.klass == klass)
|
|
256
|
+
end
|
|
257
|
+
unless found_association
|
|
258
|
+
@join_dependency.send(:build, Polyamorous::Join.new(name, @join_type, klass), parent)
|
|
259
|
+
found_association = @join_dependency.join_associations.last
|
|
260
|
+
# Leverage the stashed association functionality in AR
|
|
261
|
+
@relation = @relation.joins(found_association)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
found_association
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def build_join_dependency(relation)
|
|
268
|
+
buckets = relation.joins_values.group_by do |join|
|
|
269
|
+
case join
|
|
270
|
+
when String
|
|
271
|
+
'string_join'
|
|
272
|
+
when Hash, Symbol, Array
|
|
273
|
+
'association_join'
|
|
274
|
+
when ::ActiveRecord::Associations::JoinDependency::JoinAssociation
|
|
275
|
+
'stashed_join'
|
|
276
|
+
when Arel::Nodes::Join
|
|
277
|
+
'join_node'
|
|
278
|
+
else
|
|
279
|
+
raise 'unknown class: %s' % join.class.name
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
association_joins = buckets['association_join'] || []
|
|
284
|
+
stashed_association_joins = buckets['stashed_join'] || []
|
|
285
|
+
join_nodes = buckets['join_node'] || []
|
|
286
|
+
string_joins = (buckets['string_join'] || []).map { |x|
|
|
287
|
+
x.strip
|
|
288
|
+
}.uniq
|
|
289
|
+
|
|
290
|
+
join_list = relation.send :custom_join_ast, relation.table.from(relation.table), string_joins
|
|
291
|
+
|
|
292
|
+
join_dependency = ::ActiveRecord::Associations::JoinDependency.new(
|
|
293
|
+
relation.klass,
|
|
294
|
+
association_joins,
|
|
295
|
+
join_list
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
join_nodes.each do |join|
|
|
299
|
+
join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
join_dependency.graft(*stashed_association_joins)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def get_join_type(opt_join)
|
|
306
|
+
# Allow "inner"/:inner and "upper"/:upper
|
|
307
|
+
if opt_join.to_s.upcase == 'INNER'
|
|
308
|
+
opt_join = Arel::Nodes::InnerJoin
|
|
309
|
+
elsif opt_join.to_s.upcase == 'OUTER'
|
|
310
|
+
opt_join = Arel::Nodes::OuterJoin
|
|
311
|
+
end
|
|
312
|
+
# Default to trusting what the user gave us
|
|
313
|
+
opt_join
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module MetaSearch
|
|
2
|
+
# Raised when type casting for a column fails.
|
|
3
|
+
class TypeCastError < StandardError; end
|
|
4
|
+
|
|
5
|
+
# Raised if you don't return a relation from a custom search method.
|
|
6
|
+
class NonRelationReturnedError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised if you try to access a relation that's joining too many tables to itself.
|
|
9
|
+
# This is designed to prevent a malicious user from accessing something like
|
|
10
|
+
# :developers_company_developers_company_developers_company_developers_company_...,
|
|
11
|
+
# resulting in a query that could cause issues for your database server.
|
|
12
|
+
class JoinDepthError < StandardError; end
|
|
13
|
+
|
|
14
|
+
# Raised if you try to search on a polymorphic belongs_to association without specifying
|
|
15
|
+
# its type.
|
|
16
|
+
class PolymorphicAssociationMissingTypeError < StandardError; end
|
|
17
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require 'action_view'
|
|
2
|
+
require 'action_view/template'
|
|
3
|
+
module MetaSearch
|
|
4
|
+
Check = Struct.new(:box, :label)
|
|
5
|
+
|
|
6
|
+
module Helpers
|
|
7
|
+
module FormBuilder
|
|
8
|
+
|
|
9
|
+
def self.included(base)
|
|
10
|
+
# Only take on the check_boxes method names if someone else (Hi, José!) hasn't grabbed them.
|
|
11
|
+
alias_method :check_boxes, :checks unless base.method_defined?(:check_boxes)
|
|
12
|
+
alias_method :collection_check_boxes, :collection_checks unless base.method_defined?(:collection_check_boxes)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Like other form_for field methods (text_field, hidden_field, password_field) etc,
|
|
16
|
+
# but takes a list of hashes between the +method+ parameter and the trailing option hash,
|
|
17
|
+
# if any, to specify a number of fields to create in multiparameter fashion.
|
|
18
|
+
#
|
|
19
|
+
# Each hash *must* contain a :field_type option, which specifies a form_for method, and
|
|
20
|
+
# _may_ contain an optional :type_cast option, with one of the typical multiparameter
|
|
21
|
+
# type cast characters. Any remaining options will be merged with the defaults specified
|
|
22
|
+
# in the trailing option hash and passed along when creating that field.
|
|
23
|
+
#
|
|
24
|
+
# For example...
|
|
25
|
+
#
|
|
26
|
+
# <%= f.multiparameter_field :moderations_value_between,
|
|
27
|
+
# {:field_type => :text_field, :class => 'first'},
|
|
28
|
+
# {:field_type => :text_field, :type_cast => 'i'},
|
|
29
|
+
# :size => 5 %>
|
|
30
|
+
#
|
|
31
|
+
# ...will create the following HTML:
|
|
32
|
+
#
|
|
33
|
+
# <input class="first" id="search_moderations_value_between(1)"
|
|
34
|
+
# name="search[moderations_value_between(1)]" size="5" type="text" />
|
|
35
|
+
#
|
|
36
|
+
# <input id="search_moderations_value_between(2i)"
|
|
37
|
+
# name="search[moderations_value_between(2i)]" size="5" type="text" />
|
|
38
|
+
#
|
|
39
|
+
# As with any multiparameter input fields, these will be concatenated into an
|
|
40
|
+
# array and passed to the attribute named by the first parameter for assignment.
|
|
41
|
+
def multiparameter_field(method, *args)
|
|
42
|
+
defaults = has_multiparameter_defaults?(args) ? args.pop : {}
|
|
43
|
+
raise ArgumentError, "No multiparameter fields specified" if args.blank?
|
|
44
|
+
html = ''.html_safe
|
|
45
|
+
args.each_with_index do |field, index|
|
|
46
|
+
type = field.delete(:field_type) || raise(ArgumentError, "No :field_type specified.")
|
|
47
|
+
cast = field.delete(:type_cast) || ''
|
|
48
|
+
opts = defaults.merge(field)
|
|
49
|
+
html.safe_concat(
|
|
50
|
+
@template.send(
|
|
51
|
+
type.to_s,
|
|
52
|
+
@object_name,
|
|
53
|
+
(method.to_s + "(#{index + 1}#{cast})"),
|
|
54
|
+
objectify_options(opts))
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
html
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Behaves almost exactly like the select method, but instead of generating a select tag,
|
|
61
|
+
# generates <tt>MetaSearch::Check</tt>s. These consist of two attributes, +box+ and +label+,
|
|
62
|
+
# which are (unsurprisingly) the HTML for the check box and the label. Called without a block,
|
|
63
|
+
# this method will return an array of check boxes. Called with a block, it will yield each
|
|
64
|
+
# check box to your template.
|
|
65
|
+
#
|
|
66
|
+
# *Parameters:*
|
|
67
|
+
#
|
|
68
|
+
# * +method+ - The method name on the form_for object
|
|
69
|
+
# * +choices+ - An array of arrays, the first value in each element is the text for the
|
|
70
|
+
# label, and the last is the value for the checkbox
|
|
71
|
+
# * +options+ - An options hash to be passed through to the checkboxes
|
|
72
|
+
#
|
|
73
|
+
# *Examples:*
|
|
74
|
+
#
|
|
75
|
+
# <b>Simple formatting:</b>
|
|
76
|
+
#
|
|
77
|
+
# <h4>How many heads?</h4>
|
|
78
|
+
# <ul>
|
|
79
|
+
# <% f.checks :number_of_heads_in,
|
|
80
|
+
# [['One', 1], ['Two', 2], ['Three', 3]], :class => 'checkboxy' do |check| %>
|
|
81
|
+
# <li>
|
|
82
|
+
# <%= check.box %>
|
|
83
|
+
# <%= check.label %>
|
|
84
|
+
# </li>
|
|
85
|
+
# <% end %>
|
|
86
|
+
# </ul>
|
|
87
|
+
#
|
|
88
|
+
# This example will output the checkboxes and labels in an unordered list format.
|
|
89
|
+
#
|
|
90
|
+
# <b>Grouping:</b>
|
|
91
|
+
#
|
|
92
|
+
# Chain <tt>in_groups_of(<num>, false)</tt> on checks like so:
|
|
93
|
+
# <h4>How many heads?</h4>
|
|
94
|
+
# <p>
|
|
95
|
+
# <% f.checks(:number_of_heads_in,
|
|
96
|
+
# [['One', 1], ['Two', 2], ['Three', 3]],
|
|
97
|
+
# :class => 'checkboxy').in_groups_of(2, false) do |checks| %>
|
|
98
|
+
# <% checks.each do |check| %>
|
|
99
|
+
# <%= check.box %>
|
|
100
|
+
# <%= check.label %>
|
|
101
|
+
# <% end %>
|
|
102
|
+
# <br />
|
|
103
|
+
# <% end %>
|
|
104
|
+
# </p>
|
|
105
|
+
def checks(method, choices = [], options = {}, &block)
|
|
106
|
+
unless choices.first.respond_to?(:first) && choices.first.respond_to?(:last)
|
|
107
|
+
raise ArgumentError, 'invalid choice array specified'
|
|
108
|
+
end
|
|
109
|
+
collection_checks(method, choices, :last, :first, options, &block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Just like +checks+, but this time you can pass in a collection, value, and text method,
|
|
113
|
+
# as with collection_select.
|
|
114
|
+
#
|
|
115
|
+
# Example:
|
|
116
|
+
#
|
|
117
|
+
# <% f.collection_checks :head_sizes_in, HeadSize.all,
|
|
118
|
+
# :id, :name, :class => 'headcheck' do |check| %>
|
|
119
|
+
# <%= check.box %> <%= check.label %>
|
|
120
|
+
# <% end %>
|
|
121
|
+
def collection_checks(method, collection, value_method, text_method, options = {}, &block)
|
|
122
|
+
check_boxes = []
|
|
123
|
+
collection.each do |choice|
|
|
124
|
+
text = choice.send(text_method)
|
|
125
|
+
value = choice.send(value_method)
|
|
126
|
+
check = MetaSearch::Check.new
|
|
127
|
+
check.box = @template.check_box_tag(
|
|
128
|
+
"#{@object_name}[#{method}][]",
|
|
129
|
+
value,
|
|
130
|
+
[@object.send(method)].flatten.include?(value),
|
|
131
|
+
options.merge(:id => [@object_name, method.to_s, value.to_s.underscore].join('_'))
|
|
132
|
+
)
|
|
133
|
+
check.label = @template.label_tag([@object_name, method.to_s, value.to_s.underscore].join('_'),
|
|
134
|
+
text)
|
|
135
|
+
if block_given?
|
|
136
|
+
yield check
|
|
137
|
+
else
|
|
138
|
+
check_boxes << check
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
check_boxes unless block_given?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Creates a sort link for the MetaSearch::Builder the form is created against.
|
|
145
|
+
# Useful shorthand if your results happen to reside in the context of your
|
|
146
|
+
# form_for block.
|
|
147
|
+
# Sample usage:
|
|
148
|
+
#
|
|
149
|
+
# <%= f.sort_link :name %>
|
|
150
|
+
# <%= f.sort_link :name, 'Company Name' %>
|
|
151
|
+
# <%= f.sort_link :name, :class => 'name_sort' %>
|
|
152
|
+
# <%= f.sort_link :name, 'Company Name', :class => 'company_name_sort' %>
|
|
153
|
+
def sort_link(attribute, *args)
|
|
154
|
+
@template.sort_link @object, attribute, *args
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# If the last element of the arguments to multiparameter_field has no :field_type
|
|
160
|
+
# key, we assume it's got some defaults to be used in the other hashes.
|
|
161
|
+
def has_multiparameter_defaults?(args)
|
|
162
|
+
args.size > 1 && args.last.is_a?(Hash) && !args.last.has_key?(:field_type)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module MetaSearch
|
|
2
|
+
module Helpers
|
|
3
|
+
module FormHelper
|
|
4
|
+
def apply_form_for_options!(object_or_array, options)
|
|
5
|
+
if object_or_array.is_a?(MetaSearch::Builder)
|
|
6
|
+
builder = object_or_array
|
|
7
|
+
options[:url] ||= polymorphic_path(object_or_array.base)
|
|
8
|
+
elsif object_or_array.is_a?(Array) && (builder = object_or_array.detect {|o| o.is_a?(MetaSearch::Builder)})
|
|
9
|
+
options[:url] ||= polymorphic_path(object_or_array.map {|o| o.is_a?(MetaSearch::Builder) ? o.base : o})
|
|
10
|
+
else
|
|
11
|
+
super
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
html_options = {
|
|
16
|
+
:class => options[:as] ? "#{options[:as]}_search" : "#{builder.base.to_s.underscore}_search",
|
|
17
|
+
:id => options[:as] ? "#{options[:as]}_search" : "#{builder.base.to_s.underscore}_search",
|
|
18
|
+
:method => :get }
|
|
19
|
+
options[:html] ||= {}
|
|
20
|
+
options[:html].reverse_merge!(html_options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module MetaSearch
|
|
2
|
+
module Helpers
|
|
3
|
+
module UrlHelper
|
|
4
|
+
|
|
5
|
+
# Generates a column sort link for a given attribute of a MetaSearch::Builder object.
|
|
6
|
+
# The link maintains existing options for the sort as parameters in the URL, and
|
|
7
|
+
# sets a meta_sort parameter as well. If the first parameter after the attribute name
|
|
8
|
+
# is not a hash, it will be used as a string for alternate link text. If a hash is
|
|
9
|
+
# supplied, it will be passed to link_to as an html_options hash. The link will
|
|
10
|
+
# be assigned two css classes: sort_link and one of "asc" or "desc", depending on
|
|
11
|
+
# the current sort order. Any class supplied in the options hash will be appended.
|
|
12
|
+
#
|
|
13
|
+
# Sample usage:
|
|
14
|
+
#
|
|
15
|
+
# <%= sort_link @search, :name %>
|
|
16
|
+
# <%= sort_link @search, :name, 'Company Name' %>
|
|
17
|
+
# <%= sort_link @search, :name, :class => 'name_sort' %>
|
|
18
|
+
# <%= sort_link @search, :name, 'Company Name', :class => 'company_name_sort' %>
|
|
19
|
+
# <%= sort_link @search, :name, :default_order => :desc %>
|
|
20
|
+
# <%= sort_link @search, :name, 'Company Name', :default_order => :desc %>
|
|
21
|
+
# <%= sort_link @search, :name, :class => 'name_sort', :default_order => :desc %>
|
|
22
|
+
# <%= sort_link @search, :name, 'Company Name', :class => 'company_name_sort', :default_order => :desc %>
|
|
23
|
+
|
|
24
|
+
def sort_link(builder, attribute, *args)
|
|
25
|
+
raise ArgumentError, "Need a MetaSearch::Builder search object as first param!" unless builder.is_a?(MetaSearch::Builder)
|
|
26
|
+
attr_name = attribute.to_s
|
|
27
|
+
name = (args.size > 0 && !args.first.is_a?(Hash)) ? args.shift.to_s : builder.base.human_attribute_name(attr_name)
|
|
28
|
+
prev_attr, prev_order = builder.search_attributes['meta_sort'].to_s.split('.')
|
|
29
|
+
|
|
30
|
+
options = args.first.is_a?(Hash) ? args.shift.dup : {}
|
|
31
|
+
current_order = prev_attr == attr_name ? prev_order : nil
|
|
32
|
+
|
|
33
|
+
if options[:default_order] == :desc
|
|
34
|
+
new_order = current_order == 'desc' ? 'asc' : 'desc'
|
|
35
|
+
else
|
|
36
|
+
new_order = current_order == 'asc' ? 'desc' : 'asc'
|
|
37
|
+
end
|
|
38
|
+
options.delete(:default_order)
|
|
39
|
+
|
|
40
|
+
html_options = args.first.is_a?(Hash) ? args.shift : {}
|
|
41
|
+
css = ['sort_link', current_order].compact.join(' ')
|
|
42
|
+
html_options[:class] = [css, html_options[:class]].compact.join(' ')
|
|
43
|
+
options.merge!(
|
|
44
|
+
builder.search_key => builder.search_attributes.merge(
|
|
45
|
+
'meta_sort' => [attr_name, new_order].join('.')
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
link_to [ERB::Util.h(name), order_indicator_for(current_order)].compact.join(' ').html_safe,
|
|
49
|
+
url_for(options),
|
|
50
|
+
html_options
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def order_indicator_for(order)
|
|
56
|
+
if order == 'asc'
|
|
57
|
+
'▲'
|
|
58
|
+
elsif order == 'desc'
|
|
59
|
+
'▼'
|
|
60
|
+
else
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
en:
|
|
2
|
+
meta_search:
|
|
3
|
+
or: 'or'
|
|
4
|
+
predicates:
|
|
5
|
+
equals: "%{attribute} equals"
|
|
6
|
+
does_not_equal: "%{attribute} doesn't equal"
|
|
7
|
+
contains: "%{attribute} contains"
|
|
8
|
+
does_not_contain: "%{attribute} doesn't contain"
|
|
9
|
+
starts_with: "%{attribute} starts with"
|
|
10
|
+
does_not_start_with: "%{attribute} doesn't start with"
|
|
11
|
+
ends_with: "%{attribute} ends with"
|
|
12
|
+
does_not_end_with: "%{attribute} doesn't end with"
|
|
13
|
+
greater_than: "%{attribute} greater than"
|
|
14
|
+
less_than: "%{attribute} less than"
|
|
15
|
+
greater_than_or_equal_to: "%{attribute} greater than or equal to"
|
|
16
|
+
less_than_or_equal_to: "%{attribute} less than or equal to"
|
|
17
|
+
in: "%{attribute} is one of"
|
|
18
|
+
not_in: "%{attribute} isn't one of"
|
|
19
|
+
is_true: "%{attribute} is true"
|
|
20
|
+
is_false: "%{attribute} is false"
|
|
21
|
+
is_present: "%{attribute} is present"
|
|
22
|
+
is_blank: "%{attribute} is blank"
|
|
23
|
+
is_null: "%{attribute} is null"
|
|
24
|
+
is_not_null: "%{attribute} isn't null"
|