activemodel 3.1.12 → 3.2.0.rc1

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 (33) hide show
  1. data/CHANGELOG.md +81 -36
  2. data/README.rdoc +1 -1
  3. data/lib/active_model/attribute_methods.rb +123 -104
  4. data/lib/active_model/callbacks.rb +2 -2
  5. data/lib/active_model/conversion.rb +26 -2
  6. data/lib/active_model/dirty.rb +3 -3
  7. data/lib/active_model/errors.rb +63 -51
  8. data/lib/active_model/lint.rb +12 -3
  9. data/lib/active_model/mass_assignment_security.rb +27 -8
  10. data/lib/active_model/mass_assignment_security/permission_set.rb +5 -5
  11. data/lib/active_model/mass_assignment_security/sanitizer.rb +42 -6
  12. data/lib/active_model/naming.rb +18 -10
  13. data/lib/active_model/observer_array.rb +3 -3
  14. data/lib/active_model/observing.rb +1 -2
  15. data/lib/active_model/secure_password.rb +2 -2
  16. data/lib/active_model/serialization.rb +61 -10
  17. data/lib/active_model/serializers/json.rb +20 -14
  18. data/lib/active_model/serializers/xml.rb +55 -31
  19. data/lib/active_model/translation.rb +15 -3
  20. data/lib/active_model/validations.rb +1 -1
  21. data/lib/active_model/validations/acceptance.rb +3 -1
  22. data/lib/active_model/validations/confirmation.rb +3 -1
  23. data/lib/active_model/validations/exclusion.rb +5 -3
  24. data/lib/active_model/validations/format.rb +4 -2
  25. data/lib/active_model/validations/inclusion.rb +5 -3
  26. data/lib/active_model/validations/length.rb +22 -10
  27. data/lib/active_model/validations/numericality.rb +4 -2
  28. data/lib/active_model/validations/presence.rb +5 -3
  29. data/lib/active_model/validations/validates.rb +15 -3
  30. data/lib/active_model/validations/with.rb +4 -2
  31. data/lib/active_model/version.rb +3 -3
  32. metadata +21 -28
  33. checksums.yaml +0 -7
@@ -59,7 +59,7 @@ module ActiveModel
59
59
  # define_model_callbacks :initializer, :only => :after
60
60
  #
61
61
  # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on
62
- # that method call. To get around this you can call the define_model_callbacks
62
+ # that method call. To get around this you can call the define_model_callbacks
63
63
  # method as many times as you need.
64
64
  #
65
65
  # define_model_callbacks :create, :only => :after
@@ -93,7 +93,7 @@ module ActiveModel
93
93
  :only => [:before, :around, :after]
94
94
  }.merge(options)
95
95
 
96
- types = Array.wrap(options.delete(:only))
96
+ types = Array.wrap(options.delete(:only))
97
97
 
98
98
  callbacks.each do |callback|
99
99
  define_callbacks(callback, options)
@@ -1,9 +1,12 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/inflector'
3
+
1
4
  module ActiveModel
2
5
  # == Active Model Conversions
3
6
  #
4
- # Handles default conversions: to_model, to_key and to_param.
7
+ # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
5
8
  #
6
- # Let's take for example this non persisted object.
9
+ # Let's take for example this non-persisted object.
7
10
  #
8
11
  # class ContactMessage
9
12
  # include ActiveModel::Conversion
@@ -18,8 +21,11 @@ module ActiveModel
18
21
  # cm.to_model == self # => true
19
22
  # cm.to_key # => nil
20
23
  # cm.to_param # => nil
24
+ # cm.to_path # => "contact_messages/contact_message"
21
25
  #
22
26
  module Conversion
27
+ extend ActiveSupport::Concern
28
+
23
29
  # If your object is already designed to implement all of the Active Model
24
30
  # you can use the default <tt>:to_model</tt> implementation, which simply
25
31
  # returns self.
@@ -45,5 +51,23 @@ module ActiveModel
45
51
  def to_param
46
52
  persisted? ? to_key.join('-') : nil
47
53
  end
54
+
55
+ # Returns a string identifying the path associated with the object.
56
+ # ActionPack uses this to find a suitable partial to represent the object.
57
+ def to_partial_path
58
+ self.class._to_partial_path
59
+ end
60
+
61
+ module ClassMethods #:nodoc:
62
+ # Provide a class level cache for the to_path. This is an
63
+ # internal method and should not be accessed directly.
64
+ def _to_partial_path #:nodoc:
65
+ @_to_partial_path ||= begin
66
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self))
67
+ collection = ActiveSupport::Inflector.tableize(self)
68
+ "#{collection}/#{element}".freeze
69
+ end
70
+ end
71
+ end
48
72
  end
