diff options
Diffstat (limited to 'activemodel')
53 files changed, 1113 insertions, 310 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index be4de2e53c..3d26d646b0 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,4 +1,13 @@ -*Rails 3.1.0 (unreleased)* +* Add ability to define strict validation(with :strict => true option) that always raises exception when fails [Bogdan Gusiev] + +* Deprecate "Model.model_name.partial_path" in favor of "model.to_partial_path" [Grant Hutchins, Peter Jaros] + +* 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.0 (August 30, 2011)* + +* Alternate I18n namespace lookup is no longer supported. + Instead of "activerecord.models.admins.post", do "activerecord.models.admins/post" instead [José Valim] * attr_accessible and friends now accepts :as as option to specify a role [Josh Kalderimis] diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index b5b5edd52a..67701bc422 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -182,3 +182,30 @@ modules: p.valid? # => true {Learn more}[link:classes/ActiveModel/Validator.html] + + +== Download and installation + +The latest version of Active Model can be installed with RubyGems: + + % [sudo] gem install activemodel + +Source code can be downloaded as part of the Rails project on GitHub + +* https://github.com/rails/rails/tree/master/activemodel + + +== License + +Active Model is released under the MIT license. + + +== Support + +API documentation is at + +* http://api.rubyonrails.org + +Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: + +* https://github.com/rails/rails/issues diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 0a10912695..c4b020196d 100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -20,11 +20,11 @@ namespace :test do end require 'rake/packagetask' -require 'rake/gempackagetask' +require 'rubygems/package_task' spec = eval(File.read("#{dir}/activemodel.gemspec")) -Rake::GemPackageTask.new(spec) do |p| +Gem::PackageTask.new(spec) do |p| p.gem_spec = spec end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index ce69c4a201..260ad01b65 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -12,13 +12,11 @@ Gem::Specification.new do |s| s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.rubyforge_project = 'activemodel' s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') - s.add_dependency('i18n', '~> 0.6.0beta1') - s.add_dependency('bcrypt-ruby', '~> 2.1.4') + s.add_dependency('i18n', '~> 0.6') end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 6ee5e04267..ef0b95424e 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/class/attribute' +require 'active_support/deprecation' module ActiveModel class MissingAttributeError < NoMethodError @@ -60,7 +61,7 @@ module ActiveModel included do class_attribute :attribute_method_matchers, :instance_writer => false - self.attribute_method_matchers = [] + self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end module ClassMethods @@ -92,10 +93,10 @@ module ActiveModel # # Provides you with: # - # AttributePerson.primary_key + # Person.primary_key # # => "sysid" - # AttributePerson.inheritance_column = 'address' - # AttributePerson.inheritance_column + # Person.inheritance_column = 'address' + # Person.inheritance_column # # => 'address_id' def define_attr_method(name, value=nil, &block) sing = singleton_class @@ -284,36 +285,29 @@ module ActiveModel def define_attribute_method(attr_name) attribute_method_matchers.each do |matcher| - unless instance_method_already_implemented?(matcher.method_name(attr_name)) - generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + method_name = matcher.method_name(attr_name) + + unless instance_method_already_implemented?(method_name) + generate_method = "define_method_#{matcher.method_missing_target}" if respond_to?(generate_method) send(generate_method, attr_name) else - method_name = matcher.method_name(attr_name) + if method_name =~ COMPILABLE_REGEXP + defn = "def #{method_name}(*args)" + else + defn = "define_method(:'#{method_name}') do |*args|" + end generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - if method_defined?('#{method_name}') - undef :'#{method_name}' + #{defn} + send(:#{matcher.method_missing_target}, '#{attr_name}', *args) end RUBY - - if method_name.to_s =~ COMPILABLE_REGEXP - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - RUBY - else - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - define_method('#{method_name}') do |*args| - send('#{matcher.method_missing_target}', '#{attr_name}', *args) - end - RUBY - end end end end + attribute_method_matchers_cache.clear end # Removes all the previously dynamically defined methods from the class @@ -321,6 +315,7 @@ module ActiveModel generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } end + attribute_method_matchers_cache.clear end # Returns true if the attribute methods defined have been generated. @@ -334,17 +329,53 @@ module ActiveModel protected def instance_method_already_implemented?(method_name) - method_defined?(method_name) + generated_attribute_methods.method_defined?(method_name) end private + # The methods +method_missing+ and +respond_to?+ of this module are + # invoked often in a typical rails, both of which invoke the method + # +match_attribute_method?+. The latter method iterates through an + # array doing regular expression matches, which results in a lot of + # object creations. Most of the times it returns a +nil+ match. As the + # match result is always the same given a +method_name+, this cache is + # used to alleviate the GC, which ultimately also speeds up the app + # significantly (in our case our test suite finishes 10% faster with + # this cache). + def attribute_method_matchers_cache + @attribute_method_matchers_cache ||= {} + end + + def attribute_method_matcher(method_name) + if attribute_method_matchers_cache.key?(method_name) + attribute_method_matchers_cache[method_name] + else + # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix + # will match every time. + matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) + match = nil + matchers.detect { |method| match = method.match(method_name) } + attribute_method_matchers_cache[method_name] = match + end + end + class AttributeMethodMatcher attr_reader :prefix, :suffix, :method_missing_target - AttributeMethodMatch = Struct.new(:target, :attr_name) + AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) options.symbolize_keys! + + if options[:prefix] == '' || options[:suffix] == '' + ActiveSupport::Deprecation.warn( + "Specifying an empty prefix/suffix for an attribute method is no longer " \ + "necessary. If the un-prefixed/suffixed version of the method has not been " \ + "defined when `define_attribute_methods` is called, it will be defined " \ + "automatically." + ) + end + @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @@ -353,7 +384,7 @@ module ActiveModel def match(method_name) if @regex =~ method_name - AttributeMethodMatch.new(method_missing_target, $2) + AttributeMethodMatch.new(method_missing_target, $2, method_name) else nil end @@ -362,6 +393,10 @@ module ActiveModel def method_name(attr_name) @method_name % attr_name end + + def plain? + prefix.empty? && suffix.empty? + end end end @@ -376,13 +411,21 @@ module ActiveModel # It's also possible to instantiate related objects, so a Client class # belonging to the clients table with a +master_id+ foreign key can # instantiate master through Client#master. - def method_missing(method_id, *args, &block) - method_name = method_id.to_s - if match = match_attribute_method?(method_name) - guard_private_attribute_method!(method_name, args) - return __send__(match.target, match.attr_name, *args, &block) + def method_missing(method, *args, &block) + if respond_to_without_attributes?(method, true) + super + else + match = match_attribute_method?(method.to_s) + match ? attribute_missing(match, *args, &block) : super end - super + end + + # attribute_missing is like method_missing, but for attributes. When method_missing is + # called we check to see if there is a matching attribute method. If so, we call + # attribute_missing to dispatch the attribute. This method can be overloaded to + # customise the behaviour. + def attribute_missing(match, *args, &block) + __send__(match.target, match.attr_name, *args, &block) end # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, @@ -391,15 +434,14 @@ module ActiveModel alias :respond_to_without_attributes? :respond_to? def respond_to?(method, include_private_methods = false) if super - return true + true elsif !include_private_methods && super(method, true) # If we're here then we haven't found among non-private methods # but found among all methods. Which means that the given method is private. - return false - elsif match_attribute_method?(method.to_s) - return true + false + else + !match_attribute_method?(method.to_s).nil? end - super end protected @@ -411,19 +453,8 @@ module ActiveModel # Returns a struct representing the matching attribute method. # The struct's attributes are prefix, base and suffix. def match_attribute_method?(method_name) - self.class.attribute_method_matchers.each do |method| - if (match = method.match(method_name)) && attribute_method?(match.attr_name) - return match - end - end - nil - end - - # prevent method_missing from calling private methods with #send - def guard_private_attribute_method!(method_name, args) - if self.class.private_method_defined?(method_name) - raise NoMethodError.new("Attempt to call private method `#{method_name}'", method_name, args) - end + match = self.class.send(:attribute_method_matcher, method_name) + match && attribute_method?(match.attr_name) ? match : nil end def missing_attribute(attr_name, stack) diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 2a1f51a9a7..37d0c9a0b9 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -59,7 +59,7 @@ module ActiveModel # define_model_callbacks :initializer, :only => :after # # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on - # that method call. To get around this you can call the define_model_callbacks + # that method call. To get around this you can call the define_model_callbacks # method as many times as you need. # # define_model_callbacks :create, :only => :after diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index e3992e842a..80a3ba51c3 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,9 +1,12 @@ +require 'active_support/concern' +require 'active_support/inflector' + module ActiveModel # == Active Model Conversions # - # Handles default conversions: to_model, to_key and to_param. + # Handles default conversions: to_model, to_key, to_param, and to_partial_path. # - # Let's take for example this non persisted object. + # Let's take for example this non-persisted object. # # class ContactMessage # include ActiveModel::Conversion @@ -18,11 +21,14 @@ module ActiveModel # cm.to_model == self # => true # cm.to_key # => nil # cm.to_param # => nil + # cm.to_path # => "contact_messages/contact_message" # module Conversion + extend ActiveSupport::Concern + # If your object is already designed to implement all of the Active Model - # you can use the default to_model implementation, which simply returns - # self. + # you can use the default <tt>:to_model</tt> implementation, which simply + # returns self. # # If your model does not act like an Active Model object, then you should # define <tt>:to_model</tt> yourself returning a proxy object that wraps @@ -35,15 +41,33 @@ module ActiveModel # if the object is persisted or not. # # Note the default implementation uses persisted? just because all objects - # in Ruby 1.8.x responds to :id. + # in Ruby 1.8.x responds to <tt>:id</tt>. def to_key persisted? ? [id] : nil end # Returns a string representing the object's key suitable for use in URLs, - # or nil if persisted? is false + # or nil if <tt>persisted?</tt> is false. def to_param persisted? ? to_key.join('-') : nil end + + # Returns a string identifying the path associated with the object. + # ActionPack uses this to find a suitable partial to represent the object. + def to_partial_path + self.class._to_partial_path + end + + module ClassMethods #:nodoc: + # Provide a class level cache for the to_path. This is an + # internal method and should not be accessed directly. + def _to_partial_path #:nodoc: + @_to_partial_path ||= begin + element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)) + collection = ActiveSupport::Inflector.tableize(self) + "#{collection}/#{element}".freeze + end + end + end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 3b412d3dd7..166cccf161 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -156,7 +156,7 @@ module ActiveModel rescue TypeError, NoMethodError end - changed_attributes[attr] = value + changed_attributes[attr] = value unless changed_attributes.include?(attr) 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 22ca3efa2b..d91e4a2b6a 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -49,8 +49,8 @@ module ActiveModel # # The last three methods are required in your object for Errors to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translations - # you will not need to implement the last two. Likewise, using + # languages. Of course, if you extend your object with ActiveModel::Translation + # you will not need to implement the last two. Likewise, using # ActiveModel::Validations will handle the validation related methods # for you. # @@ -63,7 +63,7 @@ module ActiveModel class Errors include Enumerable - CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] attr_reader :messages @@ -86,8 +86,9 @@ module ActiveModel # Do the error messages include an error with key +error+? def include?(error) - messages.include? error + (v = messages[error]) && v.any? end + alias :has_key? :include? # Get messages for +key+ def get(key) @@ -117,7 +118,7 @@ module ActiveModel end # Iterates through each error key, value pair in the error messages hash. - # Yields the attribute and the error for that attribute. If the attribute + # Yields the attribute and the error for that attribute. If the attribute # has more than one error message, yields once for each error message. # # p.errors.add(:name, "can't be blank") @@ -174,11 +175,12 @@ module ActiveModel to_a.size end - # Returns true if there are any errors, false if not. + # Returns true if no errors are found, false otherwise. def empty? all? { |k, v| v && v.empty? } end alias_method :blank?, :empty? + # Returns an xml formatted representation of the Errors hash. # # p.errors.add(:name, "can't be blank") @@ -218,6 +220,9 @@ module ActiveModel elsif message.is_a?(Proc) message = message.call end + if options[:strict] + raise ActiveModel::StrictValidationFailed, message + end self[attribute] << message end @@ -248,22 +253,24 @@ module ActiveModel # # company = Company.create(:address => '123 First St.') # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] def full_messages - map { |attribute, message| - if attribute == :base - message - else - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - - I18n.t(:"errors.format", { - :default => "%{attribute} %{message}", - :attribute => attr_name, - :message => message - }) - end - } + map { |attribute, message| full_message(attribute, message) } + end + + # Returns a full message for a given attribute. + # + # company.errors.full_message(:name, "is invalid") # => + # "Name is invalid" + def full_message(attribute, message) + return message if attribute == :base + attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) + I18n.t(:"errors.format", { + :default => "%{attribute} %{message}", + :attribute => attr_name, + :message => message + }) end # Translates an error message in its default scope @@ -319,4 +326,7 @@ module ActiveModel I18n.translate(key, options) end end + + class StrictValidationFailed < StandardError + end end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index b71ef4b22e..bfe7ea1869 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -43,6 +43,16 @@ module ActiveModel assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end + # == Responds to <tt>to_partial_path</tt> + # + # Returns a string giving a relative path. This is used for looking up + # partials. For example, a BlogPost model might return "blog_posts/blog_post" + # + def test_to_partial_path + assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" + 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 @@ -66,15 +76,14 @@ module ActiveModel # == Naming # - # Model.model_name must return a string with some convenience methods as - # :human and :partial_path. Check ActiveModel::Naming for more information. + # Model.model_name must return a string with some convenience methods: + # :human, :singular, and :plural. Check ActiveModel::Naming for more information. # 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.partial_path assert_kind_of String, model_name.singular assert_kind_of String, model_name.plural end diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 483b577681..3f9feb7631 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -1,5 +1,8 @@ -require 'active_support/core_ext/class/attribute.rb' +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/array/wrap' require 'active_model/mass_assignment_security/permission_set' +require 'active_model/mass_assignment_security/sanitizer' module ActiveModel # = Active Model Mass-Assignment Security @@ -10,6 +13,9 @@ module ActiveModel class_attribute :_accessible_attributes class_attribute :_protected_attributes class_attribute :_active_authorizer + + class_attribute :_mass_assignment_sanitizer + self.mass_assignment_sanitizer = :logger end # Mass assignment security provides an interface for protecting attributes @@ -41,6 +47,16 @@ module ActiveModel # # end # + # = Configuration options + # + # * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible values are: + # * <tt>:logger</tt> (default) - writes filtered attributes to logger + # * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> on any protected attribute update + # + # You can specify your own sanitizer object eg. MySanitizer.new. + # See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation. + # + # module ClassMethods # Attributes named in this macro are protected from mass-assignment # whenever attributes are sanitized before assignment. A role for the @@ -95,8 +111,11 @@ module ActiveModel options = args.extract_options! role = options[:as] || :default - self._protected_attributes = protected_attributes_configs.dup - self._protected_attributes[role] = self.protected_attributes(role) + args + self._protected_attributes = protected_attributes_configs.dup + + Array.wrap(role).each do |name| + self._protected_attributes[name] = self.protected_attributes(name) + args + end self._active_authorizer = self._protected_attributes end @@ -154,8 +173,11 @@ module ActiveModel options = args.extract_options! role = options[:as] || :default - self._accessible_attributes = accessible_attributes_configs.dup - self._accessible_attributes[role] = self.accessible_attributes(role) + args + self._accessible_attributes = accessible_attributes_configs.dup + + Array.wrap(role).each do |name| + self._accessible_attributes[name] = self.accessible_attributes(name) + args + end self._active_authorizer = self._accessible_attributes end @@ -177,21 +199,25 @@ module ActiveModel [] end + def mass_assignment_sanitizer=(value) + self._mass_assignment_sanitizer = if value.is_a?(Symbol) + const_get(:"#{value.to_s.camelize}Sanitizer").new(self) + else + value + end + end + private def protected_attributes_configs self._protected_attributes ||= begin - default_black_list = BlackList.new(attributes_protected_by_default).tap do |w| - w.logger = self.logger if self.respond_to?(:logger) - end - Hash.new(default_black_list) + Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) } end end def accessible_attributes_configs self._accessible_attributes ||= begin - default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } - Hash.new(default_white_list) + Hash.new { |h,k| h[k] = WhiteList.new } end end end @@ -199,7 +225,7 @@ module ActiveModel protected def sanitize_for_mass_assignment(attributes, role = :default) - mass_assignment_authorizer(role).sanitize(attributes) + _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) end def mass_assignment_authorizer(role = :default) 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 9fcb94d48a..a1fcdf1a38 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -1,10 +1,8 @@ require 'set' -require 'active_model/mass_assignment_security/sanitizer' module ActiveModel module MassAssignmentSecurity class PermissionSet < Set - attr_accessor :logger def +(values) super(values.map(&:to_s)) @@ -14,6 +12,10 @@ module ActiveModel super(remove_multiparameter_id(key)) end + def deny?(key) + raise NotImplementedError, "#deny?(key) suppose to be overwritten" + end + protected def remove_multiparameter_id(key) @@ -22,7 +24,6 @@ module ActiveModel end class WhiteList < PermissionSet - include Sanitizer def deny?(key) !include?(key) @@ -30,7 +31,6 @@ module ActiveModel end class BlackList < PermissionSet - include Sanitizer def deny?(key) 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 150beb1ff2..bbdddfb50d 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -1,9 +1,14 @@ +require 'active_support/core_ext/module/delegation' + module ActiveModel module MassAssignmentSecurity - module Sanitizer + class Sanitizer + def initialize(target=nil) + end + # Returns all attributes not denied by the authorizer. - def sanitize(attributes) - sanitized_attributes = attributes.reject { |key, value| deny?(key) } + def sanitize(attributes, authorizer) + sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) } debug_protected_attribute_removal(attributes, sanitized_attributes) sanitized_attributes end @@ -12,12 +17,43 @@ module ActiveModel def debug_protected_attribute_removal(attributes, sanitized_attributes) removed_keys = attributes.keys - sanitized_attributes.keys - warn!(removed_keys) if removed_keys.any? + process_removed_attributes(removed_keys) if removed_keys.any? + end + + def process_removed_attributes(attrs) + raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten" + end + end + + class LoggerSanitizer < Sanitizer + delegate :logger, :to => :@target + + def initialize(target) + @target = target + super end - def warn!(attrs) - self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger + def logger? + @target.respond_to?(:logger) && @target.logger end + + def process_removed_attributes(attrs) + logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? + end + end + + class StrictSanitizer < Sanitizer + def process_removed_attributes(attrs) + return if (attrs - insensitive_attributes).empty? + raise ActiveModel::MassAssignmentSecurity::Error, "Can't mass-assign protected attributes: #{attrs.join(', ')}" + end + + def insensitive_attributes + ['id'] + end + end + + class Error < StandardError end end end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 74708692af..f16459ede2 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,14 +1,18 @@ 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' module ActiveModel class Name < String attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key alias_method :cache_key, :collection - def initialize(klass, namespace = nil) - super(klass.name) + deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." + + def initialize(klass, namespace = nil, name = nil) + name ||= klass.name + super(name) @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace @klass = klass diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb index 5fb73f1c78..3d463885be 100644 --- a/activemodel/lib/active_model/observer_array.rb +++ b/activemodel/lib/active_model/observer_array.rb @@ -15,7 +15,7 @@ module ActiveModel disabled_observers.include?(observer.class) end - # Disables one or more observers. This supports multiple forms: + # Disables one or more observers. This supports multiple forms: # # ORM.observers.disable :user_observer # # => disables the UserObserver @@ -38,7 +38,7 @@ module ActiveModel set_enablement(false, observers, &block) end - # Enables one or more observers. This supports multiple forms: + # Enables one or more observers. This supports multiple forms: # # ORM.observers.enable :user_observer # # => enables the UserObserver @@ -59,7 +59,7 @@ module ActiveModel # # just the duration of the block # end # - # Note: all observers are enabled by default. This method is only + # Note: all observers are enabled by default. This method is only # useful when you have previously disabled one or more observers. def enable(*observers, &block) set_enablement(true, observers, &block) diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 4682ae07ef..7a910d18e7 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -71,9 +71,7 @@ module ActiveModel # Notify list of observers of a change. def notify_observers(*arg) - for observer in observer_instances - observer.update(*arg) - end + observer_instances.each { |observer| observer.update(*arg) } end # Total number of observers. @@ -127,7 +125,7 @@ module ActiveModel # # class CommentObserver < ActiveModel::Observer # def after_save(comment) - # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment) + # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver # end # end # @@ -228,10 +226,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) #:nodoc: + def update(observed_method, object, &block) #:nodoc: return unless respond_to?(observed_method) return if disabled_for?(object) - send(observed_method, object) + send(observed_method, object, &block) end # Special method sent by the observed class when it is inherited. diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index ee94ad66cf..7a109d9a52 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -1,5 +1,3 @@ -require 'bcrypt' - module ActiveModel module SecurePassword extend ActiveSupport::Concern @@ -12,6 +10,10 @@ module ActiveModel # a "password_confirmation" attribute) are automatically added. # 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: + # + # gem 'bcrypt-ruby', '~> 3.0.0' + # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # # # Schema: User(name:string, password_digest:string) @@ -30,7 +32,12 @@ module ActiveModel # User.find_by_name("david").try(:authenticate, "notright") # => nil # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user def has_secure_password - attr_reader :password + # Load bcrypt-ruby only when has_secured_password is used to avoid make ActiveModel + # (and by extension the entire framework) 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 diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index caf44a2ee0..7bc3f997b5 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,5 +1,7 @@ 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 @@ -31,7 +33,7 @@ module ActiveModel # you want to serialize and their current value. # # Most of the time though, you will want to include the JSON or XML - # serializations. Both of these modules automatically include the + # serializations. Both of these modules automatically include the # ActiveModel::Serialization module, so there is no need to explicitly # include it. # @@ -69,18 +71,69 @@ module ActiveModel def serializable_hash(options = nil) options ||= {} - only = Array.wrap(options[:only]).map(&:to_s) - except = Array.wrap(options[:except]).map(&:to_s) - attribute_names = attributes.keys.sort - if only.any? - attribute_names &= only - elsif except.any? - attribute_names -= except + if only = options[:only] + attribute_names &= Array.wrap(only).map(&:to_s) + elsif except = options[:except] + attribute_names -= Array.wrap(except).map(&:to_s) + end + + hash = {} + attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } + + method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) } + method_names.each { |n| hash[n] = send(n) } + + serializable_add_includes(options) do |association, records, opts| + hash[association] = if records.is_a?(Enumerable) + records.map { |a| a.serializable_hash(opts) } + else + records.serializable_hash(opts) + end end - method_names = Array.wrap(options[:methods]).map { |n| n if respond_to?(n.to_s) }.compact - Hash[(attribute_names + method_names).map { |n| [n, send(n)] }] + hash end + + private + + # Hook method defining how an attribute value should be retrieved for + # serialization. By default this is assumed to be an instance named after + # the attribute. Override this method in subclasses should you need to + # retrieve the value for a given attribute differently: + # + # class MyClass + # include ActiveModel::Validations + # + # def initialize(data = {}) + # @data = data + # end + # + # def read_attribute_for_serialization(key) + # @data[key] + # end + # end + # + alias :read_attribute_for_serialization :send + + # Add associations specified via the <tt>:include</tt> option. + # + # Expects a block that takes as arguments: + # +association+ - name of the association + # +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] + + unless include.is_a?(Hash) + include = Hash[Array.wrap(include).map { |n| [n, {}] }] + end + + include.each do |association, opts| + if records = send(association) + yield association, records, opts + end + end + end end end diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 0bfbf2aa06..c845440120 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -15,53 +15,60 @@ module ActiveModel self.include_root_in_json = true end - # Returns a JSON string representing the model. Some configuration can be + # 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: # - # konata = User.find(1) - # konata.as_json + # user = User.find(1) + # user.as_json # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} } # # ActiveRecord::Base.include_root_in_json = false - # konata.as_json + # user.as_json # # => {"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 false. + # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: # - # Without any +options+, the returned JSON string will include all the model's + # 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>. + # + # Without any +options+, the returned Hash will include all the model's # attributes. For example: # - # konata = User.find(1) - # konata.as_json + # user = User.find(1) + # user.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes # included, and work similar to the +attributes+ method. For example: # - # konata.as_json(:only => [ :id, :name ]) + # user.as_json(:only => [ :id, :name ]) # # => {"id": 1, "name": "Konata Izumi"} # - # konata.as_json(:except => [ :id, :created_at, :age ]) + # 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>: # - # konata.as_json(:methods => :permalink) + # 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>: # - # konata.as_json(:include => :posts) + # 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"}, @@ -69,7 +76,7 @@ module ActiveModel # # Second level and higher order associations work as well: # - # konata.as_json(:include => { :posts => { + # user.as_json(:include => { :posts => { # :include => { :comments => { # :only => :body } }, # :only => :title } }) @@ -79,21 +86,20 @@ module ActiveModel # "title": "Welcome to the weblog"}, # {"comments": [{"body": "Don't think too hard"}], # "title": "So I was thinking"}]} - def as_json(options = nil) - hash = serializable_hash(options) - - if include_root_in_json - custom_root = options && options[:root] - hash = { custom_root || self.class.model_name.element => hash } + root = include_root_in_json + root = options[:root] if options.try(:key?, :root) + if root + root = self.class.model_name.element if root == true + { root => serializable_hash(options) } + else + serializable_hash(options) end - - hash end - def from_json(json) + def from_json(json, include_root=include_root_in_json) hash = ActiveSupport::JSON.decode(json) - hash = hash.values.first if include_root_in_json + hash = hash.values.first if include_root self.attributes = hash self end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 19639b1363..d61d9d7119 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -15,17 +15,17 @@ module ActiveModel class Attribute #:nodoc: attr_reader :name, :value, :type - def initialize(name, serializable, raw_value=nil) + def initialize(name, serializable, value) @name, @serializable = name, serializable - raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone) - @value = raw_value || @serializable.send(name) + value = value.in_time_zone if value.respond_to?(:in_time_zone) + @value = value @type = compute_type end def decorations decorations = {} decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = type unless type == :string + decorations[:type] = (type == :string) ? nil : type decorations[:nil] = true if value.nil? decorations end @@ -49,40 +49,24 @@ module ActiveModel def initialize(serializable, options = nil) @serializable = serializable @options = options ? options.dup : {} - - @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s } - @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> - # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set - # for a N level model but is set for the N+1 level models, - # then because <tt>:except</tt> is set to a default value, the second - # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if - # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def attributes_hash - attributes = @serializable.attributes - if options[:only].any? - attributes.slice(*options[:only]) - elsif options[:except].any? - attributes.except(*options[:except]) - else - attributes - end + def serializable_hash + @serializable.serializable_hash(@options.except(:include)) end - def serializable_attributes - attributes_hash.map do |name, value| - self.class::Attribute.new(name, @serializable, value) + def serializable_collection + methods = Array.wrap(options[:methods]).map(&:to_s) + serializable_hash.map do |name, value| + name = name.to_s + if methods.include?(name) + self.class::MethodAttribute.new(name, @serializable, value) + else + self.class::Attribute.new(name, @serializable, value) + end end end - def serializable_methods - Array.wrap(options[:methods]).map do |name| - self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) - end.compact - end - def serialize require 'builder' unless defined? ::Builder @@ -101,6 +85,7 @@ module ActiveModel @builder.tag!(*args) do add_attributes_and_methods + add_includes add_extra_behavior add_procs yield @builder if block_given? @@ -113,13 +98,52 @@ module ActiveModel end def add_attributes_and_methods - (serializable_attributes + serializable_methods).each do |attribute| + serializable_collection.each do |attribute| key = ActiveSupport::XmlMini.rename_key(attribute.name, options) ActiveSupport::XmlMini.to_tag(key, attribute.value, options.merge(attribute.decorations)) end end + def add_includes + @serializable.send(:serializable_add_includes, options) do |association, records, opts| + add_associations(association, records, opts) + end + end + + # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. + def add_associations(association, records, opts) + merged_options = opts.merge(options.slice(:builder, :indent)) + merged_options[:skip_instruct] = true + + if records.is_a?(Enumerable) + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) + type = options[:skip_types] ? { } : {:type => "array"} + association_name = association.to_s.singularize + merged_options[:root] = association_name + + if records.empty? + @builder.tag!(tag, type) + else + @builder.tag!(tag, type) do + records.each do |record| + if options[:skip_types] + record_type = {} + else + record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name + record_type = {:type => record_class} + end + + record.to_xml merged_options.merge(record_type) + end + end + end + else + merged_options[:root] = association.to_s + records.to_xml(merged_options) + end + end + def add_procs if procs = options.delete(:procs) Array.wrap(procs).each do |proc| @@ -139,8 +163,8 @@ module ActiveModel # Without any +options+, the returned XML string will include all the model's # attributes. For example: # - # konata = User.find(1) - # konata.to_xml + # user = User.find(1) + # user.to_xml # # <?xml version="1.0" encoding="UTF-8"?> # <user> diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 5e567307f3..8ed392abca 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -81,7 +81,7 @@ module ActiveModel # 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>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_each(*attr_names, &block) options = attr_names.extract_options!.symbolize_keys diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 4f390613aa..e628c6f306 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -49,7 +49,7 @@ module ActiveModel # before validation. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>, - # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # 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 @@ -58,6 +58,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 def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index e6d10cfff8..6573a7d264 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -50,7 +50,7 @@ module ActiveModel # 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 + # 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 @@ -58,6 +58,8 @@ module ActiveModel # <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 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 a85c23f725..644cc814a7 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/range.rb' +require 'active_support/core_ext/range' module ActiveModel @@ -54,11 +54,13 @@ module ActiveModel # 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 + # 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 + # 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 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 6f23d492eb..d3faa8c6a6 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -79,11 +79,13 @@ module ActiveModel # 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 + # 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 + # 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 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 d32aebeb88..147e2ecb69 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/range.rb' +require 'active_support/core_ext/range' module ActiveModel @@ -54,11 +54,13 @@ module ActiveModel # 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 + # 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 + # 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 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 72735cfb89..eb7aac709d 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -16,7 +16,7 @@ module ActiveModel options[:maximum] -= 1 if range.exclude_end? end - super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER)) + super end def check_validity! @@ -36,7 +36,7 @@ module ActiveModel end def validate_each(record, attribute, value) - value = options[:tokenizer].call(value) if value.kind_of?(String) + value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String) CHECKS.each do |key, validity_check| next unless check_value = options[key] @@ -62,14 +62,14 @@ module ActiveModel # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # # class Person < ActiveRecord::Base - # validates_length_of :first_name, :maximum=>30 - # validates_length_of :last_name, :maximum=>30, :message=>"less than 30 if you don't mind" + # validates_length_of :first_name, :maximum => 30 + # validates_length_of :last_name, :maximum => 30, :message => "less than 30 if you don't mind" # validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :phone, :in => 7..32, :allow_blank => true # 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: @@ -83,19 +83,21 @@ module ActiveModel # * <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>: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 + # 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 + # 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 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 ae576462e6..34d447a0fa 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -9,10 +9,6 @@ module ActiveModel RESERVED_OPTIONS = CHECKS.keys + [:only_integer] - def initialize(options) - super(options.reverse_merge(:only_integer => false, :allow_nil => false)) - end - def check_validity! keys = CHECKS.keys - [:odd, :even] options.slice(*keys).each do |option, value| @@ -106,11 +102,13 @@ module ActiveModel # * <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 + # 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 + # 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 # # The following checks can also be supplied with a proc or a symbol which corresponds to a method: # * <tt>:greater_than</tt> diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index cfb4c33dcc..35af7152db 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -35,6 +35,8 @@ module ActiveModel # * <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 # def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 7ff42de00b..b85c2453fb 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -70,8 +70,8 @@ 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+ and +:allow_nil+ can be given - # to one specific validator, as a hash: + # 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 # @@ -101,12 +101,24 @@ 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. + def validates!(*attributes) + options = attributes.extract_options! + options[:strict] = true + validates(*(attributes << options)) + end + protected # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. def _validates_default_keys - [ :if, :unless, :on, :allow_blank, :allow_nil ] + [ :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 65ae18a769..83aae206a6 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -61,7 +61,9 @@ module ActiveModel # (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 + # If you pass any additional configuration options, they will be passed # to the class and available as <tt>options</tt>: # @@ -101,7 +103,7 @@ module ActiveModel # class Person # include ActiveModel::Validations # - # validates :instance_validations + # validate :instance_validations # # def instance_validations # validates_with MyValidator @@ -116,7 +118,7 @@ module ActiveModel # class Person # include ActiveModel::Validations # - # validates :instance_validations, :on => :create + # validate :instance_validations, :on => :create # # def instance_validations # validates_with MyValidator, MyOtherValidator @@ -140,4 +142,4 @@ module ActiveModel end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index 68c138da84..dbda55ca7c 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,9 +1,9 @@ module ActiveModel module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "beta1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 022c6716bd..67471ed497 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -3,8 +3,6 @@ require 'cases/helper' class ModelWithAttributes include ActiveModel::AttributeMethods - attribute_method_suffix '' - class << self define_method(:bar) do 'original bar' @@ -24,14 +22,31 @@ end class ModelWithAttributes2 include ActiveModel::AttributeMethods + attr_accessor :attributes + attribute_method_suffix '_test' + +private + def attribute(name) + attributes[name.to_s] + end + + alias attribute_test attribute + + def private_method + "<3 <3" + end + +protected + + def protected_method + "O_o O_o" + end end class ModelWithAttributesWithSpaces include ActiveModel::AttributeMethods - attribute_method_suffix '' - def attributes { :'foo bar' => 'value of foo bar'} end @@ -45,8 +60,6 @@ end class ModelWithWeirdNamesAttributes include ActiveModel::AttributeMethods - attribute_method_suffix '' - class << self define_method(:'c?d') do 'original c?d' @@ -76,9 +89,31 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of foo", ModelWithAttributes.new.foo end + test '#define_attribute_method does not generate attribute method if already defined in attribute module' do + klass = Class.new(ModelWithAttributes) + klass.generated_attribute_methods.module_eval do + def foo + '<3' + end + end + klass.define_attribute_method(:foo) + + assert_equal '<3', klass.new.foo + end + + test '#define_attribute_method generates a method that is already defined on the host' do + klass = Class.new(ModelWithAttributes) do + def foo + super + end + end + klass.define_attribute_method(:foo) + + assert_equal 'value of foo', klass.new.foo + end + test '#define_attribute_method generates attribute method with invalid identifier characters' do ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') - ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b' assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b') @@ -130,4 +165,64 @@ class AttributeMethodsTest < ActiveModel::TestCase assert !ModelWithAttributes.new.respond_to?(:foo) assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end + + test 'acessing a suffixed attribute' do + m = ModelWithAttributes2.new + m.attributes = { 'foo' => 'bar' } + + assert_equal 'bar', m.foo + assert_equal 'bar', m.foo_test + end + + test 'explicitly specifying an empty prefix/suffix is deprecated' do + klass = Class.new(ModelWithAttributes) + + assert_deprecated { klass.attribute_method_suffix '' } + assert_deprecated { klass.attribute_method_prefix '' } + + klass.define_attribute_methods([:foo]) + + assert_equal 'value of foo', klass.new.foo + end + + test 'should not interfere with method_missing if the attr has a private/protected method' do + m = ModelWithAttributes2.new + m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' } + + # dispatches to the *method*, not the attribute + assert_equal '<3 <3', m.send(:private_method) + assert_equal 'O_o O_o', m.send(:protected_method) + + # sees that a method is already defined, so doesn't intervene + assert_raises(NoMethodError) { m.private_method } + assert_raises(NoMethodError) { m.protected_method } + 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' } + + assert !m.respond_to?(:private_method) + assert m.respond_to?(:private_method, true) + + # 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 m.respond_to?(:protected_method, true) + end + + test 'should use attribute_missing to dispatch a missing attribute' do + m = ModelWithAttributes2.new + m.attributes = { 'foo' => 'bar' } + + def m.attribute_missing(match, *args, &block) + match + end + + match = m.foo_test + + assert_equal 'foo', match.attr_name + assert_equal 'attribute_test', match.target + assert_equal 'foo_test', match.method_name + end end diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index 7669bf5f65..24552bcaf2 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/contact' +require 'models/helicopter' class ConversionTest < ActiveModel::TestCase test "to_model default implementation returns self" do @@ -22,4 +23,10 @@ class ConversionTest < ActiveModel::TestCase test "to_param default implementation returns a string of ids for persisted records" do assert_equal "1", Contact.new(:id => 1).to_param end -end
\ No newline at end of file + + test "to_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" + end +end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 858ae9cb69..98244a6290 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -106,4 +106,13 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end + test "changing the same attribute multiple times retains the correct original value" do + @model.name = "Otto" + @model.save + @model.name = "DudeFella ManGuy" + @model.name = "Mr. Manfredgensonton" + assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change + assert_equal @model.name_was, "Otto" + end + end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index a24cac40ad..4c76bb43a8 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -33,11 +33,18 @@ class ErrorsTest < ActiveModel::TestCase assert errors.include?(:foo), 'errors should include :foo' end + def test_has_key? + errors = ActiveModel::Errors.new(self) + errors[:foo] = 'omg' + assert errors.has_key?(:foo), 'errors should have key :foo' + end + test "should return true if no errors" do person = Person.new person.errors[:foo] assert person.errors.empty? assert person.errors.blank? + assert !person.errors.include?(:foo) end test "method validate! should work" do @@ -45,7 +52,6 @@ class ErrorsTest < ActiveModel::TestCase person.validate! assert_equal ["name can not be nil"], person.errors.full_messages assert_equal ["can not be nil"], person.errors[:name] - end test 'should be able to assign error' do @@ -71,7 +77,6 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "can not be blank") person.errors.add(:name, "can not be nil") 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 @@ -79,4 +84,33 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "can not be blank") assert_instance_of ActiveSupport::OrderedHash, person.errors.to_hash end + + test 'full_messages should return an array of error messages, with the attribute name included' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a + end + + test 'full_message should return the given message if attribute equals :base' do + person = Person.new + assert_equal "press the button", person.errors.full_message(:base, "press the button") + end + + test 'full_message should return the given message with the attribute name included' do + person = Person.new + assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank") + end + + test 'should return a JSON hash representation of the errors' 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 + assert_equal ["can not be blank", "can not be nil"], hash[:name] + assert_equal ["is invalid"], hash[:email] + end + end + diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 01f0158678..2e860272a4 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -10,5 +10,4 @@ require 'active_support/core_ext/string/access' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true -require 'rubygems' require 'test/unit' diff --git a/activemodel/test/cases/mass_assignment_security/black_list_test.rb b/activemodel/test/cases/mass_assignment_security/black_list_test.rb index ed168bc016..0ec7f8719c 100644 --- a/activemodel/test/cases/mass_assignment_security/black_list_test.rb +++ b/activemodel/test/cases/mass_assignment_security/black_list_test.rb @@ -16,13 +16,5 @@ class BlackListTest < ActiveModel::TestCase assert_equal false, @black_list.deny?('first_name') end - test "sanitize attributes" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' } - attributes = @black_list.sanitize(original_attributes) - - assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" - assert !attributes.key?('admin'), "Denied key should be rejected" - assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected" - end end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 9a73a5ad91..676937b5e1 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -3,36 +3,49 @@ require 'logger' require 'active_support/core_ext/object/inclusion' class SanitizerTest < ActiveModel::TestCase + attr_accessor :logger - class SanitizingAuthorizer - include ActiveModel::MassAssignmentSecurity::Sanitizer - - attr_accessor :logger - + class Authorizer < ActiveModel::MassAssignmentSecurity::PermissionSet def deny?(key) - key.in?(['admin']) + ['admin', 'id'].include?(key) end - end def setup - @sanitizer = SanitizingAuthorizer.new + @logger_sanitizer = ActiveModel::MassAssignmentSecurity::LoggerSanitizer.new(self) + @strict_sanitizer = ActiveModel::MassAssignmentSecurity::StrictSanitizer.new(self) + @authorizer = Authorizer.new end test "sanitize attributes" do original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } - attributes = @sanitizer.sanitize(original_attributes) + attributes = @logger_sanitizer.sanitize(original_attributes, @authorizer) assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" assert !attributes.key?('admin'), "Denied key should be rejected" end - test "debug mass assignment removal" do + test "debug mass assignment removal with LoggerSanitizer" do original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } log = StringIO.new - @sanitizer.logger = Logger.new(log) - @sanitizer.sanitize(original_attributes) + self.logger = Logger.new(log) + @logger_sanitizer.sanitize(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) + end + end + + test "mass assignment insensitive attributes" do + original_attributes = {'id' => 1, 'first_name' => 'allowed'} + + assert_nothing_raised do + @strict_sanitizer.sanitize(original_attributes, @authorizer) + end + end + end diff --git a/activemodel/test/cases/mass_assignment_security/white_list_test.rb b/activemodel/test/cases/mass_assignment_security/white_list_test.rb index aa3596ad2a..737b55492a 100644 --- a/activemodel/test/cases/mass_assignment_security/white_list_test.rb +++ b/activemodel/test/cases/mass_assignment_security/white_list_test.rb @@ -16,13 +16,4 @@ class WhiteListTest < ActiveModel::TestCase assert_equal true, @white_list.deny?('admin') end - test "sanitize attributes" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' } - attributes = @white_list.sanitize(original_attributes) - - assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" - assert !attributes.key?('admin'), "Denied key should be rejected" - assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected" - end - end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index 43a12eed61..be07e59a2f 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -1,6 +1,15 @@ require "cases/helper" require 'models/mass_assignment_specific' + +class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer + + def process_removed_attributes(attrs) + raise StandardError + end + +end + class MassAssignmentSecurityTest < ActiveModel::TestCase def test_attribute_protection @@ -34,6 +43,20 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal expected, sanitized end + def test_attributes_accessible_with_roles_given_as_array + user = Account.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) + assert_equal expected, sanitized + end + + def test_attributes_accessible_with_admin_role_when_roles_given_as_array + user = Account.new + expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } + sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) + assert_equal expected, sanitized + end + def test_attributes_protected_by_default firm = Firm.new expected = { } @@ -76,4 +99,15 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal sanitized, { } end + def test_custom_sanitizer + user = User.new + User.mass_assignment_sanitizer = CustomSanitizer.new + assert_raise StandardError do + user.sanitize_for_mass_assignment("admin" => true) + end + ensure + User.mass_assignment_sanitizer = nil + + end + end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index a7dde2c433..5f943729dd 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -26,7 +26,9 @@ class NamingTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'post/track_backs/track_back', @model_name.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 @@ -56,7 +58,9 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'blog/posts/post', @model_name.partial_path + assert_deprecated(/#partial_path.*#to_partial_path/) do + assert_equal 'blog/posts/post', @model_name.partial_path + end end def test_human @@ -98,7 +102,9 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'blog/posts/post', @model_name.partial_path + assert_deprecated(/#partial_path.*#to_partial_path/) do + assert_equal 'blog/posts/post', @model_name.partial_path + end end def test_human @@ -114,6 +120,46 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase end end +class NamingWithSuppliedModelNameTest < ActiveModel::TestCase + def setup + @model_name = ActiveModel::Name.new(Blog::Post, nil, 'Article') + end + + def test_singular + assert_equal 'article', @model_name.singular + end + + def test_plural + assert_equal 'articles', @model_name.plural + end + + def test_element + assert_equal 'article', @model_name.element + end + + def test_collection + 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 + + def test_route_key + assert_equal 'articles', @model_name.route_key + end + + def test_param_key + assert_equal 'article', @model_name.param_key + end +end + class NamingHelpersTest < Test::Unit::TestCase def setup @klass = Contact @@ -171,4 +217,3 @@ class NamingHelpersTest < Test::Unit::TestCase ActiveModel::Naming.send(method, *args) end end - diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb index 99b1f407ae..f6ec24ae57 100644 --- a/activemodel/test/cases/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -17,6 +17,10 @@ class FooObserver < ActiveModel::Observer def on_spec(record) stub.event_with(record) if stub end + + def around_save(record) + yield :in_around_save + end end class Foo @@ -133,4 +137,12 @@ class ObserverTest < ActiveModel::TestCase foo = Foo.new Foo.send(:notify_observers, :whatever, foo) end + + test "update passes a block on to the observer" do + yielded_value = nil + FooObserver.instance.update(:around_save, Foo.new) do |val| + yielded_value = val + end + assert_equal :in_around_save, yielded_value + end end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 8cc1ccb1e7..1ec915d245 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -1,13 +1,19 @@ require "cases/helper" +require 'active_support/core_ext/object/instance_variables' class SerializationTest < ActiveModel::TestCase class User include ActiveModel::Serialization - attr_accessor :name, :email, :gender + attr_accessor :name, :email, :gender, :address, :friends + + def initialize(name, email, gender) + @name, @email, @gender = name, email, gender + @friends = [] + end def attributes - @attributes ||= {'name' => 'nil', 'email' => 'nil', 'gender' => 'nil'} + instance_values.except("address", "friends") end def foo @@ -15,11 +21,25 @@ class SerializationTest < ActiveModel::TestCase end end + class Address + include ActiveModel::Serialization + + attr_accessor :street, :city, :state, :zip + + def attributes + instance_values + end + end + setup do - @user = User.new - @user.name = 'David' - @user.email = 'david@example.com' - @user.gender = 'male' + @user = User.new('David', 'david@example.com', 'male') + @user.address = Address.new + @user.address.street = "123 Lane" + @user.address.city = "Springfield" + @user.address.state = "CA" + @user.address.zip = 11111 + @user.friends = [User.new('Joe', 'joe@example.com', 'male'), + User.new('Sue', 'sue@example.com', 'female')] end def test_method_serializable_hash_should_work @@ -42,4 +62,82 @@ class SerializationTest < ActiveModel::TestCase 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]) + 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]) + 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]) + end + + def test_should_use_read_attribute_for_serialization + def @user.read_attribute_for_serialization(n) + "Jon" + end + + expected = { "name" => "Jon" } + assert_equal expected, @user.serializable_hash(:only => :name) + 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) + 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) + 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) + 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]) + 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"}}) + 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}}) + 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}}) + 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}}) + end + end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 500a5c575f..a754d610b9 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -8,6 +8,12 @@ class Contact include ActiveModel::Serializers::JSON include ActiveModel::Validations + def attributes=(hash) + hash.each do |k, v| + instance_variable_set("@#{k}", v) + end + end + def attributes instance_values end unless method_defined?(:attributes) @@ -34,7 +40,7 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "should not include root in json" do + test "should not include root in json (class method)" do begin Contact.include_root_in_json = false json = @contact.to_json @@ -50,6 +56,23 @@ class JsonSerializationTest < ActiveModel::TestCase end 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 + end + + test "should not include root in json (option)" do + + 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') @@ -135,6 +158,44 @@ class JsonSerializationTest < ActiveModel::TestCase end end + test "from_json should set the object's attributes" do + 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 + end + + test "from_json should work without a root (method parameter)" do + json = @contact.to_json(:root => false) + result = Contact.new.from_json(json, false) + + 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 "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 + end + test "custom as_json should be honored when generating json" do def @contact.as_json(options); { :name => name, :created_at => created_at }; end json = @contact.to_json @@ -145,4 +206,14 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"preferences":}, json end + test "custom as_json options should be extendible" do + def @contact.as_json(options = {}); super(options.merge(:only => [:name])); end + json = @contact.to_json + + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{"created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json + assert_no_match %r{"awesome":}, json + assert_no_match %r{"preferences":}, json + end + end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 8f5c196850..fc73d9dcd8 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -7,9 +7,11 @@ class Contact extend ActiveModel::Naming include ActiveModel::Serializers::Xml + attr_accessor :address, :friends + def attributes - instance_values - end unless method_defined?(:attributes) + instance_values.except("address", "friends") + end end module Admin @@ -20,6 +22,23 @@ end class Customer < Struct.new(:name) end +class Address + extend ActiveModel::Naming + include ActiveModel::Serializers::Xml + + attr_accessor :street, :city, :state, :zip + + def attributes + instance_values + end +end + +class SerializableContact < Contact + def serializable_hash(options={}) + super(options.merge(:only => [:name, :age])) + end +end + class XmlSerializationTest < ActiveModel::TestCase def setup @contact = Contact.new @@ -30,6 +49,12 @@ class XmlSerializationTest < ActiveModel::TestCase customer = Customer.new customer.name = "John" @contact.preferences = customer + @contact.address = Address.new + @contact.address.street = "123 Lane" + @contact.address.city = "Springfield" + @contact.address.state = "CA" + @contact.address.zip = 11111 + @contact.friends = [Contact.new, Contact.new] end test "should serialize default root" do @@ -77,6 +102,17 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<createdAt}, @xml end + test "should use serialiable hash" do + @contact = SerializableContact.new + @contact.name = 'aaron stack' + @contact.age = 25 + + @xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, @xml + assert_match %r{<age type="integer">25</age>}, @xml + assert_no_match %r{<awesome>}, @xml + end + test "should allow skipped types" do @xml = @contact.to_xml :skip_types => true assert_match %r{<age>25</age>}, @xml @@ -92,7 +128,7 @@ class XmlSerializationTest < ActiveModel::TestCase test "should serialize string" do assert_match %r{<name>aaron stack</name>}, @contact.to_xml end - + test "should serialize nil" do assert_match %r{<pseudonyms nil=\"true\"></pseudonyms>}, @contact.to_xml(:methods => :pseudonyms) end @@ -132,4 +168,39 @@ class XmlSerializationTest < ActiveModel::TestCase xml = @contact.to_xml(:procs => [ proc ]) assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml end + + test "should serialize string correctly when type passed" do + xml = @contact.to_xml :type => 'Contact' + assert_match %r{<contact type="Contact">}, xml + assert_match %r{<name>aaron stack</name>}, xml + end + + test "include option with singular association" do + xml = @contact.to_xml :include => :address, :indent => 0 + assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true)) + end + + test "include option with plural association" do + 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)) + assert_match %r{<friends type="array">}, xml + assert_match %r{<friend type="Contact">}, xml + end + + test "include with options" do + xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => { :address => { :only => :city } } + assert xml.include?(%(><address><city>Springfield</city></address>)) + end + + test "propagates skip_types option to included associations" do + xml = @contact.to_xml :include => :friends, :indent => 0, :skip_types => true + assert_match %r{<friends>}, xml + assert_match %r{<friend>}, xml + end end diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 3cb95b4a00..e06b04af19 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -11,7 +11,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_method_true # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :if => :condition_is_true ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => :condition_is_true ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -20,7 +20,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_method_true # When the method returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :unless => :condition_is_true ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => :condition_is_true ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert t.errors[:title].empty? @@ -28,7 +28,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_method_false # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :if => :condition_is_true_but_its_not ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => :condition_is_true_but_its_not ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert t.errors[:title].empty? @@ -36,7 +36,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_method_false # When the method returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :unless => :condition_is_true_but_its_not ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => :condition_is_true_but_its_not ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -45,7 +45,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :if => "a = 1; a == 1" ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => "a = 1; a == 1" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -54,7 +54,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :unless => "a = 1; a == 1" ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => "a = 1; a == 1" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert t.errors[:title].empty? @@ -62,7 +62,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :if => "false") + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => "false") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert t.errors[:title].empty? @@ -70,7 +70,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", :unless => "false") + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => "false") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -79,7 +79,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_block_true # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => Proc.new { |r| r.content.size > 4 } ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? @@ -89,7 +89,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_block_true # When the block returns true - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => Proc.new { |r| r.content.size > 4 } ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? @@ -98,7 +98,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_block_false # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => Proc.new { |r| r.title != "uhohuhoh"} ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? @@ -107,7 +107,7 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_block_false # When the block returns false - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}", + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => Proc.new { |r| r.title != "uhohuhoh"} ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 73647efea5..2ce714fef0 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -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 => /^Validation\smacros \w+!$/, :allow_blank => true) assert Topic.new("title" => "Shouldn't be valid").invalid? assert Topic.new("title" => "").valid? assert Topic.new("title" => nil).valid? diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 92c473de0e..413da92de4 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -42,7 +42,7 @@ class InclusionValidationTest < ActiveModel::TestCase end def test_validates_inclusion_of_with_allow_nil - Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil=>true ) + Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil => true ) assert Topic.new("title" => "a!", "content" => "abc").invalid? assert Topic.new("title" => "", "content" => "abc").invalid? diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index f02514639b..44048a9c1d 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -11,7 +11,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_allow_nil - Topic.validates_length_of( :title, :is => 5, :allow_nil=>true ) + Topic.validates_length_of( :title, :is => 5, :allow_nil => true ) assert Topic.new("title" => "ab").invalid? assert Topic.new("title" => "").invalid? @@ -20,7 +20,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_allow_blank - Topic.validates_length_of( :title, :is => 5, :allow_blank=>true ) + Topic.validates_length_of( :title, :is => 5, :allow_blank => true ) assert Topic.new("title" => "ab").invalid? assert Topic.new("title" => "").valid? @@ -176,16 +176,16 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_nasty_params - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>-6) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>6) } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within=>"a") } - assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is=>"a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is => -6) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within => 6) } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum => "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum => "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within => "a") } + assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is => "a") } end def test_validates_length_of_custom_errors_for_minimum_with_message - Topic.validates_length_of( :title, :minimum=>5, :message=>"boo %{count}" ) + Topic.validates_length_of( :title, :minimum => 5, :message => "boo %{count}" ) t = Topic.new("title" => "uhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -193,7 +193,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_minimum_with_too_short - Topic.validates_length_of( :title, :minimum=>5, :too_short=>"hoo %{count}" ) + Topic.validates_length_of( :title, :minimum => 5, :too_short => "hoo %{count}" ) t = Topic.new("title" => "uhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -201,7 +201,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_maximum_with_message - Topic.validates_length_of( :title, :maximum=>5, :message=>"boo %{count}" ) + Topic.validates_length_of( :title, :maximum => 5, :message => "boo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -222,7 +222,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_maximum_with_too_long - Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo %{count}" ) + Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -244,7 +244,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_is_with_message - Topic.validates_length_of( :title, :is=>5, :message=>"boo %{count}" ) + Topic.validates_length_of( :title, :is => 5, :message => "boo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -252,7 +252,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_custom_errors_for_is_with_wrong_length - Topic.validates_length_of( :title, :is=>5, :wrong_length=>"hoo %{count}" ) + Topic.validates_length_of( :title, :is => 5, :wrong_length => "hoo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -331,7 +331,7 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_block - Topic.validates_length_of :content, :minimum => 5, :too_short=>"Your essay must be at least %{count} words.", + Topic.validates_length_of :content, :minimum => 5, :too_short => "Your essay must be at least %{count} words.", :tokenizer => lambda {|str| str.scan(/\w+/) } t = Topic.new(:content => "this content should be long enough") assert t.valid? diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 0b50acf913..2f4376bd41 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -297,4 +297,37 @@ class ValidationsTest < ActiveModel::TestCase assert auto.valid? end + + def test_strict_validation_in_validates + Topic.validates :title, :strict => true, :presence => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_strict_validation_not_fails + Topic.validates :title, :strict => true, :presence => true + assert Topic.new(:title => "hello").valid? + end + + def test_strict_validation_particular_validator + Topic.validates :title, :presence => {:strict => true} + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_strict_validation_in_custom_validator_helper + Topic.validates_presence_of :title, :strict => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_validates_with_bang + Topic.validates! :title, :presence => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end end diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb new file mode 100644 index 0000000000..a52b6fb4dd --- /dev/null +++ b/activemodel/test/models/helicopter.rb @@ -0,0 +1,3 @@ +class Helicopter + include ActiveModel::Conversion +end diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb index 53b37369ff..1d123fa58c 100644 --- a/activemodel/test/models/mass_assignment_specific.rb +++ b/activemodel/test/models/mass_assignment_specific.rb @@ -20,6 +20,14 @@ class Person public :sanitize_for_mass_assignment end +class Account + include ActiveModel::MassAssignmentSecurity + attr_accessible :name, :email, :as => [:default, :admin] + attr_accessible :admin, :as => :admin + + public :sanitize_for_mass_assignment +end + class Firm include ActiveModel::MassAssignmentSecurity @@ -65,4 +73,4 @@ end class TightDescendant < TightPerson attr_accessible :phone_number attr_accessible :super_powers, :as => :admin -end
\ No newline at end of file +end |