diff options
Diffstat (limited to 'activemodel')
63 files changed, 1405 insertions, 700 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 8374853231..3082c7186a 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,22 +1,28 @@ -*Rails 3.0.0 [release candidate] (July 26th, 2010)* +*Rails 3.1.0 (unreleased)* -* Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis] +* 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.0 [beta 4] (June 8th, 2010)* -* JSON supports a custom root option: to_json(:root => 'custom') #4515 [Jatinder Singh] +*Rails 3.0.2 (unreleased)* + +* No changes -*Rails 3.0.0 [beta 3] (April 13th, 2010)* +*Rails 3.0.1 (October 15, 2010)* -* No changes +* No Changes, just a version bump. -*Rails 3.0.0 [beta 2] (April 1st, 2010)* +*Rails 3.0.0 (August 29, 2010)* + +* Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis] + +* JSON supports a custom root option: to_json(:root => 'custom') #4515 [Jatinder Singh] * #new_record? and #destroyed? were removed from ActiveModel::Lint. Use - persisted? instead. A model is persisted if it's not a new_record? and it was + persisted? instead. A model is persisted if it's not a new_record? and it was not destroyed? [MG] * Added validations reflection in ActiveModel::Validations [JV] @@ -27,19 +33,15 @@ * #to_key was added to ActiveModel::Lint so we can generate DOM IDs for AMo objects with composite keys [MG] - -*Rails 3.0.0 [beta 1] (February 4, 2010)* - * ActiveModel::Observer#add_observer! It has a custom hook to define after_find that should really be in a ActiveRecord::Observer subclass: - def add_observer!(klass) - klass.add_observer(self) - klass.class_eval 'def after_find() end' unless - klass.respond_to?(:after_find) - end + def add_observer!(klass) + klass.add_observer(self) + klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find) + end * Change the ActiveModel::Base.include_root_in_json default to true for Rails 3 [DHH] 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 89cacbcab4..b5b5edd52a 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -8,7 +8,7 @@ the Rails framework. Prior to Rails 3.0, if a plugin or gem developer wanted to have an object interact with Action Pack helpers, it was required to either copy chunks of code from Rails, or monkey patch entire helpers to make them handle objects -that did not exacly conform to the Active Record interface. This would result +that did not exactly conform to the Active Record interface. This would result in code duplication and fragile applications that broke on upgrades. Active Model solves this. You can include functionality from the following @@ -18,38 +18,38 @@ modules: class Person include ActiveModel::AttributeMethods - + attribute_method_prefix 'clear_' define_attribute_methods [:name, :age] - + attr_accessor :name, :age - + def clear_attribute(attr) send("#{attr}=", nil) end end - + person.clear_name person.clear_age - + {Learn more}[link:classes/ActiveModel/AttributeMethods.html] - + * Callbacks for certain operations class Person extend ActiveModel::Callbacks define_model_callbacks :create - + def create - _run_create_callbacks do + run_callbacks :create do # Your create action methods here end end end - + This generates +before_create+, +around_create+ and +after_create+ class methods that wrap your create method. - + {Learn more}[link:classes/ActiveModel/CallBacks.html] * Tracking value changes @@ -66,38 +66,38 @@ modules: person.name = 'robert' person.save person.previous_changes # => {'name' => ['bob, 'robert']} - + {Learn more}[link:classes/ActiveModel/Dirty.html] * Adding +errors+ interface to objects Exposing error messages allows objects to interact with Action Pack helpers seamlessly. - + class Person - + def initialize @errors = ActiveModel::Errors.new(self) end - + attr_accessor :name 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 = {}) "Name" end - + 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] @@ -106,9 +106,9 @@ modules: class NamedPerson extend ActiveModel::Naming end - - NamedPerson.model_name #=> "NamedPerson" - NamedPerson.model_name.human #=> "Named person" + + NamedPerson.model_name # => "NamedPerson" + NamedPerson.model_name.human # => "Named person" {Learn more}[link:classes/ActiveModel/Naming.html] @@ -117,19 +117,19 @@ modules: ActiveModel::Observers allows your object to implement the Observer pattern in a Rails App and take advantage of all the standard observer functions. - + {Learn more}[link:classes/ActiveModel/Observer.html] * Making objects serializable ActiveModel::Serialization provides a standard interface for your object to provide +to_json+ or +to_xml+ serialization. - + s = SerialPerson.new s.serializable_hash # => {"name"=>nil} s.to_json # => "{\"name\":null}" s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - + {Learn more}[link:classes/ActiveModel/Serialization.html] * Internationalization (i18n) support @@ -137,10 +137,10 @@ modules: class Person extend ActiveModel::Translation end - + Person.human_attribute_name('my_attribute') - #=> "My attribute" - + # => "My attribute" + {Learn more}[link:classes/ActiveModel/Translation.html] * Validation support @@ -157,10 +157,10 @@ modules: person = Person.new person.first_name = 'zoolander' - person.valid? #=> false + person.valid? # => false {Learn more}[link:classes/ActiveModel/Validations.html] - + * Custom validators class Person @@ -168,17 +168,17 @@ modules: validates_with HasNameValidator attr_accessor :name end - + class HasNameValidator < ActiveModel::Validator def validate(record) record.errors[:name] = "must exist" if record.name.blank? end end - + p = ValidatorPerson.new - p.valid? #=> false - p.errors.full_messages #=> ["Name must exist"] + p.valid? # => false + p.errors.full_messages # => ["Name must exist"] p.name = "Bob" - p.valid? #=> true + p.valid? # => true {Learn more}[link:classes/ActiveModel/Validator.html] diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 3fffc0d021..0a10912695 100644..100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,8 +1,5 @@ dir = File.dirname(__FILE__) -gem 'rdoc', '>= 2.5.9' -require 'rdoc' - require 'rake/testtask' task :default => :test @@ -17,26 +14,11 @@ 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 - -require 'rdoc/task' - -# Generate the RDoc documentation -RDoc::Task.new do |rdoc| - rdoc.rdoc_dir = "doc" - rdoc.title = "Active Model" - rdoc.options << '-f' << 'horo' - rdoc.options << '--charset' << 'utf-8' - rdoc.options << '--main' << 'README.rdoc' - rdoc.rdoc_files.include("README.rdoc", "CHANGELOG") - rdoc.rdoc_files.include("lib/**/*.rb") -end - - require 'rake/packagetask' require 'rake/gempackagetask' diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index c483ecbc3c..fec9c7ff8b 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('activesupport', version) - s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') + 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 5ed21a39c2..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 @@ -24,7 +24,7 @@ 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' module ActiveModel extend ActiveSupport::Autoload @@ -33,7 +33,6 @@ module ActiveModel autoload :BlockValidator, 'active_model/validator' autoload :Callbacks autoload :Conversion - autoload :DeprecatedErrorMethods autoload :Dirty autoload :EachValidator, 'active_model/validator' autoload :Errors @@ -43,10 +42,10 @@ module ActiveModel autoload :Naming autoload :Observer, 'active_model/observing' autoload :Observing + autoload :SecurePassword autoload :Serialization autoload :TestCase autoload :Translation - autoload :VERSION autoload :Validations autoload :Validator diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index a43436e008..be55581c66 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' module ActiveModel class MissingAttributeError < NoMethodError @@ -9,46 +9,46 @@ module ActiveModel # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and suffixes # to your methods as well as handling the creation of Active Record like class methods # such as +table_name+. - # + # # The requirements to implement ActiveModel::AttributeMethods are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your object - # * Call each Attribute Method module method you want to add, such as + # * Call each Attribute Method module method you want to add, such as # attribute_method_suffix or attribute_method_prefix # * Call <tt>define_attribute_methods</tt> after the other methods are # called. # * Define the various generic +_attribute+ methods that you have declared - # + # # A minimal implementation could be: - # + # # class Person # include ActiveModel::AttributeMethods - # + # # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # attribute_method_suffix '_contrived?' # attribute_method_prefix 'clear_' # define_attribute_methods ['name'] - # + # # attr_accessor :name - # + # # private - # + # # def attribute_contrived?(attr) # true # end - # + # # def clear_attribute(attr) # send("#{attr}=", nil) # end - # + # # def reset_attribute_to_default!(attr) # send("#{attr}=", "Default Name") # end # end # - # Notice that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement a <tt>attributes</tt> methods which returns a hash - # with each attribute name in your model as hash key and the attribute value as + # Note that whenever you include ActiveModel::AttributeMethods in your class, + # it requires you to implement an <tt>attributes</tt> method which returns a hash + # with each attribute name in your model as hash key and the attribute value as # hash value. # # Hash keys must be strings. @@ -56,35 +56,40 @@ module ActiveModel module AttributeMethods extend ActiveSupport::Concern + included do + class_attribute :attribute_method_matchers, :instance_writer => false + self.attribute_method_matchers = [] + end + module ClassMethods - # Defines an "attribute" method (like +inheritance_column+ or +table_name+). - # A new (class) method will be created with the given name. If a value is - # specified, the new method will return that value (as a string). - # Otherwise, the given block will be used to compute the value of the + # Defines an "attribute" method (like +inheritance_column+ or +table_name+). + # A new (class) method will be created with the given name. If a value is + # specified, the new method will return that value (as a string). + # Otherwise, the given block will be used to compute the value of the # method. # # The original method will be aliased, with the new name being prefixed - # with "original_". This allows the new method to access the original + # with "original_". This allows the new method to access the original # value. # # Example: # # class Person - # + # # include ActiveModel::AttributeMethods - # + # # cattr_accessor :primary_key # cattr_accessor :inheritance_column - # + # # define_attr_method :primary_key, "sysid" # define_attr_method( :inheritance_column ) do # original_inheritance_column + "_id" # end - # + # # end - # + # # Provides you with: - # + # # AttributePerson.primary_key # # => "sysid" # AttributePerson.inheritance_column = 'address' @@ -93,19 +98,22 @@ 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}) - undef :original_#{name} + if method_defined?('original_#{name}') + undef :'original_#{name}' end - alias_method :original_#{name}, :#{name} + alias_method :'original_#{name}', :'#{name}' eorb if block_given? sing.send :define_method, name, &block else - # use eval instead of a block to work around a memory leak in dev - # mode in fcgi - sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - def #{name}; #{value.to_s.inspect}; end - eorb + if name =~ /^[a-zA-Z_]\w*[!?=]?$/ + sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 + def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end + eorb + else + value = value.to_s if value + sing.send(:define_method, name) { value } + end end end @@ -118,20 +126,20 @@ module ActiveModel # # #{prefix}attribute(#{attr}, *args, &block) # - # An instance method <tt>#{prefix}attribute</tt> must exist and accept + # An instance method <tt>#{prefix}attribute</tt> must exist and accept # at least the +attr+ argument. # # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_prefix 'clear_' # define_attribute_methods [:name] # # private - # + # # def clear_attribute(attr) # send("#{attr}=", nil) # end @@ -143,7 +151,7 @@ module ActiveModel # person.clear_name # person.name # => nil def attribute_method_prefix(*prefixes) - attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix }) + self.attribute_method_matchers += prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix } undefine_attribute_methods end @@ -162,14 +170,14 @@ module ActiveModel # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_suffix '_short?' # define_attribute_methods [:name] # # private - # + # # def attribute_short?(attr) # send(attr).length < 5 # end @@ -180,7 +188,7 @@ module ActiveModel # person.name # => "Bob" # person.name_short? # => true def attribute_method_suffix(*suffixes) - attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix }) + self.attribute_method_matchers += suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix } undefine_attribute_methods end @@ -200,14 +208,14 @@ module ActiveModel # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # define_attribute_methods [:name] # # private - # + # # def reset_attribute_to_default!(attr) # ... # end @@ -218,29 +226,27 @@ module ActiveModel # person.reset_name_to_default! # person.name # => 'Gemma' def attribute_method_affix(*affixes) - attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] }) + self.attribute_method_matchers += affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] } undefine_attribute_methods end 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), # be sure to declare +define_attribute_methods+ after you define any # prefix, suffix or affix methods, or they will not hook in. - # + # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name, :age, :address # attribute_method_prefix 'clear_' @@ -251,36 +257,36 @@ module ActiveModel # define_attribute_methods [:name, :age, :address] # # private - # + # # def clear_attribute(attr) # ... # 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}" + + 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 - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - STR - end + 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 @@ -288,7 +294,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. @@ -300,11 +305,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) @@ -312,7 +312,7 @@ module ActiveModel private class AttributeMethodMatcher - attr_reader :prefix, :suffix + attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name) @@ -320,40 +320,34 @@ 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_name = "#{prefix}%s#{suffix}" end def match(method_name) - if matchdata = @regex.match(method_name) - AttributeMethodMatch.new(method_missing_target, matchdata[2]) + if @regex =~ method_name + AttributeMethodMatch.new(method_missing_target, $2) else nil end end def method_name(attr_name) - "#{prefix}#{attr_name}#{suffix}" - end - - def method_missing_target - :"#{prefix}attribute#{suffix}" + @method_name % attr_name end end - - def attribute_method_matchers #:nodoc: - read_inheritable_attribute(:attribute_method_matchers) || write_inheritable_attribute(:attribute_method_matchers, []) - end end - # Allows access to the object attributes, which are held in the - # <tt>@attributes</tt> hash, as though they were first-class methods. So a - # Person class with a name attribute can use Person#name and Person#name= + # Allows access to the object attributes, which are held in the + # <tt>@attributes</tt> hash, as though they were first-class methods. So a + # Person class with a name attribute can use Person#name and Person#name= # and never directly use the attributes hash -- except for multiple assigns - # with ActiveRecord#attributes=. A Milestone class can also ask - # Milestone#completed? to test that the completed attribute is not +nil+ + # with ActiveRecord#attributes=. A Milestone class can also ask + # Milestone#completed? to test that the completed attribute is not +nil+ # or 0. # - # It's also possible to instantiate related objects, so a Client class - # belonging to the clients table with a +master_id+ foreign key can + # It's also possible to instantiate related objects, so a Client class + # belonging to the clients table with a +master_id+ foreign key can # instantiate master through Client#master. def method_missing(method_id, *args, &block) method_name = method_id.to_s @@ -390,7 +384,7 @@ module ActiveModel # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) - self.class.send(:attribute_method_matchers).each do |method| + self.class.attribute_method_matchers.each do |method| if (match = method.match(method_name)) && attribute_method?(match.attr_name) return match end @@ -401,7 +395,7 @@ module ActiveModel # prevent method_missing from calling private methods with #send def guard_private_attribute_method!(method_name, args) if self.class.private_method_defined?(method_name) - raise NoMethodError.new("Attempt to call private method", method_name, args) + raise NoMethodError.new("Attempt to call private method `#{method_name}'", method_name, args) end end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 8c10c54b54..2a1f51a9a7 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -3,49 +3,46 @@ require 'active_support/callbacks' module ActiveModel # == Active Model Callbacks - # + # # Provides an interface for any class to have Active Record like callbacks. - # + # # Like the Active Record methods, the callback chain is aborted as soon as # one of the methods in the chain returns false. # # First, extend ActiveModel::Callbacks from the class you are creating: - # + # # class MyModel # extend ActiveModel::Callbacks # end - # + # # Then define a list of methods that you want callbacks attached to: - # + # # define_model_callbacks :create, :update - # + # # This will provide all three standard callbacks (before, around and after) for - # both the :create and :update methods. To implement, you need to wrap the methods + # both the :create and :update methods. To implement, you need to wrap the methods # 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. - # + # # before_create :action_before_create - # + # # def action_before_create # # Your code here # end - # - # You can choose not to have all three callbacks by passing a hash to the + # + # You can choose not to have all three callbacks by passing a hash to the # define_model_callbacks method. - # + # # define_model_callbacks :create, :only => :after, :before - # + # # Would only create the after_create and before_create callback methods in your # class. module Callbacks @@ -56,50 +53,53 @@ module ActiveModel end # define_model_callbacks accepts the same options define_callbacks does, in case - # you want to overwrite a default. Besides that, it also accepts an :only option, + # you want to overwrite a default. Besides that, it also accepts an :only option, # where you can choose if you want all types (before, around or after) or just some. # # define_model_callbacks :initializer, :only => :after - # + # # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on # that method call. To get around this you can call the define_model_callbacks # method as many times as you need. - # + # # define_model_callbacks :create, :only => :after # define_model_callbacks :update, :only => :before # define_model_callbacks :destroy, :only => :around - # + # # Would create +after_create+, +before_update+ and +around_destroy+ methods only. - # + # # You can pass in a class to before_<type>, after_<type> and around_<type>, in which # case the callback will call that class's <action>_<type> method passing the object # that the callback is being called on. - # + # # class MyModel # extend ActiveModel::Callbacks # define_model_callbacks :create - # + # # before_create AnotherClass # end - # + # # class AnotherClass # def self.before_create( obj ) # # obj is the MyModel instance that the callback is being called on # end # end - # + # def define_model_callbacks(*callbacks) options = callbacks.extract_options! - options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options) + options = { + :terminator => "result == false", + :scope => [:kind, :name], + :only => [:before, :around, :after] + }.merge(options) - types = Array.wrap(options.delete(:only)) - types = [:before, :around, :after] if types.empty? + types = Array.wrap(options.delete(:only)) callbacks.each do |callback| 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/conversion.rb b/activemodel/lib/active_model/conversion.rb index d2bd160dc7..e3992e842a 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -3,8 +3,6 @@ module ActiveModel # # Handles default conversions: to_model, to_key and to_param. # - # == Example - # # Let's take for example this non persisted object. # # class ContactMessage @@ -22,19 +20,22 @@ module ActiveModel # cm.to_param # => nil # module Conversion - # If your object is already designed to implement all of the Active Model - # you can use the default to_model implementation, which simply returns + # If your object is already designed to implement all of the Active Model + # you can use the default to_model implementation, which simply returns # self. - # - # If your model does not act like an Active Model object, then you should - # define <tt>:to_model</tt> yourself returning a proxy object that wraps + # + # If your model does not act like an Active Model object, then you should + # define <tt>:to_model</tt> yourself returning a proxy object that wraps # your object with Active Model compliant methods. def to_model self end - # Returns an Enumerable of all (primary) key attributes or nil if - # persisted? is false + # Returns an Enumerable of all key attributes if any is set, regardless + # if the object is persisted or not. + # + # Note the default implementation uses persisted? just because all objects + # in Ruby 1.8.x responds to :id. def to_key persisted? ? [id] : nil end @@ -42,7 +43,7 @@ module ActiveModel # Returns a string representing the object's key suitable for use in URLs, # or nil if persisted? is false def to_param - to_key ? to_key.join('-') : nil + persisted? ? to_key.join('-') : nil end end end diff --git a/activemodel/lib/active_model/deprecated_error_methods.rb b/activemodel/lib/active_model/deprecated_error_methods.rb deleted file mode 100644 index adc50773d9..0000000000 --- a/activemodel/lib/active_model/deprecated_error_methods.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveModel - module DeprecatedErrorMethods - def on(attribute) - message = "Errors#on have been deprecated, use Errors#[] instead.\n" - message << "Also note that the behaviour of Errors#[] has changed. Errors#[] now always returns an Array. An empty Array is " - message << "returned when there are no errors on the specified attribute." - ActiveSupport::Deprecation.warn(message) - - errors = self[attribute] - errors.size < 2 ? errors.first : errors - end - - def on_base - ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead" - ActiveSupport::Deprecation.silence { on(:base) } - end - - def add_to_base(msg) - ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#add(:base, msg) instead" - self[:base] << msg - end - - def invalid?(attribute) - ActiveSupport::Deprecation.warn "Errors#invalid?(attribute) has been deprecated, use Errors#[attribute].any? instead" - self[attribute].any? - end - - def each_full - ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead" - to_a.each { |error| yield error } - end - end -end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 2516377afd..a479795d51 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,53 +1,52 @@ require 'active_model/attribute_methods' -require 'active_support/concern' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' module ActiveModel # == Active Model Dirty # - # Provides a way to track changes in your object in the same way as + # Provides a way to track changes in your object in the same way as # Active Record does. - # + # # The requirements to implement ActiveModel::Dirty are to: # # * <tt>include ActiveModel::Dirty</tt> in your object - # * Call <tt>define_attribute_methods</tt> passing each method you want to + # * Call <tt>define_attribute_methods</tt> passing each method you want to # track - # * Call <tt>attr_name_will_change!</tt> before each change to the tracked + # * Call <tt>attr_name_will_change!</tt> before each change to the tracked # attribute - # - # If you wish to also track previous changes on save or update, you need to + # + # If you wish to also track previous changes on save or update, you need to # add - # + # # @previously_changed = changes - # + # # inside of your save or update method. - # + # # A minimal implementation could be: - # + # # class Person - # + # # include ActiveModel::Dirty - # + # # define_attribute_methods [:name] - # + # # def name # @name # end - # + # # def name=(val) # name_will_change! unless val == @name # @name = val # end - # + # # def save # @previously_changed = changes # @changed_attributes.clear # end - # + # # end - # + # # == Examples: # # A newly instantiated object is unchanged: @@ -79,7 +78,7 @@ module ActiveModel # person.changes # => { 'name' => ['Bill', 'Bob'] } # # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> - # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to + # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to # in-place attributes. # # person.name_will_change! @@ -115,7 +114,7 @@ module ActiveModel # person.name = 'bob' # person.changes # => { 'name' => ['bill', 'bob'] } def changes - changed.inject(HashWithIndifferentAccess.new){ |h, attr| h[attr] = attribute_change(attr); h } + HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end # Map of attributes that were changed when the model was saved. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 272ddb1554..c2f0228785 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -12,77 +12,96 @@ module ActiveModel # # Provides a modified +OrderedHash+ that you can include in your object # for handling error messages and interacting with Action Pack helpers. - # + # # A minimal implementation could be: - # + # # class Person - # + # # # Required dependency for ActiveModel::Errors # extend ActiveModel::Naming - # + # # def initialize # @errors = ActiveModel::Errors.new(self) # end - # + # # attr_accessor :name # attr_reader :errors - # + # # def validate! # errors.add(:name, "can not be nil") if name == nil # end - # + # # # The following methods are needed to be minimally implemented # # def read_attribute_for_validation(attr) # send(attr) # end - # + # # def Person.human_attribute_name(attr, options = {}) # attr # end - # + # # def Person.lookup_ancestors # [self] # end - # + # # end - # + # # The last three methods are required in your object for Errors to be # able to generate error messages correctly and also handle multiple # languages. Of course, if you extend your object with ActiveModel::Translations # you will not need to implement the last two. Likewise, using # ActiveModel::Validations will handle the validation related methods # for you. - # + # # The above allows you to do: - # + # # p = Person.new # p.validate! # => ["can not be nil"] # p.errors.full_messages # => ["name can not be nil"] # # etc.. - class Errors < ActiveSupport::OrderedHash - include DeprecatedErrorMethods + 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 # def initialize # @errors = ActiveModel::Errors.new(self) # 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 + + # Get messages for +key+ + def get(key) + messages[key] end - alias_method :get, :[] - alias_method :set, :[]= + # 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 + # When passed a symbol or a name of a method, returns an array of errors # for the method. - # + # # p.errors[:name] # => ["can not be nil"] # p.errors['name'] # => ["can not be nil"] def [](attribute) @@ -90,7 +109,7 @@ module ActiveModel end # Adds to the supplied attribute the supplied error message. - # + # # p.errors[:name] = "must be set" # p.errors[:name] # => ['must be set'] def []=(attribute, error) @@ -100,25 +119,25 @@ module ActiveModel # Iterates through each error key, value pair in the error messages hash. # Yields the attribute and the error for that attribute. If the attribute # 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| # # Will yield :name and "can't be blank" # end - # + # # p.errors.add(:name, "must be specified") # p.errors.each do |attribute, errors_array| # # Will yield :name and "can't be blank" # # 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 # Returns the number of error messages. - # + # # p.errors.add(:name, "can't be blank") # p.errors.size # => 1 # p.errors.add(:name, "must be specified") @@ -127,8 +146,18 @@ 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") # p.errors.add(:name, "must be specified") # p.errors.to_a # => ["name can't be blank", "name must be specified"] @@ -149,9 +178,9 @@ 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") # p.errors.add(:name, "must be specified") # p.errors.to_xml @@ -167,14 +196,18 @@ module ActiveModel # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. def as_json(options=nil) - self + to_hash + end + + def to_hash + messages.dup end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to # <tt>on(attribute)</tt> for the same attribute. More than one error can be added to the same # +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. # If no +message+ is supplied, <tt>:invalid</tt> is assumed. - # + # # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+). # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error. def add(attribute, message = nil, options = {}) @@ -191,13 +224,6 @@ module ActiveModel # Will add an error message to each of the attributes in +attributes+ that is empty. def add_on_empty(attributes, options = {}) - if options && !options.is_a?(Hash) - options = { :message => options } - ActiveSupport::Deprecation.warn \ - "ActiveModel::Errors#add_on_empty(attributes, custom_message) has been deprecated.\n" + - "Instead of passing a custom_message pass an options Hash { :message => custom_message }." - end - [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) is_empty = value.respond_to?(:empty?) ? value.empty? : false @@ -207,13 +233,6 @@ module ActiveModel # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). def add_on_blank(attributes, options = {}) - if options && !options.is_a?(Hash) - options = { :message => options } - ActiveSupport::Deprecation.warn \ - "ActiveModel::Errors#add_on_blank(attributes, custom_message) has been deprecated.\n" + - "Instead of passing a custom_message pass an options Hash { :message => custom_message }." - end - [attributes].flatten.each do |attribute| value = @base.send(:read_attribute_for_validation, attribute) add(attribute, :blank, options) if value.blank? @@ -231,41 +250,35 @@ 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 + # Translates an error message in its default scope # (<tt>activemodel.errors.messages</tt>). # - # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, - # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not - # there also, it returns the translation of the default message + # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, + # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not + # there also, it returns the translation of the default message # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name, # translated attribute name and the value are available for interpolation. # # When using inheritance in your models, it will check all the inherited # models too, but only if the model itself hasn't been found. Say you have - # <tt>class Admin < User; end</tt> and you wanted the translation for - # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, + # <tt>class Admin < User; end</tt> and you wanted the translation for + # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, # it looks for these translations: # # <ol> @@ -281,13 +294,6 @@ module ActiveModel def generate_message(attribute, type = :invalid, options = {}) type = options.delete(:message) if options[:message].is_a?(Symbol) - if options[:default] - ActiveSupport::Deprecation.warn \ - "ActiveModel::Errors#generate_message(attributes, custom_message) has been deprecated.\n" + - "Use ActiveModel::Errors#generate_message(attributes, :message => 'your message') instead." - options[:message] = options.delete(:default) - end - defaults = @base.class.lookup_ancestors.map do |klass| [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}", :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ] diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index fe650053d9..b71ef4b22e 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -1,19 +1,19 @@ -# == Active Model Lint Tests -# -# You can test whether an object is compliant with the Active Model API by -# including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will include -# tests that tell you whether your object is fully compliant, or if not, -# which aspects of the API are not implemented. -# -# These tests do not attempt to determine the semantic correctness of the -# returned values. For instance, you could implement valid? to always -# return true, and the tests would pass. It is up to you to ensure that -# the values are semantically meaningful. -# -# Objects you pass in are expected to return a compliant object from a -# call to to_model. It is perfectly fine for to_model to return self. module ActiveModel module Lint + # == Active Model Lint Tests + # + # You can test whether an object is compliant with the Active Model API by + # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will include + # tests that tell you whether your object is fully compliant, or if not, + # which aspects of the API are not implemented. + # + # These tests do not attempt to determine the semantic correctness of the + # returned values. For instance, you could implement valid? to always + # return true, and the tests would pass. It is up to you to ensure that + # the values are semantically meaningful. + # + # Objects you pass in are expected to return a compliant object from a + # call to to_model. It is perfectly fine for to_model to return self. module Tests # == Responds to <tt>to_key</tt> @@ -23,7 +23,7 @@ module ActiveModel def test_to_key assert model.respond_to?(:to_key), "The model should respond to to_key" def model.persisted?() false end - assert model.to_key.nil? + assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end # == Responds to <tt>to_param</tt> @@ -38,8 +38,9 @@ module ActiveModel # not persisted?, then to_param should always return nil. def test_to_param assert model.respond_to?(:to_param), "The model should respond to to_param" + def model.to_key() [1] end def model.persisted?() false end - assert model.to_param.nil? + assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end # == Responds to <tt>valid?</tt> @@ -79,7 +80,7 @@ module ActiveModel end # == Errors Testing - # + # # Returns an object that has :[] and :full_messages defined on it. See below # for more details. # diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 66cd9fdde6..be48415739 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 @@ -54,9 +54,7 @@ module ActiveModel # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. - # - # == Example + # tampering with URLs or forms. Example: # # class Customer # include ActiveModel::MassAssignmentSecurity diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index b74d669f0a..eb9b847509 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,19 +1,25 @@ require 'active_support/inflector' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/module/introspection' module ActiveModel class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path + attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key alias_method :cache_key, :collection - def initialize(klass) + def initialize(klass, namespace = nil) super(klass.name) + @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace + @klass = klass - @singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze + @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 + @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze end # Transform the model name into a more humane format, using I18n. By default, @@ -30,35 +36,43 @@ module ActiveModel klass.model_name.underscore.to_sym end - defaults << options.delete(:default) if options[:default] + defaults << options[:default] if options[:default] defaults << @human - options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults + options = {:scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults}.merge(options.except(:default)) I18n.translate(defaults.shift, options) end + + private + def _singularize(str) + ActiveSupport::Inflector.underscore(str).tr('/', '_') + end end # == Active Model Naming # # Creates a +model_name+ method on your object. - # + # # To implement, just extend ActiveModel::Naming in your object: - # + # # class BookCover # extend ActiveModel::Naming # end - # + # # BookCover.model_name # => "BookCover" # BookCover.model_name.human # => "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. def model_name - @_model_name ||= ActiveModel::Name.new(self) + @_model_name ||= begin + namespace = self.parents.detect { |n| n.respond_to?(:_railtie) } + ActiveModel::Name.new(self, namespace) + end end # Returns the plural class name of a record or class. Examples: @@ -85,10 +99,38 @@ module ActiveModel plural(record_or_class) == singular(record_or_class) end + # Returns string to use while generating route names. It differs for + # namespaced models regarding whether it's inside isolated engine. + # + # For isolated engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> posts + # + # For shared engine: + # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + def self.route_key(record_or_class) + model_name_from_record_or_class(record_or_class).route_key + end + + # Returns string to use for params names. It differs for + # namespaced models regarding whether it's inside isolated engine. + # + # For isolated engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> post + # + # For shared engine: + # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post + def self.param_key(record_or_class) + model_name_from_record_or_class(record_or_class).param_key + end + private def self.model_name_from_record_or_class(record_or_class) - (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name + (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name + end + + def self.convert_to_model(object) + object.respond_to?(:to_model) ? object.to_model : object end end - + end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index c6a79acf81..ef36f80bec 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -10,19 +10,23 @@ module ActiveModel module ClassMethods # == Active Model Observers Activation - # + # # 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. + # Note: Setting this does not instantiate the observers yet. # +instantiate_observers+ is called during startup, and before # each development request. def observers=(*values) @@ -34,36 +38,41 @@ module ActiveModel @observers ||= [] end + # Gets the current observer instances. + def observer_instances + @observer_instances ||= [] + end + # Instantiate the global Active Record observers. def instantiate_observers 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'" end - @observer_instances ||= [] - @observer_instances << observer + observer_instances << observer end + # Notify list of observers of a change. def notify_observers(*arg) - if defined? @observer_instances - for observer in @observer_instances - observer.update(*arg) - end + for observer in observer_instances + observer.update(*arg) end end + # Total number of observers. def count_observers - @observer_instances.size + observer_instances.size end protected def instantiate_observer(observer) #:nodoc: # string/symbol if observer.respond_to?(:to_sym) - observer = observer.to_s.camelize.constantize.instance + observer.to_s.camelize.constantize.instance elsif observer.respond_to?(:instance) observer.instance else @@ -93,7 +102,7 @@ module ActiveModel # == Active Model Observers # - # Observer classes respond to lifecycle callbacks to implement trigger-like + # Observer classes respond to life cycle callbacks to implement trigger-like # behavior outside the original class. This is a great way to reduce the # clutter that normally comes when the model class is burdened with # functionality that doesn't pertain to the core responsibility of the @@ -123,9 +132,9 @@ module ActiveModel # # Observers will by default be mapped to the class with which they share a # name. So CommentObserver will be tied to observing Comment, ProductManagerObserver - # to ProductManager, and so on. If you want to name your observer differently than - # the class you're interested in observing, you can use the Observer.observe class - # method which takes either the concrete class (Product) or a symbol for that + # to ProductManager, and so on. If you want to name your observer differently than + # the class you're interested in observing, you can use the Observer.observe class + # method which takes either the concrete class (Product) or a symbol for that # class (:product): # # class AuditObserver < ActiveModel::Observer @@ -136,7 +145,7 @@ module ActiveModel # end # end # - # If the audit observer needs to watch more than one kind of object, this can be + # If the audit observer needs to watch more than one kind of object, this can be # specified with multiple arguments: # # class AuditObserver < ActiveModel::Observer @@ -147,9 +156,13 @@ module ActiveModel # end # end # - # The AuditObserver will now act on both updates to Account and Balance by treating + # 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 e675937f4d..caf44a2ee0 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -3,25 +3,25 @@ require 'active_support/core_ext/hash/slice' module ActiveModel # == Active Model Serialization - # + # # Provides a basic serialization to a serializable_hash for your object. - # + # # A minimal implementation could be: - # + # # class Person - # + # # include ActiveModel::Serialization - # + # # attr_accessor :name - # + # # def attributes - # @attributes ||= {'name' => 'nil'} + # {'name' => name} # end - # + # # end - # + # # Which would provide you with: - # + # # person = Person.new # person.serializable_hash # => {"name"=>nil} # person.name = "Bob" @@ -29,63 +29,58 @@ module ActiveModel # # You need to declare some sort of attributes hash which contains the attributes # you want to serialize and their current value. - # - # Most of the time though, you will want to include the JSON or XML - # serializations. Both of these modules automatically include the + # + # 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 # include it. - # + # # So a minimal implementation including XML and JSON would be: - # + # # class Person - # + # # include ActiveModel::Serializers::JSON # include ActiveModel::Serializers::Xml - # + # # attr_accessor :name - # + # # def attributes - # @attributes ||= {'name' => 'nil'} + # {'name' => name} # end - # + # # end - # + # # Which would provide you with: - # + # # person = Person.new # person.serializable_hash # => {"name"=>nil} + # person.as_json # => {"name"=>nil} # person.to_json # => "{\"name\":null}" # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - # + # # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} + # person.as_json # => {"name"=>"Bob"} # 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> and <tt>:methods</tt> . module Serialization def serializable_hash(options = nil) options ||= {} - options[:only] = Array.wrap(options[:only]).map { |n| n.to_s } - options[:except] = Array.wrap(options[:except]).map { |n| n.to_s } + only = Array.wrap(options[:only]).map(&:to_s) + except = Array.wrap(options[:except]).map(&:to_s) attribute_names = attributes.keys.sort - if options[:only].any? - attribute_names &= options[:only] - elsif options[:except].any? - attribute_names -= options[:except] - end - - method_names = Array.wrap(options[:methods]).inject([]) do |methods, name| - methods << name if respond_to?(name.to_s) - methods + if only.any? + attribute_names &= only + elsif except.any? + attribute_names -= except end - (attribute_names + method_names).inject({}) { |hash, name| - hash[name] = send(name) - hash - } + method_names = Array.wrap(options[:methods]).map { |n| n if respond_to?(n.to_s) }.compact + Hash[(attribute_names + method_names).map { |n| [n, send(n)] }] end end end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index e1dbc522de..0bfbf2aa06 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -18,17 +18,17 @@ module ActiveModel # Returns a JSON string representing the model. Some configuration can be # passed through +options+. # - # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the - # top-level behavior of +to_json+. If true (the default) +to_json+ will - # emit a single root node named after the object's type. For example: + # The option <tt>include_root_in_json</tt> controls the top-level behavior + # of +as_json+. If true (the default) +as_json+ will emit a single root + # node named after the object's type. For example: # # konata = User.find(1) - # konata.to_json + # konata.as_json # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} } # # ActiveRecord::Base.include_root_in_json = false - # konata.to_json + # konata.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # @@ -39,29 +39,29 @@ module ActiveModel # attributes. For example: # # konata = User.find(1) - # konata.to_json + # konata.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes # included, and work similar to the +attributes+ method. For example: # - # konata.to_json(:only => [ :id, :name ]) + # konata.as_json(:only => [ :id, :name ]) # # => {"id": 1, "name": "Konata Izumi"} # - # konata.to_json(:except => [ :id, :created_at, :age ]) + # konata.as_json(:except => [ :id, :created_at, :age ]) # # => {"name": "Konata Izumi", "awesome": true} # # To include the result of some method calls on the model use <tt>:methods</tt>: # - # konata.to_json(:methods => :permalink) + # konata.as_json(:methods => :permalink) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "permalink": "1-konata-izumi"} # # To include associations use <tt>:include</tt>: # - # konata.to_json(:include => :posts) + # konata.as_json(:include => :posts) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, @@ -69,7 +69,7 @@ module ActiveModel # # Second level and higher order associations work as well: # - # konata.to_json(:include => { :posts => { + # konata.as_json(:include => { :posts => { # :include => { :comments => { # :only => :body } }, # :only => :title } }) @@ -79,18 +79,16 @@ module ActiveModel # "title": "Welcome to the weblog"}, # {"comments": [{"body": "Don't think too hard"}], # "title": "So I was thinking"}]} - def encode_json(encoder) - hash = serializable_hash(encoder.options) + + def as_json(options = nil) + hash = serializable_hash(options) + if include_root_in_json - custom_root = encoder.options && encoder.options[:root] + custom_root = options && options[:root] hash = { custom_root || self.class.model_name.element => hash } end - ActiveSupport::JSON.encode(hash) - end - - def as_json(options = nil) - self + hash end def from_json(json) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index ed64434b8f..d4295e6afe 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -17,7 +17,8 @@ module ActiveModel def initialize(name, serializable, raw_value=nil) @name, @serializable = name, serializable - @value = value || @serializable.send(name) + raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone) + @value = raw_value || @serializable.send(name) @type = compute_type end @@ -52,7 +53,7 @@ module ActiveModel @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> + # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set # for a N level model but is set for the N+1 level models, # then because <tt>:except</tt> is set to a default value, the second @@ -76,10 +77,9 @@ module ActiveModel end def serializable_methods - Array.wrap(options[:methods]).inject([]) do |methods, name| - methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) - methods - end + Array.wrap(options[:methods]).map do |name| + self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) + end.compact end def serialize @@ -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 0facbd6ce1..dbb76244e4 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -3,19 +3,19 @@ require 'active_support/core_ext/hash/reverse_merge' module ActiveModel # == Active Model Translation - # + # # Provides integration between your object and the Rails internationalization # (i18n) framework. - # + # # A minimal implementation could be: - # + # # class TranslatedPerson # extend ActiveModel::Translation # end - # + # # TranslatedPerson.human_attribute_name('my_attribute') # # => "My attribute" - # + # # This also provides the required class methods for hooking into the # Rails internationalization API, including being able to define a # class based i18n_scope and lookup_ancestors to find translations in @@ -28,9 +28,9 @@ module ActiveModel :activemodel end - # When localizing a string, it goes through the lookup returned by this + # When localizing a string, it goes through the lookup returned by this # method, which is used in ActiveModel::Name#human, - # ActiveModel::Errors#full_messages and + # ActiveModel::Errors#full_messages and # ActiveModel::Translation#human_attribute_name. def lookup_ancestors self.ancestors.select { |x| x.respond_to?(:model_name) } @@ -54,11 +54,5 @@ module ActiveModel options.reverse_merge! :count => 1, :default => defaults I18n.translate(defaults.shift, options) end - - # Model.human_name is deprecated. Use Model.model_name.human instead. - def human_name(*args) - ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,5]) - model_name.human(*args) - end end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 3407c59e7a..d968609e67 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -2,30 +2,31 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/hash/except' require 'active_model/errors' require 'active_model/validations/callbacks' module ActiveModel # == Active Model Validations - # + # # Provides a full validation framework to your objects. - # + # # A minimal implementation could be: - # + # # class Person # include ActiveModel::Validations - # + # # attr_accessor :first_name, :last_name # # validates_each :first_name, :last_name do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end - # + # # Which provides you with the full standard validation stack that you # know from Active Record: - # + # # person = Person.new # person.valid? # => true # person.invalid? # => false @@ -34,11 +35,11 @@ module ActiveModel # person.valid? # => false # person.invalid? # => true # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}> - # - # Note that ActiveModel::Validations automatically adds an +errors+ method - # to your instances initialized with a new ActiveModel::Errors object, so + # + # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ method + # to your instances initialized with a new <tt>ActiveModel::Errors</tt> object, so # there is no need for you to do this manually. - # + # module Validations extend ActiveSupport::Concern include ActiveSupport::Callbacks @@ -61,7 +62,7 @@ module ActiveModel # # class Person # include ActiveModel::Validations - # + # # attr_accessor :first_name, :last_name # # validates_each :first_name, :last_name do |record, attr, value| @@ -70,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 @@ -95,15 +96,15 @@ module ActiveModel # # class Comment # include ActiveModel::Validations - # + # # validate :must_be_friends # # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) # 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 @@ -113,7 +114,17 @@ module ActiveModel # end # # def must_be_friends - # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # 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 # @@ -128,15 +139,17 @@ module ActiveModel set_callback(:validate, *args, &block) end - # List all validators that are being used to validate the model using + # List all validators that are being used to validate the model using # +validates_with+ method. def validators _validators.values.flatten.uniq 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. @@ -152,7 +165,7 @@ module ActiveModel end end - # Returns the Errors object that holds all information about attribute error messages. + # Returns the +Errors+ object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) end @@ -168,14 +181,14 @@ module ActiveModel self.validation_context = current_context end - # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, + # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, # false otherwise. def invalid?(context = nil) !valid?(context) end - # Hook method defining how an attribute value should be retrieved. By default - # this is assumed to be an instance named after the attribute. Override this + # Hook method defining how an attribute value should be retrieved. By default + # this is assumed to be an instance named after the attribute. Override this # method in subclasses should you need to retrieve the value for a given # attribute differently: # @@ -194,9 +207,9 @@ module ActiveModel alias :read_attribute_for_validation :send 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 99b8966def..4f390613aa 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -14,8 +14,6 @@ module ActiveModel end def setup(klass) - # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility - # as instance_methods returns symbols unlike 1.8 which returns strings. attr_readers = attributes.reject { |name| klass.attribute_method?(name) } attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } klass.send(:attr_reader, *attr_readers) @@ -24,7 +22,7 @@ module ActiveModel end module HelperMethods - # Encapsulates the pattern of wanting to validate the acceptance of a + # Encapsulates the pattern of wanting to validate the acceptance of a # terms of service check box (or similar agreement). Example: # # class Person < ActiveRecord::Base @@ -32,33 +30,33 @@ module ActiveModel # validates_acceptance_of :eula, :message => "must be abided" # end # - # If the database column does not exist, the +terms_of_service+ attribute + # If the database column does not exist, the +terms_of_service+ attribute # is entirely virtual. This check is performed only if +terms_of_service+ # is not +nil+ and by default on save. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "must be + # * <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. + # * <tt>:accept</tt> - Specifies value that is considered accepted. # The default value is a string "1", which makes it easy to relate to - # an HTML checkbox. This should be set to +true+ if you are validating + # an HTML checkbox. This should be set to +true+ if you are validating # a database column, since the attribute is typecast from "1" to +true+ # before validation. # * <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 + # 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 (for example, - # <tt>:unless => :skip_validation</tt>, or + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (for example, + # <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 + # The method, proc or string should return or evaluate to a true or # false value. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index afd65d3dd5..adc2867ad0 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -14,7 +14,7 @@ module ActiveModel # include ActiveModel::Validations::Callbacks # # before_validation :do_stuff_before_validation - # after_validation :do_tuff_after_validation + # after_validation :do_stuff_after_validation # end # # Like other before_* callbacks if <tt>before_validation</tt> returns false @@ -40,7 +40,7 @@ module ActiveModel options = args.extract_options! options[:prepend] = true options[:if] = Array.wrap(options[:if]) - options[:if] << "!halted && value != false" + options[:if] << "!halted" options[:if] << "self.validation_context == :#{options[:on]}" if options[:on] set_callback(:validation, :after, *(args << options), &block) end @@ -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 3a80893866..e6d10cfff8 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -4,24 +4,26 @@ module ActiveModel module Validations class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) - confirmed = record.send(:"#{attribute}_confirmation") - return if confirmed.nil? || value == confirmed - record.errors.add(attribute, :confirmation, options) + if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) + record.errors.add(attribute, :confirmation, options) + end end def setup(klass) - klass.send(:attr_accessor, *attributes.map { |attribute| :"#{attribute}_confirmation" }) + klass.send(:attr_accessor, *attributes.map do |attribute| + :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation") + end.compact) end end module HelperMethods - # Encapsulates the pattern of wanting to validate a password or email + # Encapsulates the pattern of wanting to validate a password or email # address field with a confirmation. For example: # # Model: # class Person < ActiveRecord::Base # validates_confirmation_of :user_name, :password - # validates_confirmation_of :email_address, + # validates_confirmation_of :email_address, # :message => "should match confirmation" # end # @@ -29,12 +31,12 @@ module ActiveModel # <%= password_field "person", "password" %> # <%= password_field "person", "password_confirmation" %> # - # The added +password_confirmation+ attribute is virtual; it exists only + # The added +password_confirmation+ attribute is virtual; it exists only # as an in-memory attribute for validating the password. To achieve this, - # the validation adds accessors to the model for the confirmation + # the validation adds accessors to the model for the confirmation # attribute. - # - # NOTE: This check is performed only if +password_confirmation+ is not + # + # NOTE: This check is performed only if +password_confirmation+ is not # +nil+, and by default only on save. To require confirmation, make sure # to add a presence check for the confirmation attribute: # @@ -43,16 +45,17 @@ 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 - # method, proc or string should return or evaluate to a true or false + # 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</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 validates_confirmation_of(*attr_names) 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 a7af4f2b4d..72735cfb89 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -34,20 +34,17 @@ module ActiveModel end end end - + def validate_each(record, attribute, value) value = options[:tokenizer].call(value) if value.kind_of?(String) CHECKS.each do |key, validity_check| next unless check_value = options[key] - valid_value = if key == :maximum - value.nil? || value.size.send(validity_check, check_value) - else - value && value.size.send(validity_check, check_value) - end + value ||= [] if key == :maximum - next if valid_value + value_length = value.respond_to?(:length) ? value.length : value.to_s.length + next if value_length.send(validity_check, check_value) errors_options = options.except(*RESERVED_OPTIONS) errors_options[:count] = check_value @@ -87,7 +84,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 b6aff7aa6b..ae576462e6 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -24,7 +24,7 @@ module ActiveModel def validate_each(record, attr_name, value) before_type_cast = "#{attr_name}_before_type_cast" - raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast.to_sym) raw_value ||= value return if options[:allow_nil] && raw_value.nil? @@ -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 3260e6bc5a..7ff42de00b 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -9,7 +9,7 @@ module ActiveModel # validator classes ending in 'Validator'. Note that Rails default # validators can be overridden inside specific classes by creating # custom validator classes in their place such as PresenceValidator. - # + # # Examples of using the default rails validators: # # validates :terms, :acceptance => true @@ -21,25 +21,25 @@ module ActiveModel # validates :age, :numericality => true # validates :username, :presence => true # validates :username, :uniqueness => true - # + # # The power of the +validates+ method comes when using custom validators # and default validators in one call for a given attribute e.g. # # class EmailValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) # record.errors[attribute] << (options[:message] || "is not an email") unless - # value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i # end # end - # + # # class Person # include ActiveModel::Validations # attr_accessor :name, :email - # + # # validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 } # validates :email, :presence => true, :email => true # end - # + # # Validator classes may also exist within the class being validated # allowing custom modules of validators to be included as needed e.g. # @@ -48,21 +48,30 @@ module ActiveModel # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) - # record.errors[attribute] << "must start with 'the'" unless =~ /^the/i + # record.errors[attribute] << "must start with 'the'" unless value =~ /\Athe/i # end # end # # validates :name, :title => true # end # - # The validators hash can also handle regular expressions, ranges and arrays: + # 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. # # validates :email, :format => /@/ # validates :gender, :inclusion => %w(male female) # validates :password, :length => 6..20 # - # Finally, the options :if, :unless, :on, :allow_blank and :allow_nil can be given - # to one specific validator: + # When using shortcut form, ranges and arrays are passed to your + # validator's initializer as +options[:in]+ while other types including + # regular expressions and strings are passed as +options[:with]+ + # + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+ and +:allow_nil+ can be given + # to one specific validator, as a hash: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true # @@ -72,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 @@ -93,18 +103,24 @@ 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 {} when Hash options - when Regexp - { :with => options } when Range, Array { :in => options } + else + { :with => options } end 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..65ae18a769 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. @@ -38,9 +50,9 @@ module ActiveModel # end # # Configuration options: - # * <tt>on</tt> - Specifies when this validation is active + # * <tt>:on</tt> - Specifies when this validation is active # (<tt>:create</tt> or <tt>:update</tt> - # * <tt>if</tt> - Specifies a method, proc or string to call to determine + # * <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/validator.rb b/activemodel/lib/active_model/validator.rb index 163124d531..c5ed8d22d3 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -5,9 +5,9 @@ require 'active_support/core_ext/object/blank' module ActiveModel #:nodoc: # == Active Model Validator - # - # A simple base class that can be used along with - # +ActiveModel::Validations::ClassMethods.validates_with+ + # + # A simple base class that can be used along with + # ActiveModel::Validations::ClassMethods.validates_with # # class Person # include ActiveModel::Validations @@ -42,7 +42,7 @@ module ActiveModel #:nodoc: # end # end # - # To cause a validation error, you must add to the <tt>record<tt>'s errors directly + # To cause a validation error, you must add to the <tt>record</tt>'s errors directly # from within the validators message # # class MyValidator < ActiveModel::Validator @@ -61,31 +61,31 @@ module ActiveModel #:nodoc: # @my_custom_field = options[:field_name] || :first_name # end # end - # + # # The easiest way to add custom validators for validating individual attributes - # is with the convenient ActiveModel::EachValidator for example: - # + # is with the convenient <tt>ActiveModel::EachValidator</tt>. For example: + # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value) # end # end - # + # # This can now be used in combination with the +validates+ method - # (see ActiveModel::Validations::ClassMethods.validates for more on this) - # + # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this) + # # class Person # include ActiveModel::Validations # attr_accessor :title - # - # validates :title, :presence => true, :title => true + # + # validates :title, :presence => true # end - # + # # Validator may also define a +setup+ instance method which will get called - # with the class that using that validator as it's argument. This can be - # useful when there are prerequisites such as an attr_accessor being present + # with the class that using that validator as its argument. This can be + # useful when there are prerequisites such as an +attr_accessor+ being present # for example: - # + # # class MyValidator < ActiveModel::Validator # def setup(klass) # klass.send :attr_accessor, :custom_attribute @@ -94,13 +94,11 @@ module ActiveModel #:nodoc: # # This setup method is only called when used with validation macros or the # class level <tt>validates_with</tt> method. - # + # class Validator attr_reader :options - # Returns the kind of the validator. - # - # == Examples + # Returns the kind of the validator. Examples: # # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness @@ -122,18 +120,18 @@ module ActiveModel #:nodoc: # Override this method in subclasses with validation logic, adding errors # to the records +errors+ array where necessary. def validate(record) - raise NotImplementedError + raise NotImplementedError, "Subclasses must implement a validate(record) method." end end - # EachValidator is a validator which iterates through the attributes given - # in the options hash invoking the validate_each method passing in the + # +EachValidator+ is a validator which iterates through the attributes given + # in the options hash invoking the <tt>validate_each</tt> method passing in the # record, attribute and value. # - # All Active Model validations are built on top of this Validator. + # All Active Model validations are built on top of this validator. class EachValidator < Validator attr_reader :attributes - + # Returns a new validator instance. All options will be available via the # +options+ reader, however the <tt>:attributes</tt> option will be removed # and instead be made available through the +attributes+ reader. @@ -158,19 +156,18 @@ module ActiveModel #:nodoc: # Override this method in subclasses with the validation logic, adding # errors to the records +errors+ array where necessary. def validate_each(record, attribute, value) - raise NotImplementedError + raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method" end # Hook method that gets called by the initializer allowing verification # that the arguments supplied are valid. You could for example raise an - # ArgumentError when invalid options are supplied. + # +ArgumentError+ when invalid options are supplied. def check_validity! end end - # BlockValidator is a special EachValidator which receives a block on initialization - # and call this block for each attribute being validated. +validates_each+ uses this - # Validator. + # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization + # and call this block for each attribute being validated. +validates_each+ uses this validator. class BlockValidator < EachValidator def initialize(options, &block) @block = block diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index f2f4b15520..23ba42bf35 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,10 +1,10 @@ module ActiveModel module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index c60caf90d7..022c6716bd 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -2,9 +2,15 @@ require 'cases/helper' class ModelWithAttributes include ActiveModel::AttributeMethods - + attribute_method_suffix '' + class << self + define_method(:bar) do + 'original bar' + end + end + def attributes { :foo => 'value of foo' } end @@ -17,29 +23,110 @@ end class ModelWithAttributes2 include ActiveModel::AttributeMethods - + 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 ModelWithWeirdNamesAttributes + include ActiveModel::AttributeMethods + + attribute_method_suffix '' + + class << self + define_method(:'c?d') do + 'original c?d' + end + end + + def attributes + { :'a?b' => 'value of a?b' } + 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_method generates attribute method with invalid identifier characters' do + ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') + ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') + + assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b' + assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b') + 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 '#define_attr_method generates attribute method' do + ModelWithAttributes.define_attr_method(:bar, 'bar') + + assert_respond_to ModelWithAttributes, :bar + assert_equal "original bar", ModelWithAttributes.original_bar + assert_equal "bar", ModelWithAttributes.bar + ModelWithAttributes.define_attr_method(:bar) + assert !ModelWithAttributes.bar + end + + test '#define_attr_method generates attribute method with invalid identifier characters' do + ModelWithWeirdNamesAttributes.define_attr_method(:'c?d', 'c?d') + + assert_respond_to ModelWithWeirdNamesAttributes, :'c?d' + assert_equal "original c?d", ModelWithWeirdNamesAttributes.send('original_c?d') + assert_equal "c?d", ModelWithWeirdNamesAttributes.send('c?d') + 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 9675b5d450..086e7266ff 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -16,6 +16,8 @@ class CallbacksTest < ActiveModel::TestCase define_model_callbacks :create define_model_callbacks :initialize, :only => :after + define_model_callbacks :multiple, :only => [:before, :around] + define_model_callbacks :empty, :only => [] before_create :before_create around_create CallbackValidator.new @@ -35,7 +37,7 @@ class CallbacksTest < ActiveModel::TestCase end def create - _run_create_callbacks do + run_callbacks :create do @callbacks << :create @valid end @@ -67,4 +69,46 @@ class CallbacksTest < ActiveModel::TestCase assert !ModelCallbacks.respond_to?(:around_initialize) assert_respond_to ModelCallbacks, :after_initialize end + + test "only selects which types of callbacks should be created from an array list" do + assert_respond_to ModelCallbacks, :before_multiple + assert_respond_to ModelCallbacks, :around_multiple + assert !ModelCallbacks.respond_to?(:after_multiple) + end + + test "no callbacks should be created" do + assert !ModelCallbacks.respond_to?(:before_empty) + assert !ModelCallbacks.respond_to?(:around_empty) + assert !ModelCallbacks.respond_to?(:after_empty) + end + + class Violin + attr_reader :history + def initialize + @history = [] + end + extend ActiveModel::Callbacks + define_model_callbacks :create + def callback1; self.history << 'callback1'; end + def callback2; self.history << 'callback2'; end + def create + run_callbacks(:create) {} + self + end + end + class Violin1 < Violin + after_create :callback1, :callback2 + end + class Violin2 < Violin + after_create :callback1 + after_create :callback2 + end + + test "after_create callbacks with both callbacks declared in one line" do + assert_equal ["callback1", "callback2"], Violin1.new.create.history + end + test "after_create callbacks with both callbacks declared in differnt lines" do + assert_equal ["callback1", "callback2"], Violin2.new.create.history + end + end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 79b45bb298..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 @@ -62,4 +74,9 @@ class ErrorsTest < ActiveModel::TestCase end + test 'to_hash should return an ordered hash' do + person = Person.new + person.errors.add(:name, "can not be blank") + assert_instance_of ActiveSupport::OrderedHash, person.errors.to_hash + end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index a32f11484a..01f0158678 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -6,16 +6,9 @@ $:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) require 'config' require 'active_model' require 'active_support/core_ext/string/access' -require 'active_support/core_ext/hash/except' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true require 'rubygems' require 'test/unit' - -begin - require 'ruby-debug' - Debugger.start -rescue LoadError -end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index c25b0fdf00..f84e55e8d9 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -25,13 +25,13 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase end def test_mass_assignment_protection_inheritance - assert LoosePerson.accessible_attributes.blank? + assert_blank LoosePerson.accessible_attributes assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes - assert LooseDescendant.accessible_attributes.blank? + assert_blank LooseDescendant.accessible_attributes assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes - assert LooseDescendantSecond.accessible_attributes.blank? + assert_blank LooseDescendantSecond.accessible_attributes assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections' diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 5a8bff378a..a7dde2c433 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -2,6 +2,7 @@ require 'cases/helper' require 'models/contact' require 'models/sheep' require 'models/track_back' +require 'models/blog_post' class NamingTest < ActiveModel::TestCase def setup @@ -27,6 +28,90 @@ class NamingTest < ActiveModel::TestCase def test_partial_path assert_equal 'post/track_backs/track_back', @model_name.partial_path end + + def test_human + assert_equal 'Track back', @model_name.human + end +end + +class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase + def setup + @model_name = ActiveModel::Name.new(Blog::Post, Blog) + end + + def test_singular + assert_equal 'blog_post', @model_name.singular + end + + def test_plural + assert_equal 'blog_posts', @model_name.plural + end + + def test_element + assert_equal 'post', @model_name.element + end + + def test_collection + assert_equal 'blog/posts', @model_name.collection + end + + def test_partial_path + assert_equal 'blog/posts/post', @model_name.partial_path + end + + def test_human + assert_equal 'Post', @model_name.human + end + + def test_route_key + assert_equal 'posts', @model_name.route_key + end + + def test_param_key + assert_equal 'post', @model_name.param_key + end + + def test_recognizing_namespace + assert_equal 'Post', Blog::Post.model_name.instance_variable_get("@unnamespaced") + end +end + +class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase + def setup + @model_name = ActiveModel::Name.new(Blog::Post) + end + + def test_singular + assert_equal 'blog_post', @model_name.singular + end + + def test_plural + assert_equal 'blog_posts', @model_name.plural + end + + def test_element + assert_equal 'post', @model_name.element + end + + def test_collection + assert_equal 'blog/posts', @model_name.collection + end + + def test_partial_path + assert_equal 'blog/posts/post', @model_name.partial_path + end + + def test_human + assert_equal 'Post', @model_name.human + end + + def test_route_key + assert_equal 'blog_posts', @model_name.route_key + end + + def test_param_key + assert_equal 'blog_post', @model_name.param_key + end end class NamingHelpersTest < Test::Unit::TestCase @@ -36,6 +121,12 @@ class NamingHelpersTest < Test::Unit::TestCase @singular = 'contact' @plural = 'contacts' @uncountable = Sheep + @route_key = 'contacts' + @param_key = 'contact' + end + + def test_to_model_called_on_record + assert_equal 'post_named_track_backs', plural(Post::TrackBack.new) end def test_singular @@ -54,6 +145,22 @@ class NamingHelpersTest < Test::Unit::TestCase assert_equal @plural, plural(@klass) end + def test_route_key + assert_equal @route_key, route_key(@record) + end + + def test_route_key_for_class + assert_equal @route_key, route_key(@klass) + end + + def test_param_key + assert_equal @param_key, param_key(@record) + end + + def test_param_key_for_class + assert_equal @param_key, param_key(@klass) + end + def test_uncountable assert uncountable?(@uncountable), "Expected 'sheep' to be uncoutable" assert !uncountable?(@klass), "Expected 'contact' to be countable" 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/serialization_test.rb b/activemodel/test/cases/serialization_test.rb new file mode 100644 index 0000000000..8cc1ccb1e7 --- /dev/null +++ b/activemodel/test/cases/serialization_test.rb @@ -0,0 +1,45 @@ +require "cases/helper" + +class SerializationTest < ActiveModel::TestCase + class User + include ActiveModel::Serialization + + attr_accessor :name, :email, :gender + + def attributes + @attributes ||= {'name' => 'nil', 'email' => 'nil', 'gender' => 'nil'} + end + + def foo + 'i_am_foo' + end + end + + setup do + @user = User.new + @user.name = 'David' + @user.email = 'david@example.com' + @user.gender = 'male' + end + + def test_method_serializable_hash_should_work + expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} + assert_equal expected , @user.serializable_hash + end + + def test_method_serializable_hash_should_work_with_only_option + expected = {"name"=>"David"} + assert_equal expected , @user.serializable_hash(:only => [:name]) + end + + def test_method_serializable_hash_should_work_with_except_option + expected = {"gender"=>"male", "email"=>"david@example.com"} + assert_equal expected , @user.serializable_hash(:except => [:name]) + end + + def test_method_serializable_hash_should_work_with_methods_option + expected = {"name"=>"David", "gender"=>"male", :foo=>"i_am_foo", "email"=>"david@example.com"} + assert_equal expected , @user.serializable_hash(:methods => [:foo]) + end + +end diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializeration/json_serialization_test.rb index 1ac991a8f1..500a5c575f 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializeration/json_serialization_test.rb @@ -1,10 +1,12 @@ require 'cases/helper' require 'models/contact' +require 'models/automobile' require 'active_support/core_ext/object/instance_variables' class Contact extend ActiveModel::Naming include ActiveModel::Serializers::JSON + include ActiveModel::Validations def attributes instance_values @@ -104,16 +106,43 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should return OrderedHash for errors" do - car = Automobile.new - - # run the validation - car.valid? - + contact = Contact.new + contact.errors.add :name, "can't be blank" + contact.errors.add :name, "is too short (minimum is 2 characters)" + contact.errors.add :age, "must be 16 or over" + hash = ActiveSupport::OrderedHash.new - hash[:make] = "can't be blank" - hash[:model] = "is too short (minimum is 2 characters)" - assert_equal hash.to_json, car.errors.to_json + hash[:name] = ["can't be blank", "is too short (minimum is 2 characters)"] + hash[:age] = ["must be 16 or over"] + assert_equal hash.to_json, contact.errors.to_json + end + + test "serializable_hash should not modify options passed in argument" do + options = { :except => :name } + @contact.serializable_hash(options) + + assert_nil options[:only] + assert_equal :name, options[:except] + end + + test "as_json should return a hash" do + json = @contact.as_json + + assert_kind_of Hash, json + assert_kind_of Hash, json['contact'] + %w(name age created_at awesome preferences).each do |field| + assert_equal @contact.send(field), json['contact'][field] + end end - - + + test "custom as_json should be honored when generating json" do + def @contact.as_json(options); { :name => name, :created_at => created_at }; end + json = @contact.to_json + + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json + assert_no_match %r{"awesome":}, json + assert_no_match %r{"preferences":}, json + end + end diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializeration/xml_serialization_test.rb index ff786658b7..b6a2f88667 100644 --- a/activemodel/test/cases/serializeration/xml_serialization_test.rb +++ b/activemodel/test/cases/serializeration/xml_serialization_test.rb @@ -70,6 +70,13 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<CreatedAt}, @xml end + test "should allow lower-camelized tags" do + @xml = @contact.to_xml :root => 'xml_contact', :camelize => :lower + assert_match %r{^<xmlContact>}, @xml + assert_match %r{</xmlContact>$}, @xml + assert_match %r{<createdAt}, @xml + end + test "should allow skipped types" do @xml = @contact.to_xml :skip_types => true assert_match %r{<age>25</age>}, @xml @@ -107,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 e25d308ca1..1b1d972d5c 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -6,7 +6,7 @@ class ActiveModelI18nTests < ActiveModel::TestCase def setup I18n.backend = I18n::Backend::Simple.new end - + def test_translated_model_attributes I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } } assert_equal 'person name attribute', Person.human_attribute_name('name') @@ -16,7 +16,24 @@ class ActiveModelI18nTests < ActiveModel::TestCase I18n.backend.store_translations 'en', :attributes => { :name => 'name default attribute' } assert_equal 'name default attribute', Person.human_attribute_name('name') end - + + def test_translated_model_attributes_using_default_option + assert_equal 'name default attribute', Person.human_attribute_name('name', :default => "name default attribute") + end + + def test_translated_model_attributes_using_default_option_as_symbol + I18n.backend.store_translations 'en', :default_name => 'name default attribute' + assert_equal 'name default attribute', Person.human_attribute_name('name', :default => :default_name) + end + + def test_translated_model_attributes_falling_back_to_default + assert_equal 'Name', Person.human_attribute_name('name') + end + + def test_translated_model_attributes_using_default_option_as_symbol_and_falling_back_to_default + assert_equal 'Name', Person.human_attribute_name('name', :default => :default_name) + end + def test_translated_model_attributes_with_symbols I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } } assert_equal 'person name attribute', Person.human_attribute_name(:name) @@ -32,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 @@ -46,5 +70,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} } assert_equal 'person model', Child.model_name.human end + + def test_human_does_not_modify_options + options = {:default => 'person model'} + Person.model_name.human(options) + assert_equal({:default => 'person model'}, options) + 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/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 1e6180a938..f02514639b 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -342,6 +342,17 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["Your essay must be at least 5 words."], t.errors[:content] end + def test_validates_length_of_for_fixnum + Topic.validates_length_of(:approved, :is => 4) + + t = Topic.new("title" => "uhohuhoh", "content" => "whatever", :approved => 1) + assert t.invalid? + assert t.errors[:approved].any? + + t = Topic.new("title" => "uhohuhoh", "content" => "whatever", :approved => 1234) + assert t.valid? + end + def test_validates_length_of_for_ruby_class Person.validates_length_of :karma, :minimum => 5 diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index e1d7d40c47..08f6169ca5 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -14,7 +14,7 @@ class NumericalityValidationTest < ActiveModel::TestCase NIL = [nil] BLANK = ["", " ", " \t \r \n"] - BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significent digits + BIGDECIMAL_STRINGS = %w(12345678901234567890.1234567890) # 30 significant digits FLOAT_STRINGS = %w(0.0 +0.0 -0.0 10.0 10.5 -10.5 -0.0001 -090.1 90.1e1 -90.1e5 -90.1e-5 90e-5) INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index b85e491a9c..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,13 +23,24 @@ 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 person.valid? assert_equal ['my custom message'], person.errors[:salary] end - + def test_validates_with_validator_class Person.validates :karma, :email => true 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 @@ -93,22 +114,40 @@ class ValidatesTest < ActiveModel::TestCase person.valid? assert_equal ['my custom message'], person.errors[:karma] end - + def test_validates_with_unknown_validator assert_raise(ArgumentError) { Person.validates :karma, :unknown => true } end - + def test_validates_with_included_validator PersonWithValidator.validates :title, :presence => true person = PersonWithValidator.new person.valid? assert_equal ['Local validator'], person.errors[:title] end - + def test_validates_with_included_validator_and_options PersonWithValidator.validates :title, :presence => { :custom => ' please' } person = PersonWithValidator.new person.valid? assert_equal ['Local validator please'], person.errors[:title] end + + def test_validates_with_included_validator_and_wildcard_shortcut + # Shortcut for PersonWithValidator.validates :title, :like => { :with => "Mr." } + PersonWithValidator.validates :title, :like => "Mr." + person = PersonWithValidator.new + person.title = "Ms. Pacman" + 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 8d6bdeb6a5..2f36195627 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -25,9 +25,11 @@ class ValidationsTest < ActiveModel::TestCase r = Reply.new r.title = "There's no content!" assert r.invalid?, "A reply without content shouldn't be saveable" + assert r.after_validation_performed, "after_validation callback should be called" r.content = "Messa content!" assert r.valid?, "A reply with content should be saveable" + assert r.after_validation_performed, "after_validation callback should be called" end def test_single_attr_validation_and_error_msg @@ -146,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? @@ -170,10 +180,10 @@ class ValidationsTest < ActiveModel::TestCase assert_match %r{<errors>}, xml assert_match %r{<error>Title can't be blank</error>}, xml assert_match %r{<error>Content can't be blank</error>}, xml - + hash = ActiveSupport::OrderedHash.new - hash[:title] = "can't be blank" - hash[:content] = "can't be blank" + hash[:title] = ["can't be blank"] + hash[:content] = ["can't be blank"] assert_equal t.errors.to_json, hash.to_json end @@ -185,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 => '' @@ -213,42 +223,6 @@ class ValidationsTest < ActiveModel::TestCase assert !t.invalid? end - def test_deprecated_error_messages_on - Topic.validates_presence_of :title - - t = Topic.new - assert t.invalid? - - [:title, "title"].each do |attribute| - assert_deprecated { assert_equal "can't be blank", t.errors.on(attribute) } - end - - Topic.validates_each(:title) do |record, attribute| - record.errors[attribute] << "invalid" - end - - assert t.invalid? - - [:title, "title"].each do |attribute| - assert_deprecated do - assert t.errors.on(attribute).include?("invalid") - assert t.errors.on(attribute).include?("can't be blank") - end - end - end - - def test_deprecated_errors_on_base_and_each - t = Topic.new - assert t.valid? - - assert_deprecated { t.errors.add_to_base "invalid topic" } - assert_deprecated { assert_equal "invalid topic", t.errors.on_base } - assert_deprecated { assert t.errors.invalid?(:base) } - - all_errors = t.errors.to_a - assert_deprecated { assert_equal all_errors, t.errors.each_full{|err| err} } - end - def test_validation_with_message_as_proc Topic.validates_presence_of(:title, :message => proc { "no blanks here".upcase }) @@ -280,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/blog_post.rb b/activemodel/test/models/blog_post.rb new file mode 100644 index 0000000000..d289177259 --- /dev/null +++ b/activemodel/test/models/blog_post.rb @@ -0,0 +1,13 @@ +module Blog + def self._railtie + Object.new + end + + def self.table_name_prefix + "blog_" + end + + class Post + extend ActiveModel::Naming + end +end diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb index 14a8be9ebc..2fbcf79c3d 100644 --- a/activemodel/test/models/custom_reader.rb +++ b/activemodel/test/models/custom_reader.rb @@ -4,11 +4,11 @@ class CustomReader def initialize(data = {}) @data = data end - + def []=(key, value) @data[key] = value end - + def read_attribute_for_validation(key) @data[key] end diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb index cf16a38618..e896e90f98 100644 --- a/activemodel/test/models/person.rb +++ b/activemodel/test/models/person.rb @@ -9,5 +9,9 @@ class Person end end +class Person::Gender + extend ActiveModel::Translation +end + class Child < Person end diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb index f9763ea853..505ed880c1 100644 --- a/activemodel/test/models/person_with_validator.rb +++ b/activemodel/test/models/person_with_validator.rb @@ -1,11 +1,24 @@ class PersonWithValidator include ActiveModel::Validations - + class PresenceValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank? end end + class LikeValidator < ActiveModel::EachValidator + def initialize(options) + @with = options[:with] + super + end + + def validate_each(record, attribute, value) + unless value[@with] + record.errors.add attribute, "does not appear to be like #{@with}" + end + end + end + attr_accessor :title, :karma end diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb index 175dbe6477..7aba055c4f 100644 --- a/activemodel/test/models/sheep.rb +++ b/activemodel/test/models/sheep.rb @@ -1,4 +1,3 @@ class Sheep extend ActiveModel::Naming end -
\ No newline at end of file diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index f25b774cd7..c9af78f595 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -1,7 +1,15 @@ 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 + + after_validation :perform_after_validation def initialize(attributes = {}) attributes.each do |key, value| @@ -16,4 +24,17 @@ class Topic def condition_is_true_but_its_not false end + + def perform_after_validation + 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/track_back.rb b/activemodel/test/models/track_back.rb index d137e4ff8f..768c96ecf0 100644 --- a/activemodel/test/models/track_back.rb +++ b/activemodel/test/models/track_back.rb @@ -1,4 +1,11 @@ class Post class TrackBack + def to_model + NamedTrackBack.new + end + end + + class NamedTrackBack + extend ActiveModel::Naming end end
\ No newline at end of file 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 |