aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib/active_model')
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb54
-rw-r--r--activemodel/lib/active_model/dirty.rb2
-rw-r--r--activemodel/lib/active_model/errors.rb25
-rw-r--r--activemodel/lib/active_model/lint.rb4
-rw-r--r--activemodel/lib/active_model/mass_assignment_security.rb117
-rw-r--r--activemodel/lib/active_model/naming.rb15
-rw-r--r--activemodel/lib/active_model/observer_array.rb98
-rw-r--r--activemodel/lib/active_model/observing.rb35
-rw-r--r--activemodel/lib/active_model/secure_password.rb7
-rw-r--r--activemodel/lib/active_model/translation.rb2
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb27
-rw-r--r--activemodel/lib/active_model/validations/format.rb46
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb26
-rw-r--r--activemodel/lib/active_model/validations/length.rb3
-rw-r--r--activemodel/lib/active_model/validator.rb7
15 files changed, 368 insertions, 100 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 2a99450a3d..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,11 +108,17 @@ module ActiveModel
if block_given?
sing.send :define_method, name, &block
else
- # use eval instead of a block to work around a memory leak in dev
- # mode in fcgi
- 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 }
+ end
end
end
@@ -229,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
@@ -273,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 a479795d51..5ede78617a 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -8,7 +8,7 @@ module ActiveModel
# Provides a way to track changes in your object in the same way as
# Active Record does.
#
- # The requirements to implement ActiveModel::Dirty are to:
+ # The requirements for implementing ActiveModel::Dirty are:
#
# * <tt>include ActiveModel::Dirty</tt> in your object
# * Call <tt>define_attribute_methods</tt> passing each method you want to
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index c2f0228785..22ca3efa2b 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -278,25 +278,24 @@ module ActiveModel
# When using inheritance in your models, it will check all the inherited
# models too, but only if the model itself hasn't been found. Say you have
# <tt>class Admin < User; end</tt> and you wanted the translation for
- # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+,
+ # the <tt>:blank</tt> error message for the <tt>title</tt> attribute,
# it looks for these translations:
#
- # <ol>
- # <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li>
- # <li><tt>activemodel.errors.models.admin.blank</tt></li>
- # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li>
- # <li><tt>activemodel.errors.models.user.blank</tt></li>
- # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
- # <li><tt>activemodel.errors.messages.blank</tt></li>
- # <li><tt>errors.attributes.title.blank</tt></li>
- # <li><tt>errors.messages.blank</tt></li>
- # </ol>
+ # * <tt>activemodel.errors.models.admin.attributes.title.blank</tt>
+ # * <tt>activemodel.errors.models.admin.blank</tt>
+ # * <tt>activemodel.errors.models.user.attributes.title.blank</tt>
+ # * <tt>activemodel.errors.models.user.blank</tt>
+ # * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope)
+ # * <tt>activemodel.errors.messages.blank</tt>
+ # * <tt>errors.attributes.title.blank</tt>
+ # * <tt>errors.messages.blank</tt>
+ #
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)
defaults = @base.class.lookup_ancestors.map do |klass|
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}",
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ]
+ [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
+ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
defaults << options.delete(:message)
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 957d1b9d70..b71ef4b22e 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -23,7 +23,7 @@ module ActiveModel
def test_to_key
assert model.respond_to?(:to_key), "The model should respond to to_key"
def model.persisted?() false end
- assert model.to_key.nil?
+ assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
end
# == Responds to <tt>to_param</tt>
@@ -40,7 +40,7 @@ module ActiveModel
assert model.respond_to?(:to_param), "The model should respond to to_param"
def model.to_key() [1] end
def model.persisted?() false end
- assert model.to_param.nil?
+ assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
end
# == Responds to <tt>valid?</tt>
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 eb9b847509..315ccd40e9 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/module/introspection'
module ActiveModel
class Name < String
- attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key
+ attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key
alias_method :cache_key, :collection
def initialize(klass, namespace = nil)
@@ -20,6 +20,7 @@ module ActiveModel
@partial_path = "#{@collection}/#{@element}".freeze
@param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze
@route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze
+ @i18n_key = self.underscore.to_sym
end
# Transform the model name into a more humane format, using I18n. By default,
@@ -33,7 +34,7 @@ module ActiveModel
@klass.respond_to?(:i18n_scope)
defaults = @klass.lookup_ancestors.map do |klass|
- klass.model_name.underscore.to_sym
+ klass.model_name.i18n_key
end
defaults << options[:default] if options[:default]
@@ -44,9 +45,10 @@ module ActiveModel
end
private
- def _singularize(str)
- ActiveSupport::Inflector.underscore(str).tr('/', '_')
- end
+
+ def _singularize(string, replacement='_')
+ ActiveSupport::Inflector.underscore(string).tr('/', replacement)
+ end
end
# == Active Model Naming
@@ -62,6 +64,9 @@ module ActiveModel
# BookCover.model_name # => "BookCover"
# BookCover.model_name.human # => "Book cover"
#
+ # BookCover.model_name.i18n_key # => "book_cover"
+ # 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
# method below, or rolling your own is required.
diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb
new file mode 100644
index 0000000000..b8aa9cc1e2
--- /dev/null
+++ b/activemodel/lib/active_model/observer_array.rb
@@ -0,0 +1,98 @@
+require 'set'
+
+module ActiveModel
+ # Stores the enabled/disabled state of individual observers for
+ # a particular model classes.
+ class ObserverArray < Array
+ INSTANCES = Hash.new do |hash, model_class|
+ hash[model_class] = new(model_class)
+ end
+
+ def self.for(model_class)
+ return nil unless model_class < ActiveModel::Observing
+ INSTANCES[model_class]
+ end
+
+ # returns false if:
+ # - the ObserverArray for the given model's class has the given observer
+ # in its disabled_observers set.
+ # - or that is the case at any level of the model's superclass chain.
+ def self.observer_enabled?(observer, model)
+ klass = model.class
+ observer_class = observer.class
+
+ loop do
+ break unless array = self.for(klass)
+ return false if array.disabled_observers.include?(observer_class)
+ klass = klass.superclass
+ end
+
+ true # observers are enabled by default
+ end
+
+ def disabled_observers
+ @disabled_observers ||= Set.new
+ end
+
+ attr_reader :model_class
+ def initialize(model_class, *args)
+ @model_class = model_class
+ super(*args)
+ end
+
+ def disable(*observers, &block)
+ set_enablement(false, observers, &block)
+ end
+
+ def enable(*observers, &block)
+ set_enablement(true, observers, &block)
+ end
+
+ private
+
+ 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 transaction
+ orig_disabled_observers = disabled_observers.dup
+
+ begin
+ yield
+ ensure
+ @disabled_observers = orig_disabled_observers
+ end
+ end
+
+ def set_enablement(enabled, observers)
+ if block_given?
+ transaction do
+ set_enablement(enabled, observers)
+ yield
+ end
+ else
+ observers = ActiveModel::Observer.all_observers 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
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb
index ef36f80bec..e1a2ce218d 100644
--- a/activemodel/lib/active_model/observing.rb
+++ b/activemodel/lib/active_model/observing.rb
@@ -1,8 +1,10 @@
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'
module ActiveModel
module Observing
@@ -30,12 +32,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.for(self)
end
# Gets the current observer instances.
@@ -43,7 +45,7 @@ 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
@@ -76,7 +78,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
@@ -197,6 +203,23 @@ module ActiveModel
nil
end
end
+
+ def subclasses
+ @subclasses ||= []
+ end
+
+ # List of all observer subclasses, sub-subclasses, etc.
+ # Necessary so we can disable or enable all observers.
+ def all_observers
+ subclasses.each_with_object(subclasses.dup) do |subclass, array|
+ array.concat(subclass.all_observers)
+ end
+ end
+ end
+
+ def self.inherited(subclass)
+ subclasses << subclass
+ super
end
# Start observing the declared classes and their subclasses.
@@ -210,7 +233,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)
+ if respond_to?(observed_method) && ObserverArray.observer_enabled?(self, object)
+ send(observed_method, object)
+ end
end
# Special method sent by the observed class when it is inherited.
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 957d0ddaaa..ee94ad66cf 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -31,11 +31,10 @@ module ActiveModel
# User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
def has_secure_password
attr_reader :password
- attr_accessor :password_confirmation
validates_confirmation_of :password
validates_presence_of :password_digest
-
+
include InstanceMethodsOnActivation
if respond_to?(:attributes_protected_by_default)
@@ -59,7 +58,9 @@ module ActiveModel
# Encrypts the password into the password_digest attribute.
def password=(unencrypted_password)
@password = unencrypted_password
- self.password_digest = BCrypt::Password.create(unencrypted_password)
+ unless unencrypted_password.blank?
+ self.password_digest = BCrypt::Password.create(unencrypted_password)
+ end
end
end
end
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index dbb76244e4..920a133159 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -44,7 +44,7 @@ module ActiveModel
# Specify +options+ with additional translating options.
def human_attribute_name(attribute, options = {})
defaults = lookup_ancestors.map do |klass|
- :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}"
+ :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
end
defaults << :"attributes.#{attribute}"
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index e38e565d09..a85c23f725 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -1,18 +1,35 @@
+require 'active_support/core_ext/range.rb'
+
module ActiveModel
# == Active Model Exclusion Validator
module Validations
class ExclusionValidator < EachValidator
+ ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
+ "and must be supplied as the :in option of the configuration hash"
+
def check_validity!
- raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
- ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
+ unless [:include?, :call].any? { |method| options[:in].respond_to?(method) }
+ raise ArgumentError, ERROR_MESSAGE
+ end
end
def validate_each(record, attribute, value)
- if options[:in].include?(value)
+ delimiter = options[:in]
+ exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
+ if exclusions.send(inclusion_method(exclusions), value)
record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value))
end
end
+
+ private
+
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
+ # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt>
+ # uses the previous logic of comparing a value with the range endpoints.
+ def inclusion_method(enumerable)
+ enumerable.is_a?(Range) ? :cover? : :include?
+ end
end
module HelperMethods
@@ -22,10 +39,14 @@ module ActiveModel
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed"
+ # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, :message => "should not be the same as your username or first name"
# end
#
# Configuration options:
# * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of.
+ # This can be supplied as a proc or lambda which returns an enumerable. If the enumerable
+ # is a range the test is performed with <tt>Range#cover?</tt>
+ # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index 541f53a834..6f23d492eb 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -4,10 +4,12 @@ module ActiveModel
module Validations
class FormatValidator < EachValidator
def validate_each(record, attribute, value)
- if options[:with] && value.to_s !~ options[:with]
- record.errors.add(attribute, :invalid, options.except(:with).merge!(:value => value))
- elsif options[:without] && value.to_s =~ options[:without]
- record.errors.add(attribute, :invalid, options.except(:without).merge!(:value => value))
+ if options[:with]
+ regexp = option_call(record, :with)
+ record_error(record, attribute, :with, value) if value.to_s !~ regexp
+ elsif options[:without]
+ regexp = option_call(record, :without)
+ record_error(record, attribute, :without, value) if value.to_s =~ regexp
end
end
@@ -16,12 +18,25 @@ module ActiveModel
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
end
- if options[:with] && !options[:with].is_a?(Regexp)
- raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
- end
+ check_options_validity(options, :with)
+ check_options_validity(options, :without)
+ end
+
+ private
- if options[:without] && !options[:without].is_a?(Regexp)
- raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
+ def option_call(record, name)
+ option = options[name]
+ option.respond_to?(:call) ? option.call(record) : option
+ end
+
+ def record_error(record, attribute, name, value)
+ record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value))
+ end
+
+ def check_options_validity(options, name)
+ option = options[name]
+ if option && !option.is_a?(Regexp) && !option.respond_to?(:call)
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
end
end
end
@@ -40,17 +55,26 @@ module ActiveModel
# validates_format_of :email, :without => /NOSPAM/
# end
#
+ # You can also provide a proc or lambda which will determine the regular expression that will be used to validate the attribute
+ #
+ # class Person < ActiveRecord::Base
+ # # Admin can have number as a first letter in their screen name
+ # validates_format_of :screen_name, :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i }
+ # end
+ #
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
#
- # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression,
- # or else an exception will be raised.
+ # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression
+ # or a proc or lambda, or else an exception will be raised.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation.
+ # This can be provided as a proc or lambda returning regular expression which will be called at runtime.
# * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation.
+ # This can be provided as a proc or lambda returning regular expression which will be called at runtime.
# * <tt>:on</tt> - Specifies when this validation is active. Runs in all
# validation contexts by default (+nil+), other options are <tt>:create</tt>
# and <tt>:update</tt>.
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 92ac940f36..d32aebeb88 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -5,20 +5,30 @@ module ActiveModel
# == Active Model Inclusion Validator
module Validations
class InclusionValidator < EachValidator
+ ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
+ "and must be supplied as the :in option of the configuration hash"
+
def check_validity!
- raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
- ":in option of the configuration hash" unless options[:in].respond_to?(:include?)
+ unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) }
+ raise ArgumentError, ERROR_MESSAGE
+ end
end
def validate_each(record, attribute, value)
- record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) unless options[:in].send(include?, value)
+ delimiter = options[:in]
+ exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
+ unless exclusions.send(inclusion_method(exclusions), value)
+ record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value))
+ end
end
+ private
+
# In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
# range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt>
# uses the previous logic of comparing a value with the range endpoints.
- def include?
- options[:in].is_a?(Range) ? :cover? : :include?
+ def inclusion_method(enumerable)
+ enumerable.is_a?(Range) ? :cover? : :include?
end
end
@@ -29,11 +39,13 @@ module ActiveModel
# validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list"
+ # validates_inclusion_of :states, :in => lambda{ |person| STATES[person.country] }
# end
#
# Configuration options:
- # * <tt>:in</tt> - An enumerable object of available items.
- # If the enumerable is a range the test is performed with <tt>Range#cover?</tt>
+ # * <tt>:in</tt> - An enumerable object of available items. This can be
+ # supplied as a proc or lambda which returns an enumerable. If the enumerable
+ # is a range the test is performed with <tt>Range#cover?</tt>
# (backported in Active Support for 1.8), otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index 7af6c83460..72735cfb89 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -43,7 +43,8 @@ module ActiveModel
value ||= [] if key == :maximum
- next if value && value.size.send(validity_check, check_value)
+ value_length = value.respond_to?(:length) ? value.length : value.to_s.length
+ next if value_length.send(validity_check, check_value)
errors_options = options.except(*RESERVED_OPTIONS)
errors_options[:count] = check_value
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 1c6123eb09..5304743389 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/array/wrap'
require "active_support/core_ext/module/anonymous"
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/object/inclusion'
module ActiveModel #:nodoc:
@@ -67,7 +68,7 @@ module ActiveModel #:nodoc:
#
# class TitleValidator < ActiveModel::EachValidator
# def validate_each(record, attribute, value)
- # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value)
+ # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless value.in?(['Mr.', 'Mrs.', 'Dr.'])
# end
# end
#
@@ -120,7 +121,7 @@ module ActiveModel #:nodoc:
# Override this method in subclasses with validation logic, adding errors
# to the records +errors+ array where necessary.
def validate(record)
- raise NotImplementedError
+ raise NotImplementedError, "Subclasses must implement a validate(record) method."
end
end
@@ -156,7 +157,7 @@ module ActiveModel #:nodoc:
# Override this method in subclasses with the validation logic, adding
# errors to the records +errors+ array where necessary.
def validate_each(record, attribute, value)
- raise NotImplementedError
+ raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
end
# Hook method that gets called by the initializer allowing verification