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.
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