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
@@ -136,9 +136,10 @@ module Formtastic
136
136
  return true if options[:required] == true
137
137
  return false if not_required_through_negated_validation?
138
138
  if validations?
139
- validations.select { |validator|
139
+ validations.any? { |validator|
140
140
  if validator.options.key?(:on)
141
- return false if (validator.options[:on] != :save) && ((object.new_record? && validator.options[:on] != :create) || (!object.new_record? && validator.options[:on] != :update))
141
+ validator_on = Array(validator.options[:on])
142
+ next false if (validator_on.exclude?(:save)) && ((object.new_record? && validator_on.exclude?(:create)) || (!object.new_record? && validator_on.exclude?(:update)))
142
143
  end
143
144
  case validator.kind
144
145
  when :presence
@@ -152,7 +153,7 @@ module Formtastic
152
153
  else
153
154
  false
154
155
  end
155
- }.any?
156
+ }
156
157
  else
157
158
  return responds_to_global_required? && !!builder.all_fields_required_by_default
158
159
  end
@@ -192,6 +193,21 @@ module Formtastic
192
193
  validation_limit || column_limit
193
194
  end
194
195
 
196
+ def readonly?
197
+ readonly_from_options? || readonly_attribute?
198
+ end
199
+
200
+ def readonly_attribute?
201
+ object_class = self.object.class
202
+ object_class.respond_to?(:readonly_attributes) &&
203
+ self.object.persisted? &&
204
+ column.respond_to?(:name) &&
205
+ object_class.readonly_attributes.include?(column.name.to_s)
206
+ end
207
+
208
+ def readonly_from_options?
209
+ options[:input_html] && options[:input_html][:readonly]
210
+ end
195
211
  end
196
212
  end
197
213
  end
@@ -69,6 +69,11 @@ module Formtastic
69
69
  include Base::Collections
70
70
  include Base::Choices
71
71
 
72
+ def initialize(*args)
73
+ super
74
+ raise Formtastic::UnsupportedEnumCollection if collection_from_enum?
75
+ end
76
+
72
77
  def to_html
73
78
  input_wrapping do
74
79
  choices_wrapping do
@@ -175,11 +180,14 @@ module Formtastic
175
180
  def make_selected_values
176
181
  if object.respond_to?(method)
177
182
  selected_items = object.send(method)
178
-
179
183
  # Construct an array from the return value, regardless of the return type
180
184
  selected_items = [*selected_items].compact.flatten
181
185
 
182
- [*selected_items.map { |o| send_or_call_or_object(value_method, o) }].compact
186
+ selected = []
187
+ selected_items.map do |selected_item|
188
+ selected_item_id = selected_item.id if selected_item.respond_to? :id
189
+ item = send_or_call_or_object(value_method, selected_item) || selected_item_id
190
+ end.compact
183
191
  else
184
192
  []
185
193
  end
@@ -67,8 +67,10 @@ module Formtastic
67
67
  class CountryInput
68
68
  include Base
69
69
 
70
+ CountrySelectPluginMissing = Class.new(StandardError)
71
+
70
72
  def to_html
71
- raise "To use the :country input, please install a country_select plugin, like this one: https://github.com/stefanpenner/country_select" unless builder.respond_to?(:country_select)
73
+ raise CountrySelectPluginMissing, "To use the :country input, please install a country_select plugin, like this one: https://github.com/stefanpenner/country_select" unless builder.respond_to?(:country_select)
72
74
  input_wrapping do
73
75
  label_html <<
74
76
  builder.country_select(method, priority_countries, input_options, input_html_options)
@@ -31,6 +31,10 @@ module Formtastic
31
31
  # `Section.all` for a `Post` form with an input for a `belongs_to :section` association.
32
32
  # You can override or customise this collection through the `:collection` option (see examples).
33
33
  #
