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.
- data/CHANGELOG.md +81 -36
- data/README.rdoc +1 -1
- data/lib/active_model/attribute_methods.rb +123 -104
- data/lib/active_model/callbacks.rb +2 -2
- data/lib/active_model/conversion.rb +26 -2
- data/lib/active_model/dirty.rb +3 -3
- data/lib/active_model/errors.rb +63 -51
- data/lib/active_model/lint.rb +12 -3
- data/lib/active_model/mass_assignment_security.rb +27 -8
- data/lib/active_model/mass_assignment_security/permission_set.rb +5 -5
- data/lib/active_model/mass_assignment_security/sanitizer.rb +42 -6
- data/lib/active_model/naming.rb +18 -10
- data/lib/active_model/observer_array.rb +3 -3
- data/lib/active_model/observing.rb +1 -2
- data/lib/active_model/secure_password.rb +2 -2
- data/lib/active_model/serialization.rb +61 -10
- data/lib/active_model/serializers/json.rb +20 -14
- data/lib/active_model/serializers/xml.rb +55 -31
- data/lib/active_model/translation.rb +15 -3
- data/lib/active_model/validations.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +3 -1
- data/lib/active_model/validations/confirmation.rb +3 -1
- data/lib/active_model/validations/exclusion.rb +5 -3
- data/lib/active_model/validations/format.rb +4 -2
- data/lib/active_model/validations/inclusion.rb +5 -3
- data/lib/active_model/validations/length.rb +22 -10
- data/lib/active_model/validations/numericality.rb +4 -2
- data/lib/active_model/validations/presence.rb +5 -3
- data/lib/active_model/validations/validates.rb +15 -3
- data/lib/active_model/validations/with.rb +4 -2
- data/lib/active_model/version.rb +3 -3
- metadata +21 -28
- 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.
|
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
|
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
|
7
|
+
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
|
5
8
|
#
|
6
|
-
# Let's take for example this non
|
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
|
data/lib/active_model/dirty.rb
CHANGED
@@ -29,7 +29,7 @@ module ActiveModel
|
|
29
29
|
#
|
30
30
|
# include ActiveModel::Dirty
|
31
31
|
#
|
32
|
-
# define_attribute_methods
|
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
|
-
|
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+.
|
data/lib/active_model/errors.rb
CHANGED
@@ -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.
|
53
|
-
# you will not need to implement the last two.
|
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.
|
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
|
225
|
-
#
|
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
|
233
|
-
|
234
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
315
|
-
|
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
|
data/lib/active_model/lint.rb
CHANGED
@@ -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
|
70
|
-
# :human and :
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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(/\(
|
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)
|