techthumb-formtastic 1.rails3.sha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +584 -0
  3. data/Rakefile +127 -0
  4. data/generators/form/USAGE +16 -0
  5. data/generators/form/form_generator.rb +120 -0
  6. data/generators/form/templates/view__form.html.erb +5 -0
  7. data/generators/form/templates/view__form.html.haml +4 -0
  8. data/generators/formtastic/formtastic_generator.rb +24 -0
  9. data/generators/formtastic/templates/formtastic.css +131 -0
  10. data/generators/formtastic/templates/formtastic.rb +54 -0
  11. data/generators/formtastic/templates/formtastic_changes.css +14 -0
  12. data/generators/formtastic_stylesheets/formtastic_stylesheets_generator.rb +16 -0
  13. data/init.rb +5 -0
  14. data/lib/formtastic/i18n.rb +35 -0
  15. data/lib/formtastic/layout_helper.rb +10 -0
  16. data/lib/formtastic/railtie.rb +12 -0
  17. data/lib/formtastic/util.rb +36 -0
  18. data/lib/formtastic.rb +1870 -0
  19. data/lib/generators/formtastic/form/form_generator.rb +86 -0
  20. data/lib/generators/formtastic/install/install_generator.rb +24 -0
  21. data/lib/locale/en.yml +8 -0
  22. data/rails/init.rb +2 -0
  23. data/spec/buttons_spec.rb +166 -0
  24. data/spec/commit_button_spec.rb +401 -0
  25. data/spec/custom_builder_spec.rb +98 -0
  26. data/spec/defaults_spec.rb +20 -0
  27. data/spec/error_proc_spec.rb +27 -0
  28. data/spec/errors_spec.rb +105 -0
  29. data/spec/form_helper_spec.rb +142 -0
  30. data/spec/helpers/layout_helper_spec.rb +21 -0
  31. data/spec/i18n_spec.rb +152 -0
  32. data/spec/include_blank_spec.rb +74 -0
  33. data/spec/input_spec.rb +786 -0
  34. data/spec/inputs/boolean_input_spec.rb +104 -0
  35. data/spec/inputs/check_boxes_input_spec.rb +426 -0
  36. data/spec/inputs/country_input_spec.rb +118 -0
  37. data/spec/inputs/date_input_spec.rb +168 -0
  38. data/spec/inputs/datetime_input_spec.rb +310 -0
  39. data/spec/inputs/file_input_spec.rb +34 -0
  40. data/spec/inputs/hidden_input_spec.rb +78 -0
  41. data/spec/inputs/numeric_input_spec.rb +44 -0
  42. data/spec/inputs/password_input_spec.rb +46 -0
  43. data/spec/inputs/radio_input_spec.rb +243 -0
  44. data/spec/inputs/select_input_spec.rb +546 -0
  45. data/spec/inputs/string_input_spec.rb +64 -0
  46. data/spec/inputs/text_input_spec.rb +34 -0
  47. data/spec/inputs/time_input_spec.rb +206 -0
  48. data/spec/inputs/time_zone_input_spec.rb +110 -0
  49. data/spec/inputs_spec.rb +476 -0
  50. data/spec/label_spec.rb +89 -0
  51. data/spec/semantic_errors_spec.rb +98 -0
  52. data/spec/semantic_fields_for_spec.rb +45 -0
  53. data/spec/spec.opts +2 -0
  54. data/spec/spec_helper.rb +289 -0
  55. data/spec/support/custom_macros.rb +494 -0
  56. data/spec/support/output_buffer.rb +4 -0
  57. data/spec/support/test_environment.rb +45 -0
  58. metadata +235 -0
