diff options
Diffstat (limited to 'activerecord')
39 files changed, 1431 insertions, 155 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d92b89cfe0..90be9b700a 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,29 @@ *Edge* +* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin] + +* change_column_default preserves the not-null constraint. #617 [Tarmo Tänav] + +* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334] + +* Add :accessible option to associations for allowing (opt-in) mass assignment. #474. [David Dollar] Example : + + class Post < ActiveRecord::Base + belongs_to :author, :accessible => true + has_many :comments, :accessible => true + end + + post = Post.create({ + :title => 'Accessible Attributes', + :author => { :name => 'David Dollar' }, + :comments => [ + { :body => 'First Post!' }, + { :body => 'Nested Hashes are great!' } + ] + }) + + post.comments << { :body => 'Another Comment' } + * Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example : # Ensure essay contains at least 100 words. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 60b17e02b9..983528aff7 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -30,7 +30,7 @@ desc 'Run mysql, sqlite, and postgresql tests by default' task :default => :test desc 'Run mysql, sqlite, and postgresql tests' -task :test => %w(test_mysql test_sqlite test_sqlite3 test_postgresql) +task :test => %w(test_mysql test_sqlite3 test_postgresql) for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase ) Rake::TestTask.new("test_#{adapter}") { |t| diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d4f7170305..17a7949959 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -80,3 +80,8 @@ end require 'active_record/connection_adapters/abstract_adapter' require 'active_record/schema_dumper' + +I18n.backend.populate do + require 'active_record/locale/en-US.rb' +end + diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 194c4ee5db..c7594809b7 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -188,7 +188,6 @@ module ActiveRecord through_records end - # FIXME: quoting def preload_belongs_to_association(records, reflection, preload_options={}) options = reflection.options primary_key_name = reflection.primary_key_name @@ -227,9 +226,19 @@ module ActiveRecord table_name = klass.quoted_table_name primary_key = klass.primary_key - conditions = "#{table_name}.#{primary_key} IN (?)" + conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)" conditions << append_conditions(options, preload_options) - associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq], + column_type = klass.columns.detect{|c| c.name == primary_key}.type + ids = id_map.keys.uniq.map do |id| + if column_type == :integer + id.to_i + elsif column_type == :float + id.to_f + else + id + end + end + associated_records = klass.find(:all, :conditions => [conditions, ids], :include => options[:include], :select => options[:select], :joins => options[:joins], @@ -243,7 +252,7 @@ module ActiveRecord table_name = reflection.klass.quoted_table_name if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'" + conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" else foreign_key = reflection.primary_key_name conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)" diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a0d3c2b5bf..2d506c0418 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -739,7 +739,9 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. - # + # [:accessible] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). + # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -853,6 +855,8 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -969,6 +973,8 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -1181,6 +1187,8 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. + # [:accessible<] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # has_and_belongs_to_many :projects @@ -1256,6 +1264,8 @@ module ActiveRecord association = association_proxy_class.new(self, reflection) end + new_value = reflection.klass.new(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) + if association_proxy_class == HasOneThroughAssociation association.create_through_record(new_value) self.send(reflection.name, new_value) @@ -1504,7 +1514,7 @@ module ActiveRecord :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate + :validate, :accessible ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) @@ -1514,7 +1524,7 @@ module ActiveRecord def create_has_one_reflection(association_id, options) options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key + :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key, :accessible ) create_reflection(:has_one, association_id, options, self) @@ -1530,7 +1540,7 @@ module ActiveRecord def create_belongs_to_reflection(association_id, options) options.assert_valid_keys( :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, - :counter_cache, :extend, :polymorphic, :readonly, :validate + :counter_cache, :extend, :polymorphic, :readonly, :validate, :accessible ) reflection = create_reflection(:belongs_to, association_id, options, self) @@ -1550,7 +1560,7 @@ module ActiveRecord :finder_sql, :delete_sql, :insert_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate + :validate, :accessible ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) @@ -1627,10 +1637,15 @@ module ActiveRecord join_dependency.joins_for_table_name(table) }.flatten.compact.uniq + order = options[:order] + if scoped_order = (scope && scope[:order]) + order = order ? "#{order}, #{scoped_order}" : scoped_order + end + is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order) sql = "SELECT " if is_distinct - sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order]) + sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", order) else sql << primary_key end @@ -1644,8 +1659,8 @@ module ActiveRecord add_conditions!(sql, options[:conditions], scope) add_group!(sql, options[:group], scope) - if options[:order] && is_distinct - connection.add_order_by_for_association_limiting!(sql, options) + if order && is_distinct + connection.add_order_by_for_association_limiting!(sql, :order => order) else add_order!(sql, options[:order], scope) end @@ -2019,7 +2034,7 @@ module ActiveRecord jt_sti_extra = " AND %s.%s = %s" % [ connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(through_reflection.active_record.inheritance_column), - through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)] + through_reflection.klass.quote_value(through_reflection.klass.sti_name)] end when :belongs_to first_key = primary_key @@ -2087,7 +2102,7 @@ module ActiveRecord join << %(AND %s.%s = %s ) % [ connection.quote_table_name(aliased_table_name), connection.quote_column_name(klass.inheritance_column), - klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record? + klass.quote_value(klass.sti_name)] unless klass.descends_from_active_record? [through_reflection, reflection].each do |ref| join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions] diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index eb39714909..a28be9eed1 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -78,11 +78,14 @@ module ActiveRecord @loaded = false end - def build(attributes = {}) + def build(attributes = {}, &block) if attributes.is_a?(Array) - attributes.collect { |attr| build(attr) } + attributes.collect { |attr| build(attr, &block) } else - build_record(attributes) { |record| set_belongs_to_association_for(record) } + build_record(attributes) do |record| + block.call(record) if block_given? + set_belongs_to_association_for(record) + end end end @@ -94,6 +97,8 @@ module ActiveRecord @owner.transaction do flatten_deeper(records).each do |record| + record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash) + raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| result &&= insert_record(record) unless @owner.new_record? @@ -226,6 +231,10 @@ module ActiveRecord # Replace this collection with +other_array+ # This will perform a diff and delete/add only records that have changed. def replace(other_array) + other_array.map! do |val| + val.is_a?(Hash) ? @reflection.klass.new(val) : val + end if @reflection.options[:accessible] + other_array.each { |val| raise_on_type_mismatch(val) } load_target diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 255ca0b67b..9cb64223e2 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -464,6 +464,10 @@ module ActiveRecord #:nodoc: cattr_accessor :schema_format , :instance_writer => false @@schema_format = :ruby + # Specify whether or not to use timestamps for migration numbers + cattr_accessor :timestamped_migrations , :instance_writer => false + @@timestamped_migrations = true + # Determine whether to store the full constant name including namespace when using STI superclass_delegating_accessor :store_full_sti_class self.store_full_sti_class = false @@ -852,7 +856,7 @@ module ActiveRecord #:nodoc: def update_counters(id, counters) updates = counters.inject([]) { |list, (counter_name, increment)| sign = increment < 0 ? "-" : "+" - list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}" + list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}" }.join(", ") update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}") end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index d4c8a80448..31d6c7942c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,4 +1,5 @@ require 'date' +require 'set' require 'bigdecimal' require 'bigdecimal/util' @@ -6,6 +7,8 @@ module ActiveRecord module ConnectionAdapters #:nodoc: # An abstract definition of a column in a table. class Column + TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set + module Format ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ @@ -135,11 +138,7 @@ module ActiveRecord # convert something to a boolean def value_to_boolean(value) - if value == true || value == false - value - else - !(value.to_s !~ /\A(?:1|t|true)\Z/i) - end + TRUE_VALUES.include?(value) end # convert something to a BigDecimal @@ -257,7 +256,10 @@ module ActiveRecord def to_sql column_sql = "#{base.quote_column_name(name)} #{sql_type}" - add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key + column_options = {} + column_options[:null] = null unless null.nil? + column_options[:default] = default unless default.nil? + add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key column_sql end alias to_s :to_sql diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index f48b107a2a..47dbf5a5f3 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -118,6 +118,19 @@ module ActiveRecord @connection end + def open_transactions + @open_transactions ||= 0 + end + + def increment_open_transactions + @open_transactions ||= 0 + @open_transactions += 1 + end + + def decrement_open_transactions + @open_transactions -= 1 + end + def log_info(sql, name, runtime) if @logger && @logger.debug? name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})" diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 4b13ac8be0..35b9ed4746 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -437,18 +437,29 @@ module ActiveRecord end def change_column_default(table_name, column_name, default) #:nodoc: - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end - execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}") + change_column table_name, column_name, column.sql_type, :null => null end def change_column(table_name, column_name, type, options = {}) #:nodoc: + column = column_for(table_name, column_name) + unless options_include_default?(options) - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - else - raise "No such column: #{table_name}.#{column_name}" - end + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null end change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" @@ -460,6 +471,7 @@ module ActiveRecord options = {} if column = columns(table_name).find { |c| c.name == column_name.to_s } options[:default] = column.default + options[:null] = column.null else raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" end @@ -536,6 +548,13 @@ module ActiveRecord def version @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 40f34e2dfa..6a20f41a4b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -49,7 +49,6 @@ module ActiveRecord private def extract_limit(sql_type) case sql_type - when /^integer/i; 4 when /^bigint/i; 8 when /^smallint/i; 2 else super @@ -623,6 +622,19 @@ module ActiveRecord end end + # Returns the current database name. + def current_database + query('select current_database()')[0][0] + end + + # Returns the current database encoding format. + def encoding + query(<<-end_sql)[0][0] + SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database + WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + # Sets the schema search path to a string of comma-separated schema names. # Names beginning with $ have to be quoted (e.g. $user => '$user'). # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 51cfd10e5c..84f8c0284e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -238,6 +238,15 @@ module ActiveRecord end end + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + alter_table(table_name) do |definition| + definition[column_name].null = null + end + end + def change_column(table_name, column_name, type, options = {}) #:nodoc: alter_table(table_name) do |definition| include_default = options_include_default?(options) @@ -251,6 +260,9 @@ module ActiveRecord end def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index e19614e31f..622cfc3c3f 100755 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) all_loaded_fixtures.update(fixtures_map) - connection.transaction(Thread.current['open_transactions'].to_i == 0) do + connection.transaction(connection.open_transactions.zero?) do fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } fixtures.each { |fixture| fixture.insert_fixtures } @@ -541,10 +541,11 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) label.to_s.hash.abs end - attr_reader :table_name + attr_reader :table_name, :name def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter + @name = table_name # preserve fixture base name @class_name = class_name || (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize) @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" @@ -929,7 +930,7 @@ module Test #:nodoc: load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end - ActiveRecord::Base.send :increment_open_transactions + ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.begin_db_transaction # Load fixtures for every test. else @@ -950,9 +951,9 @@ module Test #:nodoc: end # Rollback changes if a transaction is active. - if use_transactional_fixtures? && Thread.current['open_transactions'] != 0 + if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0 ActiveRecord::Base.connection.rollback_db_transaction - Thread.current['open_transactions'] = 0 + ActiveRecord::Base.connection.decrement_open_transactions end ActiveRecord::Base.verify_active_connections! end @@ -963,9 +964,9 @@ module Test #:nodoc: fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) unless fixtures.nil? if fixtures.instance_of?(Fixtures) - @loaded_fixtures[fixtures.table_name] = fixtures + @loaded_fixtures[fixtures.name] = fixtures else - fixtures.each { |f| @loaded_fixtures[f.table_name] = f } + fixtures.each { |f| @loaded_fixtures[f.name] = f } end end end diff --git a/activerecord/lib/active_record/locale/en-US.rb b/activerecord/lib/active_record/locale/en-US.rb new file mode 100644 index 0000000000..b31e13ed3a --- /dev/null +++ b/activerecord/lib/active_record/locale/en-US.rb @@ -0,0 +1,25 @@ +I18n.backend.store_translations :'en-US', { + :active_record => { + :error_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" + } + } +}
\ No newline at end of file diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index e095b3c766..731a350854 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -238,6 +238,22 @@ module ActiveRecord # lower than the current schema version: when migrating up, those # never-applied "interleaved" migrations will be automatically applied, and # when migrating down, never-applied "interleaved" migrations will be skipped. + # + # == Timestamped Migrations + # + # By default, Rails generates migrations that look like: + # + # 20080717013526_your_migration_name.rb + # + # The prefix is a generation timestamp (in UTC). + # + # If you'd prefer to use numeric prefixes, you can turn timestamped migrations + # off by setting: + # + # config.active_record.timestamped_migrations = false + # + # In environment.rb. + # class Migration @@verbose = true cattr_accessor :verbose diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 080e3d0f5e..d5a1c5fe08 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -103,7 +103,7 @@ module ActiveRecord attr_reader :proxy_scope, :proxy_options [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/ + unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?)/ delegate m, :to => :proxy_found end end @@ -140,6 +140,14 @@ module ActiveRecord @found ? @found.empty? : count.zero? end + def any? + if block_given? + proxy_found.any? { |*block_args| yield(*block_args) } + else + !empty? + end + end + protected def proxy_found @found || load_found diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 25e0e61c69..b35e407cc1 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -20,7 +20,7 @@ module ActiveRecord # ActiveRecord::Base.observers = Cacher, GarbageCollector # # Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is - # called during startup, and before each development request. + # called during startup, and before each development request. def observers=(*observers) @observers = observers.flatten end @@ -130,11 +130,11 @@ module ActiveRecord # Observers register themselves in the model class they observe, since it is the class that # notifies them of events when they occur. As a side-effect, when an observer is loaded its # corresponding model class is loaded. - # + # # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and - # application initializers. Now observers are loaded after application initializers, + # application initializers. Now observers are loaded after application initializers, # so observed models can make use of extensions. - # + # # If by any chance you are using observed models in the initialization you can still # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are # singletons and that call instantiates and registers them. @@ -189,7 +189,9 @@ module ActiveRecord def add_observer!(klass) klass.add_observer(self) - klass.class_eval 'def after_find() end' unless klass.method_defined?(:after_find) + if respond_to?(:after_find) && !klass.method_defined?(:after_find) + klass.class_eval 'def after_find() end' + end end end end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 7dee962c8a..ca5591ae35 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -22,11 +22,22 @@ module ActiveRecord end end + def assert_sql(*patterns_to_match) + $queries_executed = [] + yield + ensure + failed_patterns = [] + patterns_to_match.each do |pattern| + failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql } + end + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found." + end + def assert_queries(num = 1) - $query_count = 0 + $queries_executed = [] yield ensure - assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." + assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed." end def assert_no_queries(&block) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index b014ab757d..0531afbb52 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -76,25 +76,14 @@ module ActiveRecord module ClassMethods # See ActiveRecord::Transactions::ClassMethods for detailed documentation. def transaction(&block) - increment_open_transactions + connection.increment_open_transactions begin - connection.transaction(Thread.current['start_db_transaction'], &block) + connection.transaction(connection.open_transactions == 1, &block) ensure - decrement_open_transactions + connection.decrement_open_transactions end end - - private - def increment_open_transactions #:nodoc: - open = Thread.current['open_transactions'] ||= 0 - Thread.current['start_db_transaction'] = open.zero? - Thread.current['open_transactions'] = open + 1 - end - - def decrement_open_transactions #:nodoc: - Thread.current['open_transactions'] -= 1 - end end def transaction(&block) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 2647fbba92..b957ee3b9e 100755 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -18,37 +18,18 @@ module ActiveRecord # determine whether the object is in a valid state to be saved. See usage example in Validations. class Errors include Enumerable + + class << self + def default_error_messages + ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('active_record.error_messages').") + I18n.translate 'active_record.error_messages' + end + end def initialize(base) # :nodoc: @base, @errors = base, {} end - @@default_error_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 %d characters)", - :too_short => "is too short (minimum is %d characters)", - :wrong_length => "is the wrong length (should be %d characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than %d", - :greater_than_or_equal_to => "must be greater than or equal to %d", - :equal_to => "must be equal to %d", - :less_than => "must be less than %d", - :less_than_or_equal_to => "must be less than or equal to %d", - :odd => "must be odd", - :even => "must be even" - } - - # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. - cattr_accessor :default_error_messages - - # Adds an error to the base object instead of any particular attribute. This is used # to report errors that don't tie to any specific attribute, but rather to the object # as a whole. These error messages don't get prepended with any field name when iterating @@ -61,27 +42,36 @@ module ActiveRecord # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. # If no +msg+ is supplied, "invalid" is assumed. - def add(attribute, msg = @@default_error_messages[:invalid]) - @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? - @errors[attribute.to_s] << msg - end + def add(attribute, message = nil) + message ||= I18n.translate :"active_record.error_messages.invalid" + @errors[attribute.to_s] ||= [] + @errors[attribute.to_s] << message + end # Will add an error message to each of the attributes in +attributes+ that is empty. - def add_on_empty(attributes, msg = @@default_error_messages[:empty]) + def add_on_empty(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - is_empty = value.respond_to?("empty?") ? value.empty? : false - add(attr, msg) unless !value.nil? && !is_empty + is_empty = value.respond_to?("empty?") ? value.empty? : false + add(attr, generate_message(attr, :empty, :default => custom_message)) unless !value.nil? && !is_empty end end # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). - def add_on_blank(attributes, msg = @@default_error_messages[:blank]) + def add_on_blank(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - add(attr, msg) if value.blank? + add(attr, generate_message(attr, :blank, :default => custom_message)) if value.blank? end end + + def generate_message(attr, key, options = {}) + msgs = base_classes(@base.class).map{|klass| :"custom.#{klass.name.underscore}.#{attr}.#{key}"} + msgs << options[:default] if options[:default] + msgs << key + + I18n.t nil, options.merge(:default => msgs, :scope => [:active_record, :error_messages]) + end # Returns true if the specified +attribute+ has errors associated with it. # @@ -164,24 +154,26 @@ module ActiveRecord # 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 + # 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 = [] @errors.each_key do |attr| - @errors[attr].each do |msg| - next if msg.nil? + @errors[attr].each do |message| + next unless message if attr == "base" - full_messages << msg + full_messages << message else - full_messages << @base.class.human_attribute_name(attr) + " " + msg + key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}" + attr_name = I18n.translate(key, :locale => options[:locale], :default => @base.class.human_attribute_name(attr)) + full_messages << attr_name + " " + message end end end full_messages - end + end # Returns true if no errors have been added. def empty? @@ -226,6 +218,17 @@ module ActiveRecord full_messages.each { |msg| e.error(msg) } end end + + protected + + # TODO maybe this should be on ActiveRecord::Base, maybe #self_and_descendents_from_active_record + def base_classes(klass) + classes = [klass] + while klass != klass.base_class + classes << klass = klass.superclass + end + classes + end end @@ -388,13 +391,16 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + message = record.errors.generate_message(attr_name, :confirmation, :default => configuration[:message]) + record.errors.add(attr_name, message) + end end end @@ -422,7 +428,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } + configuration = { :on => :save, :allow_nil => true, :accept => "1" } configuration.update(attr_names.extract_options!) db_cols = begin @@ -434,7 +440,10 @@ module ActiveRecord attr_accessor(*names) validates_each(attr_names,configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] + unless value == configuration[:accept] + message = record.errors.generate_message(attr_name, :accepted, :default => configuration[:message]) + record.errors.add(attr_name, message) + end end end @@ -461,7 +470,7 @@ module ActiveRecord # method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) # can't use validates_each here, because it cannot cope with nonexistent attributes, @@ -509,10 +518,7 @@ module ActiveRecord def validates_length_of(*attrs) # Merge given options with defaults. options = { - :too_long => ActiveRecord::Errors.default_error_messages[:too_long], - :too_short => ActiveRecord::Errors.default_error_messages[:too_short], - :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length], - :tokenizer => lambda {|value| value.split(//)} + :tokenizer => lambda {|value| value.split(//)} }.merge(DEFAULT_VALIDATION_OPTIONS) options.update(attrs.extract_options!.symbolize_keys) @@ -535,15 +541,14 @@ module ActiveRecord when :within, :in raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - too_short = options[:too_short] % option_value.begin - too_long = options[:too_long] % option_value.end - validates_each(attrs, options) do |record, attr, value| value = options[:tokenizer].call(value) if value.kind_of?(String) if value.nil? or value.size < option_value.begin - record.errors.add(attr, too_short) + message = record.errors.generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + record.errors.add(attr, message) elsif value.size > option_value.end - record.errors.add(attr, too_long) + message = record.errors.generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end) + record.errors.add(attr, message) end end when :is, :minimum, :maximum @@ -553,11 +558,14 @@ module ActiveRecord validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } - message = (options[:message] || options[message_options[option]]) % option_value - validates_each(attrs, options) do |record, attr, value| value = options[:tokenizer].call(value) if value.kind_of?(String) - record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + unless !value.nil? and value.size.method(validity_checks[option])[option_value] + key = message_options[option] + custom_message = options[:message] || options[key] + message = record.errors.generate_message(attr, key, :default => custom_message, :count => option_value) + record.errors.add(attr, message) + end end end end @@ -599,7 +607,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_uniqueness_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true } + configuration = { :case_sensitive => true } configuration.update(attr_names.extract_options!) validates_each(attr_names,configuration) do |record, attr_name, value| @@ -656,10 +664,13 @@ module ActiveRecord # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate # column in ruby when case sensitive option if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? - found = results.any? { |a| a[attr_name.to_s] == value } + found = results.any? { |a| a[attr_name.to_s] == value.to_s } + end + + if found + message = record.errors.generate_message(attr_name, :taken, :default => configuration[:message]) + record.errors.add(attr_name, message) end - - record.errors.add(attr_name, configuration[:message]) if found end end end @@ -689,13 +700,16 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } + configuration = { :on => :save, :with => nil } configuration.update(attr_names.extract_options!) raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with] + unless value.to_s =~ configuration[:with] + message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -719,7 +733,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -727,7 +741,10 @@ module ActiveRecord raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) + unless enum.include?(value) + message = record.errors.generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -751,7 +768,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -759,7 +776,10 @@ module ActiveRecord raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) + if enum.include?(value) + message = record.errors.generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -795,12 +815,14 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless - (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value) + record.errors.add(attr_name, message) + end end end @@ -848,15 +870,17 @@ module ActiveRecord if configuration[:only_integer] unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + record.errors.add(attr_name, message) next end raw_value = raw_value.to_i else - begin + begin raw_value = Kernel.Float(raw_value) rescue ArgumentError, TypeError - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) + record.errors.add(attr_name, message) next end end @@ -864,10 +888,12 @@ module ActiveRecord numericality_options.each do |option| case option when :odd, :even - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + message = record.errors.generate_message(attr_name, option, :value => raw_value, :default => configuration[:message]) + record.errors.add(attr_name, message) + end else - message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option] - message = message % configuration[option] if configuration[option] + message = record.errors.generate_message(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 11f9870534..04770646b2 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -65,6 +65,12 @@ class AdapterTest < ActiveRecord::TestCase end end + if current_adapter?(:PostgreSQLAdapter) + def test_encoding + assert_not_nil @connection.encoding + end + end + def test_table_alias def @connection.test_table_alias_length() 10; end class << @connection diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb new file mode 100644 index 0000000000..7c470616a5 --- /dev/null +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' +require 'models/post' +require 'models/tagging' + +module Namespaced + class Post < ActiveRecord::Base + set_table_name 'posts' + has_one :tagging, :as => :taggable, :class_name => 'Tagging' + end +end + +class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase + + def setup + generate_test_objects + end + + def generate_test_objects + post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 ) + tagging = Tagging.create( :taggable => post ) + end + + def test_class_names + old = ActiveRecord::Base.store_full_sti_class + + ActiveRecord::Base.store_full_sti_class = false + post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + assert_nil post.tagging + + ActiveRecord::Base.store_full_sti_class = true + post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + assert_equal 'Tagging', post.tagging.class.name + ensure + ActiveRecord::Base.store_full_sti_class = old + end +end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index e90edbb213..f8b8b1f96d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -425,6 +425,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, first_topic.replies.to_ary.size end + def test_build_via_block + company = companies(:first_firm) + new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } } + assert !company.clients_of_firm.loaded? + + assert_equal "Another Client", new_client.name + assert new_client.new_record? + assert_equal new_client, company.clients_of_firm.last + company.name += '-changed' + assert_queries(2) { assert company.save } + assert !new_client.new_record? + assert_equal 2, company.clients_of_firm(true).size + end + + def test_build_many_via_block + company = companies(:first_firm) + new_clients = assert_no_queries do + company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| + client.name = "changed" + end + end + + assert_equal 2, new_clients.size + assert_equal "changed", new_clients.first.name + assert_equal "changed", new_clients.last.name + + company.name += '-changed' + assert_queries(3) { assert company.save } + assert_equal 3, company.clients_of_firm(true).size + end + def test_create_without_loading_association first_firm = companies(:first_firm) Firm.column_names @@ -968,4 +999,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.clients.loaded? end + def test_joins_with_namespaced_model_should_use_correct_type + old = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = true + + firm = Namespaced::Firm.create({ :name => 'Some Company' }) + firm.clients.create({ :name => 'Some Client' }) + + stats = Namespaced::Firm.find(firm.id, { + :select => "#{Namespaced::Firm.table_name}.*, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", + :joins => :clients, + :group => "#{Namespaced::Firm.table_name}.id" + }) + assert_equal 1, stats.num_clients.to_i + + ensure + ActiveRecord::Base.store_full_sti_class = old + end + end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 59349dd7cf..4904feeb7d 100755 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -189,6 +189,114 @@ class AssociationProxyTest < ActiveRecord::TestCase end end + def test_belongs_to_mass_assignment + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + author_attributes = { :name => 'David Dollar' } + + assert_no_difference 'Author.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:author => author_attributes})) + end + end + + assert_difference 'Author.count' do + post = Post.create(post_attributes.merge({:creatable_author => author_attributes})) + assert_equal post.creatable_author.name, author_attributes[:name] + end + end + + def test_has_one_mass_assignment + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + comment_attributes = { :body => 'Setter Takes Hash' } + + assert_no_difference 'Comment.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:uncreatable_comment => comment_attributes})) + end + end + + assert_difference 'Comment.count' do + post = Post.create(post_attributes.merge({:creatable_comment => comment_attributes})) + assert_equal post.creatable_comment.body, comment_attributes[:body] + end + end + + def test_has_many_mass_assignment + post = posts(:welcome) + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + comment_attributes = { :body => 'Setter Takes Hash' } + + assert_no_difference 'Comment.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:comments => [comment_attributes]})) + end + assert_raise(ActiveRecord::AssociationTypeMismatch) do + post.comments << comment_attributes + end + end + + assert_difference 'Comment.count' do + post = Post.create(post_attributes.merge({:creatable_comments => [comment_attributes]})) + assert_equal post.creatable_comments.last.body, comment_attributes[:body] + end + + assert_difference 'Comment.count' do + post.creatable_comments << comment_attributes + assert_equal post.comments.last.body, comment_attributes[:body] + end + + post.creatable_comments = [comment_attributes, comment_attributes] + assert_equal post.creatable_comments.count, 2 + end + + def test_has_and_belongs_to_many_mass_assignment + post = posts(:welcome) + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + category_attributes = { :name => 'Accessible Association', :type => 'Category' } + + assert_no_difference 'Category.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:categories => [category_attributes]})) + end + assert_raise(ActiveRecord::AssociationTypeMismatch) do + post.categories << category_attributes + end + end + + assert_difference 'Category.count' do + post = Post.create(post_attributes.merge({:creatable_categories => [category_attributes]})) + assert_equal post.creatable_categories.last.name, category_attributes[:name] + end + + assert_difference 'Category.count' do + post.creatable_categories << category_attributes + assert_equal post.creatable_categories.last.name, category_attributes[:name] + end + + post.creatable_categories = [category_attributes, category_attributes] + assert_equal post.creatable_categories.count, 2 + end + + def test_association_proxy_setter_can_take_hash + special_comment_attributes = { :body => 'Setter Takes Hash' } + + post = posts(:welcome) + post.creatable_comment = { :body => 'Setter Takes Hash' } + + assert_equal post.creatable_comment.body, special_comment_attributes[:body] + end + + def test_association_collection_can_take_hash + post_attributes = { :title => 'Setter Takes', :body => 'Hash' } + david = authors(:david) + + post = (david.posts << post_attributes).last + assert_equal post.title, post_attributes[:title] + + david.posts = [post_attributes, post_attributes] + assert_equal david.posts.count, 2 + end + def setup_dangling_association josh = Author.create(:name => "Josh") p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a4be629fbd..9e4f268db7 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -19,6 +19,7 @@ require 'models/warehouse_thing' require 'rexml/document' class Category < ActiveRecord::Base; end +class Categorization < ActiveRecord::Base; end class Smarts < ActiveRecord::Base; end class CreditCard < ActiveRecord::Base class PinNumber < ActiveRecord::Base @@ -75,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations def test_table_exists assert !NonExistentTable.table_exists? @@ -130,7 +131,7 @@ class BasicsTest < ActiveRecord::TestCase def test_read_attributes_before_type_cast category = Category.new({:name=>"Test categoty", :type => nil}) - category_attrs = {"name"=>"Test categoty", "type" => nil} + category_attrs = {"name"=>"Test categoty", "type" => nil, "categorizations_count" => nil} assert_equal category_attrs , category.attributes_before_type_cast end @@ -614,6 +615,22 @@ class BasicsTest < ActiveRecord::TestCase assert_equal -2, Topic.find(2).replies_count end + def test_update_counter + category = Category.first + assert_nil category.categorizations_count + assert_equal 2, category.categorizations.count + + Category.update_counters(category.id, "categorizations_count" => category.categorizations.count) + category.reload + assert_not_nil category.categorizations_count + assert_equal 2, category.categorizations_count + + Category.update_counters(category.id, "categorizations_count" => category.categorizations.count) + category.reload + assert_not_nil category.categorizations_count + assert_equal 4, category.categorizations_count + end + def test_update_all assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'") assert_equal "bulk updated!", Topic.find(1).content diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb new file mode 100644 index 0000000000..540f42f4b6 --- /dev/null +++ b/activerecord/test/cases/column_definition_test.rb @@ -0,0 +1,36 @@ +require "cases/helper" + +class ColumnDefinitionTest < ActiveRecord::TestCase + def setup + @adapter = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(nil) + def @adapter.native_database_types + {:string => "varchar"} + end + end + + # Avoid column definitions in create table statements like: + # `title` varchar(255) DEFAULT NULL NULL + def test_should_not_include_default_clause_when_default_is_null + column = ActiveRecord::ConnectionAdapters::Column.new("title", nil, "varchar(20)") + column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new( + @adapter, column.name, "string", + column.limit, column.precision, column.scale, column.default, column.null) + assert_equal "title varchar(20) NULL", column_def.to_sql + end + + def test_should_include_default_clause_when_default_is_present + column = ActiveRecord::ConnectionAdapters::Column.new("title", "Hello", "varchar(20)") + column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new( + @adapter, column.name, "string", + column.limit, column.precision, column.scale, column.default, column.null) + assert_equal %Q{title varchar(20) DEFAULT 'Hello' NULL}, column_def.to_sql + end + + def test_should_specify_not_null_if_null_option_is_false + column = ActiveRecord::ConnectionAdapters::Column.new("title", "Hello", "varchar(20)", false) + column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new( + @adapter, column.name, "string", + column.limit, column.precision, column.scale, column.default, column.null) + assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index aca7cfb367..6ba7597f56 100755 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -15,6 +15,7 @@ require 'models/pirate' require 'models/treasure' require 'models/matey' require 'models/ship' +require 'models/book' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true @@ -373,6 +374,34 @@ class CheckSetTableNameFixturesTest < ActiveRecord::TestCase end end +class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase + set_fixture_class :items => Book + fixtures :items + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account our set_fixture_class + self.use_transactional_fixtures = false + + def test_named_accessor + assert_kind_of Book, items(:dvd) + end +end + +class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase + set_fixture_class :items => Book, :funny_jokes => Joke + fixtures :items, :funny_jokes + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account our set_fixture_class + self.use_transactional_fixtures = false + + def test_named_accessor_of_differently_named_fixture + assert_kind_of Book, items(:dvd) + end + + def test_named_accessor_of_same_named_fixture + assert_kind_of Joke, funny_jokes(:a_joke) + end +end + class CustomConnectionFixturesTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses @@ -432,11 +461,11 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase alias_method :teardown, :blank_teardown def test_no_rollback_in_teardown_unless_transaction_active - assert_equal 0, Thread.current['open_transactions'] + assert_equal 0, ActiveRecord::Base.connection.open_transactions assert_raise(RuntimeError) { ar_setup_fixtures } - assert_equal 0, Thread.current['open_transactions'] + assert_equal 0, ActiveRecord::Base.connection.open_transactions assert_nothing_raised { ar_teardown_fixtures } - assert_equal 0, Thread.current['open_transactions'] + assert_equal 0, ActiveRecord::Base.connection.open_transactions end private diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index dc83300efa..0530ba9bd9 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -32,13 +32,13 @@ end ActiveRecord::Base.connection.class.class_eval do IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/] - def execute_with_counting(sql, name = nil, &block) - $query_count ||= 0 - $query_count += 1 unless IGNORED_SQL.any? { |r| sql =~ r } - execute_without_counting(sql, name, &block) + def execute_with_query_record(sql, name = nil, &block) + $queries_executed ||= [] + $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } + execute_without_query_record(sql, name, &block) end - alias_method_chain :execute, :counting + alias_method_chain :execute, :query_record end # Make with_scope public for tests diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 47fb5c608f..4fd38bfbc9 100755 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -191,6 +191,13 @@ class InheritanceTest < ActiveRecord::TestCase assert_not_nil account.instance_variable_get("@firm"), "nil proves eager load failed" end + def test_eager_load_belongs_to_primary_key_quoting + con = Account.connection + assert_sql(/\(#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)\)/) do + Account.find(1, :include => :firm) + end + end + def test_alt_eager_loading switch_to_alt_inheritance_column test_eager_load_belongs_to_something_inherited diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 258f7c7a0f..ab005c6b00 100755 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -143,7 +143,7 @@ class LifecycleTest < ActiveRecord::TestCase assert_equal developer.name, multi_observer.record.name end - def test_observing_after_find_when_not_defined_on_the_model + def test_after_find_can_be_observed_when_its_not_defined_on_the_model observer = MinimalisticObserver.instance assert_equal Minimalistic, MinimalisticObserver.observed_class @@ -151,6 +151,42 @@ class LifecycleTest < ActiveRecord::TestCase assert_equal minimalistic, observer.minimalistic end + def test_after_find_can_be_observed_when_its_defined_on_the_model + observer = TopicObserver.instance + assert_equal Topic, TopicObserver.observed_class + + topic = Topic.find(1) + assert_equal topic, observer.topic + end + + def test_after_find_is_not_created_if_its_not_used + # use a fresh class so an observer can't have defined an + # after_find on it + model_class = Class.new(ActiveRecord::Base) + observer_class = Class.new(ActiveRecord::Observer) + observer_class.observe(model_class) + + observer = observer_class.instance + + assert !model_class.method_defined?(:after_find) + end + + def test_after_find_is_not_clobbered_if_it_already_exists + # use a fresh observer class so we can instantiate it (Observer is + # a Singleton) + model_class = Class.new(ActiveRecord::Base) do + def after_find; end + end + original_method = model_class.instance_method(:after_find) + observer_class = Class.new(ActiveRecord::Observer) do + def after_find; end + end + observer_class.observe(model_class) + + observer = observer_class.instance + assert_equal original_method, model_class.instance_method(:after_find) + end + def test_invalid_observer assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers } end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 4482b487dd..7ecf755ef8 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -3,6 +3,7 @@ require 'bigdecimal/util' require 'models/person' require 'models/topic' +require 'models/developer' require MIGRATIONS_ROOT + "/valid/1_people_have_last_names" require MIGRATIONS_ROOT + "/valid/2_we_need_reminders" @@ -511,7 +512,12 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.connection.create_table(:hats) do |table| table.column :hat_name, :string, :default => nil end - assert_raises(ActiveRecord::ActiveRecordError) do + exception = if current_adapter?(:PostgreSQLAdapter) + ActiveRecord::StatementInvalid + else + ActiveRecord::ActiveRecordError + end + assert_raises(exception) do Person.connection.rename_column "hats", "nonexistent", "should_fail" end ensure @@ -697,6 +703,55 @@ if ActiveRecord::Base.connection.supports_migrations? Person.connection.drop_table :testings rescue nil end + def test_keeping_default_and_notnull_constaint_on_change + Person.connection.create_table :testings do |t| + t.column :title, :string + end + person_klass = Class.new(Person) + person_klass.set_table_name 'testings' + + person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 + person_klass.reset_column_information + assert_equal 99, person_klass.columns_hash["wealth"].default + assert_equal false, person_klass.columns_hash["wealth"].null + assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")} + + # change column default to see that column doesn't lose its not null definition + person_klass.connection.change_column_default "testings", "wealth", 100 + person_klass.reset_column_information + assert_equal 100, person_klass.columns_hash["wealth"].default + assert_equal false, person_klass.columns_hash["wealth"].null + + # rename column to see that column doesn't lose its not null and/or default definition + person_klass.connection.rename_column "testings", "wealth", "money" + person_klass.reset_column_information + assert_nil person_klass.columns_hash["wealth"] + assert_equal 100, person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + + # change column + person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000 + person_klass.reset_column_information + assert_equal 1000, person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + + # change column, make it nullable and clear default + person_klass.connection.change_column "testings", "money", :integer, :null => true, :default => nil + person_klass.reset_column_information + assert_nil person_klass.columns_hash["money"].default + assert_equal true, person_klass.columns_hash["money"].null + + # change_column_null, make it not nullable and set null values to a default value + person_klass.connection.execute('UPDATE testings SET money = NULL') + person_klass.connection.change_column_null "testings", "money", false, 2000 + person_klass.reset_column_information + assert_nil person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + assert_equal [2000], Person.connection.select_values("SELECT money FROM testings").map { |s| s.to_i }.sort + ensure + Person.connection.drop_table :testings rescue nil + end + def test_change_column_default_to_null Person.connection.change_column_default "people", "first_name", nil Person.reset_column_information diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index eb3e43c8ac..7c3e0f2ca6 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -57,4 +57,29 @@ class MultipleDbTest < ActiveRecord::TestCase assert Course.connection end + + def test_transactions_across_databases + c1 = Course.find(1) + e1 = Entrant.find(1) + + begin + Course.transaction do + Entrant.transaction do + c1.name = "Typo" + e1.name = "Typo" + c1.save + e1.save + raise "No I messed up." + end + end + rescue + # Yup caught it + end + + assert_equal "Typo", c1.name + assert_equal "Typo", e1.name + + assert_equal "Ruby Development", Course.find(1).name + assert_equal "Ruby Developer", Entrant.find(1).name + end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 0c1eb23428..e21ffbbdba 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -184,6 +184,28 @@ class NamedScopeTest < ActiveRecord::TestCase end end + def test_any_should_not_load_results + topics = Topic.base + assert_queries(1) do + topics.expects(:empty?).returns(true) + assert !topics.any? + end + end + + def test_any_should_call_proxy_found_if_using_a_block + topics = Topic.base + assert_queries(1) do + topics.expects(:empty?).never + topics.any? { true } + end + end + + def test_any_should_not_fire_query_if_named_scope_loaded + topics = Topic.base + topics.collect # force load + assert_no_queries { assert topics.any? } + end + def test_should_build_with_proxy_options topic = Topic.approved.build({}) assert topic.approved diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb new file mode 100644 index 0000000000..86834fe920 --- /dev/null +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -0,0 +1,623 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class ActiveRecordValidationsI18nTests < Test::Unit::TestCase + def setup + reset_callbacks Topic + @topic = Topic.new + I18n.backend.store_translations('en-US', :active_record => {:error_messages => {:custom => nil}}) + end + + def teardown + reset_callbacks Topic + load 'active_record/locale/en-US.rb' + end + + def unique_topic + @unique ||= Topic.create :title => 'unique!' + end + + def replied_topic + @replied_topic ||= begin + topic = Topic.create(:title => "topic") + topic.replies << Reply.new + topic + end + end + + def reset_callbacks(*models) + models.each do |model| + model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new) + end + end + + def test_default_error_messages_is_deprecated + assert_deprecated('ActiveRecord::Errors.default_error_messages') do + ActiveRecord::Errors.default_error_messages + end + end + + # ActiveRecord::Errors + uses_mocha 'ActiveRecord::Errors' do + def test_errors_generate_message_translates_custom_model_attribute_key + global_scope = [:active_record, :error_messages] + custom_scope = global_scope + [:custom, 'topic', :title] + + I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid] + @topic.errors.generate_message :title, :invalid, :default => 'default from class def' + end + + def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti + custom_scope = [:active_record, :error_messages, :custom, 'topic', :title] + + I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid] + Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def' + 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.instance_variable_set :@errors, { 'title' => 'empty' } + I18n.expects(:translate).with(:"active_record.human_attribute_names.topic.title", :locale => 'en-US', :default => 'Title').returns('Title') + @topic.errors.full_messages :locale => 'en-US' + end + end + + # ActiveRecord::Validations + uses_mocha 'ActiveRecord::Validations' do + # 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_uniqueness_of w/ mocha + + def test_validates_uniqueness_of_generates_message + Topic.validates_uniqueness_of :title + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil}) + @topic.valid? + end + + def test_validates_uniqueness_of_generates_message_with_custom_default_message + Topic.validates_uniqueness_of :title, :message => 'custom' + @topic.title = unique_topic.title + @topic.errors.expects(:generate_message).with(:title, :taken, {: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_associated w/ mocha + + def test_validates_associated_generates_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + + def test_validates_associated_generates_message_with_custom_default_message + Topic.validates_associated :replies + replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil}) + replied_topic.valid? + end + end + + # validates_confirmation_of w/o mocha + + def test_validates_confirmation_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + + Topic.validates_confirmation_of :title + @topic.title_confirmation = 'foo' + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_confirmation_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}} + + Topic.validates_confirmation_of :title + @topic.title_confirmation = 'foo' + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + # validates_acceptance_of w/o mocha + + def test_validates_acceptance_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + + Topic.validates_acceptance_of :title, :allow_nil => false + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_acceptance_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}} + + Topic.validates_acceptance_of :title, :allow_nil => false + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + # validates_presence_of w/o mocha + + def test_validates_presence_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + + Topic.validates_presence_of :title + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_presence_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}} + + Topic.validates_presence_of :title + @topic.valid? + assert_equal 'global message', @topic.errors.on(: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-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + + Topic.validates_length_of :title, :within => 3..5 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}} + + Topic.validates_length_of :title, :within => 3..5 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + # validates_length_of :is w/o mocha + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + # validates_uniqueness_of w/o mocha + + def test_validates_length_of_within_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_length_of_within_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}} + + Topic.validates_length_of :title, :is => 5 + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + + # validates_format_of w/o mocha + + def test_validates_format_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_format_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/ + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + # validates_inclusion_of w/o mocha + + def test_validates_inclusion_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + + Topic.validates_inclusion_of :title, :in => %w(a b c) + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_inclusion_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}} + + Topic.validates_inclusion_of :title, :in => %w(a b c) + @topic.valid? + assert_equal 'global message', @topic.errors.on(:title) + end + + # validates_exclusion_of w/o mocha + + def test_validates_exclusion_of_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(:title) + end + + def test_validates_exclusion_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(: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-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}} + + Topic.validates_numericality_of :title + @topic.title = 'a' + @topic.valid? + assert_equal 'custom message', @topic.errors.on(:title) + end + + def test_validates_numericality_of_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(: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-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(:title) + end + + def test_validates_numericality_of_only_integer_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(: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-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(:title) + end + + def test_validates_numericality_of_odd_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(: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-US', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(:title) + end + + def test_validates_numericality_of_less_than_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_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.on(:title) + end + + + # validates_associated w/o mocha + + def test_validates_associated_finds_custom_model_key_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}} + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal 'custom message', replied_topic.errors.on(:replies) + end + + def test_validates_associated_finds_global_default_translation + I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}} + + Topic.validates_associated :replies + replied_topic.valid? + assert_equal 'global message', replied_topic.errors.on(:replies) + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 0742e2c632..4b2d28c80b 100755 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -477,6 +477,15 @@ class ValidationsTest < ActiveRecord::TestCase assert_not_equal "has already been taken", t3.errors.on(:title) end + def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer + Topic.validates_uniqueness_of(:title, :case_sensitve => true) + t = Topic.create!('title' => 101) + + t2 = Topic.new('title' => 101) + assert !t2.valid? + assert t2.errors.on(:title) + end + def test_validate_uniqueness_with_non_standard_table_names i1 = WarehouseThing.create(:value => 1000) assert !i1.valid?, "i1 should not be valid" @@ -853,7 +862,9 @@ class ValidationsTest < ActiveRecord::TestCase end def test_validates_length_with_globally_modified_error_message - ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' + ActiveSupport::Deprecation.silence do + ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' + end Topic.validates_length_of :title, :minimum => 10 t = Topic.create(:title => 'too short') assert !t.valid? diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 82e069005a..136dc39cf3 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,5 +1,5 @@ class Author < ActiveRecord::Base - has_many :posts + has_many :posts, :accessible => true has_many :posts_with_comments, :include => :comments, :class_name => "Post" has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id' has_many :posts_with_categories, :include => :categories, :class_name => "Post" diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index e6aa810146..cd435948a1 100755 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -18,6 +18,13 @@ end module Namespaced class Company < ::Company end + + class Firm < ::Company + has_many :clients, :class_name => 'Namespaced::Client' + end + + class Client < ::Company + end end class Firm < Company diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 3adbc0ce1f..e23818eb33 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -33,6 +33,12 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' + belongs_to :creatable_author, :class_name => 'Author', :accessible => true + has_one :uncreatable_comment, :class_name => 'Comment', :accessible => false, :order => 'id desc' + has_one :creatable_comment, :class_name => 'Comment', :accessible => true, :order => 'id desc' + has_many :creatable_comments, :class_name => 'Comment', :accessible => true, :dependent => :destroy + has_and_belongs_to_many :creatable_categories, :class_name => 'Category', :accessible => true + has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 29c91a4464..487a08f4ab 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -66,6 +66,7 @@ ActiveRecord::Schema.define do create_table :categories, :force => true do |t| t.string :name, :null => false t.string :type + t.integer :categorizations_count end create_table :categories_posts, :force => true, :id => false do |t| |