49
73
  end
@@ -29,7 +29,7 @@ module ActiveModel
29
29
  #
30
30
  # include ActiveModel::Dirty
31
31
  #
32
- # define_attribute_methods = [:name]
32
+ # define_attribute_methods [:name]
33
33
  #
34
34
  # def name
35
35
  # @name
@@ -98,7 +98,7 @@ module ActiveModel
98
98
  # person.name = 'bob'
99
99
  # person.changed? # => true
100
100
  def changed?
101
- !changed_attributes.empty?
101
+ changed_attributes.any?
102
102
  end
103
103
 
104
104
  # List of attributes with unsaved changes.
@@ -156,7 +156,7 @@ module ActiveModel
156
156
  rescue TypeError, NoMethodError
157
157
  end
158
158
 
159
- changed_attributes[attr] = value
159
+ changed_attributes[attr] = value unless changed_attributes.include?(attr)
160
160
  end
161
161
 
162
162
  # Handle <tt>reset_*!</tt> for +method_missing+.
@@ -49,8 +49,8 @@ module ActiveModel
49
49
  #
50
50
  # The last three methods are required in your object for Errors to be
51
51
  # able to generate error messages correctly and also handle multiple
52
- # languages. Of course, if you extend your object with ActiveModel::Translations
53
- # you will not need to implement the last two. Likewise, using
52
+ # languages. Of course, if you extend your object with ActiveModel::Translation
53
+ # you will not need to implement the last two. Likewise, using
54
54
  # ActiveModel::Validations will handle the validation related methods
55
55
  # for you.
56
56
  #
@@ -63,7 +63,7 @@ module ActiveModel
63
63
  class Errors
64
64
  include Enumerable
65
65
 
66
- CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank]
66
+ CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
67
67
 
68
68
  attr_reader :messages
69
69
 
@@ -79,19 +79,6 @@ module ActiveModel
79
79
  @messages = ActiveSupport::OrderedHash.new
80
80
  end
81
81
 
82
- def initialize_dup(other)
83
- @messages = other.messages.dup
84
- end
85
-
86
- # Backport dup from 1.9 so that #initialize_dup gets called
87
- unless Object.respond_to?(:initialize_dup)
88
- def dup # :nodoc:
89
- copy = super
90
- copy.initialize_dup(self)
91
- copy
92
- end
93
- end
94
-
95
82
  # Clear the messages
96
83
  def clear
97
84
  messages.clear
@@ -101,6 +88,7 @@ module ActiveModel
101
88
  def include?(error)
102
89
  (v = messages[error]) && v.any?
103
90
  end
91
+ alias :has_key? :include?
104
92
 
105
93
  # Get messages for +key+
106
94
  def get(key)
@@ -112,11 +100,6 @@ module ActiveModel
112
100
  messages[key] = value
113
101
  end
114
102
 
115
- # Delete messages for +key+
116
- def delete(key)
117
- messages.delete(key)
118
- end
119
-
120
103
  # When passed a symbol or a name of a method, returns an array of errors
121
104
  # for the method.
122
105
  #
@@ -131,11 +114,11 @@ module ActiveModel
131
114
  # p.errors[:name] = "must be set"
132
115
  # p.errors[:name] # => ['must be set']
133
116
  def []=(attribute, error)
134
- self[attribute] << error
117
+ self[attribute.to_sym] << error
135
118
  end
136
119
 
137
120
  # Iterates through each error key, value pair in the error messages hash.
138
- # Yields the attribute and the error for that attribute. If the attribute
121
+ # Yields the attribute and the error for that attribute. If the attribute
139
122
  # has more than one error message, yields once for each error message.
140
123
  #
141
124
  # p.errors.add(:name, "can't be blank")
@@ -193,10 +176,12 @@ module ActiveModel
193
176
  end
194
177
 
195
178
  # Returns true if no errors are found, false otherwise.
179
+ # If the error message is a string it can be empty.
196
180
  def empty?
197
- all? { |k, v| v && v.empty? }
181
+ all? { |k, v| v && v.empty? && !v.is_a?(String) }
198
182
  end
199
183
  alias_method :blank?, :empty?
184
+
200
185
  # Returns an xml formatted representation of the Errors hash.
201
186
  #
202
187
  # p.errors.add(:name, "can't be blank")
@@ -221,20 +206,16 @@ module ActiveModel
221
206
  messages.dup
222
207
  end
223
208
 
224
- # Adds +message+ to the error messages on +attribute+, which will be returned on a call to
225
- # <tt>on(attribute)</tt> for the same attribute. More than one error can be added to the same
226
- # +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
209
+ # Adds +message+ to the error messages on +attribute+. More than one error can be added to the same
210
+ # +attribute+.
227
211
  # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
