activerecord 1.10.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (84) hide show
  1. data/CHANGELOG +187 -19
  2. data/RUNNING_UNIT_TESTS +11 -0
  3. data/lib/active_record.rb +3 -1
  4. data/lib/active_record/acts/list.rb +25 -14
  5. data/lib/active_record/acts/nested_set.rb +4 -4
  6. data/lib/active_record/acts/tree.rb +18 -1
  7. data/lib/active_record/associations.rb +90 -17
  8. data/lib/active_record/associations/association_collection.rb +44 -5
  9. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +17 -4
  10. data/lib/active_record/associations/has_many_association.rb +13 -3
  11. data/lib/active_record/associations/has_one_association.rb +19 -0
  12. data/lib/active_record/base.rb +292 -268
  13. data/lib/active_record/callbacks.rb +14 -14
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +137 -75
  15. data/lib/active_record/connection_adapters/db2_adapter.rb +10 -8
  16. data/lib/active_record/connection_adapters/mysql_adapter.rb +91 -64
  17. data/lib/active_record/connection_adapters/oci_adapter.rb +6 -6
  18. data/lib/active_record/connection_adapters/postgresql_adapter.rb +113 -60
  19. data/lib/active_record/connection_adapters/sqlite_adapter.rb +15 -12
  20. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +159 -132
  21. data/lib/active_record/fixtures.rb +59 -12
  22. data/lib/active_record/locking.rb +10 -9
  23. data/lib/active_record/migration.rb +112 -5
  24. data/lib/active_record/query_cache.rb +64 -0
  25. data/lib/active_record/timestamp.rb +10 -8
  26. data/lib/active_record/validations.rb +121 -26
  27. data/rakefile +16 -10
  28. data/test/aaa_create_tables_test.rb +26 -48
  29. data/test/abstract_unit.rb +3 -0
  30. data/test/aggregations_test.rb +19 -19
  31. data/test/association_callbacks_test.rb +110 -0
  32. data/test/associations_go_eager_test.rb +48 -14
  33. data/test/associations_test.rb +344 -142
  34. data/test/base_test.rb +150 -31
  35. data/test/binary_test.rb +7 -0
  36. data/test/callbacks_test.rb +24 -5
  37. data/test/column_alias_test.rb +2 -2
  38. data/test/connections/native_sqlserver_odbc/connection.rb +26 -0
  39. data/test/deprecated_associations_test.rb +27 -28
  40. data/test/deprecated_finder_test.rb +8 -9
  41. data/test/finder_test.rb +52 -17
  42. data/test/fixtures/author.rb +39 -0
  43. data/test/fixtures/categories.yml +7 -0
  44. data/test/fixtures/categories_posts.yml +8 -0
  45. data/test/fixtures/category.rb +2 -0
  46. data/test/fixtures/comment.rb +3 -1
  47. data/test/fixtures/comments.yml +43 -1
  48. data/test/fixtures/companies.yml +14 -0
  49. data/test/fixtures/company.rb +1 -1
  50. data/test/fixtures/computers.yml +2 -1
  51. data/test/fixtures/db_definitions/db2.sql +7 -2
  52. data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
  53. data/test/fixtures/db_definitions/mysql.sql +11 -6
  54. data/test/fixtures/db_definitions/oci.sql +7 -2
  55. data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
  56. data/test/fixtures/db_definitions/postgresql.sql +8 -5
  57. data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
  58. data/test/fixtures/db_definitions/sqlite.sql +9 -4
  59. data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
  60. data/test/fixtures/db_definitions/sqlserver.sql +12 -7
  61. data/test/fixtures/developer.rb +8 -1
  62. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  63. data/test/fixtures/post.rb +8 -2
  64. data/test/fixtures/posts.yml +21 -0
  65. data/test/fixtures/project.rb +14 -1
  66. data/test/fixtures/subscriber.rb +3 -0
  67. data/test/fixtures_test.rb +14 -0
  68. data/test/inheritance_test.rb +30 -22
  69. data/test/lifecycle_test.rb +3 -4
  70. data/test/locking_test.rb +2 -4
  71. data/test/migration_test.rb +186 -0
  72. data/test/mixin_nested_set_test.rb +19 -19
  73. data/test/mixin_test.rb +88 -88
  74. data/test/modules_test.rb +5 -10
  75. data/test/multiple_db_test.rb +2 -0
  76. data/test/pk_test.rb +8 -12
  77. data/test/reflection_test.rb +8 -4
  78. data/test/schema_test_postgresql.rb +63 -0
  79. data/test/thread_safety_test.rb +4 -1
  80. data/test/transactions_test.rb +9 -2
  81. data/test/unconnected_test.rb +1 -0
  82. data/test/validations_test.rb +151 -8
  83. metadata +11 -5
  84. data/test/migration_mysql.rb +0 -104
