formtastic 3.1.3 → 3.1.4

Sign up to get free protection for your applications and to get access to all the features.
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