diff options
Diffstat (limited to 'activerecord')
42 files changed, 847 insertions, 1303 deletions
diff --git a/activerecord/Gemfile b/activerecord/Gemfile deleted file mode 100644 index 9cf37b0af0..0000000000 --- a/activerecord/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -Gem.sources.each { |uri| source uri } -sibling = "#{File.dirname(__FILE__)}/.." - -gem "activesupport", "3.0.pre", :vendored_at => "#{sibling}/activesupport" -gem "activemodel", "3.0.pre", :vendored_at => "#{sibling}/activemodel" -gem "arel", :git => "git://github.com/rails/arel.git", :branch => 'master' - -only :test do - gem "mocha" -end diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 1d8062e042..f7585f789b 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -195,8 +195,6 @@ spec = eval(File.read('activerecord.gemspec')) Rake::GemPackageTask.new(spec) do |p| p.gem_spec = spec - p.need_tar = true - p.need_zip = true end task :lines do diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index bae027bd2a..204cddde47 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', '= 3.0.pre') s.add_dependency('activemodel', '= 3.0.pre') - s.add_dependency('arel', '~> 0.1.0') + s.add_dependency('arel', '~> 0.1.1') s.require_path = 'lib' s.autorequire = 'active_record' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 2d66fa9fcb..8195e78826 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -21,16 +21,11 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -bundled = "#{File.dirname(__FILE__)}/../vendor/gems/environment" -if File.exist?("#{bundled}.rb") - require bundled -else - activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" - $:.unshift(activesupport_path) if File.directory?(activesupport_path) +activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" +$:.unshift(activesupport_path) if File.directory?(activesupport_path) - activemodel_path = "#{File.dirname(__FILE__)}/../../activemodel/lib" - $:.unshift(activemodel_path) if File.directory?(activemodel_path) -end +activemodel_path = "#{File.dirname(__FILE__)}/../../activemodel/lib" +$:.unshift(activemodel_path) if File.directory?(activemodel_path) require 'active_support' require 'active_model' @@ -51,6 +46,7 @@ module ActiveRecord autoload :AssociationPreload, 'active_record/association_preload' autoload :Associations, 'active_record/associations' autoload :AttributeMethods, 'active_record/attribute_methods' + autoload :Attributes, 'active_record/attributes' autoload :AutosaveAssociation, 'active_record/autosave_association' autoload :Relation, 'active_record/relation' autoload :Base, 'active_record/base' @@ -74,7 +70,7 @@ module ActiveRecord autoload :TestCase, 'active_record/test_case' autoload :Timestamp, 'active_record/timestamp' autoload :Transactions, 'active_record/transactions' - autoload :Validator, 'active_record/validator' + autoload :Types, 'active_record/types' autoload :Validations, 'active_record/validations' module AttributeMethods @@ -87,6 +83,20 @@ module ActiveRecord autoload :Write, 'active_record/attribute_methods/write' end + module Attributes + autoload :Aliasing, 'active_record/attributes/aliasing' + autoload :Store, 'active_record/attributes/store' + autoload :Typecasting, 'active_record/attributes/typecasting' + end + + module Type + autoload :Number, 'active_record/types/number' + autoload :Object, 'active_record/types/object' + autoload :Serialize, 'active_record/types/serialize' + autoload :TimeWithZone, 'active_record/types/time_with_zone' + autoload :Unknown, 'active_record/types/unknown' + end + module Locking autoload :Optimistic, 'active_record/locking/optimistic' autoload :Pessimistic, 'active_record/locking/pessimistic' diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index a4e144f233..74921241f7 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -8,25 +8,18 @@ module ActiveRecord end def read_attribute_before_type_cast(attr_name) - @attributes[attr_name] + _attributes.without_typecast[attr_name] end # Returns a hash of attributes before typecasting and deserialization. def attributes_before_type_cast - self.attribute_names.inject({}) do |attrs, name| - attrs[name] = read_attribute_before_type_cast(name) - attrs - end + _attributes.without_typecast end private # Handle *_before_type_cast for method_missing. def attribute_before_type_cast(attribute_name) - if attribute_name == 'id' - read_attribute_before_type_cast(self.class.primary_key) - else - read_attribute_before_type_cast(attribute_name) - end + read_attribute_before_type_cast(attribute_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index a949d80120..0154ee35f8 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -8,23 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - unless value = read_attribute(attr_name) - false - else - column = self.class.columns_hash[attr_name] - if column.nil? - if Numeric === value || value !~ /[^0-9]/ - !value.to_i.zero? - else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) - !value.blank? - end - elsif column.number? - !value.zero? - else - !value.blank? - end - end + _attributes.has?(attr_name) end private @@ -35,3 +19,5 @@ module ActiveRecord end end end + + diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 3da3d9d8cc..97caec7744 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -37,11 +37,7 @@ module ActiveRecord protected def define_method_attribute(attr_name) - if self.serialized_attributes[attr_name] - define_read_method_for_serialized_attribute(attr_name) - else - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) - end + define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) if attr_name == primary_key && attr_name != "id" define_read_method(:id, attr_name, columns_hash[attr_name]) @@ -49,18 +45,12 @@ module ActiveRecord end private - # Define read method for serialized attribute. - def define_read_method_for_serialized_attribute(attr_name) - generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) - end # Define an attribute reader method. Cope with nil column. def define_read_method(symbol, attr_name, column) - cast_code = column.type_cast_code('v') if column - access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" - + access_code = "_attributes['#{attr_name}']" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless _attributes.key?('#{attr_name}'); ") end if cache_attribute?(attr_name) @@ -73,38 +63,7 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' - if !(value = @attributes[attr_name]).nil? - if column = column_for_attribute(attr_name) - if unserializable_attribute?(attr_name, column) - unserialize_attribute(attr_name) - else - column.type_cast(value) - end - else - value - end - else - nil - end - end - - # Returns true if the attribute is of a text column and marked for serialization. - def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes[attr_name] - end - - # Returns the unserialized object of the attribute. - def unserialize_attribute(attr_name) - unserialized_object = object_from_yaml(@attributes[attr_name]) - - if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - else - raise SerializationTypeMismatch, - "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" - end + _attributes[attr_name] end private diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index a8e3e28a7a..4ac0c7f608 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -12,48 +12,20 @@ module ActiveRecord end module ClassMethods + + def cache_attribute?(attr_name) + time_zone_aware?(attr_name) || super + end + protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. - def define_method_attribute(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body = <<-EOV - def #{attr_name}(reload = false) - cached = @attributes_cache['#{attr_name}'] - return cached if cached && !reload - time = read_attribute('#{attr_name}') - @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) - else - super - end - end - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body = <<-EOV - def #{attr_name}=(time) - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, time) - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) - else - super - end + def time_zone_aware?(attr_name) + column = columns_hash[attr_name] + time_zone_aware_attributes && + !skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) && + [:datetime, :timestamp].include?(column.type) end - private - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index e31acac050..37eadbe0a9 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,14 +17,9 @@ module ActiveRecord # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float # columns are turned into +nil+. def write_attribute(attr_name, value) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' + attr_name = _attributes.unalias(attr_name) @attributes_cache.delete(attr_name) - if (column = column_for_attribute(attr_name)) && column.number? - @attributes[attr_name] = convert_number_column_value(value) - else - @attributes[attr_name] = value - end + _attributes[attr_name] = value end private diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb new file mode 100644 index 0000000000..e4d9e89821 --- /dev/null +++ b/activerecord/lib/active_record/attributes.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module Attributes + + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + _attributes.key?(attr_name) + end + + # Returns an array of names for the attributes available on this object sorted alphabetically. + def attribute_names + _attributes.keys.sort! + end + + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + def attributes + attributes = _attributes.dup + attributes.typecast! unless _attributes.frozen? + attributes.to_h + end + + protected + + # Not to be confused with the public #attributes method, which returns a typecasted Hash. + def _attributes + @attributes + end + + def initialize_attribute_store(merge_attributes = nil) + @attributes = ActiveRecord::Attributes::Store.new + @attributes.merge!(merge_attributes) if merge_attributes + @attributes.types.merge!(self.class.attribute_types) + @attributes.aliases.merge!('id' => self.class.primary_key) unless 'id' == self.class.primary_key + @attributes + end + + end +end diff --git a/activerecord/lib/active_record/attributes/aliasing.rb b/activerecord/lib/active_record/attributes/aliasing.rb new file mode 100644 index 0000000000..db77739d1f --- /dev/null +++ b/activerecord/lib/active_record/attributes/aliasing.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module Attributes + module Aliasing + # Allows access to keys using aliased names. + # + # Example: + # class Attributes < Hash + # include Aliasing + # end + # + # attributes = Attributes.new + # attributes.aliases['id'] = 'fancy_primary_key' + # attributes['fancy_primary_key'] = 2020 + # + # attributes['id'] + # => 2020 + # + # Additionally, symbols are always aliases of strings: + # attributes[:fancy_primary_key] + # => 2020 + # + def [](key) + super(unalias(key)) + end + + def []=(key, value) + super(unalias(key), value) + end + + def aliases + @aliases ||= {} + end + + def unalias(key) + key = key.to_s + aliases[key] || key + end + + end + end +end + diff --git a/activerecord/lib/active_record/attributes/store.rb b/activerecord/lib/active_record/attributes/store.rb new file mode 100644 index 0000000000..61109f4acc --- /dev/null +++ b/activerecord/lib/active_record/attributes/store.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Attributes + class Store < Hash + include ActiveRecord::Attributes::Typecasting + include ActiveRecord::Attributes::Aliasing + + # Attributes not mapped to a column are handled using Type::Unknown, + # which enables boolean typecasting for unmapped keys. + def types + @types ||= Hash.new(Type::Unknown.new) + end + + end + end +end diff --git a/activerecord/lib/active_record/attributes/typecasting.rb b/activerecord/lib/active_record/attributes/typecasting.rb new file mode 100644 index 0000000000..56c32f9895 --- /dev/null +++ b/activerecord/lib/active_record/attributes/typecasting.rb @@ -0,0 +1,117 @@ +module ActiveRecord + module Attributes + module Typecasting + # Typecasts values during access based on their key mapping to a Type. + # + # Example: + # class Attributes < Hash + # include Typecasting + # end + # + # attributes = Attributes.new + # attributes.types['comments_count'] = Type::Integer + # attributes['comments_count'] = '5' + # + # attributes['comments_count'] + # => 5 + # + # To support keys not mapped to a typecaster, add a default to types. + # attributes.types.default = Type::Unknown + # attributes['age'] = '25' + # attributes['age'] + # => '25' + # + # A valid type supports #cast, #precast, #boolean, and #appendable? methods. + # + def [](key) + value = super(key) + typecast_read(key, value) + end + + def []=(key, value) + super(key, typecast_write(key, value)) + end + + def to_h + hash = {} + hash.merge!(self) + hash + end + + def dup # :nodoc: + copy = super + copy.types = types.dup + copy + end + + # Provides a duplicate with typecasting disabled. + # + # Example: + # attributes = Attributes.new + # attributes.types['comments_count'] = Type::Integer + # attributes['comments_count'] = '5' + # + # attributes.without_typecast['comments_count'] + # => '5' + # + def without_typecast + dup.without_typecast! + end + + def without_typecast! + types.clear + self + end + + def typecast! + keys.each { |key| self[key] = self[key] } + self + end + + # Check if key has a value that typecasts to true. + # + # attributes = Attributes.new + # attributes.types['comments_count'] = Type::Integer + # + # attributes['comments_count'] = 0 + # attributes.has?('comments_count') + # => false + # + # attributes['comments_count'] = 1 + # attributes.has?('comments_count') + # => true + # + def has?(key) + value = self[key] + boolean_typecast(key, value) + end + + def types + @types ||= {} + end + + protected + + def types=(other_types) + @types = other_types + end + + def boolean_typecast(key, value) + value ? types[key].boolean(value) : false + end + + def typecast_read(key, value) + type = types[key] + value = type.cast(value) + self[key] = value if type.appendable? && !frozen? + + value + end + + def typecast_write(key, value) + types[key].precast(value) + end + + end + end +end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 283aa7ddfc..4e6090458a 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,5 +1,7 @@ +require 'benchmark' require 'yaml' require 'set' +require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/time' require 'active_support/core_ext/class/attribute_accessors' @@ -1384,7 +1386,8 @@ module ActiveRecord #:nodoc: subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end - def self_and_descendants_from_active_record#nodoc: + # Set the lookup ancestors for ActiveModel. + def lookup_ancestors #:nodoc: klass = self classes = [klass] while klass != klass.base_class @@ -1398,32 +1401,9 @@ module ActiveRecord #:nodoc: [self] end - # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example: - # Person.human_attribute_name("first_name") # => "First name" - # This used to be deprecated in favor of humanize, but is now preferred, because it automatically uses the I18n - # module now. - # Specify +options+ with additional translating options. - def human_attribute_name(attribute_key_name, options = {}) - defaults = self_and_descendants_from_active_record.map do |klass| - :"#{klass.name.underscore}.#{attribute_key_name}" - end - defaults << options[:default] if options[:default] - defaults.flatten! - defaults << attribute_key_name.to_s.humanize - options[:count] ||= 1 - I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes])) - end - - # Transform the modelname into a more humane format, using I18n. - # By default, it will underscore then humanize the class name (BlogPost.human_name #=> "Blog post"). - # Default scope of the translation is activerecord.models - # Specify +options+ with additional translating options. - def human_name(options = {}) - defaults = self_and_descendants_from_active_record.map do |klass| - :"#{klass.name.underscore}" - end - defaults << self.name.underscore.humanize - I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options)) + # Set the i18n scope to overwrite ActiveModel. + def i18n_scope #:nodoc: + :activerecord end # True if this isn't a concrete subclass needing a STI type condition. @@ -1463,38 +1443,6 @@ module ActiveRecord #:nodoc: connection.quote(object) end - # Log and benchmark multiple statements in a single block. Example: - # - # Project.benchmark("Creating project") do - # project = Project.create("name" => "stuff") - # project.create_manager("name" => "David") - # project.milestones << Milestone.find(:all) - # end - # - # The benchmark is only recorded if the current level of the logger is less than or equal to the <tt>log_level</tt>, - # which makes it easy to include benchmarking statements in production software that will remain inexpensive because - # the benchmark will only be conducted if the log level is low enough. - # - # The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false. - def benchmark(title, log_level = Logger::DEBUG, use_silence = true) - if logger && logger.level <= log_level - result = nil - ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } - logger.add(log_level, '%s (%.1fms)' % [title, ms]) - result - else - yield - end - end - - # Silences the logger for the duration of the block. - def silence - old_logger_level, logger.level = logger.level, Logger::ERROR if logger - yield - ensure - logger.level = old_logger_level if logger - end - # Overwrite the default class equality method to provide support for association proxies. def ===(object) object.is_a?(self) @@ -1674,7 +1622,7 @@ module ActiveRecord #:nodoc: def instantiate(record) object = find_sti_class(record[inheritance_column]).allocate - object.instance_variable_set(:'@attributes', record) + object.send(:initialize_attribute_store, record) object.instance_variable_set(:'@attributes_cache', {}) object.send(:_run_find_callbacks) @@ -2445,7 +2393,7 @@ module ActiveRecord #:nodoc: # In both instances, valid attribute keys are determined by the column names of the associated table -- # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) - @attributes = attributes_from_column_definition + initialize_attribute_store(attributes_from_column_definition) @attributes_cache = {} @new_record = true ensure_proper_type @@ -2471,7 +2419,7 @@ module ActiveRecord #:nodoc: callback(:after_initialize) if respond_to_without_attributes?(:after_initialize) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) cloned_attributes.delete(self.class.primary_key) - @attributes = cloned_attributes + initialize_attribute_store(cloned_attributes) clear_aggregation_cache @attributes_cache = {} @new_record = true @@ -2697,7 +2645,7 @@ module ActiveRecord #:nodoc: def reload(options = nil) clear_aggregation_cache clear_association_cache - @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) + _attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) @attributes_cache = {} self end @@ -2794,16 +2742,6 @@ module ActiveRecord #:nodoc: !value.blank? end - # Returns true if the given attribute is in the attributes hash - def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) - end - - # Returns an array of names for the attributes available on this object sorted alphabetically. - def attribute_names - @attributes.keys.sort - end - # Returns the column object for the named attribute. def column_for_attribute(name) self.class.columns_hash[name.to_s] @@ -2927,18 +2865,6 @@ module ActiveRecord #:nodoc: end end - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end - def remove_attributes_protected_from_mass_assignment(attributes) safe_attributes = if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil? @@ -3057,7 +2983,7 @@ module ActiveRecord #:nodoc: end def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) + if self.class.send(:time_zone_aware?, name) Time.zone.local(*values) else Time.time_with_datetime_fallback(@@default_timezone, *values) @@ -3144,15 +3070,13 @@ module ActiveRecord #:nodoc: comma_pair_list(quote_columns(quoter, hash)) end - def object_from_yaml(string) - return string unless string.is_a?(String) && string =~ /^---/ - YAML::load(string) rescue string - end end Base.class_eval do extend ActiveModel::Naming extend QueryCache::ClassMethods + extend ActiveSupport::Benchmarkable + include Validations include Locking::Optimistic, Locking::Pessimistic include AttributeMethods @@ -3160,6 +3084,7 @@ module ActiveRecord #:nodoc: include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty + include Attributes, Types include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope include ActiveModel::Conversion @@ -3169,6 +3094,7 @@ module ActiveRecord #:nodoc: include AutosaveAssociation, NestedAttributes include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization + end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 12253eac3f..377f2a44c5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -211,9 +211,10 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) @connection_mutex.synchronize do - conn.run_callbacks :checkin - @checked_out.delete conn - @queue.signal + conn.run_callbacks :checkin do + @checked_out.delete conn + @queue.signal + end end end @@ -255,9 +256,10 @@ module ActiveRecord end def checkout_and_verify(c) - c.verify! - c.run_callbacks :checkout - @checked_out << c + c.run_callbacks :checkout do + c.verify! + @checked_out << c + end c end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 694e1e561c..8fae26b790 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,4 +1,3 @@ -require 'benchmark' require 'date' require 'bigdecimal' require 'bigdecimal/util' @@ -12,8 +11,6 @@ require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' -require 'active_support/core_ext/benchmark' - module ActiveRecord module ConnectionAdapters # :nodoc: # ActiveRecord supports multiple database systems. AbstractAdapter and @@ -33,6 +30,7 @@ module ActiveRecord include Quoting, DatabaseStatements, SchemaStatements include QueryCache include ActiveSupport::Callbacks + define_callbacks :checkout, :checkin @@row_even = true @@ -193,6 +191,7 @@ module ActiveRecord end def log_info(sql, name, ms) + @runtime += ms if @logger && @logger.debug? name = '%s (%.1fms)' % [name || 'SQL', ms] @logger.debug(format_log_entry(name, sql.squeeze(' '))) @@ -200,13 +199,8 @@ module ActiveRecord end protected - def log(sql, name) - event = ActiveSupport::Orchestra.instrument(:sql, :sql => sql, :name => name) do - yield if block_given? - end - @runtime += event.duration - log_info(sql, name, event.duration) - event.result + def log(sql, name, &block) + ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name, &block) rescue Exception => e # Log message and raise exception. # Set last_verification to 0, so that connection gets verified diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 5a49fc2d2f..c9c2892ba4 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -11,11 +11,11 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to RAILS_ROOT, but only if + # Allow database path relative to Rails.root, but only if # the database path is not the special path that tells # Sqlite to build a database only in memory. - if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database] - config[:database] = File.expand_path(config[:database], RAILS_ROOT) + if Object.const_defined?(:Rails) && ':memory:' != config[:database] + config[:database] = File.expand_path(config[:database], Rails.root) end end end diff --git a/activerecord/lib/active_record/notifications.rb b/activerecord/lib/active_record/notifications.rb new file mode 100644 index 0000000000..a5ce7ac524 --- /dev/null +++ b/activerecord/lib/active_record/notifications.rb @@ -0,0 +1,5 @@ +require 'active_support/notifications' + +ActiveSupport::Notifications.subscribe("sql") do |event| + ActiveRecord::Base.connection.log_info(event.payload[:sql], event.payload[:name], event.duration) +end diff --git a/activerecord/lib/active_record/types.rb b/activerecord/lib/active_record/types.rb new file mode 100644 index 0000000000..74f569352b --- /dev/null +++ b/activerecord/lib/active_record/types.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module Types + extend ActiveSupport::Concern + + module ClassMethods + + def attribute_types + attribute_types = {} + columns.each do |column| + options = {} + options[:time_zone_aware] = time_zone_aware?(column.name) + options[:serialize] = serialized_attributes[column.name] + + attribute_types[column.name] = to_type(column, options) + end + attribute_types + end + + private + + def to_type(column, options = {}) + type_class = if options[:time_zone_aware] + Type::TimeWithZone + elsif options[:serialize] + Type::Serialize + elsif [ :integer, :float, :decimal ].include?(column.type) + Type::Number + else + Type::Object + end + + type_class.new(column, options) + end + + end + + end +end diff --git a/activerecord/lib/active_record/types/number.rb b/activerecord/lib/active_record/types/number.rb new file mode 100644 index 0000000000..cfbe877575 --- /dev/null +++ b/activerecord/lib/active_record/types/number.rb @@ -0,0 +1,30 @@ +module ActiveRecord + module Type + class Number < Object + + def boolean(value) + value = cast(value) + !(value.nil? || value.zero?) + end + + def precast(value) + convert_number_column_value(value) + end + + private + + def convert_number_column_value(value) + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/object.rb b/activerecord/lib/active_record/types/object.rb new file mode 100644 index 0000000000..ec3f861abd --- /dev/null +++ b/activerecord/lib/active_record/types/object.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module Type + module Casting + + def cast(value) + typecaster.type_cast(value) + end + + def precast(value) + value + end + + def boolean(value) + cast(value).present? + end + + # Attributes::Typecasting stores appendable? types (e.g. serialized Arrays) when typecasting reads. + def appendable? + false + end + + end + + class Object + include Casting + + attr_reader :name, :options + attr_reader :typecaster + + def initialize(typecaster = nil, options = {}) + @typecaster, @options = typecaster, options + end + + end + + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/serialize.rb b/activerecord/lib/active_record/types/serialize.rb new file mode 100644 index 0000000000..7b6af1981f --- /dev/null +++ b/activerecord/lib/active_record/types/serialize.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + class Serialize < Object + + def cast(value) + unserialize(value) + end + + def appendable? + true + end + + protected + + def unserialize(value) + unserialized_object = object_from_yaml(value) + + if unserialized_object.is_a?(@options[:serialize]) || unserialized_object.nil? + unserialized_object + else + raise SerializationTypeMismatch, + "#{name} was supposed to be a #{@options[:serialize]}, but was a #{unserialized_object.class.to_s}" + end + end + + def object_from_yaml(string) + return string unless string.is_a?(String) && string =~ /^---/ + YAML::load(string) rescue string + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/time_with_zone.rb b/activerecord/lib/active_record/types/time_with_zone.rb new file mode 100644 index 0000000000..3a8b9292f9 --- /dev/null +++ b/activerecord/lib/active_record/types/time_with_zone.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module Type + class TimeWithZone < Object + + def cast(time) + time = super(time) + time.acts_like?(:time) ? time.in_time_zone : time + end + + def precast(time) + unless time.acts_like?(:time) + time = time.is_a?(String) ? ::Time.zone.parse(time) : time.to_time rescue time + end + time = time.in_time_zone rescue nil if time + super(time) + end + + end + end +end diff --git a/activerecord/lib/active_record/types/unknown.rb b/activerecord/lib/active_record/types/unknown.rb new file mode 100644 index 0000000000..f832c7b304 --- /dev/null +++ b/activerecord/lib/active_record/types/unknown.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module Type + # Useful for handling attributes not mapped to types. Performs some boolean typecasting, + # but otherwise leaves the value untouched. + class Unknown + + def cast(value) + value + end + + def precast(value) + value + end + + # Attempts typecasting to handle numeric, false and blank values. + def boolean(value) + empty = (numeric?(value) && value.to_i.zero?) || false?(value) || value.blank? + !empty + end + + def appendable? + false + end + + protected + + def false?(value) + ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + end + + def numeric?(value) + Numeric === value || value !~ /[^0-9]/ + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index e61b253192..0365cb592f 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -17,90 +17,6 @@ module ActiveRecord end end - class Errors < ActiveModel::Errors - class << self - def default_error_messages - message = "Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages')." - ActiveSupport::Deprecation.warn(message) - - I18n.translate 'activerecord.errors.messages' - end - end - - # Returns all the full error messages in an array. - # - # class Company < ActiveRecord::Base - # validates_presence_of :name, :address, :email - # validates_length_of :name, :in => 5..30 - # end - # - # 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"] - def full_messages(options = {}) - full_messages = [] - - each do |attribute, messages| - messages = Array.wrap(messages) - next if messages.empty? - - if attribute == :base - messages.each {|m| full_messages << m } - else - attr_name = @base.class.human_attribute_name(attribute.to_s) - prefix = attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') - messages.each do |m| - full_messages << "#{prefix}#{m}" - end - end - end - - full_messages - end - - # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>). - # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, - # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the - # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name, - # translated attribute name and the value are available for interpolation. - # - # When using inheritance in your models, it will check all the inherited models too, but only if the model itself - # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt> - # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations: - # - # <ol> - # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li> - # <li><tt>activerecord.errors.models.admin.blank</tt></li> - # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li> - # <li><tt>activerecord.errors.models.user.blank</tt></li> - # <li><tt>activerecord.errors.messages.blank</tt></li> - # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li> - # </ol> - def generate_message(attribute, message = :invalid, options = {}) - message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) - - defaults = @base.class.self_and_descendants_from_active_record.map do |klass| - [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", - :"models.#{klass.name.underscore}.#{message}" ] - end - - defaults << options.delete(:default) - defaults = defaults.compact.flatten << :"messages.#{message}" - - key = defaults.shift - value = @base.respond_to?(attribute) ? @base.send(attribute) : nil - - options = { :default => defaults, - :model => @base.class.human_name, - :attribute => @base.class.human_attribute_name(attribute.to_s), - :value => value, - :scope => [:activerecord, :errors] - }.merge(options) - - I18n.translate(key, options) - end - end - module Validations extend ActiveSupport::Concern @@ -165,11 +81,6 @@ module ActiveRecord errors.empty? end - - # Returns the Errors object that holds all information about attribute error messages. - def errors - @errors ||= Errors.new(self) - end end end end diff --git a/activerecord/lib/active_record/validator.rb b/activerecord/lib/active_record/validator.rb deleted file mode 100644 index 83a33f4dcd..0000000000 --- a/activerecord/lib/active_record/validator.rb +++ /dev/null @@ -1,68 +0,0 @@ -module ActiveRecord #:nodoc: - - # A simple base class that can be used along with ActiveRecord::Base.validates_with - # - # class Person < ActiveRecord::Base - # validates_with MyValidator - # end - # - # class MyValidator < ActiveRecord::Validator - # def validate - # if some_complex_logic - # record.errors[:base] = "This record is invalid" - # end - # end - # - # private - # def some_complex_logic - # # ... - # end - # end - # - # Any class that inherits from ActiveRecord::Validator will have access to <tt>record</tt>, - # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>. - # - # class Person < ActiveRecord::Base - # validates_with MyValidator - # end - # - # class MyValidator < ActiveRecord::Validator - # def validate - # record # => The person instance being validated - # options # => Any non-standard options passed to validates_with - # end - # end - # - # To cause a validation error, you must add to the <tt>record<tt>'s errors directly - # from within the validators message - # - # class MyValidator < ActiveRecord::Validator - # def validate - # record.errors[:base] << "This is some custom error message" - # record.errors[:first_name] << "This is some complex validation" - # # etc... - # end - # end - # - # To add behavior to the initialize method, use the following signature: - # - # class MyValidator < ActiveRecord::Validator - # def initialize(record, options) - # super - # @my_custom_field = options[:field_name] || :first_name - # end - # end - # - class Validator - attr_reader :record, :options - - def initialize(record, options) - @record = record - @options = options - end - - def validate - raise "You must override this method" - end - end -end diff --git a/activerecord/test/cases/attributes/aliasing_test.rb b/activerecord/test/cases/attributes/aliasing_test.rb new file mode 100644 index 0000000000..7ee25779f1 --- /dev/null +++ b/activerecord/test/cases/attributes/aliasing_test.rb @@ -0,0 +1,20 @@ +require "cases/helper" + +class AliasingTest < ActiveRecord::TestCase + + class AliasingAttributes < Hash + include ActiveRecord::Attributes::Aliasing + end + + test "attribute access with aliasing" do + attributes = AliasingAttributes.new + attributes[:name] = 'Batman' + attributes.aliases['nickname'] = 'name' + + assert_equal 'Batman', attributes[:name], "Symbols should point to Strings" + assert_equal 'Batman', attributes['name'] + assert_equal 'Batman', attributes['nickname'] + assert_equal 'Batman', attributes[:nickname] + end + +end diff --git a/activerecord/test/cases/attributes/typecasting_test.rb b/activerecord/test/cases/attributes/typecasting_test.rb new file mode 100644 index 0000000000..8a3b551375 --- /dev/null +++ b/activerecord/test/cases/attributes/typecasting_test.rb @@ -0,0 +1,120 @@ +require "cases/helper" + +class TypecastingTest < ActiveRecord::TestCase + + class TypecastingAttributes < Hash + include ActiveRecord::Attributes::Typecasting + end + + module MockType + class Object + + def cast(value) + value + end + + def precast(value) + value + end + + def boolean(value) + !value.blank? + end + + def appendable? + false + end + + end + + class Integer < Object + + def cast(value) + value.to_i + end + + def precast(value) + value ? value : 0 + end + + def boolean(value) + !Float(value).zero? + end + + end + + class Serialize < Object + + def cast(value) + YAML::load(value) rescue value + end + + def precast(value) + value + end + + def appendable? + true + end + + end + end + + def setup + @attributes = TypecastingAttributes.new + @attributes.types.default = MockType::Object.new + @attributes.types['comments_count'] = MockType::Integer.new + end + + test "typecast on read" do + attributes = @attributes.merge('comments_count' => '5') + assert_equal 5, attributes['comments_count'] + end + + test "typecast on write" do + @attributes['comments_count'] = false + + assert_equal 0, @attributes.to_h['comments_count'] + end + + test "serialized objects" do + attributes = @attributes.merge('tags' => [ 'peanut butter' ].to_yaml) + attributes.types['tags'] = MockType::Serialize.new + attributes['tags'] << 'jelly' + + assert_equal [ 'peanut butter', 'jelly' ], attributes['tags'] + end + + test "without typecasting" do + @attributes.merge!('comments_count' => '5') + attributes = @attributes.without_typecast + + assert_equal '5', attributes['comments_count'] + assert_equal 5, @attributes['comments_count'], "Original attributes should typecast" + end + + + test "typecast all attributes" do + attributes = @attributes.merge('title' => 'I love sandwiches', 'comments_count' => '5') + attributes.typecast! + + assert_equal({ 'title' => 'I love sandwiches', 'comments_count' => 5 }, attributes) + end + + test "query for has? value" do + attributes = @attributes.merge('comments_count' => '1') + + assert_equal true, attributes.has?('comments_count') + attributes['comments_count'] = '0' + assert_equal false, attributes.has?('comments_count') + end + + test "attributes to Hash" do + attributes_hash = { 'title' => 'I love sandwiches', 'comments_count' => '5' } + attributes = @attributes.merge(attributes_hash) + + assert_equal Hash, attributes.to_h.class + assert_equal attributes_hash, attributes.to_h + end + +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index bfb2df313b..5c2911eca1 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -2195,9 +2195,9 @@ class BasicsTest < ActiveRecord::TestCase log = StringIO.new ActiveRecord::Base.logger = Logger.new(log) ActiveRecord::Base.logger.level = Logger::WARN - ActiveRecord::Base.benchmark("Debug Topic Count", Logger::DEBUG) { Topic.count } - ActiveRecord::Base.benchmark("Warn Topic Count", Logger::WARN) { Topic.count } - ActiveRecord::Base.benchmark("Error Topic Count", Logger::ERROR) { Topic.count } + ActiveRecord::Base.benchmark("Debug Topic Count", :level => :debug) { Topic.count } + ActiveRecord::Base.benchmark("Warn Topic Count", :level => :warn) { Topic.count } + ActiveRecord::Base.benchmark("Error Topic Count", :level => :error) { Topic.count } assert_no_match /Debug Topic Count/, log.string assert_match /Warn Topic Count/, log.string assert_match /Error Topic Count/, log.string @@ -2209,8 +2209,8 @@ class BasicsTest < ActiveRecord::TestCase original_logger = ActiveRecord::Base.logger log = StringIO.new ActiveRecord::Base.logger = Logger.new(log) - ActiveRecord::Base.benchmark("Logging", Logger::DEBUG, true) { ActiveRecord::Base.logger.debug "Loud" } - ActiveRecord::Base.benchmark("Logging", Logger::DEBUG, false) { ActiveRecord::Base.logger.debug "Quiet" } + ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => true) { ActiveRecord::Base.logger.debug "Loud" } + ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" } assert_no_match /Loud/, log.string assert_match /Quiet/, log.string ensure diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index aa09c7061f..871cfa6468 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,5 +1,10 @@ -$:.unshift(File.dirname(__FILE__) + '/../../lib') -$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib') +root = File.expand_path('../../../..', __FILE__) +begin + require "#{root}/vendor/gems/environment" +rescue LoadError + $:.unshift("#{root}/activesupport/lib") + $:.unshift("#{root}/activerecord/lib") +end require 'config' diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb index d59c53cec8..ae4dcfb81e 100644 --- a/activerecord/test/cases/i18n_test.rb +++ b/activerecord/test/cases/i18n_test.rb @@ -30,17 +30,17 @@ class ActiveRecordI18nTests < Test::Unit::TestCase def test_translated_model_names I18n.backend.store_translations 'en', :activerecord => {:models => {:topic => 'topic model'} } - assert_equal 'topic model', Topic.human_name + assert_equal 'topic model', Topic.model_name.human end def test_translated_model_names_with_sti I18n.backend.store_translations 'en', :activerecord => {:models => {:reply => 'reply model'} } - assert_equal 'reply model', Reply.human_name + assert_equal 'reply model', Reply.model_name.human end def test_translated_model_names_with_sti_fallback I18n.backend.store_translations 'en', :activerecord => {:models => {:topic => 'topic model'} } - assert_equal 'topic model', Reply.human_name + assert_equal 'topic model', Reply.model_name.human end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index bb9013c2a1..f9eea3d118 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -4,14 +4,14 @@ require "timeout" class PooledConnectionsTest < ActiveRecord::TestCase def setup - super + @per_test_teardown = [] @connection = ActiveRecord::Base.remove_connection end def teardown ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.establish_connection(@connection) - super + @per_test_teardown.each {|td| td.call } end def checkout_connections @@ -113,6 +113,23 @@ class PooledConnectionsTest < ActiveRecord::TestCase assert_equal 3, after_count - before_count end + def test_connection_pool_callbacks + checked_out, checked_in = false, false + ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do + set_callback(:checkout, :after) { checked_out = true } + set_callback(:checkin, :before) { checked_in = true } + end + @per_test_teardown << proc do + ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do + reset_callbacks :checkout + reset_callbacks :checkin + end + end + checkout_checkin_connections 1, 1 + assert checked_out + assert checked_in + end + private def add_record(name) diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index f3ed8ccd8d..99e248743a 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -15,8 +15,8 @@ class ReflectionTest < ActiveRecord::TestCase end def test_human_name - assert_equal "Price estimate", PriceEstimate.human_name - assert_equal "Subscriber", Subscriber.human_name + assert_equal "Price estimate", PriceEstimate.model_name.human + assert_equal "Subscriber", Subscriber.model_name.human end def test_column_null_not_null diff --git a/activerecord/test/cases/types/number_test.rb b/activerecord/test/cases/types/number_test.rb new file mode 100644 index 0000000000..ee7216a0f1 --- /dev/null +++ b/activerecord/test/cases/types/number_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class NumberTest < ActiveRecord::TestCase + + def setup + @column = ActiveRecord::ConnectionAdapters::Column.new('comments_count', 0, 'integer') + @number = ActiveRecord::Type::Number.new(@column) + end + + test "typecast" do + assert_equal 1, @number.cast(1) + assert_equal 1, @number.cast('1') + assert_equal 0, @number.cast('') + + assert_equal 0, @number.precast(false) + assert_equal 1, @number.precast(true) + assert_equal nil, @number.precast('') + assert_equal 0, @number.precast(0) + end + + test "cast as boolean" do + assert_equal true, @number.boolean('1') + assert_equal true, @number.boolean(1) + + assert_equal false, @number.boolean(0) + assert_equal false, @number.boolean('0') + assert_equal false, @number.boolean(nil) + end + +end diff --git a/activerecord/test/cases/types/object_test.rb b/activerecord/test/cases/types/object_test.rb new file mode 100644 index 0000000000..f2667a9b00 --- /dev/null +++ b/activerecord/test/cases/types/object_test.rb @@ -0,0 +1,24 @@ +require "cases/helper" + +class ObjectTest < ActiveRecord::TestCase + + def setup + @column = ActiveRecord::ConnectionAdapters::Column.new('name', '', 'date') + @object = ActiveRecord::Type::Object.new(@column) + end + + test "typecast with column" do + date = Date.new(2009, 7, 10) + assert_equal date, @object.cast('10-07-2009') + assert_equal nil, @object.cast('') + + assert_equal date, @object.precast(date) + end + + test "cast as boolean" do + assert_equal false, @object.boolean(nil) + assert_equal false, @object.boolean('false') + assert_equal true, @object.boolean('10-07-2009') + end + +end diff --git a/activerecord/test/cases/types/serialize_test.rb b/activerecord/test/cases/types/serialize_test.rb new file mode 100644 index 0000000000..e9423a5b9d --- /dev/null +++ b/activerecord/test/cases/types/serialize_test.rb @@ -0,0 +1,20 @@ +require "cases/helper" + +class SerializeTest < ActiveRecord::TestCase + + test "typecast" do + serializer = ActiveRecord::Type::Serialize.new(column = nil, :serialize => Array) + + assert_equal [], serializer.cast([].to_yaml) + assert_equal ['1'], serializer.cast(['1'].to_yaml) + assert_equal nil, serializer.cast(nil.to_yaml) + end + + test "cast as boolean" do + serializer = ActiveRecord::Type::Serialize.new(column = nil, :serialize => Array) + + assert_equal true, serializer.boolean(['1'].to_yaml) + assert_equal false, serializer.boolean([].to_yaml) + end + +end
\ No newline at end of file diff --git a/activerecord/test/cases/types/time_with_zone_test.rb b/activerecord/test/cases/types/time_with_zone_test.rb new file mode 100644 index 0000000000..b3de79a6c8 --- /dev/null +++ b/activerecord/test/cases/types/time_with_zone_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" + +class TimeWithZoneTest < ActiveRecord::TestCase + + def setup + @column = ActiveRecord::ConnectionAdapters::Column.new('created_at', 0, 'datetime') + @time_with_zone = ActiveRecord::Type::TimeWithZone.new(@column) + end + + test "typecast" do + Time.use_zone("Pacific Time (US & Canada)") do + time_string = "2009-10-07 21:29:10" + time = Time.zone.parse(time_string) + + # assert_equal time, @time_with_zone.cast(time_string) + assert_equal nil, @time_with_zone.cast('') + assert_equal nil, @time_with_zone.cast(nil) + + assert_equal time, @time_with_zone.precast(time) + assert_equal time, @time_with_zone.precast(time_string) + assert_equal time, @time_with_zone.precast(time.to_time) + # assert_equal "#{time.to_date.to_s} 00:00:00 -0700", @time_with_zone.precast(time.to_date).to_s + end + end + + test "cast as boolean" do + Time.use_zone('Central Time (US & Canada)') do + time = Time.zone.now + + assert_equal true, @time_with_zone.boolean(time) + assert_equal true, @time_with_zone.boolean(time.to_date) + assert_equal true, @time_with_zone.boolean(time.to_time) + + assert_equal true, @time_with_zone.boolean(time.to_s) + assert_equal true, @time_with_zone.boolean(time.to_date.to_s) + assert_equal true, @time_with_zone.boolean(time.to_time.to_s) + + assert_equal false, @time_with_zone.boolean('') + end + end + +end diff --git a/activerecord/test/cases/types/unknown_test.rb b/activerecord/test/cases/types/unknown_test.rb new file mode 100644 index 0000000000..230d67b2fb --- /dev/null +++ b/activerecord/test/cases/types/unknown_test.rb @@ -0,0 +1,29 @@ +require "cases/helper" + +class UnknownTest < ActiveRecord::TestCase + + test "typecast attributes does't modify values" do + unkown = ActiveRecord::Type::Unknown.new + person = { 'name' => '0' } + + assert_equal person['name'], unkown.cast(person['name']) + assert_equal person['name'], unkown.precast(person['name']) + end + + test "cast as boolean" do + person = { 'id' => 0, 'name' => ' ', 'admin' => 'false', 'votes' => '0' } + unkown = ActiveRecord::Type::Unknown.new + + assert_equal false, unkown.boolean(person['votes']) + assert_equal false, unkown.boolean(person['admin']) + assert_equal false, unkown.boolean(person['name']) + assert_equal false, unkown.boolean(person['id']) + + person = { 'id' => 5, 'name' => 'Eric', 'admin' => 'true', 'votes' => '25' } + assert_equal true, unkown.boolean(person['votes']) + assert_equal true, unkown.boolean(person['admin']) + assert_equal true, unkown.boolean(person['name']) + assert_equal true, unkown.boolean(person['id']) + end + +end
\ No newline at end of file diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb new file mode 100644 index 0000000000..403a9a6e02 --- /dev/null +++ b/activerecord/test/cases/types_test.rb @@ -0,0 +1,32 @@ +require "cases/helper" +require 'models/topic' + +class TypesTest < ActiveRecord::TestCase + + test "attribute types from columns" do + begin + ActiveRecord::Base.time_zone_aware_attributes = true + attribute_type_classes = {} + Topic.attribute_types.each { |key, type| attribute_type_classes[key] = type.class } + + expected = { "id" => ActiveRecord::Type::Number, + "replies_count" => ActiveRecord::Type::Number, + "parent_id" => ActiveRecord::Type::Number, + "content" => ActiveRecord::Type::Serialize, + "written_on" => ActiveRecord::Type::TimeWithZone, + "title" => ActiveRecord::Type::Object, + "author_name" => ActiveRecord::Type::Object, + "approved" => ActiveRecord::Type::Object, + "parent_title" => ActiveRecord::Type::Object, + "bonus_time" => ActiveRecord::Type::Object, + "type" => ActiveRecord::Type::Object, + "last_read" => ActiveRecord::Type::Object, + "author_email_address" => ActiveRecord::Type::Object } + + assert_equal expected, attribute_type_classes + ensure + ActiveRecord::Base.time_zone_aware_attributes = false + end + end + +end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 3794a0ebb9..3f96d7973b 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -17,26 +17,7 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase } end - # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) - def test_generate_message_inclusion_with_default_message - assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') - end - - def test_generate_message_inclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) - def test_generate_message_exclusion_with_default_message - assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') - end - - def test_generate_message_exclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') - end - # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) def test_generate_message_invalid_with_default_message assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') end @@ -45,107 +26,6 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') end - # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) - def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil) - end - - def test_generate_message_confirmation_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message') - end - - # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) - def test_generate_message_accepted_with_default_message - assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil) - end - - def test_generate_message_accepted_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message') - end - - # add_on_empty: generate_message(attr, :empty, :default => custom_message) - def test_generate_message_empty_with_default_message - assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil) - end - - def test_generate_message_empty_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message') - end - - # add_on_blank: generate_message(attr, :blank, :default => custom_message) - def test_generate_message_blank_with_default_message - assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil) - end - - def test_generate_message_blank_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message') - end - - # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) - def test_generate_message_too_long_with_default_message - assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10) - end - - def test_generate_message_too_long_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) - def test_generate_message_too_short_with_default_message - assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10) - end - - def test_generate_message_too_short_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) - def test_generate_message_wrong_length_with_default_message - assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) - end - - def test_generate_message_wrong_length_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) - end - - # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - def test_generate_message_not_a_number_with_default_message - assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') - end - - def test_generate_message_not_a_number_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) - def test_generate_message_greater_than_with_default_message - assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_greater_than_or_equal_to_with_default_message - assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_equal_to_with_default_message - assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_with_default_message - assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_or_equal_to_with_default_message - assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_odd_with_default_message - assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_even_with_default_message - assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) - end - # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) def test_generate_message_taken_with_default_message assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') @@ -155,4 +35,13 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title') end + # ActiveRecord#RecordInvalid exception + + test "RecordInvalid exception can be localized" do + topic = Topic.new + topic.errors.add(:title, :invalid) + topic.errors.add(:title, :blank) + assert_equal "Validation failed: Title is invalid, Title can't be blank", ActiveRecord::RecordInvalid.new(topic).message + end + end diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 252138c0d6..532de67d99 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -30,20 +30,6 @@ class I18nValidationTest < ActiveRecord::TestCase end end - def test_percent_s_interpolation_syntax_in_error_messages_was_deprecated - assert_not_deprecated do - default = "%s interpolation syntax was deprecated" - assert_equal default, I18n.t(:does_not_exist, :default => default, :value => 'this') - end - end - - def test_percent_d_interpolation_syntax_in_error_messages_was_deprecated - assert_not_deprecated do - default = "%d interpolation syntaxes are deprecated" - assert_equal default, I18n.t(:does_not_exist, :default => default, :count => 2) - end - end - # ActiveRecord::Errors def test_errors_generate_message_translates_custom_model_attribute_key I18n.expects(:translate).with( @@ -162,722 +148,4 @@ class I18nValidationTest < ActiveRecord::TestCase assert_equal ['global message'], replied_topic.errors[:replies] end - def test_errors_add_on_empty_generates_message - @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil}) - @topic.errors.add_on_empty :title - end - - def test_errors_add_on_empty_generates_message_with_custom_default_message - @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'}) - @topic.errors.add_on_empty :title, 'custom' - end - - def test_errors_add_on_blank_generates_message - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) - @topic.errors.add_on_blank :title - end - - def test_errors_add_on_blank_generates_message_with_custom_default_message - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) - @topic.errors.add_on_blank :title, 'custom' - end - - def test_errors_full_messages_translates_human_attribute_name_for_model_attributes - @topic.errors.add('title', 'empty') - I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title') - @topic.errors.full_messages :locale => 'en' - end - - # ActiveRecord::Validations - # validates_confirmation_of w/ mocha - def test_validates_confirmation_of_generates_message - Topic.validates_confirmation_of :title - @topic.title_confirmation = 'foo' - @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil}) - @topic.valid? - end - - def test_validates_confirmation_of_generates_message_with_custom_default_message - Topic.validates_confirmation_of :title, :message => 'custom' - @topic.title_confirmation = 'foo' - @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'}) - @topic.valid? - end - - # validates_acceptance_of w/ mocha - - def test_validates_acceptance_of_generates_message - Topic.validates_acceptance_of :title, :allow_nil => false - @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil}) - @topic.valid? - end - - def test_validates_acceptance_of_generates_message_with_custom_default_message - Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false - @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'}) - @topic.valid? - end - - # validates_presence_of w/ mocha - - def test_validates_presence_of_generates_message - Topic.validates_presence_of :title - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil}) - @topic.valid? - end - - def test_validates_presence_of_generates_message_with_custom_default_message - Topic.validates_presence_of :title, :message => 'custom' - @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_short - Topic.validates_length_of :title, :within => 3..5 - @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message - Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' - @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long - Topic.validates_length_of :title, :within => 3..5 - @topic.title = 'this title is too long' - @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message - Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom' - @topic.title = 'this title is too long' - @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) - @topic.valid? - end - - # validates_length_of :within w/ mocha - - def test_validates_length_of_within_generates_message_with_title_too_short - Topic.validates_length_of :title, :within => 3..5 - @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message - Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom' - @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long - Topic.validates_length_of :title, :within => 3..5 - @topic.title = 'this title is too long' - @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message - Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom' - @topic.title = 'this title is too long' - @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'}) - @topic.valid? - end - - # validates_length_of :is w/ mocha - - def test_validates_length_of_is_generates_message - Topic.validates_length_of :title, :is => 5 - @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil}) - @topic.valid? - end - - def test_validates_length_of_is_generates_message_with_custom_default_message - Topic.validates_length_of :title, :is => 5, :message => 'custom' - @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'}) - @topic.valid? - end - - # validates_format_of w/ mocha - - def test_validates_format_of_generates_message - Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @topic.title = '72x' - @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil}) - @topic.valid? - end - - def test_validates_format_of_generates_message_with_custom_default_message - Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom' - @topic.title = '72x' - @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'}) - @topic.valid? - end - - # validates_inclusion_of w/ mocha - - def test_validates_inclusion_of_generates_message - Topic.validates_inclusion_of :title, :in => %w(a b c) - @topic.title = 'z' - @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil}) - @topic.valid? - end - - def test_validates_inclusion_of_generates_message_with_custom_default_message - Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom' - @topic.title = 'z' - @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'}) - @topic.valid? - end - - # validates_exclusion_of w/ mocha - - def test_validates_exclusion_of_generates_message - Topic.validates_exclusion_of :title, :in => %w(a b c) - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil}) - @topic.valid? - end - - def test_validates_exclusion_of_generates_message_with_custom_default_message - Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom' - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'}) - @topic.valid? - end - - # validates_numericality_of without :only_integer w/ mocha - - def test_validates_numericality_of_generates_message - Topic.validates_numericality_of :title - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :message => 'custom' - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) - @topic.valid? - end - - # validates_numericality_of with :only_integer w/ mocha - - def test_validates_numericality_of_only_integer_generates_message - Topic.validates_numericality_of :title, :only_integer => true - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom' - @topic.title = 'a' - @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) - @topic.valid? - end - - # validates_numericality_of :odd w/ mocha - - def test_validates_numericality_of_odd_generates_message - Topic.validates_numericality_of :title, :only_integer => true, :odd => true - @topic.title = 0 - @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_odd_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom' - @topic.title = 0 - @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'}) - @topic.valid? - end - - # validates_numericality_of :less_than w/ mocha - - def test_validates_numericality_of_less_than_generates_message - Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @topic.title = 1 - @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil}) - @topic.valid? - end - - def test_validates_numericality_of_odd_generates_message_with_custom_default_message - Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom' - @topic.title = 1 - @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'}) - @topic.valid? - end - - # validates_confirmation_of w/o mocha - - def test_validates_confirmation_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:confirmation => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}} - - Topic.validates_confirmation_of :title - @topic.title_confirmation = 'foo' - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_confirmation_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}} - - Topic.validates_confirmation_of :title - @topic.title_confirmation = 'foo' - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_acceptance_of w/o mocha - - def test_validates_acceptance_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:accepted => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}} - - Topic.validates_acceptance_of :title, :allow_nil => false - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_acceptance_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}} - - Topic.validates_acceptance_of :title, :allow_nil => false - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_presence_of w/o mocha - - def test_validates_presence_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:blank => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:blank => 'global message'}}} - - Topic.validates_presence_of :title - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_presence_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:blank => 'global message'}}} - - Topic.validates_presence_of :title - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_length_of :within w/o mocha - - def test_validates_length_of_within_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:too_short => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}} - - Topic.validates_length_of :title, :within => 3..5 - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_length_of_within_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}} - - Topic.validates_length_of :title, :within => 3..5 - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_length_of :is w/o mocha - - def test_validates_length_of_is_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}} - - Topic.validates_length_of :title, :is => 5 - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_length_of_is_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}} - - Topic.validates_length_of :title, :is => 5 - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - def test_validates_length_of_is_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}} - - Topic.validates_length_of :title, :is => 5 - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_length_of_is_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}} - - Topic.validates_length_of :title, :is => 5 - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - - # validates_format_of w/o mocha - - def test_validates_format_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:invalid => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} - - Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_format_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} - - Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_inclusion_of w/o mocha - - def test_validates_inclusion_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:inclusion => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}} - - Topic.validates_inclusion_of :title, :in => %w(a b c) - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_inclusion_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}} - - Topic.validates_inclusion_of :title, :in => %w(a b c) - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_exclusion_of w/o mocha - - def test_validates_exclusion_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:exclusion => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}} - - Topic.validates_exclusion_of :title, :in => %w(a b c) - @topic.title = 'a' - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_exclusion_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}} - - Topic.validates_exclusion_of :title, :in => %w(a b c) - @topic.title = 'a' - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_numericality_of without :only_integer w/o mocha - - def test_validates_numericality_of_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}} - - Topic.validates_numericality_of :title - @topic.title = 'a' - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_numericality_of_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true - @topic.title = 'a' - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_numericality_of with :only_integer w/o mocha - - def test_validates_numericality_of_only_integer_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true - @topic.title = 'a' - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_numericality_of_only_integer_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true - @topic.title = 'a' - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_numericality_of :odd w/o mocha - - def test_validates_numericality_of_odd_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:odd => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:odd => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true, :odd => true - @topic.title = 0 - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_numericality_of_odd_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:odd => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true, :odd => true - @topic.title = 0 - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - # validates_numericality_of :less_than w/o mocha - - def test_validates_numericality_of_less_than_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:less_than => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @topic.title = 1 - @topic.valid? - assert_equal ['custom message'], @topic.errors[:title] - end - - def test_validates_numericality_of_less_than_finds_global_default_translation - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}} - - Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0 - @topic.title = 1 - @topic.valid? - assert_equal ['global message'], @topic.errors[:title] - end - - def test_validations_with_message_symbol_must_translate - I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:custom_error => "I am a custom error"}}} - Topic.validates_presence_of :title, :message => :custom_error - @topic.title = nil - @topic.valid? - assert_equal ["I am a custom error"], @topic.errors[:title] - end - - def test_validates_with_message_symbol_must_translate_per_attribute - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}} - Topic.validates_presence_of :title, :message => :custom_error - @topic.title = nil - @topic.valid? - assert_equal ["I am a custom error"], @topic.errors[:title] - end - - def test_validates_with_message_symbol_must_translate_per_model - I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:custom_error => "I am a custom error"}}}} - Topic.validates_presence_of :title, :message => :custom_error - @topic.title = nil - @topic.valid? - assert_equal ["I am a custom error"], @topic.errors[:title] - end - - def test_validates_with_message_string - Topic.validates_presence_of :title, :message => "I am a custom error" - @topic.title = nil - @topic.valid? - assert_equal ["I am a custom error"], @topic.errors[:title] - end -end - -class ActiveRecordValidationsGenerateMessageI18nTests < ActiveRecord::TestCase - - def setup - @topic = Topic.new - I18n.backend.store_translations :'en', { - :activerecord => { - :errors => { - :messages => { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is {{count}} characters)", - :too_short => "is too short (minimum is {{count}} characters)", - :wrong_length => "is the wrong length (should be {{count}} characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than {{count}}", - :greater_than_or_equal_to => "must be greater than or equal to {{count}}", - :equal_to => "must be equal to {{count}}", - :less_than => "must be less than {{count}}", - :less_than_or_equal_to => "must be less than or equal to {{count}}", - :odd => "must be odd", - :even => "must be even" - } - } - } - } - end - - # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) - def test_generate_message_inclusion_with_default_message - assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title') - end - - def test_generate_message_inclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) - def test_generate_message_exclusion_with_default_message - assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title') - end - - def test_generate_message_exclusion_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) - def test_generate_message_invalid_with_default_message - assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title') - end - - def test_generate_message_invalid_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message]) - def test_generate_message_confirmation_with_default_message - assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil) - end - - def test_generate_message_confirmation_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message') - end - - # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message]) - def test_generate_message_accepted_with_default_message - assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil) - end - - def test_generate_message_accepted_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message') - end - - # add_on_empty: generate_message(attr, :empty, :default => custom_message) - def test_generate_message_empty_with_default_message - assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil) - end - - def test_generate_message_empty_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message') - end - - # add_on_blank: generate_message(attr, :blank, :default => custom_message) - def test_generate_message_blank_with_default_message - assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil) - end - - def test_generate_message_blank_with_custom_message - assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message') - end - - # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) - def test_generate_message_too_long_with_default_message - assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10) - end - - def test_generate_message_too_long_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) - def test_generate_message_too_short_with_default_message - assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10) - end - - def test_generate_message_too_short_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10) - end - - # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value) - def test_generate_message_wrong_length_with_default_message - assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10) - end - - def test_generate_message_wrong_length_with_custom_message - assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10) - end - - # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message]) - def test_generate_message_taken_with_default_message - assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title') - end - - def test_generate_message_taken_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) - def test_generate_message_not_a_number_with_default_message - assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title') - end - - def test_generate_message_not_a_number_with_custom_message - assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title') - end - - # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) - def test_generate_message_greater_than_with_default_message - assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_greater_than_or_equal_to_with_default_message - assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_equal_to_with_default_message - assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_with_default_message - assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_less_than_or_equal_to_with_default_message - assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_odd_with_default_message - assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10) - end - - def test_generate_message_even_with_default_message - assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) - end - # ActiveRecord#RecordInvalid exception - - test "RecordInvalid exception can be localized" do - topic = Topic.new - topic.errors.add(:title, :invalid) - topic.errors.add(:title, :blank) - assert_equal "Validation failed: Title is invalid, Title can't be blank", ActiveRecord::RecordInvalid.new(topic).message - end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5cdb623eef..130231c622 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -148,40 +148,9 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal "100,000", d.salary_before_type_cast end - def test_validates_length_with_globally_modified_error_message - defaults = ActiveSupport::Deprecation.silence { ActiveRecord::Errors.default_error_messages } - original_message = defaults[:too_short] - defaults[:too_short] = 'tu est trops petit hombre {{count}}' - - Topic.validates_length_of :title, :minimum => 10 - t = Topic.create(:title => 'too short') - assert !t.valid? - - assert_equal ['tu est trops petit hombre 10'], t.errors[:title] - - ensure - defaults[:too_short] = original_message - end - def test_validates_acceptance_of_as_database_column Topic.validates_acceptance_of(:author_name) topic = Topic.create("author_name" => "Dan Brown") assert_equal "Dan Brown", topic["author_name"] end - - def test_deprecated_validation_instance_methods - tom = DeprecatedPerson.new - - assert_deprecated do - assert tom.invalid? - assert_equal ["always invalid", "invalid on create"], tom.errors[:name] - end - - tom.save(false) - - assert_deprecated do - assert tom.invalid? - assert_equal ["always invalid", "invalid on update"], tom.errors[:name] - end - end end |