34
+ # For radio inputs that map to ActiveRecord `enum` attributes, Formtastic will automatically
35
+ # load in your enum options to be used as the radio button choices. This can be overridden with
36
+ # the `:collection` option, or augmented with I18n translations. See examples below.
37
+ #
34
38
  # The way on which Formtastic renders the `value` attribute and label for each choice in the `:collection` is
35
39
  # customisable (see examples below). When not provided, we fall back to a list of methods to try on each
36
40
  # object such as `:to_label`, `:name` and `:to_s`, which are defined in the configurations
@@ -100,6 +104,22 @@ module Formtastic
100
104
  # @example Set HTML options on a specific radio input option with a 3rd element in the array for a collection member
101
105
  # <%= f.input :author, :as => :radio, :collection => [["Test", 'test'], ["Try", "try", {:disabled => true}]]
102
106
  #
107
+ # @example Using ActiveRecord enum attribute with i18n translation:
108
+ # # post.rb
109
+ # class Post < ActiveRecord::Base
110
+ # enum :status => [ :active, :archived ]
111
+ # end
112
+ # # en.yml
113
+ # en:
114
+ # activerecord:
115
+ # attributes:
116
+ # post:
117
+ # statuses:
118
+ # active: I am active!
119
+ # archived: I am archived!
120
+ # # form
121
+ # <%= f.input :status, :as => :radio %>
122
+ #
103
123
  # @see Formtastic::Helpers::InputsHelper#input InputsHelper#input for full documentation of all possible options.
104
124
  # @see Formtastic::Inputs::RadioInput as an alternative for `belongs_to` associations
105
125
  #
@@ -8,6 +8,7 @@ module Formtastic
8
8
  # This is the default input choice when:
9
9
  #
10
10
  # * the database column type is an `:integer` and there is an association (`belongs_to`)
11
+ # * the database column type is an `:integer` and there is an enum defined (`enum`)
11
12
  # * the database column type is a `:string` and the `:collection` option is used
12
13
  # * there an object with an association, but no database column on the object (`has_many`, etc)
13
14
  # * there is no object and the `:collection` option is used
@@ -38,6 +39,13 @@ module Formtastic
38
39
  # `:to_s`, which are defined in the configurations `collection_label_methods` and
39
40
  # `collection_value_methods` (see examples below).
40
41
  #
42
+ # For select inputs that map to ActiveRecord `enum` attributes, Formtastic will automatically
43
+ # load in your enum options to be used as the select's options. This can be overridden with
44
+ # the `:collection` option, or augmented with I18n translations. See examples below.
45
+ # An error is raised if you try to render a multi-select with an enum, as ActiveRecord can
46
+ # only store one choice in the database.
47
+ #
48
+ #
41
49
  # @example Basic `belongs_to` example with full form context
42
50
  #
43
51
  # <%= semantic_form_for @post do |f| %>
@@ -124,6 +132,21 @@ module Formtastic
124
132
  # <%= f.input :author, :as => :select, :prompt => true %> => <option value="">Please select</option>
125
133
  # <%= f.input :author, :as => :select, :prompt => "Please select an author" %>
126
134
  #
135
+ # @example Using ActiveRecord enum attribute with i18n translation:
136
+ # # post.rb
137
+ # class Post < ActiveRecord::Base
138
+ # enum :status => [ :active, :archived ]
139
+ # end
140
+ # # en.yml
141
+ # en:
142
+ # activerecord:
143
+ # attributes:
144
+ # post:
145
+ # statuses:
146
+ # active: I am active!
147
+ # archived: I am archived!
148
+ # # form
149
+ # <%= f.input :status, :as => :select %>
127
150
  #
128
151
  # @see Formtastic::Helpers::InputsHelper#input InputsHelper#input for full documentation of all possible options.
129
152
  # @see Formtastic::Inputs::CheckBoxesInput CheckBoxesInput as an alternative for `has_many` and `has_and_belongs_to_many` associations
@@ -134,6 +157,11 @@ module Formtastic
134
157
  include Base
135
158
  include Base::Collections
136
159
 