@@ -101,8 +101,8 @@ require 'csv'
101
101
  # ...
102
102
  #
103
103
  # By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here tho), we trigger
104
- # the testing environment to automatically load the appropriate fixtures into the database before each test, and
105
- # automatically delete them after each test.
104
+ # the testing environment to automatically load the appropriate fixtures into the database before each test.
105
+ # To ensure consistent data, the environment deletes the fixtures before running the load.
106
106
  #
107
107
  # In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
108
108
  # of the test case. It is named after the symbol... so, in our example, there would be a hash available called
@@ -129,6 +129,16 @@ require 'csv'
129
129
  # - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
130
130
  # self.use_instantiated_fixtures = :no_instances
131
131
  #
132
+ # Even if auto-instantiated fixtures are disabled, you can still access them
133
+ # by name via special dynamic methods. Each method has the same name as the
134
+ # model, and accepts the name of the fixture to instantiate:
135
+ #
136
+ # fixtures :web_sites
137
+ #
138
+ # def test_find
139
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
140
+ # end
141
+ #
132
142
  # = Dynamic fixtures with ERb
133
143
  #
134
144
  # Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
@@ -159,13 +169,13 @@ require 'csv'
159
169
  # fixtures :foos
160
170
  #
161
171
  # def test_godzilla
162
- # assert !Foo.find_all.emtpy?
172
+ # assert !Foo.find(:all).empty?
163
173
  # Foo.destroy_all
164
- # assert Foo.find_all.emtpy?
174
+ # assert Foo.find(:all).empty?
165
175
  # end
166
176
  #
167
177
  # def test_godzilla_aftermath
168
- # assert !Foo.find_all.emtpy?
178
+ # assert !Foo.find(:all).empty?
169
179
  # end
170
180
  # end
171
181
  #
@@ -278,7 +288,7 @@ class Fixtures < Hash
278
288
  yaml = YAML::load(erb_render(IO.read(yaml_file_path)))
279
289
  yaml.each { |name, data| self[name] = Fixture.new(data, @class_name) } if yaml
280
290
  rescue Exception=>boom
281
- raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html"
291
+ raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{boom.class}: #{boom}"
282
292
  end
283
293
  elsif File.file?(csv_file_path)
284
294
  # CSV fixtures
@@ -400,9 +410,13 @@ module Test #:nodoc:
400
410
  self.use_instantiated_fixtures = true
401
411
  self.pre_loaded_fixtures = false
402
412
 
413
+ @@already_loaded_fixtures = {}
414
+
403
415
  def self.fixtures(*table_names)
404
- self.fixture_table_names |= table_names.flatten
405
- require_fixture_classes
416
+ table_names = table_names.flatten
417
+ self.fixture_table_names |= table_names
418
+ require_fixture_classes(table_names)
419
+ setup_fixture_accessors(table_names)
406
420
  end
407
421
 
408
422
  def self.require_fixture_classes(table_names=nil)
@@ -415,20 +429,53 @@ module Test #:nodoc:
415
429
  end
416
430
  end
417
431
 
432
+ def self.setup_fixture_accessors(table_names=nil)
433
+ (table_names || fixture_table_names).each do |table_name|
434
+ table_name = table_name.to_s.tr('.','_')
435
+ define_method(table_name) do |fixture, *optionals|
436
+ force_reload = optionals.shift
437
+ @fixture_cache[table_name] ||= Hash.new
438
+ @fixture_cache[table_name][fixture] = nil if force_reload
439
+ @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
440
+ end
441
+ end
442
+ end
443
+
444
+ def self.uses_transaction(*methods)
445
+ @uses_transaction ||= []
446
+ @uses_transaction.concat methods.map { |m| m.to_s }
447
+ end
448
+
449
+ def self.uses_transaction?(method)
450
+ @uses_transaction && @uses_transaction.include?(method.to_s)
451
+ end
452
+
453
+ def use_transactional_fixtures?
454
+ use_transactional_fixtures &&
455
+ !self.class.uses_transaction?(method_name)
456
+ end
457
+
418
458
  def setup_with_fixtures
