formtastic 3.1.3 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/.travis.yml +12 -10
  2. data/Appraisals +11 -7
  3. data/CHANGELOG +12 -0
  4. data/DEPRECATIONS +3 -0
  5. data/{README.textile → README.md} +629 -616
  6. data/formtastic.gemspec +3 -3
  7. data/gemfiles/rails_3.2.gemfile +1 -0
  8. data/gemfiles/rails_edge.gemfile +5 -0
  9. data/lib/formtastic.rb +4 -0
  10. data/lib/formtastic/form_builder.rb +3 -0
  11. data/lib/formtastic/helpers.rb +1 -1
  12. data/lib/formtastic/helpers/enum.rb +13 -0
  13. data/lib/formtastic/helpers/fieldset_wrapper.rb +6 -6
  14. data/lib/formtastic/helpers/form_helper.rb +1 -1
  15. data/lib/formtastic/helpers/input_helper.rb +5 -1
  16. data/lib/formtastic/helpers/inputs_helper.rb +16 -20
  17. data/lib/formtastic/inputs/base/choices.rb +1 -1
  18. data/lib/formtastic/inputs/base/collections.rb +41 -4
  19. data/lib/formtastic/inputs/base/html.rb +7 -6
  20. data/lib/formtastic/inputs/base/naming.rb +4 -4
  21. data/lib/formtastic/inputs/base/options.rb +2 -3
  22. data/lib/formtastic/inputs/base/validations.rb +19 -3
  23. data/lib/formtastic/inputs/check_boxes_input.rb +10 -2
  24. data/lib/formtastic/inputs/country_input.rb +3 -1
  25. data/lib/formtastic/inputs/radio_input.rb +20 -0
  26. data/lib/formtastic/inputs/select_input.rb +28 -0
  27. data/lib/formtastic/inputs/time_zone_input.rb +16 -6
  28. data/lib/formtastic/localizer.rb +15 -15
  29. data/lib/formtastic/namespaced_class_finder.rb +1 -1
  30. data/lib/formtastic/version.rb +1 -1
  31. data/lib/generators/formtastic/form/form_generator.rb +1 -1
  32. data/lib/generators/formtastic/input/input_generator.rb +46 -0
  33. data/lib/generators/templates/formtastic.rb +10 -7
  34. data/lib/generators/templates/input.rb +19 -0
  35. data/spec/fast_spec_helper.rb +12 -0
  36. data/spec/generators/formtastic/input/input_generator_spec.rb +124 -0
  37. data/spec/helpers/form_helper_spec.rb +4 -4
  38. data/spec/inputs/base/collections_spec.rb +76 -0
  39. data/spec/inputs/base/validations_spec.rb +342 -0
  40. data/spec/inputs/check_boxes_input_spec.rb +66 -20
  41. data/spec/inputs/country_input_spec.rb +4 -4
  42. data/spec/inputs/radio_input_spec.rb +28 -0
  43. data/spec/inputs/readonly_spec.rb +50 -0
  44. data/spec/inputs/select_input_spec.rb +71 -11
  45. data/spec/inputs/time_zone_input_spec.rb +35 -9
  46. data/spec/spec_helper.rb +2 -30
  47. data/spec/support/shared_examples.rb +69 -0
  48. metadata +23 -12
  49. data/spec/support/deferred_garbage_collection.rb +0 -21
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  s.rdoc_options = ["--charset=UTF-8"]
22
- s.extra_rdoc_files = ["README.textile"]
22
+ s.extra_rdoc_files = ["README.md"]
23
23
 
24
24
  s.required_ruby_version = '>= 1.9.3'
25
25
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
@@ -28,14 +28,14 @@ Gem::Specification.new do |s|
28
28
  s.add_dependency(%q<actionpack>, [">= 3.2.13"])
29
29
 
30
30
  s.add_development_dependency(%q<nokogiri>)