228
212
  #
229
213
  # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+).
230
214
  # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error.
231
215
  def add(attribute, message = nil, options = {})
232
- message ||= :invalid
233
-
234
- if message.is_a?(Symbol)
235
- message = generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
236
- elsif message.is_a?(Proc)
237
- message = message.call
216
+ message = normalize_message(attribute, message, options)
217
+ if options[:strict]
218
+ raise ActiveModel::StrictValidationFailed, message
238
219
  end
239
220
 
240
221
  self[attribute] << message
@@ -257,6 +238,15 @@ module ActiveModel
257
238
  end
258
239
  end
259
240
 
241
+ # Returns true if an error on the attribute with the given message is present, false otherwise.
242
+ # +message+ is treated the same as for +add+.
243
+ # p.errors.add :name, :blank
244
+ # p.errors.added? :name, :blank # => true
245
+ def added?(attribute, message = nil, options = {})
246
+ message = normalize_message(attribute, message, options)
247
+ self[attribute].include? message
248
+ end
249
+
260
250
  # Returns all the full error messages in an array.
261
251
  #
262
252
  # class Company
@@ -268,20 +258,22 @@ module ActiveModel
268
258
  # company.errors.full_messages # =>
269
259
  # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
270
260
  def full_messages
271
- map { |attribute, message|
272
- if attribute == :base
273
- message
274
- else
275
- attr_name = attribute.to_s.gsub('.', '_').humanize
276
- attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
277
-
278
- I18n.t(:"errors.format", {
279
- :default => "%{attribute} %{message}",
280
- :attribute => attr_name,
281
- :message => message
282
- })
283
- end
284
- }
261
+ map { |attribute, message| full_message(attribute, message) }
262
+ end
263
+
264
+ # Returns a full message for a given attribute.
265
+ #
266
+ # company.errors.full_message(:name, "is invalid") # =>
267
+ # "Name is invalid"
268
+ def full_message(attribute, message)
269
+ return message if attribute == :base
270
+ attr_name = attribute.to_s.gsub('.', '_').humanize
271
+ attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
272
+ I18n.t(:"errors.format", {
273
+ :default => "%{attribute} %{message}",
274
+ :attribute => attr_name,
275
+ :message => message
276
+ })
285
277
  end
286
278
 
287
279
  # Translates an error message in its default scope
@@ -311,13 +303,17 @@ module ActiveModel
311
303
  def generate_message(attribute, type = :invalid, options = {})
312
304
  type = options.delete(:message) if options[:message].is_a?(Symbol)
313
305
 
314
- defaults = @base.class.lookup_ancestors.map do |klass|
315
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
316
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
306
+ if @base.class.respond_to?(:i18n_scope)
307
+ defaults = @base.class.lookup_ancestors.map do |klass|
308
+ [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
309
+ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
310
+ end
311
+ else
312
+ defaults = []
317
313
  end
318
314
 
319
315
  defaults << options.delete(:message)
320
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}"
316
+ defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
321
317
  defaults << :"errors.attributes.#{attribute}.#{type}"
322
318
  defaults << :"errors.messages.#{type}"
323
319
 
@@ -336,5 +332,21 @@ module ActiveModel
336
332
 
337
333
  I18n.translate(key, options)
338
334
  end
335
+
336
+ private
337
+ def normalize_message(attribute, message, options)
338
+ message ||= :invalid
339
+
340
+ if message.is_a?(Symbol)
341
+ generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
342
+ elsif message.is_a?(Proc)
343
+ message.call
344
+ else
345
+ message
346
+ end
347
+ end
348
+ end
349
+
350
+ class StrictValidationFailed < StandardError
339
351
  end
340
352
  end
@@ -43,6 +43,16 @@ module ActiveModel
43
43
  assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
44
44
  end
45
45
 
46
+ # == Responds to <tt>to_partial_path</tt>
47
+ #
48
+ # Returns a string giving a relative path. This is used for looking up
49
+ # partials. For example, a BlogPost model might return "blog_posts/blog_post"
50
+ #
51
+ def test_to_partial_path
52
+ assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path"
53
+ assert_kind_of String, model.to_partial_path
54
+ end
55
+
46
56
  # == Responds to <tt>valid?</tt>
47
57
  #
48
58
  # Returns a boolean that specifies whether the object is in a valid or invalid
@@ -66,15 +76,14 @@ module ActiveModel
66
76
 
67
77
  # == Naming
68
78
  #
69
- # Model.model_name must return a string with some convenience methods as
70
- # :human and :partial_path. Check ActiveModel::Naming for more information.
79
+ # Model.model_name must return a string with some convenience methods:
80
+ # :human, :singular, and :plural. Check ActiveModel::Naming for more information.
71
81
  #
