Functional tests now work properly, bearing in mind whether a user is logged in or...
[depot.git] / vendor / rails / actionpack / lib / action_view / helpers / form_options_helper.rb
1 require 'cgi'
2 require 'erb'
3 require 'action_view/helpers/form_helper'
4
5 module ActionView
6 module Helpers
7 # Provides a number of methods for turning different kinds of containers into a set of option tags.
8 # == Options
9 # The <tt>collection_select</tt>, <tt>country_select</tt>, <tt>select</tt>,
10 # and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter,
11 # a hash.
12 #
13 # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
14 #
15 # For example,
16 #
17 # select("post", "category", Post::CATEGORIES, {:include_blank => true})
18 #
19 # could become:
20 #
21 # <select name="post[category]">
22 # <option></option>
23 # <option>joke</option>
24 # <option>poem</option>
25 # </select>
26 #
27 # Another common case is a select tag for an <tt>belongs_to</tt>-associated object.
28 #
29 # Example with @post.person_id => 2:
30 #
31 # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
32 #
33 # could become:
34 #
35 # <select name="post[person_id]">
36 # <option value="">None</option>
37 # <option value="1">David</option>
38 # <option value="2" selected="selected">Sam</option>
39 # <option value="3">Tobias</option>
40 # </select>
41 #
42 # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
43 #
44 # Example:
45 #
46 # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
47 #
48 # could become:
49 #
50 # <select name="post[person_id]">
51 # <option value="">Select Person</option>
52 # <option value="1">David</option>
53 # <option value="2">Sam</option>
54 # <option value="3">Tobias</option>
55 # </select>
56 #
57 # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
58 # option to be in the +html_options+ parameter.
59 #
60 # Example:
61 #
62 # select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
63 #
64 # becomes:
65 #
66 # <select name="album[][genre]" id="album__genre">
67 # <option value="rap">rap</option>
68 # <option value="rock">rock</option>
69 # <option value="country">country</option>
70 # </select>
71 module FormOptionsHelper
72 include ERB::Util
73
74 # Create a select tag and a series of contained option tags for the provided object and method.
75 # The option currently held by the object will be selected, provided that the object is available.
76 # See options_for_select for the required format of the choices parameter.
77 #
78 # Example with @post.person_id => 1:
79 # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true })
80 #
81 # could become:
82 #
83 # <select name="post[person_id]">
84 # <option value=""></option>
85 # <option value="1" selected="selected">David</option>
86 # <option value="2">Sam</option>
87 # <option value="3">Tobias</option>
88 # </select>
89 #
90 # This can be used to provide a default set of options in the standard way: before rendering the create form, a
91 # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
92 # to the database. Instead, a second model object is created when the create request is received.
93 # This allows the user to submit a form page more than once with the expected results of creating multiple records.
94 # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
95 #
96 # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection
97 # or <tt>:selected => nil</tt> to leave all options unselected.
98 def select(object, method, choices, options = {}, html_options = {})
99 InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options)
100 end
101
102 # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
103 # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
104 # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
105 # or <tt>:include_blank</tt> in the +options+ hash.
106 #
107 # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
108 # of +collection+. The return values are used as the +value+ attribute and contents of each
109 # <tt><option></tt> tag, respectively.
110 #
111 # Example object structure for use with this method:
112 # class Post < ActiveRecord::Base
113 # belongs_to :author
114 # end
115 # class Author < ActiveRecord::Base
116 # has_many :posts
117 # def name_with_initial
118 # "#{first_name.first}. #{last_name}"
119 # end
120 # end
121 #
122 # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
123 # collection_select(:post, :author_id, Author.find(:all), :id, :name_with_initial, {:prompt => true})
124 #
125 # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
126 # <select name="post[author_id]">
127 # <option value="">Please select</option>
128 # <option value="1" selected="selected">D. Heinemeier Hansson</option>
129 # <option value="2">D. Thomas</option>
130 # <option value="3">M. Clark</option>
131 # </select>
132 def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
133 InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
134 end
135
136 # Return select and option tags for the given object and method, using
137 # #time_zone_options_for_select to generate the list of option tags.
138 #
139 # In addition to the <tt>:include_blank</tt> option documented above,
140 # this method also supports a <tt>:model</tt> option, which defaults
141 # to TimeZone. This may be used by users to specify a different time
142 # zone model object. (See +time_zone_options_for_select+ for more
143 # information.)
144 #
145 # You can also supply an array of TimeZone objects
146 # as +priority_zones+, so that they will be listed above the rest of the
147 # (long) list. (You can use TimeZone.us_zones as a convenience for
148 # obtaining a list of the US time zones, or a Regexp to select the zones
149 # of your choice)
150 #
151 # Finally, this method supports a <tt>:default</tt> option, which selects
152 # a default TimeZone if the object's time zone is +nil+.
153 #
154 # Examples:
155 # time_zone_select( "user", "time_zone", nil, :include_blank => true)
156 #
157 # time_zone_select( "user", "time_zone", nil, :default => "Pacific Time (US & Canada)" )
158 #
159 # time_zone_select( "user", 'time_zone', TimeZone.us_zones, :default => "Pacific Time (US & Canada)")
160 #
161 # time_zone_select( "user", 'time_zone', [ TimeZone['Alaska'], TimeZone['Hawaii'] ])
162 #
163 # time_zone_select( "user", 'time_zone', /Australia/)
164 #
165 # time_zone_select( "user", "time_zone", TZInfo::Timezone.all.sort, :model => TZInfo::Timezone)
166 def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
167 InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
168 end
169
170 # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
171 # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
172 # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
173 # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
174 # may also be an array of values to be selected when using a multiple select.
175 #
176 # Examples (call, result):
177 # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
178 # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
179 #
180 # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
181 # <option>VISA</option>\n<option selected="selected">MasterCard</option>
182 #
183 # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
184 # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
185 #
186 # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
187 # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option>
188 #
189 # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
190 def options_for_select(container, selected = nil)
191 container = container.to_a if Hash === container
192
193 options_for_select = container.inject([]) do |options, element|
194 text, value = option_text_and_value(element)
195 selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
196 options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}>#{html_escape(text.to_s)}</option>)
197 end
198
199 options_for_select.join("\n")
200 end
201
202 # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
203 # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
204 # If +selected+ is specified, the element returning a match on +value_method+ will get the selected option tag.
205 #
206 # Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate an input tag:
207 # options_from_collection_for_select(@project.people, "id", "name")
208 # <option value="#{person.id}">#{person.name}</option>
209 #
210 # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
211 def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
212 options = collection.map do |element|
213 [element.send(text_method), element.send(value_method)]
214 end
215 options_for_select(options, selected)
216 end
217
218 # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
219 # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
220 #
221 # Parameters:
222 # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
223 # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
224 # array of child objects representing the <tt><option></tt> tags.
225 # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
226 # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
227 # * +option_key_method+ - The name of a method which, when called on a child object of a member of
228 # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
229 # * +option_value_method+ - The name of a method which, when called on a child object of a member of
230 # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
231 # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
232 # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
233 # to +option_key_method+. If +nil+, no selection is made.
234 #
235 # Example object structure for use with this method:
236 # class Continent < ActiveRecord::Base
237 # has_many :countries
238 # # attribs: id, name
239 # end
240 # class Country < ActiveRecord::Base
241 # belongs_to :continent
242 # # attribs: id, name, continent_id
243 # end
244 #
245 # Sample usage:
246 # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
247 #
248 # Possible output:
249 # <optgroup label="Africa">
250 # <option value="1">Egypt</option>
251 # <option value="4">Rwanda</option>
252 # ...
253 # </optgroup>
254 # <optgroup label="Asia">
255 # <option value="3" selected="selected">China</option>
256 # <option value="12">India</option>
257 # <option value="5">Japan</option>
258 # ...
259 # </optgroup>
260 #
261 # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
262 # wrap the output in an appropriate <tt><select></tt> tag.
263 def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
264 collection.inject("") do |options_for_select, group|
265 group_label_string = eval("group.#{group_label_method}")
266 options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
267 options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
268 options_for_select += '</optgroup>'
269 end
270 end
271
272 # Returns a string of option tags for pretty much any time zone in the
273 # world. Supply a TimeZone name as +selected+ to have it marked as the
274 # selected option tag. You can also supply an array of TimeZone objects
275 # as +priority_zones+, so that they will be listed above the rest of the
276 # (long) list. (You can use TimeZone.us_zones as a convenience for
277 # obtaining a list of the US time zones, or a Regexp to select the zones
278 # of your choice)
279 #
280 # The +selected+ parameter must be either +nil+, or a string that names
281 # a TimeZone.
282 #
283 # By default, +model+ is the TimeZone constant (which can be obtained
284 # in Active Record as a value object). The only requirement is that the
285 # +model+ parameter be an object that responds to +all+, and returns
286 # an array of objects that represent time zones.
287 #
288 # NOTE: Only the option tags are returned, you have to wrap this call in
289 # a regular HTML select tag.
290 def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
291 zone_options = ""
292
293 zones = model.all
294 convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
295
296 if priority_zones
297 if priority_zones.is_a?(Regexp)
298 priority_zones = model.all.find_all {|z| z =~ priority_zones}
299 end
300 zone_options += options_for_select(convert_zones[priority_zones], selected)
301 zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
302
303 zones = zones.reject { |z| priority_zones.include?( z ) }
304 end
305
306 zone_options += options_for_select(convert_zones[zones], selected)
307 zone_options
308 end
309
310 private
311 def option_text_and_value(option)
312 # Options are [text, value] pairs or strings used for both.
313 if !option.is_a?(String) and option.respond_to?(:first) and option.respond_to?(:last)
314 [option.first, option.last]
315 else
316 [option, option]
317 end
318 end
319
320 def option_value_selected?(value, selected)
321 if selected.respond_to?(:include?) && !selected.is_a?(String)
322 selected.include? value
323 else
324 value == selected
325 end
326 end
327 end
328
329 class InstanceTag #:nodoc:
330 include FormOptionsHelper
331
332 def to_select_tag(choices, options, html_options)
333 html_options = html_options.stringify_keys
334 add_default_name_and_id(html_options)
335 value = value(object)
336 selected_value = options.has_key?(:selected) ? options[:selected] : value
337 content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options)
338 end
339
340 def to_collection_select_tag(collection, value_method, text_method, options, html_options)
341 html_options = html_options.stringify_keys
342 add_default_name_and_id(html_options)
343 value = value(object)
344 content_tag(
345 "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options
346 )
347 end
348
349 def to_time_zone_select_tag(priority_zones, options, html_options)
350 html_options = html_options.stringify_keys
351 add_default_name_and_id(html_options)
352 value = value(object)
353 content_tag("select",
354 add_options(
355 time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone),
356 options, value
357 ), html_options
358 )
359 end
360
361 private
362 def add_options(option_tags, options, value = nil)
363 if options[:include_blank]
364 option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
365 end
366 if value.blank? && options[:prompt]
367 ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags
368 else
369 option_tags
370 end
371 end
372 end
373
374 class FormBuilder
375 def select(method, choices, options = {}, html_options = {})
376 @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
377 end
378
379 def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
380 @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
381 end
382
383 def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
384 @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
385 end
386 end
387 end
388 end