419
459
  if pre_loaded_fixtures && !use_transactional_fixtures
420
460
  raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
421
461
  end
422
462
 
463
+ @fixture_cache = Hash.new
464
+
423
465
  # Load fixtures once and begin transaction.
424
- if use_transactional_fixtures
425
- load_fixtures unless @already_loaded_fixtures
426
- @already_loaded_fixtures = true
466
+ if use_transactional_fixtures?
467
+ if @@already_loaded_fixtures[self.class]
468
+ @loaded_fixtures = @@already_loaded_fixtures[self.class]
469
+ else
470
+ load_fixtures
471
+ @@already_loaded_fixtures[self.class] = @loaded_fixtures
472
+ end
427
473
  ActiveRecord::Base.lock_mutex
428
474
  ActiveRecord::Base.connection.begin_db_transaction
429
475
 
430
476
  # Load fixtures for every test.
431
477
  else
478
+ @@already_loaded_fixtures[self.class] = nil
432
479
  load_fixtures
433
480
  end
434
481
 
@@ -440,7 +487,7 @@ module Test #:nodoc:
440
487
 
441
488
  def teardown_with_fixtures
442
489
  # Rollback changes.
443
- if use_transactional_fixtures
490
+ if use_transactional_fixtures?
444
491
  ActiveRecord::Base.connection.rollback_db_transaction
445
492
  ActiveRecord::Base.unlock_mutex
446
493
  end
@@ -32,14 +32,15 @@ module ActiveRecord
32
32
  previous_value = self.lock_version
33
33
  self.lock_version = previous_value + 1
34
34
 
35
- affected_rows = connection.update(
36
- "UPDATE #{self.class.table_name} "+
37
- "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
38
- "WHERE #{self.class.primary_key} = #{quote(id)} AND lock_version = #{quote(previous_value)}",
39
- "#{self.class.name} Update with optimistic locking"
40
- )
41
-
42
- raise(ActiveRecord::StaleObjectError, "Attempted to update a stale object") unless affected_rows == 1
35
+ affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
36
+ UPDATE #{self.class.table_name}
37
+ SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
38
+ WHERE #{self.class.primary_key} = #{quote(id)} AND lock_version = #{quote(previous_value)}
39
+ end_sql
40
+
41
+ unless affected_rows == 1
42
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
43
+ end
43
44
  else
44
45
  update_without_lock
45
46
  end
@@ -54,4 +55,4 @@ module ActiveRecord
54
55
  lock_optimistically && respond_to?(:lock_version)
55
56
  end
56
57
  end
57
- end
58
+ end
@@ -2,7 +2,113 @@ module ActiveRecord
2
2
  class IrreversibleMigration < ActiveRecordError#:nodoc:
3
3
  end
4
4
 
