activemodel 3.0.20 → 3.1.0.beta1
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.
- data/CHANGELOG +17 -73
- data/MIT-LICENSE +1 -1
- data/README.rdoc +5 -5
- data/lib/active_model.rb +2 -2
- data/lib/active_model/attribute_methods.rb +46 -53
- data/lib/active_model/callbacks.rb +2 -5
- data/lib/active_model/conversion.rb +0 -2
- data/lib/active_model/dirty.rb +3 -4
- data/lib/active_model/errors.rb +55 -56
- data/lib/active_model/lint.rb +2 -2
- data/lib/active_model/mass_assignment_security.rb +96 -47
- data/lib/active_model/naming.rb +55 -13
- data/lib/active_model/observer_array.rb +104 -0
- data/lib/active_model/observing.rb +53 -18
- data/lib/active_model/secure_password.rb +67 -0
- data/lib/active_model/serialization.rb +4 -11
- data/lib/active_model/serializers/json.rb +18 -18
- data/lib/active_model/serializers/xml.rb +26 -5
- data/lib/active_model/translation.rb +4 -11
- data/lib/active_model/validations.rb +23 -23
- data/lib/active_model/validations/acceptance.rb +3 -5
- data/lib/active_model/validations/callbacks.rb +5 -19
- data/lib/active_model/validations/confirmation.rb +6 -5
- data/lib/active_model/validations/exclusion.rb +27 -3
- data/lib/active_model/validations/format.rb +38 -12
- data/lib/active_model/validations/inclusion.rb +30 -23
- data/lib/active_model/validations/length.rb +3 -1
- data/lib/active_model/validations/numericality.rb +4 -2
- data/lib/active_model/validations/presence.rb +3 -2
- data/lib/active_model/validations/validates.rb +23 -9
- data/lib/active_model/validations/with.rb +14 -2
- data/lib/active_model/validator.rb +16 -18
- data/lib/active_model/version.rb +3 -3
- metadata +71 -58
- checksums.yaml +0 -7
- data/lib/active_model/deprecated_error_methods.rb +0 -33
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
# Stores the enabled/disabled state of individual observers for
|
5
|
+
# a particular model classes.
|
6
|
+
class ObserverArray < Array
|
7
|
+
attr_reader :model_class
|
8
|
+
def initialize(model_class, *args)
|
9
|
+
@model_class = model_class
|
10
|
+
super(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def disabled_for?(observer)
|
14
|
+
disabled_observers.include?(observer.class)
|
15
|
+
end
|
16
|
+
|
17
|
+
def disable(*observers, &block)
|
18
|
+
set_enablement(false, observers, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def enable(*observers, &block)
|
22
|
+
set_enablement(true, observers, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def disabled_observers
|
28
|
+
@disabled_observers ||= Set.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def observer_class_for(observer)
|
32
|
+
return observer if observer.is_a?(Class)
|
33
|
+
|
34
|
+
if observer.respond_to?(:to_sym) # string/symbol
|
35
|
+
observer.to_s.camelize.constantize
|
36
|
+
else
|
37
|
+
raise ArgumentError, "#{observer} was not a class or a " +
|
38
|
+
"lowercase, underscored class name as expected."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def start_transaction
|
43
|
+
disabled_observer_stack.push(disabled_observers.dup)
|
44
|
+
each_subclass_array do |array|
|
45
|
+
array.start_transaction
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def disabled_observer_stack
|
50
|
+
@disabled_observer_stack ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def end_transaction
|
54
|
+
@disabled_observers = disabled_observer_stack.pop
|
55
|
+
each_subclass_array do |array|
|
56
|
+
array.end_transaction
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def transaction
|
61
|
+
start_transaction
|
62
|
+
|
63
|
+
begin
|
64
|
+
yield
|
65
|
+
ensure
|
66
|
+
end_transaction
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def each_subclass_array
|
71
|
+
model_class.descendants.each do |subclass|
|
72
|
+
yield subclass.observers
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_enablement(enabled, observers)
|
77
|
+
if block_given?
|
78
|
+
transaction do
|
79
|
+
set_enablement(enabled, observers)
|
80
|
+
yield
|
81
|
+
end
|
82
|
+
else
|
83
|
+
observers = ActiveModel::Observer.descendants if observers == [:all]
|
84
|
+
observers.each do |obs|
|
85
|
+
klass = observer_class_for(obs)
|
86
|
+
|
87
|
+
unless klass < ActiveModel::Observer
|
88
|
+
raise ArgumentError.new("#{obs} does not refer to a valid observer")
|
89
|
+
end
|
90
|
+
|
91
|
+
if enabled
|
92
|
+
disabled_observers.delete(klass)
|
93
|
+
else
|
94
|
+
disabled_observers << klass
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
each_subclass_array do |array|
|
99
|
+
array.set_enablement(enabled, observers)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -1,73 +1,95 @@
|
|
1
1
|
require 'singleton'
|
2
|
+
require 'active_model/observer_array'
|
2
3
|
require 'active_support/core_ext/array/wrap'
|
3
4
|
require 'active_support/core_ext/module/aliasing'
|
4
5
|
require 'active_support/core_ext/module/remove_method'
|
5
6
|
require 'active_support/core_ext/string/inflections'
|
7
|
+
require 'active_support/core_ext/enumerable'
|
8
|
+
require 'active_support/descendants_tracker'
|
6
9
|
|
7
10
|
module ActiveModel
|
8
11
|
module Observing
|
9
12
|
extend ActiveSupport::Concern
|
10
13
|
|
14
|
+
included do
|
15
|
+
extend ActiveSupport::DescendantsTracker
|
16
|
+
end
|
17
|
+
|
11
18
|
module ClassMethods
|
12
19
|
# == Active Model Observers Activation
|
13
20
|
#
|
14
21
|
# Activates the observers assigned. Examples:
|
15
22
|
#
|
23
|
+
# class ORM
|
24
|
+
# include ActiveModel::Observing
|
25
|
+
# end
|
26
|
+
#
|
16
27
|
# # Calls PersonObserver.instance
|
17
|
-
#
|
28
|
+
# ORM.observers = :person_observer
|
18
29
|
#
|
19
30
|
# # Calls Cacher.instance and GarbageCollector.instance
|
20
|
-
#
|
31
|
+
# ORM.observers = :cacher, :garbage_collector
|
21
32
|
#
|
22
33
|
# # Same as above, just using explicit class references
|
23
|
-
#
|
34
|
+
# ORM.observers = Cacher, GarbageCollector
|
24
35
|
#
|
25
36
|
# Note: Setting this does not instantiate the observers yet.
|
26
37
|
# +instantiate_observers+ is called during startup, and before
|
27
38
|
# each development request.
|
28
39
|
def observers=(*values)
|
29
|
-
|
40
|
+
observers.replace(values.flatten)
|
30
41
|
end
|
31
42
|
|
32
43
|
# Gets the current observers.
|
33
44
|
def observers
|
34
|
-
@observers ||=
|
45
|
+
@observers ||= ObserverArray.new(self)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Gets the current observer instances.
|
49
|
+
def observer_instances
|
50
|
+
@observer_instances ||= []
|
35
51
|
end
|
36
52
|
|
37
|
-
# Instantiate the global
|
53
|
+
# Instantiate the global observers.
|
38
54
|
def instantiate_observers
|
39
55
|
observers.each { |o| instantiate_observer(o) }
|
40
56
|
end
|
41
57
|
|
58
|
+
# Add a new observer to the pool.
|
59
|
+
# The new observer needs to respond to 'update', otherwise it
|
60
|
+
# raises an +ArgumentError+ exception.
|
42
61
|
def add_observer(observer)
|
43
62
|
unless observer.respond_to? :update
|
44
63
|
raise ArgumentError, "observer needs to respond to `update'"
|
45
64
|
end
|
46
|
-
|
47
|
-
@observer_instances << observer
|
65
|
+
observer_instances << observer
|
48
66
|
end
|
49
67
|
|
68
|
+
# Notify list of observers of a change.
|
50
69
|
def notify_observers(*arg)
|
51
|
-
|
52
|
-
|
53
|
-
observer.update(*arg)
|
54
|
-
end
|
70
|
+
for observer in observer_instances
|
71
|
+
observer.update(*arg)
|
55
72
|
end
|
56
73
|
end
|
57
74
|
|
75
|
+
# Total number of observers.
|
58
76
|
def count_observers
|
59
|
-
|
77
|
+
observer_instances.size
|
60
78
|
end
|
61
79
|
|
62
80
|
protected
|
63
81
|
def instantiate_observer(observer) #:nodoc:
|
64
82
|
# string/symbol
|
65
83
|
if observer.respond_to?(:to_sym)
|
66
|
-
observer
|
84
|
+
observer.to_s.camelize.constantize.instance
|
67
85
|
elsif observer.respond_to?(:instance)
|
68
86
|
observer.instance
|
69
87
|
else
|
70
|
-
raise ArgumentError,
|
88
|
+
raise ArgumentError,
|
89
|
+
"#{observer} must be a lowercase, underscored class name (or an " +
|
90
|
+
"instance of the class itself) responding to the instance " +
|
91
|
+
"method. Example: Person.observers = :big_brother # calls " +
|
92
|
+
"BigBrother.instance"
|
71
93
|
end
|
72
94
|
end
|
73
95
|
|
@@ -124,8 +146,8 @@ module ActiveModel
|
|
124
146
|
# Observers will by default be mapped to the class with which they share a
|
125
147
|
# name. So CommentObserver will be tied to observing Comment, ProductManagerObserver
|
126
148
|
# to ProductManager, and so on. If you want to name your observer differently than
|
127
|
-
# the class you're interested in observing, you can use the Observer.observe
|
128
|
-
# method which takes either the concrete class (Product) or a symbol for that
|
149
|
+
# the class you're interested in observing, you can use the <tt>Observer.observe</tt>
|
150
|
+
# class method which takes either the concrete class (Product) or a symbol for that
|
129
151
|
# class (:product):
|
130
152
|
#
|
131
153
|
# class AuditObserver < ActiveModel::Observer
|
@@ -150,8 +172,13 @@ module ActiveModel
|
|
150
172
|
# The AuditObserver will now act on both updates to Account and Balance by treating
|
151
173
|
# them both as records.
|
152
174
|
#
|
175
|
+
# If you're using an Observer in a Rails application with Active Record, be sure to
|
176
|
+
# read about the necessary configuration in the documentation for
|
177
|
+
# ActiveRecord::Observer.
|
178
|
+
#
|
153
179
|
class Observer
|
154
180
|
include Singleton
|
181
|
+
extend ActiveSupport::DescendantsTracker
|
155
182
|
|
156
183
|
class << self
|
157
184
|
# Attaches the observer to the supplied model classes.
|
@@ -197,7 +224,9 @@ module ActiveModel
|
|
197
224
|
|
198
225
|
# Send observed_method(object) if the method exists.
|
199
226
|
def update(observed_method, object) #:nodoc:
|
200
|
-
|
227
|
+
return unless respond_to?(observed_method)
|
228
|
+
return if disabled_for?(object)
|
229
|
+
send(observed_method, object)
|
201
230
|
end
|
202
231
|
|
203
232
|
# Special method sent by the observed class when it is inherited.
|
@@ -211,5 +240,11 @@ module ActiveModel
|
|
211
240
|
def add_observer!(klass) #:nodoc:
|
212
241
|
klass.add_observer(self)
|
213
242
|
end
|
243
|
+
|
244
|
+
def disabled_for?(object)
|
245
|
+
klass = object.class
|
246
|
+
return false unless klass.respond_to?(:observers)
|
247
|
+
klass.observers.disabled_for?(self)
|
248
|
+
end
|
214
249
|
end
|
215
250
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module SecurePassword
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Adds methods to set and authenticate against a BCrypt password.
|
9
|
+
# This mechanism requires you to have a password_digest attribute.
|
10
|
+
#
|
11
|
+
# Validations for presence of password, confirmation of password (using
|
12
|
+
# a "password_confirmation" attribute) are automatically added.
|
13
|
+
# You can add more validations by hand if need be.
|
14
|
+
#
|
15
|
+
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
16
|
+
#
|
17
|
+
# # Schema: User(name:string, password_digest:string)
|
18
|
+
# class User < ActiveRecord::Base
|
19
|
+
# has_secure_password
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch")
|
23
|
+
# user.save # => false, password required
|
24
|
+
# user.password = "mUc3m00RsqyRe"
|
25
|
+
# user.save # => false, confirmation doesn't match
|
26
|
+
# user.password_confirmation = "mUc3m00RsqyRe"
|
27
|
+
# user.save # => true
|
28
|
+
# user.authenticate("notright") # => false
|
29
|
+
# user.authenticate("mUc3m00RsqyRe") # => user
|
30
|
+
# User.find_by_name("david").try(:authenticate, "notright") # => nil
|
31
|
+
# User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
|
32
|
+
def has_secure_password
|
33
|
+
attr_reader :password
|
34
|
+
|
35
|
+
validates_confirmation_of :password
|
36
|
+
validates_presence_of :password_digest
|
37
|
+
|
38
|
+
include InstanceMethodsOnActivation
|
39
|
+
|
40
|
+
if respond_to?(:attributes_protected_by_default)
|
41
|
+
def self.attributes_protected_by_default
|
42
|
+
super + ['password_digest']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module InstanceMethodsOnActivation
|
49
|
+
# Returns self if the password is correct, otherwise false.
|
50
|
+
def authenticate(unencrypted_password)
|
51
|
+
if BCrypt::Password.new(password_digest) == unencrypted_password
|
52
|
+
self
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Encrypts the password into the password_digest attribute.
|
59
|
+
def password=(unencrypted_password)
|
60
|
+
@password = unencrypted_password
|
61
|
+
unless unencrypted_password.blank?
|
62
|
+
self.password_digest = BCrypt::Password.create(unencrypted_password)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -15,7 +15,7 @@ module ActiveModel
|
|
15
15
|
# attr_accessor :name
|
16
16
|
#
|
17
17
|
# def attributes
|
18
|
-
#
|
18
|
+
# {'name' => name}
|
19
19
|
# end
|
20
20
|
#
|
21
21
|
# end
|
@@ -45,7 +45,7 @@ module ActiveModel
|
|
45
45
|
# attr_accessor :name
|
46
46
|
#
|
47
47
|
# def attributes
|
48
|
-
#
|
48
|
+
# {'name' => name}
|
49
49
|
# end
|
50
50
|
#
|
51
51
|
# end
|
@@ -79,15 +79,8 @@ module ActiveModel
|
|
79
79
|
attribute_names -= except
|
80
80
|
end
|
81
81
|
|
82
|
-
method_names = Array.wrap(options[:methods]).
|
83
|
-
|
84
|
-
methods
|
85
|
-
end
|
86
|
-
|
87
|
-
(attribute_names + method_names).inject({}) { |hash, name|
|
88
|
-
hash[name] = send(name)
|
89
|
-
hash
|
90
|
-
}
|
82
|
+
method_names = Array.wrap(options[:methods]).map { |n| n if respond_to?(n.to_s) }.compact
|
83
|
+
Hash[(attribute_names + method_names).map { |n| [n, send(n)] }]
|
91
84
|
end
|
92
85
|
end
|
93
86
|
end
|
@@ -18,58 +18,58 @@ module ActiveModel
|
|
18
18
|
# Returns a JSON string representing the model. Some configuration can be
|
19
19
|
# passed through +options+.
|
20
20
|
#
|
21
|
-
# The option <tt>
|
22
|
-
#
|
23
|
-
#
|
21
|
+
# The option <tt>include_root_in_json</tt> controls the top-level behavior
|
22
|
+
# of +as_json+. If true (the default) +as_json+ will emit a single root
|
23
|
+
# node named after the object's type. For example:
|
24
24
|
#
|
25
25
|
# konata = User.find(1)
|
26
|
-
# konata.
|
26
|
+
# konata.as_json
|
27
27
|
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
|
28
28
|
# "created_at": "2006/08/01", "awesome": true} }
|
29
29
|
#
|
30
30
|
# ActiveRecord::Base.include_root_in_json = false
|
31
|
-
# konata.
|
31
|
+
# konata.as_json
|
32
32
|
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
33
33
|
# "created_at": "2006/08/01", "awesome": true}
|
34
34
|
#
|
35
|
-
# The remainder of the examples in this section assume include_root_in_json
|
36
|
-
#
|
35
|
+
# The remainder of the examples in this section assume +include_root_in_json+
|
36
|
+
# is false.
|
37
37
|
#
|
38
|
-
# Without any +options+, the returned JSON string will include all
|
39
|
-
#
|
38
|
+
# Without any +options+, the returned JSON string will include all the model's
|
39
|
+
# attributes. For example:
|
40
40
|
#
|
41
41
|
# konata = User.find(1)
|
42
|
-
# konata.
|
42
|
+
# konata.as_json
|
43
43
|
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
44
44
|
# "created_at": "2006/08/01", "awesome": true}
|
45
45
|
#
|
46
46
|
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
|
47
47
|
# included, and work similar to the +attributes+ method. For example:
|
48
48
|
#
|
49
|
-
# konata.
|
49
|
+
# konata.as_json(:only => [ :id, :name ])
|
50
50
|
# # => {"id": 1, "name": "Konata Izumi"}
|
51
51
|
#
|
52
|
-
# konata.
|
52
|
+
# konata.as_json(:except => [ :id, :created_at, :age ])
|
53
53
|
# # => {"name": "Konata Izumi", "awesome": true}
|
54
54
|
#
|
55
|
-
# To include
|
55
|
+
# To include the result of some method calls on the model use <tt>:methods</tt>:
|
56
56
|
#
|
57
|
-
# konata.
|
57
|
+
# konata.as_json(:methods => :permalink)
|
58
58
|
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
59
59
|
# "created_at": "2006/08/01", "awesome": true,
|
60
60
|
# "permalink": "1-konata-izumi"}
|
61
61
|
#
|
62
|
-
# To include associations
|
62
|
+
# To include associations use <tt>:include</tt>:
|
63
63
|
#
|
64
|
-
# konata.
|
64
|
+
# konata.as_json(:include => :posts)
|
65
65
|
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
66
66
|
# "created_at": "2006/08/01", "awesome": true,
|
67
67
|
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
|
68
68
|
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
|
69
69
|
#
|
70
|
-
#
|
70
|
+
# Second level and higher order associations work as well:
|
71
71
|
#
|
72
|
-
# konata.
|
72
|
+
# konata.as_json(:include => { :posts => {
|
73
73
|
# :include => { :comments => {
|
74
74
|
# :only => :body } },
|
75
75
|
# :only => :title } })
|
@@ -33,7 +33,6 @@ module ActiveModel
|
|
33
33
|
protected
|
34
34
|
|
35
35
|
def compute_type
|
36
|
-
return if value.nil?
|
37
36
|
type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
|
38
37
|
type ||= :string if value.respond_to?(:to_str)
|
39
38
|
type ||= :yaml
|
@@ -78,10 +77,9 @@ module ActiveModel
|
|
78
77
|
end
|
79
78
|
|
80
79
|
def serializable_methods
|
81
|
-
Array.wrap(options[:methods]).
|
82
|
-
|
83
|
-
|
84
|
-
end
|
80
|
+
Array.wrap(options[:methods]).map do |name|
|
81
|
+
self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
|
82
|
+
end.compact
|
85
83
|
end
|
86
84
|
|
87
85
|
def serialize
|
@@ -136,6 +134,29 @@ module ActiveModel
|
|
136
134
|
|
137
135
|
# Returns XML representing the model. Configuration can be
|
138
136
|
# passed through +options+.
|
137
|
+
#
|
138
|
+
# Without any +options+, the returned XML string will include all the model's
|
139
|
+
# attributes. For example:
|
140
|
+
#
|
141
|
+
# konata = User.find(1)
|
142
|
+
# konata.to_xml
|
143
|
+
#
|
144
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
145
|
+
# <user>
|
146
|
+
# <id type="integer">1</id>
|
147
|
+
# <name>David</name>
|
148
|
+
# <age type="integer">16</age>
|
149
|
+
# <created-at type="datetime">2011-01-30T22:29:23Z</created-at>
|
150
|
+
# </user>
|
151
|
+
#
|
152
|
+
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
|
153
|
+
# included, and work similar to the +attributes+ method.
|
154
|
+
#
|
155
|
+
# To include the result of some method calls on the model use <tt>:methods</tt>.
|
156
|
+
#
|
157
|
+
# To include associations use <tt>:include</tt>.
|
158
|
+
#
|
159
|
+
# For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml.
|
139
160
|
def to_xml(options = {}, &block)
|
140
161
|
Serializer.new(self, options).serialize(&block)
|
141
162
|
end
|