diff options
author | Jon Leighton <j@jonathanleighton.com> | 2011-03-02 21:24:56 +0000 |
---|---|---|
committer | Jon Leighton <j@jonathanleighton.com> | 2011-03-04 09:30:27 +0000 |
commit | 735844db712c511dd8abf36a5279318fbc0ff9d0 (patch) | |
tree | 5fbd5d224ef85d8c878bf221db98b422c9345466 /activemodel | |
parent | 9a98c766e045aebc2ef6d5b716936b73407f095d (diff) | |
parent | b171b9e73dcc6a89b1da652da61c5127fe605b51 (diff) | |
download | rails-735844db712c511dd8abf36a5279318fbc0ff9d0.tar.gz rails-735844db712c511dd8abf36a5279318fbc0ff9d0.tar.bz2 rails-735844db712c511dd8abf36a5279318fbc0ff9d0.zip |
Merge branch 'master' into nested_has_many_through
Conflicts:
activerecord/CHANGELOG
activerecord/lib/active_record/association_preload.rb
activerecord/lib/active_record/associations.rb
activerecord/lib/active_record/associations/class_methods/join_dependency.rb
activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
activerecord/lib/active_record/associations/has_many_association.rb
activerecord/lib/active_record/associations/has_many_through_association.rb
activerecord/lib/active_record/associations/has_one_association.rb
activerecord/lib/active_record/associations/has_one_through_association.rb
activerecord/lib/active_record/associations/through_association_scope.rb
activerecord/lib/active_record/reflection.rb
activerecord/test/cases/associations/has_many_through_associations_test.rb
activerecord/test/cases/associations/has_one_through_associations_test.rb
activerecord/test/cases/reflection_test.rb
activerecord/test/cases/relations_test.rb
activerecord/test/fixtures/memberships.yml
activerecord/test/models/categorization.rb
activerecord/test/models/category.rb
activerecord/test/models/member.rb
activerecord/test/models/reference.rb
activerecord/test/models/tagging.rb
Diffstat (limited to 'activemodel')
46 files changed, 543 insertions, 163 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 4e963c77b0..3082c7186a 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,15 +1,20 @@ *Rails 3.1.0 (unreleased)* -* No changes +* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH] + +* ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov] + *Rails 3.0.2 (unreleased)* * No changes + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. + *Rails 3.0.0 (August 29, 2010)* * Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis] diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index a345a2419d..7ad1051066 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2010 David Heinemeier Hansson +Copyright (c) 2004-2011 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 9b96bfaba7..d9a9bdae3e 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -41,7 +41,7 @@ modules: define_model_callbacks :create def create - _run_create_callbacks do + run_callbacks :create do # Your create action methods here end end @@ -84,7 +84,7 @@ modules: attr_reader :errors def validate! - errors.add(:name, "can not be nil") if name == nil + errors.add(:name, "can not be nil") if name.nil? end def ErrorsPerson.human_attribute_name(attr, options = {}) @@ -94,10 +94,10 @@ modules: end person.errors.full_messages - # => ["Name Can not be nil"] + # => ["Name can not be nil"] person.errors.full_messages - # => ["Name Can not be nil"] + # => ["Name can not be nil"] {Learn more}[link:classes/ActiveModel/Errors.html] diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 0372c7a03e..0a10912695 100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -14,7 +14,7 @@ namespace :test do task :isolated do ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) Dir.glob("#{dir}/test/**/*_test.rb").all? do |file| - system(ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file) + sh(ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file) end or raise "Failures" end end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 1f38e70c36..fec9c7ff8b 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -22,4 +22,5 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') s.add_dependency('i18n', '~> 0.5.0') + s.add_dependency('bcrypt-ruby', '~> 2.1.4') end diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index be0f24ff92..d0e2a6f39c 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -42,6 +42,7 @@ module ActiveModel autoload :Naming autoload :Observer, 'active_model/observing' autoload :Observing + autoload :SecurePassword autoload :Serialization autoload :TestCase autoload :Translation diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index fc5f5c4c66..2a99450a3d 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -98,7 +98,7 @@ module ActiveModel def define_attr_method(name, value=nil, &block) sing = singleton_class sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - if method_defined?(:'original_#{name}') + if method_defined?('original_#{name}') undef :'original_#{name}' end alias_method :'original_#{name}', :'#{name}' @@ -109,7 +109,7 @@ module ActiveModel # 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.to_s.inspect}; end + def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end eorb end end @@ -229,15 +229,13 @@ module ActiveModel def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| - module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{matcher.method_name(new_name)}(*args) - send(:#{matcher.method_name(old_name)}, *args) - end - STR + define_method(matcher.method_name(new_name)) do |*args| + send(matcher.method_name(old_name), *args) + end end end - # Declares a the attributes that should be prefixed and suffixed by + # Declares the attributes that should be prefixed and suffixed by # ActiveModel::AttributeMethods. # # To use, pass in an array of attribute names (as strings or symbols), @@ -262,30 +260,30 @@ module ActiveModel # end # end def define_attribute_methods(attr_names) - return if attribute_methods_generated? - attr_names.each do |attr_name| - attribute_method_matchers.each do |matcher| - unless instance_method_already_implemented?(matcher.method_name(attr_name)) - generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + attr_names.each { |attr_name| define_attribute_method(attr_name) } + end - if respond_to?(generate_method) - send(generate_method, attr_name) - else - method_name = matcher.method_name(attr_name) + def define_attribute_method(attr_name) + attribute_method_matchers.each do |matcher| + unless instance_method_already_implemented?(matcher.method_name(attr_name)) + generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - if method_defined?(:'#{method_name}') - undef :'#{method_name}' - end - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - STR - end + if respond_to?(generate_method) + send(generate_method, attr_name) + else + method_name = matcher.method_name(attr_name) + + generated_attribute_methods.module_eval <<-STR, __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 end end end - @attribute_methods_generated = true end # Removes all the previously dynamically defined methods from the class @@ -293,7 +291,6 @@ module ActiveModel generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } end - @attribute_methods_generated = nil end # Returns true if the attribute methods defined have been generated. @@ -305,11 +302,6 @@ module ActiveModel end end - # Returns true if the attribute methods defined have been generated. - def attribute_methods_generated? - @attribute_methods_generated ||= nil - end - protected def instance_method_already_implemented?(method_name) method_defined?(method_name) @@ -325,7 +317,7 @@ module ActiveModel options.symbolize_keys! @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ - @method_missing_target = :"#{@prefix}attribute#{@suffix}" + @method_missing_target = "#{@prefix}attribute#{@suffix}" @method_name = "#{prefix}%s#{suffix}" end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index aaa41f5ec6..2a1f51a9a7 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -24,14 +24,11 @@ module ActiveModel # you want callbacks on in a block so that the callbacks get a chance to fire: # # def create - # _run_create_callbacks do + # run_callbacks :create do # # Your create action methods here # end # end # - # The _run_<method_name>_callbacks methods are dynamically created when you extend - # the <tt>ActiveModel::Callbacks</tt> module. - # # Then in your class, you can use the +before_create+, +after_create+ and +around_create+ # methods, just as you would in an Active Record module. # @@ -102,7 +99,7 @@ module ActiveModel define_callbacks(callback, options) types.each do |type| - send(:"_define_#{type}_model_callback", self, callback) + send("_define_#{type}_model_callback", self, callback) end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 1dfd0b6132..a479795d51 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,5 +1,4 @@ require 'active_model/attribute_methods' -require 'active_support/concern' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index fdca852c7a..c2f0228785 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -60,9 +60,13 @@ module ActiveModel # p.validate! # => ["can not be nil"] # p.errors.full_messages # => ["name can not be nil"] # # etc.. - class Errors < ActiveSupport::OrderedHash + class Errors + include Enumerable + CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + attr_reader :messages + # Pass in the instance of the object that is using the errors object. # # class Person @@ -71,12 +75,29 @@ module ActiveModel # end # end def initialize(base) - @base = base - super() + @base = base + @messages = ActiveSupport::OrderedHash.new + end + + # Clear the messages + def clear + messages.clear + end + + # Do the error messages include an error with key +error+? + def include?(error) + messages.include? error end - alias_method :get, :[] - alias_method :set, :[]= + # Get messages for +key+ + def get(key) + messages[key] + end + + # Set messages for +key+ to +value+ + def set(key, value) + messages[key] = value + end # When passed a symbol or a name of a method, returns an array of errors # for the method. @@ -110,7 +131,7 @@ module ActiveModel # # then yield :name and "must be specified" # end def each - each_key do |attribute| + messages.each_key do |attribute| self[attribute].each { |error| yield attribute, error } end end @@ -125,6 +146,16 @@ module ActiveModel values.flatten.size end + # Returns all message values + def values + messages.values + end + + # Returns all message keys + def keys + messages.keys + end + # Returns an array of error messages, with the attribute name included # # p.errors.add(:name, "can't be blank") @@ -147,7 +178,7 @@ module ActiveModel def empty? all? { |k, v| v && v.empty? } end - + alias_method :blank?, :empty? # Returns an xml formatted representation of the Errors hash. # # p.errors.add(:name, "can't be blank") @@ -169,9 +200,7 @@ module ActiveModel end def to_hash - hash = ActiveSupport::OrderedHash.new - each { |k, v| (hash[k] ||= []) << v } - hash + messages.dup end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to @@ -221,26 +250,20 @@ module ActiveModel # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] def full_messages - full_messages = [] - - each do |attribute, messages| - messages = Array.wrap(messages) - next if messages.empty? - + map { |attribute, message| if attribute == :base - messages.each {|m| full_messages << m } + message else attr_name = attribute.to_s.gsub('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - options = { :default => "%{attribute} %{message}", :attribute => attr_name } - messages.each do |m| - full_messages << I18n.t(:"errors.format", options.merge(:message => m)) - end + I18n.t(:"errors.format", { + :default => "%{attribute} %{message}", + :attribute => attr_name, + :message => message + }) end - end - - full_messages + } end # Translates an error message in its default scope @@ -272,8 +295,8 @@ module ActiveModel 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.i18n_key}.attributes.#{attribute}.#{type}", - :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] + [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}", + :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ] end defaults << options.delete(:message) diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 66cd9fdde6..97e31d4243 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -20,32 +20,32 @@ module ActiveModel # For example, a logged in user may need to assign additional attributes depending # on their role: # - # class AccountsController < ApplicationController - # include ActiveModel::MassAssignmentSecurity + # class AccountsController < ApplicationController + # include ActiveModel::MassAssignmentSecurity # - # attr_accessible :first_name, :last_name + # attr_accessible :first_name, :last_name # - # def self.admin_accessible_attributes - # accessible_attributes + [ :plan_id ] - # end + # def self.admin_accessible_attributes + # accessible_attributes + [ :plan_id ] + # end # - # def update - # ... - # @account.update_attributes(account_params) - # ... - # end + # def update + # ... + # @account.update_attributes(account_params) + # ... + # end # - # protected + # protected # - # def account_params - # sanitize_for_mass_assignment(params[:account]) - # end + # def account_params + # sanitize_for_mass_assignment(params[:account]) + # end # - # def mass_assignment_authorizer - # admin ? admin_accessible_attributes : super - # end + # def mass_assignment_authorizer + # admin ? admin_accessible_attributes : super + # end # - # end + # end # module ClassMethods # Attributes named in this macro are protected from mass-assignment diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 44aedc8efd..eb9b847509 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, :i18n_key + attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key alias_method :cache_key, :collection def initialize(klass, namespace = nil) @@ -20,7 +20,6 @@ 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 = _singularize(self, '.').to_sym end # Transform the model name into a more humane format, using I18n. By default, @@ -34,7 +33,7 @@ module ActiveModel @klass.respond_to?(:i18n_scope) defaults = @klass.lookup_ancestors.map do |klass| - klass.model_name.i18n_key + klass.model_name.underscore.to_sym end defaults << options[:default] if options[:default] @@ -45,10 +44,9 @@ module ActiveModel end private - - def _singularize(string, replacement='_') - ActiveSupport::Inflector.underscore(string).tr('/', replacement) - end + def _singularize(str) + ActiveSupport::Inflector.underscore(str).tr('/', '_') + end end # == Active Model Naming @@ -64,12 +62,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.. + # method below, or rolling your own is required. module Naming # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information. diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 0d2dd36e59..af036b560e 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -13,14 +13,18 @@ module ActiveModel # # Activates the observers assigned. Examples: # + # class ORM + # include ActiveModel::Observing + # end + # # # Calls PersonObserver.instance - # ActiveRecord::Base.observers = :person_observer + # ORM.observers = :person_observer # # # Calls Cacher.instance and GarbageCollector.instance - # ActiveRecord::Base.observers = :cacher, :garbage_collector + # ORM.observers = :cacher, :garbage_collector # # # Same as above, just using explicit class references - # ActiveRecord::Base.observers = Cacher, GarbageCollector + # ORM.observers = Cacher, GarbageCollector # # Note: Setting this does not instantiate the observers yet. # +instantiate_observers+ is called during startup, and before @@ -44,6 +48,7 @@ module ActiveModel observers.each { |o| instantiate_observer(o) } end + # Add a new observer to the pool. def add_observer(observer) unless observer.respond_to? :update raise ArgumentError, "observer needs to respond to `update'" @@ -51,12 +56,14 @@ module ActiveModel observer_instances << observer end + # Notify list of observers of a change. def notify_observers(*arg) for observer in observer_instances observer.update(*arg) end end + # Total number of observers. def count_observers observer_instances.size end @@ -152,6 +159,10 @@ module ActiveModel # The AuditObserver will now act on both updates to Account and Balance by treating # them both as records. # + # If you're using an Observer in a Rails application with Active Record, be sure to + # read about the necessary configuration in the documentation for + # ActiveRecord::Observer. + # class Observer include Singleton diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb new file mode 100644 index 0000000000..957d0ddaaa --- /dev/null +++ b/activemodel/lib/active_model/secure_password.rb @@ -0,0 +1,66 @@ +require 'bcrypt' + +module ActiveModel + module SecurePassword + extend ActiveSupport::Concern + + module ClassMethods + # 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 + # a "password_confirmation" attribute) are automatically added. + # You can add more validations by hand if need be. + # + # Example using Active Record (which automatically includes ActiveModel::SecurePassword): + # + # # Schema: User(name:string, password_digest:string) + # class User < ActiveRecord::Base + # has_secure_password + # end + # + # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") + # user.save # => false, password required + # user.password = "mUc3m00RsqyRe" + # user.save # => false, confirmation doesn't match + # user.password_confirmation = "mUc3m00RsqyRe" + # user.save # => true + # user.authenticate("notright") # => false + # user.authenticate("mUc3m00RsqyRe") # => user + # User.find_by_name("david").try(:authenticate, "notright") # => nil + # 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) + def self.attributes_protected_by_default + super + ['password_digest'] + end + end + end + end + + 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 + end + + # Encrypts the password into the password_digest attribute. + def password=(unencrypted_password) + @password = unencrypted_password + self.password_digest = BCrypt::Password.create(unencrypted_password) + end + end + end +end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 37739b98a1..caf44a2ee0 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -15,7 +15,7 @@ module ActiveModel # attr_accessor :name # # def attributes - # @attributes ||= {'name' => 'nil'} + # {'name' => name} # end # # end @@ -45,7 +45,7 @@ module ActiveModel # attr_accessor :name # # def attributes - # @attributes ||= {'name' => 'nil'} + # {'name' => name} # end # # end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index b897baa614..d4295e6afe 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -134,6 +134,29 @@ module ActiveModel # Returns XML representing the model. Configuration can be # passed through +options+. + # + # Without any +options+, the returned XML string will include all the model's + # attributes. For example: + # + # konata = User.find(1) + # konata.to_xml + # + # <?xml version="1.0" encoding="UTF-8"?> + # <user> + # <id type="integer">1</id> + # <name>David</name> + # <age type="integer">16</age> + # <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 + # included, and work similar to the +attributes+ method. + # + # To include the result of some method calls on the model use <tt>:methods</tt>. + # + # To include associations use <tt>:include</tt>. + # + # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. def to_xml(options = {}, &block) Serializer.new(self, options).serialize(&block) end diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 920a133159..dbb76244e4 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.i18n_key}.#{attribute}" + :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}" end defaults << :"attributes.#{attribute}" diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index b044caa8d3..98af88b5a0 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -71,8 +71,8 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies when this validation is active (default is - # <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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 @@ -104,7 +104,7 @@ module ActiveModel # end # end # - # Or with a block which is passed with the current record to be validated: + # With a block which is passed with the current record to be validated: # # class Comment # include ActiveModel::Validations @@ -118,6 +118,16 @@ module ActiveModel # end # end # + # Or with a block where self points to the current record to be validated: + # + # class Comment + # include ActiveModel::Validations + # + # validate do + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # end + # end + # def validate(*args, &block) options = args.extract_options! if options.key?(:on) @@ -136,8 +146,10 @@ module ActiveModel end # List all validators that being used to validate a specific attribute. - def validators_on(attribute) - _validators[attribute.to_sym] + def validators_on(*attributes) + attributes.map do |attribute| + _validators[attribute.to_sym] + end.flatten end # Check if method is an attribute method or not. @@ -197,7 +209,7 @@ module ActiveModel protected def run_validations! - _run_validate_callbacks + run_callbacks :validate errors.empty? end end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index a7296d8e1d..4f390613aa 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -37,9 +37,9 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:on</tt> - Specifies when this validation is active (default is - # <tt>:save</tt>, other options are <tt>:create</tt> and - # <tt>:update</tt>). + # * <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>:allow_nil</tt> - Skip validation if attribute is +nil+ (default # is true). # * <tt>:accept</tt> - Specifies value that is considered accepted. diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 621518de5b..adc2867ad0 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -50,7 +50,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! - _run_validation_callbacks { super } + run_callbacks(:validation) { super } end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 00df10cef0..e6d10cfff8 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -45,8 +45,9 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "doesn't match # confirmation"). - # * <tt>:on</tt> - Specifies when this validation is active (default is - # <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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 diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 4138892786..e38e565d09 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -29,6 +29,9 @@ module ActiveModel # * <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. diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 104f403492..541f53a834 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -51,7 +51,9 @@ module ActiveModel # * <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. # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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. diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 049b093618..92ac940f36 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/range.rb' + module ActiveModel # == Active Model Inclusion Validator @@ -9,9 +11,14 @@ module ActiveModel end def validate_each(record, attribute, value) - unless options[:in].include?(value) - record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) - end + record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) unless options[:in].send(include?, 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 include? + options[:in].is_a?(Range) ? :cover? : :include? end end @@ -26,9 +33,14 @@ module ActiveModel # # 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> + # (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>: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. diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 5a46ecb4ac..7af6c83460 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -83,7 +83,9 @@ module ActiveModel # * <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 (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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. diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 95fe20de75..ae576462e6 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -93,7 +93,9 @@ module ActiveModel # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is not a number"). - # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>). + # * <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. diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 28c4640b17..cfb4c33dcc 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -26,8 +26,9 @@ module ActiveModel # # Configuration options: # * <tt>message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, - # <tt>:update</tt>). + # * <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. diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 77c5073c6e..7ff42de00b 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -55,6 +55,10 @@ module ActiveModel # validates :name, :title => true # end # + # Additionally validator classes may be in another namespace and still used within any class. + # + # validates :name, :'file/title' => true + # # The validators hash can also handle regular expressions, ranges, # arrays and strings in shortcut form, e.g. # @@ -77,17 +81,18 @@ module ActiveModel # def validates(*attributes) defaults = attributes.extract_options! - validations = defaults.slice!(:if, :unless, :on, :allow_blank, :allow_nil) + validations = defaults.slice!(*_validates_default_keys) raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? - raise ArgumentError, "Attribute names must be symbols" if attributes.any?{ |attribute| !attribute.is_a?(Symbol) } raise ArgumentError, "You need to supply at least one validation" if validations.empty? defaults.merge!(:attributes => attributes) validations.each do |key, options| + key = "#{key.to_s.camelize}Validator" + begin - validator = const_get("#{key.to_s.camelize}Validator") + validator = key.include?('::') ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end @@ -98,6 +103,12 @@ module ActiveModel protected + # When creating custom validators, it might be useful to be able to specify + # additional default keys. This can be done by overwriting this method. + def _validates_default_keys + [ :if, :unless, :on, :allow_blank, :allow_nil ] + end + def _parse_validates_options(options) #:nodoc: case options when TrueClass @@ -112,4 +123,4 @@ module ActiveModel end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 200efd4eb5..1663697727 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -8,6 +8,18 @@ module ActiveModel end end + class WithValidator < EachValidator + def validate_each(record, attr, val) + method_name = options[:with] + + if record.method(method_name).arity == 0 + record.send method_name + else + record.send method_name, attr + end + end + end + module ClassMethods # Passes the record off to the class or classes specified and allows them # to add errors based on more complex conditions. diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 54cf8380ab..b001adb35a 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -21,25 +21,59 @@ class ModelWithAttributes2 attribute_method_suffix '_test' end +class ModelWithAttributesWithSpaces + include ActiveModel::AttributeMethods + + attribute_method_suffix '' + + def attributes + { :'foo bar' => 'value of foo bar'} + end + +private + def attribute(name) + attributes[name.to_sym] + end +end + class AttributeMethodsTest < ActiveModel::TestCase test 'unrelated classes should not share attribute method matchers' do assert_not_equal ModelWithAttributes.send(:attribute_method_matchers), ModelWithAttributes2.send(:attribute_method_matchers) end + test '#define_attribute_method generates attribute method' do + ModelWithAttributes.define_attribute_method(:foo) + + assert_respond_to ModelWithAttributes.new, :foo + assert_equal "value of foo", ModelWithAttributes.new.foo + end + test '#define_attribute_methods generates attribute methods' do ModelWithAttributes.define_attribute_methods([:foo]) - assert ModelWithAttributes.attribute_methods_generated? assert_respond_to ModelWithAttributes.new, :foo assert_equal "value of foo", ModelWithAttributes.new.foo end + test '#define_attribute_methods generates attribute methods with spaces in their names' do + ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar']) + + assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' + assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') + end + + test '#alias_attribute works with attributes with spaces in their names' do + ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar']) + ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') + + assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar + end + test '#undefine_attribute_methods removes attribute methods' do ModelWithAttributes.define_attribute_methods([:foo]) ModelWithAttributes.undefine_attribute_methods - assert !ModelWithAttributes.attribute_methods_generated? assert !ModelWithAttributes.new.respond_to?(:foo) assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 069d907fb2..086e7266ff 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -37,7 +37,7 @@ class CallbacksTest < ActiveModel::TestCase end def create - _run_create_callbacks do + run_callbacks :create do @callbacks << :create @valid end @@ -92,7 +92,7 @@ class CallbacksTest < ActiveModel::TestCase def callback1; self.history << 'callback1'; end def callback2; self.history << 'callback2'; end def create - _run_create_callbacks {} + run_callbacks(:create) {} self end end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 27821c333b..a24cac40ad 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -25,7 +25,19 @@ class ErrorsTest < ActiveModel::TestCase def self.lookup_ancestors [self] end + end + + def test_include? + errors = ActiveModel::Errors.new(self) + errors[:foo] = 'omg' + assert errors.include?(:foo), 'errors should include :foo' + end + test "should return true if no errors" do + person = Person.new + person.errors[:foo] + assert person.errors.empty? + assert person.errors.blank? end test "method validate! should work" do diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb new file mode 100644 index 0000000000..4a47a7a226 --- /dev/null +++ b/activemodel/test/cases/secure_password_test.rb @@ -0,0 +1,45 @@ +require 'cases/helper' +require 'models/user' +require 'models/visitor' +require 'models/administrator' + +class SecurePasswordTest < ActiveModel::TestCase + + setup do + @user = User.new + end + + test "password must be present" do + assert !@user.valid? + assert_equal 1, @user.errors.size + end + + test "password must match confirmation" do + @user.password = "thiswillberight" + @user.password_confirmation = "wrong" + + assert !@user.valid? + + @user.password_confirmation = "thiswillberight" + + assert @user.valid? + end + + test "authenticate" do + @user.password = "secret" + + assert !@user.authenticate("wrong") + assert @user.authenticate("secret") + end + + test "visitor#password_digest should be protected against mass assignment" do + assert Visitor.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::BlackList) + assert Visitor.active_authorizer.include?(:password_digest) + end + + test "Administrator's mass_assignment_authorizer should be WhiteList" do + assert Administrator.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList) + assert !Administrator.active_authorizer.include?(:password_digest) + assert Administrator.active_authorizer.include?(:name) + end +end diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializeration/xml_serialization_test.rb index cc19d322b3..b6a2f88667 100644 --- a/activemodel/test/cases/serializeration/xml_serialization_test.rb +++ b/activemodel/test/cases/serializeration/xml_serialization_test.rb @@ -114,7 +114,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize yaml" do - assert_match %r{<preferences type=\"yaml\">--- !ruby/struct:Customer \nname: John\n</preferences>}, @contact.to_xml + assert_match %r{<preferences type=\"yaml\">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml end test "should call proc on object" do diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index c299d6eb5e..1b1d972d5c 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -49,6 +49,13 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'person name attribute', Child.human_attribute_name('name') end + def test_translated_model_attributes_with_attribute_matching_namespaced_model_name + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:gender => 'person gender'}, :"person/gender" => {:attribute => 'person gender attribute'}}} + + assert_equal 'person gender', Person.human_attribute_name('gender') + assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') + end + def test_translated_model_names I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} } assert_equal 'person model', Person.model_name.human diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 5cb7bff4e7..e9f0e430fe 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -55,14 +55,6 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ["Person's name not found"], @person.errors.full_messages end - def test_errors_full_messages_translates_human_attribute_name_for_model_in_module_attributes - I18n.backend.store_translations('en', :activemodel => {:attributes => {:person_module => {:person => {:name => "Person in Module's name"}}}}) - person = PersonModule::Person.new - person.errors.add(:name, 'not found') - PersonModule::Person.expects(:human_attribute_name).with(:name, :default => 'Name').returns("Person in Module's name") - assert_equal ["Person in Module's name not found"], person.errors.full_messages - end - def test_errors_full_messages_uses_format I18n.backend.store_translations('en', :errors => {:format => "Field %{attribute} %{message}"}) @person.errors.add('name', 'empty') @@ -371,15 +363,4 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ["I am a custom error"], @person.errors[:title] end - def test_model_with_module_i18n_scope - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person_module => {:person => {:blank => 'generic blank'}}}}} - PersonModule::Person.validates_presence_of :title - person = PersonModule::Person.new - person.valid? - assert_equal ['generic blank'], person.errors[:title] - - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person_module => {:person => {:attributes => {:title => {:blank => 'title cannot be blank'}}}}}}} - person.valid? - assert_equal ['title cannot be blank'], person.errors[:title] - end end diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 0716b4f087..62f2ec785d 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -10,6 +10,15 @@ class InclusionValidationTest < ActiveModel::TestCase Topic.reset_callbacks(:validate) end + def test_validates_inclusion_of_range + Topic.validates_inclusion_of( :title, :in => 'aaa'..'bbb' ) + assert Topic.new("title" => "bbc", "content" => "abc").invalid? + assert Topic.new("title" => "aa", "content" => "abc").invalid? + assert Topic.new("title" => "aaa", "content" => "abc").valid? + assert Topic.new("title" => "abc", "content" => "abc").valid? + assert Topic.new("title" => "bbb", "content" => "abc").valid? + end + def test_validates_inclusion_of Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) ) diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 666c48c8a0..779f6c8448 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -1,8 +1,10 @@ # encoding: utf-8 require 'cases/helper' require 'models/person' +require 'models/topic' require 'models/person_with_validator' require 'validators/email_validator' +require 'validators/namespace/email_validator' class ValidatesTest < ActiveModel::TestCase setup :reset_callbacks @@ -10,6 +12,7 @@ class ValidatesTest < ActiveModel::TestCase def reset_callbacks Person.reset_callbacks(:validate) + Topic.reset_callbacks(:validate) PersonWithValidator.reset_callbacks(:validate) end @@ -20,6 +23,17 @@ class ValidatesTest < ActiveModel::TestCase assert_equal ['is not a number'], person.errors[:title] end + def test_validates_with_attribute_specified_as_string + Person.validates "title", :numericality => true + person = Person.new + person.valid? + assert_equal ['is not a number'], person.errors[:title] + + person = Person.new + person.title = 123 + assert person.valid? + end + def test_validates_with_built_in_validation_and_options Person.validates :salary, :numericality => { :message => 'my custom message' } person = Person.new @@ -34,6 +48,13 @@ class ValidatesTest < ActiveModel::TestCase assert_equal ['is not an email'], person.errors[:karma] end + def test_validates_with_namespaced_validator_class + Person.validates :karma, :'namespace/email' => true + person = Person.new + person.valid? + assert_equal ['is not an email'], person.errors[:karma] + end + def test_validates_with_if_as_local_conditions Person.validates :karma, :presence => true, :email => { :unless => :condition_is_true } person = Person.new @@ -120,4 +141,13 @@ class ValidatesTest < ActiveModel::TestCase person.valid? assert_equal ['does not appear to be like Mr.'], person.errors[:title] end + + def test_defining_extra_default_keys_for_validates + Topic.validates :title, :confirmation => true, :message => 'Y U NO CONFIRM' + topic = Topic.new + topic.title = "What's happening" + topic.title_confirmation = "Not this" + assert !topic.valid? + assert_equal ['Y U NO CONFIRM'], topic.errors[:title] + end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 6d825cd316..07c1bd0533 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -171,4 +171,25 @@ class ValidatesWithTest < ActiveModel::TestCase assert topic.errors[:title].empty? assert topic.errors[:content].empty? end + + test "validates_with can validate with an instance method" do + Topic.validates :title, :with => :my_validation + + topic = Topic.new :title => "foo" + assert topic.valid? + assert topic.errors[:title].empty? + + topic = Topic.new + assert !topic.valid? + assert_equal ['is missing'], topic.errors[:title] + end + + test "optionally pass in the attribute being validated when validating with an instance method" do + Topic.validates :title, :content, :with => :my_validation_with_arg + + topic = Topic.new :title => "foo" + assert !topic.valid? + assert topic.errors[:title].empty? + assert_equal ['is missing'], topic.errors[:content] + end end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 55b477dd10..2f36195627 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -148,6 +148,14 @@ class ValidationsTest < ActiveModel::TestCase end def test_validate_block + Topic.validate { errors.add("title", "will never be valid") } + t = Topic.new("title" => "Title", "content" => "whatever") + assert t.invalid? + assert t.errors[:title].any? + assert_equal ["will never be valid"], t.errors["title"] + end + + def test_validate_block_with_params Topic.validate { |topic| topic.errors.add("title", "will never be valid") } t = Topic.new("title" => "Title", "content" => "whatever") assert t.invalid? @@ -187,7 +195,7 @@ class ValidationsTest < ActiveModel::TestCase assert t.invalid? assert_equal "can't be blank", t.errors["title"].first Topic.validates_presence_of :title, :author_name - Topic.validate {|topic| topic.errors.add('author_email_address', 'will never be valid')} + Topic.validate {errors.add('author_email_address', 'will never be valid')} Topic.validates_length_of :title, :content, :minimum => 2 t = Topic.new :title => '' @@ -246,6 +254,24 @@ class ValidationsTest < ActiveModel::TestCase assert_equal 10, Topic.validators_on(:title).first.options[:minimum] end + def test_list_of_validators_on_multiple_attributes + Topic.validates :title, :length => { :minimum => 10 } + Topic.validates :author_name, :presence => true, :format => /a/ + + validators = Topic.validators_on(:title, :author_name) + + assert_equal [ + ActiveModel::Validations::FormatValidator, + ActiveModel::Validations::LengthValidator, + ActiveModel::Validations::PresenceValidator + ], validators.map { |v| v.class }.sort_by { |c| c.to_s } + end + + def test_list_of_validators_will_be_empty_when_empty + Topic.validates :title, :length => { :minimum => 10 } + assert_equal [], Topic.validators_on(:author_name) + end + def test_validations_on_the_instance_level auto = Automobile.new diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb new file mode 100644 index 0000000000..a48f8b064f --- /dev/null +++ b/activemodel/test/models/administrator.rb @@ -0,0 +1,10 @@ +class Administrator + include ActiveModel::Validations + include ActiveModel::SecurePassword + include ActiveModel::MassAssignmentSecurity + + attr_accessor :name, :password_digest + attr_accessible :name + + has_secure_password +end diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb index eb84f7a27d..e896e90f98 100644 --- a/activemodel/test/models/person.rb +++ b/activemodel/test/models/person.rb @@ -9,10 +9,9 @@ class Person end end -class Child < Person +class Person::Gender + extend ActiveModel::Translation end -module PersonModule - class Person < ::Person - end +class Child < Person end diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index ff34565bdb..c9af78f595 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -2,6 +2,10 @@ class Topic include ActiveModel::Validations include ActiveModel::Validations::Callbacks + def self._validates_default_keys + super | [ :message ] + end + attr_accessor :title, :author_name, :content, :approved attr_accessor :after_validation_performed @@ -25,4 +29,12 @@ class Topic self.after_validation_performed = true end + def my_validation + errors.add :title, "is missing" unless title + end + + def my_validation_with_arg(attr) + errors.add attr, "is missing" unless send(attr) + end + end diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb new file mode 100644 index 0000000000..e221bb8091 --- /dev/null +++ b/activemodel/test/models/user.rb @@ -0,0 +1,8 @@ +class User + include ActiveModel::Validations + include ActiveModel::SecurePassword + + has_secure_password + + attr_accessor :password_digest, :password_salt +end diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb new file mode 100644 index 0000000000..36c0a16688 --- /dev/null +++ b/activemodel/test/models/visitor.rb @@ -0,0 +1,9 @@ +class Visitor + include ActiveModel::Validations + include ActiveModel::SecurePassword + include ActiveModel::MassAssignmentSecurity + + has_secure_password + + attr_accessor :password_digest +end diff --git a/activemodel/test/validators/namespace/email_validator.rb b/activemodel/test/validators/namespace/email_validator.rb new file mode 100644 index 0000000000..57e2793ce2 --- /dev/null +++ b/activemodel/test/validators/namespace/email_validator.rb @@ -0,0 +1,6 @@ +require 'validators/email_validator' + +module Namespace + class EmailValidator < ::EmailValidator + end +end |