meta_search 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|