data/lib/formtastic.rb ADDED
@@ -0,0 +1,1870 @@
1
+ # coding: utf-8
2
+ require File.join(File.dirname(__FILE__), *%w[formtastic i18n])
3
+ require File.join(File.dirname(__FILE__), *%w[formtastic util])
4
+ require File.join(File.dirname(__FILE__), *%w[formtastic railtie]) if defined?(::Rails::Railtie)
5
+
6
+ module Formtastic #:nodoc:
7
+
8
+ class SemanticFormBuilder < ActionView::Helpers::FormBuilder
9
+ class_inheritable_accessor :default_text_field_size, :default_text_area_height, :all_fields_required_by_default, :include_blank_for_select_by_default,
10
+ :required_string, :optional_string, :inline_errors, :label_str_method, :collection_value_methods, :collection_label_methods,
11
+ :inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default, :escape_html_entities_in_hints_and_labels, :default_commit_button_accesskey,
12
+ :instance_reader => false
13
+
14
+ self.default_text_field_size = 50
15
+ self.default_text_area_height = 20
16
+ self.all_fields_required_by_default = true
17
+ self.include_blank_for_select_by_default = true
18
+ self.required_string = proc { ::Formtastic::Util.html_safe(%{<abbr title="#{::Formtastic::I18n.t(:required)}">*</abbr>}) }
19
+ self.optional_string = ''
20
+ self.inline_errors = :sentence
21
+ self.label_str_method = :humanize
22
+ self.collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
23
+ self.collection_value_methods = %w[id to_s]
24
+ self.inline_order = [ :input, :hints, :errors ]
25
+ self.file_methods = [ :file?, :public_filename, :filename ]
26
+ self.priority_countries = ["Australia", "Canada", "United Kingdom", "United States"]
27
+ self.i18n_lookups_by_default = false
28
+ self.escape_html_entities_in_hints_and_labels = true
29
+ self.default_commit_button_accesskey = nil
30
+
31
+ RESERVED_COLUMNS = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
32
+
33
+ INLINE_ERROR_TYPES = [:sentence, :list, :first]
34
+
35
+ attr_accessor :template
36
+
37
+ # Returns a suitable form input for the given +method+, using the database column information
38
+ # and other factors (like the method name) to figure out what you probably want.
39
+ #
40
+ # Options:
41
+ #
42
+ # * :as - override the input type (eg force a :string to render as a :password field)
43
+ # * :label - use something other than the method name as the label text, when false no label is printed
44
+ # * :required - specify if the column is required (true) or not (false)
45
+ # * :hint - provide some text to hint or help the user provide the correct information for a field
46
+ # * :input_html - provide options that will be passed down to the generated input
47
+ # * :wrapper_html - provide options that will be passed down to the li wrapper
48
+ #
49
+ # Input Types:
50
+ #
51
+ # Most inputs map directly to one of ActiveRecord's column types by default (eg string_input),
52
+ # but there are a few special cases and some simplification (:integer, :float and :decimal
53
+ # columns all map to a single numeric_input, for example).
54
+ #
55
+ # * :select (a select menu for associations) - default to association names
56
+ # * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations
57
+ # * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations
58
+ # * :time_zone (a select menu with time zones)
59
+ # * :password (a password input) - default for :string column types with 'password' in the method name
60
+ # * :text (a textarea) - default for :text column types
61
+ # * :date (a date select) - default for :date column types
62
+ # * :datetime (a date and time select) - default for :datetime and :timestamp column types
63
+ # * :time (a time select) - default for :time column types
64
+ # * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
65
+ # * :string (a text field) - default for :string column types
66
+ # * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
67
+ # * :country (a select menu of country names) - requires a country_select plugin to be installed
68
+ # * :hidden (a hidden field) - creates a hidden field (added for compatibility)
69
+ #
70
+ # Example:
71
+ #
72
+ # <% semantic_form_for @employee do |form| %>
73
+ # <% form.inputs do -%>
74
+ # <%= form.input :secret, :value => "Hello" %>
75
+ # <%= form.input :name, :label => "Full Name" %>
76
+ # <%= form.input :manager_id, :as => :radio %>
77
+ # <%= form.input :hired_at, :as => :date, :label => "Date Hired" %>
78
+ # <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
79
+ # <% end %>
80
+ # <% end %>
81
+ #
82
+ def input(method, options = {})
83
+ if options.key?(:selected) || options.key?(:checked) || options.key?(:default)
84
+ ::ActiveSupport::Deprecation.warn(
85
+ "The :selected, :checked (and :default) options are deprecated in Formtastic and will be removed from 1.0. " <<
86
+ "Please set default values in your models (using an after_initialize callback) or in your controller set-up. " <<
87
+ "See http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html for more information.", caller)
88
+ end
89
+
90
+ options[:required] = method_required?(method) unless options.key?(:required)
91
+ options[:as] ||= default_input_type(method, options)
92
+
93
+ html_class = [ options[:as], (options[:required] ? :required : :optional) ]
94
+ html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
95
+
96
+ wrapper_html = options.delete(:wrapper_html) || {}
97
+ wrapper_html[:id] ||= generate_html_id(method)
98
+ wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
99
+
100
+ if options[:input_html] && options[:input_html][:id]
101
+ options[:label_html] ||= {}
102
+ options[:label_html][:for] ||= options[:input_html][:id]
103
+ end
104
+
105
+ input_parts = self.class.inline_order.dup
106
+ input_parts = input_parts - [:errors, :hints] if options[:as] == :hidden
107
+
108
+ list_item_content = input_parts.map do |type|
109
+ send(:"inline_#{type}_for", method, options)
110
+ end.compact.join("\n")
111
+
112
+ return template.content_tag(:li, Formtastic::Util.html_safe(list_item_content), wrapper_html)
113
+ end
114
+
115
+ # Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
116
+ # called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
117
+ # or with a list of fields. These two examples are functionally equivalent:
118
+ #
119
+ # # With a block:
120
+ # <% semantic_form_for @post do |form| %>
121
+ # <% form.inputs do %>
122
+ # <%= form.input :title %>
123
+ # <%= form.input :body %>
124
+ # <% end %>
125
+ # <% end %>
126
+ #
127
+ # # With a list of fields:
128
+ # <% semantic_form_for @post do |form| %>
129
+ # <%= form.inputs :title, :body %>
130
+ # <% end %>
131
+ #
132
+ # # Output:
133
+ # <form ...>
134
+ # <fieldset class="inputs">
135
+ # <ol>
136
+ # <li class="string">...</li>
137
+ # <li class="text">...</li>
138
+ # </ol>
139
+ # </fieldset>
140
+ # </form>
141
+ #
142
+ # === Quick Forms
143
+ #
144
+ # When called without a block or a field list, an input is rendered for each column in the
145
+ # model's database table, just like Rails' scaffolding. You'll obviously want more control
146
+ # than this in a production application, but it's a great way to get started, then come back
147
+ # later to customise the form with a field list or a block of inputs. Example:
148
+ #
149
+ # <% semantic_form_for @post do |form| %>
150
+ # <%= form.inputs %>
151
+ # <% end %>
152
+ #
153
+ # With a few arguments:
154
+ # <% semantic_form_for @post do |form| %>
155
+ # <%= form.inputs "Post details", :title, :body %>
156
+ # <% end %>
157
+ #
158
+ # === Options
159
+ #
160
+ # All options (with the exception of :name/:title) are passed down to the fieldset as HTML
161
+ # attributes (id, class, style, etc). If provided, the :name/:title option is passed into a
162
+ # legend tag inside the fieldset.
163
+ #
164
+ # # With a block:
165
+ # <% semantic_form_for @post do |form| %>
166
+ # <% form.inputs :name => "Create a new post", :style => "border:1px;" do %>
167
+ # ...
168
+ # <% end %>
169
+ # <% end %>
170
+ #
171
+ # # With a list (the options must come after the field list):
172
+ # <% semantic_form_for @post do |form| %>
173
+ # <%= form.inputs :title, :body, :name => "Create a new post", :style => "border:1px;" %>
174
+ # <% end %>
175
+ #
176
+ # # ...or the equivalent:
177
+ # <% semantic_form_for @post do |form| %>
178
+ # <%= form.inputs "Create a new post", :title, :body, :style => "border:1px;" %>
179
+ # <% end %>
180
+ #
181
+ # === It's basically a fieldset!
182
+ #
183
+ # Instead of hard-coding fieldsets & legends into your form to logically group related fields,
184
+ # use inputs:
185
+ #
186
+ # <% semantic_form_for @post do |f| %>
187
+ # <% f.inputs do %>
188
+ # <%= f.input :title %>
189
+ # <%= f.input :body %>
190
+ # <% end %>
191
+ # <% f.inputs :name => "Advanced", :id => "advanced" do %>
192
+ # <%= f.input :created_at %>
193
+ # <%= f.input :user_id, :label => "Author" %>
194
+ # <% end %>
195
+ # <% f.inputs "Extra" do %>
196
+ # <%= f.input :update_at %>
197
+ # <% end %>
198
+ # <% end %>
199
+ #
200
+ # # Output:
201
+ # <form ...>
202
+ # <fieldset class="inputs">
203
+ # <ol>
204
+ # <li class="string">...</li>
205
+ # <li class="text">...</li>
206
+ # </ol>
207
+ # </fieldset>
208
+ # <fieldset class="inputs" id="advanced">
209
+ # <legend><span>Advanced</span></legend>
210
+ # <ol>
211
+ # <li class="datetime">...</li>
212
+ # <li class="select">...</li>
213
+ # </ol>
214
+ # </fieldset>
215
+ # <fieldset class="inputs">
216
+ # <legend><span>Extra</span></legend>
217
+ # <ol>
218
+ # <li class="datetime">...</li>
219
+ # </ol>
220
+ # </fieldset>
221
+ # </form>
222
+ #
223
+ # === Nested attributes
224
+ #
225
+ # As in Rails, you can use semantic_fields_for to nest attributes:
226
+ #
227
+ # <% semantic_form_for @post do |form| %>
228
+ # <%= form.inputs :title, :body %>
229
+ #
230
+ # <% form.semantic_fields_for :author, @bob do |author_form| %>
231
+ # <% author_form.inputs do %>
232
+ # <%= author_form.input :first_name, :required => false %>
233
+ # <%= author_form.input :last_name %>
234
+ # <% end %>
235
+ # <% end %>
236
+ # <% end %>
237
+ #
238
+ # But this does not look formtastic! This is equivalent:
239
+ #
240
+ # <% semantic_form_for @post do |form| %>
241
+ # <%= form.inputs :title, :body %>
242
+ # <% form.inputs :for => [ :author, @bob ] do |author_form| %>
243
+ # <%= author_form.input :first_name, :required => false %>
244
+ # <%= author_form.input :last_name %>
245
+ # <% end %>
246
+ # <% end %>
247
+ #
248
+ # And if you don't need to give options to your input call, you could do it
249
+ # in just one line:
250
+ #
251
+ # <% semantic_form_for @post do |form| %>
252
+ # <%= form.inputs :title, :body %>
253
+ # <%= form.inputs :first_name, :last_name, :for => @bob %>
254
+ # <% end %>
255
+ #
256
+ # Just remember that calling inputs generates a new fieldset to wrap your
257
+ # inputs. If you have two separate models, but, semantically, on the page
258
+ # they are part of the same fieldset, you should use semantic_fields_for
259
+ # instead (just as you would do with Rails' form builder).
260
+ #
261
+ def inputs(*args, &block)
262
+ title = field_set_title_from_args(*args)
263
+ html_options = args.extract_options!
264
+ html_options[:class] ||= "inputs"
265
+ html_options[:name] = title
266
+
267
+ if html_options[:for] # Nested form
268
+ inputs_for_nested_attributes(*(args << html_options), &block)
269
+ elsif block_given?
270
+ field_set_and_list_wrapping(*(args << html_options), &block)
271
+ else
272
+ if @object && args.empty?
273
+ args = self.association_columns(:belongs_to)
274
+ args += self.content_columns
275
+ args -= RESERVED_COLUMNS
276
+ args.compact!
277
+ end
278
+ legend = args.shift if args.first.is_a?(::String)
279
+ contents = args.collect { |method| input(method.to_sym) }
280
+ args.unshift(legend) if legend.present?
281
+
282
+ field_set_and_list_wrapping(*((args << html_options) << contents))
283
+ end
284
+ end
285
+ alias :input_field_set :inputs
286
+
287
+ # Creates a fieldset and ol tag wrapping for form buttons / actions as list items.
288
+ # See inputs documentation for a full example. The fieldset's default class attriute
289
+ # is set to "buttons".
290
+ #
291
+ # See inputs for html attributes and special options.
292
+ def buttons(*args, &block)
293
+ html_options = args.extract_options!
294
+ html_options[:class] ||= "buttons"
295
+
296
+ if block_given?
297
+ field_set_and_list_wrapping(html_options, &block)
298
+ else
299
+ args = [:commit] if args.empty?
300
+ contents = args.map { |button_name| send(:"#{button_name}_button") }
301
+ field_set_and_list_wrapping(html_options, contents)
302
+ end
303
+ end
304
+ alias :button_field_set :buttons
305
+
306
+ # Creates a submit input tag with the value "Save [model name]" (for existing records) or
307
+ # "Create [model name]" (for new records) by default:
308
+ #
309
+ # <%= form.commit_button %> => <input name="commit" type="submit" value="Save Post" />
310
+ #
311
+ # The value of the button text can be overridden:
312
+ #
313
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
314
+ # <%= form.commit_button :label => "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
315
+ #
316
+ # And you can pass html atributes down to the input, with or without the button text:
317
+ #
318
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
319
+ # <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty {create|update|submit}" />
320
+ #
321
+ def commit_button(*args)
322
+ options = args.extract_options!
323
+ text = options.delete(:label) || args.shift
324
+
325
+ if @object && @object.respond_to?(:new_record?)
326
+ key = @object.new_record? ? :create : :update
327
+
328
+ # Deal with some complications with ActiveRecord::Base.human_name and two name models (eg UserPost)
329
+ # ActiveRecord::Base.human_name falls back to ActiveRecord::Base.name.humanize ("Userpost")
330
+ # if there's no i18n, which is pretty crappy. In this circumstance we want to detect this
331
+ # fall back (human_name == name.humanize) and do our own thing name.underscore.humanize ("User Post")
332
+ if @object.class.model_name.respond_to?(:human)
333
+ object_name = @object.class.model_name.human
334
+ else
335
+ object_human_name = @object.class.human_name # default is UserPost => "Userpost", but i18n may do better ("User post")
336
+ crappy_human_name = @object.class.name.humanize # UserPost => "Userpost"
337
+ decent_human_name = @object.class.name.underscore.humanize # UserPost => "User post"
338
+ object_name = (object_human_name == crappy_human_name) ? decent_human_name : object_human_name
339
+ end
340
+ else
341
+ key = :submit
342
+ object_name = @object_name.to_s.send(self.class.label_str_method)
343
+ end
344
+
345
+ text = (self.localized_string(key, text, :action, :model => object_name) ||
346
+ ::Formtastic::I18n.t(key, :model => object_name)) unless text.is_a?(::String)
347
+
348
+ button_html = options.delete(:button_html) || {}
349
+ button_html.merge!(:class => [button_html[:class], key].compact.join(' '))
350
+ element_class = ['commit', options.delete(:class)].compact.join(' ') # TODO: Add class reflecting on form action.
351
+ accesskey = (options.delete(:accesskey) || self.class.default_commit_button_accesskey) unless button_html.has_key?(:accesskey)
352
+ button_html = button_html.merge(:accesskey => accesskey) if accesskey
353
+ template.content_tag(:li, Formtastic::Util.html_safe(self.submit(text, button_html)), :class => element_class)
354
+ end
355
+
356
+ # A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
357
+ # for nesting forms:
358
+ #
359
+ # # Example:
360
+ # <% semantic_form_for @post do |post| %>
361
+ # <% post.semantic_fields_for :author do |author| %>
362
+ # <% author.inputs :name %>
363
+ # <% end %>
364
+ # <% end %>
365
+ #
366
+ # # Output:
367
+ # <form ...>
368
+ # <fieldset class="inputs">
369
+ # <ol>
370
+ # <li class="string"><input type='text' name='post[author][name]' id='post_author_name' /></li>
371
+ # </ol>
372
+ # </fieldset>
373
+ # </form>
374
+ #
375
+ def semantic_fields_for(record_or_name_or_array, *args, &block)
376
+ opts = args.extract_options!
377
+ opts[:builder] ||= self.class
378
+ args.push(opts)
379
+ fields_for(record_or_name_or_array, *args, &block)
380
+ end
381
+
382
+ # Generates the label for the input. It also accepts the same arguments as
383
+ # Rails label method. It has three options that are not supported by Rails
384
+ # label method:
385
+ #
386
+ # * :required - Appends an abbr tag if :required is true
387
+ # * :label - An alternative form to give the label content. Whenever label
388
+ # is false, a blank string is returned.
389
+ # * :input_name - Gives the input to match for. This is needed when you want to
390
+ # to call f.label :authors but it should match :author_ids.
391
+ #
392
+ # == Examples
393
+ #
394
+ # f.label :title # like in rails, except that it searches the label on I18n API too
395
+ #
396
+ # f.label :title, "Your post title"
397
+ # f.label :title, :label => "Your post title" # Added for formtastic API
398
+ #
399
+ # f.label :title, :required => true # Returns <label>Title<abbr title="required">*</abbr></label>
400
+ #
401
+ def label(method, options_or_text=nil, options=nil)
402
+ if options_or_text.is_a?(Hash)
403
+ return "" if options_or_text[:label] == false
404
+ options = options_or_text
405
+ text = options.delete(:label)
406
+ else
407
+ text = options_or_text
408
+ options ||= {}
409
+ end
410
+
411
+ text = localized_string(method, text, :label) || humanized_attribute_name(method)
412
+ text += required_or_optional_string(options.delete(:required))
413
+ text = Formtastic::Util.html_safe(text)
414
+
415
+ # special case for boolean (checkbox) labels, which have a nested input
416
+ if options.key?(:label_prefix_for_nested_input)
417
+ text = options.delete(:label_prefix_for_nested_input) + text
418
+ end
419
+
420
+ input_name = options.delete(:input_name) || method
421
+ super(input_name, text, options)
422
+ end
423
+
424
+ # Generates error messages for the given method. Errors can be shown as list,
425
+ # as sentence or just the first error can be displayed. If :none is set, no error is shown.
426
+ #
427
+ # This method is also aliased as errors_on, so you can call on your custom
428
+ # inputs as well:
429
+ #
430
+ # semantic_form_for :post do |f|
431
+ # f.text_field(:body)
432
+ # f.errors_on(:body)
433
+ # end
434
+ #
435
+ def inline_errors_for(method, options = nil) #:nodoc:
436
+ if render_inline_errors?
437
+ errors = [@object.errors[method.to_sym]]
438
+ errors << [@object.errors[association_primary_key(method)]] if association_macro_for_method(method) == :belongs_to
439
+ errors = errors.flatten.compact.uniq
440
+ send(:"error_#{self.class.inline_errors}", [*errors]) if errors.any?
441
+ else
442
+ nil
443
+ end
444
+ end
445
+ alias :errors_on :inline_errors_for
446
+
447
+ # Generates error messages for given method names and for base.
448
+ # You can pass a hash with html options that will be added to ul tag
449
+ #
450
+ # == Examples
451
+ #
452
+ # f.semantic_errors # This will show only errors on base
453
+ # f.semantic_errors :state # This will show errors on base and state
454
+ # f.semantic_errors :state, :class => "awesome" # errors will be rendered in ul.awesome
455
+ #
456
+ def semantic_errors(*args)
457
+ html_options = args.extract_options!
458
+ full_errors = args.inject([]) do |array, method|
459
+ attribute = localized_string(method, method.to_sym, :label) || humanized_attribute_name(method)
460
+ errors = Array(@object.errors[method.to_sym]).to_sentence
461
+ errors.present? ? array << [attribute, errors].join(" ") : array ||= []
462
+ end
463
+ full_errors << @object.errors[:base]
464
+ full_errors.flatten!
465
+ full_errors.compact!
466
+ return nil if full_errors.blank?
467
+ html_options[:class] ||= "errors"
468
+ template.content_tag(:ul, html_options) do
469
+ Formtastic::Util.html_safe(full_errors.map { |error| template.content_tag(:li, Formtastic::Util.html_safe(error)) }.join)
470
+ end
471
+ end
472
+
473
+ protected
474
+
475
+ def render_inline_errors?
476
+ @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(self.class.inline_errors)
477
+ end
478
+
479
+ # Collects content columns (non-relation columns) for the current form object class.
480
+ #
481
+ def content_columns #:nodoc:
482
+ self.model_name.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
483
+ end
484
+
485
+ # Collects association columns (relation columns) for the current form object class.
486
+ #
487
+ def association_columns(*by_associations) #:nodoc:
488
+ if @object.present? && @object.class.respond_to?(:reflections)
489
+ @object.class.reflections.collect do |name, _|
490
+ if by_associations.present?
491
+ name if by_associations.include?(_.macro)
492
+ else
493
+ name
494
+ end
495
+ end.compact
496
+ else
497
+ []
498
+ end
499
+ end
500
+
501
+ # Returns nil, or a symbol like :belongs_to or :has_many
502
+ def association_macro_for_method(method) #:nodoc:
503
+ reflection = self.reflection_for(method)
504
+ reflection.macro if reflection
505
+ end
506
+
507
+ def association_primary_key(method)
508
+ reflection = self.reflection_for(method)
509
+ reflection.options[:foreign_key] if reflection && !reflection.options[:foreign_key].blank?
510
+ :"#{method}_id"
511
+ end
512
+
513
+ # Prepare options to be sent to label
514
+ #
515
+ def options_for_label(options) #:nodoc:
516
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
517
+ end
518
+
519
+ # Deals with :for option when it's supplied to inputs methods. Additional
520
+ # options to be passed down to :for should be supplied using :for_options
521
+ # key.
522
+ #
523
+ # It should raise an error if a block with arity zero is given.
524
+ #
525
+ def inputs_for_nested_attributes(*args, &block) #:nodoc:
526
+ options = args.extract_options!
527
+ args << options.merge!(:parent => { :builder => self, :for => options[:for] })
528
+
529
+ fields_for_block = if block_given?
530
+ raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
531
+ 'but the block does not accept any argument.' if block.arity <= 0
532
+
533
+ lambda do |f|
534
+ contents = f.inputs(*args){ block.call(f) }
535
+ template.concat(contents) if ::Formtastic::Util.rails3?
536
+ contents
537
+ end
538
+ else
539
+ lambda do |f|
540
+ contents = f.inputs(*args)
541
+ template.concat(contents) if ::Formtastic::Util.rails3?
542
+ contents
543
+ end
544
+ end
545
+
546
+ fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
547
+ semantic_fields_for(*fields_for_args, &fields_for_block)
548
+ end
549
+
550
+ # Remove any Formtastic-specific options before passing the down options.
551
+ #
552
+ def strip_formtastic_options(options) #:nodoc:
553
+ options.except(:value_method, :label_method, :collection, :required, :label,
554
+ :as, :hint, :input_html, :label_html, :value_as_class, :find_options)
555
+ end
556
+
557
+ # Determins if the attribute (eg :title) should be considered required or not.
558
+ #
559
+ # * if the :required option was provided in the options hash, the true/false value will be
560
+ # returned immediately, allowing the view to override any guesswork that follows:
561
+ #
562
+ # * if the :required option isn't provided in the options hash, and the ValidationReflection
563
+ # plugin is installed (http://github.com/redinger/validation_reflection), or the object is
564
+ # an ActiveModel, true is returned
565
+ # if the validates_presence_of macro has been used in the class for this attribute, or false
566
+ # otherwise.
567
+ #
568
+ # * if the :required option isn't provided, and validates_presence_of can't be determined, the
569
+ # configuration option all_fields_required_by_default is used.
570
+ #
571
+ def method_required?(attribute) #:nodoc:
572
+ if @object && @object.class.respond_to?(:reflect_on_validations_for)
573
+ attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
574
+
575
+ @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
576
+ validation.macro == :validates_presence_of &&
577
+ validation.name == attribute_sym &&
578
+ (validation.options.present? ? options_require_validation?(validation.options) : true)
579
+ end
580
+ else
581
+ if @object && @object.class.respond_to?(:validators_on)
582
+ attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
583
+ !@object.class.validators_on(attribute_sym).find{|validator| (validator.kind == :presence) && (validator.options.present? ? options_require_validation?(validator.options) : true)}.nil?
584
+ else
585
+ self.class.all_fields_required_by_default
586
+ end
587
+ end
588
+ end
589
+
590
+ # Determines whether the given options evaluate to true
591
+ def options_require_validation?(options) #nodoc
592
+ if_condition = !options[:if].nil?
593
+ condition = if_condition ? options[:if] : options[:unless]
594
+
595
+ condition = if condition.respond_to?(:call)
596
+ condition.call(@object)
597
+ elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
598
+ @object.send(condition)
599
+ else
600
+ condition
601
+ end
602
+
603
+ if_condition ? !!condition : !condition
604
+ end
605
+
606
+ def basic_input_helper(form_helper_method, type, method, options) #:nodoc:
607
+ html_options = options.delete(:input_html) || {}
608
+ html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password, :text].include?(type)
609
+
610
+ self.label(method, options_for_label(options)) <<
611
+ self.send(form_helper_method, method, html_options)
612
+ end
613
+
614
+ # Outputs a label and standard Rails text field inside the wrapper.
615
+ def string_input(method, options)
616
+ basic_input_helper(:text_field, :string, method, options)
617
+ end
618
+
619
+ # Outputs a label and standard Rails password field inside the wrapper.
620
+ def password_input(method, options)
621
+ basic_input_helper(:password_field, :password, method, options)
622
+ end
623
+
624
+ # Outputs a label and standard Rails text field inside the wrapper.
625
+ def numeric_input(method, options)
626
+ basic_input_helper(:text_field, :numeric, method, options)
627
+ end
628
+
629
+ # Ouputs a label and standard Rails text area inside the wrapper.
630
+ def text_input(method, options)
631
+ basic_input_helper(:text_area, :text, method, options)
632
+ end
633
+
634
+ # Outputs a label and a standard Rails file field inside the wrapper.
635
+ def file_input(method, options)
636
+ basic_input_helper(:file_field, :file, method, options)
637
+ end
638
+
639
+ # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
640
+ # Additionals options can be given using :input_hml. Should :input_html not be
641
+ # specified every option except for formtastic options will be sent straight
642
+ # to hidden input element.
643
+ #
644
+ def hidden_input(method, options)
645
+ options ||= {}
646
+ html_options = options.delete(:input_html) || strip_formtastic_options(options)
647
+ self.hidden_field(method, html_options)
648
+ end
649
+
650
+ # Outputs a label and a select box containing options from the parent
651
+ # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
652
+ # is has_many or has_and_belongs_to_many the select box will be set as multi-select
653
+ # and size = 5
654
+ #
655
+ # Example (belongs_to):
656
+ #
657
+ # f.input :author
658
+ #
659
+ # <label for="book_author_id">Author</label>
660
+ # <select id="book_author_id" name="book[author_id]">
661
+ # <option value=""></option>
662
+ # <option value="1">Justin French</option>
663
+ # <option value="2">Jane Doe</option>
664
+ # </select>
665
+ #
666
+ # Example (has_many):
667
+ #
668
+ # f.input :chapters
669
+ #
670
+ # <label for="book_chapter_ids">Chapters</label>
671
+ # <select id="book_chapter_ids" name="book[chapter_ids]">
672
+ # <option value=""></option>
673
+ # <option value="1">Chapter 1</option>
674
+ # <option value="2">Chapter 2</option>
675
+ # </select>
676
+ #
677
+ # Example (has_and_belongs_to_many):
678
+ #
679
+ # f.input :authors
680
+ #
681
+ # <label for="book_author_ids">Authors</label>
682
+ # <select id="book_author_ids" name="book[author_ids]">
683
+ # <option value=""></option>
684
+ # <option value="1">Justin French</option>
685
+ # <option value="2">Jane Doe</option>
686
+ # </select>
687
+ #
688
+ #
689
+ # You can customize the options available in the select by passing in a collection (an Array or
690
+ # Hash) through the :collection option. If not provided, the choices are found by inferring the
691
+ # parent's class name from the method name and simply calling find(:all) on it
692
+ # (VehicleOwner.find(:all) in the example above).
693
+ #
694
+ # Examples:
695
+ #
696
+ # f.input :author, :collection => @authors
697
+ # f.input :author, :collection => Author.find(:all)
698
+ # f.input :author, :collection => [@justin, @kate]
699
+ # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
700
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
701
+ #
702
+ # The :label_method option allows you to customize the text label inside each option tag two ways:
703
+ #
704
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
705
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
706
+ #
707
+ # Examples:
708
+ #
709
+ # f.input :author, :label_method => :full_name
710
+ # f.input :author, :label_method => :login
711
+ # f.input :author, :label_method => :full_name_with_post_count
712
+ # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
713
+ #
714
+ # The :value_method option provides the same customization of the value attribute of each option tag.
715
+ #
716
+ # Examples:
717
+ #
718
+ # f.input :author, :value_method => :full_name
719
+ # f.input :author, :value_method => :login
720
+ # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
721
+ #
722
+ # You can pre-select a specific option value by passing in the :selected option.
723
+ #
724
+ # Examples:
725
+ #
726
+ # f.input :author, :selected => current_user.id
727
+ # f.input :author, :value_method => :login, :selected => current_user.login
728
+ # f.input :authors, :value_method => :login, :selected => Author.most_popular.collect(&:id)
729
+ # f.input :authors, :value_method => :login, :selected => nil # override any defaults: select none
730
+ #
731
+ # You can pass html_options to the select tag using :input_html => {}
732
+ #
733
+ # Examples:
734
+ #
735
+ # f.input :authors, :input_html => {:size => 20, :multiple => true}
736
+ #
737
+ # By default, all select inputs will have a blank option at the top of the list. You can add
738
+ # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
739
+ #
740
+ #
741
+ # You can group the options in optgroup elements by passing the :group_by option
742
+ # (Note: only tested for belongs_to relations)
743
+ #
744
+ # Examples:
745
+ #
746
+ # f.input :author, :group_by => :continent
747
+ #
748
+ # All the other options should work as expected. If you want to call a custom method on the
749
+ # group item. You can include the option:group_label_method
750
+ # Examples:
751
+ #
752
+ # f.input :author, :group_by => :continents, :group_label_method => :something_different
753
+ #
754
+ def select_input(method, options)
755
+ html_options = options.delete(:input_html) || {}
756
+ options = set_include_blank(options)
757
+ html_options[:multiple] = html_options[:multiple] || options.delete(:multiple)
758
+ html_options.delete(:multiple) if html_options[:multiple].nil?
759
+
760
+ reflection = self.reflection_for(method)
761
+ if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
762
+ options[:include_blank] = false
763
+ html_options[:multiple] = true if html_options[:multiple].nil?
764
+ html_options[:size] ||= 5
765
+ end
766
+ options[:selected] = options[:selected].first if options[:selected].present? && html_options[:multiple] == false
767
+ input_name = generate_association_input_name(method)
768
+
769
+ select_html = if options[:group_by]
770
+ # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
771
+ # The formtastic user however shouldn't notice this too much.
772
+ raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
773
+ label, value = detect_label_and_value_method!(raw_collection)
774
+ group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
775
+ group_label_method = options[:group_label_method] || detect_label_method(group_collection)
776
+ group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
777
+ group_association = options[:group_association] || detect_group_association(method, options[:group_by])
778
+
779
+ # Here comes the monster with 8 arguments
780
+ self.grouped_collection_select(input_name, group_collection,
781
+ group_association, group_label_method,
782
+ value, label,
783
+ strip_formtastic_options(options), html_options)
784
+ else
785
+ collection = find_collection_for_column(method, options)
786
+
787
+ self.select(input_name, collection, strip_formtastic_options(options), html_options)
788
+ end
789
+
790
+ self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
791
+ end
792
+ alias :boolean_select_input :select_input
793
+
794
+ # Outputs a timezone select input as Rails' time_zone_select helper. You
795
+ # can give priority zones as option.
796
+ #
797
+ # Examples:
798
+ #
799
+ # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
800
+ #
801
+ # You can pre-select a specific option value by passing in the :selected option.
802
+ # Note: Right now only works if the form object attribute value is not set (nil),
803
+ # because of how the core helper is implemented.
804
+ #
805
+ # Examples:
806
+ #
807
+ # f.input :my_favorite_time_zone, :as => :time_zone, :selected => 'Singapore'
808
+ #
809
+ def time_zone_input(method, options)
810
+ html_options = options.delete(:input_html) || {}
811
+ selected_value = options.delete(:selected)
812
+
813
+ self.label(method, options_for_label(options)) <<
814
+ self.time_zone_select(method, options.delete(:priority_zones),
815
+ strip_formtastic_options(options).merge(:default => selected_value), html_options)
816
+ end
817
+
818
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
819
+ # items, one for each possible choice in the belongs_to association. Each li contains a
820
+ # label and a radio input.
821
+ #
822
+ # Example:
823
+ #
824
+ # f.input :author, :as => :radio
825
+ #
826
+ # Output:
827
+ #
828
+ # <fieldset>
829
+ # <legend><span>Author</span></legend>
830
+ # <ol>
831
+ # <li>
832
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
833
+ # </li>
834
+ # <li>
835
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
836
+ # </li>
837
+ # </ol>
838
+ # </fieldset>
839
+ #
840
+ # You can customize the choices available in the radio button set by passing in a collection (an Array or
841
+ # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
842
+ # (Author.find(:all) in the example above).
843
+ #
844
+ # Examples:
845
+ #
846
+ # f.input :author, :as => :radio, :collection => @authors
847
+ # f.input :author, :as => :radio, :collection => Author.find(:all)
848
+ # f.input :author, :as => :radio, :collection => [@justin, @kate]
849
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
850
+ #
851
+ # The :label_method option allows you to customize the label for each radio button two ways:
852
+ #
853
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
854
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
855
+ #
856
+ # Examples:
857
+ #
858
+ # f.input :author, :as => :radio, :label_method => :full_name
859
+ # f.input :author, :as => :radio, :label_method => :login
860
+ # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
861
+ # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
862
+ #
863
+ # The :value_method option provides the same customization of the value attribute of each option tag.
864
+ #
865
+ # Examples:
866
+ #
867
+ # f.input :author, :as => :radio, :value_method => :full_name
868
+ # f.input :author, :as => :radio, :value_method => :login
869
+ # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
870
+ #
871
+ # You can force a particular radio button in the collection to be checked with the :selected option.
872
+ #
873
+ # Examples:
874
+ #
875
+ # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
876
+ # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
877
+ #
878
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
879
+ # button / label combination to contain a class with the value of the radio button (useful for
880
+ # applying specific CSS or Javascript to a particular radio button).
881
+ #
882
+ def radio_input(method, options)
883
+ collection = find_collection_for_column(method, options)
884
+ html_options = strip_formtastic_options(options).merge(options.delete(:input_html) || {})
885
+
886
+ input_name = generate_association_input_name(method)
887
+ value_as_class = options.delete(:value_as_class)
888
+ input_ids = []
889
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
890
+ selected_value = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
891
+
892
+ list_item_content = collection.map do |c|
893
+ label = c.is_a?(Array) ? c.first : c
894
+ value = c.is_a?(Array) ? c.last : c
895
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
896
+ input_ids << input_id
897
+
898
+ html_options[:checked] = selected_value == value if selected_option_is_present
899
+
900
+ li_content = template.content_tag(:label,
901
+ Formtastic::Util.html_safe("#{self.radio_button(input_name, value, html_options)} #{escape_html_entities(label)}"),
902
+ :for => input_id
903
+ )
904
+
905
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
906
+ template.content_tag(:li, Formtastic::Util.html_safe(li_content), li_options)
907
+ end
908
+
909
+ template.content_tag(:fieldset,
910
+ legend_tag(method, options) << template.content_tag(:ol, Formtastic::Util.html_safe(list_item_content.join))
911
+ )
912
+ end
913
+ alias :boolean_radio_input :radio_input
914
+
915
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
916
+ # items (li), one for each fragment for the date (year, month, day). Each li contains a label
917
+ # (eg "Year") and a select box. Overwriting the label is possible by adding the :labels option.
918
+ # :labels should be a hash with the field (e.g. day) as key and the label text as value.
919
+ # See date_or_datetime_input for a more detailed output example.
920
+ #
921
+ # You can pre-select a specific option value by passing in the :selected option.
922
+ #
923
+ # Examples:
924
+ #
925
+ # f.input :created_at, :as => :date, :selected => 1.day.ago
926
+ # f.input :created_at, :as => :date, :selected => nil # override any defaults: select none
927
+ # f.input :created_at, :as => :date, :labels => { :year => "Year", :month => "Month", :day => "Day" }
928
+ #
929
+ # Some of Rails' options for select_date are supported, but not everything yet, see
930
+ # documentation of date_or_datetime_input() for more information.
931
+ def date_input(method, options)
932
+ options = set_include_blank(options)
933
+ date_or_datetime_input(method, options.merge(:discard_hour => true))
934
+ end
935
+
936
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
937
+ # items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
938
+ # contains a label (eg "Year") and a select box. Overwriting the label is possible by adding
939
+ # the :labels option. :labels should be a hash with the field (e.g. day) as key and the label
940
+ # text as value. See date_or_datetime_input for a more detailed output example.
941
+ #
942
+ # You can pre-select a specific option value by passing in the :selected option.
943
+ #
944
+ # Examples:
945
+ #
946
+ # f.input :created_at, :as => :datetime, :selected => 1.day.ago
947
+ # f.input :created_at, :as => :datetime, :selected => nil # override any defaults: select none
948
+ # f.input :created_at, :as => :date, :labels => { :year => "Year", :month => "Month", :day => "Day",
949
+ # :hour => "Hour", :minute => "Minute" }
950
+ #
951
+ # Some of Rails' options for select_date are supported, but not everything yet, see
952
+ # documentation of date_or_datetime_input() for more information.
953
+ def datetime_input(method, options)
954
+ options = set_include_blank(options)
955
+ date_or_datetime_input(method, options)
956
+ end
957
+
958
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
959
+ # items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
960
+ # (eg "Hour") and a select box. Overwriting the label is possible by adding the :labels option.
961
+ # :labels should be a hash with the field (e.g. day) as key and the label text as value.
962
+ # See date_or_datetime_input for a more detailed output example.
963
+ #
964
+ # You can pre-select a specific option value by passing in the :selected option.
965
+ #
966
+ # Examples:
967
+ #
968
+ # f.input :created_at, :as => :time, :selected => 1.hour.ago
969
+ # f.input :created_at, :as => :time, :selected => nil # override any defaults: select none
970
+ # f.input :created_at, :as => :date, :labels => { :hour => "Hour", :minute => "Minute" }
971
+ #
972
+ # Some of Rails' options for select_time are supported, but not everything yet, see
973
+ # documentation of date_or_datetime_input() for more information.
974
+ def time_input(method, options)
975
+ options = set_include_blank(options)
976
+ date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
977
+ end
978
+
979
+ # Helper method used by :as => (:date|:datetime|:time). Generates a fieldset containing a
980
+ # legend (for what would normally be considered the label), and an ordered list of list items
981
+ # for year, month, day, hour, etc, each containing a label and a select. Example:
982
+ #
983
+ # <fieldset>
984
+ # <legend>Created At</legend>
985
+ # <ol>
986
+ # <li>
987
+ # <label for="user_created_at_1i">Year</label>
988
+ # <select id="user_created_at_1i" name="user[created_at(1i)]">
989
+ # <option value="2003">2003</option>
990
+ # ...
991
+ # <option value="2013">2013</option>
992
+ # </select>
993
+ # </li>
994
+ # <li>
995
+ # <label for="user_created_at_2i">Month</label>
996
+ # <select id="user_created_at_2i" name="user[created_at(2i)]">
997
+ # <option value="1">January</option>
998
+ # ...
999
+ # <option value="12">December</option>
1000
+ # </select>
1001
+ # </li>
1002
+ # <li>
1003
+ # <label for="user_created_at_3i">Day</label>
1004
+ # <select id="user_created_at_3i" name="user[created_at(3i)]">
1005
+ # <option value="1">1</option>
1006
+ # ...
1007
+ # <option value="31">31</option>
1008
+ # </select>
1009
+ # </li>
1010
+ # </ol>
1011
+ # </fieldset>
1012
+ #
1013
+ # This is an absolute abomination, but so is the official Rails select_date().
1014
+ #
1015
+ # Options:
1016
+ #
1017
+ # * @:order => [:month, :day, :year]@
1018
+ # * @:include_seconds@ => true@
1019
+ # * @:selected => Time.mktime(2008)@
1020
+ # * @:selected => Date.new(2008)@
1021
+ # * @:selected => nil@
1022
+ # * @:discard_(year|month|day|hour|minute) => true@
1023
+ # * @:include_blank => true@
1024
+ # * @:labels => {}@
1025
+ def date_or_datetime_input(method, options)
1026
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
1027
+ i18n_date_order = ::I18n.t(:order, :scope => [:date])
1028
+ i18n_date_order = nil unless i18n_date_order.is_a?(Array)
1029
+ inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
1030
+ inputs = [] if options[:ignore_date]
1031
+ labels = options.delete(:labels) || {}
1032
+
1033
+ time_inputs = [:hour, :minute]
1034
+ time_inputs << :second if options[:include_seconds]
1035
+
1036
+ list_items_capture = ""
1037
+ hidden_fields_capture = ""
1038
+
1039
+ datetime = options[:selected]
1040
+ datetime = @object.send(method) if @object && @object.send(method) # object value trumps :selected value
1041
+
1042
+ html_options = options.delete(:input_html) || {}
1043
+ input_ids = []
1044
+
1045
+ (inputs + time_inputs).each do |input|
1046
+ input_ids << input_id = generate_html_id(method, "#{position[input]}i")
1047
+
1048
+ field_name = "#{method}(#{position[input]}i)"
1049
+ if options[:"discard_#{input}"]
1050
+ break if time_inputs.include?(input)
1051
+
1052
+ hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
1053
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
1054
+ else
1055
+ opts = strip_formtastic_options(options).merge(:prefix => @object_name, :field_name => field_name, :default => datetime)
1056
+ item_label_text = labels[input] || ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
1057
+
1058
+ list_items_capture << template.content_tag(:li, Formtastic::Util.html_safe([
1059
+ !item_label_text.blank? ? template.content_tag(:label, Formtastic::Util.html_safe(item_label_text), :for => input_id) : "",
1060
+ template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
1061
+ ].join(""))
1062
+ )
1063
+ end
1064
+ end
1065
+
1066
+ hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
1067
+ end
1068
+
1069
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
1070
+ # items, one for each possible choice in the belongs_to association. Each li contains a
1071
+ # label and a check_box input.
1072
+ #
1073
+ # This is an alternative for has many and has and belongs to many associations.
1074
+ #
1075
+ # Example:
1076
+ #
1077
+ # f.input :author, :as => :check_boxes
1078
+ #
1079
+ # Output:
1080
+ #
1081
+ # <fieldset>
1082
+ # <legend class="label"><label>Authors</label></legend>
1083
+ # <ol>
1084
+ # <li>
1085
+ # <input type="hidden" name="book[author_id][1]" value="">
1086
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
1087
+ # </li>
1088
+ # <li>
1089
+ # <input type="hidden" name="book[author_id][2]" value="">
1090
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
1091
+ # </li>
1092
+ # </ol>
1093
+ # </fieldset>
1094
+ #
1095
+ # Notice that the value of the checkbox is the same as the id and the hidden
1096
+ # field has empty value. You can override the hidden field value using the
1097
+ # unchecked_value option.
1098
+ #
1099
+ # You can customize the options available in the set by passing in a collection (Array) of
1100
+ # ActiveRecord objects through the :collection option. If not provided, the choices are found
1101
+ # by inferring the parent's class name from the method name and simply calling find(:all) on
1102
+ # it (Author.find(:all) in the example above).
1103
+ #
1104
+ # Examples:
1105
+ #
1106
+ # f.input :author, :as => :check_boxes, :collection => @authors
1107
+ # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
1108
+ # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
1109
+ #
1110
+ # The :label_method option allows you to customize the label for each checkbox two ways:
1111
+ #
1112
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
1113
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
1114
+ #
1115
+ # Examples:
1116
+ #
1117
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
1118
+ # f.input :author, :as => :check_boxes, :label_method => :login
1119
+ # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
1120
+ # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
1121
+ #
1122
+ # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
1123
+ #
1124
+ # Examples:
1125
+ #
1126
+ # f.input :author, :as => :check_boxes, :value_method => :full_name
1127
+ # f.input :author, :as => :check_boxes, :value_method => :login
1128
+ # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
1129
+ #
1130
+ # You can pre-select/check a specific checkbox value by passing in the :selected option (alias :checked works as well).
1131
+ #
1132
+ # Examples:
1133
+ #
1134
+ # f.input :authors, :as => :check_boxes, :selected => @justin
1135
+ # f.input :authors, :as => :check_boxes, :selected => Author.most_popular.collect(&:id)
1136
+ # f.input :authors, :as => :check_boxes, :selected => nil # override any defaults: select none
1137
+ #
1138
+ #
1139
+ # Formtastic works around a bug in rails handling of check box collections by
1140
+ # not generating the hidden fields for state checking of the checkboxes
1141
+ # The :hidden_fields option provides a way to re-enable these hidden inputs by
1142
+ # setting it to true.
1143
+ #
1144
+ # f.input :authors, :as => :check_boxes, :hidden_fields => false
1145
+ # f.input :authors, :as => :check_boxes, :hidden_fields => true
1146
+ #
1147
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
1148
+ # combination to contain a class with the value of the radio button (useful for applying specific
1149
+ # CSS or Javascript to a particular checkbox).
1150
+ #
1151
+ def check_boxes_input(method, options)
1152
+ collection = find_collection_for_column(method, options)
1153
+ html_options = options.delete(:input_html) || {}
1154
+
1155
+ input_name = generate_association_input_name(method)
1156
+ hidden_fields = options.delete(:hidden_fields)
1157
+ value_as_class = options.delete(:value_as_class)
1158
+ unchecked_value = options.delete(:unchecked_value) || ''
1159
+ html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
1160
+ input_ids = []
1161
+
1162
+ selected_values = find_selected_values_for_column(method, options)
1163
+ disabled_option_is_present = options.key?(:disabled)
1164
+ disabled_values = [*options[:disabled]] if disabled_option_is_present
1165
+
1166
+ li_options = value_as_class ? { :class => [method.to_s.singularize, 'default'].join('_') } : {}
1167
+
1168
+ list_item_content = collection.map do |c|
1169
+ label = c.is_a?(Array) ? c.first : c
1170
+ value = c.is_a?(Array) ? c.last : c
1171
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
1172
+ input_ids << input_id
1173
+
1174
+ html_options[:checked] = selected_values.include?(value)
1175
+ html_options[:disabled] = disabled_values.include?(value) if disabled_option_is_present
1176
+ html_options[:id] = input_id
1177
+
1178
+ li_content = template.content_tag(:label,
1179
+ Formtastic::Util.html_safe("#{self.create_check_boxes(input_name, html_options, value, unchecked_value, hidden_fields)} #{escape_html_entities(label)}"),
1180
+ :for => input_id
1181
+ )
1182
+
1183
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
1184
+ template.content_tag(:li, Formtastic::Util.html_safe(li_content), li_options)
1185
+ end
1186
+
1187
+ fieldset_content = legend_tag(method, options)
1188
+ fieldset_content << self.create_hidden_field_for_check_boxes(input_name, value_as_class) unless hidden_fields
1189
+ fieldset_content << template.content_tag(:ol, Formtastic::Util.html_safe(list_item_content.join))
1190
+ template.content_tag(:fieldset, fieldset_content)
1191
+ end
1192
+
1193
+ # Used by check_boxes input. The selected values will be set either by:
1194
+ #
1195
+ # * Explicitly provided through :selected or :checked
1196
+ # * Values retrieved through an association
1197
+ #
1198
+ # If the collection is not a hash or an array of strings, fixnums or symbols,
1199
+ # we use value_method to retrieve an array with the values
1200
+ #
1201
+ def find_selected_values_for_column(method, options)
1202
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
1203
+ if selected_option_is_present
1204
+ selected_values = (options.key?(:checked) ? options[:checked] : options[:selected])
1205
+ elsif object.respond_to?(method)
1206
+ collection = [object.send(method)].compact.flatten
1207
+ label, value = detect_label_and_value_method!(collection, options)
1208
+ selected_values = collection.map { |o| send_or_call(value, o) }
1209
+ end
1210
+ selected_values = [*selected_values].compact
1211
+ selected_values
1212
+ end
1213
+
1214
+ # Outputs a custom hidden field for check_boxes
1215
+ def create_hidden_field_for_check_boxes(method, value_as_class) #:nodoc:
1216
+ options = value_as_class ? { :class => [method.to_s.singularize, 'default'].join('_') } : {}
1217
+ input_name = "#{object_name}[#{method.to_s}][]"
1218
+ template.hidden_field_tag(input_name, '', options)
1219
+ end
1220
+
1221
+ # Outputs a checkbox tag. If called with no_hidden_input = true a plain check_box_tag is returned,
1222
+ # otherwise the helper uses the output generated by the rails check_box method.
1223
+ def create_check_boxes(input_name, html_options = {}, checked_value = "1", unchecked_value = "0", hidden_fields = false) #:nodoc:
1224
+ return template.check_box_tag(input_name, checked_value, html_options[:checked], html_options) unless hidden_fields == true
1225
+ self.check_box(input_name, html_options, checked_value, unchecked_value)
1226
+ end
1227
+
1228
+ # Outputs a country select input, wrapping around a regular country_select helper.
1229
+ # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1230
+ # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1231
+ # same way.
1232
+ #
1233
+ # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1234
+ #
1235
+ # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1236
+ # which you can change to suit your market and user base (see README for more info on config).
1237
+ #
1238
+ # Examples:
1239
+ # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1240
+ # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1241
+ #
1242
+ def country_input(method, options)
1243
+ raise "To use the :country input, please install a country_select plugin, like this one: http://github.com/rails/iso-3166-country-select" unless self.respond_to?(:country_select)
1244
+
1245
+ html_options = options.delete(:input_html) || {}
1246
+ priority_countries = options.delete(:priority_countries) || self.class.priority_countries
1247
+
1248
+ self.label(method, options_for_label(options)) <<
1249
+ self.country_select(method, priority_countries, strip_formtastic_options(options), html_options)
1250
+ end
1251
+
1252
+ # Outputs a label containing a checkbox and the label text. The label defaults
1253
+ # to the column name (method name) and can be altered with the :label option.
1254
+ # :checked_value and :unchecked_value options are also available.
1255
+ #
1256
+ # You can pre-select/check the boolean checkbox by passing in the :selected option (alias :checked works as well).
1257
+ #
1258
+ # Examples:
1259
+ #
1260
+ # f.input :allow_comments, :as => :boolean, :selected => true # override any default value: selected/checked
1261
+ #
1262
+ def boolean_input(method, options)
1263
+ html_options = options.delete(:input_html) || {}
1264
+ checked = options.key?(:checked) ? options[:checked] : options[:selected]
1265
+ html_options[:checked] = checked == true if [:selected, :checked].any? { |k| options.key?(k) }
1266
+ checked_value = options.delete(:checked_value) || '1'
1267
+ unchecked_value = options.delete(:unchecked_value) || '0'
1268
+
1269
+ input = self.check_box(method, strip_formtastic_options(options).merge(html_options),
1270
+ checked_value, unchecked_value)
1271
+ options = options_for_label(options)
1272
+
1273
+ # the label() method will insert this nested input into the label at the last minute
1274
+ options[:label_prefix_for_nested_input] = input
1275
+
1276
+ self.label(method, options)
1277
+ end
1278
+
1279
+ # Generates an input for the given method using the type supplied with :as.
1280
+ def inline_input_for(method, options)
1281
+ send(:"#{options.delete(:as)}_input", method, options)
1282
+ end
1283
+
1284
+ # Generates hints for the given method using the text supplied in :hint.
1285
+ #
1286
+ def inline_hints_for(method, options) #:nodoc:
1287
+ options[:hint] = localized_string(method, options[:hint], :hint)
1288
+ return if options[:hint].blank? or options[:hint].kind_of? Hash
1289
+ template.content_tag(:p, Formtastic::Util.html_safe(options[:hint]), :class => 'inline-hints')
1290
+ end
1291
+
1292
+ # Creates an error sentence by calling to_sentence on the errors array.
1293
+ #
1294
+ def error_sentence(errors) #:nodoc:
1295
+ template.content_tag(:p, Formtastic::Util.html_safe(errors.to_sentence.untaint), :class => 'inline-errors')
1296
+ end
1297
+
1298
+ # Creates an error li list.
1299
+ #
1300
+ def error_list(errors) #:nodoc:
1301
+ list_elements = []
1302
+ errors.each do |error|
1303
+ list_elements << template.content_tag(:li, Formtastic::Util.html_safe(error.untaint))
1304
+ end
1305
+ template.content_tag(:ul, Formtastic::Util.html_safe(list_elements.join("\n")), :class => 'errors')
1306
+ end
1307
+
1308
+ # Creates an error sentence containing only the first error
1309
+ #
1310
+ def error_first(errors) #:nodoc:
1311
+ template.content_tag(:p, Formtastic::Util.html_safe(errors.first.untaint), :class => 'inline-errors')
1312
+ end
1313
+
1314
+ # Generates the required or optional string. If the value set is a proc,
1315
+ # it evaluates the proc first.
1316
+ #
1317
+ def required_or_optional_string(required) #:nodoc:
1318
+ string_or_proc = case required
1319
+ when true
1320
+ self.class.required_string
1321
+ when false
1322
+ self.class.optional_string
1323
+ else
1324
+ required
1325
+ end
1326
+
1327
+ if string_or_proc.is_a?(Proc)
1328
+ string_or_proc.call
1329
+ else
1330
+ string_or_proc.to_s
1331
+ end
1332
+ end
1333
+
1334
+ # Generates a fieldset and wraps the content in an ordered list. When working
1335
+ # with nested attributes (in Rails 2.3), it allows %i as interpolation option
1336
+ # in :name. So you can do:
1337
+ #
1338
+ # f.inputs :name => 'Task #%i', :for => :tasks
1339
+ #
1340
+ # or the shorter equivalent:
1341
+ #
1342
+ # f.inputs 'Task #%i', :for => :tasks
1343
+ #
1344
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
1345
+ # 'Task #3' and so on.
1346
+ #
1347
+ # Note: Special case for the inline inputs (non-block):
1348
+ # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
1349
+ # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
1350
+ # f.inputs :title, :body, :author # First argument is a column => (no legend)
1351
+ #
1352
+ def field_set_and_list_wrapping(*args, &block) #:nodoc:
1353
+ contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
1354
+ html_options = args.extract_options!
1355
+
1356
+ legend = html_options.delete(:name).to_s
1357
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
1358
+ legend = template.content_tag(:legend, template.content_tag(:span, Formtastic::Util.html_safe(legend))) unless legend.blank?
1359
+
1360
+ if block_given?
1361
+ contents = if template.respond_to?(:is_haml?) && template.is_haml?
1362
+ template.capture_haml(&block)
1363
+ else
1364
+ template.capture(&block)
1365
+ end
1366
+ end
1367
+
1368
+ # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
1369
+ contents = contents.join if contents.respond_to?(:join)
1370
+ fieldset = template.content_tag(:fieldset,
1371
+ Formtastic::Util.html_safe(legend) << template.content_tag(:ol, Formtastic::Util.html_safe(contents)),
1372
+ html_options.except(:builder, :parent)
1373
+ )
1374
+
1375
+ template.concat(fieldset) if block_given? && !Formtastic::Util.rails3?
1376
+ fieldset
1377
+ end
1378
+
1379
+ def field_set_title_from_args(*args) #:nodoc:
1380
+ options = args.extract_options!
1381
+ options[:name] ||= options.delete(:title)
1382
+ title = options[:name]
1383
+
1384
+ if title.blank?
1385
+ valid_name_classes = [::String, ::Symbol]
1386
+ valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
1387
+ title = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
1388
+ end
1389
+ title = localized_string(title, title, :title) if title.is_a?(::Symbol)
1390
+ title
1391
+ end
1392
+
1393
+ # Also generates a fieldset and an ordered list but with label based in
1394
+ # method. This methods is currently used by radio and datetime inputs.
1395
+ #
1396
+ def field_set_and_list_wrapping_for_method(method, options, contents) #:nodoc:
1397
+ contents = contents.join if contents.respond_to?(:join)
1398
+
1399
+ template.content_tag(:fieldset,
1400
+ template.content_tag(:legend,
1401
+ self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
1402
+ ) <<
1403
+ template.content_tag(:ol, Formtastic::Util.html_safe(contents))
1404
+ )
1405
+ end
1406
+
1407
+ # Generates the legend for radiobuttons and checkboxes
1408
+ def legend_tag(method, options = {})
1409
+ (options[:label] == false) ? Formtastic::Util.html_safe("") : template.content_tag(:legend,
1410
+ template.label_tag(nil, localized_string(method, options[:label], :label) || humanized_attribute_name(method), :for => nil), :class => :label
1411
+ )
1412
+ end
1413
+
1414
+ # For methods that have a database column, take a best guess as to what the input method
1415
+ # should be. In most cases, it will just return the column type (eg :string), but for special
1416
+ # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1417
+ # something different (like :password and :select).
1418
+ #
1419
+ # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
1420
+ # default is a :string, a similar behaviour to Rails' scaffolding.
1421
+ #
1422
+ def default_input_type(method, options = {}) #:nodoc:
1423
+ if column = self.column_for(method)
1424
+ # Special cases where the column type doesn't map to an input method.
1425
+ case column.type
1426
+ when :string
1427
+ return :password if method.to_s =~ /password/
1428
+ return :country if method.to_s =~ /country$/
1429
+ return :time_zone if method.to_s =~ /time_zone/
1430
+ when :integer
1431
+ return :select if method.to_s =~ /_id$/
1432
+ return :numeric
1433
+ when :float, :decimal
1434
+ return :numeric
1435
+ when :timestamp
1436
+ return :datetime
1437
+ end
1438
+
1439
+ # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
1440
+ return :select if column.type == :string && options.key?(:collection)
1441
+ # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
1442
+ return column.type
1443
+ else
1444
+ if @object
1445
+ return :select if self.reflection_for(method)
1446
+
1447
+ file = @object.send(method) if @object.respond_to?(method)
1448
+ return :file if file && self.class.file_methods.any? { |m| file.respond_to?(m) }
1449
+ end
1450
+
1451
+ return :select if options.key?(:collection)
1452
+ return :password if method.to_s =~ /password/
1453
+ return :string
1454
+ end
1455
+ end
1456
+
1457
+ # Used by select and radio inputs. The collection can be retrieved by
1458
+ # three ways:
1459
+ #
1460
+ # * Explicitly provided through :collection
1461
+ # * Retrivied through an association
1462
+ # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
1463
+ #
1464
+ # If the collection is not a hash or an array of strings, fixnums or arrays,
1465
+ # we use label_method and value_method to retreive an array with the
1466
+ # appropriate label and value.
1467
+ #
1468
+ def find_collection_for_column(column, options) #:nodoc:
1469
+ collection = find_raw_collection_for_column(column, options)
1470
+
1471
+ # Return if we have an Array of strings, fixnums or arrays
1472
+ return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
1473
+ [Array, Fixnum, String, Symbol].include?(collection.first.class) &&
1474
+ !(options.include?(:label_method) || options.include?(:value_method))
1475
+
1476
+ label, value = detect_label_and_value_method!(collection, options)
1477
+ collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
1478
+ end
1479
+
1480
+ # As #find_collection_for_column but returns the collection without mapping the label and value
1481
+ #
1482
+ def find_raw_collection_for_column(column, options) #:nodoc:
1483
+ collection = if options[:collection]
1484
+ options.delete(:collection)
1485
+ elsif reflection = self.reflection_for(column)
1486
+ options[:find_options] ||= {}
1487
+
1488
+ if conditions = reflection.options[:conditions]
1489
+ if reflection.klass.respond_to?(:merge_conditions)
1490
+ options[:find_options][:conditions] = reflection.klass.merge_conditions(conditions, options[:find_options][:conditions])
1491
+ reflection.klass.all(options[:find_options])
1492
+ else
1493
+ reflection.klass.where(conditions).where(options[:find_options][:conditions])
1494
+ end
1495
+ else
1496
+ reflection.klass.all(options[:find_options])
1497
+ end
1498
+ else
1499
+ create_boolean_collection(options)
1500
+ end
1501
+
1502
+ collection = collection.to_a if collection.is_a?(Hash)
1503
+ collection
1504
+ end
1505
+
1506
+ # Detects the label and value methods from a collection using methods set in
1507
+ # collection_label_methods and collection_value_methods. For some ruby core
1508
+ # classes sensible defaults have been defined. It will use and delete the options
1509
+ # :label_method and :value_methods when present.
1510
+ #
1511
+ def detect_label_and_value_method!(collection, options = {})
1512
+ sample = collection.first || collection.last
1513
+
1514
+ case sample
1515
+ when Array
1516
+ label, value = :first, :last
1517
+ when Integer
1518
+ label, value = :to_s, :to_i
1519
+ when String, NilClass
1520
+ label, value = :to_s, :to_s
1521
+ end
1522
+
1523
+ # Order of preference: user supplied method, class defaults, auto-detect
1524
+ label = options[:label_method] || label || self.class.collection_label_methods.find { |m| sample.respond_to?(m) }
1525
+ value = options[:value_method] || value || self.class.collection_value_methods.find { |m| sample.respond_to?(m) }
1526
+
1527
+ [label, value]
1528
+ end
1529
+
1530
+ # Return the label collection method when none is supplied using the
1531
+ # values set in collection_label_methods.
1532
+ #
1533
+ def detect_label_method(collection) #:nodoc:
1534
+ detect_label_and_value_method!(collection).first
1535
+ end
1536
+
1537
+ # Detects the method to call for fetching group members from the groups when grouping select options
1538
+ #
1539
+ def detect_group_association(method, group_by)
1540
+ object_to_method_reflection = self.reflection_for(method)
1541
+ method_class = object_to_method_reflection.klass
1542
+
1543
+ method_to_group_association = method_class.reflect_on_association(group_by)
1544
+ group_class = method_to_group_association.klass
1545
+
1546
+ # This will return in the normal case
1547
+ return method.to_s.pluralize.to_sym if group_class.reflect_on_association(method.to_s.pluralize)
1548
+
1549
+ # This is for belongs_to associations named differently than their class
1550
+ # form.input :parent, :group_by => :customer
1551
+ # eg.
1552
+ # class Project
1553
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1554
+ # belongs_to :customer
1555
+ # end
1556
+ # class Customer
1557
+ # has_many :projects
1558
+ # end
1559
+ group_method = method_class.to_s.underscore.pluralize.to_sym
1560
+ return group_method if group_class.reflect_on_association(group_method) # :projects
1561
+
1562
+ # This is for has_many associations named differently than their class
1563
+ # eg.
1564
+ # class Project
1565
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1566
+ # belongs_to :customer
1567
+ # end
1568
+ # class Customer
1569
+ # has_many :tasks, :class_name => 'Project', :foreign_key => 'customer_id'
1570
+ # end
1571
+ possible_associations = group_class.reflect_on_all_associations(:has_many).find_all{|assoc| assoc.klass == object_class}
1572
+ return possible_associations.first.name.to_sym if possible_associations.count == 1
1573
+
1574
+ raise "Cannot infer group association for #{method} grouped by #{group_by}, there were #{possible_associations.empty? ? 'no' : possible_associations.size} possible associations. Please specify using :group_association"
1575
+
1576
+ end
1577
+
1578
+ # Returns a hash to be used by radio and select inputs when a boolean field
1579
+ # is provided.
1580
+ #
1581
+ def create_boolean_collection(options) #:nodoc:
1582
+ options[:true] ||= ::Formtastic::I18n.t(:yes)
1583
+ options[:false] ||= ::Formtastic::I18n.t(:no)
1584
+ options[:value_as_class] = true unless options.key?(:value_as_class)
1585
+
1586
+ [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1587
+ end
1588
+
1589
+ # Used by association inputs (select, radio) to generate the name that should
1590
+ # be used for the input
1591
+ #
1592
+ # belongs_to :author; f.input :author; will generate 'author_id'
1593
+ # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1594
+ # has_many :authors; f.input :authors; will generate 'author_ids'
1595
+ # has_and_belongs_to_many will act like has_many
1596
+ #
1597
+ def generate_association_input_name(method) #:nodoc:
1598
+ if reflection = self.reflection_for(method)
1599
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1600
+ "#{method.to_s.singularize}_ids"
1601
+ else
1602
+ reflection.options[:foreign_key] || "#{method}_id"
1603
+ end
1604
+ else
1605
+ method
1606
+ end.to_sym
1607
+ end
1608
+
1609
+ # If an association method is passed in (f.input :author) try to find the
1610
+ # reflection object.
1611
+ #
1612
+ def reflection_for(method) #:nodoc:
1613
+ @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1614
+ end
1615
+
1616
+ # Get a column object for a specified attribute method - if possible.
1617
+ #
1618
+ def column_for(method) #:nodoc:
1619
+ @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1620
+ end
1621
+
1622
+ # Generates default_string_options by retrieving column information from
1623
+ # the database.
1624
+ #
1625
+ def default_string_options(method, type) #:nodoc:
1626
+ column = self.column_for(method)
1627
+
1628
+ if type == :text
1629
+ { :cols => self.class.default_text_field_size, :rows => self.class.default_text_area_height }
1630
+ elsif type == :numeric || column.nil? || column.limit.nil?
1631
+ { :size => self.class.default_text_field_size }
1632
+ else
1633
+ { :maxlength => column.limit,
1634
+ :size => self.class.default_text_field_size && [column.limit, self.class.default_text_field_size].min }
1635
+ end
1636
+ end
1637
+
1638
+ # Generate the html id for the li tag.
1639
+ # It takes into account options[:index] and @auto_index to generate li
1640
+ # elements with appropriate index scope. It also sanitizes the object
1641
+ # and method names.
1642
+ #
1643
+ def generate_html_id(method_name, value='input') #:nodoc:
1644
+ if options.has_key?(:index)
1645
+ index = "_#{options[:index]}"
1646
+ elsif defined?(@auto_index)
1647
+ index = "_#{@auto_index}"
1648
+ else
1649
+ index = ""
1650
+ end
1651
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1652
+
1653
+ "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1654
+ end
1655
+
1656
+ # Gets the nested_child_index value from the parent builder. In Rails 2.3
1657
+ # it always returns a fixnum. In next versions it returns a hash with each
1658
+ # association that the parent builds.
1659
+ #
1660
+ def parent_child_index(parent) #:nodoc:
1661
+ duck = parent[:builder].instance_variable_get('@nested_child_index')
1662
+
1663
+ if duck.is_a?(Hash)
1664
+ child = parent[:for]
1665
+ child = child.first if child.respond_to?(:first)
1666
+ duck[child].to_i + 1
1667
+ else
1668
+ duck.to_i + 1
1669
+ end
1670
+ end
1671
+
1672
+ def sanitized_object_name #:nodoc:
1673
+ @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1674
+ end
1675
+
1676
+ def humanized_attribute_name(method) #:nodoc:
1677
+ if @object && @object.class.respond_to?(:human_attribute_name)
1678
+ humanized_name = @object.class.human_attribute_name(method.to_s)
1679
+ if humanized_name == method.to_s.send(:humanize)
1680
+ method.to_s.send(self.class.label_str_method)
1681
+ else
1682
+ humanized_name
1683
+ end
1684
+ else
1685
+ method.to_s.send(self.class.label_str_method)
1686
+ end
1687
+ end
1688
+
1689
+ # Internal generic method for looking up localized values within Formtastic
1690
+ # using I18n, if no explicit value is set and I18n-lookups are enabled.
1691
+ #
1692
+ # Enabled/Disable this by setting:
1693
+ #
1694
+ # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1695
+ #
1696
+ # Lookup priority:
1697
+ #
1698
+ # 'formtastic.%{type}.%{model}.%{action}.%{attribute}'
1699
+ # 'formtastic.%{type}.%{model}.%{attribute}'
1700
+ # 'formtastic.%{type}.%{attribute}'
1701
+ #
1702
+ # Example:
1703
+ #
1704
+ # 'formtastic.labels.post.edit.title'
1705
+ # 'formtastic.labels.post.title'
1706
+ # 'formtastic.labels.title'
1707
+ #
1708
+ # NOTE: Generic, but only used for form input titles/labels/hints/actions (titles = legends, actions = buttons).
1709
+ #
1710
+ def localized_string(key, value, type, options = {}) #:nodoc:
1711
+ key = value if value.is_a?(::Symbol)
1712
+
1713
+ if value.is_a?(::String)
1714
+ escape_html_entities(value)
1715
+ else
1716
+ use_i18n = value.nil? ? self.class.i18n_lookups_by_default : (value != false)
1717
+
1718
+ if use_i18n
1719
+ model_name, nested_model_name = normalize_model_name(self.model_name.underscore)
1720
+ action_name = template.params[:action].to_s rescue ''
1721
+ attribute_name = key.to_s
1722
+
1723
+ defaults = ::Formtastic::I18n::SCOPES.collect do |i18n_scope|
1724
+ i18n_path = i18n_scope.dup
1725
+ i18n_path.gsub!('%{action}', action_name)
1726
+ i18n_path.gsub!('%{model}', model_name)
1727
+ i18n_path.gsub!('%{nested_model}', nested_model_name) unless nested_model_name.nil?
1728
+ i18n_path.gsub!('%{attribute}', attribute_name)
1729
+ i18n_path.gsub!('..', '.')
1730
+ i18n_path.to_sym
1731
+ end
1732
+ defaults << ''
1733
+
1734
+ i18n_value = ::Formtastic::I18n.t(defaults.shift,
1735
+ options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
1736
+ i18n_value = escape_html_entities(i18n_value) if i18n_value.is_a?(::String)
1737
+ i18n_value.blank? ? nil : i18n_value
1738
+ end
1739
+ end
1740
+ end
1741
+
1742
+ def model_name
1743
+ @object.present? ? @object.class.name : @object_name.to_s.classify
1744
+ end
1745
+
1746
+ def normalize_model_name(name)
1747
+ if name =~ /(.+)\[(.+)\]/
1748
+ [$1, $2]
1749
+ else
1750
+ [name]
1751
+ end
1752
+ end
1753
+
1754
+ def send_or_call(duck, object)
1755
+ if duck.is_a?(Proc)
1756
+ duck.call(object)
1757
+ else
1758
+ object.send(duck)
1759
+ end
1760
+ end
1761
+
1762
+ def set_include_blank(options)
1763
+ unless options.key?(:include_blank) || options.key?(:prompt)
1764
+ options[:include_blank] = self.class.include_blank_for_select_by_default
1765
+ end
1766
+ options
1767
+ end
1768
+
1769
+ def escape_html_entities(string) #:nodoc:
1770
+ if self.class.escape_html_entities_in_hints_and_labels
1771
+ # Acceppt html_safe flag as indicator to skip escaping
1772
+ string = template.escape_once(string) unless string.respond_to?(:html_safe?) && string.html_safe? == true
1773
+ end
1774
+ string
1775
+ end
1776
+
1777
+ end
1778
+
1779
+ # Wrappers around form_for (etc) with :builder => SemanticFormBuilder.
1780
+ #
1781
+ # * semantic_form_for(@post)
1782
+ # * semantic_fields_for(@post)
1783
+ # * semantic_form_remote_for(@post)
1784
+ # * semantic_remote_form_for(@post)
1785
+ #
1786
+ # Each of which are the equivalent of:
1787
+ #
1788
+ # * form_for(@post, :builder => Formtastic::SemanticFormBuilder))
1789
+ # * fields_for(@post, :builder => Formtastic::SemanticFormBuilder))
1790
+ # * form_remote_for(@post, :builder => Formtastic::SemanticFormBuilder))
1791
+ # * remote_form_for(@post, :builder => Formtastic::SemanticFormBuilder))
1792
+ #
1793
+ # Example Usage:
1794
+ #
1795
+ # <% semantic_form_for @post do |f| %>
1796
+ # <%= f.input :title %>
1797
+ # <%= f.input :body %>
1798
+ # <% end %>
1799
+ #
1800
+ # The above examples use a resource-oriented style of form_for() helper where only the @post
1801
+ # object is given as an argument, but the generic style is also supported, as are forms with
1802
+ # inline objects (Post.new) rather than objects with instance variables (@post):
1803
+ #
1804
+ # <% semantic_form_for :post, @post, :url => posts_path do |f| %>
1805
+ # ...
1806
+ # <% end %>
1807
+ #
1808
+ # <% semantic_form_for :post, Post.new, :url => posts_path do |f| %>
1809
+ # ...
1810
+ # <% end %>
1811
+ module SemanticFormHelper
1812
+ @@builder = ::Formtastic::SemanticFormBuilder
1813
+ mattr_accessor :builder
1814
+
1815
+ # Override the default ActiveRecordHelper behaviour of wrapping the input.
1816
+ # This gets taken care of semantically by adding an error class to the LI tag
1817
+ # containing the input.
1818
+ #
1819
+ FIELD_ERROR_PROC = proc do |html_tag, instance_tag|
1820
+ html_tag
1821
+ end
1822
+
1823
+ def with_custom_field_error_proc(&block)
1824
+ default_field_error_proc = ::ActionView::Base.field_error_proc
1825
+ ::ActionView::Base.field_error_proc = FIELD_ERROR_PROC
1826
+ yield
1827
+ ensure
1828
+ ::ActionView::Base.field_error_proc = default_field_error_proc
1829
+ end
1830
+
1831
+ def semantic_remote_form_for_wrapper(record_or_name_or_array, *args, &proc)
1832
+ options = args.extract_options!
1833
+ if self.respond_to? :remote_form_for
1834
+ semantic_remote_form_for_real(record_or_name_or_array, *(args << options), &proc)
1835
+ else
1836
+ options[:remote] = true
1837
+ semantic_form_for(record_or_name_or_array, *(args << options), &proc)
1838
+ end
1839
+ end
1840
+
1841
+ [:form_for, :fields_for, :remote_form_for].each do |meth|
1842
+ module_eval <<-END_SRC, __FILE__, __LINE__ + 1
1843
+ def semantic_#{meth}(record_or_name_or_array, *args, &proc)
1844
+ options = args.extract_options!
1845
+ options[:builder] ||= @@builder
1846
+ options[:html] ||= {}
1847
+
1848
+ singularizer = defined?(ActiveModel::Naming.singular) ? ActiveModel::Naming.method(:singular) : ActionController::RecordIdentifier.method(:singular_class_name)
1849
+
1850
+ class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
1851
+ class_names << "formtastic"
1852
+ class_names << case record_or_name_or_array
1853
+ when String, Symbol then record_or_name_or_array.to_s # :post => "post"
1854
+ when Array then singularizer.call(record_or_name_or_array.last.class) # [@post, @comment] # => "comment"
1855
+ else singularizer.call(record_or_name_or_array.class) # @post => "post"
1856
+ end
1857
+ options[:html][:class] = class_names.join(" ")
1858
+
1859
+ with_custom_field_error_proc do
1860
+ #{meth}(record_or_name_or_array, *(args << options), &proc)
1861
+ end
1862
+ end
1863
+ END_SRC
1864
+ end
1865
+ alias :semantic_remote_form_for_real :semantic_remote_form_for
1866
+ alias :semantic_remote_form_for :semantic_remote_form_for_wrapper
1867
+ alias :semantic_form_remote_for :semantic_remote_form_for
1868
+
1869
+ end
1870
+ end