aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG4
-rw-r--r--activemodel/activemodel.gemspec2
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb49
-rw-r--r--activemodel/lib/active_model/dirty.rb2
-rw-r--r--activemodel/lib/active_model/mass_assignment_security.rb117
-rw-r--r--activemodel/lib/active_model/naming.rb2
-rw-r--r--activemodel/lib/active_model/observer_array.rb147
-rw-r--r--activemodel/lib/active_model/observing.rb45
-rw-r--r--activemodel/lib/active_model/translation.rb4
-rw-r--r--activemodel/lib/active_model/validations.rb2
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb4
-rw-r--r--activemodel/lib/active_model/version.rb2
-rw-r--r--activemodel/test/cases/mass_assignment_security_test.rb39
-rw-r--r--activemodel/test/cases/observer_array_test.rb220
-rw-r--r--activemodel/test/cases/observing_test.rb5
-rw-r--r--activemodel/test/cases/secure_password_test.rb11
-rw-r--r--activemodel/test/cases/validations_test.rb14
-rw-r--r--activemodel/test/models/mass_assignment_specific.rb11
-rw-r--r--activemodel/test/models/observers.rb27
19 files changed, 634 insertions, 73 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG
index 03287fac7a..c26924cf09 100644
--- a/activemodel/CHANGELOG
+++ b/activemodel/CHANGELOG
@@ -1,5 +1,7 @@
*Rails 3.1.0 (unreleased)*
+* attr_accessible and friends now accepts :as as option to specify a role [Josh Kalderimis]
+
* Add support for proc or lambda as an option for InclusionValidator,
ExclusionValidator, and FormatValidator [Prem Sichanugrist]
@@ -13,6 +15,8 @@
* ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov]
+* Add support for selectively enabling/disabling observers [Myron Marston]
+
*Rails 3.0.2 (unreleased)*
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index 9f80673bb8..ce69c4a201 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -19,6 +19,6 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('builder', '~> 3.0.0')
- s.add_dependency('i18n', '~> 0.5.0')
+ s.add_dependency('i18n', '~> 0.6.0beta1')
s.add_dependency('bcrypt-ruby', '~> 2.1.4')
end
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index be55581c66..6ee5e04267 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -56,6 +56,8 @@ module ActiveModel
module AttributeMethods
extend ActiveSupport::Concern
+ COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
+
included do
class_attribute :attribute_method_matchers, :instance_writer => false
self.attribute_method_matchers = []
@@ -106,10 +108,13 @@ module ActiveModel
if block_given?
sing.send :define_method, name, &block
else
- if name =~ /^[a-zA-Z_]\w*[!?=]?$/
- sing.class_eval <<-eorb, __FILE__, __LINE__ + 1
- def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end
- eorb
+ # If we can compile the method name, do it. Otherwise use define_method.
+ # This is an important *optimization*, please don't change it. define_method
+ # has slower dispatch and consumes more memory.
+ if name =~ COMPILABLE_REGEXP
+ sing.class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end
+ RUBY
else
value = value.to_s if value
sing.send(:define_method, name) { value }
@@ -232,8 +237,19 @@ module ActiveModel
def alias_attribute(new_name, old_name)
attribute_method_matchers.each do |matcher|
- define_method(matcher.method_name(new_name)) do |*args|
- send(matcher.method_name(old_name), *args)
+ matcher_new = matcher.method_name(new_name).to_s
+ matcher_old = matcher.method_name(old_name).to_s
+
+ if matcher_new =~ COMPILABLE_REGEXP && matcher_old =~ COMPILABLE_REGEXP
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{matcher_new}(*args)
+ send(:#{matcher_old}, *args)
+ end
+ RUBY
+ else
+ define_method(matcher_new) do |*args|
+ send(matcher_old, *args)
+ end
end
end
end
@@ -276,14 +292,25 @@ module ActiveModel
else
method_name = matcher.method_name(attr_name)
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
if method_defined?('#{method_name}')
undef :'#{method_name}'
end
- define_method('#{method_name}') do |*args|
- send('#{matcher.method_missing_target}', '#{attr_name}', *args)
- end
- STR
+ RUBY
+
+ if method_name.to_s =~ COMPILABLE_REGEXP
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method_name}(*args)
+ send(:#{matcher.method_missing_target}, '#{attr_name}', *args)
+ end
+ RUBY
+ else
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ define_method('#{method_name}') do |*args|
+ send('#{matcher.method_missing_target}', '#{attr_name}', *args)
+ end
+ RUBY
+ end
end
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 5ede78617a..3b412d3dd7 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -93,7 +93,7 @@ module ActiveModel
attribute_method_affix :prefix => 'reset_', :suffix => '!'
end
- # Do any attributes have unsaved changes?
+ # Returns true if any attribute have unsaved changes, false otherwise.
# person.changed? # => false
# person.name = 'bob'
# person.changed? # => true
diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb
index be48415739..01eef762fd 100644
--- a/activemodel/lib/active_model/mass_assignment_security.rb
+++ b/activemodel/lib/active_model/mass_assignment_security.rb
@@ -24,10 +24,7 @@ module ActiveModel
# include ActiveModel::MassAssignmentSecurity
#
# attr_accessible :first_name, :last_name
- #
- # def self.admin_accessible_attributes
- # accessible_attributes + [ :plan_id ]
- # end
+ # attr_accessible :first_name, :last_name, :plan_id, :as => :admin
#
# def update
# ...
@@ -38,18 +35,17 @@ module ActiveModel
# protected
#
# def account_params
- # sanitize_for_mass_assignment(params[:account])
- # end
- #
- # def mass_assignment_authorizer
- # admin ? admin_accessible_attributes : super
+ # scope = admin ? :admin : :default
+ # sanitize_for_mass_assignment(params[:account], scope)
# end
#
# end
#
module ClassMethods
# Attributes named in this macro are protected from mass-assignment
- # whenever attributes are sanitized before assignment.
+ # whenever attributes are sanitized before assignment. A scope for the
+ # attributes is optional, if no scope is provided then :default is used.
+ # A scope can be defined by using the :as option.
#
# Mass-assignment to these attributes will simply be ignored, to assign
# to them you can use direct writer methods. This is meant to protect
@@ -60,36 +56,58 @@ module ActiveModel
# include ActiveModel::MassAssignmentSecurity
#
# attr_accessor :name, :credit_rating
- # attr_protected :credit_rating
#
- # def attributes=(values)
- # sanitize_for_mass_assignment(values).each do |k, v|
+ # attr_protected :credit_rating, :last_login
+ # attr_protected :last_login, :as => :admin
+ #
+ # def assign_attributes(values, options = {})
+ # sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
# send("#{k}=", v)
# end
# end
# end
#
+ # When using a :default scope :
+ #
# customer = Customer.new
- # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" }
+ # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
# customer.name # => "David"
# customer.credit_rating # => nil
+ # customer.last_login # => nil
#
# customer.credit_rating = "Average"
# customer.credit_rating # => "Average"
#
+ # And using the :admin scope :
+ #
+ # customer = Customer.new
+ # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
+ # customer.name # => "David"
+ # customer.credit_rating # => "Excellent"
+ # customer.last_login # => nil
+ #
# To start from an all-closed default and enable attributes as needed,
# have a look at +attr_accessible+.
#
# Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
# to sanitize attributes won't provide sufficient protection.
- def attr_protected(*names)
- self._protected_attributes = self.protected_attributes + names
+ def attr_protected(*args)
+ options = args.extract_options!
+ scope = options[:as] || :default
+
+ self._protected_attributes = protected_attributes_configs.dup
+ self._protected_attributes[scope] = self.protected_attributes(scope) + args
+
self._active_authorizer = self._protected_attributes
end
# Specifies a white list of model attributes that can be set via
# mass-assignment.
#
+ # Like +attr_protected+, a scope for the attributes is optional,
+ # if no scope is provided then :default is used. A scope can be defined by
+ # using the :as option.
+ #
# This is the opposite of the +attr_protected+ macro: Mass-assignment
# will only set attributes in this list, to assign to the rest of
# attributes you can use direct writer methods. This is meant to protect
@@ -102,57 +120,90 @@ module ActiveModel
# include ActiveModel::MassAssignmentSecurity
#
# attr_accessor :name, :credit_rating
+ #
# attr_accessible :name
+ # attr_accessible :name, :credit_rating, :as => :admin
#
- # def attributes=(values)
- # sanitize_for_mass_assignment(values).each do |k, v|
+ # def assign_attributes(values, options = {})
+ # sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
# send("#{k}=", v)
# end
# end
# end
#
+ # When using a :default scope :
+ #
# customer = Customer.new
- # customer.attributes = { :name => "David", :credit_rating => "Excellent" }
+ # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
# customer.name # => "David"
# customer.credit_rating # => nil
#
# customer.credit_rating = "Average"
# customer.credit_rating # => "Average"
#
+ # And using the :admin scope :
+ #
+ # customer = Customer.new
+ # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
+ # customer.name # => "David"
+ # customer.credit_rating # => "Excellent"
+ #
# Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
# to sanitize attributes won't provide sufficient protection.
- def attr_accessible(*names)
- self._accessible_attributes = self.accessible_attributes + names
+ def attr_accessible(*args)
+ options = args.extract_options!
+ scope = options[:as] || :default
+
+ self._accessible_attributes = accessible_attributes_configs.dup
+ self._accessible_attributes[scope] = self.accessible_attributes(scope) + args
+
self._active_authorizer = self._accessible_attributes
end
- def protected_attributes
- self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w|
- w.logger = self.logger if self.respond_to?(:logger)
- end
+ def protected_attributes(scope = :default)
+ protected_attributes_configs[scope]
end
- def accessible_attributes
- self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
+ def accessible_attributes(scope = :default)
+ accessible_attributes_configs[scope]
end
- def active_authorizer
- self._active_authorizer ||= protected_attributes
+ def active_authorizers
+ self._active_authorizer ||= protected_attributes_configs
end
+ alias active_authorizer active_authorizers
def attributes_protected_by_default
[]
end
+
+ private
+
+ def protected_attributes_configs
+ self._protected_attributes ||= begin
+ default_black_list = BlackList.new(attributes_protected_by_default).tap do |w|
+ w.logger = self.logger if self.respond_to?(:logger)
+ end
+ Hash.new(default_black_list)
+ end
+ end
+
+ def accessible_attributes_configs
+ self._accessible_attributes ||= begin
+ default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
+ Hash.new(default_white_list)
+ end
+ end
end
protected
- def sanitize_for_mass_assignment(attributes)
- mass_assignment_authorizer.sanitize(attributes)
+ def sanitize_for_mass_assignment(attributes, scope = :default)
+ mass_assignment_authorizer(scope).sanitize(attributes)
end
- def mass_assignment_authorizer
- self.class.active_authorizer
+ def mass_assignment_authorizer(scope = :default)
+ self.class.active_authorizer[scope]
end
end
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 315ccd40e9..74708692af 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -68,7 +68,7 @@ module ActiveModel
# BookModule::BookCover.model_name.i18n_key # => "book_module.book_cover"
#
# Providing the functionality that ActiveModel::Naming provides in your object
- # is required to pass the Active Model Lint test. So either extending the provided
+ # is required to pass the Active Model Lint test. So either extending the provided
# method below, or rolling your own is required.
module Naming
# Returns an ActiveModel::Name object for module. It can be
diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb
new file mode 100644
index 0000000000..5fb73f1c78
--- /dev/null
+++ b/activemodel/lib/active_model/observer_array.rb
@@ -0,0 +1,147 @@
+require 'set'
+
+module ActiveModel
+ # Stores the enabled/disabled state of individual observers for
+ # a particular model class.
+ class ObserverArray < Array
+ attr_reader :model_class
+ def initialize(model_class, *args)
+ @model_class = model_class
+ super(*args)
+ end
+
+ # Returns true if the given observer is disabled for the model class.
+ def disabled_for?(observer)
+ disabled_observers.include?(observer.class)
+ end
+
+ # Disables one or more observers. This supports multiple forms:
+ #
+ # ORM.observers.disable :user_observer
+ # # => disables the UserObserver
+ #
+ # User.observers.disable AuditTrail
+ # # => disables the AuditTrail observer for User notifications.
+ # # Other models will still notify the AuditTrail observer.
+ #
+ # ORM.observers.disable :observer_1, :observer_2
+ # # => disables Observer1 and Observer2 for all models.
+ #
+ # ORM.observers.disable :all
+ # # => disables all observers for all models.
+ #
+ # User.observers.disable :all do
+ # # all user observers are disabled for
+ # # just the duration of the block
+ # end
+ def disable(*observers, &block)
+ set_enablement(false, observers, &block)
+ end
+
+ # Enables one or more observers. This supports multiple forms:
+ #
+ # ORM.observers.enable :user_observer
+ # # => enables the UserObserver
+ #
+ # User.observers.enable AuditTrail
+ # # => enables the AuditTrail observer for User notifications.
+ # # Other models will not be affected (i.e. they will not
+ # # trigger notifications to AuditTrail if previously disabled)
+ #
+ # ORM.observers.enable :observer_1, :observer_2
+ # # => enables Observer1 and Observer2 for all models.
+ #
+ # ORM.observers.enable :all
+ # # => enables all observers for all models.
+ #
+ # User.observers.enable :all do
+ # # all user observers are enabled for
+ # # just the duration of the block
+ # end
+ #
+ # Note: all observers are enabled by default. This method is only
+ # useful when you have previously disabled one or more observers.
+ def enable(*observers, &block)
+ set_enablement(true, observers, &block)
+ end
+
+ protected
+
+ def disabled_observers
+ @disabled_observers ||= Set.new
+ end
+
+ def observer_class_for(observer)
+ return observer if observer.is_a?(Class)
+
+ if observer.respond_to?(:to_sym) # string/symbol
+ observer.to_s.camelize.constantize
+ else
+ raise ArgumentError, "#{observer} was not a class or a " +
+ "lowercase, underscored class name as expected."
+ end
+ end
+
+ def start_transaction
+ disabled_observer_stack.push(disabled_observers.dup)
+ each_subclass_array do |array|
+ array.start_transaction
+ end
+ end
+
+ def disabled_observer_stack
+ @disabled_observer_stack ||= []
+ end
+
+ def end_transaction
+ @disabled_observers = disabled_observer_stack.pop
+ each_subclass_array do |array|
+ array.end_transaction
+ end
+ end
+
+ def transaction
+ start_transaction
+
+ begin
+ yield
+ ensure
+ end_transaction
+ end
+ end
+
+ def each_subclass_array
+ model_class.descendants.each do |subclass|
+ yield subclass.observers
+ end
+ end
+
+ def set_enablement(enabled, observers)
+ if block_given?
+ transaction do
+ set_enablement(enabled, observers)
+ yield
+ end
+ else
+ observers = ActiveModel::Observer.descendants if observers == [:all]
+ observers.each do |obs|
+ klass = observer_class_for(obs)
+
+ unless klass < ActiveModel::Observer
+ raise ArgumentError.new("#{obs} does not refer to a valid observer")
+ end
+
+ if enabled
+ disabled_observers.delete(klass)
+ else
+ disabled_observers << klass
+ end
+ end
+
+ each_subclass_array do |array|
+ array.set_enablement(enabled, observers)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb
index ef36f80bec..4682ae07ef 100644
--- a/activemodel/lib/active_model/observing.rb
+++ b/activemodel/lib/active_model/observing.rb
@@ -1,13 +1,20 @@
require 'singleton'
+require 'active_model/observer_array'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/enumerable'
+require 'active_support/descendants_tracker'
module ActiveModel
module Observing
extend ActiveSupport::Concern
+ included do
+ extend ActiveSupport::DescendantsTracker
+ end
+
module ClassMethods
# == Active Model Observers Activation
#
@@ -30,12 +37,16 @@ module ActiveModel
# +instantiate_observers+ is called during startup, and before
# each development request.
def observers=(*values)
- @observers = values.flatten
+ observers.replace(values.flatten)
end
- # Gets the current observers.
+ # Gets an array of observers observing this model.
+ # The array also provides +enable+ and +disable+ methods
+ # that allow you to selectively enable and disable observers.
+ # (see <tt>ActiveModel::ObserverArray.enable</tt> and
+ # <tt>ActiveModel::ObserverArray.disable</tt> for more on this)
def observers
- @observers ||= []
+ @observers ||= ObserverArray.new(self)
end
# Gets the current observer instances.
@@ -43,12 +54,14 @@ module ActiveModel
@observer_instances ||= []
end
- # Instantiate the global Active Record observers.
+ # Instantiate the global observers.
def instantiate_observers
observers.each { |o| instantiate_observer(o) }
end
# Add a new observer to the pool.
+ # The new observer needs to respond to 'update', otherwise it
+ # raises an +ArgumentError+ exception.
def add_observer(observer)
unless observer.respond_to? :update
raise ArgumentError, "observer needs to respond to `update'"
@@ -76,7 +89,11 @@ module ActiveModel
elsif observer.respond_to?(:instance)
observer.instance
else
- 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"
+ 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"
end
end
@@ -133,8 +150,8 @@ module ActiveModel
# Observers will by default be mapped to the class with which they share a
# name. So CommentObserver will be tied to observing Comment, ProductManagerObserver
# to ProductManager, and so on. If you want to name your observer differently than
- # the class you're interested in observing, you can use the Observer.observe class
- # method which takes either the concrete class (Product) or a symbol for that
+ # the class you're interested in observing, you can use the <tt>Observer.observe</tt>
+ # class method which takes either the concrete class (Product) or a symbol for that
# class (:product):
#
# class AuditObserver < ActiveModel::Observer
@@ -165,6 +182,7 @@ module ActiveModel
#
class Observer
include Singleton
+ extend ActiveSupport::DescendantsTracker
class << self
# Attaches the observer to the supplied model classes.
@@ -208,9 +226,12 @@ module ActiveModel
self.class.observed_classes
end
- # Send observed_method(object) if the method exists.
+ # Send observed_method(object) if the method exists and
+ # the observer is enabled for the given object's class.
def update(observed_method, object) #:nodoc:
- send(observed_method, object) if respond_to?(observed_method)
+ return unless respond_to?(observed_method)
+ return if disabled_for?(object)
+ send(observed_method, object)
end
# Special method sent by the observed class when it is inherited.
@@ -224,5 +245,11 @@ module ActiveModel
def add_observer!(klass) #:nodoc:
klass.add_observer(self)
end
+
+ def disabled_for?(object)
+ klass = object.class
+ return false unless klass.respond_to?(:observers)
+ klass.observers.disabled_for?(self)
+ end
end
end
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index 920a133159..6d64c81b5f 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -18,12 +18,12 @@ module ActiveModel
#
# This also provides the required class methods for hooking into the
# Rails internationalization API, including being able to define a
- # class based i18n_scope and lookup_ancestors to find translations in
+ # class based +i18n_scope+ and +lookup_ancestors+ to find translations in
# parent classes.
module Translation
include ActiveModel::Naming
- # Returns the i18n_scope for the class. Overwrite if you want custom lookup.
+ # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
def i18n_scope
:activemodel
end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index d968609e67..5e567307f3 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -133,7 +133,7 @@ module ActiveModel
if options.key?(:on)
options = options.dup
options[:if] = Array.wrap(options[:if])
- options[:if] << "validation_context == :#{options[:on]}"
+ options[:if].unshift("validation_context == :#{options[:on]}")
end
args << options
set_callback(:validate, *args, &block)
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index adc2867ad0..22a77320dc 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -31,7 +31,7 @@ module ActiveModel
options = args.last
if options.is_a?(Hash) && options[:on]
options[:if] = Array.wrap(options[:if])
- options[:if] << "self.validation_context == :#{options[:on]}"
+ options[:if].unshift("self.validation_context == :#{options[:on]}")
end
set_callback(:validation, :before, *args, &block)
end
@@ -41,7 +41,7 @@ module ActiveModel
options[:prepend] = true
options[:if] = Array.wrap(options[:if])
options[:if] << "!halted"
- options[:if] << "self.validation_context == :#{options[:on]}" if options[:on]
+ options[:if].unshift("self.validation_context == :#{options[:on]}") if options[:on]
set_callback(:validation, :after, *(args << options), &block)
end
end
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index 23ba42bf35..68c138da84 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -3,7 +3,7 @@ module ActiveModel
MAJOR = 3
MINOR = 1
TINY = 0
- PRE = "beta"
+ PRE = "beta1"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb
index f84e55e8d9..b22ce874ea 100644
--- a/activemodel/test/cases/mass_assignment_security_test.rb
+++ b/activemodel/test/cases/mass_assignment_security_test.rb
@@ -10,10 +10,27 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase
assert_equal expected, sanitized
end
+ def test_only_moderator_scope_attribute_accessible
+ user = SpecialUser.new
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator)
+ assert_equal expected, sanitized
+
+ sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true })
+ assert_equal({}, sanitized)
+ end
+
def test_attributes_accessible
user = Person.new
expected = { "name" => "John Smith", "email" => "john@smith.com" }
- sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true))
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
+ assert_equal expected, sanitized
+ end
+
+ def test_admin_scoped_attributes_accessible
+ user = Person.new
+ expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true }
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin)
assert_equal expected, sanitized
end
@@ -26,20 +43,30 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase
def test_mass_assignment_protection_inheritance
assert_blank LoosePerson.accessible_attributes
- assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes
+ assert_equal Set.new(['credit_rating', 'administrator']), LoosePerson.protected_attributes
+
+ assert_blank LoosePerson.accessible_attributes
+ assert_equal Set.new(['credit_rating']), LoosePerson.protected_attributes(:admin)
assert_blank LooseDescendant.accessible_attributes
- assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
+ assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
assert_blank LooseDescendantSecond.accessible_attributes
- assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
+ assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
'Running attr_protected twice in one class should merge the protections'
assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default
- assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
+ assert_equal Set.new(['name', 'address']), TightPerson.accessible_attributes
+
+ assert_blank TightPerson.protected_attributes(:admin) - TightPerson.attributes_protected_by_default
+ assert_equal Set.new(['name', 'address', 'admin']), TightPerson.accessible_attributes(:admin)
assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default
- assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
+ assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes
+
+ assert_blank TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default
+ assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin)
+
end
def test_mass_assignment_multiparameter_protector
diff --git a/activemodel/test/cases/observer_array_test.rb b/activemodel/test/cases/observer_array_test.rb
new file mode 100644
index 0000000000..fc5f18008b
--- /dev/null
+++ b/activemodel/test/cases/observer_array_test.rb
@@ -0,0 +1,220 @@
+require 'cases/helper'
+require 'models/observers'
+
+class ObserverArrayTest < ActiveModel::TestCase
+ def teardown
+ ORM.observers.enable :all
+ Budget.observers.enable :all
+ Widget.observers.enable :all
+ end
+
+ def assert_observer_notified(model_class, observer_class)
+ observer_class.instance.before_save_invocations.clear
+ model_instance = model_class.new
+ model_instance.save
+ assert_equal [model_instance], observer_class.instance.before_save_invocations
+ end
+
+ def assert_observer_not_notified(model_class, observer_class)
+ observer_class.instance.before_save_invocations.clear
+ model_instance = model_class.new
+ model_instance.save
+ assert_equal [], observer_class.instance.before_save_invocations
+ end
+
+ test "all observers are enabled by default" do
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable individual observers using a class constant" do
+ ORM.observers.disable WidgetObserver
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can enable individual observers using a class constant" do
+ ORM.observers.disable :all
+ ORM.observers.enable AuditTrail
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable individual observers using a symbol" do
+ ORM.observers.disable :budget_observer
+
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can enable individual observers using a symbol" do
+ ORM.observers.disable :all
+ ORM.observers.enable :audit_trail
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable multiple observers at a time" do
+ ORM.observers.disable :widget_observer, :budget_observer
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can enable multiple observers at a time" do
+ ORM.observers.disable :all
+ ORM.observers.enable :widget_observer, :budget_observer
+
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_not_notified Budget, AuditTrail
+ end
+
+ test "can disable all observers using :all" do
+ ORM.observers.disable :all
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_not_notified Budget, AuditTrail
+ end
+
+ test "can enable all observers using :all" do
+ ORM.observers.disable :all
+ ORM.observers.enable :all
+
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable observers on individual models without affecting those observers on other models" do
+ Widget.observers.disable :all
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can enable observers on individual models without affecting those observers on other models" do
+ ORM.observers.disable :all
+ Budget.observers.enable AuditTrail
+
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can disable observers for the duration of a block" do
+ yielded = false
+ ORM.observers.disable :budget_observer do
+ yielded = true
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_not_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ assert yielded
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "can enable observers for the duration of a block" do
+ yielded = false
+ Widget.observers.disable :all
+
+ Widget.observers.enable :all do
+ yielded = true
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ assert yielded
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "raises an appropriate error when a developer accidentally enables or disables the wrong class (i.e. Widget instead of WidgetObserver)" do
+ assert_raise ArgumentError do
+ ORM.observers.enable :widget
+ end
+
+ assert_raise ArgumentError do
+ ORM.observers.enable Widget
+ end
+
+ assert_raise ArgumentError do
+ ORM.observers.disable :widget
+ end
+
+ assert_raise ArgumentError do
+ ORM.observers.disable Widget
+ end
+ end
+
+ test "allows #enable at the superclass level to override #disable at the subclass level when called last" do
+ Widget.observers.disable :all
+ ORM.observers.enable :all
+
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ test "allows #disable at the superclass level to override #enable at the subclass level when called last" do
+ Budget.observers.enable :audit_trail
+ ORM.observers.disable :audit_trail
+
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_not_notified Budget, AuditTrail
+ end
+
+ test "can use the block form at different levels of the hierarchy" do
+ yielded = false
+ Widget.observers.disable :all
+
+ ORM.observers.enable :all do
+ yielded = true
+ assert_observer_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+
+ assert yielded
+ assert_observer_not_notified Widget, WidgetObserver
+ assert_observer_notified Budget, BudgetObserver
+ assert_observer_not_notified Widget, AuditTrail
+ assert_observer_notified Budget, AuditTrail
+ end
+end
+
diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb
index 63686843b6..99b1f407ae 100644
--- a/activemodel/test/cases/observing_test.rb
+++ b/activemodel/test/cases/observing_test.rb
@@ -43,6 +43,11 @@ class ObservingTest < ActiveModel::TestCase
assert ObservedModel.observers.include?(:bar), ":bar not in #{ObservedModel.observers.inspect}"
end
+ test "uses an ObserverArray so observers can be disabled" do
+ ObservedModel.observers = [:foo, :bar]
+ assert ObservedModel.observers.is_a?(ActiveModel::ObserverArray)
+ end
+
test "instantiates observer names passed as strings" do
ObservedModel.observers << 'foo_observer'
FooObserver.expects(:instance)
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index c455cf57b3..6950c3be1f 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -45,13 +45,14 @@ class SecurePasswordTest < ActiveModel::TestCase
end
test "visitor#password_digest should be protected against mass assignment" do
- assert Visitor.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::BlackList)
- assert Visitor.active_authorizer.include?(:password_digest)
+ assert Visitor.active_authorizers[:default].kind_of?(ActiveModel::MassAssignmentSecurity::BlackList)
+ assert Visitor.active_authorizers[:default].include?(:password_digest)
end
test "Administrator's mass_assignment_authorizer should be WhiteList" do
- assert Administrator.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList)
- assert !Administrator.active_authorizer.include?(:password_digest)
- assert Administrator.active_authorizer.include?(:name)
+ active_authorizer = Administrator.active_authorizers[:default]
+ assert active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList)
+ assert !active_authorizer.include?(:password_digest)
+ assert active_authorizer.include?(:name)
end
end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 2f36195627..0b50acf913 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -212,6 +212,20 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal 'is too short (minimum is 2 characters)', t.errors[key][0]
end
+ def test_validaton_with_if_and_on
+ Topic.validates_presence_of :title, :if => Proc.new{|x| x.author_name = "bad"; true }, :on => :update
+
+ t = Topic.new(:title => "")
+
+ # If block should not fire
+ assert t.valid?
+ assert t.author_name.nil?
+
+ # If block should fire
+ assert t.invalid?(:update)
+ assert t.author_name == "bad"
+ end
+
def test_invalid_should_be_the_opposite_of_valid
Topic.validates_presence_of :title
diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb
index 2a8fe170c2..53b37369ff 100644
--- a/activemodel/test/models/mass_assignment_specific.rb
+++ b/activemodel/test/models/mass_assignment_specific.rb
@@ -5,9 +5,17 @@ class User
public :sanitize_for_mass_assignment
end
+class SpecialUser
+ include ActiveModel::MassAssignmentSecurity
+ attr_accessible :name, :email, :as => :moderator
+
+ public :sanitize_for_mass_assignment
+end
+
class Person
include ActiveModel::MassAssignmentSecurity
attr_accessible :name, :email
+ attr_accessible :name, :email, :admin, :as => :admin
public :sanitize_for_mass_assignment
end
@@ -32,6 +40,7 @@ end
class LoosePerson
include ActiveModel::MassAssignmentSecurity
attr_protected :credit_rating, :administrator
+ attr_protected :credit_rating, :as => :admin
end
class LooseDescendant < LoosePerson
@@ -46,6 +55,7 @@ end
class TightPerson
include ActiveModel::MassAssignmentSecurity
attr_accessible :name, :address
+ attr_accessible :name, :address, :admin, :as => :admin
def self.attributes_protected_by_default
["mobile_number"]
@@ -54,4 +64,5 @@ end
class TightDescendant < TightPerson
attr_accessible :phone_number
+ attr_accessible :super_powers, :as => :admin
end \ No newline at end of file
diff --git a/activemodel/test/models/observers.rb b/activemodel/test/models/observers.rb
new file mode 100644
index 0000000000..3729b3435e
--- /dev/null
+++ b/activemodel/test/models/observers.rb
@@ -0,0 +1,27 @@
+class ORM
+ include ActiveModel::Observing
+
+ def save
+ notify_observers :before_save
+ end
+
+ class Observer < ActiveModel::Observer
+ def before_save_invocations
+ @before_save_invocations ||= []
+ end
+
+ def before_save(record)
+ before_save_invocations << record
+ end
+ end
+end
+
+class Widget < ORM; end
+class Budget < ORM; end
+class WidgetObserver < ORM::Observer; end
+class BudgetObserver < ORM::Observer; end
+class AuditTrail < ORM::Observer
+ observe :widget, :budget
+end
+
+ORM.instantiate_observers