5
- class Migration #:nodoc:
5
+ # Migrations can manage the evolution of a schema used by several physical databases. It's a solution
6
+ # to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
7
+ # push that change to other developers and to the production server. With migrations, you can describe the transformations
8
+ # in self-contained classes that can be checked into version control systems and executed against another database that
9
+ # might be one, two, or five versions behind.
10
+ #
11
+ # Example of a simple migration:
12
+ #
13
+ # class AddSsl < ActiveRecord::Migration
14
+ # def self.up
15
+ # add_column :accounts, :ssl_enabled, :boolean, :default => 1
16
+ # end
17
+ #
18
+ # def self.down
19
+ # remove_column :accounts, :ssl_enabled
20
+ # end
21
+ # end
22
+ #
23
+ # This migration will add a boolean flag to the accounts table and remove it again, if you're backing out of the migration.
24
+ # It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
25
+ # or remove the migration. These methods can consist of both the migration specific methods, like add_column and remove_column,
26
+ # but may also contain regular Ruby code for generating data needed for the transformations.
27
+ #
28
+ # Example of a more complex migration that also needs to initialize data:
29
+ #
30
+ # class AddSystemSettings < ActiveRecord::Migration
31
+ # def self.up
32
+ # create_table :system_settings do |t|
33
+ # t.column :name, :string
34
+ # t.column :label, :string
35
+ # t.column :value, :text
36
+ # t.column :type, :string
37
+ # t.column :position, :integer
38
+ # end
39
+ #
40
+ # SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
41
+ # end
42
+ #
43
+ # def self.down
44
+ # drop_table :system_settings
45
+ # end
46
+ # end
47
+ #
48
+ # This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
49
+ # that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
50
+ # in one block call.
51
+ #
52
+ # == Available transformations
53
+ #
54
+ # * <tt>create_table(name, options)</tt> Creates a table called +name+ and makes the table object available to a block
55
+ # that can then add columns to it, following the same format as add_column. See example above. The options hash is for
56
+ # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
57
+ # * <tt>drop_table(name)</tt>: Drops the table called +name+.
58
+ # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
59
+ # named +column_name+ specified to be one of the following types:
60
+ # :string, :text, :integer, :float, :datetime, :timestamp, :time, :date, :binary, :boolean. A default value can be specified
61
+ # by passing an +options+ hash like { :default => 11 }.
62
+ # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
63
+ # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
64
+ # parameters as add_column.
65
+ # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
66
+ # * <tt>add_index(table_name, column_name)</tt>: Add a new index with the name of the column on the column.
67
+ # * <tt>remove_index(table_name, column_name)</tt>: Remove the index called the same as the column.
68
+ #
69
+ # == Irreversible transformations
70
+ #
71
+ # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
72
+ # an <tt>IrreversibleMigration</tt> exception in their +down+ method.
73
+ #
74
+ # == Running migrations from within Rails
75
+ #
76
+ # The Rails package has support for migrations with the <tt>script/generate migration my_new_migration</tt> command and
77
+ # with the <tt>rake migrate</tt> command that'll run all the pending migrations. It'll even create the needed schema_info
78
+ # table automatically if it's missing.
79
+ #
80
+ # == Database support
81
+ #
82
+ # Migrations are currently only supported in MySQL and PostgreSQL.
83
+ #
84
+ # == More examples
85
+ #
86
+ # Not all migrations change the schema. Some just fix the data:
87
+ #
88
+ # class RemoveEmptyTags < ActiveRecord::Migration
89
+ # def self.up
90
+ # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
91
+ # end
92
+ #
93
+ # def self.down
94
+ # # not much we can do to restore deleted data
95
+ # end
96
+ # end
97
+ #
98
+ # Others remove columns when they migrate up instead of down:
99
+ #
100
+ # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
101
+ # def self.up
102
+ # remove_column :items, :incomplete_items_count
103
+ # remove_column :items, :completed_items_count
104
+ # end
105
+ #
106
+ # def self.down
107
+ # add_column :items, :incomplete_items_count
108
+ # add_column :items, :completed_items_count
109
+ # end
110
+ # end
111
+ class Migration
6
112
  class << self
7
113
  def up() end
8
114
  def down() end
@@ -17,11 +123,11 @@ module ActiveRecord
17
123
  class Migrator#:nodoc:
18
124
  class << self
19
125
  def up(migrations_path, target_version = nil)
20
- new(:up, migrations_path, target_version).migrate
126
+ self.new(:up, migrations_path, target_version).migrate
21
127
  end
22
128
 
23
129
  def down(migrations_path, target_version = nil)
24
- new(:down, migrations_path, target_version).migrate
130
+ self.new(:down, migrations_path, target_version).migrate
25
131
  end
26
132
 
27
133
  def current_version
@@ -30,6 +136,7 @@ module ActiveRecord
30
136
  end
31
137
 
32
138
  def initialize(direction, migrations_path, target_version = nil)
139
+ raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
33
140
  @direction, @migrations_path, @target_version = direction, migrations_path, target_version
34
141
  Base.connection.initialize_schema_information
35
142
  end
@@ -59,7 +166,7 @@ module ActiveRecord
59
166
  end
60
167
 
61
168
  def migration_files
62
- files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
169
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort
63
170
  down? ? files.reverse : files
64
171
  end
65
172
 
