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.
@@ -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
+ '&#9650;'
58
+ elsif order == 'desc'
59
+ '&#9660;'
60
+ else
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ require 'meta_search/helpers/form_builder'
2
+ require 'meta_search/helpers/form_helper'
3
+ require 'meta_search/helpers/url_helper'
@@ -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"