72
82
  def test_model_naming
73
83
  assert model.class.respond_to?(:model_name), "The model should respond to model_name"
74
84
  model_name = model.class.model_name
75
85
  assert_kind_of String, model_name
76
86
  assert_kind_of String, model_name.human
77
- assert_kind_of String, model_name.partial_path
78
87
  assert_kind_of String, model_name.singular
79
88
  assert_kind_of String, model_name.plural
80
89
  end
@@ -1,6 +1,8 @@
1
- require 'active_support/core_ext/class/attribute.rb'
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/core_ext/string/inflections'
2
3
  require 'active_support/core_ext/array/wrap'
3
4
  require 'active_model/mass_assignment_security/permission_set'
5
+ require 'active_model/mass_assignment_security/sanitizer'
4
6
 
5
7
  module ActiveModel
6
8
  # = Active Model Mass-Assignment Security
@@ -11,6 +13,9 @@ module ActiveModel
11
13
  class_attribute :_accessible_attributes
12
14
  class_attribute :_protected_attributes
13
15
  class_attribute :_active_authorizer
16
+
17
+ class_attribute :_mass_assignment_sanitizer
18
+ self.mass_assignment_sanitizer = :logger
14
19
  end
15
20
 
16
21
  # Mass assignment security provides an interface for protecting attributes
@@ -42,6 +47,16 @@ module ActiveModel
42
47
  #
43
48
  # end
44
49
  #
50
+ # = Configuration options
51
+ #
52
+ # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible values are:
53
+ # * <tt>:logger</tt> (default) - writes filtered attributes to logger
54
+ # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> on any protected attribute update
55
+ #
56
+ # You can specify your own sanitizer object eg. MySanitizer.new.
57
+ # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation.
58
+ #
59
+ #
45
60
  module ClassMethods
46
61
  # Attributes named in this macro are protected from mass-assignment
47
62
  # whenever attributes are sanitized before assignment. A role for the
@@ -184,21 +199,25 @@ module ActiveModel
184
199
  []
185
200
  end
186
201
 
202
+ def mass_assignment_sanitizer=(value)
203
+ self._mass_assignment_sanitizer = if value.is_a?(Symbol)
204
+ const_get(:"#{value.to_s.camelize}Sanitizer").new(self)
205
+ else
206
+ value
207
+ end
208
+ end
209
+
187
210
  private
188
211
 
189
212
  def protected_attributes_configs
190
213
  self._protected_attributes ||= begin
191
- default_black_list = BlackList.new(attributes_protected_by_default).tap do |w|
192
- w.logger = self.logger if self.respond_to?(:logger)
193
- end
194
- Hash.new(default_black_list)
214
+ Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) }
195
215
  end
196
216
  end
197
217
 
198
218
  def accessible_attributes_configs
199
219
  self._accessible_attributes ||= begin
200
- default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
201
- Hash.new(default_white_list)
220
+ Hash.new { |h,k| h[k] = WhiteList.new }
202
221
  end
203
222
  end
204
223
  end
@@ -206,7 +225,7 @@ module ActiveModel
206
225
  protected
207
226
 
208
227
  def sanitize_for_mass_assignment(attributes, role = :default)
209
- mass_assignment_authorizer(role).sanitize(attributes)
228
+ _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role))
210
229
  end
211
230
 
212
231
  def mass_assignment_authorizer(role = :default)
@@ -1,10 +1,8 @@
1
1
  require 'set'
2
- require 'active_model/mass_assignment_security/sanitizer'
3
2
 
4
3
  module ActiveModel
5
4
  module MassAssignmentSecurity
6
5
  class PermissionSet < Set
7
- attr_accessor :logger
8
6
 
9
7
  def +(values)
10
8
  super(values.map(&:to_s))
@@ -14,15 +12,18 @@ module ActiveModel
14
12
  super(remove_multiparameter_id(key))
15
13
  end
16
14
 
15
+ def deny?(key)
16
+ raise NotImplementedError, "#deny?(key) suppose to be overwritten"
17
+ end
18
+
17
19
  protected
18
20
 
19
21
  def remove_multiparameter_id(key)
20
- key.to_s.gsub(/\(.+/m, '')
22
+ key.to_s.gsub(/\(.+/, '')
21
23
  end
22
24
  end
23
25
 
24
26
  class WhiteList < PermissionSet
25
- include Sanitizer
26
27
 
27
28
  def deny?(key)
28
29
  !include?(key)
@@ -30,7 +31,6 @@ module ActiveModel
30
31
  end
31
32
 
32
33
  class BlackList < PermissionSet
33
- include Sanitizer
34
34
 
35
35
  def deny?(key)
36
36
  include?(key)