@@ -91,4 +198,4 @@ module ActiveRecord
91
198
  (up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
92
199
  end
93
200
  end
94
- end
201
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveRecord
2
+ class QueryCache #:nodoc:
3
+ def initialize(connection)
4
+ @connection = connection
5
+ @query_cache = {}
6
+ end
7
+
8
+ def clear_query_cache
9
+ @query_cache = {}
10
+ end
11
+
12
+ def select_all(sql, name = nil)
13
+ @query_cache[sql] ||= @connection.select_all(sql, name)
14
+ end
15
+
16
+ def select_one(sql, name = nil)
17
+ @query_cache[sql] ||= @connection.select_one(sql, name)
18
+ end
19
+
20
+ def columns(table_name, name = nil)
21
+ @query_cache["SHOW FIELDS FROM #{table_name}"] ||= @connection.columns(table_name, name)
22
+ end
23
+
24
+ def insert(sql, name = nil, pk = nil, id_value = nil)
25
+ clear_query_cache
26
+ @connection.insert(sql, name, pk, id_value)
27
+ end
28
+
29
+ def update(sql, name = nil)
30
+ clear_query_cache
31
+ @connection.update(sql, name)
32
+ end
33
+
34
+ def delete(sql, name = nil)
35
+ clear_query_cache
36
+ @connection.delete(sql, name)
37
+ end
38
+
39
+ private
40
+ def method_missing(method, *arguments)
41
+ @connection.send(method, *arguments)
42
+ end
43
+ end
44
+
45
+ class Base
46
+ # Set the connection for the class with caching on
47
+ def self.connection=(spec)
48
+ raise ConnectionNotEstablished unless spec
49
+
50
+ conn = spec.config[:query_cache] ?
51
+ QueryCache.new(self.send(spec.adapter_method, spec.config)) :
52
+ self.send(spec.adapter_method, spec.config)
53
+
54
+ Thread.current['active_connections'] ||= {}
55
+ Thread.current['active_connections'][self] = conn
56
+ end
57
+ end
58
+
59
+ class AbstractAdapter #:nodoc:
60
+ # Stub method to be able to treat the connection the same whether the query cache has been turned on or not
61
+ def clear_query_cache
62
+ end
63
+ end
64
+ end
@@ -19,21 +19,23 @@ module ActiveRecord
19
19
  end
20
20
 
21
21
  def create_with_timestamps #:nodoc:
22
+ if record_timestamps
22
23
  t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
23
- write_attribute("created_at", t) if record_timestamps && respond_to?(:created_at) && created_at.nil?
24
- write_attribute("created_on", t) if record_timestamps && respond_to?(:created_on) && created_on.nil?
25
-
26
- write_attribute("updated_at", t) if record_timestamps && respond_to?(:updated_at)
27
- write_attribute("updated_on", t) if record_timestamps && respond_to?(:updated_on)
24
+ write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
25
+ write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
28
26
 
27
+ write_attribute('updated_at', t) if respond_to?(:updated_at)
28
+ write_attribute('updated_on', t) if respond_to?(:updated_on)
29
+ end
29
30
  create_without_timestamps
30
31
  end
31
32
 
32
33
  def update_with_timestamps #:nodoc:
34
+ if record_timestamps
33
35
  t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
34
- write_attribute("updated_at", t) if record_timestamps && respond_to?(:updated_at)
35
- write_attribute("updated_on", t) if record_timestamps && respond_to?(:updated_on)
36
-
36
+ write_attribute('updated_at', t) if respond_to?(:updated_at)
37
+ write_attribute('updated_on', t) if respond_to?(:updated_on)
38
+ end
37
39
  update_without_timestamps
38
40
  end
39
41
  end
@@ -11,10 +11,12 @@ module ActiveRecord
11
11
 
12
12
  @@default_error_messages = {
13
13
  :inclusion => "is not included in the list",
14
+ :exclusion => "is reserved",
14
15
  :invalid => "is invalid",
15
16
  :confirmation => "doesn't match confirmation",
16
17
  :accepted => "must be accepted",
17
18
  :empty => "can't be empty",
19
+ :blank => "can't be blank",
18
20
  :too_long => "is too long (max is %d characters)",
19
21
  :too_short => "is too short (min is %d characters)",
20
22
  :wrong_length => "is the wrong length (should be %d characters)",
@@ -43,7 +45,7 @@ module ActiveRecord
43
45
  @errors[attribute.to_s] << msg
44
46
  end
45
47
 
46
- # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
48
+ # Will add an error message to each of the attributes in +attributes+ that is empty.
47
49
  def add_on_empty(attributes, msg = @@default_error_messages[:empty])
48
50
  for attr in [attributes].flatten
49
51
  value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
@@ -51,6 +53,14 @@ module ActiveRecord
51
53
  add(attr, msg) unless !value.nil? && !is_empty
52
54
  end
53
55
  end
56
+
57
+ # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
58
+ def add_on_blank(attributes, msg = @@default_error_messages[:blank])
59
+ for attr in [attributes].flatten
60
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
61
+ add(attr, msg) if value.blank?
62
+ end
63
+ end
54
64
 
55
65
  # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
56
66
  # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
@@ -203,12 +213,6 @@ module ActiveRecord
203
213
  :message => nil
204
214
  }.freeze
205
215
 
206
- DEFAULT_SIZE_VALIDATION_OPTIONS = DEFAULT_VALIDATION_OPTIONS.merge(
207
- :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
208
- :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
209
- :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
210
- ).freeze
211
-
212
216
  ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
213
217
 
214
218
  def validate(*methods, &block)
@@ -226,6 +230,29 @@ module ActiveRecord
226
230
  write_inheritable_set(:validate_on_update, methods)
227
231
  end
228
232
 
233
+ def condition_block?(condition)
234
+ condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1)
235
+ end
236
+
237
+ # Determine from the given condition (whether a block, procedure, method or string)
238
+ # whether or not to validate the record. See #validates_each.
239
+ def evaluate_condition(condition, record)
240
+ case condition
241
+ when Symbol: record.send(condition)
242
+ when String: eval(condition, binding)
243
+ else
244
+ if condition_block?(condition)
245
+ condition.call(record)
246
+ else
247
+ raise(
248
+ ActiveRecordError,
249
+ "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
250
+ "class implementing a static validation method"
251
+ )
252
+ end
253
+ end
254
+ end
255
+
229
256
  # Validates each attribute against a block.
