aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model/mass_assignment_security.rb117
-rw-r--r--activemodel/lib/active_model/observer_array.rb104
-rw-r--r--activemodel/lib/active_model/observing.rb28
-rw-r--r--activemodel/lib/active_model/validations.rb2
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb4
5 files changed, 215 insertions, 40 deletions
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/observer_array.rb b/activemodel/lib/active_model/observer_array.rb
new file mode 100644
index 0000000000..ab7f86007f
--- /dev/null
+++ b/activemodel/lib/active_model/observer_array.rb
@@ -0,0 +1,104 @@
+require 'set'
+
+module ActiveModel
+ # Stores the enabled/disabled state of individual observers for
+ # a particular model classes.
+ class ObserverArray < Array
+ attr_reader :model_class
+ def initialize(model_class, *args)
+ @model_class = model_class
+ super(*args)
+ end
+
+ def disabled_for?(observer)
+ disabled_observers.include?(observer.class)
+ end
+
+ def disable(*observers, &block)
+ set_enablement(false, observers, &block)
+ end
+
+ 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 3c80d584fe..c1ac4eb4af 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,12 @@ 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.
def observers
- @observers ||= []
+ @observers ||= ObserverArray.new(self)
end
# Gets the current observer instances.
@@ -76,7 +83,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
@@ -165,6 +176,7 @@ module ActiveModel
#
class Observer
include Singleton
+ extend ActiveSupport::DescendantsTracker
class << self
# Attaches the observer to the supplied model classes.
@@ -210,7 +222,9 @@ module ActiveModel
# Send observed_method(object) if the method exists.
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 +238,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/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