31
- s.add_development_dependency(%q<rspec-rails>, ["~> 2.14"])
31
+ s.add_development_dependency(%q<rspec-rails>, ["~> 3.3.2"])
32
32
  s.add_development_dependency(%q<rspec_tag_matchers>, ["~> 1.0"])
33
33
  s.add_development_dependency(%q<hpricot>, ["~> 0.8.3"])
34
34
  s.add_development_dependency(%q<RedCloth>, ["~> 4.2"]) # for YARD Textile formatting
35
35
  s.add_development_dependency(%q<yard>, ["~> 0.8"])
36
36
  s.add_development_dependency(%q<colored>, ["~> 1.2"])
37
37
  s.add_development_dependency(%q<tzinfo>)
38
- s.add_development_dependency(%q<ammeter>, ["1.1.1"])
38
+ s.add_development_dependency(%q<ammeter>, ["~> 1.1.3"])
39
39
  s.add_development_dependency(%q<appraisal>, ["~> 1.0"])
40
40
  s.add_development_dependency(%q<rake>)
41
41
  s.add_development_dependency(%q<activemodel>, [">= 3.2.13"])
@@ -3,5 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 3.2.0"
6
+ gem "test-unit-minitest", :platform => [:ruby_22, :ruby_23]
6
7
 
7
8
  gemspec :path => "../"
@@ -6,5 +6,10 @@ gem "rails", :git => "git://github.com/rails/rails.git"
6
6
  gem "rack", :github => "rack/rack"
7
7
  gem "i18n", :github => "svenfuchs/i18n"
8
8
  gem "arel", :github => "rails/arel"
9
+ gem "rspec-rails", :github => "rspec/rspec-rails"
10
+ gem "rspec-mocks", :github => "rspec/rspec-mocks"
11
+ gem "rspec-support", :github => "rspec/rspec-support"
12
+ gem "rspec-core", :github => "rspec/rspec-core"
13
+ gem "rspec-expectations", :github => "rspec/rspec-expectations"
9
14
 
10
15
  gemspec :path => "../"
@@ -42,4 +42,8 @@ module Formtastic
42
42
  class UnsupportedMethodForAction < ArgumentError
43
43
  end
44
44
 
45
+ # @private
46
+ class UnsupportedEnumCollection < NameError
47
+ end
48
+
45
49
  end
@@ -51,6 +51,9 @@ module Formtastic
51
51
  # Will be {Formtastic::ActionClassFinder} by default in 4.0.
52
52
  configure :action_class_finder#, Formtastic::ActionClassFinder
53
53
 
54
+ configure :skipped_columns, [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
55
+ configure :priority_time_zones, []
56
+
54
57
  attr_reader :template
55
58
 
56
59
  attr_reader :auto_index
@@ -9,8 +9,8 @@ module Formtastic
9
9
  autoload :FormHelper, 'formtastic/helpers/form_helper'
10
10
  autoload :InputHelper, 'formtastic/helpers/input_helper'
11
11
  autoload :InputsHelper, 'formtastic/helpers/inputs_helper'
12
- autoload :LabelHelper, 'formtastic/helpers/label_helper'
13
12
  autoload :Reflection, 'formtastic/helpers/reflection'
13
+ autoload :Enum, 'formtastic/helpers/enum'
14
14
  end
15
15
  end
16
16
 
@@ -0,0 +1,13 @@
1
+ module Formtastic
2
+ module Helpers
3
+ # @private
4
+ module Enum
5
+ # Returns the enum (if defined) for the given method
6
+ def enum_for(method) # @private
7
+ if @object.respond_to?(:defined_enums)
8
+ @object.defined_enums[method.to_s]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -22,7 +22,7 @@ module Formtastic
22
22
  # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
23
23
  # f.inputs :title, :body, :author # First argument is a column => (no legend)
24
24
  def field_set_and_list_wrapping(*args, &block) # @private
25
- contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
25
+ contents = args[-1].is_a?(::Hash) ? '' : args.pop.flatten
26
26
  html_options = args.extract_options!
27
27
 
28
28
  if block_given?
@@ -47,7 +47,7 @@ module Formtastic
47
47
 
48
48
  def field_set_legend(html_options)
49
49
  legend = (html_options[:name] || '').to_s
50
- legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
50
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent] && legend.include?('%i') # only applying if String includes '%i' avoids argument error when $DEBUG is true
51
51
  legend = template.content_tag(:legend, template.content_tag(:span, Formtastic::Util.html_safe(legend))) unless legend.blank?
