diff options
Diffstat (limited to 'activemodel')
67 files changed, 1311 insertions, 975 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index bd9ed996fe..847ae7f237 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,4 +1,74 @@ -## Rails 3.2.0 (unreleased) ## +## Rails 4.0.0 (unreleased) ## + +* Changed `AM::Serializers::JSON.include_root_in_json' default value to false. + Now, AM Serializers and AR objects have the same default behaviour. Fixes #6578. + + class User < ActiveRecord::Base; end + + class Person + include ActiveModel::Model + include ActiveModel::AttributeMethods + include ActiveModel::Serializers::JSON + + attr_accessor :name, :age + + def attributes + instance_values + end + end + + user.as_json + => {"id"=>1, "name"=>"Konata Izumi", "age"=>16, "awesome"=>true} + # root is not included + + person.as_json + => {"name"=>"Francesco", "age"=>22} + # root is not included + + *Francesco Rodriguez* + +* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell* + +* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella* + +* Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran* + +* `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev* + +* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim* + +* When `^` or `$` are used in the regular expression provided to `validates_format_of` and the :multiline option is not set to true, an exception will be raised. This is to prevent security vulnerabilities when using `validates_format_of`. The problem is described in detail in the Rails security guide. + +## Rails 3.2.6 (Jun 12, 2012) ## + +* No changes. + +## Rails 3.2.5 (Jun 1, 2012) ## + +* No changes. + + +## Rails 3.2.4 (May 31, 2012) ## + +* No changes. + + +## Rails 3.2.3 (March 30, 2012) ## + +* No changes. + + +## Rails 3.2.2 (March 1, 2012) ## + +* No changes. + + +## Rails 3.2.1 (January 26, 2012) ## + +* No changes. + + +## Rails 3.2.0 (January 20, 2012) ## * Deprecated `define_attr_method` in `ActiveModel::AttributeMethods`, because this only existed to support methods like `set_table_name` in Active Record, which are themselves being deprecated. @@ -13,6 +83,27 @@ * Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior *Bogdan Gusiev* + +## Rails 3.1.3 (November 20, 2011) ## + +* No changes + + +## Rails 3.1.2 (November 18, 2011) ## + +* No changes + + +## Rails 3.1.1 (October 7, 2011) ## + +* Remove hard dependency on bcrypt-ruby to avoid make ActiveModel dependent on a binary library. + You must add the gem explicitly to your Gemfile if you want use ActiveModel::SecurePassword: + + gem 'bcrypt-ruby', '~> 3.0.0' + + See GH #2687. *Guillermo Iguaran* + + ## Rails 3.1.0 (August 30, 2011) ## * Alternate I18n namespace lookup is no longer supported. @@ -36,12 +127,37 @@ * Add support for selectively enabling/disabling observers *Myron Marston* +## Rails 3.0.12 (March 1, 2012) ## + +* No changes. + + +## Rails 3.0.11 (November 18, 2011) ## + +* No changes. + + +## Rails 3.0.10 (August 16, 2011) ## + +* No changes. + + +## Rails 3.0.9 (June 16, 2011) ## + +* No changes. + + +## Rails 3.0.8 (June 7, 2011) ## + +* No changes. + + ## Rails 3.0.7 (April 18, 2011) ## * No changes. -* Rails 3.0.6 (April 5, 2011) +## Rails 3.0.6 (April 5, 2011) ## * Fix when database column name has some symbolic characters (e.g. Oracle CASE# VARCHAR2(20)) #5818 #6850 *Robert Pankowecki, Santiago Pastorino* diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 9208145507..b4565b5881 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -9,10 +9,31 @@ 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 exactly conform to the Active Record interface. This would result -in code duplication and fragile applications that broke on upgrades. +in code duplication and fragile applications that broke on upgrades. Active +Model solves this by defining an explicit API. You can read more about the +API in ActiveModel::Lint::Tests. -Active Model solves this. You can include functionality from the following -modules: +Active Model provides a default module that implements the basic API required +to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>. + + class Person + include ActiveModel::Model + + attr_accessor :name, :age + validates_presence_of :name + end + + person = Person.new(:name => 'bob', :age => '18') + person.name # => 'bob' + person.age # => '18' + person.valid? # => true + +It includes model name introspections, conversions, translations and +validations, resulting in a class suitable to be used with Action Pack. +See <tt>ActiveModel::Model</tt> for more examples. + +Active Model also provides the following functionality to have ORM-like +behavior out of the box: * Add attribute magic to objects @@ -20,7 +41,7 @@ modules: include ActiveModel::AttributeMethods attribute_method_prefix 'clear_' - define_attribute_methods [:name, :age] + define_attribute_methods :name, :age attr_accessor :name, :age @@ -50,7 +71,7 @@ modules: This generates +before_create+, +around_create+ and +after_create+ class methods that wrap your create method. - {Learn more}[link:classes/ActiveModel/CallBacks.html] + {Learn more}[link:classes/ActiveModel/Callbacks.html] * Tracking value changes @@ -87,18 +108,14 @@ modules: errors.add(:name, "can not be nil") if name.nil? end - def ErrorsPerson.human_attribute_name(attr, options = {}) + def self.human_attribute_name(attr, options = {}) "Name" end - end person.errors.full_messages # => ["Name can not be nil"] - person.errors.full_messages - # => ["Name can not be nil"] - {Learn more}[link:classes/ActiveModel/Errors.html] * Model name introspection @@ -118,6 +135,16 @@ modules: pattern in a Rails App and take advantage of all the standard observer functions. + class PersonObserver < ActiveModel::Observer + def after_create(person) + person.logger.info("New person added!") + end + + def after_destroy(person) + person.logger.warn("Person with an id of #{person.id} was destroyed!") + end + end + {Learn more}[link:classes/ActiveModel/Observer.html] * Making objects serializable @@ -163,7 +190,7 @@ modules: * Custom validators - class Person + class ValidatorPerson include ActiveModel::Validations validates_with HasNameValidator attr_accessor :name @@ -171,7 +198,7 @@ modules: class HasNameValidator < ActiveModel::Validator def validate(record) - record.errors[:name] = "must exist" if record.name.blank? + record.errors[:name] = "must exist" if record.name.blank? end end @@ -182,7 +209,7 @@ modules: p.valid? # => true {Learn more}[link:classes/ActiveModel/Validator.html] - + == Download and installation diff --git a/activemodel/Rakefile b/activemodel/Rakefile index c4b020196d..fc5aaf9f8f 100755..100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -8,6 +8,7 @@ Rake::TestTask.new do |t| t.libs << "test" t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb").sort t.warning = true + t.verbose = true end namespace :test do diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 60c1d16934..66f324a1a1 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -5,9 +5,10 @@ Gem::Specification.new do |s| s.name = 'activemodel' s.version = version s.summary = 'A toolkit for building modeling frameworks (part of Rails).' - s.description = 'A toolkit for building modeling frameworks like Active Record and Active Resource. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' + s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 85514e63fd..f6bacf5ec0 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -21,8 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require 'active_support' require 'active_model/version' @@ -32,13 +30,13 @@ module ActiveModel autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks - autoload :Configuration autoload :Conversion autoload :Dirty autoload :EachValidator, 'active_model/validator' autoload :Errors autoload :Lint autoload :MassAssignmentSecurity + autoload :Model autoload :Name, 'active_model/naming' autoload :Naming autoload :Observer, 'active_model/observing' @@ -58,5 +56,6 @@ module ActiveModel end end -require 'active_support/i18n' -I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml' +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 71ab1501c8..b5b50847c2 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -28,7 +28,7 @@ module ActiveModel # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # attribute_method_suffix '_contrived?' # attribute_method_prefix 'clear_' - # define_attribute_methods ['name'] + # define_attribute_methods 'name' # # attr_accessor :name # @@ -61,8 +61,7 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - extend ActiveModel::Configuration - config_attribute :attribute_method_matchers + class_attribute :attribute_method_matchers, instance_writer: false self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end @@ -86,7 +85,7 @@ module ActiveModel # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_prefix 'clear_' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -124,7 +123,7 @@ module ActiveModel # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_suffix '_short?' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -162,7 +161,7 @@ module ActiveModel # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' - # define_attribute_methods [:name] + # define_attribute_methods :name # # private # @@ -180,11 +179,23 @@ module ActiveModel undefine_attribute_methods end + + # Allows you to make aliases for attributes. + # + # class Person + # attr_accessor :name + # alias_attribute :nickname, :name + # end + # + # person = Person.new + # person.nickname = "Bob" + # person.nickname # => "Bob" + # person.name # => "Bob" def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s matcher_old = matcher.method_name(old_name).to_s - define_optimized_call self, matcher_new, matcher_old + define_proxy_call false, self, matcher_new, matcher_old end end @@ -204,7 +215,7 @@ module ActiveModel # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or # # attribute_method_affix declares. - # define_attribute_methods [:name, :age, :address] + # define_attribute_methods :name, :age, :address # # private # @@ -212,8 +223,8 @@ module ActiveModel # ... # end # end - def define_attribute_methods(attr_names) - attr_names.each { |attr_name| define_attribute_method(attr_name) } + def define_attribute_methods(*attr_names) + attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) } end def define_attribute_method(attr_name) @@ -223,10 +234,10 @@ module ActiveModel unless instance_method_already_implemented?(method_name) generate_method = "define_method_#{matcher.method_missing_target}" - if respond_to?(generate_method) + if respond_to?(generate_method, true) send(generate_method, attr_name) else - define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s + define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end @@ -243,11 +254,7 @@ module ActiveModel # Returns true if the attribute methods defined have been generated. def generated_attribute_methods #:nodoc: - @generated_attribute_methods ||= begin - mod = Module.new - include mod - mod - end + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } end protected @@ -285,7 +292,7 @@ module ActiveModel # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This fallbacks `define_method` # and `send` if the given names cannot be compiled. - def define_optimized_call(mod, name, send, *extra) #:nodoc: + def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: if name =~ NAME_COMPILABLE_REGEXP defn = "def #{name}(*args)" else @@ -295,7 +302,7 @@ module ActiveModel extra = (extra.map(&:inspect) << "*args").join(", ") if send =~ CALL_COMPILABLE_REGEXP - target = "#{send}(#{extra})" + target = "#{"self." unless include_private}#{send}(#{extra})" else target = "send(:'#{send}', #{extra})" end @@ -325,14 +332,14 @@ module ActiveModel end @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' - @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@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 @regex =~ method_name - AttributeMethodMatch.new(method_missing_target, $2, method_name) + AttributeMethodMatch.new(method_missing_target, $1, method_name) else nil end @@ -394,7 +401,7 @@ module ActiveModel protected def attribute_method?(attr_name) - attributes.include?(attr_name) + respond_to_without_attributes?(:attributes) && attributes.include?(attr_name) end private diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 25d26ede52..ebb4b51aa3 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -88,6 +88,7 @@ module ActiveModel options = callbacks.extract_options! options = { :terminator => "result == false", + :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name], :only => [:before, :around, :after] }.merge(options) @@ -124,7 +125,7 @@ module ActiveModel def self.after_#{callback}(*args, &block) options = args.extract_options! options[:prepend] = true - options[:if] = Array(options[:if]) << "!halted && value != false" + options[:if] = Array(options[:if]) << "value != false" set_callback(:#{callback}, :after, *(args << options), &block) end CALLBACK diff --git a/activemodel/lib/active_model/configuration.rb b/activemodel/lib/active_model/configuration.rb deleted file mode 100644 index 1757c12ebf..0000000000 --- a/activemodel/lib/active_model/configuration.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/class/attribute_accessors' - -module ActiveModel - # This API is for Rails' internal use and is not currently considered 'public', so - # it may change in the future without warning. - # - # It creates configuration attributes that can be inherited from a module down - # to a class that includes the module. E.g. - # - # module MyModel - # extend ActiveModel::Configuration - # config_attribute :awesome - # self.awesome = true - # end - # - # class Post - # include MyModel - # end - # - # Post.awesome # => true - # - # Post.awesome = false - # Post.awesome # => false - # MyModel.awesome # => true - # - # We assume that the module will have a ClassMethods submodule containing methods - # to be transferred to the including class' singleton class. - # - # Config options can also be defined directly on a class: - # - # class Post - # extend ActiveModel::Configuration - # config_attribute :awesome - # end - # - # So this allows us to define a module that doesn't care about whether it is being - # included in a class or a module: - # - # module Awesomeness - # extend ActiveSupport::Concern - # - # included do - # extend ActiveModel::Configuration - # config_attribute :awesome - # self.awesome = true - # end - # end - # - # class Post - # include Awesomeness - # end - # - # module AwesomeModel - # include Awesomeness - # end - module Configuration #:nodoc: - def config_attribute(name, options = {}) - klass = self.is_a?(Class) ? ClassAttribute : ModuleAttribute - klass.new(self, name, options).define - end - - class Attribute - attr_reader :host, :name, :options - - def initialize(host, name, options) - @host, @name, @options = host, name, options - end - - def instance_writer? - options.fetch(:instance_writer, false) - end - end - - class ClassAttribute < Attribute - def define - if options[:global] - host.cattr_accessor name, :instance_writer => instance_writer? - else - host.class_attribute name, :instance_writer => instance_writer? - end - end - end - - class ModuleAttribute < Attribute - def class_methods - @class_methods ||= begin - if host.const_defined?(:ClassMethods, false) - host.const_get(:ClassMethods) - else - host.const_set(:ClassMethods, Module.new) - end - end - end - - def define - host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__ - attr_accessor :#{name} - def #{name}?; !!#{name}; end - CODE - - name, host = self.name, self.host - - class_methods.class_eval do - define_method(name) { host.send(name) } - define_method("#{name}?") { !!send(name) } - end - - host.class_eval <<-CODE - def #{name}; defined?(@#{name}) ? @#{name} : self.class.#{name}; end - def #{name}?; !!#{name}; end - CODE - - if options[:global] - class_methods.class_eval do - define_method("#{name}=") { |val| host.send("#{name}=", val) } - end - else - class_methods.class_eval <<-CODE, __FILE__, __LINE__ - def #{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end - end - CODE - end - - host.send(:attr_writer, name) if instance_writer? - end - end - end -end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index c7c805f1a2..d7f30f0920 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -21,7 +21,7 @@ module ActiveModel # cm.to_model == self # => true # cm.to_key # => nil # cm.to_param # => nil - # cm.to_path # => "contact_messages/contact_message" + # cm.to_partial_path # => "contact_messages/contact_message" # module Conversion extend ActiveSupport::Concern @@ -57,7 +57,7 @@ module ActiveModel end module ClassMethods #:nodoc: - # Provide a class level cache for the to_path. This is an + # Provide a class level cache for #to_partial_path. This is an # internal method and should not be accessed directly. def _to_partial_path #:nodoc: @_to_partial_path ||= begin diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 026f077ee7..7014d8114f 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,6 +1,7 @@ require 'active_model/attribute_methods' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/object/blank' module ActiveModel # == Active Model Dirty @@ -10,14 +11,14 @@ module ActiveModel # # The requirements for implementing ActiveModel::Dirty are: # - # * <tt>include ActiveModel::Dirty</tt> in your object + # * <tt>include ActiveModel::Dirty</tt> in your object. # * Call <tt>define_attribute_methods</tt> passing each method you want to - # track + # track. # * Call <tt>attr_name_will_change!</tt> before each change to the tracked - # attribute + # attribute. # # If you wish to also track previous changes on save or update, you need to - # add + # add: # # @previously_changed = changes # @@ -26,10 +27,9 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Dirty # - # define_attribute_methods [:name] + # define_attribute_methods :name # # def name # @name @@ -44,46 +44,49 @@ module ActiveModel # @previously_changed = changes # @changed_attributes.clear # end - # # end # - # == Examples: - # # A newly instantiated object is unchanged: + # # person = Person.find_by_name('Uncle Bob') # person.changed? # => false # # Change the name: + # # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true - # person.name_was # => 'Uncle Bob' - # person.name_change # => ['Uncle Bob', 'Bob'] + # person.name_was # => "Uncle Bob" + # person.name_change # => ["Uncle Bob", "Bob"] # person.name = 'Bill' - # person.name_change # => ['Uncle Bob', 'Bill'] + # person.name_change # => ["Uncle Bob", "Bill"] # # Save the changes: + # # person.save # person.changed? # => false # person.name_changed? # => false # # Assigning the same value leaves the attribute unchanged: + # # person.name = 'Bill' # person.name_changed? # => false # person.name_change # => nil # # Which attributes have changed? + # # person.name = 'Bob' - # person.changed # => ['name'] - # person.changes # => { 'name' => ['Bill', 'Bob'] } + # person.changed # => ["name"] + # 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 # in-place attributes. # # person.name_will_change! + # person.name_change # => ["Bill", "Bill"] # person.name << 'y' - # person.name_change # => ['Bill', 'Billy'] + # person.name_change # => ["Bill", "Billy"] module Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods @@ -93,40 +96,50 @@ module ActiveModel attribute_method_affix :prefix => 'reset_', :suffix => '!' end - # Returns true if any attribute have unsaved changes, false otherwise. + # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. + # # person.changed? # => false # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.any? + changed_attributes.present? end - # List of attributes with unsaved changes. + # Returns an array with the name of the attributes with unsaved changes. + # # person.changed # => [] # person.name = 'bob' - # person.changed # => ['name'] + # person.changed # => ["name"] def changed changed_attributes.keys end - # Map of changed attrs => [original value, new value]. + # Returns a hash of changed attributes indicating their original + # and new values like <tt>attr => [original value, new value]</tt>. + # # person.changes # => {} # person.name = 'bob' - # person.changes # => { 'name' => ['bill', 'bob'] } + # person.changes # => { "name" => ["bill", "bob"] } def changes HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end - # Map of attributes that were changed when the model was saved. - # person.name # => 'bob' + # Returns a hash of attributes that were changed before the model was saved. + # + # person.name # => "bob" # person.name = 'robert' # person.save - # person.previous_changes # => {'name' => ['bob, 'robert']} + # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes @previously_changed end - # Map of change <tt>attr => original value</tt>. + # Returns a hash of the attributes with unsaved changes indicating their original + # values like <tt>attr => original value</tt>. + # + # person.name # => "bob" + # person.name = 'robert' + # person.changed_attributes # => {"name" => "bob"} def changed_attributes @changed_attributes ||= {} end @@ -150,13 +163,15 @@ module ActiveModel # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) + return if attribute_changed?(attr) + begin value = __send__(attr) value = value.duplicable? ? value.clone : value rescue TypeError, NoMethodError end - changed_attributes[attr] = value unless changed_attributes.include?(attr) + changed_attributes[attr] = value end # Handle <tt>reset_*!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 025915fe6f..aba6618b56 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -3,13 +3,11 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/ordered_hash' module ActiveModel # == Active Model Errors # - # Provides a modified +OrderedHash+ that you can include in your object + # Provides a modified +Hash+ that you can include in your object # for handling error messages and interacting with Action Pack helpers. # # A minimal implementation could be: @@ -75,7 +73,12 @@ module ActiveModel # end def initialize(base) @base = base - @messages = ActiveSupport::OrderedHash.new + @messages = {} + end + + def initialize_dup(other) + @messages = other.messages.dup + super end # Clear the messages @@ -99,6 +102,11 @@ module ActiveModel messages[key] = value end + # Delete messages for +key+ + def delete(key) + messages.delete(key) + end + # When passed a symbol or a name of a method, returns an array of errors # for the method. # @@ -113,7 +121,7 @@ module ActiveModel # p.errors[:name] = "must be set" # p.errors[:name] # => ['must be set'] def []=(attribute, error) - self[attribute.to_sym] << error + self[attribute] << error end # Iterates through each error key, value pair in the error messages hash. @@ -121,12 +129,12 @@ module ActiveModel # has more than one error message, yields once for each error message. # # p.errors.add(:name, "can't be blank") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # end # # p.errors.add(:name, "must be specified") - # p.errors.each do |attribute, errors_array| + # p.errors.each do |attribute, error| # # Will yield :name and "can't be blank" # # then yield :name and "must be specified" # end @@ -193,28 +201,39 @@ module ActiveModel # # <error>name must be specified</error> # # </errors> def to_xml(options={}) - to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) + to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options)) end - # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. + # Returns an Hash that can be used as the JSON representation for this object. + # Options: + # * <tt>:full_messages</tt> - determines if json object should contain + # full messages or not. Default: <tt>false</tt>. def as_json(options=nil) - to_hash + to_hash(options && options[:full_messages]) end - def to_hash - messages.dup + def to_hash(full_messages = false) + if full_messages + messages = {} + self.messages.each do |attribute, array| + messages[attribute] = array.map { |message| full_message(attribute, message) } + end + messages + else + self.messages.dup + end end # Adds +message+ to the error messages on +attribute+. More than one error can be added to the same # +attribute+. # 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 symbol, it will be translated using the appropriate scope (see +generate_message+). # 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 = {}) message = normalize_message(attribute, message, options) if options[:strict] - raise ActiveModel::StrictValidationFailed, message + raise ActiveModel::StrictValidationFailed, full_message(attribute, message) end self[attribute] << message @@ -266,7 +285,7 @@ module ActiveModel # "Name is invalid" def full_message(attribute, message) return message if attribute == :base - attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = attribute.to_s.tr('.', '_').humanize attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) I18n.t(:"errors.format", { :default => "%{attribute} %{message}", @@ -327,7 +346,7 @@ module ActiveModel :model => @base.class.model_name.human, :attribute => @base.class.human_attribute_name(attribute), :value => value - }.merge(options) + }.merge!(options) I18n.translate(key, options) end @@ -336,9 +355,10 @@ module ActiveModel def normalize_message(attribute, message, options) message ||= :invalid - if message.is_a?(Symbol) + case message + when Symbol generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS)) - elsif message.is_a?(Proc) + when Proc message.call else message diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index bfe7ea1869..88b730626c 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -3,9 +3,13 @@ module ActiveModel # == 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. + # 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. + # + # Note an object is not required to implement all APIs in order to work + # with Action Pack. This module only intends to provide guidance in case + # you want all features out of the box. # # These tests do not attempt to determine the semantic correctness of the # returned values. For instance, you could implement valid? to always @@ -19,7 +23,8 @@ module ActiveModel # == Responds to <tt>to_key</tt> # # Returns an Enumerable of all (primary) key attributes - # or nil if model.persisted? is false + # or nil if model.persisted? is false. This is used by + # dom_id to generate unique ids for the object. def test_to_key assert model.respond_to?(:to_key), "The model should respond to to_key" def model.persisted?() false end @@ -53,22 +58,13 @@ module ActiveModel assert_kind_of String, model.to_partial_path end - # == Responds to <tt>valid?</tt> - # - # Returns a boolean that specifies whether the object is in a valid or invalid - # state. - def test_valid? - assert model.respond_to?(:valid?), "The model should respond to valid?" - assert_boolean model.valid?, "valid?" - end - # == Responds to <tt>persisted?</tt> # # Returns a boolean that specifies whether the object has been persisted yet. # This is used when calculating the URL for an object. If the object is - # not persisted, a form for that object, for instance, will be POSTed to the - # collection. If it is persisted, a form for the object will be PUT to the - # URL for the object. + # not persisted, a form for that object, for instance, will route to the + # create action. If it is persisted, a form for the object will routes to + # the update action. def test_persisted? assert model.respond_to?(:persisted?), "The model should respond to persisted?" assert_boolean model.persisted?, "persisted?" @@ -82,33 +78,23 @@ module ActiveModel def test_model_naming assert model.class.respond_to?(:model_name), "The model should respond to model_name" model_name = model.class.model_name - assert_kind_of String, model_name - assert_kind_of String, model_name.human - assert_kind_of String, model_name.singular - assert_kind_of String, model_name.plural + assert model_name.respond_to?(:to_str) + assert model_name.human.respond_to?(:to_str) + assert model_name.singular.respond_to?(:to_str) + assert model_name.plural.respond_to?(:to_str) end # == Errors Testing # - # Returns an object that has :[] and :full_messages defined on it. See below - # for more details. - # - # Returns an Array of Strings that are the errors for the attribute in - # question. If localization is used, the Strings should be localized - # for the current locale. If no error is present, this method should - # return an empty Array. + # Returns an object that implements [](attribute) defined which returns an + # Array of Strings that are the errors for the attribute in question. + # If localization is used, the Strings should be localized for the current + # locale. If no error is present, this method should return an empty Array. def test_errors_aref assert model.respond_to?(:errors), "The model should respond to errors" assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" end - # Returns an Array of all error messages for the object. Each message - # should contain information about the field, if applicable. - def test_errors_full_messages - assert model.respond_to?(:errors), "The model should respond to errors" - assert model.errors.full_messages.is_a?(Array), "errors#full_messages should return an Array" - end - private def model assert @model.respond_to?(:to_model), "The object should respond_to to_model" diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index ba49c6beaa..d17848c861 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -9,7 +9,7 @@ en: inclusion: "is not included in the list" exclusion: "is reserved" invalid: "is invalid" - confirmation: "doesn't match confirmation" + confirmation: "doesn't match %{attribute}" accepted: "must be accepted" empty: "can't be empty" blank: "can't be blank" diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 13495d6786..8f2c0bf00a 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -9,13 +9,11 @@ module ActiveModel extend ActiveSupport::Concern included do - extend ActiveModel::Configuration + class_attribute :_accessible_attributes, instance_writer: false + class_attribute :_protected_attributes, instance_writer: false + class_attribute :_active_authorizer, instance_writer: false - config_attribute :_accessible_attributes - config_attribute :_protected_attributes - config_attribute :_active_authorizer - - config_attribute :_mass_assignment_sanitizer + class_attribute :_mass_assignment_sanitizer, instance_writer: false self.mass_assignment_sanitizer = :logger end @@ -62,7 +60,7 @@ module ActiveModel # Attributes named in this macro are protected from mass-assignment # whenever attributes are sanitized before assignment. A role for the # attributes is optional, if no role is provided then :default is used. - # A role can be defined by using the :as option. + # A role can be defined by using the :as option with a symbol or an array of symbols as the value. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect @@ -85,7 +83,7 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the :default role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5 }, :as => :default) @@ -93,7 +91,7 @@ module ActiveModel # customer.email # => "a@b.com" # customer.logins_count # => nil # - # And using the :admin role : + # And using the :admin role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin) @@ -107,8 +105,9 @@ module ActiveModel # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ - # to sanitize attributes won't provide sufficient protection. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_protected+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_protected(*args) options = args.extract_options! role = options[:as] || :default @@ -127,7 +126,7 @@ module ActiveModel # # Like +attr_protected+, a role for the attributes is optional, # if no role is provided then :default is used. A role can be defined by - # using the :as option. + # using the :as option with a symbol or an array of symbols as the value. # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of @@ -152,7 +151,7 @@ module ActiveModel # end # end # - # When using the :default role : + # When using the :default role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) @@ -162,15 +161,16 @@ module ActiveModel # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # - # And using the :admin role : + # And using the :admin role: # # customer = Customer.new # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) # customer.name # => "David" # customer.credit_rating # => "Excellent" # - # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ - # to sanitize attributes won't provide sufficient protection. + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of + # +attr_accessible+ to sanitize attributes provides basically the same + # functionality, but it makes a bit tricky to deal with nested attributes. def attr_accessible(*args) options = args.extract_options! role = options[:as] || :default @@ -226,12 +226,12 @@ module ActiveModel protected - def sanitize_for_mass_assignment(attributes, role = :default) - _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) + def sanitize_for_mass_assignment(attributes, role = nil) + _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role)) end - def mass_assignment_authorizer(role = :default) - self.class.active_authorizer[role] + def mass_assignment_authorizer(role) + self.class.active_authorizer[role || :default] end end end diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb index 9661349503..415ab0ad17 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -5,7 +5,7 @@ module ActiveModel class PermissionSet < Set def +(values) - super(values.map(&:to_s)) + super(values.compact.map(&:to_s)) end def include?(key) diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb index cfeb4aa7cd..44ce5a489d 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -2,20 +2,18 @@ module ActiveModel module MassAssignmentSecurity class Sanitizer # Returns all attributes not denied by the authorizer. - def sanitize(attributes, authorizer) - sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) } - debug_protected_attribute_removal(attributes, sanitized_attributes) + def sanitize(klass, attributes, authorizer) + rejected = [] + sanitized_attributes = attributes.reject do |key, value| + rejected << key if authorizer.deny?(key) + end + process_removed_attributes(klass, rejected) unless rejected.empty? sanitized_attributes end protected - def debug_protected_attribute_removal(attributes, sanitized_attributes) - removed_keys = attributes.keys - sanitized_attributes.keys - process_removed_attributes(removed_keys) if removed_keys.any? - end - - def process_removed_attributes(attrs) + def process_removed_attributes(klass, attrs) raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten" end end @@ -34,8 +32,21 @@ module ActiveModel @target.respond_to?(:logger) && @target.logger end - def process_removed_attributes(attrs) - logger.warn "Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? + def backtrace + if defined? Rails + Rails.backtrace_cleaner.clean(caller) + else + caller + end + end + + def process_removed_attributes(klass, attrs) + if logger? + logger.warn do + "WARNING: Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}\n" + + backtrace.map { |trace| "\t#{trace}" }.join("\n") + end + end end end @@ -44,9 +55,9 @@ module ActiveModel super() end - def process_removed_attributes(attrs) + def process_removed_attributes(klass, attrs) return if (attrs - insensitive_attributes).empty? - raise ActiveModel::MassAssignmentSecurity::Error.new(attrs) + raise ActiveModel::MassAssignmentSecurity::Error.new(klass, attrs) end def insensitive_attributes @@ -55,8 +66,8 @@ module ActiveModel end class Error < StandardError - def initialize(attrs) - super("Can't mass-assign protected attributes: #{attrs.join(', ')}") + def initialize(klass, attrs) + super("Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}") end end end diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb new file mode 100644 index 0000000000..3af95b09b0 --- /dev/null +++ b/activemodel/lib/active_model/model.rb @@ -0,0 +1,76 @@ +module ActiveModel + + # == Active Model Basic Model + # + # Includes the required interface for an object to interact with <tt>ActionPack</tt>, + # using different <tt>ActiveModel</tt> modules. It includes model name introspections, + # conversions, translations and validations. Besides that, it allows you to + # initialize the object with a hash of attributes, pretty much like + # <tt>ActiveRecord</tt> does. + # + # A minimal implementation could be: + # + # class Person + # include ActiveModel::Model + # attr_accessor :name, :age + # end + # + # person = Person.new(:name => 'bob', :age => '18') + # person.name # => 'bob' + # person.age # => 18 + # + # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt> to + # return <tt>false</tt>, which is the most common case. You may want to override it + # in your class to simulate a different scenario: + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name + # + # def persisted? + # self.id == 1 + # end + # end + # + # person = Person.new(:id => 1, :name => 'bob') + # person.persisted? # => true + # + # Also, if for some reason you need to run code on <tt>initialize</tt>, make sure you + # call super if you want the attributes hash initialization to happen. + # + # class Person + # include ActiveModel::Model + # attr_accessor :id, :name, :omg + # + # def initialize(attributes={}) + # super + # @omg ||= true + # end + # end + # + # person = Person.new(:id => 1, :name => 'bob') + # person.omg # => true + # + # For more detailed information on other functionalities available, please refer + # to the specific modules included in <tt>ActiveModel::Model</tt> (see below). + module Model + def self.included(base) + base.class_eval do + extend ActiveModel::Naming + extend ActiveModel::Translation + include ActiveModel::Validations + include ActiveModel::Conversion + end + end + + def initialize(params={}) + params.each do |attr, value| + self.public_send("#{attr}=", value) + end if params + end + + def persisted? + false + end + end +end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 755e54efcd..2b5fc57a3a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,39 +1,40 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/module/deprecation' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/object/blank' module ActiveModel - class Name < String - attr_reader :singular, :plural, :element, :collection, :partial_path, - :singular_route_key, :route_key, :param_key, :i18n_key + class Name + include Comparable + + attr_reader :singular, :plural, :element, :collection, + :singular_route_key, :route_key, :param_key, :i18n_key, + :name alias_method :cache_key, :collection - deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + :to_str, :to => :name def initialize(klass, namespace = nil, name = nil) - name ||= klass.name - - raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank? + @name = name || klass.name - super(name) + raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank? - @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace + @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace @klass = klass - @singular = _singularize(self).freeze - @plural = ActiveSupport::Inflector.pluralize(@singular).freeze - @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze - @human = ActiveSupport::Inflector.humanize(@element).freeze - @collection = ActiveSupport::Inflector.tableize(self).freeze - @partial_path = "#{@collection}/#{@element}".freeze - @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze - @i18n_key = self.underscore.to_sym + @singular = _singularize(@name) + @plural = ActiveSupport::Inflector.pluralize(@singular) + @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name)) + @human = ActiveSupport::Inflector.humanize(@element) + @collection = ActiveSupport::Inflector.tableize(@name) + @param_key = (namespace ? _singularize(@unnamespaced) : @singular) + @i18n_key = @name.underscore.to_sym @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup) - @singular_route_key = ActiveSupport::Inflector.singularize(@route_key).freeze + @singular_route_key = ActiveSupport::Inflector.singularize(@route_key) @route_key << "_index" if @plural == @singular - @route_key.freeze end # Transform the model name into a more humane format, using I18n. By default, @@ -53,7 +54,7 @@ module ActiveModel defaults << options[:default] if options[:default] defaults << @human - options = {:scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults}.merge(options.except(:default)) + options = { :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults }.merge!(options.except(:default)) I18n.translate(defaults.shift, options) end diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb index 3d463885be..8de6918d18 100644 --- a/activemodel/lib/active_model/observer_array.rb +++ b/activemodel/lib/active_model/observer_array.rb @@ -17,6 +17,11 @@ module ActiveModel # Disables one or more observers. This supports multiple forms: # + # ORM.observers.disable :all + # # => disables all observers for all models subclassed from + # # an ORM base class that includes ActiveModel::Observing + # # e.g. ActiveRecord::Base + # # ORM.observers.disable :user_observer # # => disables the UserObserver # @@ -27,9 +32,6 @@ module ActiveModel # ORM.observers.disable :observer_1, :observer_2 # # => disables Observer1 and Observer2 for all models. # - # ORM.observers.disable :all - # # => disables all observers for all models. - # # User.observers.disable :all do # # all user observers are disabled for # # just the duration of the block @@ -40,6 +42,11 @@ module ActiveModel # Enables one or more observers. This supports multiple forms: # + # ORM.observers.enable :all + # # => enables all observers for all models subclassed from + # # an ORM base class that includes ActiveModel::Observing + # # e.g. ActiveRecord::Base + # # ORM.observers.enable :user_observer # # => enables the UserObserver # @@ -51,9 +58,6 @@ module ActiveModel # ORM.observers.enable :observer_1, :observer_2 # # => enables Observer1 and Observer2 for all models. # - # ORM.observers.enable :all - # # => enables all observers for all models. - # # User.observers.enable :all do # # all user observers are enabled for # # just the duration of the block diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index a8309bf682..f5ea285ccb 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -4,6 +4,8 @@ require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/enumerable' +require 'active_support/deprecation' +require 'active_support/core_ext/object/try' require 'active_support/descendants_tracker' module ActiveModel @@ -63,33 +65,40 @@ module ActiveModel # raises an +ArgumentError+ exception. def add_observer(observer) unless observer.respond_to? :update - raise ArgumentError, "observer needs to respond to `update'" + raise ArgumentError, "observer needs to respond to 'update'" end observer_instances << observer end # Notify list of observers of a change. - def notify_observers(*arg) - observer_instances.each { |observer| observer.update(*arg) } + def notify_observers(*args) + observer_instances.each { |observer| observer.update(*args) } end # Total number of observers. - def count_observers + def observers_count observer_instances.size end + def count_observers + msg = "count_observers is deprecated in favor of observers_count" + ActiveSupport::Deprecation.warn(msg) + observers_count + end + protected def instantiate_observer(observer) #:nodoc: # string/symbol if observer.respond_to?(:to_sym) - observer.to_s.camelize.constantize.instance - elsif observer.respond_to?(:instance) + observer = observer.to_s.camelize.constantize + end + if observer.respond_to?(:instance) observer.instance else raise ArgumentError, - "#{observer} must be a lowercase, underscored class name (or an " + - "instance of the class itself) responding to the instance " + - "method. Example: Person.observers = :big_brother # calls " + + "#{observer} must be a lowercase, underscored class name (or " + + "the class itself) responding to the method :instance. " + + "Example: Person.observers = :big_brother # calls " + "BigBrother.instance" end end @@ -101,17 +110,24 @@ module ActiveModel end end - private - # Fires notifications to model's observers - # - # def save - # notify_observers(:before_save) - # ... - # notify_observers(:after_save) - # end - def notify_observers(method) - self.class.notify_observers(method, self) - end + # Fires notifications to model's observers + # + # def save + # notify_observers(:before_save) + # ... + # notify_observers(:after_save) + # end + # + # Custom notifications can be sent in a similar fashion: + # + # notify_observers(:custom_notification, :foo) + # + # This will call +custom_notification+, passing as arguments + # the current object and :foo. + # + def notify_observers(method, *extra_args) + self.class.notify_observers(method, self, *extra_args) + end end # == Active Model Observers @@ -186,7 +202,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - redefine_method(:observed_classes) { models } + singleton_class.redefine_method(:observed_classes) { models } end # Returns an array of Classes to observe. @@ -205,15 +221,12 @@ module ActiveModel # The class observed by default is inferred from the observer's class name: # assert_equal Person, PersonObserver.observed_class def observed_class - if observed_class_name = name[/(.*)Observer/, 1] - observed_class_name.constantize - else - nil - end + name[/(.*)Observer/, 1].try :constantize end end # Start observing the declared classes and their subclasses. + # Called automatically by the instance method. def initialize observed_classes.each { |klass| add_observer!(klass) } end @@ -224,10 +237,10 @@ module ActiveModel # Send observed_method(object) if the method exists and # the observer is enabled for the given object's class. - def update(observed_method, object, &block) #:nodoc: + def update(observed_method, object, *extra_args, &block) #:nodoc: return unless respond_to?(observed_method) return if disabled_for?(object) - send(observed_method, object, &block) + send(observed_method, object, *extra_args, &block) end # Special method sent by the observed class when it is inherited. @@ -242,6 +255,7 @@ module ActiveModel klass.add_observer(self) end + # Returns true if notifications are disabled for this object. def disabled_for?(object) klass = object.class return false unless klass.respond_to?(:observers) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index db78864c67..3eab745c89 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -6,8 +6,9 @@ module ActiveModel # Adds methods to set and authenticate against a BCrypt password. # This mechanism requires you to have a password_digest attribute. # - # Validations for presence of password, confirmation of password (using + # Validations for presence of password on create, confirmation of password (using # a "password_confirmation" attribute) are automatically added. + # If you wish to turn off validations, pass 'validations: false' as an argument. # You can add more validations by hand if need be. # # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password: @@ -29,18 +30,22 @@ module ActiveModel # 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, "notright") # => false # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user - def has_secure_password + def has_secure_password(options = {}) # Load bcrypt-ruby only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library. gem 'bcrypt-ruby', '~> 3.0.0' require 'bcrypt' attr_reader :password - - validates_confirmation_of :password - validates_presence_of :password_digest + + if options.fetch(:validations, true) + validates_confirmation_of :password + validates_presence_of :password, :on => :create + end + + before_create { raise "Password digest missing on new record" if password_digest.blank? } include InstanceMethodsOnActivation @@ -55,17 +60,14 @@ module ActiveModel module InstanceMethodsOnActivation # Returns self if the password is correct, otherwise false. def authenticate(unencrypted_password) - if BCrypt::Password.new(password_digest) == unencrypted_password - self - else - false - end + BCrypt::Password.new(password_digest) == unencrypted_password && self end - # Encrypts the password into the password_digest attribute. + # Encrypts the password into the password_digest attribute, only if the + # new password is not blank. def password=(unencrypted_password) - @password = unencrypted_password unless unencrypted_password.blank? + @password = unencrypted_password self.password_digest = BCrypt::Password.create(unencrypted_password) end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index ba9721cc70..6d8fd21814 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,7 +1,5 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/array/wrap' - module ActiveModel # == Active Model Serialization @@ -11,15 +9,13 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Serialization # # attr_accessor :name # # def attributes - # {'name' => name} + # {'name' => nil} # end - # # end # # Which would provide you with: @@ -29,27 +25,29 @@ module ActiveModel # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # - # You need to declare some sort of attributes hash which contains the attributes - # you want to serialize and their current value. + # You need to declare an attributes hash which contains the attributes + # you want to serialize. Attributes must be strings, not symbols. + # When called, serializable hash will use + # instance methods that match the name of the attributes hash's keys. + # In order to override this behavior, take a look at the private + # method +read_attribute_for_serialization+. # # Most of the time though, you will want to include the JSON or XML # serializations. Both of these modules automatically include the - # ActiveModel::Serialization module, so there is no need to explicitly + # <tt>ActiveModel::Serialization</tt> module, so there is no need to explicitly # include it. # - # So a minimal implementation including XML and JSON would be: + # A minimal implementation including XML and JSON would be: # # class Person - # # include ActiveModel::Serializers::JSON # include ActiveModel::Serializers::Xml # # attr_accessor :name # # def attributes - # {'name' => name} + # {'name' => nil} # end - # # end # # Which would provide you with: @@ -66,12 +64,17 @@ module ActiveModel # person.to_json # => "{\"name\":\"Bob\"}" # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # - # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . + # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and <tt>:include</tt>. + # The following are all valid examples: + # + # person.serializable_hash(:only => 'name') + # person.serializable_hash(:include => :address) + # person.serializable_hash(:include => { :address => { :only => 'city' }}) module Serialization def serializable_hash(options = nil) options ||= {} - attribute_names = attributes.keys.sort + attribute_names = attributes.keys if only = options[:only] attribute_names &= Array(only).map(&:to_s) elsif except = options[:except] @@ -81,12 +84,11 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - method_names = Array(options[:methods]).select { |n| respond_to?(n) } - method_names.each { |n| hash[n] = send(n) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } serializable_add_includes(options) do |association, records, opts| - hash[association] = if records.is_a?(Enumerable) - records.map { |a| a.serializable_hash(opts) } + hash[association.to_s] = if records.respond_to?(:to_ary) + records.to_ary.map { |a| a.serializable_hash(opts) } else records.serializable_hash(opts) end @@ -123,13 +125,13 @@ module ActiveModel # +records+ - the association record(s) to be serialized # +opts+ - options for the association records def serializable_add_includes(options = {}) #:nodoc: - return unless include = options[:include] + return unless includes = options[:include] - unless include.is_a?(Hash) - include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + unless includes.is_a?(Hash) + includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] end - include.each do |association, opts| + includes.each do |association, opts| if records = send(association) yield association, records, opts end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 63ab8e7edc..b4baf3a946 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,83 +10,82 @@ module ActiveModel included do extend ActiveModel::Naming - extend ActiveModel::Configuration - config_attribute :include_root_in_json - self.include_root_in_json = true + class_attribute :include_root_in_json + self.include_root_in_json = false end # Returns a hash representing the model. Some configuration can be # passed through +options+. # # 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: + # of +as_json+. If true +as_json+ will emit a single root node named after + # the object's type. The default value for <tt>include_root_in_json</tt> + # option is +false+. # # user = User.find(1) # user.as_json - # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} } + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true} + # + # ActiveRecord::Base.include_root_in_json = true # - # ActiveRecord::Base.include_root_in_json = false # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} + # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true } } # - # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: + # This behavior can also be achieved by setting the <tt>:root</tt> option + # to +true+ as in: # # user = User.find(1) - # user.as_json(root: false) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. + # user.as_json(root: true) + # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true } } # # Without any +options+, the returned Hash will include all the model's - # attributes. For example: + # attributes. # # user = User.find(1) # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} + # # => { "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: + # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit + # the attributes included, and work similar to the +attributes+ method. # - # user.as_json(:only => [ :id, :name ]) - # # => {"id": 1, "name": "Konata Izumi"} + # user.as_json(only: [:id, :name]) + # # => { "id" => 1, "name" => "Konata Izumi" } # - # user.as_json(:except => [ :id, :created_at, :age ]) - # # => {"name": "Konata Izumi", "awesome": true} + # user.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>: # - # user.as_json(:methods => :permalink) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "permalink": "1-konata-izumi"} + # user.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>: # - # user.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"}, - # {"id": 2, author_id: 1, "title": "So I was thinking"}]} + # user.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" }, + # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] } # # Second level and higher order associations work as well: # - # user.as_json(:include => { :posts => { - # :include => { :comments => { - # :only => :body } }, - # :only => :title } }) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], - # "title": "Welcome to the weblog"}, - # {"comments": [{"body": "Don't think too hard"}], - # "title": "So I was thinking"}]} + # user.as_json(include: { posts: { + # include: { comments: { + # only: :body } }, + # only: :title } }) + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true, + # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ], + # # "title" => "Welcome to the weblog" }, + # # { "comments" => [ { "body" => "Don't think too hard" } ], + # # "title" => "So I was thinking" } ] } def as_json(options = nil) root = include_root_in_json root = options[:root] if options.try(:key?, :root) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 5084298210..2b3e9ce134 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -115,7 +115,9 @@ module ActiveModel merged_options = opts.merge(options.slice(:builder, :indent)) merged_options[:skip_instruct] = true - if records.is_a?(Enumerable) + if records.respond_to?(:to_ary) + records = records.to_ary + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) type = options[:skip_types] ? { } : {:type => "array"} association_name = association.to_s.singularize @@ -170,7 +172,7 @@ module ActiveModel # <id type="integer">1</id> # <name>David</name> # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> # </user> # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 02b7c54d61..7a86701f73 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/hash/reverse_merge' - module ActiveModel # == Active Model Translation @@ -43,19 +41,20 @@ module ActiveModel # # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) - defaults = [] - parts = attribute.to_s.split(".", 2) + options = { :count => 1 }.merge!(options) + parts = attribute.to_s.split(".") attribute = parts.pop - namespace = parts.pop + namespace = parts.join("/") unless parts.empty? + attributes_scope = "#{self.i18n_scope}.attributes" if namespace - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}" end - defaults << :"#{self.i18n_scope}.attributes.#{namespace}.#{attribute}" + defaults << :"#{attributes_scope}.#{namespace}.#{attribute}" else - lookup_ancestors.each do |klass| - defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" + defaults = lookup_ancestors.map do |klass| + :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}" end end @@ -63,7 +62,7 @@ module ActiveModel defaults << options.delete(:default) if options[:default] defaults << attribute.humanize - options.reverse_merge! :count => 1, :default => defaults + options[:default] = defaults I18n.translate(defaults.shift, options) end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 15b8e824ac..06eebf79d9 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -33,7 +33,7 @@ module ActiveModel # person.first_name = 'zoolander' # person.valid? # => false # person.invalid? # => true - # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}> + # person.errors # => #<Hash {:first_name=>["starts with z."]}> # # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+ method # to your instances initialized with a new <tt>ActiveModel::Errors</tt> object, so @@ -52,8 +52,7 @@ module ActiveModel attr_accessor :validation_context define_callbacks :validate, :scope => :name - extend ActiveModel::Configuration - config_attribute :_validators + class_attribute :_validators self._validators = Hash.new { |h,k| h[k] = [] } end @@ -65,7 +64,7 @@ module ActiveModel # # attr_accessor :first_name, :last_name # - # validates_each :first_name, :last_name do |record, attr, value| + # validates_each :first_name, :last_name, :allow_blank => true do |record, attr, value| # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z # end # end @@ -128,6 +127,19 @@ module ActiveModel # end # end # + # Options: + # * <tt>:on</tt> - Specifies the context where this validation is active + # (e.g. <tt>:on => :create</tt> or <tt>:on => :custom_validation_context</tt>) + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, + # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a true or false value. def validate(*args, &block) options = args.extract_options! if options.key?(:on) @@ -145,7 +157,7 @@ module ActiveModel _validators.values.flatten.uniq end - # List all validators that being used to validate a specific attribute. + # List all validators that are being used to validate a specific attribute. def validators_on(*attributes) attributes.map do |attribute| _validators[attribute.to_sym] @@ -165,6 +177,12 @@ module ActiveModel end end + # Clean the +Errors+ object if instance is duped + def initialize_dup(other) # :nodoc: + @errors = nil + super + end + # Returns the +Errors+ object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index e628c6f306..38abd0c1fa 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -23,7 +23,7 @@ module ActiveModel module HelperMethods # Encapsulates the pattern of wanting to validate the acceptance of a - # terms of service check box (or similar agreement). Example: + # terms of service check box (or similar agreement). # # class Person < ActiveRecord::Base # validates_acceptance_of :terms_of_service @@ -59,7 +59,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a true or # false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index c80ace7b82..dbafd0bd1a 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -8,7 +8,8 @@ module ActiveModel # Provides an interface for any class to have <tt>before_validation</tt> and # <tt>after_validation</tt> callbacks. # - # First, extend ActiveModel::Callbacks from the class you are creating: + # First, include ActiveModel::Validations::Callbacks from the class you are + # creating: # # class MyModel # include ActiveModel::Validations::Callbacks @@ -23,7 +24,7 @@ module ActiveModel included do include ActiveSupport::Callbacks - define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] + define_callbacks :validation, :terminator => "result == false", :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name] end module ClassMethods @@ -40,7 +41,6 @@ module ActiveModel options = args.extract_options! options[:prepend] = true options[:if] = Array(options[:if]) - options[:if] << "!halted" options[:if].unshift("self.validation_context == :#{options[:on]}") if options[:on] set_callback(:validation, :after, *(args << options), &block) end diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb new file mode 100644 index 0000000000..b632a2bd6b --- /dev/null +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -0,0 +1,31 @@ +require 'active_support/core_ext/range.rb' + +module ActiveModel + module Validations + module Clusivity + ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << + "and must be supplied as the :in option of the configuration hash" + + def check_validity! + unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end + end + + private + + def include?(record, value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + exclusions.send(inclusion_method(exclusions), value) + end + + # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the + # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> + # uses the previous logic of comparing a value with the range endpoints. + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? + end + end + end +end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 6573a7d264..ede34d15bc 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -5,7 +5,8 @@ module ActiveModel class ConfirmationValidator < EachValidator def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed) - record.errors.add(attribute, :confirmation, options) + human_attribute_name = record.class.human_attribute_name(attribute) + record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name)) end end @@ -18,7 +19,7 @@ module ActiveModel module HelperMethods # Encapsulates the pattern of wanting to validate a password or email - # address field with a confirmation. For example: + # address field with a confirmation. # # Model: # class Person < ActiveRecord::Base @@ -37,7 +38,7 @@ module ActiveModel # attribute. # # NOTE: This check is performed only if +password_confirmation+ is not - # +nil+, and by default only on save. To require confirmation, make sure + # +nil+. To require confirmation, make sure # to add a presence check for the confirmation attribute: # # validates_presence_of :password_confirmation, :if => :password_changed? @@ -59,7 +60,7 @@ module ActiveModel # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 644cc814a7..edd42d85f2 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,66 +1,55 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" module ActiveModel # == Active Model Exclusion Validator module Validations class ExclusionValidator < EachValidator - ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << - "and must be supplied as the :in option of the configuration hash" - - def check_validity! - unless [:include?, :call].any? { |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - if exclusions.send(inclusion_method(exclusions), value) + if include?(record, value) record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value)) end end - - private - - # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the - # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> - # uses the previous logic of comparing a value with the range endpoints. - def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods - # Validates that the value of the specified attribute is not in a particular enumerable object. + # Validates that the value of the specified attribute is not in a + # particular enumerable object. # # class Person < ActiveRecord::Base # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed" - # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, :message => "should not be the same as your username or first name" + # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, + # :message => "should not be the same as your username or first name" # end # # Configuration options: - # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. - # This can be supplied as a proc or lambda which returns an enumerable. If the enumerable - # is a range the test is performed with <tt>Range#cover?</tt> - # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. - # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be + # part of. This can be supplied as a proc or lambda which returns an + # enumerable. If the enumerable is a range the test is performed with + # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. + # * <tt>:message</tt> - Specifies a custom error message (default is: "is + # reserved"). + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute + # is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the + # attribute is blank(default is +false+). # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index d3faa8c6a6..ffdf842d94 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -32,60 +32,89 @@ module ActiveModel def record_error(record, attribute, name, value) record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value)) end - + + def regexp_using_multiline_anchors?(regexp) + regexp.source.start_with?("^") || + (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$")) + end + def check_options_validity(options, name) option = options[name] if option && !option.is_a?(Regexp) && !option.respond_to?(:call) raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" + elsif option && option.is_a?(Regexp) && + regexp_using_multiline_anchors?(option) && options[:multiline] != true + raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ + "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ + ":multiline => true option?" end end end module HelperMethods - # Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided. - # You can require that the attribute matches the regular expression: + # Validates whether the value of the specified attribute is of the correct + # form, going by the regular expression provided.You can require that the + # attribute matches the regular expression: # # class Person < ActiveRecord::Base - # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create + # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :on => :create # end # - # Alternatively, you can require that the specified attribute does _not_ match the regular expression: + # Alternatively, you can require that the specified attribute does _not_ + # match the regular expression: # # class Person < ActiveRecord::Base # validates_format_of :email, :without => /NOSPAM/ # end # - # You can also provide a proc or lambda which will determine the regular expression that will be used to validate the attribute + # You can also provide a proc or lambda which will determine the regular + # expression that will be used to validate the attribute. # # class Person < ActiveRecord::Base # # Admin can have number as a first letter in their screen name - # validates_format_of :screen_name, :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i } + # validates_format_of :screen_name, + # :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i } # end # - # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. + # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the + # string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # - # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression - # or a proc or lambda, or else an exception will be raised. + # Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass the + # :multiline => true option in case you use any of these two anchors in the provided + # regular expression. In most cases, you should be using <tt>\A</tt> and <tt>\z</tt>. + # + # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. + # In addition, both must be a regular expression or a proc or lambda, or + # else an exception will be raised. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation. - # This can be provided as a proc or lambda returning regular expression which will be called at runtime. - # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. - # This can be provided as a proc or lambda returning regular expression which will be called at runtime. + # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute + # is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to true, skips this validation if the + # attribute is blank (default is +false+). + # * <tt>:with</tt> - Regular expression that if the attribute matches will + # result in a successful validation. This can be provided as a proc or lambda + # returning regular expression which will be called at runtime. + # * <tt>:without</tt> - Regular expression that if the attribute does not match + # will result in a successful validation. This can be provided as a proc or + # lambda returning regular expression which will be called at runtime. # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # * <tt>:multiline</tt> - Set to true if your regular expression contains + # anchors that match the beginning or end of lines as opposed to the + # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>. def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 147e2ecb69..8810f2a3c1 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,39 +1,22 @@ -require 'active_support/core_ext/range' +require "active_model/validations/clusivity" module ActiveModel # == Active Model Inclusion Validator module Validations class InclusionValidator < EachValidator - ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << - "and must be supplied as the :in option of the configuration hash" - - def check_validity! - unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } - raise ArgumentError, ERROR_MESSAGE - end - end + include Clusivity def validate_each(record, attribute, value) - delimiter = options[:in] - exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter - unless exclusions.send(inclusion_method(exclusions), value) + unless include?(record, value) record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) end end - - private - - # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the - # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> - # uses the previous logic of comparing a value with the range endpoints. - def inclusion_method(enumerable) - enumerable.is_a?(Range) ? :cover? : :include? - end end module HelperMethods - # Validates whether the value of the specified attribute is available in a particular enumerable object. + # Validates whether the value of the specified attribute is available in a + # particular enumerable object. # # class Person < ActiveRecord::Base # validates_inclusion_of :gender, :in => %w( m f ) @@ -45,22 +28,27 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc or lambda which returns an enumerable. If the enumerable - # is a range the test is performed with <tt>Range#cover?</tt> - # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. - # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). + # is a range the test is performed with <tt>Range#cover?</tt>, 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. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. - # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 0eba241333..64b4fe2d74 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/encoding" - module ActiveModel # == Active Model Length Validator @@ -29,8 +27,8 @@ module ActiveModel keys.each do |key| value = options[key] - unless value.is_a?(Integer) && value >= 0 - raise ArgumentError, ":#{key} must be a nonnegative Integer" + unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY + raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity" end end end @@ -74,35 +72,46 @@ module ActiveModel # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" # validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters" # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.", :tokenizer => lambda { |str| str.scan(/\w+/) } + # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.", + # :tokenizer => lambda { |str| str.scan(/\w+/) } # end # # Configuration options: # * <tt>:minimum</tt> - The minimum size of the attribute. # * <tt>:maximum</tt> - The maximum size of the attribute. # * <tt>:is</tt> - The exact size of the attribute. - # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute. + # * <tt>:within</tt> - A range specifying the minimum and maximum size of the + # attribute. # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. - # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %{count} characters)"). - # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %{count} characters)"). - # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %{count} characters)"). - # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. + # * <tt>:too_long</tt> - The error message if the attribute goes over the + # maximum (default is: "is too long (maximum is %{count} characters)"). + # * <tt>:too_short</tt> - The error message if the attribute goes under the + # minimum (default is: "is too short (min is %{count} characters)"). + # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method + # and the attribute is the wrong size (default is: "is the wrong length + # (should be %{count} characters)"). + # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, + # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate + # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to - # count words as in above example.) - # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. + # (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to count words + # as in above example). Defaults to <tt>lambda{ |value| value.split(//) }</tt> + # which counts individual characters. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index bb9f9679fc..40b5b92b84 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -79,9 +79,10 @@ module ActiveModel end module HelperMethods - # Validates whether the value of the specified attribute is numeric by trying to convert it to - # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression - # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true). + # Validates whether the value of the specified attribute is numeric by trying + # to convert it to a float with Kernel.Float (if <tt>only_integer</tt> is false) + # or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt> (if + # <tt>only_integer</tt> is set to true). # # class Person < ActiveRecord::Base # validates_numericality_of :value, :on => :create @@ -92,37 +93,50 @@ module ActiveModel # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float columns empty strings are converted to +nil+. - # * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value. - # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value. + # * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, + # e.g. an integral value (default is +false+). + # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is + # +false+). Notice that for fixnum and float columns empty strings are + # converted to +nil+. + # * <tt>:greater_than</tt> - Specifies the value must be greater than the + # supplied value. + # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater + # than or equal the supplied value. # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value. - # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value. - # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value. - # * <tt>:other_than</tt> - Specifies the value must be other than the supplied value. + # * <tt>:less_than</tt> - Specifies the value must be less than the supplied + # value. + # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or + # equal the supplied value. + # * <tt>:other_than</tt> - Specifies the value must be other than the supplied + # value. # * <tt>:odd</tt> - Specifies the value must be an odd number. # * <tt>:even</tt> - Specifies the value must be an even number. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # + # The following checks can also be supplied with a proc or a symbol which + # corresponds to a method: # - # The following checks can also be supplied with a proc or a symbol which corresponds to a method: # * <tt>:greater_than</tt> # * <tt>:greater_than_or_equal_to</tt> # * <tt>:equal_to</tt> # * <tt>:less_than</tt> # * <tt>:less_than_or_equal_to</tt> # + # For example: + # # class Person < ActiveRecord::Base # validates_numericality_of :width, :less_than => Proc.new { |person| person.height } # validates_numericality_of :width, :greater_than => :minimum_weight # end - # def validates_numericality_of(*attr_names) validates_with NumericalityValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 9a643a6f5c..018ef1e733 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -11,7 +11,8 @@ module ActiveModel end module HelperMethods - # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example: + # Validates that the specified attributes are not blank (as defined by + # Object#blank?). Happens by default on save. # # class Person < ActiveRecord::Base # validates_presence_of :first_name @@ -19,25 +20,28 @@ module ActiveModel # # The first_name attribute must be in the object and it cannot be blank. # - # If you want to validate the presence of a boolean field (where the real values are true and false), - # you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # If you want to validate the presence of a boolean field (where the real values + # are true and false), you will want to use + # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. # - # This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>. + # This is due to the way Object#blank? handles boolean values: + # <tt>false.blank? # => true</tt>. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if + # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or + # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information - # + # See <tt>ActiveModel::Validation#validates!</tt> for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 8e09f6ac35..6c13d2b4a2 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/hash/slice' module ActiveModel - # == Active Model validates method module Validations module ClassMethods @@ -59,7 +58,7 @@ module ActiveModel # # validates :name, :'film/title' => true # - # The validators hash can also handle regular expressions, ranges, + # The validators hash can also handle regular expressions, ranges, # arrays and strings in shortcut form, e.g. # # validates :email, :format => /@/ @@ -70,7 +69,7 @@ module ActiveModel # 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+, +:allow_nil+ and +:strict+ + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ # can be given to one specific validator, as a hash: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true @@ -80,7 +79,7 @@ module ActiveModel # validates :password, :presence => true, :confirmation => true, :if => :password_required? # def validates(*attributes) - defaults = attributes.extract_options! + defaults = attributes.extract_options!.dup validations = defaults.slice!(*_validates_default_keys) raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? @@ -89,6 +88,7 @@ module ActiveModel defaults.merge!(:attributes => attributes) validations.each do |key, options| + next unless options key = "#{key.to_s.camelize}Validator" begin @@ -101,12 +101,12 @@ module ActiveModel end end - # This method is used to define validation that can not be corrected by end user - # and is considered exceptional. - # So each validator defined with bang or <tt>:strict</tt> option set to <tt>true</tt> - # will always raise <tt>ActiveModel::InternalValidationFailed</tt> instead of adding error - # when validation fails - # See <tt>validates</tt> for more information about validation itself. + # This method is used to define validations that cannot be corrected by end + # users and are considered exceptional. So each validator defined with bang + # or <tt>:strict</tt> option set to <tt>true</tt> will always raise + # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error + # when validation fails. + # See <tt>validates</tt> for more information about the validation itself. def validates!(*attributes) options = attributes.extract_options! options[:strict] = true @@ -118,7 +118,7 @@ module ActiveModel # 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 , :strict] + [:if, :unless, :on, :allow_blank, :allow_nil , :strict] end def _parse_validates_options(options) #:nodoc: diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 72b8562b93..66cc9daa2c 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -62,8 +62,8 @@ module ActiveModel # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). # The method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information - + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # # If you pass any additional configuration options, they will be passed # to the class and available as <tt>options</tt>: # @@ -77,7 +77,6 @@ module ActiveModel # options[:my_custom_key] # => "my custom value" # end # end - # def validates_with(*args, &block) options = args.extract_options! args.each do |klass| @@ -126,14 +125,13 @@ module ActiveModel # end # # Standard configuration options (:on, :if and :unless), which are - # available on the class version of validates_with, should instead be - # placed on the <tt>validates</tt> method as these are applied and tested - # in the callback + # available on the class version of +validates_with+, should instead be + # placed on the +validates+ method as these are applied and tested + # in the callback. # # If you pass any additional configuration options, they will be passed - # to the class and available as <tt>options</tt>, please refer to the - # class version of this method for more information - # + # to the class and available as +options+, please refer to the + # class version of this method for more information. def validates_with(*args, &block) options = args.extract_options! args.each do |klass| diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 0c6e49bee2..e2f2cecc09 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -10,7 +10,7 @@ class ModelWithAttributes end def attributes - { :foo => 'value of foo' } + { :foo => 'value of foo', :baz => 'value of baz' } end private @@ -76,7 +76,28 @@ private end end +class ModelWithRubyKeywordNamedAttributes + include ActiveModel::AttributeMethods + + def attributes + { :begin => 'value of begin', :end => 'value of end' } + end + +private + def attribute(name) + attributes[name.to_sym] + end +end + +class ModelWithoutAttributesMethod + include ActiveModel::AttributeMethods +end + class AttributeMethodsTest < ActiveModel::TestCase + test 'method missing works correctly even if attributes method is not defined' do + assert_raises(NoMethodError) { ModelWithoutAttributesMethod.new.foo } + end + test 'unrelated classes should not share attribute method matchers' do assert_not_equal ModelWithAttributes.send(:attribute_method_matchers), ModelWithAttributes2.send(:attribute_method_matchers) @@ -119,29 +140,45 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b') end + test '#define_attribute_methods works passing multiple arguments' do + ModelWithAttributes.define_attribute_methods(:foo, :baz) + + assert_equal "value of foo", ModelWithAttributes.new.foo + assert_equal "value of baz", ModelWithAttributes.new.baz + end + test '#define_attribute_methods generates attribute methods' do - ModelWithAttributes.define_attribute_methods([:foo]) + ModelWithAttributes.define_attribute_methods(:foo) 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']) + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') end test '#alias_attribute works with attributes with spaces in their names' do - ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar']) + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar end + test '#alias_attribute works with attributes named as a ruby keyword' do + ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) + + assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from + assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + end + test '#undefine_attribute_methods removes attribute methods' do - ModelWithAttributes.define_attribute_methods([:foo]) + ModelWithAttributes.define_attribute_methods(:foo) ModelWithAttributes.undefine_attribute_methods assert !ModelWithAttributes.new.respond_to?(:foo) @@ -162,7 +199,7 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_deprecated { klass.attribute_method_suffix '' } assert_deprecated { klass.attribute_method_prefix '' } - klass.define_attribute_methods([:foo]) + klass.define_attribute_methods(:foo) assert_equal 'value of foo', klass.new.foo end @@ -180,6 +217,12 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_raises(NoMethodError) { m.protected_method } end + class ClassWithProtected + protected + def protected_method + end + end + test 'should not interfere with respond_to? if the attribute has a private/protected method' do m = ModelWithAttributes2.new m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' } @@ -187,9 +230,11 @@ class AttributeMethodsTest < ActiveModel::TestCase assert !m.respond_to?(:private_method) assert m.respond_to?(:private_method, true) + c = ClassWithProtected.new + # This is messed up, but it's how Ruby works at the moment. Apparently it will be changed # in the future. - assert m.respond_to?(:protected_method) + assert_equal c.respond_to?(:protected_method), m.respond_to?(:protected_method) assert m.respond_to?(:protected_method, true) end diff --git a/activemodel/test/cases/configuration_test.rb b/activemodel/test/cases/configuration_test.rb deleted file mode 100644 index a172fa26a3..0000000000 --- a/activemodel/test/cases/configuration_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'cases/helper' - -class ConfigurationOnModuleTest < ActiveModel::TestCase - def setup - @mod = mod = Module.new do - extend ActiveSupport::Concern - extend ActiveModel::Configuration - - config_attribute :omg - self.omg = "default" - - config_attribute :wtf, global: true - self.wtf = "default" - - config_attribute :boolean - - config_attribute :lol, instance_writer: true - end - - @klass = Class.new do - include mod - end - - @subklass = Class.new(@klass) - end - - test "default" do - assert_equal "default", @mod.omg - assert_equal "default", @klass.omg - assert_equal "default", @klass.new.omg - end - - test "setting" do - @mod.omg = "lol" - assert_equal "lol", @mod.omg - end - - test "setting on class including the module" do - @klass.omg = "lol" - assert_equal "lol", @klass.omg - assert_equal "lol", @klass.new.omg - assert_equal "default", @mod.omg - end - - test "setting on subclass of class including the module" do - @subklass.omg = "lol" - assert_equal "lol", @subklass.omg - assert_equal "default", @klass.omg - assert_equal "default", @mod.omg - end - - test "setting on instance" do - assert !@klass.new.respond_to?(:omg=) - - @klass.lol = "lol" - obj = @klass.new - assert_equal "lol", obj.lol - obj.lol = "omg" - assert_equal "omg", obj.lol - assert_equal "lol", @klass.lol - assert_equal "lol", @klass.new.lol - obj.lol = false - assert !obj.lol? - end - - test "global attribute" do - assert_equal "default", @mod.wtf - assert_equal "default", @klass.wtf - - @mod.wtf = "wtf" - - assert_equal "wtf", @mod.wtf - assert_equal "wtf", @klass.wtf - - @klass.wtf = "lol" - - assert_equal "lol", @mod.wtf - assert_equal "lol", @klass.wtf - end - - test "boolean" do - assert_equal false, @mod.boolean? - assert_equal false, @klass.new.boolean? - @mod.boolean = true - assert_equal true, @mod.boolean? - assert_equal true, @klass.new.boolean? - end -end - -class ConfigurationOnClassTest < ActiveModel::TestCase - def setup - @klass = Class.new do - extend ActiveModel::Configuration - - config_attribute :omg - self.omg = "default" - - config_attribute :wtf, global: true - self.wtf = "default" - - config_attribute :omg2, instance_writer: true - config_attribute :wtf2, instance_writer: true, global: true - end - - @subklass = Class.new(@klass) - end - - test "defaults" do - assert_equal "default", @klass.omg - assert_equal "default", @klass.wtf - assert_equal "default", @subklass.omg - assert_equal "default", @subklass.wtf - end - - test "changing" do - @klass.omg = "lol" - assert_equal "lol", @klass.omg - assert_equal "lol", @subklass.omg - end - - test "changing in subclass" do - @subklass.omg = "lol" - assert_equal "lol", @subklass.omg - assert_equal "default", @klass.omg - end - - test "changing global" do - @klass.wtf = "wtf" - assert_equal "wtf", @klass.wtf - assert_equal "wtf", @subklass.wtf - - @subklass.wtf = "lol" - assert_equal "lol", @klass.wtf - assert_equal "lol", @subklass.wtf - end - - test "instance_writer" do - obj = @klass.new - - @klass.omg2 = "omg" - @klass.wtf2 = "wtf" - - assert_equal "omg", obj.omg2 - assert_equal "wtf", obj.wtf2 - - obj.omg2 = "lol" - obj.wtf2 = "lol" - - assert_equal "lol", obj.omg2 - assert_equal "lol", obj.wtf2 - assert_equal "omg", @klass.omg2 - assert_equal "lol", @klass.wtf2 - end -end diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index 24552bcaf2..d679ad41aa 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -24,7 +24,7 @@ class ConversionTest < ActiveModel::TestCase assert_equal "1", Contact.new(:id => 1).to_param end - test "to_path default implementation returns a string giving a relative path" do + test "to_partial_path default implementation returns a string giving a relative path" do assert_equal "contacts/contact", Contact.new.to_partial_path assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path, "ActiveModel::Conversion#to_partial_path caching should be class-specific" diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 98244a6290..eaaf910bac 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods [:name, :color] + define_attribute_methods :name, :color def initialize @name = nil diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 8ddedb160a..3bc0d58351 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -27,12 +27,27 @@ class ErrorsTest < ActiveModel::TestCase end end + def test_delete + errors = ActiveModel::Errors.new(self) + errors[:foo] = 'omg' + errors.delete(:foo) + assert_empty errors[:foo] + end + def test_include? errors = ActiveModel::Errors.new(self) errors[:foo] = 'omg' assert errors.include?(:foo), 'errors should include :foo' end + def test_dup + errors = ActiveModel::Errors.new(self) + errors[:foo] = 'bar' + errors_dup = errors.dup + errors_dup[:bar] = 'omg' + assert_not_same errors_dup.messages, errors.messages + end + def test_has_key? errors = ActiveModel::Errors.new(self) errors[:foo] = 'omg' @@ -136,10 +151,10 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a end - test 'to_hash should return an ordered hash' do + test 'to_hash should return a hash' do person = Person.new person.errors.add(:name, "can not be blank") - assert_instance_of ActiveSupport::OrderedHash, person.errors.to_hash + assert_instance_of ::Hash, person.errors.to_hash end test 'full_messages should return an array of error messages, with the attribute name included' do @@ -169,6 +184,16 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["is invalid"], hash[:email] end + test 'should return a JSON hash representation of the errors with full messages' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + person.errors.add(:email, "is invalid") + hash = person.errors.as_json(:full_messages => true) + assert_equal ["name can not be blank", "name can not be nil"], hash[:name] + assert_equal ["email is invalid"], hash[:email] + end + test "generate_message should work without i18n_scope" do person = Person.new assert !Person.respond_to?(:i18n_scope) diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 4347b17cbc..7d6f11b5a5 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,8 +1,5 @@ require File.expand_path('../../../../load_paths', __FILE__) -lib = File.expand_path("#{File.dirname(__FILE__)}/../../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - require 'config' require 'active_model' require 'active_support/core_ext/string/access' diff --git a/activemodel/test/cases/lint_test.rb b/activemodel/test/cases/lint_test.rb index 68372160cd..8faf93c056 100644 --- a/activemodel/test/cases/lint_test.rb +++ b/activemodel/test/cases/lint_test.rb @@ -7,14 +7,10 @@ class LintTest < ActiveModel::TestCase extend ActiveModel::Naming include ActiveModel::Conversion - def valid?() true end def persisted?() false end def errors - obj = Object.new - def obj.[](key) [] end - def obj.full_messages() [] end - obj + Hash.new([]) end end diff --git a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb index d005b638e4..8082c49852 100644 --- a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb +++ b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb @@ -13,6 +13,12 @@ class PermissionSetTest < ActiveModel::TestCase assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" end + test "+ compacts added collection values" do + added_collection = [ nil ] + new_list = @permission_list + added_collection + assert_equal new_list, @permission_list, "did not add collection to #{@permission_list.inspect}}" + end + test "include? normalizes multi-parameter keys" do multi_param_key = 'admin(1)' new_list = @permission_list += [ 'admin' ] diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 3660b9b1e5..418a24294a 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -19,7 +19,7 @@ class SanitizerTest < ActiveModel::TestCase test "sanitize attributes" do original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } - attributes = @logger_sanitizer.sanitize(original_attributes, @authorizer) + attributes = @logger_sanitizer.sanitize(self.class, original_attributes, @authorizer) assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" assert !attributes.key?('admin'), "Denied key should be rejected" @@ -29,14 +29,14 @@ class SanitizerTest < ActiveModel::TestCase original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } log = StringIO.new self.logger = ActiveSupport::Logger.new(log) - @logger_sanitizer.sanitize(original_attributes, @authorizer) + @logger_sanitizer.sanitize(self.class, original_attributes, @authorizer) assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}") end test "debug mass assignment removal with StrictSanitizer" do original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } assert_raise ActiveModel::MassAssignmentSecurity::Error do - @strict_sanitizer.sanitize(original_attributes, @authorizer) + @strict_sanitizer.sanitize(self.class, original_attributes, @authorizer) end end @@ -44,7 +44,7 @@ class SanitizerTest < ActiveModel::TestCase original_attributes = {'id' => 1, 'first_name' => 'allowed'} assert_nothing_raised do - @strict_sanitizer.sanitize(original_attributes, @authorizer) + @strict_sanitizer.sanitize(self.class, original_attributes, @authorizer) end end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index be07e59a2f..0c6352cd71 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -4,7 +4,7 @@ require 'models/mass_assignment_specific' class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer - def process_removed_attributes(attrs) + def process_removed_attributes(klass, attrs) raise StandardError end @@ -19,6 +19,13 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal expected, sanitized end + def test_attribute_protection_when_role_is_nil + user = User.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), nil) + assert_equal expected, sanitized + end + def test_only_moderator_role_attribute_accessible user = SpecialUser.new expected = { "name" => "John Smith", "email" => "john@smith.com" } diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb new file mode 100644 index 0000000000..d93fd96b88 --- /dev/null +++ b/activemodel/test/cases/model_test.rb @@ -0,0 +1,26 @@ +require 'cases/helper' + +class ModelTest < ActiveModel::TestCase + include ActiveModel::Lint::Tests + + class BasicModel + include ActiveModel::Model + attr_accessor :attr + end + + def setup + @model = BasicModel.new + end + + def test_initialize_with_params + object = BasicModel.new(:attr => "value") + assert_equal object.attr, "value" + end + + def test_initialize_with_nil_or_empty_hash_params_does_not_explode + assert_nothing_raised do + BasicModel.new() + BasicModel.new({}) + end + end +end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 1e14d83bcb..49d8706ac2 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -25,12 +25,6 @@ class NamingTest < ActiveModel::TestCase assert_equal 'post/track_backs', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'post/track_backs/track_back', @model_name.partial_path - end - end - def test_human assert_equal 'Track back', @model_name.human end @@ -61,12 +55,6 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase assert_equal 'blog/posts', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'blog/posts/post', @model_name.partial_path - end - end - def test_human assert_equal 'Post', @model_name.human end @@ -105,12 +93,6 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase assert_equal 'blog/posts', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'blog/posts/post', @model_name.partial_path - end - end - def test_human assert_equal 'Post', @model_name.human end @@ -149,12 +131,6 @@ class NamingWithSuppliedModelNameTest < ActiveModel::TestCase assert_equal 'articles', @model_name.collection end - def test_partial_path - assert_deprecated(/#partial_path.*#to_partial_path/) do - assert_equal 'articles/article', @model_name.partial_path - end - end - def test_human assert_equal 'Article', @model_name.human end diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb index f6ec24ae57..ade6026602 100644 --- a/activemodel/test/cases/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -14,8 +14,8 @@ class FooObserver < ActiveModel::Observer attr_accessor :stub - def on_spec(record) - stub.event_with(record) if stub + def on_spec(record, *args) + stub.event_with(record, *args) if stub end def around_save(record) @@ -70,23 +70,38 @@ class ObservingTest < ActiveModel::TestCase ObservedModel.instantiate_observers end + test "raises an appropriate error when a developer accidentally adds the wrong class (i.e. Widget instead of WidgetObserver)" do + assert_raise ArgumentError do + ObservedModel.observers = ['string'] + ObservedModel.instantiate_observers + end + assert_raise ArgumentError do + ObservedModel.observers = [:string] + ObservedModel.instantiate_observers + end + assert_raise ArgumentError do + ObservedModel.observers = [String] + ObservedModel.instantiate_observers + end + end + test "passes observers to subclasses" do FooObserver.instance bar = Class.new(Foo) - assert_equal Foo.count_observers, bar.count_observers + assert_equal Foo.observers_count, bar.observers_count end end class ObserverTest < ActiveModel::TestCase def setup ObservedModel.observers = :foo_observer - FooObserver.instance_eval do + FooObserver.singleton_class.instance_eval do alias_method :original_observed_classes, :observed_classes end end def teardown - FooObserver.instance_eval do + FooObserver.singleton_class.instance_eval do undef_method :observed_classes alias_method :observed_classes, :original_observed_classes end @@ -98,44 +113,51 @@ class ObserverTest < ActiveModel::TestCase test "tracks implicit observable models" do instance = FooObserver.new - assert instance.send(:observed_classes).include?(Foo), "Foo not in #{instance.send(:observed_classes).inspect}" - assert !instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{instance.send(:observed_classes).inspect}" + assert_equal [Foo], instance.observed_classes end test "tracks explicit observed model class" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe ObservedModel instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "tracks explicit observed model as string" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe 'observed_model' instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "tracks explicit observed model as symbol" do - old_instance = FooObserver.new - assert !old_instance.send(:observed_classes).include?(ObservedModel), "ObservedModel in #{old_instance.send(:observed_classes).inspect}" FooObserver.observe :observed_model instance = FooObserver.new - assert instance.send(:observed_classes).include?(ObservedModel), "ObservedModel not in #{instance.send(:observed_classes).inspect}" + assert_equal [ObservedModel], instance.observed_classes end test "calls existing observer event" do foo = Foo.new FooObserver.instance.stub = stub FooObserver.instance.stub.expects(:event_with).with(foo) - Foo.send(:notify_observers, :on_spec, foo) + Foo.notify_observers(:on_spec, foo) + end + + test "calls existing observer event from the instance" do + foo = Foo.new + FooObserver.instance.stub = stub + FooObserver.instance.stub.expects(:event_with).with(foo) + foo.notify_observers(:on_spec) + end + + test "passes extra arguments" do + foo = Foo.new + FooObserver.instance.stub = stub + FooObserver.instance.stub.expects(:event_with).with(foo, :bar) + Foo.send(:notify_observers, :on_spec, foo, :bar) end test "skips nonexistent observer event" do foo = Foo.new - Foo.send(:notify_observers, :whatever, foo) + Foo.notify_observers(:whatever, foo) end test "update passes a block on to the observer" do @@ -145,4 +167,15 @@ class ObserverTest < ActiveModel::TestCase end assert_equal :in_around_save, yielded_value end + + test "observe redefines observed_classes class method" do + class BarObserver < ActiveModel::Observer + observe :foo + end + + assert_equal [Foo], BarObserver.observed_classes + + BarObserver.observe(ObservedModel) + assert_equal [ObservedModel], BarObserver.observed_classes + end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 4338a3fc53..5f18909301 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -7,28 +7,38 @@ class SecurePasswordTest < ActiveModel::TestCase setup do @user = User.new + @visitor = Visitor.new end test "blank password" do - @user.password = '' - assert !@user.valid?, 'user should be invalid' + @user.password = @visitor.password = '' + assert !@user.valid?(:create), 'user should be invalid' + assert @visitor.valid?(:create), 'visitor should be valid' end test "nil password" do - @user.password = nil - assert !@user.valid?, 'user should be invalid' + @user.password = @visitor.password = nil + assert !@user.valid?(:create), 'user should be invalid' + assert @visitor.valid?(:create), 'visitor should be valid' + end + + test "blank password doesn't override previous password" do + @user.password = 'test' + @user.password = '' + assert_equal @user.password, 'test' end test "password must be present" do - assert !@user.valid? + assert !@user.valid?(:create) assert_equal 1, @user.errors.size end - test "password must match confirmation" do - @user.password = "thiswillberight" - @user.password_confirmation = "wrong" + test "match confirmation" do + @user.password = @visitor.password = "thiswillberight" + @user.password_confirmation = @visitor.password_confirmation = "wrong" assert !@user.valid? + assert @visitor.valid? @user.password_confirmation = "thiswillberight" @@ -53,4 +63,14 @@ class SecurePasswordTest < ActiveModel::TestCase assert !active_authorizer.include?(:password_digest) assert active_authorizer.include?(:name) end + + test "User should not be created with blank digest" do + assert_raise RuntimeError do + @user.run_callbacks :create + end + @user.password = "supersecretpassword" + assert_nothing_raised do + @user.run_callbacks :create + end + end end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index b8dad9d51f..d2ba9fd95d 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -43,38 +43,38 @@ class SerializationTest < ActiveModel::TestCase end def test_method_serializable_hash_should_work - expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} - assert_equal expected , @user.serializable_hash + 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]) + 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]) + 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]) + expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "email"=>"david@example.com"} + assert_equal expected, @user.serializable_hash(:methods => [:foo]) end def test_method_serializable_hash_should_work_with_only_and_methods - expected = {:foo=>"i_am_foo"} - assert_equal expected , @user.serializable_hash(:only => [], :methods => [:foo]) + expected = {"foo"=>"i_am_foo"} + assert_equal expected, @user.serializable_hash(:only => [], :methods => [:foo]) end def test_method_serializable_hash_should_work_with_except_and_methods - expected = {"gender"=>"male", :foo=>"i_am_foo"} - assert_equal expected , @user.serializable_hash(:except => [:name, :email], :methods => [:foo]) + expected = {"gender"=>"male", "foo"=>"i_am_foo"} + assert_equal expected, @user.serializable_hash(:except => [:name, :email], :methods => [:foo]) end def test_should_not_call_methods_that_dont_respond - expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} - assert_equal expected , @user.serializable_hash(:methods => [:bar]) + expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} + assert_equal expected, @user.serializable_hash(:methods => [:bar]) end def test_should_use_read_attribute_for_serialization @@ -87,65 +87,82 @@ class SerializationTest < ActiveModel::TestCase end def test_include_option_with_singular_association - expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com", - :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}} - assert_equal expected , @user.serializable_hash(:include => :address) + expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com", + "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}} + assert_equal expected, @user.serializable_hash(:include => :address) end def test_include_option_with_plural_association - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, - {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} - assert_equal expected , @user.serializable_hash(:include => :friends) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected, @user.serializable_hash(:include => :friends) end def test_include_option_with_empty_association @user.friends = [] - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", :friends=>[]} - assert_equal expected , @user.serializable_hash(:include => :friends) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", "friends"=>[]} + assert_equal expected, @user.serializable_hash(:include => :friends) + end + + class FriendList + def initialize(friends) + @friends = friends + end + + def to_ary + @friends + end + end + + def test_include_option_with_ary + @user.friends = FriendList.new(@user.friends) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected, @user.serializable_hash(:include => :friends) end def test_multiple_includes - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}, - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, - {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} - assert_equal expected , @user.serializable_hash(:include => [:address, :friends]) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}, + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected, @user.serializable_hash(:include => [:address, :friends]) end def test_include_with_options - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :address=>{"street"=>"123 Lane"}} - assert_equal expected , @user.serializable_hash(:include => {:address => {:only => "street"}}) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "address"=>{"street"=>"123 Lane"}} + assert_equal expected, @user.serializable_hash(:include => {:address => {:only => "street"}}) end def test_nested_include @user.friends.first.friends = [@user] - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', - :friends => [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]}, - {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', :friends => []}]} - assert_equal expected , @user.serializable_hash(:include => {:friends => {:include => :friends}}) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', + "friends"=> [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', "friends"=> []}]} + assert_equal expected, @user.serializable_hash(:include => {:friends => {:include => :friends}}) end def test_only_include - expected = {"name"=>"David", :friends => [{"name" => "Joe"}, {"name" => "Sue"}]} - assert_equal expected , @user.serializable_hash(:only => :name, :include => {:friends => {:only => :name}}) + expected = {"name"=>"David", "friends" => [{"name" => "Joe"}, {"name" => "Sue"}]} + assert_equal expected, @user.serializable_hash(:only => :name, :include => {:friends => {:only => :name}}) end def test_except_include expected = {"name"=>"David", "email"=>"david@example.com", - :friends => [{"name" => 'Joe', "email" => 'joe@example.com'}, - {"name" => "Sue", "email" => 'sue@example.com'}]} - assert_equal expected , @user.serializable_hash(:except => :gender, :include => {:friends => {:except => :gender}}) + "friends"=> [{"name" => 'Joe', "email" => 'joe@example.com'}, + {"name" => "Sue", "email" => 'sue@example.com'}]} + assert_equal expected, @user.serializable_hash(:except => :gender, :include => {:friends => {:except => :gender}}) end def test_multiple_includes_with_options - expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", - :address=>{"street"=>"123 Lane"}, - :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, - {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} - assert_equal expected , @user.serializable_hash(:include => [{:address => {:only => "street"}}, :friends]) + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + "address"=>{"street"=>"123 Lane"}, + "friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected, @user.serializable_hash(:include => [{:address => {:only => "street"}}, :friends]) end - end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 4ac5fb1779..e2690f1827 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -31,10 +31,15 @@ class JsonSerializationTest < ActiveModel::TestCase @contact.preferences = { 'shows' => 'anime' } end - test "should include root in json" do + def teardown + # set to the default value + Contact.include_root_in_json = false + end + + test "should not include root in json (class method)" do json = @contact.to_json - assert_match %r{^\{"contact":\{}, json + assert_no_match %r{^\{"contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) @@ -42,41 +47,31 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "should not include root in json (class method)" do - begin - Contact.include_root_in_json = false - json = @contact.to_json - - assert_no_match %r{^\{"contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json - ensure - Contact.include_root_in_json = true - end + test "should include root in json if include_root_in_json is true" do + Contact.include_root_in_json = true + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json end test "should include root in json (option) even if the default is set to false" do - begin - Contact.include_root_in_json = false - json = @contact.to_json(:root => true) - assert_match %r{^\{"contact":\{}, json - ensure - Contact.include_root_in_json = true - end + json = @contact.to_json(root: true) + assert_match %r{^\{"contact":\{}, json end test "should not include root in json (option)" do - - json = @contact.to_json(:root => false) + json = @contact.to_json(root: false) assert_no_match %r{^\{"contact":\{}, json end test "should include custom root in json" do - json = @contact.to_json(:root => 'json_contact') + json = @contact.to_json(root: 'json_contact') assert_match %r{^\{"json_contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json @@ -107,7 +102,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should allow attribute filtering with except" do - json = @contact.to_json(:except => [:name, :age]) + json = @contact.to_json(except: [:name, :age]) assert_no_match %r{"name":"Konata Izumi"}, json assert_no_match %r{"age":16}, json @@ -122,35 +117,36 @@ class JsonSerializationTest < ActiveModel::TestCase def @contact.favorite_quote; "Constraints are liberating"; end # Single method. - assert_match %r{"label":"Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label) + assert_match %r{"label":"Has cheezburger"}, @contact.to_json(only: :name, methods: :label) # Both methods. - methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote]) + methods_json = @contact.to_json(only: :name, methods: [:label, :favorite_quote]) assert_match %r{"label":"Has cheezburger"}, methods_json assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end - test "should return OrderedHash for errors" do + test "should return Hash for errors" do 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 = {} 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 } + 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 + test "as_json should return a hash if include_root_in_json is true" do + Contact.include_root_in_json = true json = @contact.as_json assert_kind_of Hash, json @@ -160,7 +156,7 @@ class JsonSerializationTest < ActiveModel::TestCase end end - test "from_json should set the object's attributes" do + test "from_json should work without a root (class attribute)" do json = @contact.to_json result = Contact.new.from_json(json) @@ -172,7 +168,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "from_json should work without a root (method parameter)" do - json = @contact.to_json(:root => false) + json = @contact.to_json result = Contact.new.from_json(json, false) assert_equal result.name, @contact.name @@ -182,24 +178,19 @@ class JsonSerializationTest < ActiveModel::TestCase assert_equal result.preferences, @contact.preferences end - test "from_json should work without a root (class attribute)" do - begin - Contact.include_root_in_json = false - json = @contact.to_json - result = Contact.new.from_json(json) - - assert_equal result.name, @contact.name - assert_equal result.age, @contact.age - assert_equal Time.parse(result.created_at), @contact.created_at - assert_equal result.awesome, @contact.awesome - assert_equal result.preferences, @contact.preferences - ensure - Contact.include_root_in_json = true - end + test "from_json should work with a root (method parameter)" do + json = @contact.to_json(root: :true) + result = Contact.new.from_json(json, true) + + assert_equal result.name, @contact.name + assert_equal result.age, @contact.age + assert_equal Time.parse(result.created_at), @contact.created_at + assert_equal result.awesome, @contact.awesome + assert_equal result.preferences, @contact.preferences end test "custom as_json should be honored when generating json" do - def @contact.as_json(options); { :name => name, :created_at => created_at }; end + def @contact.as_json(options); { name: name, created_at: created_at }; end json = @contact.to_json assert_match %r{"name":"Konata Izumi"}, json @@ -209,7 +200,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "custom as_json options should be extendible" do - def @contact.as_json(options = {}); super(options.merge(:only => [:name])); end + def @contact.as_json(options = {}); super(options.merge(only: [:name])); end json = @contact.to_json assert_match %r{"name":"Konata Izumi"}, json @@ -217,5 +208,4 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"awesome":}, json assert_no_match %r{"preferences":}, json end - end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 38aecf51ff..7eb48abc3c 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -140,7 +140,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize datetime" do - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end test "should serialize boolean" do @@ -188,6 +188,23 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<friend type="Contact">}, xml end + class FriendList + def initialize(friends) + @friends = friends + end + + def to_ary + @friends + end + end + + test "include option with ary" do + @contact.friends = FriendList.new(@contact.friends) + xml = @contact.to_xml :include => :friends, :indent => 0 + assert_match %r{<friends type="array">}, xml + assert_match %r{<friend type="Contact">}, xml + end + test "multiple includes" do xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => [ :address, :friends ] assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true)) diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 54e86d48db..fd833cdd06 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -56,6 +56,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') end + def test_translated_deeply_nested_model_attributes + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/contacts/addresses" => {:street => 'Deeply Nested Address Street'}}} + assert_equal 'Deeply Nested Address Street', Person.human_attribute_name('contacts.addresses.street') + end + def test_translated_nested_model_attributes I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/addresses" => {:street => 'Person Address Street'}}} assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street') @@ -82,9 +87,15 @@ class ActiveModelI18nTests < ActiveModel::TestCase end def test_human_does_not_modify_options - options = {:default => 'person model'} + options = { :default => 'person model' } Person.model_name.human(options) - assert_equal({:default => 'person model'}, options) + assert_equal({ :default => 'person model' }, options) + end + + def test_human_attribute_name_does_not_modify_options + options = { :default => 'Cool gender' } + Person.human_attribute_name('gender', options) + assert_equal({ :default => 'Cool gender' }, options) end end diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index 1cf09758f9..e4f602bd80 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -5,11 +5,10 @@ class Dog include ActiveModel::Validations include ActiveModel::Validations::Callbacks - attr_accessor :name - attr_writer :history + attr_accessor :name, :history - def history - @history ||= [] + def initialize + @history = [] end end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index d0418170fa..f7556a249f 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -44,7 +44,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase p.karma_confirmation = "None" assert p.invalid? - assert_equal ["doesn't match confirmation"], p.errors[:karma] + assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation] p.karma = "None" assert p.valid? @@ -52,4 +52,23 @@ class ConfirmationValidationTest < ActiveModel::TestCase Person.reset_callbacks(:validate) end + def test_title_confirmation_with_i18n_attribute + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations('en', { + :errors => {:messages => {:confirmation => "doesn't match %{attribute}"}}, + :activemodel => {:attributes => {:topic => {:title => 'Test Title'}}} + }) + + Topic.validates_confirmation_of(:title) + + t = Topic.new("title" => "We should be confirmed","title_confirmation" => "") + assert t.invalid? + assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] + + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 41a1131bcb..308a3c6cef 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -11,7 +11,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format - Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") + Topic.validates_format_of(:title, :content, :with => /\AValidation\smacros \w+!\z/, :message => "is bad data") t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!") assert t.invalid?, "Shouldn't be valid" @@ -27,7 +27,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_with_allow_blank - Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank => true) + Topic.validates_format_of(:title, :with => /\AValidation\smacros \w+!\z/, :allow_blank => true) assert Topic.new("title" => "Shouldn't be valid").invalid? assert Topic.new("title" => "").valid? assert Topic.new("title" => nil).valid? @@ -36,7 +36,7 @@ class PresenceValidationTest < ActiveModel::TestCase # testing ticket #3142 def test_validate_format_numeric - Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") + Topic.validates_format_of(:title, :content, :with => /\A[1-9][0-9]*\z/, :message => "is bad data") t = Topic.new("title" => "72x", "content" => "6789") assert t.invalid?, "Shouldn't be valid" @@ -63,11 +63,21 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_with_formatted_message - Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be %{value}") + Topic.validates_format_of(:title, :with => /\AValid Title\z/, :message => "can't be %{value}") t = Topic.new(:title => 'Invalid title') assert t.invalid? assert_equal ["can't be Invalid title"], t.errors[:title] end + + def test_validate_format_of_with_multiline_regexp_should_raise_error + assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /^Valid Title$/) } + end + + def test_validate_format_of_with_multiline_regexp_and_option + assert_nothing_raised(ArgumentError) do + Topic.validates_format_of(:title, :with => /^Valid Title$/, :multiline => true) + end + end def test_validate_format_with_not_option Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo") diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 0679e67f84..df0fcd243a 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -37,7 +37,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase # validates_confirmation_of: generate_message(attr_name, :confirmation, :message => custom_message) def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @person.errors.generate_message(:title, :confirmation) + assert_equal "doesn't match Title", @person.errors.generate_message(:title, :confirmation) end def test_generate_message_confirmation_with_custom_message diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index e9f0e430fe..bb751cf9c5 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -81,7 +81,7 @@ class I18nValidationTest < ActiveModel::TestCase test "validates_confirmation_of on generated message #{name}" do Person.validates_confirmation_of :title, validation_options @person.title_confirmation = 'foo' - @person.errors.expects(:generate_message).with(:title, :confirmation, generate_message_options) + @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(:attribute => 'Title')) @person.valid? end end @@ -141,7 +141,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_format_of on generated message #{name}" do - Person.validates_format_of :title, validation_options.merge(:with => /^[1-9][0-9]*$/) + Person.validates_format_of :title, validation_options.merge(:with => /\A[1-9][0-9]*\z/) @person.title = '72x' @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(:value => '72x')) @person.valid? @@ -217,24 +217,29 @@ class I18nValidationTest < ActiveModel::TestCase # To make things DRY this macro is defined to define 3 tests for every validation case. def self.set_expectations_for_validation(validation, error_type, &block_that_sets_validation) + if error_type == :confirmation + attribute = :title_confirmation + else + attribute = :title + end # test "validates_confirmation_of finds custom model key translation when blank" test "#{validation} finds custom model key translation when #{error_type}" do - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message'}}}}}} I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} yield(@person, {}) @person.valid? - assert_equal ['custom message'], @person.errors[:title] + assert_equal ['custom message'], @person.errors[attribute] end # test "validates_confirmation_of finds custom model key translation with interpolation when blank" test "#{validation} finds custom model key translation with interpolation when #{error_type}" do - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {error_type => 'custom message with %{extra}'}}}}}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message with %{extra}'}}}}}} I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}} yield(@person, {:extra => "extra information"}) @person.valid? - assert_equal ['custom message with extra information'], @person.errors[:title] + assert_equal ['custom message with extra information'], @person.errors[attribute] end # test "validates_confirmation_of finds global default key translation when blank" @@ -243,7 +248,7 @@ class I18nValidationTest < ActiveModel::TestCase yield(@person, {}) @person.valid? - assert_equal ['global message'], @person.errors[:title] + assert_equal ['global message'], @person.errors[attribute] end end @@ -286,7 +291,7 @@ class I18nValidationTest < ActiveModel::TestCase # validates_format_of w/o mocha set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| - Person.validates_format_of :title, options_to_merge.merge(:with => /^[1-9][0-9]*$/) + Person.validates_format_of :title, options_to_merge.merge(:with => /\A[1-9][0-9]*\z/) end # validates_inclusion_of w/o mocha diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index aa86d9d959..113bfd6337 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -357,4 +357,22 @@ class LengthValidationTest < ActiveModel::TestCase ensure Person.reset_callbacks(:validate) end + + def test_validates_length_of_for_infinite_maxima + Topic.validates_length_of(:title, :within => 5..Float::INFINITY) + + t = Topic.new("title" => "1234") + assert t.invalid? + assert t.errors[:title].any? + + t.title = "12345" + assert t.valid? + + Topic.validates_length_of(:author_name, :maximum => Float::INFINITY) + + assert t.valid? + + t.author_name = "A very long author name that should still be valid." * 100 + assert t.valid? + end end diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 575154ffbd..90bc018ae1 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -154,6 +154,6 @@ class ValidatesTest < ActiveModel::TestCase topic.title = "What's happening" topic.title_confirmation = "Not this" assert !topic.valid? - assert_equal ['Y U NO CONFIRM'], topic.errors[:title] + assert_equal ['Y U NO CONFIRM'], topic.errors[:title_confirmation] end end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 2f4376bd41..8ea9745fbf 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -58,8 +58,7 @@ class ValidationsTest < ActiveModel::TestCase r = Reply.new r.valid? - errors = [] - r.errors.each {|attr, messages| errors << [attr.to_s, messages] } + errors = r.errors.collect {|attr, messages| [attr.to_s, messages]} assert errors.include?(["title", "is Empty"]) assert errors.include?(["content", "is Empty"]) @@ -181,7 +180,7 @@ class ValidationsTest < ActiveModel::TestCase 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 = {} hash[:title] = ["can't be blank"] hash[:content] = ["can't be blank"] assert_equal t.errors.to_json, hash.to_json @@ -311,7 +310,7 @@ class ValidationsTest < ActiveModel::TestCase end def test_strict_validation_particular_validator - Topic.validates :title, :presence => {:strict => true} + Topic.validates :title, :presence => { :strict => true } assert_raises ActiveModel::StrictValidationFailed do Topic.new.valid? end @@ -330,4 +329,39 @@ class ValidationsTest < ActiveModel::TestCase Topic.new.valid? end end + + def test_validates_with_false_hash_value + Topic.validates :title, :presence => false + assert Topic.new.valid? + end + + def test_strict_validation_error_message + Topic.validates :title, :strict => true, :presence => true + + exception = assert_raises(ActiveModel::StrictValidationFailed) do + Topic.new.valid? + end + assert_equal "Title can't be blank", exception.message + end + + def test_does_not_modify_options_argument + options = { :presence => true } + Topic.validates :title, options + assert_equal({ :presence => true }, options) + end + + def test_dup_validity_is_independent + Topic.validates_presence_of :title + topic = Topic.new("title" => "Litterature") + topic.valid? + + duped = topic.dup + duped.title = nil + assert duped.invalid? + + topic.title = nil + duped.title = 'Mathematics' + assert topic.invalid? + assert duped.valid? + end end diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb index a48f8b064f..2d6d34b3e2 100644 --- a/activemodel/test/models/administrator.rb +++ b/activemodel/test/models/administrator.rb @@ -1,7 +1,10 @@ class Administrator + extend ActiveModel::Callbacks include ActiveModel::Validations include ActiveModel::SecurePassword include ActiveModel::MassAssignmentSecurity + + define_model_callbacks :create attr_accessor :name, :password_digest attr_accessible :name diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index e221bb8091..4b11df12bf 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -1,6 +1,9 @@ class User + extend ActiveModel::Callbacks include ActiveModel::Validations include ActiveModel::SecurePassword + + define_model_callbacks :create has_secure_password diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb index 36c0a16688..d15f448516 100644 --- a/activemodel/test/models/visitor.rb +++ b/activemodel/test/models/visitor.rb @@ -1,9 +1,12 @@ class Visitor + extend ActiveModel::Callbacks include ActiveModel::Validations include ActiveModel::SecurePassword include ActiveModel::MassAssignmentSecurity + + define_model_callbacks :create - has_secure_password + has_secure_password(validations: false) - attr_accessor :password_digest + attr_accessor :password_digest, :password_confirmation end |