160
+ def initialize(*args)
161
+ super
162
+ raise Formtastic::UnsupportedEnumCollection if collection_from_enum? && multiple?
163
+ end
164
+
137
165
  def to_html
138
166
  input_wrapping do
139
167
  label_html <<
@@ -28,9 +28,19 @@ module Formtastic
28
28
  #
29
29
  # @see Formtastic::Helpers::InputsHelper#input InputsHelper#input for full documentation of all possible options.
30
30
  #
31
- # @todo document :priority_zones option
32
- # @todo configurable default :priority_zones?
33
- class TimeZoneInput
31
+ # The priority_zones option:
32
+ # Since this input actually uses Rails' `time_zone_select` helper, the :priority_zones
33
+ # option needs to be an array of ActiveSupport::TimeZone objects.
34
+ #
35
+ # And you can configure default value using
36
+ #
37
+ # ```
38
+ # Formtastic::FormBuilder.priority_time_zones = [timezone1, timezone2]
39
+ # ```
40
+ #
41
+ # See http://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/time_zone_select for more information.
42
+ #
43
+ class TimeZoneInput
34
44
  include Base
35
45
 
36
46
  def to_html
@@ -39,10 +49,10 @@ module Formtastic
39
49
  builder.time_zone_select(method, priority_zones, input_options, input_html_options)
40
50
  end
41
51
  end
42
-
52
+
43
53
  def priority_zones
44
- options[:priority_zones] || [] # TODO config?
54
+ options[:priority_zones] || Formtastic::FormBuilder.priority_time_zones
45
55
  end
46
56
  end
47
57
  end
48
- end
58
+ end
@@ -1,5 +1,5 @@
1
1
  module Formtastic
2
- # Implementation for looking up localized values within Formtastic using I18n, if no
2
+ # Implementation for looking up localized values within Formtastic using I18n, if no
3
3
  # explicit value (like the `:label` option) is set and I18n-lookups are enabled in the
4
4
  # configuration.
5
5
  #
@@ -28,50 +28,50 @@ module Formtastic
28
28
  def get(key)
29
29
  cache[key]
30
30
  end
31
-
31
+
32
32
  def has_key?(key)
33
33
  cache.has_key?(key)
34
34
  end
35
-
35
+
36
36
  def set(key, result)
37
37
  cache[key] = result
38
38
  end
39
-
39
+
40
40
  def cache
41
41
  @cache ||= {}
42
42
  end
43
-
43
+
44
44
  def clear!
45
45
  cache.clear
46
46
  end
47
47
  end
48
-
48
+
49
49
  attr_accessor :builder
50
-
50
+
51
51
  def self.cache
52
52
  @cache ||= Cache.new
53
53
  end
54
-
54
+
55
55
  def initialize(current_builder)
56
- self.builder = current_builder
56
+ self.builder = current_builder
57
57
  end
58
58
 
59
59
  def localize(key, value, type, options = {}) # @private
60
60
  key = value if value.is_a?(::Symbol)
61
-
61
+
62
62
  if value.is_a?(::String)
63
63
  escape_html_entities(value)
64
64
  else
65
65
  use_i18n = value.nil? ? i18n_lookups_by_default : (value != false)
66
66
  use_cache = i18n_cache_lookups
67
67
  cache = self.class.cache
68
-
68
+
69
69
  if use_i18n
70
70
  model_name, nested_model_name = normalize_model_name(builder.model_name.underscore)
71
71
 
72
72
  action_name = builder.template.params[:action].to_s rescue ''
73
73
  attribute_name = key.to_s
74
-
74
+
75
75
  # look in the cache first
76
76
  if use_cache
77
77
  cache_key = [::I18n.locale, action_name, model_name, nested_model_name, attribute_name, key, value, type, options]
@@ -86,7 +86,7 @@ module Formtastic
86
86
  i18n_path.gsub!('%{model}', model_name)
87
87
  i18n_path.gsub!('%{nested_model}', nested_model_name) unless nested_model_name.nil?
88
88
  i18n_path.gsub!('%{attribute}', attribute_name)