52
52
  legend
53
53
  end
@@ -57,20 +57,20 @@ module Formtastic
57
57
  def parent_child_index(parent) # @private
58
58
  # Could be {"post[authors_attributes]"=>0} or { :authors => 0 }
59
59
  duck = parent[:builder].instance_variable_get('@nested_child_index')
60
-
60
+
61
61
  # Could be symbol for the association, or a model (or an array of either, I think? TODO)
62
62
  child = parent[:for]
63
63
  # Pull a sybol or model out of Array (TODO: check if there's an Array)
64
64
  child = child.first if child.respond_to?(:first)
65
65
  # If it's an object, get a symbol from the class name
66
66
  child = child.class.name.underscore.to_sym unless child.is_a?(Symbol)
67
-
67
+
68
68
  key = "#{parent[:builder].object_name}[#{child}_attributes]"
69
69
 
70
- # TODO: One of the tests produces a scenario where duck is "0" and the test looks for a "1"
70
+ # TODO: One of the tests produces a scenario where duck is "0" and the test looks for a "1"
71
71
  # in the legend, so if we have a number, return it with a +1 until we can verify this scenario.
72
72
  return duck + 1 if duck.is_a?(Fixnum)
73
-
73
+
74
74
  # First try to extract key from duck Hash, then try child
75
75
  (duck[key] || duck[child]).to_i + 1
76
76
  end
@@ -163,7 +163,7 @@ module Formtastic
163
163
  class_names << @@default_form_class
164
164
  model_class_name = case record_or_name_or_array
165
165
  when String, Symbol then record_or_name_or_array.to_s # :post => "post"
166
- when Array then options[:as] || singularizer.call(record_or_name_or_array.last.class) # [@post, @comment] # => "comment"
166
+ when Array then options[:as] || singularizer.call(record_or_name_or_array[-1].class) # [@post, @comment] # => "comment"
167
167
  else options[:as] || singularizer.call(record_or_name_or_array.class) # @post => "post"
168
168
  end
169
169
  class_names << @@default_form_model_class_proc.call(model_class_name)
@@ -40,6 +40,7 @@ module Formtastic
40
40
  private_constant(:INPUT_CLASS_DEPRECATION)
41
41
 
42
42
  include Formtastic::Helpers::Reflection
43
+ include Formtastic::Helpers::Enum
43
44
  include Formtastic::Helpers::FileColumnDetection
44
45
 
45
46
  # Returns a chunk of HTML markup for a given `method` on the form object, wrapped in
@@ -134,6 +135,7 @@ module Formtastic
134
135
  #
135
136
  # @option options :input_html [Hash]
136
137
  # Override or add to the HTML attributes to be passed down to the `<input>` tag
138
+ # (If you use attr_readonly method in your model, formtastic will automatically set those attributes's input readonly)
137
139
  #
138
140
  # @option options :wrapper_html [Hash]
139
141
  # Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
@@ -259,7 +261,8 @@ module Formtastic
259
261
  return :file if is_file?(method, options)
260
262
  end
261
263
 
262
- if column = column_for(method)
264
+ column = column_for(method)
265
+ if column && column.type
263
266
  # Special cases where the column type doesn't map to an input method.
264
267
  case column.type
265
268
  when :string
@@ -273,6 +276,7 @@ module Formtastic
273
276
  return :color if method.to_s =~ /color/
