activemodel 3.0.20 → 3.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|