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.
- data/CHANGELOG +187 -19
- data/RUNNING_UNIT_TESTS +11 -0
- data/lib/active_record.rb +3 -1
- data/lib/active_record/acts/list.rb +25 -14
- data/lib/active_record/acts/nested_set.rb +4 -4
- data/lib/active_record/acts/tree.rb +18 -1
- data/lib/active_record/associations.rb +90 -17
- data/lib/active_record/associations/association_collection.rb +44 -5
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +17 -4
- data/lib/active_record/associations/has_many_association.rb +13 -3
- data/lib/active_record/associations/has_one_association.rb +19 -0
- data/lib/active_record/base.rb +292 -268
- data/lib/active_record/callbacks.rb +14 -14
- data/lib/active_record/connection_adapters/abstract_adapter.rb +137 -75
- data/lib/active_record/connection_adapters/db2_adapter.rb +10 -8
- data/lib/active_record/connection_adapters/mysql_adapter.rb +91 -64
- data/lib/active_record/connection_adapters/oci_adapter.rb +6 -6
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +113 -60
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +15 -12
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +159 -132
- data/lib/active_record/fixtures.rb +59 -12
- data/lib/active_record/locking.rb +10 -9
- data/lib/active_record/migration.rb +112 -5
- data/lib/active_record/query_cache.rb +64 -0
- data/lib/active_record/timestamp.rb +10 -8
- data/lib/active_record/validations.rb +121 -26
- data/rakefile +16 -10
- data/test/aaa_create_tables_test.rb +26 -48
- data/test/abstract_unit.rb +3 -0
- data/test/aggregations_test.rb +19 -19
- data/test/association_callbacks_test.rb +110 -0
- data/test/associations_go_eager_test.rb +48 -14
- data/test/associations_test.rb +344 -142
- data/test/base_test.rb +150 -31
- data/test/binary_test.rb +7 -0
- data/test/callbacks_test.rb +24 -5
- data/test/column_alias_test.rb +2 -2
- data/test/connections/native_sqlserver_odbc/connection.rb +26 -0
- data/test/deprecated_associations_test.rb +27 -28
- data/test/deprecated_finder_test.rb +8 -9
- data/test/finder_test.rb +52 -17
- data/test/fixtures/author.rb +39 -0
- data/test/fixtures/categories.yml +7 -0
- data/test/fixtures/categories_posts.yml +8 -0
- data/test/fixtures/category.rb +2 -0
- data/test/fixtures/comment.rb +3 -1
- data/test/fixtures/comments.yml +43 -1
- data/test/fixtures/companies.yml +14 -0
- data/test/fixtures/company.rb +1 -1
- data/test/fixtures/computers.yml +2 -1
- data/test/fixtures/db_definitions/db2.sql +7 -2
- data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
- data/test/fixtures/db_definitions/mysql.sql +11 -6
- data/test/fixtures/db_definitions/oci.sql +7 -2
- data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
- data/test/fixtures/db_definitions/postgresql.sql +8 -5
- data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlite.sql +9 -4
- data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlserver.sql +12 -7
- data/test/fixtures/developer.rb +8 -1
- data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
- data/test/fixtures/post.rb +8 -2
- data/test/fixtures/posts.yml +21 -0
- data/test/fixtures/project.rb +14 -1
- data/test/fixtures/subscriber.rb +3 -0
- data/test/fixtures_test.rb +14 -0
- data/test/inheritance_test.rb +30 -22
- data/test/lifecycle_test.rb +3 -4
- data/test/locking_test.rb +2 -4
- data/test/migration_test.rb +186 -0
- data/test/mixin_nested_set_test.rb +19 -19
- data/test/mixin_test.rb +88 -88
- data/test/modules_test.rb +5 -10
- data/test/multiple_db_test.rb +2 -0
- data/test/pk_test.rb +8 -12
- data/test/reflection_test.rb +8 -4
- data/test/schema_test_postgresql.rb +63 -0
- data/test/thread_safety_test.rb +4 -1
- data/test/transactions_test.rb +9 -2
- data/test/unconnected_test.rb +1 -0
- data/test/validations_test.rb +151 -8
- metadata +11 -5
- 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
|
105
|
-
#
|
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.
|
172
|
+
# assert !Foo.find(:all).empty?
|
163
173
|
# Foo.destroy_all
|
164
|
-
# assert Foo.
|
174
|
+
# assert Foo.find(:all).empty?
|
165
175
|
# end
|
166
176
|
#
|
167
177
|
# def test_godzilla_aftermath
|
168
|
-
# assert !Foo.
|
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
|
-
|
405
|
-
|
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
|
-
|
426
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
35
|
-
|
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
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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: "
|
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
|
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: "
|
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[:
|
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
|
-
|
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 =
|
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
|
-
|
383
|
-
|
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.
|
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.
|
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.
|
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? ?
|
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.
|