230
257
  #
231
258
  # class Person < ActiveRecord::Base
@@ -237,16 +264,22 @@ module ActiveRecord
237
264
  # Options:
238
265
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
239
266
  # * <tt>allow_nil</tt> - Skip validation if attribute is nil.
267
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
268
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
269
+ # method, proc or string should return or evaluate to a true or false value.
240
270
  def validates_each(*attrs)
241
271
  options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
242
272
  attrs = attrs.flatten
243
273
 
244
274
  # Declare the validation.
245
275
  send(validation_method(options[:on] || :save)) do |record|
246
- attrs.each do |attr|
247
- value = record.send(attr)
248
- next if value.nil? && options[:allow_nil]
249
- yield record, attr, value
276
+ # Don't validate when there is an :if condition and that condition is false
277
+ unless options[:if] && !evaluate_condition(options[:if], record)
278
+ attrs.each do |attr|
279
+ value = record.send(attr)
280
+ next if value.nil? && options[:allow_nil]
281
+ yield record, attr, value
282
+ end
250
283
  end
251
284
  end
252
285
  end
@@ -270,6 +303,9 @@ module ActiveRecord
270
303
  # Configuration options:
271
304
  # * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
272
305
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
306
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
307
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
308
+ # method, proc or string should return or evaluate to a true or false value.
273
309
  def validates_confirmation_of(*attr_names)
274
310
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
275
311
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
@@ -292,12 +328,13 @@ module ActiveRecord
292
328
  # terms_of_service is not nil and by default on save.
293
329
  #
294
330
  # Configuration options:
295
- # * <tt>message</tt> - A custom error message (default is: "can't be empty")
331
+ # * <tt>message</tt> - A custom error message (default is: "must be accepted")
296
332
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
297
333
  # * <tt>accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
298
334
  # makes it easy to relate to an HTML checkbox.
299
- #
300
-
335
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
336
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
337
+ # method, proc or string should return or evaluate to a true or false value.
301
338
  def validates_acceptance_of(*attr_names)
302
339
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
303
340
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
@@ -309,20 +346,25 @@ module ActiveRecord
309
346
  end
310
347
  end
311
348
 
312
- # Validates that the specified attributes are neither nil nor empty. Happens by default on save.
349
+ # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save.
313
350
  #