274
277
  when :integer
275
278
  return :select if reflection_for(method)
279
+ return :select if enum_for(method)
276
280
  return :number
277
281
  when :float, :decimal
278
282
  return :number
@@ -47,10 +47,6 @@ module Formtastic
47
47
  include Formtastic::Helpers::FieldsetWrapper
48
48
  include Formtastic::LocalizedString
49
49
 
50
- # Which columns to skip when automatically rendering a form without any fields specified.
51
- SKIPPED_COLUMNS = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
52
-
53
-
54
50
  # {#inputs} creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
55
51
  # called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
56
52
  # or with a list of fields (accepting all default arguments and options). These two examples
@@ -136,7 +132,7 @@ module Formtastic
136
132
  # <% end %>
137
133
  # <% end %>
138
134
  #
139
- # {#inputs} also provides a DSL similar to `fields_for` / `semantic_fields_for` to reduce the
135
+ # {#inputs} also provides a DSL similar to `fields_for` / `semantic_fields_for` to reduce the
140
136
  # lines of code a little:
141
137
  #
142
138
  # <% semantic_form_for @user do |f| %>
@@ -162,7 +158,7 @@ module Formtastic
162
158
  # All options except `:name`, `:title` and `:for` will be passed down to the fieldset as HTML
163
159
  # attributes (id, class, style, etc).
164
160
  #
165
- # When nesting `inputs()` inside another `inputs()` block, the nested content will
161
+ # When nesting `inputs()` inside another `inputs()` block, the nested content will
166
162
  # automatically be wrapped in an `<li>` tag to preserve the HTML validity (a `<fieldset>`
167
163
  # cannot be a direct descendant of an `<ol>`.
168
164
  #
@@ -183,12 +179,12 @@ module Formtastic
183
179
  # <% end %>
184
180
  #
185
181
  # @example Quick form: Skip one or more fields
186
- # <%= f.inputs, :except => [:featured, :something_for_admin_only] %>
187
- # <%= f.inputs, :except => :featured %>
182
+ # <%= f.inputs :except => [:featured, :something_for_admin_only] %>
183
+ # <%= f.inputs :except => :featured %>
188
184
  #
189
185
  # @example Short hand: Render inputs for a named set of attributes and simple associations on the model, with all default arguments and options
190
186
  # <% semantic_form_for @post do |form| %>
191
- # <%= f.inputs, :title, :body, :user, :categories %>
187
+ # <%= f.inputs :title, :body, :user, :categories %>
192
188
  # <% end %>
193
189
  #
194
190
  # @example Block: Render inputs for attributes and simple associations with full control over arguments and options
@@ -283,7 +279,7 @@ module Formtastic
283
279
  def inputs(*args, &block)
284
280
  wrap_it = @already_in_an_inputs_block ? true : false
285
281
  @already_in_an_inputs_block = true
286
-
282
+
287
283
  title = field_set_title_from_args(*args)
288
284
  html_options = args.extract_options!
289
285
  html_options[:class] ||= "inputs"
@@ -303,21 +299,21 @@ module Formtastic
303
299
  field_set_and_list_wrapping(*((args << html_options) << contents))
304
300
  end
305
301
  end
306
-
302
+
307
303
  out = template.content_tag(:li, out, :class => "input") if wrap_it
308
304
  @already_in_an_inputs_block = wrap_it
309
305
  out
310
306
  end
311
307
 
312
308
  protected
313
-
309
+
314
310
  def default_columns_for_object
315
311
  cols = association_columns(:belongs_to)
316
312
  cols += content_columns
317
- cols -= SKIPPED_COLUMNS
313
+ cols -= Formtastic::FormBuilder.skipped_columns
318
314
  cols.compact
319
315
  end
320
-
316
+
321
317
  def fieldset_contents_from_column_list(columns)
322
318
  columns.collect do |method|
323
319
  if @object
