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.
Files changed (36) hide show
  1. data/CHANGELOG +17 -73
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +5 -5
  4. data/lib/active_model.rb +2 -2
  5. data/lib/active_model/attribute_methods.rb +46 -53
  6. data/lib/active_model/callbacks.rb +2 -5
  7. data/lib/active_model/conversion.rb +0 -2
  8. data/lib/active_model/dirty.rb +3 -4
  9. data/lib/active_model/errors.rb +55 -56
  10. data/lib/active_model/lint.rb +2 -2
  11. data/lib/active_model/mass_assignment_security.rb +96 -47
  12. data/lib/active_model/naming.rb +55 -13
  13. data/lib/active_model/observer_array.rb +104 -0
  14. data/lib/active_model/observing.rb +53 -18
  15. data/lib/active_model/secure_password.rb +67 -0
  16. data/lib/active_model/serialization.rb +4 -11
  17. data/lib/active_model/serializers/json.rb +18 -18
  18. data/lib/active_model/serializers/xml.rb +26 -5
  19. data/lib/active_model/translation.rb +4 -11
  20. data/lib/active_model/validations.rb +23 -23
  21. data/lib/active_model/validations/acceptance.rb +3 -5
  22. data/lib/active_model/validations/callbacks.rb +5 -19
  23. data/lib/active_model/validations/confirmation.rb +6 -5
  24. data/lib/active_model/validations/exclusion.rb +27 -3
  25. data/lib/active_model/validations/format.rb +38 -12
  26. data/lib/active_model/validations/inclusion.rb +30 -23
  27. data/lib/active_model/validations/length.rb +3 -1
  28. data/lib/active_model/validations/numericality.rb +4 -2
  29. data/lib/active_model/validations/presence.rb +3 -2
  30. data/lib/active_model/validations/validates.rb +23 -9
  31. data/lib/active_model/validations/with.rb +14 -2
  32. data/lib/active_model/validator.rb +16 -18
  33. data/lib/active_model/version.rb +3 -3
  34. metadata +71 -58
  35. checksums.yaml +0 -7
  36. 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
- # ActiveRecord::Base.observers = :person_observer
28
+ # ORM.observers = :person_observer
18
29
  #
19
30
  # # Calls Cacher.instance and GarbageCollector.instance
20
- # ActiveRecord::Base.observers = :cacher, :garbage_collector
31
+ # ORM.observers = :cacher, :garbage_collector
21
32
  #
22
33
  # # Same as above, just using explicit class references
23
- # ActiveRecord::Base.observers = Cacher, GarbageCollector
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
- @observers = values.flatten
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 Active Record observers.
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
- @observer_instances ||= []
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
- if defined? @observer_instances
52
- for observer in @observer_instances
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
- @observer_instances.size
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 = observer.to_s.camelize.constantize.instance
84
+ observer.to_s.camelize.constantize.instance
67
85
  elsif observer.respond_to?(:instance)
68
86
  observer.instance
69
87
  else
70
- raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
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 class
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
- send(observed_method, object) if respond_to?(observed_method)
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
- # @attributes ||= {'name' => 'nil'}
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
- # @attributes ||= {'name' => 'nil'}
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]).inject([]) do |methods, name|
83
- methods << name if respond_to?(name.to_s)
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>ActiveModel::Base.include_root_in_json</tt> controls the
22
- # top-level behavior of <tt>to_json</tt>. It is <tt>true</tt> by default. When it is <tt>true</tt>,
23
- # <tt>to_json</tt> will emit a single root node named after the object's type. For example:
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.to_json
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.to_json
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 is set to
36
- # <tt>false</tt>.
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
- # the model's attributes. For example:
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.to_json
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.to_json(:only => [ :id, :name ])
49
+ # konata.as_json(:only => [ :id, :name ])
50
50
  # # => {"id": 1, "name": "Konata Izumi"}
51
51
  #
52
- # konata.to_json(:except => [ :id, :created_at, :age ])
52
+ # konata.as_json(:except => [ :id, :created_at, :age ])
53
53
  # # => {"name": "Konata Izumi", "awesome": true}
54
54
  #
55
- # To include any methods on the model, use <tt>:methods</tt>.
55
+ # To include the result of some method calls on the model use <tt>:methods</tt>:
56
56
  #
57
- # konata.to_json(:methods => :permalink)
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, use <tt>:include</tt>.
62
+ # To include associations use <tt>:include</tt>:
63
63
  #
64
- # konata.to_json(:include => :posts)
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
- # 2nd level and higher order associations work as well:
70
+ # Second level and higher order associations work as well:
71
71
  #
72
- # konata.to_json(:include => { :posts => {
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]).inject([]) do |methods, name|
82
- methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
83
- methods
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