314
351
  # Configuration options:
315
- # * <tt>message</tt> - A custom error message (default is: "has already been taken")
352
+ # * <tt>message</tt> - A custom error message (default is: "can't be blank")
316
353
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
354
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
355
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
356
+ # method, proc or string should return or evaluate to a true or false value.
317
357
  def validates_presence_of(*attr_names)
318
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:empty], :on => :save }
358
+ configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
319
359
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
320
360
 
321
361
  # can't use validates_each here, because it cannot cope with non-existant attributes,
322
362
  # while errors.add_on_empty can
323
363
  attr_names.each do |attr_name|
324
364
  send(validation_method(configuration[:on])) do |record|
325
- record.errors.add_on_empty(attr_name,configuration[:message])
365
+ unless configuration[:if] and not evaluate_condition(configuration[:if], record)
366
+ record.errors.add_on_blank(attr_name,configuration[:message])
367
+ end
326
368
  end
327
369
  end
328
370
  end
@@ -351,9 +393,14 @@ module ActiveRecord
351
393
  # * <tt>wrong_length</tt> - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)")
352
394
  # * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
353
395
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
396
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
397
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
398
+ # method, proc or string should return or evaluate to a true or false value.
354
399
  def validates_length_of(*attrs)
355
400
  # Merge given options with defaults.
356
- options = DEFAULT_SIZE_VALIDATION_OPTIONS.dup
401
+ options = {:too_long => ActiveRecord::Errors.default_error_messages[:too_long],
402
+ :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
403
+ :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]}.merge(DEFAULT_VALIDATION_OPTIONS)
357
404
  options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
358
405
 
359
406
  # Ensure that one and only one range option is specified.
@@ -372,15 +419,17 @@ module ActiveRecord
372
419
  option_value = options[range_options.first]
373
420
 
374
421
  # Declare different validations per option.
375
-
376
422
  validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
377
423
  message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
378
424
 
379
425
  case option
380
426
  when :within, :in
381
427
  raise ArgumentError, ':within must be a Range' unless option_value.is_a?(Range) # '
382
- validates_length_of attrs, :minimum => option_value.begin, :allow_nil => options[:allow_nil]
383
- validates_length_of attrs, :maximum => option_value.end, :allow_nil => options[:allow_nil]
428
+ (options_without_range = options.dup).delete(option)
429
+ (options_with_minimum = options_without_range.dup).store(:minimum, option_value.begin)
430
+ validates_length_of attrs, options_with_minimum
431
+ (options_with_maximum = options_without_range.dup).store(:maximum, option_value.end)
432
+ validates_length_of attrs, options_with_maximum
384
433
  when :is, :minimum, :maximum
385
434
  raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 # '
386
435
  message = options[:message] || options[message_options[option]]
@@ -407,17 +456,20 @@ module ActiveRecord
407
456
  # Configuration options:
408
457
  # * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
409
458
  # * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
459
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
460
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
461
+ # method, proc or string should return or evaluate to a true or false value.
410
462
  def validates_uniqueness_of(*attr_names)
411
463
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
412
464
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
413
465
 
414
466
  if scope = configuration[:scope]
415
467
  validates_each(attr_names,configuration) do |record, attr_name, value|
416
- record.errors.add(attr_name, configuration[:message]) if record.class.find_first(record.new_record? ? ["#{attr_name} = ? AND #{scope} = ?", record.send(attr_name), record.send(scope)] : ["#{attr_name} = ? AND #{record.class.primary_key} <> ? AND #{scope} = ?", record.send(attr_name), record.send(:id), record.send(scope)])
468
+ record.errors.add(attr_name, configuration[:message]) if record.class.find(:first, :conditions => (record.new_record? ? ["#{attr_name} = ? AND #{scope} = ?", record.send(attr_name), record.send(scope)] : ["#{attr_name} = ? AND #{record.class.primary_key} <> ? AND #{scope} = ?", record.send(attr_name), record.send(:id), record.send(scope)]))
417
469
  end
418
470
  else
419
471
  validates_each(attr_names,configuration) do |record, attr_name, value|