@@ -328,13 +324,13 @@ module Formtastic
328
324
  elsif @object.class.respond_to?(:associations)
329
325
  if (@object.class.associations[method.to_sym] && @object.class.associations[method.to_sym].options[:polymorphic] == true)
330
326
  raise PolymorphicInputWithoutCollectionError.new("Please provide a collection for :#{method} input (you'll need to use block form syntax). Inputs for polymorphic associations can only be used when an explicit :collection is provided.")
331
- end
332
- end
327
+ end
328
+ end
333
329
  end
334
330
  input(method.to_sym)
335
331
  end
336
332
  end
337
-
333
+
338
334
  # Collects association columns (relation columns) for the current form object class. Skips
339
335
  # polymorphic associations because we can't guess which class to use for an automatically
340
336
  # generated input.
@@ -343,7 +339,7 @@ module Formtastic
343
339
  @object.class.reflections.collect do |name, association_reflection|
344
340
  if by_associations.present?
345
341
  if by_associations.include?(association_reflection.macro) && association_reflection.options[:polymorphic] != true
346
- name
342
+ name
347
343
  end
348
344
  else
349
345
  name
@@ -377,10 +373,10 @@ module Formtastic
377
373
  lambda do |f|
378
374
  contents = f.inputs(*args) do
379
375
  if block.arity == 1 # for backwards compatibility with REE & Ruby 1.8.x
380
- block.call(f)
376
+ yield(f)
381
377
  else
382
378
  index = parent_child_index(options[:parent]) if options[:parent]
383
- block.call(f, index)
379
+ yield(f, index)
384
380
  end
385
381
  end
386
382
  template.concat(contents)
@@ -64,7 +64,7 @@ module Formtastic
64
64
  end
65
65
 
66
66
  def custom_choice_html_options(choice)
67
- (choice.is_a?(Array) && choice.size > 2) ? choice.last : {}
67
+ (choice.is_a?(Array) && choice.size > 2) ? choice[-1] : {}
68
68
  end
69
69
 
70
70
  def choice_html_safe_value(choice)
@@ -12,7 +12,7 @@ module Formtastic
12
12
  end
13
13
 
14
14
  def value_method
15
- @value_method ||= (value_method_from_options || label_and_value_method.last)
15
+ @value_method ||= (value_method_from_options || label_and_value_method[-1])
16
16
  end
17
17
 
18
18
  def value_method_from_options
@@ -24,7 +24,7 @@ module Formtastic
24
24
  end
25
25
 
26
26
  def label_and_value_method_from_collection(_collection)
27
- sample = _collection.first || _collection.last
27
+ sample = _collection.first || _collection[-1]
28
28
 
29
29
  case sample
30
30
  when Array
@@ -43,7 +43,7 @@ module Formtastic
43
43
  end
44
44
 
45
45
  def raw_collection
46
- @raw_collection ||= (collection_from_options || collection_from_association || collection_for_boolean)
46
+ @raw_collection ||= (collection_from_options || collection_from_enum || collection_from_association || collection_for_boolean)
47
47
  end
48
48
 
49
49
  def collection
@@ -83,7 +83,7 @@ module Formtastic
83
83
 
84
84
  scope_conditions = conditions_from_reflection.empty? ? nil : {:conditions => conditions_from_reflection}
85
85
  where_conditions = (scope_conditions && scope_conditions[:conditions]) || {}
86
-
86
+
87
87
  if Util.rails3?
88
88
  reflection.klass.scoped(scope_conditions).where({}) # where is uneccessary, but keeps the stubbing simpler while we support rails3
89
89
  else
@@ -92,6 +92,43 @@ module Formtastic
92
92
  end
93
93
  end
94
94
 
