meta_search 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +101 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/meta_search/builder.rb +247 -0
- data/lib/meta_search/exceptions.rb +3 -0
- data/lib/meta_search/helpers/action_view.rb +168 -0
- data/lib/meta_search/model_compatibility.rb +8 -0
- data/lib/meta_search/railtie.rb +21 -0
- data/lib/meta_search/searches/active_record.rb +19 -0
- data/lib/meta_search/searches/base.rb +46 -0
- data/lib/meta_search/utility.rb +85 -0
- data/lib/meta_search/where.rb +174 -0
- data/lib/meta_search.rb +31 -0
- data/meta_search.gemspec +83 -0
- data/test/fixtures/companies.yml +17 -0
- data/test/fixtures/company.rb +9 -0
- data/test/fixtures/data_type.rb +4 -0
- data/test/fixtures/data_types.yml +15 -0
- data/test/fixtures/developer.rb +5 -0
- data/test/fixtures/developers.yml +55 -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 +24 -0
- data/test/fixtures/schema.rb +47 -0
- data/test/helper.rb +37 -0
- data/test/test_search.rb +351 -0
- data/test/test_view_helpers.rb +149 -0
- metadata +116 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ernie Miller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
= MetaSearch
|
2
|
+
|
3
|
+
Extensible searching for your form_for enjoyment.
|
4
|
+
|
5
|
+
== Getting Started
|
6
|
+
|
7
|
+
Add a line to your Gemfile:
|
8
|
+
|
9
|
+
gem "meta_search"
|
10
|
+
|
11
|
+
In your controller:
|
12
|
+
|
13
|
+
def index
|
14
|
+
@search = Article.search(params[:search])
|
15
|
+
@articles = @search.all
|
16
|
+
end
|
17
|
+
|
18
|
+
In your view:
|
19
|
+
|
20
|
+
<% form_for :search, @search, :html => {:method => :get} do |f| %>
|
21
|
+
<%= f.label :title_contains %>
|
22
|
+
<%= f.text_field :title_contains %><br />
|
23
|
+
<%= f.label :comments_created_at_greater_than, 'With comments after' %>
|
24
|
+
<%= f.datetime_select :comments_created_at_greater_than, :include_blank => true %><br />
|
25
|
+
<!-- etc... -->
|
26
|
+
<%= f.submit %>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
The default Where types are listed at MetaSearch::Where. Options for the search method are documented at MetaSearch::Searches::Base.
|
30
|
+
|
31
|
+
== Advanced usage
|
32
|
+
|
33
|
+
=== Adding a new Where
|
34
|
+
|
35
|
+
If none of the built-in search criteria work for you, you can add a new Where (or 5). To do so,
|
36
|
+
create an initializer (<tt>/config/initializers/meta_search.rb</tt>, for instance) and add lines
|
37
|
+
like:
|
38
|
+
|
39
|
+
MetaSearch::Where.add :between, :btw, {:condition => 'BETWEEN', :substitutions => '? AND ?'}
|
40
|
+
|
41
|
+
See MetaSearch::Where for info on the supported options.
|
42
|
+
|
43
|
+
=== multiparameter_field
|
44
|
+
|
45
|
+
The example Where above adds support for a "between" search, which requires an array with
|
46
|
+
two parameters. These can be passed using Rails multiparameter attributes. To make life easier,
|
47
|
+
MetaSearch adds a helper for this:
|
48
|
+
|
49
|
+
<%= f.multiparameter_field :moderations_value_between,
|
50
|
+
{:field_type => :text_field}, {:field_type => :text_field}, :size => 5 %>
|
51
|
+
|
52
|
+
<tt>multiparameter_field</tt> works pretty much like the other FormBuilder helpers, but it
|
53
|
+
lets you sandwich a list of fields, each in hash format, between the attribute and the usual
|
54
|
+
options hash. See MetaSearch::Helpers::FormBuilder for more info.
|
55
|
+
|
56
|
+
=== check_boxes and collection_check_boxes
|
57
|
+
|
58
|
+
If you need to get an array into your where, and you don't care about parameter order,
|
59
|
+
you might choose to use a select or collection_select with multiple selection enabled,
|
60
|
+
but everyone hates multiple selection boxes. MetaSearch adds a couple of additional
|
61
|
+
helpers, +check_boxes+ and +collection_check_boxes+ to handle multiple selections in a
|
62
|
+
more visually appealing manner. It can be called with or without a block, so something
|
63
|
+
like this is possible:
|
64
|
+
|
65
|
+
<table>
|
66
|
+
<th colspan="2">How many heads?</th>
|
67
|
+
<% f.check_boxes :number_of_heads_in,
|
68
|
+
[['One', 1], ['Two', 2], ['Three', 3]], :class => 'checkboxy' do |c| %>
|
69
|
+
<tr>
|
70
|
+
<td><%= c[:check_box] %></td>
|
71
|
+
<td><%= c[:label] %></td>
|
72
|
+
</tr>
|
73
|
+
<% end %>
|
74
|
+
</table>
|
75
|
+
|
76
|
+
Again, full documentation is in MetaSearch::Helpers::FormBuilder.
|
77
|
+
|
78
|
+
=== Excluding attributes and associations
|
79
|
+
|
80
|
+
If you'd like to prevent certain associations or attributes from being searchable, you can control
|
81
|
+
this inside your models:
|
82
|
+
|
83
|
+
class Article < ActiveRecord::Base
|
84
|
+
metasearch_exclude_attr :some_private_data, :another_private_column
|
85
|
+
metasearch_exclude_assoc :an_association_that_should_not_be_searched, :and_another
|
86
|
+
end
|
87
|
+
|
88
|
+
You get the idea. Excluded attributes on a model will be honored across associations, so
|
89
|
+
if an Article <tt>has_many :comments</tt> and the Comment model looks something like this:
|
90
|
+
|
91
|
+
Comment < ActiveRecord::Base
|
92
|
+
validates_presence_of :user_id, :body
|
93
|
+
metasearch_exclude_attr :user_id
|
94
|
+
end
|
95
|
+
|
96
|
+
Then your call to <tt>Article.search</tt> will allow <tt>:comments_body_contains</tt>
|
97
|
+
but not <tt>:comments_user_id_equals</tt> to be passed.
|
98
|
+
|
99
|
+
== Copyright
|
100
|
+
|
101
|
+
Copyright (c) 2010 {Ernie Miller}[http://metautonomo.us]. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "meta_search"
|
8
|
+
gem.summary = %Q{ActiveRecord 3 object-based searching.}
|
9
|
+
gem.description = %Q{Adds a search method to your ActiveRecord models which returns an object to be used in form_for while constructing a search. Works with Rails 3 only.}
|
10
|
+
gem.email = "ernie@metautonomo.us"
|
11
|
+
gem.homepage = "http://metautonomo.us"
|
12
|
+
gem.authors = ["Ernie Miller"]
|
13
|
+
gem.add_development_dependency "activerecord", ">= 3.0.0.beta"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "meta_search #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'meta_search/model_compatibility'
|
2
|
+
require 'meta_search/exceptions'
|
3
|
+
require 'meta_search/where'
|
4
|
+
require 'meta_search/utility'
|
5
|
+
|
6
|
+
module MetaSearch
|
7
|
+
# Builder is the workhorse of MetaSearch -- it is the class that handles dynamically generating
|
8
|
+
# methods based on a supplied model, and is what gets instantiated when you call your model's search
|
9
|
+
# method. Builder doesn't generate any methods until they're needed, using method_missing to compare
|
10
|
+
# requested method names against your model's attributes, associations, and the configured Where
|
11
|
+
# list.
|
12
|
+
#
|
13
|
+
# === Attributes
|
14
|
+
#
|
15
|
+
# * +base+ - The base model that Builder wraps.
|
16
|
+
# * +search_attributes+ - Attributes that have been assigned (search terms)
|
17
|
+
# * +relation+ - The ActiveRecord::Relation representing the current search.
|
18
|
+
# * +join_dependency+ - The JoinDependency object representing current association join
|
19
|
+
# dependencies. It's used internally to avoid joining association tables more than
|
20
|
+
# once when constructing search queries.
|
21
|
+
class Builder
|
22
|
+
include ModelCompatibility
|
23
|
+
include Utility
|
24
|
+
|
25
|
+
attr_reader :base, :search_attributes, :relation, :join_dependency
|
26
|
+
delegate *RELATION_METHODS, :to => :relation
|
27
|
+
|
28
|
+
# Initialize a new Builder. Requires a base model to wrap, and supports a couple of options
|
29
|
+
# for how it will expose this model and its associations to your controllers/views.
|
30
|
+
def initialize(base, opts = {})
|
31
|
+
@base = base
|
32
|
+
@opts = opts
|
33
|
+
@associations = {}
|
34
|
+
@join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@base, [], nil)
|
35
|
+
@search_attributes = {}
|
36
|
+
@relation = @base.scoped
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the column info for the given model attribute (if not excluded as outlined above)
|
40
|
+
def column(attr)
|
41
|
+
@base.columns_hash[attr.to_s] if self.includes_attribute?(attr)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return the association reflection for the named association (if not excluded as outlined
|
45
|
+
# above)
|
46
|
+
def association(association)
|
47
|
+
if self.includes_association?(association)
|
48
|
+
@associations[association.to_sym] ||= @base.reflect_on_association(association.to_sym)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return the column info for given association and column (if the association is not
|
53
|
+
# excluded from search)
|
54
|
+
def association_column(association, attr)
|
55
|
+
if self.includes_association?(association)
|
56
|
+
assoc = self.association(association)
|
57
|
+
assoc.klass.columns_hash[attr.to_s] unless assoc.klass._metasearch_exclude_attributes.include?(attr.to_s)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def included_attributes
|
62
|
+
@included_attributes ||= @base.column_names - @base._metasearch_exclude_attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
def includes_attribute?(attr)
|
66
|
+
self.included_attributes.include?(attr.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
def included_associations
|
70
|
+
@included_associations ||= @base.reflect_on_all_associations.map {|a| a.name.to_s} - @base._metasearch_exclude_associations
|
71
|
+
end
|
72
|
+
|
73
|
+
def includes_association?(assoc)
|
74
|
+
self.included_associations.include?(assoc.to_s)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Build the search with the given search options. Options are in the form of a hash
|
78
|
+
# with keys matching the names creted by the Builder's "wheres" as outlined in
|
79
|
+
# MetaSearch::Where
|
80
|
+
def build(opts)
|
81
|
+
opts ||= {}
|
82
|
+
@relation = @base.scoped
|
83
|
+
opts.stringify_keys!
|
84
|
+
opts = collapse_multiparameter_options(opts)
|
85
|
+
assign_attributes(opts)
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def method_missing(method_id, *args, &block)
|
92
|
+
if match = method_id.to_s.match(/^(.*)\(([0-9]+).*\)$/)
|
93
|
+
method_name, index = match.captures
|
94
|
+
vals = self.send(method_name)
|
95
|
+
vals.is_a?(Array) ? vals[index.to_i - 1] : nil
|
96
|
+
elsif match = matches_attribute_method(method_id)
|
97
|
+
condition, attribute, association = match.captures.reverse
|
98
|
+
build_method(association, attribute, condition)
|
99
|
+
self.send(preferred_method_name(method_id), *args)
|
100
|
+
elsif match = matches_where_method(method_id)
|
101
|
+
condition = match.captures.first
|
102
|
+
build_where_method(condition, Where.new(condition))
|
103
|
+
self.send(method_id, *args)
|
104
|
+
else
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def build_method(association, attribute, suffix)
|
110
|
+
if association.blank?
|
111
|
+
build_attribute_method(attribute, suffix)
|
112
|
+
else
|
113
|
+
build_association_method(association, attribute, suffix)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_association_method(association, attribute, type)
|
118
|
+
metaclass.instance_eval do
|
119
|
+
define_method("#{association}_#{attribute}_#{type}") do
|
120
|
+
search_attributes["#{association}_#{attribute}_#{type}"]
|
121
|
+
end
|
122
|
+
|
123
|
+
define_method("#{association}_#{attribute}_#{type}=") do |val|
|
124
|
+
search_attributes["#{association}_#{attribute}_#{type}"] = cast_attributes(association_type_for(association, attribute), val)
|
125
|
+
where = Where.new(type)
|
126
|
+
if where.valid_substitutions?(search_attributes["#{association}_#{attribute}_#{type}"])
|
127
|
+
join = build_or_find_association(association)
|
128
|
+
self.send("add_#{type}_where", join.aliased_table_name, attribute, search_attributes["#{association}_#{attribute}_#{type}"])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_attribute_method(attribute, type)
|
135
|
+
metaclass.instance_eval do
|
136
|
+
define_method("#{attribute}_#{type}") do
|
137
|
+
search_attributes["#{attribute}_#{type}"]
|
138
|
+
end
|
139
|
+
|
140
|
+
define_method("#{attribute}_#{type}=") do |val|
|
141
|
+
search_attributes["#{attribute}_#{type}"] = cast_attributes(type_for(attribute), val)
|
142
|
+
where = Where.new(type)
|
143
|
+
if where.valid_substitutions?(search_attributes["#{attribute}_#{type}"])
|
144
|
+
self.send("add_#{type}_where", @base.table_name, attribute, search_attributes["#{attribute}_#{type}"])
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_where_method(condition, where)
|
151
|
+
metaclass.instance_eval do
|
152
|
+
define_method("add_#{condition}_where") do |table, attribute, *args|
|
153
|
+
args.flatten! unless where.keep_arrays?
|
154
|
+
@relation = @relation.where(
|
155
|
+
"#{quote_table_name table}.#{quote_column_name attribute} " +
|
156
|
+
"#{where.condition} #{where.substitutions}", *format_params(where.formatter, *args)
|
157
|
+
)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def format_params(formatter, *params)
|
163
|
+
par = params.map {|p| formatter.call(p)}
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_or_find_association(association)
|
167
|
+
found_association = @join_dependency.join_associations.detect do |assoc|
|
168
|
+
assoc.reflection.name == association.to_sym
|
169
|
+
end
|
170
|
+
unless found_association
|
171
|
+
@relation = @relation.joins(association.to_sym)
|
172
|
+
@join_dependency.send(:build, association.to_sym, @join_dependency.join_base)
|
173
|
+
found_association = @join_dependency.join_associations.last
|
174
|
+
end
|
175
|
+
found_association
|
176
|
+
end
|
177
|
+
|
178
|
+
def matches_attribute_method(method_id)
|
179
|
+
method_name = preferred_method_name(method_id)
|
180
|
+
where = Where.new(method_id) rescue nil
|
181
|
+
return nil unless method_name && where
|
182
|
+
match = method_name.match("^(.*)_(#{where.name})=?$")
|
183
|
+
attribute, condition = match.captures
|
184
|
+
if where.types.include?(type_for(attribute))
|
185
|
+
return match
|
186
|
+
elsif match = matches_association(method_name, attribute, condition)
|
187
|
+
association, attribute = match.captures
|
188
|
+
return match if where.types.include?(association_type_for(association, attribute))
|
189
|
+
end
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
def preferred_method_name(method_id)
|
194
|
+
method_name = method_id.to_s
|
195
|
+
where = Where.new(method_name) rescue nil
|
196
|
+
return nil unless where
|
197
|
+
where.aliases.each do |a|
|
198
|
+
break if method_name.sub!(/#{a}(=?)$/, "#{where.name}\\1")
|
199
|
+
end
|
200
|
+
method_name
|
201
|
+
end
|
202
|
+
|
203
|
+
def matches_association(method_id, attribute, condition)
|
204
|
+
self.included_associations.each do |association|
|
205
|
+
test_attribute = attribute.dup
|
206
|
+
if test_attribute.gsub!(/^#{association}_/, '') &&
|
207
|
+
match = method_id.to_s.match("^(#{association})_(#{test_attribute})_(#{condition})=?$")
|
208
|
+
return match
|
209
|
+
end
|
210
|
+
end
|
211
|
+
nil
|
212
|
+
end
|
213
|
+
|
214
|
+
def matches_where_method(method_id)
|
215
|
+
if match = method_id.to_s.match(/^add_(.*)_where$/)
|
216
|
+
condition = match.captures.first
|
217
|
+
Where.get(condition) ? match : nil
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def assign_attributes(opts)
|
222
|
+
opts.each_pair do |k, v|
|
223
|
+
self.send("#{k}=", v)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def type_for(attribute)
|
228
|
+
column = self.column(attribute)
|
229
|
+
column.type if column
|
230
|
+
end
|
231
|
+
|
232
|
+
def class_for(attribute)
|
233
|
+
column = self.column(attribute)
|
234
|
+
column.klass if column
|
235
|
+
end
|
236
|
+
|
237
|
+
def association_type_for(association, attribute)
|
238
|
+
column = self.association_column(association, attribute)
|
239
|
+
column.type if column
|
240
|
+
end
|
241
|
+
|
242
|
+
def association_class_for(association, attribute)
|
243
|
+
column = self.association_column(association, attribute)
|
244
|
+
column.klass if column
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'action_dispatch'
|
3
|
+
|
4
|
+
module ActionDispatch::Http::FilterParameters #:nodoc:
|
5
|
+
protected
|
6
|
+
# Temp fix for Rails 3 beta buggy parameter filtering on arrays
|
7
|
+
def process_parameter_filter(original_params) #:nodoc:
|
8
|
+
return original_params.dup unless filtering_parameters?
|
9
|
+
|
10
|
+
filtered_params = {}
|
11
|
+
regexps, blocks = compile_parameter_filter
|
12
|
+
|
13
|
+
original_params.each do |key, value|
|
14
|
+
if regexps.find { |r| key =~ r }
|
15
|
+
value = '[FILTERED]'
|
16
|
+
elsif value.is_a?(Hash)
|
17
|
+
value = process_parameter_filter(value)
|
18
|
+
elsif value.is_a?(Array)
|
19
|
+
value = value.map { |v| v.is_a?(Hash) ? process_parameter_filter(v) : v }
|
20
|
+
elsif blocks.present?
|
21
|
+
key = key.dup
|
22
|
+
value = value.dup if value.duplicable?
|
23
|
+
blocks.each { |b| b.call(key, value) }
|
24
|
+
end
|
25
|
+
|
26
|
+
filtered_params[key] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
filtered_params
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module MetaSearch::Helpers
|
34
|
+
module FormBuilder
|
35
|
+
def self.enable! #:nodoc:
|
36
|
+
::ActionView::Helpers::FormBuilder.class_eval do
|
37
|
+
include FormBuilder
|
38
|
+
self.field_helpers += ['multiparameter_field', 'check_boxes', 'collection_check_boxes']
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Like other form_for field methods (text_field, hidden_field, password_field) etc,
|
43
|
+
# but takes a list of hashes between the +method+ parameter and the trailing option hash,
|
44
|
+
# if any, to specify a number of fields to create in multiparameter fashion.
|
45
|
+
#
|
46
|
+
# Each hash *must* contain a :field_type option, which specifies a form_for method, and
|
47
|
+
# _may_ contain an optional :type_cast option, with one of the typical multiparameter
|
48
|
+
# type cast characters. Any remaining options will be merged with the defaults specified
|
49
|
+
# in the trailing option hash and passed along when creating that field.
|
50
|
+
#
|
51
|
+
# For example...
|
52
|
+
#
|
53
|
+
# <%= f.multiparameter_field :moderations_value_between,
|
54
|
+
# {:field_type => :text_field, :class => 'first'},
|
55
|
+
# {:field_type => :text_field, :type_cast => 'i'},
|
56
|
+
# :size => 5 %>
|
57
|
+
#
|
58
|
+
# ...will create the following HTML:
|
59
|
+
#
|
60
|
+
# <input class="first" id="search_moderations_value_between(1)"
|
61
|
+
# name="search[moderations_value_between(1)]" size="5" type="text" />
|
62
|
+
#
|
63
|
+
# <input id="search_moderations_value_between(2i)"
|
64
|
+
# name="search[moderations_value_between(2i)]" size="5" type="text" />
|
65
|
+
#
|
66
|
+
# As with any multiparameter input fields, these will be concatenated into an
|
67
|
+
# array and passed to the attribute named by the first parameter for assignment.
|
68
|
+
def multiparameter_field(method, *args)
|
69
|
+
defaults = has_multiparameter_defaults?(args) ? args.pop : {}
|
70
|
+
raise ArgumentError, "No multiparameter fields specified" if args.blank?
|
71
|
+
html = ''.html_safe
|
72
|
+
args.each_with_index do |field, index|
|
73
|
+
type = field.delete(:field_type) || raise(ArgumentError, "No :field_type specified.")
|
74
|
+
cast = field.delete(:type_cast) || ''
|
75
|
+
opts = defaults.merge(field)
|
76
|
+
html.safe_concat(
|
77
|
+
@template.send(
|
78
|
+
type.to_s,
|
79
|
+
@object_name,
|
80
|
+
(method.to_s + "(#{index + 1}#{cast})"),
|
81
|
+
objectify_options(opts))
|
82
|
+
)
|
83
|
+
end
|
84
|
+
html
|
85
|
+
end
|
86
|
+
|
87
|
+
# Behaves almost exactly like the select method, but instead of generating a select tag,
|
88
|
+
# generates checkboxes. Since these checkboxes are just a checkbox and label with no
|
89
|
+
# additional formatting by default, this method can also take a block parameter.
|
90
|
+
#
|
91
|
+
# *Parameters:*
|
92
|
+
#
|
93
|
+
# * +method+ - The method name on the form_for object
|
94
|
+
# * +choices+ - An array of arrays, the first value in each element is the text for the
|
95
|
+
# label, and the last is the value for the checkbox
|
96
|
+
# * +options+ - An options hash to be passed through to the checkboxes
|
97
|
+
#
|
98
|
+
# If a block is supplied, rather than just rendering the checkboxes and labels, the block
|
99
|
+
# will receive a hash with two keys, :check_box and :label
|
100
|
+
#
|
101
|
+
# *Examples:*
|
102
|
+
#
|
103
|
+
# Simple usage:
|
104
|
+
#
|
105
|
+
# <%= f.check_boxes :number_of_heads_in,
|
106
|
+
# [['One', 1], ['Two', 2], ['Three', 3]], :class => 'checkboxy' %>
|
107
|
+
#
|
108
|
+
# This will result in three checkboxes, with the labels "One", "Two", and "Three", and
|
109
|
+
# corresponding numeric values, which will be sent as an array to the :number_of_heads_in
|
110
|
+
# attribute of the form_for object.
|
111
|
+
#
|
112
|
+
# Additional formatting:
|
113
|
+
#
|
114
|
+
# <table>
|
115
|
+
# <th colspan="2">How many heads?</th>
|
116
|
+
# <% f.check_boxes :number_of_heads_in,
|
117
|
+
# [['One', 1], ['Two', 2], ['Three', 3]], :class => 'checkboxy' do |c| %>
|
118
|
+
# <tr>
|
119
|
+
# <td><%= c[:check_box] %></td>
|
120
|
+
# <td><%= c[:label] %></td>
|
121
|
+
# </tr>
|
122
|
+
# <% end %>
|
123
|
+
# </table>
|
124
|
+
#
|
125
|
+
# This example will output the checkboxes and labels in a tabular format. You get the idea.
|
126
|
+
def check_boxes(method, choices = [], options = {}, &block)
|
127
|
+
unless choices.first.respond_to?(:first) && choices.first.respond_to?(:last)
|
128
|
+
raise ArgumentError, 'invalid choice array specified'
|
129
|
+
end
|
130
|
+
collection_check_boxes(method, choices, :last, :first, options, &block)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Just like +check_boxes+, but this time you can pass in a collection, value, and text method,
|
134
|
+
# as with collection_select.
|
135
|
+
#
|
136
|
+
# Example:
|
137
|
+
#
|
138
|
+
# <%= f.collection_check_boxes :head_sizes_in, HeadSize.all,
|
139
|
+
# :id, :name, :class => 'head-check' %>
|
140
|
+
def collection_check_boxes(method, collection, value_method, text_method, options = {}, &block)
|
141
|
+
html = ''.html_safe
|
142
|
+
collection.each do |choice|
|
143
|
+
text = choice.send(text_method)
|
144
|
+
value = choice.send(value_method)
|
145
|
+
c = {}
|
146
|
+
c[:check_box] = @template.check_box_tag(
|
147
|
+
"#{@object_name}[#{method}][]",
|
148
|
+
value,
|
149
|
+
[@object.send(method)].flatten.include?(value),
|
150
|
+
options.merge(:id => [@object_name, method.to_s, value.to_s.underscore].join('_'))
|
151
|
+
)
|
152
|
+
c[:label] = @template.label_tag([@object_name, method.to_s, value.to_s.underscore].join('_'),
|
153
|
+
text)
|
154
|
+
yield c if block_given?
|
155
|
+
html.safe_concat(c[:check_box] + c[:label])
|
156
|
+
end
|
157
|
+
html
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
# If the last element of the arguments to multiparameter_field has no :field_type
|
163
|
+
# key, we assume it's got some defaults to be used in the other hashes.
|
164
|
+
def has_multiparameter_defaults?(args)
|
165
|
+
args.size > 1 && args.last.is_a?(Hash) && !args.last.has_key?(:field_type)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'meta_search'
|
2
|
+
|
3
|
+
module MetaSearch
|
4
|
+
class Railtie < Rails::Railtie #:nodoc:
|
5
|
+
railtie_name :meta_search
|
6
|
+
|
7
|
+
initializer "meta_search.active_record" do |app|
|
8
|
+
if defined? ::ActiveRecord
|
9
|
+
require 'meta_search/searches/active_record'
|
10
|
+
MetaSearch::Searches::ActiveRecord.enable!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer "meta_search.action_view" do |app|
|
15
|
+
if defined? ::ActionView
|
16
|
+
require 'meta_search/helpers/action_view'
|
17
|
+
MetaSearch::Helpers::FormBuilder.enable!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'meta_search/searches/base'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
module MetaSearch::Searches
|
5
|
+
module ActiveRecord
|
6
|
+
include MetaSearch::Searches::Base
|
7
|
+
|
8
|
+
# Mixes MetaSearch into ActiveRecord::Base.
|
9
|
+
def self.enable!
|
10
|
+
::ActiveRecord::Base.class_eval do
|
11
|
+
class_attribute :_metasearch_exclude_attributes
|
12
|
+
class_attribute :_metasearch_exclude_associations
|
13
|
+
self._metasearch_exclude_attributes = []
|
14
|
+
self._metasearch_exclude_associations = []
|
15
|
+
extend ActiveRecord
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|