diff options
Diffstat (limited to 'activemodel/lib')
29 files changed, 497 insertions, 310 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 85514e63fd..ded1b752df 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -21,8 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require 'active_support' require 'active_model/version' @@ -39,6 +37,7 @@ module ActiveModel autoload :Errors autoload :Lint autoload :MassAssignmentSecurity + autoload :Model autoload :Name, 'active_model/naming' autoload :Naming autoload :Observer, 'active_model/observing' @@ -58,5 +57,6 @@ module ActiveModel end end -require 'active_support/i18n' -I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 97a83e58af..846d0d7f86 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -28,7 +28,7 @@ module ActiveModel # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # attribute_method_suffix '_contrived?' # attribute_method_prefix 'clear_' - # define_attribute_methods ['name'] + # define_attribute_methods 'name' # # attr_accessor :name # @@ -86,7 +86,7 @@ module ActiveModel # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_prefix 'clear_' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -124,7 +124,7 @@ module ActiveModel # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_suffix '_short?' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -162,7 +162,7 @@ module ActiveModel # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -180,11 +180,23 @@ module ActiveModel undefine_attribute_methods end + + # Allows you to make aliases for attributes. + # + # class Person + # attr_accessor :name + # alias_attribute :nickname, :name + # end + # + # person = Person.new + # person.nickname = "Bob" + # person.nickname # => "Bob" + # person.name # => "Bob" def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s matcher_old = matcher.method_name(old_name).to_s - define_optimized_call self, matcher_new, matcher_old + define_proxy_call false, self, matcher_new, matcher_old end end @@ -204,7 +216,7 @@ module ActiveModel # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or # # attribute_method_affix declares. - # define_attribute_methods [:name, :age, :address] + # define_attribute_methods :name, :age, :address # # private # @@ -212,8 +224,8 @@ module ActiveModel # ... # end # end - def define_attribute_methods(attr_names) - attr_names.each { |attr_name| define_attribute_method(attr_name) } + def define_attribute_methods(*attr_names) + attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) } end def define_attribute_method(attr_name) @@ -226,7 +238,7 @@ module ActiveModel if respond_to?(generate_method, true) send(generate_method, attr_name) else - define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s + define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end @@ -243,11 +255,7 @@ module ActiveModel # Returns true if the attribute methods defined have been generated. def generated_attribute_methods #:nodoc: - @generated_attribute_methods ||= begin - mod = Module.new - include mod - mod - end + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } end protected @@ -285,7 +293,7 @@ module ActiveModel # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This fallbacks `define_method` # and `send` if the given names cannot be compiled. - def define_optimized_call(mod, name, send, *extra) #:nodoc: + def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: if name =~ NAME_COMPILABLE_REGEXP defn = "def #{name}(*args)" else @@ -295,7 +303,7 @@ module ActiveModel extra = (extra.map(&:inspect) << "*args").join(", ") if send =~ CALL_COMPILABLE_REGEXP - target = "#{send}(#{extra})" + target = "#{"self." unless include_private}#{send}(#{extra})" else target = "send(:'#{send}', #{extra})" end diff --git a/activemodel/lib/active_model/configuration.rb b/activemodel/lib/active_model/configuration.rb index 1757c12ebf..ba5a6a2075 100644 --- a/activemodel/lib/active_model/configuration.rb +++ b/activemodel/lib/active_model/configuration.rb @@ -95,7 +95,7 @@ module ActiveModel end def define - host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 attr_accessor :#{name} def #{name}?; !!#{name}; end CODE @@ -107,7 +107,7 @@ module ActiveModel define_method("#{name}?") { !!send(name) } end - host.class_eval <<-CODE + host.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}; defined?(@#{name}) ? @#{name} : self.class.#{name}; end def #{name}?; !!#{name}; end CODE @@ -117,7 +117,7 @@ module ActiveModel define_method("#{name}=") { |val| host.send("#{name}=", val) } end else - class_methods.class_eval <<-CODE, __FILE__, __LINE__ + class_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(val) singleton_class.class_eval do remove_possible_method(:#{name}) diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index c7c805f1a2..d7f30f0920 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -21,7 +21,7 @@ module ActiveModel # cm.to_model == self # => true # cm.to_key # => nil # cm.to_param # => nil - # cm.to_path # => "contact_messages/contact_message" + # cm.to_partial_path # => "contact_messages/contact_message" # module Conversion extend ActiveSupport::Concern @@ -57,7 +57,7 @@ module ActiveModel end module ClassMethods #:nodoc: - # Provide a class level cache for the to_path. This is an + # Provide a class level cache for #to_partial_path. This is an # internal method and should not be accessed directly. def _to_partial_path #:nodoc: @_to_partial_path ||= begin diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 026f077ee7..3f2fd12db7 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,6 +1,7 @@ require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/object/blank' module ActiveModel # == Active Model Dirty @@ -29,7 +30,7 @@ module ActiveModel # # include ActiveModel::Dirty # - # define_attribute_methods [:name] + # define_attribute_methods :name # # def name # @name @@ -82,6 +83,7 @@ module ActiveModel # in-place attributes. # # person.name_will_change! + # person.name_change # => ['Bill', 'Bill'] # person.name << 'y' # person.name_change # => ['Bill', 'Billy'] module Dirty @@ -98,7 +100,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.any? + changed_attributes.present? end # List of attributes with unsaved changes. @@ -150,13 +152,15 @@ module ActiveModel # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) + return if attribute_changed?(attr) + begin value = __send__(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end - changed_attributes[attr] = value unless changed_attributes.include?(attr) + changed_attributes[attr] = value end # Handle <tt>reset_*!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 042f9cd7e2..aba6618b56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -3,7 +3,6 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/reverse_merge' module ActiveModel # == Active Model Errors @@ -130,12 +129,12 @@ module ActiveModel # has more than one error message, yields once for each error message. # # p.errors.add(:name, "can't be blank") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # end # # p.errors.add(:name, "must be specified") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end @@ -202,12 +201,12 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) end # Returns an Hash that can be used as the JSON representation for this object. # Options: - # * <tt>:full_messages</tt> - determines if json object should contain + # * <tt>:full_messages</tt> - determines if json object should contain # full messages or not. Default: <tt>false</tt>. def as_json(options=nil) to_hash(options && options[:full_messages]) @@ -217,7 +216,7 @@ module ActiveModel if full_messages messages = {} self.messages.each do |attribute, array| - messages[attribute] = array.map{|message| full_message(attribute, message) } + messages[attribute] = array.map { |message| full_message(attribute, message) } end messages else @@ -286,7 +285,7 @@ module ActiveModel # "Name is invalid" def full_message(attribute, message) return message if attribute == :base - attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = attribute.to_s.tr('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) I18n.t(:"errors.format", { :default => "%{attribute} %{message}", @@ -347,7 +346,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value - }.merge(options) + }.merge!(options) I18n.translate(key, options) end @@ -356,9 +355,10 @@ module ActiveModel def normalize_message(attribute, message, options) message ||= :invalid - if message.is_a?(Symbol) + case message + when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - elsif message.is_a?(Proc) + when Proc message.call else message diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index a10fdefd1a..88b730626c 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -78,10 +78,10 @@ module ActiveModel def test_model_naming assert model.class.respond_to?(:model_name), "The model should respond to model_name" model_name = model.class.model_name - assert_kind_of String, model_name - assert_kind_of String, model_name.human - assert_kind_of String, model_name.singular - assert_kind_of String, model_name.plural + assert model_name.respond_to?(:to_str) + assert model_name.human.respond_to?(:to_str) + assert model_name.singular.respond_to?(:to_str) + assert model_name.plural.respond_to?(:to_str) end # == Errors Testing diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index ba49c6beaa..d17848c861 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -9,7 +9,7 @@ en: inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" - confirmation: "doesn't match confirmation" + confirmation: "doesn't match %{attribute}" accepted: "must be accepted" empty: "can't be empty" blank: "can't be blank" diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 95de039676..893fbf92c3 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -85,7 +85,7 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the :default role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5 }, :as => :default) @@ -93,7 +93,7 @@ module ActiveModel # customer.email # => "a@b.com" # customer.logins_count # => nil # - # And using the :admin role : + # And using the :admin role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin) @@ -107,8 +107,9 @@ module ActiveModel # 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. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_protected+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_protected(*args) options = args.extract_options! role = options[:as] || :default @@ -152,7 +153,7 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the :default role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) @@ -162,15 +163,16 @@ module ActiveModel # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # - # And using the :admin role : + # And using the :admin role: # # 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. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_accessible+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_accessible(*args) options = args.extract_options! role = options[:as] || :default @@ -227,7 +229,7 @@ module ActiveModel protected def sanitize_for_mass_assignment(attributes, role = nil) - _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) + _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role)) end def mass_assignment_authorizer(role) diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb index cfeb4aa7cd..44ce5a489d 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -2,20 +2,18 @@ module ActiveModel module MassAssignmentSecurity class Sanitizer # Returns all attributes not denied by the authorizer. - def sanitize(attributes, authorizer) - sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) } - debug_protected_attribute_removal(attributes, sanitized_attributes) + def sanitize(klass, attributes, authorizer) + rejected = [] + sanitized_attributes = attributes.reject do |key, value| + rejected << key if authorizer.deny?(key) + end + process_removed_attributes(klass, rejected) unless rejected.empty? sanitized_attributes end protected - def debug_protected_attribute_removal(attributes, sanitized_attributes) - removed_keys = attributes.keys - sanitized_attributes.keys - process_removed_attributes(removed_keys) if removed_keys.any? - end - - def process_removed_attributes(attrs) + def process_removed_attributes(klass, attrs) raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten" end end @@ -34,8 +32,21 @@ module ActiveModel @target.respond_to?(:logger) && @target.logger end - def process_removed_attributes(attrs) - logger.warn "Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? + def backtrace + if defined? Rails + Rails.backtrace_cleaner.clean(caller) + else + caller + end + end + + def process_removed_attributes(klass, attrs) + if logger? + logger.warn do + "WARNING: Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}\n" + + backtrace.map { |trace| "\t#{trace}" }.join("\n") + end + end end end @@ -44,9 +55,9 @@ module ActiveModel super() end - def process_removed_attributes(attrs) + def process_removed_attributes(klass, attrs) return if (attrs - insensitive_attributes).empty? - raise ActiveModel::MassAssignmentSecurity::Error.new(attrs) + raise ActiveModel::MassAssignmentSecurity::Error.new(klass, attrs) end def insensitive_attributes @@ -55,8 +66,8 @@ module ActiveModel end class Error < StandardError - def initialize(attrs) - super("Can't mass-assign protected attributes: #{attrs.join(', ')}") + def initialize(klass, attrs) + super("Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}") end end end diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb new file mode 100644 index 0000000000..3af95b09b0 --- /dev/null +++ b/activemodel/lib/active_model/model.rb @@ -0,0 +1,76 @@ +module ActiveModel + + # == Active Model Basic Model + # + # Includes the required interface for an object to interact with <tt>ActionPack</tt>, + # using different <tt>ActiveModel</tt> modules. It includes model name introspections, + # conversions, translations and validations. Besides that, it allows you to + # initialize the object with a hash of attributes, pretty much like + # <tt>ActiveRecord</tt> does. + # + # A minimal implementation could be: + # + # class Person + # include ActiveModel::Model + # attr_accessor :name, :age + # end + # + # person = Person.new(:name => 'bob', :age => '18') + # person.name # => 'bob' + # person.age # => 18 + # + # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt> to + # return <tt>false</tt>, which is the most common case. You may want to override it + # in your class to simulate a different scenario: + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name + # + # def persisted? + # self.id == 1 + # end + # end + # + # person = Person.new(:id => 1, :name => 'bob') + # person.persisted? # => true + # + # Also, if for some reason you need to run code on <tt>initialize</tt>, make sure you + # call super if you want the attributes hash initialization to happen. + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name, :omg + # + # def initialize(attributes={}) + # super + # @omg ||= true + # end + # end + # + # person = Person.new(:id => 1, :name => 'bob') + # person.omg # => true + # + # For more detailed information on other functionalities available, please refer + # to the specific modules included in <tt>ActiveModel::Model</tt> (see below). + module Model + def self.included(base) + base.class_eval do + extend ActiveModel::Naming + extend ActiveModel::Translation + include ActiveModel::Validations + include ActiveModel::Conversion + end + end + + def initialize(params={}) + params.each do |attr, value| + self.public_send("#{attr}=", value) + end if params + end + + def persisted? + false + end + end +end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 755e54efcd..2b5fc57a3a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,39 +1,40 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/module/deprecation' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/object/blank' module ActiveModel - class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path, - :singular_route_key, :route_key, :param_key, :i18n_key + class Name + include Comparable + + attr_reader :singular, :plural, :element, :collection, + :singular_route_key, :route_key, :param_key, :i18n_key, + :name alias_method :cache_key, :collection - deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + :to_str, :to => :name def initialize(klass, namespace = nil, name = nil) - name ||= klass.name - - raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank? + @name = name || klass.name - super(name) + raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank? - @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace + @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace @klass = klass - @singular = _singularize(self).freeze - @plural = ActiveSupport::Inflector.pluralize(@singular).freeze - @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze - @human = ActiveSupport::Inflector.humanize(@element).freeze - @collection = ActiveSupport::Inflector.tableize(self).freeze - @partial_path = "#{@collection}/#{@element}".freeze - @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze - @i18n_key = self.underscore.to_sym + @singular = _singularize(@name) + @plural = ActiveSupport::Inflector.pluralize(@singular) + @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name)) + @human = ActiveSupport::Inflector.humanize(@element) + @collection = ActiveSupport::Inflector.tableize(@name) + @param_key = (namespace ? _singularize(@unnamespaced) : @singular) + @i18n_key = @name.underscore.to_sym @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup) - @singular_route_key = ActiveSupport::Inflector.singularize(@route_key).freeze + @singular_route_key = ActiveSupport::Inflector.singularize(@route_key) @route_key << "_index" if @plural == @singular - @route_key.freeze end # Transform the model name into a more humane format, using I18n. By default, @@ -53,7 +54,7 @@ module ActiveModel defaults << options[:default] if options[:default] defaults << @human - options = {:scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults}.merge(options.except(:default)) + options = { :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults }.merge!(options.except(:default)) I18n.translate(defaults.shift, options) end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 32f2aa46bd..f5ea285ccb 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -4,6 +4,8 @@ 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/deprecation' +require 'active_support/core_ext/object/try' require 'active_support/descendants_tracker' module ActiveModel @@ -69,27 +71,34 @@ module ActiveModel end # Notify list of observers of a change. - def notify_observers(*arg) - observer_instances.each { |observer| observer.update(*arg) } + def notify_observers(*args) + observer_instances.each { |observer| observer.update(*args) } end # Total number of observers. - def count_observers + def observers_count observer_instances.size end + def count_observers + msg = "count_observers is deprecated in favor of observers_count" + ActiveSupport::Deprecation.warn(msg) + observers_count + end + protected def instantiate_observer(observer) #:nodoc: # string/symbol if observer.respond_to?(:to_sym) - observer.to_s.camelize.constantize.instance - elsif observer.respond_to?(:instance) + observer = observer.to_s.camelize.constantize + end + if 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 " + + "#{observer} must be a lowercase, underscored class name (or " + + "the class itself) responding to the method :instance. " + + "Example: Person.observers = :big_brother # calls " + "BigBrother.instance" end end @@ -101,17 +110,24 @@ module ActiveModel end end - private - # Fires notifications to model's observers - # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) - # end - def notify_observers(method) - self.class.notify_observers(method, self) - end + # Fires notifications to model's observers + # + # def save + # notify_observers(:before_save) + # ... + # notify_observers(:after_save) + # end + # + # Custom notifications can be sent in a similar fashion: + # + # notify_observers(:custom_notification, :foo) + # + # This will call +custom_notification+, passing as arguments + # the current object and :foo. + # + def notify_observers(method, *extra_args) + self.class.notify_observers(method, self, *extra_args) + end end # == Active Model Observers @@ -186,7 +202,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - redefine_method(:observed_classes) { models } + singleton_class.redefine_method(:observed_classes) { models } end # Returns an array of Classes to observe. @@ -205,15 +221,12 @@ module ActiveModel # The class observed by default is inferred from the observer's class name: # assert_equal Person, PersonObserver.observed_class def observed_class - if observed_class_name = name[/(.*)Observer/, 1] - observed_class_name.constantize - else - nil - end + name[/(.*)Observer/, 1].try :constantize end end # Start observing the declared classes and their subclasses. + # Called automatically by the instance method. def initialize observed_classes.each { |klass| add_observer!(klass) } end @@ -224,10 +237,10 @@ module ActiveModel # Send observed_method(object) if the method exists and # the observer is enabled for the given object's class. - def update(observed_method, object, &block) #:nodoc: + def update(observed_method, object, *extra_args, &block) #:nodoc: return unless respond_to?(observed_method) return if disabled_for?(object) - send(observed_method, object, &block) + send(observed_method, object, *extra_args, &block) end # Special method sent by the observed class when it is inherited. @@ -242,6 +255,7 @@ module ActiveModel klass.add_observer(self) end + # Returns true if notifications are disabled for this object. def disabled_for?(object) klass = object.class return false unless klass.respond_to?(:observers) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index e7a57cf691..3eab745c89 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -6,8 +6,9 @@ module ActiveModel # Adds methods to set and authenticate against a BCrypt password. # This mechanism requires you to have a password_digest attribute. # - # Validations for presence of password, confirmation of password (using + # Validations for presence of password on create, confirmation of password (using # a "password_confirmation" attribute) are automatically added. + # If you wish to turn off validations, pass 'validations: false' as an argument. # You can add more validations by hand if need be. # # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password: @@ -31,16 +32,20 @@ module ActiveModel # user.authenticate("mUc3m00RsqyRe") # => user # User.find_by_name("david").try(:authenticate, "notright") # => false # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user - def has_secure_password + def has_secure_password(options = {}) # Load bcrypt-ruby only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library. gem 'bcrypt-ruby', '~> 3.0.0' require 'bcrypt' attr_reader :password - - validates_confirmation_of :password - validates_presence_of :password_digest + + if options.fetch(:validations, true) + validates_confirmation_of :password + validates_presence_of :password, :on => :create + end + + before_create { raise "Password digest missing on new record" if password_digest.blank? } include InstanceMethodsOnActivation @@ -55,17 +60,14 @@ module ActiveModel module InstanceMethodsOnActivation # Returns self if the password is correct, otherwise false. def authenticate(unencrypted_password) - if BCrypt::Password.new(password_digest) == unencrypted_password - self - else - false - end + BCrypt::Password.new(password_digest) == unencrypted_password && self end - # Encrypts the password into the password_digest attribute. + # Encrypts the password into the password_digest attribute, only if the + # new password is not blank. def password=(unencrypted_password) - @password = unencrypted_password unless unencrypted_password.blank? + @password = unencrypted_password self.password_digest = BCrypt::Password.create(unencrypted_password) end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 51f078e662..6d8fd21814 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,7 +1,5 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/array/wrap' - module ActiveModel # == Active Model Serialization @@ -11,7 +9,6 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Serialization # # attr_accessor :name @@ -19,7 +16,6 @@ module ActiveModel # def attributes # {'name' => nil} # end - # # end # # Which would provide you with: @@ -30,20 +26,20 @@ module ActiveModel # person.serializable_hash # => {"name"=>"Bob"} # # You need to declare an attributes hash which contains the attributes - # you want to serialize. When called, serializable hash will use + # you want to serialize. Attributes must be strings, not symbols. + # When called, serializable hash will use # instance methods that match the name of the attributes hash's keys. # In order to override this behavior, take a look at the private - # method read_attribute_for_serialization. + # method +read_attribute_for_serialization+. # # Most of the time though, you will want to include the JSON or XML # serializations. Both of these modules automatically include the - # ActiveModel::Serialization module, so there is no need to explicitly + # <tt>ActiveModel::Serialization</tt> module, so there is no need to explicitly # include it. # - # So a minimal implementation including XML and JSON would be: + # A minimal implementation including XML and JSON would be: # # class Person - # # include ActiveModel::Serializers::JSON # include ActiveModel::Serializers::Xml # @@ -52,7 +48,6 @@ module ActiveModel # def attributes # {'name' => nil} # end - # # end # # Which would provide you with: @@ -69,12 +64,17 @@ module ActiveModel # person.to_json # => "{\"name\":\"Bob\"}" # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # - # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . + # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and <tt>:include</tt>. + # The following are all valid examples: + # + # person.serializable_hash(:only => 'name') + # person.serializable_hash(:include => :address) + # person.serializable_hash(:include => { :address => { :only => 'city' }}) module Serialization def serializable_hash(options = nil) options ||= {} - attribute_names = attributes.keys.sort + attribute_names = attributes.keys if only = options[:only] attribute_names &= Array(only).map(&:to_s) elsif except = options[:except] @@ -84,12 +84,11 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - method_names = Array(options[:methods]).select { |n| respond_to?(n) } - method_names.each { |n| hash[n.to_s] = send(n) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } serializable_add_includes(options) do |association, records, opts| - hash[association] = if records.is_a?(Enumerable) - records.map { |a| a.serializable_hash(opts) } + hash[association.to_s] = if records.respond_to?(:to_ary) + records.to_ary.map { |a| a.serializable_hash(opts) } else records.serializable_hash(opts) end @@ -126,13 +125,13 @@ module ActiveModel # +records+ - the association record(s) to be serialized # +opts+ - options for the association records def serializable_add_includes(options = {}) #:nodoc: - return unless include = options[:include] + return unless includes = options[:include] - unless include.is_a?(Hash) - include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + unless includes.is_a?(Hash) + includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] end - include.each do |association, opts| + includes.each do |association, opts| if records = send(association) yield association, records, opts end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 5084298210..2b3e9ce134 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -115,7 +115,9 @@ module ActiveModel merged_options = opts.merge(options.slice(:builder, :indent)) merged_options[:skip_instruct] = true - if records.is_a?(Enumerable) + if records.respond_to?(:to_ary) + records = records.to_ary + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) type = options[:skip_types] ? { } : {:type => "array"} association_name = association.to_s.singularize @@ -170,7 +172,7 @@ module ActiveModel # <id type="integer">1</id> # <name>David</name> # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> # </user> # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 02b7c54d61..6f0ca92e2a 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/reverse_merge' - module ActiveModel # == Active Model Translation @@ -43,19 +41,20 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - defaults = [] + options = { :count => 1 }.merge!(options) parts = attribute.to_s.split(".", 2) attribute = parts.pop namespace = parts.pop + attributes_scope = "#{self.i18n_scope}.attributes" if namespace - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" end - defaults << :"#{self.i18n_scope}.attributes.#{namespace}.#{attribute}" + defaults << :"#{attributes_scope}.#{namespace}.#{attribute}" else - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}" end end @@ -63,7 +62,7 @@ module ActiveModel defaults << options.delete(:default) if options[:default] defaults << attribute.humanize - options.reverse_merge! :count => 1, :default => defaults + options[:default] = defaults I18n.translate(defaults.shift, options) end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 0e15155b85..611e9ffd55 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -65,7 +65,7 @@ module ActiveModel # # attr_accessor :first_name, :last_name # - # validates_each :first_name, :last_name do |record, attr, value| + # validates_each :first_name, :last_name, :allow_blank => true do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end @@ -128,6 +128,19 @@ module ActiveModel # end # end # + # Options: + # * <tt>:on</tt> - Specifies the context where this validation is active + # (e.g. <tt>:on => :create</tt> or <tt>:on => :custom_validation_context</tt>) + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, + # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. def validate(*args, &block) options = args.extract_options! if options.key?(:on) @@ -145,7 +158,7 @@ module ActiveModel _validators.values.flatten.uniq end - # List all validators that being used to validate a specific attribute. + # List all validators that are being used to validate a specific attribute. def validators_on(*attributes) attributes.map do |attribute| _validators[attribute.to_sym] @@ -165,6 +178,12 @@ module ActiveModel end end + # Clean the +Errors+ object if instance is duped + def initialize_dup(other) # :nodoc: + @errors = nil + super + end + # Returns the +Errors+ object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index e628c6f306..38abd0c1fa 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -23,7 +23,7 @@ module ActiveModel module HelperMethods # Encapsulates the pattern of wanting to validate the acceptance of a - # terms of service check box (or similar agreement). Example: + # terms of service check box (or similar agreement). # # class Person < ActiveRecord::Base # validates_acceptance_of :terms_of_service @@ -59,7 +59,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a true or # false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index c39c85e1af..dbafd0bd1a 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -8,7 +8,8 @@ module ActiveModel # Provides an interface for any class to have <tt>before_validation</tt> and # <tt>after_validation</tt> callbacks. # - # First, extend ActiveModel::Callbacks from the class you are creating: + # First, include ActiveModel::Validations::Callbacks from the class you are + # creating: # # class MyModel # include ActiveModel::Validations::Callbacks diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb new file mode 100644 index 0000000000..b632a2bd6b --- /dev/null +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -0,0 +1,31 @@ +require 'active_support/core_ext/range.rb' + +module ActiveModel + module Validations + module Clusivity + 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! + unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end + end + + private + + def include?(record, value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + exclusions.send(inclusion_method(exclusions), value) + end + + # 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 + end +end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index e8526303e2..ede34d15bc 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -5,7 +5,8 @@ module ActiveModel class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - record.errors.add(attribute, :confirmation, options) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name)) end end @@ -18,7 +19,7 @@ module ActiveModel module HelperMethods # Encapsulates the pattern of wanting to validate a password or email - # address field with a confirmation. For example: + # address field with a confirmation. # # Model: # class Person < ActiveRecord::Base @@ -59,7 +60,7 @@ module ActiveModel # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 644cc814a7..4f09679541 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,66 +1,56 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" 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! - unless [:include?, :call].any? { |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - if exclusions.send(inclusion_method(exclusions), value) + if include?(record, 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 - # Validates that the value of the specified attribute is not in a particular enumerable object. + # Validates that the value of the specified attribute is not in a + # particular enumerable object. # # class Person < ActiveRecord::Base # 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" + # 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+). + # * <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+). # * <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>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index d3faa8c6a6..dd87e312f9 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -42,50 +42,62 @@ module ActiveModel end module HelperMethods - # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided. - # You can require that the attribute matches the regular expression: + # Validates whether the value of the specified attribute is of the correct + # form, going by the regular expression provided.You can require that the + # attribute matches the regular expression: # # class Person < ActiveRecord::Base # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create # end # - # Alternatively, you can require that the specified attribute does _not_ match the regular expression: + # Alternatively, you can require that the specified attribute does _not_ + # match the regular expression: # # class Person < ActiveRecord::Base # 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 + # 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 } + # 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. + # 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 a proc or lambda, 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>: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>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 147e2ecb69..ffdbed0fc1 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,39 +1,22 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" 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! - unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - unless exclusions.send(inclusion_method(exclusions), value) + unless include?(record, 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 inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods - # Validates whether the value of the specified attribute is available in a particular enumerable object. + # Validates whether the value of the specified attribute is available in a + # particular enumerable object. # # class Person < ActiveRecord::Base # validates_inclusion_of :gender, :in => %w( m f ) @@ -47,20 +30,25 @@ module ActiveModel # 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+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <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+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the + # attribute is blank (default is +false+). # * <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>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 037f8c2db8..64b4fe2d74 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -72,35 +72,46 @@ module ActiveModel # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" # validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters" # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.", :tokenizer => lambda { |str| str.scan(/\w+/) } + # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.", + # :tokenizer => lambda { |str| str.scan(/\w+/) } # end # # Configuration options: # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. # * <tt>:is</tt> - The exact size of the attribute. - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. + # * <tt>:within</tt> - A range specifying the minimum and maximum size of the + # attribute. # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %{count} characters)"). - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %{count} characters)"). - # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %{count} characters)"). - # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. + # * <tt>:too_long</tt> - The error message if the attribute goes over the + # maximum (default is: "is too long (maximum is %{count} characters)"). + # * <tt>:too_short</tt> - The error message if the attribute goes under the + # minimum (default is: "is too short (min is %{count} characters)"). + # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method + # and the attribute is the wrong size (default is: "is the wrong length + # (should be %{count} characters)"). + # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, + # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate + # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. # * <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>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to - # count words as in above example.) - # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. + # (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to count words + # as in above example). Defaults to <tt>lambda{ |value| value.split(//) }</tt> + # which counts individual characters. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index bb9f9679fc..40b5b92b84 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -79,9 +79,10 @@ module ActiveModel end module HelperMethods - # Validates whether the value of the specified attribute is numeric by trying to convert it to - # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). + # Validates whether the value of the specified attribute is numeric by trying + # to convert it to a float with Kernel.Float (if <tt>only_integer</tt> is false) + # or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt> (if + # <tt>only_integer</tt> is set to true). # # class Person < ActiveRecord::Base # validates_numericality_of :value, :on => :create @@ -92,37 +93,50 @@ module ActiveModel # * <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>. - # * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float columns empty strings are converted to +nil+. - # * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value. - # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value. + # * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, + # e.g. an integral value (default is +false+). + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is + # +false+). Notice that for fixnum and float columns empty strings are + # converted to +nil+. + # * <tt>:greater_than</tt> - Specifies the value must be greater than the + # supplied value. + # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater + # than or equal the supplied value. # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value. - # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value. - # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value. - # * <tt>:other_than</tt> - Specifies the value must be other than the supplied value. + # * <tt>:less_than</tt> - Specifies the value must be less than the supplied + # value. + # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or + # equal the supplied value. + # * <tt>:other_than</tt> - Specifies the value must be other than the supplied + # value. # * <tt>:odd</tt> - Specifies the value must be an odd number. # * <tt>:even</tt> - Specifies the value must be an even number. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # + # The following checks can also be supplied with a proc or a symbol which + # corresponds to a method: # - # The following checks can also be supplied with a proc or a symbol which corresponds to a method: # * <tt>:greater_than</tt> # * <tt>:greater_than_or_equal_to</tt> # * <tt>:equal_to</tt> # * <tt>:less_than</tt> # * <tt>:less_than_or_equal_to</tt> # + # For example: + # # class Person < ActiveRecord::Base # validates_numericality_of :width, :less_than => Proc.new { |person| person.height } # validates_numericality_of :width, :greater_than => :minimum_weight # end - # def validates_numericality_of(*attr_names) validates_with NumericalityValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 9a643a6f5c..018ef1e733 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -11,7 +11,8 @@ module ActiveModel end module HelperMethods - # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: + # Validates that the specified attributes are not blank (as defined by + # Object#blank?). Happens by default on save. # # class Person < ActiveRecord::Base # validates_presence_of :first_name @@ -19,25 +20,28 @@ module ActiveModel # # The first_name attribute must be in the object and it cannot be blank. # - # If you want to validate the presence of a boolean field (where the real values are true and false), - # you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # If you want to validate the presence of a boolean field (where the real values + # are true and false), you will want to use + # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. # - # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. + # This is due to the way Object#blank? handles boolean values: + # <tt>false.blank? # => true</tt>. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <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>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information - # + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 72b8562b93..66cc9daa2c 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -62,8 +62,8 @@ module ActiveModel # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information - + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # # If you pass any additional configuration options, they will be passed # to the class and available as <tt>options</tt>: # @@ -77,7 +77,6 @@ module ActiveModel # options[:my_custom_key] # => "my custom value" # end # end - # def validates_with(*args, &block) options = args.extract_options! args.each do |klass| @@ -126,14 +125,13 @@ module ActiveModel # end # # Standard configuration options (:on, :if and :unless), which are - # available on the class version of validates_with, should instead be - # placed on the <tt>validates</tt> method as these are applied and tested - # in the callback + # available on the class version of +validates_with+, should instead be + # placed on the +validates+ method as these are applied and tested + # in the callback. # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>, please refer to the - # class version of this method for more information - # + # to the class and available as +options+, please refer to the + # class version of this method for more information. def validates_with(*args, &block) options = args.extract_options! args.each do |klass| |