95
+ # Assuming the following model:
96
+ #
97
+ # class Post < ActiveRecord::Base
98
+ # enum :status => [ :active, :archived ]
99
+ # end
100
+ #
101
+ # We would end up with a collection like this:
102
+ #
103
+ # [["Active", "active"], ["Archived", "archived"]
104
+ #
105
+ # The first element in each array uses String#humanize, but I18n
106
+ # translations are available too. Set them with the following structure.
107
+ #
108
+ # en:
109
+ # activerecord:
110
+ # attributes:
111
+ # post:
112
+ # statuses:
113
+ # active: Custom Active Label Here
114
+ # archived: Custom Archived Label Here
115
+ def collection_from_enum
116
+ if collection_from_enum?
117
+ method_name = method.to_s
118
+
119
+ enum_options_hash = object.defined_enums[method_name]
120
+ enum_options_hash.map do |name, value|
121
+ key = "activerecord.attributes.#{object_name}.#{method_name.pluralize}.#{name}"
122
+ label = ::I18n.translate(key, :default => name.humanize)
123
+ [label, name]
124
+ end
125
+ end
126
+ end
127
+
128
+ def collection_from_enum?
129
+ object.respond_to?(:defined_enums) && object.defined_enums.has_key?(method.to_s)
130
+ end
131
+
95
132
  def collection_for_boolean
96
133
  true_text = options[:true] || Formtastic::I18n.t(:yes)
97
134
  false_text = options[:false] || Formtastic::I18n.t(:no)
@@ -2,7 +2,7 @@ module Formtastic
2
2
  module Inputs
3
3
  module Base
4
4
  module Html
5
-
5
+
6
6
  # Defines how the instance of an input should be rendered to a HTML string.
7
7
  #
8
8
  # @abstract Implement this method in your input class to describe how the input should render itself.
@@ -17,15 +17,16 @@ module Formtastic
17
17
  def to_html
18
18
  raise NotImplementedError
19
19
  end
20
-
20
+
21
21
  def input_html_options
22
- {
22
+ {
23
23
  :id => dom_id,
24
24
  :required => required_attribute?,
25
- :autofocus => autofocus?
25
+ :autofocus => autofocus?,
26
+ :readonly => readonly?
26
27
  }.merge(options[:input_html] || {})
27
28
  end
28
-
29
+
29
30
  def dom_id
30
31
  [
31
32
  builder.dom_id_namespace,
@@ -34,7 +35,7 @@ module Formtastic
34
35
  association_primary_key || sanitized_method_name
35
36
  ].reject { |x| x.blank? }.join('_')
36
37
  end
37
-
38
+
38
39
  def dom_index
39
40
  if builder.options.has_key?(:index)
40
41
  builder.options[:index]
@@ -4,9 +4,9 @@ module Formtastic
4
4
  module Naming
5
5
 
6
6
  def as
7
- self.class.name.split("::").last.underscore.gsub(/_input$/, '')
7
+ self.class.name.split("::")[-1].underscore.gsub(/_input$/, '')
8
8
  end
9
-
9
+
10
10
  def sanitized_object_name
11
11
  object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
12
12
  end
@@ -18,7 +18,7 @@ module Formtastic
18
18
  def attributized_method_name
19
19
  method.to_s.gsub(/_id$/, '').to_sym
20
20
  end
21
-
21
+
22
22
  def humanized_method_name
23
23
  if builder.label_str_method != :humanize
24
24
  # Special case where label_str_method should trump the human_attribute_name
@@ -39,4 +39,4 @@ module Formtastic
39
39
  end
40
40
  end
41
41
  end
42
- end
42
+ end
@@ -2,15 +2,14 @@ module Formtastic
2
2
  module Inputs
3
3
  module Base
4
4
  module Options
5
-
5
+
6
6
  def input_options
7
7
  options.except(*formtastic_options)
8
8
  end
9
-
9
+
10
10
  def formtastic_options
11
11
  [:priority_countries, :priority_zones, :member_label, :member_value, :collection, :required, :label, :as, :hint, :input_html, :value_as_class, :class]
12
12
  end
13
-
14
13
  end
15
14
  end
16
15
  end