89
- i18n_path.gsub!('..', '.')
89
+ i18n_path.tr!('..', '.')
90
90
  i18n_path.to_sym
91
91
  end
92
92
  defaults << ''
@@ -103,7 +103,7 @@ module Formtastic
103
103
  options[:default] = defaults
104
104
  i18n_value = ::I18n.t(default_key, options)
105
105
  end
106
-
106
+
107
107
  # save the result to the cache
108
108
  result = (i18n_value.is_a?(::String) && i18n_value.present?) ? escape_html_entities(i18n_value) : nil
109
109
  cache.set(cache_key, result) if use_cache
@@ -143,7 +143,7 @@ module Formtastic
143
143
  def i18n_lookups_by_default
144
144
  builder.i18n_lookups_by_default
145
145
  end
146
-
146
+
147
147
  def i18n_cache_lookups
148
148
  builder.i18n_cache_lookups
149
149
  end
@@ -29,7 +29,7 @@ module Formtastic
29
29
  end
30
30
 
31
31
  def self.use_const_defined?
32
- defined?(Rails) && ::Rails.application && ::Rails.application.config.eager_load
32
+ defined?(Rails) && ::Rails.application && ::Rails.application.config.respond_to?(:eager_load) && ::Rails.application.config.eager_load
33
33
  end
34
34
 
35
35
  # @param namespaces [Array<Module>]
@@ -1,3 +1,3 @@
1
1
  module Formtastic
2
- VERSION = "3.1.3"
2
+ VERSION = "3.1.4"
3
3
  end
@@ -75,7 +75,7 @@ module Formtastic
75
75
  # Skips Active Record Timestamps.
76
76
  def content_columns
77
77
  model.content_columns.select do |column|
78
- !Formtastic::Helpers::InputsHelper::SKIPPED_COLUMNS.include? column.name.to_sym
78
+ !Formtastic::FormBuilder.skipped_columns.include? column.name.to_sym
79
79
  end
80
80
  end
81
81
 
@@ -0,0 +1,46 @@
1
+ module Formtastic
2
+
3
+ # Modify existing inputs, subclass them, or create your own from scratch.
4
+ # @example
5
+ # !!!shell
6
+ # $ rails generate formtastic:input HatSize
7
+
8
+ # @example Define input name using underscore convention
9
+ # !!!shell
10
+ # $ rails generate formtastic:input hat_size
11
+
12
+ # @example Override an existing input behavior
13
+ # !!!shell
14
+ # $ rails generate formtastic:input string --extend
15
+
16
+ # @example Extend an existing input behavior
17
+ # !!!shell
18
+ # $ rails generate formtastic:input FlexibleText --extend string
19
+ class InputGenerator < Rails::Generators::NamedBase
20
+
21
+ argument :name, :type => :string, :required => true, :banner => 'FILE_NAME'
22
+
23
+ source_root File.expand_path('../../../templates', __FILE__)
24
+
25
+ class_option :extend
26
+
27
+ def create
28
+ normalize_file_name
29
+ define_extension_sentence
30
+ template "input.rb", "app/inputs/#{name.underscore}_input.rb"
31
+ end
32
+
33
+ protected
34
+
35
+ def normalize_file_name
36
+ name.chomp!("Input") if name.ends_with?("Input")
37
+ name.chomp!("_input") if name.ends_with?("_input")
38
+ name.chomp!("input") if name.ends_with?("input")
39
+ end
40
+
41
+ def define_extension_sentence
42
+ @extension_sentence = "< Formtastic::Inputs::#{name.camelize}Input" if options[:extend] == "extend"
43
+ @extension_sentence ||= "< Formtastic::Inputs::#{options[:extend].camelize}Input" if options[:extend]
44
+ end
45
+ end
46
+ end
@@ -17,15 +17,15 @@
17
17
  # Defaults to true.
18
18
  # Formtastic::FormBuilder.include_blank_for_select_by_default = true
19
19
 
