activemodel 3.1.12 → 3.2.0.rc1

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