document_form 0.1.0

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