20
- # Set the string that will be appended to the labels/fieldsets which are required
20
+ # Set the string that will be appended to the labels/fieldsets which are required.
21
21
  # It accepts string or procs and the default is a localized version of
22
22
  # '<abbr title="required">*</abbr>'. In other words, if you configure formtastic.required
23
23
  # in your locale, it will replace the abbr title properly. But if you don't want to use
24
- # abbr tag, you can simply give a string as below
24
+ # abbr tag, you can simply give a string as below.
25
25
  # Formtastic::FormBuilder.required_string = "(required)"
26
26
 
27
- # Set the string that will be appended to the labels/fieldsets which are optional
28
- # Defaults to an empty string ("") and also accepts procs (see required_string above)
27
+ # Set the string that will be appended to the labels/fieldsets which are optional.
28
+ # Defaults to an empty string ("") and also accepts procs (see required_string above).
29
29
  # Formtastic::FormBuilder.optional_string = "(optional)"
30
30
 
31
31
  # Set the way inline errors will be displayed.
@@ -33,7 +33,7 @@
33
33
  # Formtastic::FormBuilder.inline_errors = :sentence
34
34
  # Formtastic uses the following classes as default for hints, inline_errors and error list
35
35
 
36
- # If you override the class here, please ensure to override it in your stylesheets as well
36
+ # If you override the class here, please ensure to override it in your stylesheets as well.
37
37
  # Formtastic::FormBuilder.default_hint_class = "inline-hints"
38
38
  # Formtastic::FormBuilder.default_inline_error_class = "inline-errors"
39
39
  # Formtastic::FormBuilder.default_error_list_class = "errors"
@@ -70,7 +70,7 @@
70
70
  # can change it to any class you want.
71
71
  # Formtastic::Helpers::FormHelper.default_form_class = 'formtastic'
72
72
 
73
- # Formtastic will infer a class name from the model, array, string ot symbol you pass to the
73
+ # Formtastic will infer a class name from the model, array, string or symbol you pass to the
74
74
  # form builder. You can customize the way that class is presented by overriding
75
75
  # this proc.
76
76
  # Formtastic::Helpers::FormHelper.default_form_model_class_proc = proc { |model_class_name| model_class_name }
@@ -85,7 +85,7 @@
85
85
  # Formtastic::FormBuilder.use_required_attribute = false
86
86
 
87
87
  # You can opt-in to new HTML5 browser validations (for things like email and url inputs) by setting
88
- # this to true. Doing so will add a `novalidate` attribute to the `<form>` tag.
88
+ # this to true. Doing so will omit the `novalidate` attribute from the `<form>` tag.
89
89
  # See http://diveintohtml5.org/forms.html#validation for more info.
90
90
  # Formtastic::FormBuilder.perform_browser_validations = true
91
91
 
@@ -108,3 +108,6 @@ Formtastic::FormBuilder.action_class_finder = Formtastic::ActionClassFinder
108
108
  # Define custom namespaces in which to look up your Action classes. Default is
109
109
  # to look up in the global scope and in Formtastic::Actions.
110
110
  # Formtastic::FormBuilder.action_namespaces = [ ::Object, ::MyActionsModule, ::Formtastic::Actions ]
111
+
112
+ # Which columns to skip when automatically rendering a form without any fields specified.
113
+ # Formtastic::FormBuilder.skipped_columns = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
@@ -0,0 +1,19 @@
1
+ class <%= name.camelize %>Input <%= @extension_sentence %>
2
+ <%- if !options[:extend] -%>
3
+ include Formtastic::Inputs::Base
4
+ <%- end -%>
5
+
6
+ <%- if !options[:extend] || (options[:extend] == "extend") -%>
7
+ def to_html
8
+ # Add your custom input definition here.
9
+ <%- if options[:extend] == "extend" -%>
10
+ super
11
+ <%- end -%>
12
+ end
13
+
14
+ <%- else -%>
15
+ def input_html_options
16
+ # Add your custom input extension here.
17
+ end
18
+ <%- end -%>
19
+ end