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
@@ -1,9 +1,14 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
module MassAssignmentSecurity
|
3
|
-
|
5
|
+
class Sanitizer
|
6
|
+
def initialize(target=nil)
|
7
|
+
end
|
8
|
+
|
4
9
|
# Returns all attributes not denied by the authorizer.
|
5
|
-
def sanitize(attributes)
|
6
|
-
sanitized_attributes = attributes.reject { |key, value| deny?(key) }
|
10
|
+
def sanitize(attributes, authorizer)
|
11
|
+
sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) }
|
7
12
|
debug_protected_attribute_removal(attributes, sanitized_attributes)
|
8
13
|
sanitized_attributes
|
9
14
|
end
|
@@ -12,12 +17,43 @@ module ActiveModel
|
|
12
17
|
|
13
18
|
def debug_protected_attribute_removal(attributes, sanitized_attributes)
|
14
19
|
removed_keys = attributes.keys - sanitized_attributes.keys
|
15
|
-
|
20
|
+
process_removed_attributes(removed_keys) if removed_keys.any?
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_removed_attributes(attrs)
|
24
|
+
raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class LoggerSanitizer < Sanitizer
|
29
|
+
delegate :logger, :to => :@target
|
30
|
+
|
31
|
+
def initialize(target)
|
32
|
+
@target = target
|
33
|
+
super
|
16
34
|
end
|
17
35
|
|
18
|
-
def
|
19
|
-
|
36
|
+
def logger?
|
37
|
+
@target.respond_to?(:logger) && @target.logger
|
20
38
|
end
|
39
|
+
|
40
|
+
def process_removed_attributes(attrs)
|
41
|
+
logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class StrictSanitizer < Sanitizer
|
46
|
+
def process_removed_attributes(attrs)
|
47
|
+
return if (attrs - insensitive_attributes).empty?
|
48
|
+
raise ActiveModel::MassAssignmentSecurity::Error, "Can't mass-assign protected attributes: #{attrs.join(', ')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def insensitive_attributes
|
52
|
+
['id']
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Error < StandardError
|
21
57
|
end
|
22
58
|
end
|
23
59
|
end
|
data/lib/active_model/naming.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'active_support/inflector'
|
2
2
|
require 'active_support/core_ext/hash/except'
|
3
3
|
require 'active_support/core_ext/module/introspection'
|
4
|
+
require 'active_support/core_ext/module/deprecation'
|
4
5
|
|
5
6
|
module ActiveModel
|
6
7
|
class Name < String
|
@@ -9,17 +10,22 @@ module ActiveModel
|
|
9
10
|
|
10
11
|
alias_method :cache_key, :collection
|
11
12
|
|
13
|
+
deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead."
|
14
|
+
|
12
15
|
def initialize(klass, namespace = nil, name = nil)
|
13
16
|
name ||= klass.name
|
17
|
+
|
18
|
+
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank?
|
19
|
+
|
14
20
|
super(name)
|
15
|
-
@unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace
|
16
21
|
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
22
|
+
@unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace
|
23
|
+
@klass = klass
|
24
|
+
@singular = _singularize(self).freeze
|
25
|
+
@plural = ActiveSupport::Inflector.pluralize(@singular).freeze
|
26
|
+
@element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
|
27
|
+
@human = ActiveSupport::Inflector.humanize(@element).freeze
|
28
|
+
@collection = ActiveSupport::Inflector.tableize(self).freeze
|
23
29
|
@partial_path = "#{@collection}/#{@element}".freeze
|
24
30
|
@param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze
|
25
31
|
@i18n_key = self.underscore.to_sym
|
@@ -71,8 +77,8 @@ module ActiveModel
|
|
71
77
|
# BookCover.model_name # => "BookCover"
|
72
78
|
# BookCover.model_name.human # => "Book cover"
|
73
79
|
#
|
74
|
-
# BookCover.model_name.i18n_key # =>
|
75
|
-
# BookModule::BookCover.model_name.i18n_key # => "book_module
|
80
|
+
# BookCover.model_name.i18n_key # => :book_cover
|
81
|
+
# BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
|
76
82
|
#
|
77
83
|
# Providing the functionality that ActiveModel::Naming provides in your object
|
78
84
|
# is required to pass the Active Model Lint test. So either extending the provided
|
@@ -82,7 +88,9 @@ module ActiveModel
|
|
82
88
|
# used to retrieve all kinds of naming-related information.
|
83
89
|
def model_name
|
84
90
|
@_model_name ||= begin
|
85
|
-
namespace = self.parents.detect
|
91
|
+
namespace = self.parents.detect do |n|
|
92
|
+
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
93
|
+
end
|
86
94
|
ActiveModel::Name.new(self, namespace)
|
87
95
|
end
|
88
96
|
end
|
@@ -15,7 +15,7 @@ module ActiveModel
|
|
15
15
|
disabled_observers.include?(observer.class)
|
16
16
|
end
|
17
17
|
|
18
|
-
# Disables one or more observers.
|
18
|
+
# Disables one or more observers. This supports multiple forms:
|
19
19
|
#
|
20
20
|
# ORM.observers.disable :user_observer
|
21
21
|
# # => disables the UserObserver
|
@@ -38,7 +38,7 @@ module ActiveModel
|
|
38
38
|
set_enablement(false, observers, &block)
|
39
39
|
end
|
40
40
|
|
41
|
-
# Enables one or more observers.
|
41
|
+
# Enables one or more observers. This supports multiple forms:
|
42
42
|
#
|
43
43
|
# ORM.observers.enable :user_observer
|
44
44
|
# # => enables the UserObserver
|
@@ -59,7 +59,7 @@ module ActiveModel
|
|
59
59
|
# # just the duration of the block
|
60
60
|
# end
|
61
61
|
#
|
62
|
-
# Note: all observers are enabled by default.
|
62
|
+
# Note: all observers are enabled by default. This method is only
|
63
63
|
# useful when you have previously disabled one or more observers.
|
64
64
|
def enable(*observers, &block)
|
65
65
|
set_enablement(true, observers, &block)
|
@@ -187,8 +187,7 @@ module ActiveModel
|
|
187
187
|
def observe(*models)
|
188
188
|
models.flatten!
|
189
189
|
models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
|
190
|
-
|
191
|
-
define_method(:observed_classes) { models }
|
190
|
+
redefine_method(:observed_classes) { models }
|
192
191
|
end
|
193
192
|
|
194
193
|
# Returns an array of Classes to observe.
|
@@ -32,8 +32,8 @@ module ActiveModel
|
|
32
32
|
# User.find_by_name("david").try(:authenticate, "notright") # => nil
|
33
33
|
# User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
|
34
34
|
def has_secure_password
|
35
|
-
# Load bcrypt-ruby only when
|
36
|
-
# (and by extension the entire framework) dependent on a binary library.
|
35
|
+
# Load bcrypt-ruby only when has_secure_password is used.
|
36
|
+
# This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library.
|
37
37
|
gem 'bcrypt-ruby', '~> 3.0.0'
|
38
38
|
require 'bcrypt'
|
39
39
|
|
@@ -33,7 +33,7 @@ module ActiveModel
|
|
33
33
|
# you want to serialize and their current value.
|
34
34
|
#
|
35
35
|
# Most of the time though, you will want to include the JSON or XML
|
36
|
-
# serializations.
|
36
|
+
# serializations. Both of these modules automatically include the
|
37
37
|
# ActiveModel::Serialization module, so there is no need to explicitly
|
38
38
|
# include it.
|
39
39
|
#
|
@@ -71,18 +71,69 @@ module ActiveModel
|
|
71
71
|
def serializable_hash(options = nil)
|
72
72
|
options ||= {}
|
73
73
|
|
74
|
-
only = Array.wrap(options[:only]).map(&:to_s)
|
75
|
-
except = Array.wrap(options[:except]).map(&:to_s)
|
76
|
-
|
77
74
|
attribute_names = attributes.keys.sort
|
78
|
-
if only
|
79
|
-
attribute_names &= only
|
80
|
-
elsif except
|
81
|
-
attribute_names -= except
|
75
|
+
if only = options[:only]
|
76
|
+
attribute_names &= Array.wrap(only).map(&:to_s)
|
77
|
+
elsif except = options[:except]
|
78
|
+
attribute_names -= Array.wrap(except).map(&:to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
hash = {}
|
82
|
+
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
|
83
|
+
|
84
|
+
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
|
85
|
+
method_names.each { |n| hash[n] = send(n) }
|
86
|
+
|
87
|
+
serializable_add_includes(options) do |association, records, opts|
|
88
|
+
hash[association] = if records.is_a?(Enumerable)
|
89
|
+
records.map { |a| a.serializable_hash(opts) }
|
90
|
+
else
|
91
|
+
records.serializable_hash(opts)
|
92
|
+
end
|
82
93
|
end
|
83
94
|
|
84
|
-
|
85
|
-
Hash[(attribute_names + method_names).map { |n| [n, send(n)] }]
|
95
|
+
hash
|
86
96
|
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Hook method defining how an attribute value should be retrieved for
|
101
|
+
# serialization. By default this is assumed to be an instance named after
|
102
|
+
# the attribute. Override this method in subclasses should you need to
|
103
|
+
# retrieve the value for a given attribute differently:
|
104
|
+
#
|
105
|
+
# class MyClass
|
106
|
+
# include ActiveModel::Validations
|
107
|
+
#
|
108
|
+
# def initialize(data = {})
|
109
|
+
# @data = data
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# def read_attribute_for_serialization(key)
|
113
|
+
# @data[key]
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
alias :read_attribute_for_serialization :send
|
118
|
+
|
119
|
+
# Add associations specified via the <tt>:include</tt> option.
|
120
|
+
#
|
121
|
+
# Expects a block that takes as arguments:
|
122
|
+
# +association+ - name of the association
|
123
|
+
# +records+ - the association record(s) to be serialized
|
124
|
+
# +opts+ - options for the association records
|
125
|
+
def serializable_add_includes(options = {}) #:nodoc:
|
126
|
+
return unless include = options[:include]
|
127
|
+
|
128
|
+
unless include.is_a?(Hash)
|
129
|
+
include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
|
130
|
+
end
|
131
|
+
|
132
|
+
include.each do |association, opts|
|
133
|
+
if records = send(association)
|
134
|
+
yield association, records, opts
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
87
138
|
end
|
88
139
|
end
|
@@ -15,7 +15,7 @@ module ActiveModel
|
|
15
15
|
self.include_root_in_json = true
|
16
16
|
end
|
17
17
|
|
18
|
-
# Returns a
|
18
|
+
# Returns a hash representing the model. Some configuration can be
|
19
19
|
# passed through +options+.
|
20
20
|
#
|
21
21
|
# The option <tt>include_root_in_json</tt> controls the top-level behavior
|
@@ -32,10 +32,17 @@ module ActiveModel
|
|
32
32
|
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
33
33
|
# "created_at": "2006/08/01", "awesome": true}
|
34
34
|
#
|
35
|
-
#
|
36
|
-
# is false.
|
35
|
+
# This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
|
37
36
|
#
|
38
|
-
#
|
37
|
+
# user = User.find(1)
|
38
|
+
# user.as_json(root: false)
|
39
|
+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
40
|
+
# "created_at": "2006/08/01", "awesome": true}
|
41
|
+
#
|
42
|
+
# The remainder of the examples in this section assume include_root_in_json is set to
|
43
|
+
# <tt>false</tt>.
|
44
|
+
#
|
45
|
+
# Without any +options+, the returned Hash will include all the model's
|
39
46
|
# attributes. For example:
|
40
47
|
#
|
41
48
|
# user = User.find(1)
|
@@ -79,21 +86,20 @@ module ActiveModel
|
|
79
86
|
# "title": "Welcome to the weblog"},
|
80
87
|
# {"comments": [{"body": "Don't think too hard"}],
|
81
88
|
# "title": "So I was thinking"}]}
|
82
|
-
|
83
89
|
def as_json(options = nil)
|
84
|
-
|
85
|
-
|
86
|
-
if
|
87
|
-
|
88
|
-
|
90
|
+
root = include_root_in_json
|
91
|
+
root = options[:root] if options.try(:key?, :root)
|
92
|
+
if root
|
93
|
+
root = self.class.model_name.element if root == true
|
94
|
+
{ root => serializable_hash(options) }
|
95
|
+
else
|
96
|
+
serializable_hash(options)
|
89
97
|
end
|
90
|
-
|
91
|
-
hash
|
92
98
|
end
|
93
99
|
|
94
|
-
def from_json(json)
|
100
|
+
def from_json(json, include_root=include_root_in_json)
|
95
101
|
hash = ActiveSupport::JSON.decode(json)
|
96
|
-
hash = hash.values.first if
|
102
|
+
hash = hash.values.first if include_root
|
97
103
|
self.attributes = hash
|
98
104
|
self
|
99
105
|
end
|
@@ -15,10 +15,10 @@ module ActiveModel
|
|
15
15
|
class Attribute #:nodoc:
|
16
16
|
attr_reader :name, :value, :type
|
17
17
|
|
18
|
-
def initialize(name, serializable,
|
18
|
+
def initialize(name, serializable, value)
|
19
19
|
@name, @serializable = name, serializable
|
20
|
-
|
21
|
-
@value =
|
20
|
+
value = value.in_time_zone if value.respond_to?(:in_time_zone)
|
21
|
+
@value = value
|
22
22
|
@type = compute_type
|
23
23
|
end
|
24
24
|
|
@@ -49,40 +49,24 @@ module ActiveModel
|
|
49
49
|
def initialize(serializable, options = nil)
|
50
50
|
@serializable = serializable
|
51
51
|
@options = options ? options.dup : {}
|
52
|
-
|
53
|
-
@options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s }
|
54
|
-
@options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s }
|
55
52
|
end
|
56
53
|
|
57
|
-
|
58
|
-
|
59
|
-
# for a N level model but is set for the N+1 level models,
|
60
|
-
# then because <tt>:except</tt> is set to a default value, the second
|
61
|
-
# level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
|
62
|
-
# <tt>:only</tt> is set, always delete <tt>:except</tt>.
|
63
|
-
def attributes_hash
|
64
|
-
attributes = @serializable.attributes
|
65
|
-
if options[:only].any?
|
66
|
-
attributes.slice(*options[:only])
|
67
|
-
elsif options[:except].any?
|
68
|
-
attributes.except(*options[:except])
|
69
|
-
else
|
70
|
-
attributes
|
71
|
-
end
|
54
|
+
def serializable_hash
|
55
|
+
@serializable.serializable_hash(@options.except(:include))
|
72
56
|
end
|
73
57
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
58
|
+
def serializable_collection
|
59
|
+
methods = Array.wrap(options[:methods]).map(&:to_s)
|
60
|
+
serializable_hash.map do |name, value|
|
61
|
+
name = name.to_s
|
62
|
+
if methods.include?(name)
|
63
|
+
self.class::MethodAttribute.new(name, @serializable, value)
|
64
|
+
else
|
65
|
+
self.class::Attribute.new(name, @serializable, value)
|
66
|
+
end
|
77
67
|
end
|
78
68
|
end
|
79
69
|
|
80
|
-
def serializable_methods
|
81
|
-
Array.wrap(options[:methods]).map do |name|
|
82
|
-
self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
|
83
|
-
end.compact
|
84
|
-
end
|
85
|
-
|
86
70
|
def serialize
|
87
71
|
require 'builder' unless defined? ::Builder
|
88
72
|
|
@@ -101,6 +85,7 @@ module ActiveModel
|
|
101
85
|
|
102
86
|
@builder.tag!(*args) do
|
103
87
|
add_attributes_and_methods
|
88
|
+
add_includes
|
104
89
|
add_extra_behavior
|
105
90
|
add_procs
|
106
91
|
yield @builder if block_given?
|
@@ -113,13 +98,52 @@ module ActiveModel
|
|
113
98
|
end
|
114
99
|
|
115
100
|
def add_attributes_and_methods
|
116
|
-
|
101
|
+
serializable_collection.each do |attribute|
|
117
102
|
key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
|
118
103
|
ActiveSupport::XmlMini.to_tag(key, attribute.value,
|
119
104
|
options.merge(attribute.decorations))
|
120
105
|
end
|
121
106
|
end
|
122
107
|
|
108
|
+
def add_includes
|
109
|
+
@serializable.send(:serializable_add_includes, options) do |association, records, opts|
|
110
|
+
add_associations(association, records, opts)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
|
115
|
+
def add_associations(association, records, opts)
|
116
|
+
merged_options = opts.merge(options.slice(:builder, :indent))
|
117
|
+
merged_options[:skip_instruct] = true
|
118
|
+
|
119
|
+
if records.is_a?(Enumerable)
|
120
|
+
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
|
121
|
+
type = options[:skip_types] ? { } : {:type => "array"}
|
122
|
+
association_name = association.to_s.singularize
|
123
|
+
merged_options[:root] = association_name
|
124
|
+
|
125
|
+
if records.empty?
|
126
|
+
@builder.tag!(tag, type)
|
127
|
+
else
|
128
|
+
@builder.tag!(tag, type) do
|
129
|
+
records.each do |record|
|
130
|
+
if options[:skip_types]
|
131
|
+
record_type = {}
|
132
|
+
else
|
133
|
+
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
|
134
|
+
record_type = {:type => record_class}
|
135
|
+
end
|
136
|
+
|
137
|
+
record.to_xml merged_options.merge(record_type)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
else
|
142
|
+
merged_options[:root] = association.to_s
|
143
|
+
records.to_xml(merged_options)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
123
147
|
def add_procs
|
124
148
|
if procs = options.delete(:procs)
|
125
149
|
Array.wrap(procs).each do |proc|
|