420
- record.errors.add(attr_name, configuration[:message]) if record.class.find_first(record.new_record? ? ["#{attr_name} = ?", record.send(attr_name)] : ["#{attr_name} = ? AND #{record.class.primary_key} <> ?", record.send(attr_name), record.send(:id) ] )
472
+ record.errors.add(attr_name, configuration[:message]) if record.class.find(:first, :conditions => (record.new_record? ? ["#{attr_name} = ?", record.send(attr_name)] : ["#{attr_name} = ? AND #{record.class.primary_key} <> ?", record.send(attr_name), record.send(:id) ] ))
421
473
  end
422
474
  end
423
475
  end
@@ -435,6 +487,9 @@ module ActiveRecord
435
487
  # * <tt>message</tt> - A custom error message (default is: "is invalid")
436
488
  # * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!)
437
489
  # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
490
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
491
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
492
+ # method, proc or string should return or evaluate to a true or false value.
438
493
  def validates_format_of(*attr_names)
439
494
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
440
495
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
@@ -457,6 +512,9 @@ module ActiveRecord
457
512
  # * <tt>in</tt> - An enumerable object of available items
458
513
  # * <tt>message</tt> - Specifies a customer error message (default is: "is not included in the list")
459
514
  # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
515
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
516
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
517
+ # method, proc or string should return or evaluate to a true or false value.
460
518
  def validates_inclusion_of(*attr_names)
461
519
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
462
520
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
@@ -470,6 +528,33 @@ module ActiveRecord
470
528
  end
471
529
  end
472
530
 
531
+ # Validates that the value of the specified attribute is not in a particular enumerable object.
532
+ #
533
+ # class Person < ActiveRecord::Base
534
+ # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
535
+ # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
536
+ # end
537
+ #
538
+ # Configuration options:
539
+ # * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of
540
+ # * <tt>message</tt> - Specifies a customer error message (default is: "is reserved")
541
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
542
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
543
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
544
+ # method, proc or string should return or evaluate to a true or false value.
545
+ def validates_exclusion_of(*attr_names)
546
+ configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
547
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
548
+
549
+ enum = configuration[:in] || configuration[:within]
550
+
551
+ 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?")
552
+
553
+ validates_each(attr_names, configuration) do |record, attr_name, value|
554
+ record.errors.add(attr_name, configuration[:message]) if enum.include?(value)
555
+ end
556
+ end
557
+
473
558
  # Validates whether the associated object or objects are all themselves valid. Works with any kind of association.
474
559
  #
475
560
  # class Book < ActiveRecord::Base
@@ -487,10 +572,16 @@ module ActiveRecord
487
572
  # validates_associated :book
488
573
  # end
489
574
  #
490
- # this would specify a circular dependency and cause infinite recursion. The Rails team recommends against this practice.
575
+ # ...this would specify a circular dependency and cause infinite recursion.
576
+ #
577
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
578
+ # is both present and guaranteed to be valid, you also need to use validates_presence_of.
491
579
  #
492
580
  # Configuration options:
493
581
  # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
582
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
583
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
584
+ # method, proc or string should return or evaluate to a true or false value.
494
585
  def validates_associated(*attr_names)
495
586
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
496
587
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
@@ -514,6 +605,9 @@ module ActiveRecord
514
605
  # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
515
606
  # * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
516
607
  # * <tt>allow_nil</tt> Skip validation if attribute is nil (default is false). Notice that for fixnum and float columsn empty strings are converted to nil
608
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
609
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
610
+ # method, proc or string should return or evaluate to a true or false value.
517
611
  def validates_numericality_of(*attr_names)
518
612
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save,
519
613
  :only_integer => false, :allow_nil => false }
@@ -525,6 +619,7 @@ module ActiveRecord
525
619
  end
526
620
  else
527
621
  validates_each(attr_names,configuration) do |record, attr_name,value|
622
+ next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil?
528
623
  begin
529
624
  Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s)
530
625
  rescue ArgumentError, TypeError
@@ -558,7 +653,7 @@ module ActiveRecord
558
653
  # Attempts to save the record just like Base.save but will raise a RecordInvalid exception instead of returning false
559
654
  # if the record is not valid.
560
655
  def save!
561
- valid? ? save_without_validation : raise(RecordInvalid)
656
+ valid? ? save(false) : raise(RecordInvalid)
562
657
  end
563
658
 
564
659
  # Updates a single attribute and saves the record without going through the normal validation procedure.