activerecord 1.4.0 → 1.5.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 (55) hide show
  1. data/CHANGELOG +98 -0
  2. data/install.rb +1 -0
  3. data/lib/active_record.rb +1 -0
  4. data/lib/active_record/acts/list.rb +19 -16
  5. data/lib/active_record/associations.rb +164 -164
  6. data/lib/active_record/associations/association_collection.rb +44 -71
  7. data/lib/active_record/associations/association_proxy.rb +76 -0
  8. data/lib/active_record/associations/belongs_to_association.rb +74 -0
  9. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +34 -21
  10. data/lib/active_record/associations/has_many_association.rb +34 -30
  11. data/lib/active_record/associations/has_one_association.rb +48 -0
  12. data/lib/active_record/base.rb +62 -18
  13. data/lib/active_record/callbacks.rb +17 -8
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +11 -10
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -0
  16. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -1
  17. data/lib/active_record/connection_adapters/sqlite_adapter.rb +94 -73
  18. data/lib/active_record/deprecated_associations.rb +46 -8
  19. data/lib/active_record/fixtures.rb +1 -1
  20. data/lib/active_record/observer.rb +5 -1
  21. data/lib/active_record/support/binding_of_caller.rb +72 -68
  22. data/lib/active_record/support/breakpoint.rb +526 -524
  23. data/lib/active_record/support/class_inheritable_attributes.rb +105 -29
  24. data/lib/active_record/support/core_ext.rb +1 -0
  25. data/lib/active_record/support/core_ext/hash.rb +5 -0
  26. data/lib/active_record/support/core_ext/hash/keys.rb +35 -0
  27. data/lib/active_record/support/core_ext/numeric.rb +7 -0
  28. data/lib/active_record/support/core_ext/numeric/bytes.rb +33 -0
  29. data/lib/active_record/support/core_ext/numeric/time.rb +59 -0
  30. data/lib/active_record/support/core_ext/string.rb +5 -0
  31. data/lib/active_record/support/core_ext/string/inflections.rb +41 -0
  32. data/lib/active_record/support/dependencies.rb +1 -14
  33. data/lib/active_record/support/inflector.rb +6 -6
  34. data/lib/active_record/support/misc.rb +0 -24
  35. data/lib/active_record/validations.rb +34 -1
  36. data/lib/active_record/vendor/mysql411.rb +305 -0
  37. data/rakefile +11 -2
  38. data/test/abstract_unit.rb +1 -2
  39. data/test/associations_test.rb +234 -23
  40. data/test/base_test.rb +50 -1
  41. data/test/callbacks_test.rb +16 -0
  42. data/test/connections/native_mysql/connection.rb +2 -2
  43. data/test/connections/native_sqlite3/connection.rb +34 -0
  44. data/test/deprecated_associations_test.rb +36 -2
  45. data/test/fixtures/company.rb +2 -0
  46. data/test/fixtures/computer.rb +3 -0
  47. data/test/fixtures/computers.yml +3 -0
  48. data/test/fixtures/db_definitions/db2.sql +5 -0
  49. data/test/fixtures/db_definitions/mysql.sql +5 -0
  50. data/test/fixtures/db_definitions/postgresql.sql +5 -0
  51. data/test/fixtures/db_definitions/sqlite.sql +5 -0
  52. data/test/fixtures/db_definitions/sqlserver.sql +5 -1
  53. data/test/fixtures/fixture_database.sqlite +0 -0
  54. data/test/validations_test.rb +21 -0
  55. metadata +22 -2
@@ -271,6 +271,39 @@ module ActiveRecord
271
271
  end
272
272
  end
273
273
  end
274
+
275
+ # Validates whether the associated object or objects are all themselves valid. Works with any kind of assocation.
276
+ #
277
+ # class Book < ActiveRecord::Base
278
+ # has_many :pages
279
+ # belongs_to :library
280
+ #
281
+ # validates_associated :pages, :library
282
+ # end
283
+ #
284
+ # Warning: If, after the above definition, you then wrote:
285
+ #
286
+ # class Page < ActiveRecord::Base
287
+ # belongs_to :book
288
+ #
289
+ # validates_associated :book
290
+ # end
291
+ #
292
+ # this would specify a circular dependency and cause infinite recursion. The Rails team recommends against this practice.
293
+ #
294
+ # Configuration options:
295
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
296
+ def validates_associated(*attr_names)
297
+ configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
298
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
299
+
300
+ for attr_name in attr_names
301
+ class_eval(%(#{validation_method(configuration[:on])} %{
302
+ errors.add("#{attr_name}", "#{configuration[:message]}") unless
303
+ (#{attr_name}.is_a?(Array) ? #{attr_name} : [#{attr_name}]).inject(true){ |memo, record| memo and (record.nil? or record.valid?) }
304
+ }))
305
+ end
306
+ end
274
307
 
275
308
 
276
309
  private
@@ -293,7 +326,7 @@ module ActiveRecord
293
326
  # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
294
327
  # in Base is replaced with this when the validations module is mixed in, which it is by default.
295
328
  def update_attribute_with_validation_skipping(name, value)
296
- @attributes[name] = value
329
+ self[name] = value
297
330
  save(false)
298
331
  end
299
332
 
@@ -0,0 +1,305 @@
1
+ #
2
+ # mysq411.rb - 0.1 - Matt Mower <self@mattmower.com>
3
+ #
4
+ # The native Ruby MySQL client (mysql.rb) by Tomita Masahiro does not (yet) handle the new MySQL
5
+ # protocol introduced in MySQL 4.1.1. This protocol introduces a new authentication scheme as
6
+ # well as modifications to the client/server exchanges themselves.
7
+ #
8
+ # mysql411.rb modifies the Mysql class to add MySQL 4.1.x support. It modifies the connection
9
+ # algorithm to detect a 4.1.1 server and respond with the new authentication scheme, otherwise using
10
+ # the original one. Similarly for the changes to packet structures and field definitions, etc...
11
+ #
12
+ # It redefines serveral methods which behave differently depending upon the server context. The
13
+ # way I have implemented this is to alias the old method, create a new alternative method, and redefine
14
+ # the original method as a selector which calls the appropriate method based upon the server version.
15
+ # There may have been a neater way to do this.
16
+ #
17
+ # In general I've tried not to change the original code any more than necessary, i.e. even where I
18
+ # redefine a method I have made the smallest number of changes possible, rather than rewriting from
19
+ # scratch.
20
+ #
21
+ # *Caveat Lector* This code passes all current ActiveRecord unit tests however this is no guarantee that
22
+ # full & correct MySQL 4.1 support has been achieved.
23
+ #
24
+
25
+ require 'digest/sha1'
26
+
27
+ #
28
+ # Extend the Mysql class to work with MySQL 4.1.1+ servers. After version
29
+ # 4.1.1 the password hashing function (and some other connection details) have
30
+ # changed rendering the previous Mysql class unable to connect:
31
+ #
32
+ #
33
+
34
+ class Mysql
35
+ CLIENT_PROTOCOL_41 = 512
36
+ CLIENT_SECURE_CONNECTION = 32768
37
+
38
+ def real_connect( host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil )
39
+ @server_status = SERVER_STATUS_AUTOCOMMIT
40
+
41
+ if( host == nil || host == "localhost" ) && defined? UNIXSocket
42
+ unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
43
+ sock = UNIXSocket::new( unix_socket )
44
+ @host_info = Error::err( Error::CR_LOCALHOST_CONNECTION )
45
+ @unix_socket = unix_socket
46
+ else
47
+ sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
48
+ @host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
49
+ end
50
+
51
+ @host = host ? host.dup : nil
52
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
53
+ @net = Net::new sock
54
+
55
+ a = read
56
+
57
+ @protocol_version = a.slice!(0)
58
+ @server_version, a = a.split(/\0/,2)
59
+
60
+ # Store the version number components for speedy comparison
61
+ version, ostag = @server_version.split( /-/, 2 )
62
+ @major_ver, @minor_ver, @revision_num = version.split( /\./ ).map { |v| v.to_i }
63
+
64
+ @thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
65
+ if a.size >= 2 then
66
+ @server_capabilities, = a.slice!(0,2).unpack("v")
67
+ end
68
+ if a.size >= 16 then
69
+ @server_language, @server_status = a.unpack("cv")
70
+ end
71
+
72
+ # Set the flags we'll send back to the server
73
+ flag = 0 if flag == nil
74
+ flag |= @client_flag | CLIENT_CAPABILITIES
75
+ flag |= CLIENT_CONNECT_WITH_DB if db
76
+
77
+ if version_meets_minimum?( 4, 1, 1 )
78
+ # In 4.1.1+ the seed comes in two parts which must be combined
79
+ a.slice!( 0, 16 )
80
+ seed_part_2 = a.slice!( 0, 12 );
81
+ @scramble_buff << seed_part_2
82
+
83
+ flag |= CLIENT_FOUND_ROWS
84
+ flag |= CLIENT_PROTOCOL_41
85
+ flag |= CLIENT_SECURE_CONNECTION if @server_capabilities & CLIENT_SECURE_CONNECTION;
86
+
87
+ if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
88
+ @db = db.dup
89
+ end
90
+
91
+ scrambled_password = scramble411( passwd, @scramble_buff, @protocol_version==9 )
92
+ data = make_client_auth_packet_41( flag, user, scrambled_password, db )
93
+ else
94
+ scrambled_password = scramble( passwd, @scramble_buff, @protocol_version == 9 )
95
+ data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scrambled_password
96
+ if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
97
+ data << "\0"+db
98
+ @db = db.dup
99
+ end
100
+ end
101
+
102
+ write data
103
+ read
104
+ self
105
+ end
106
+ alias :connect :real_connect
107
+
108
+ # Pack the authentication information into depending upon whether an initial database has
109
+ # been specified
110
+ def make_client_auth_packet_41( flag, user, password, db )
111
+ if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
112
+ template = "VVcx23a#{user.size+1}cA#{password.size}a#{db.size+1}"
113
+ else
114
+ template = "VVcx23a#{user.size+1}cA#{password.size}x"
115
+ end
116
+
117
+ [ flag, @max_allowed_packet, @server_language, user, password.size, password, db ].pack( template )
118
+ end
119
+
120
+ def version_meets_minimum?( major, minor, revision )
121
+ @major_ver >= major && @minor_ver >= minor && @revision_num >= revision
122
+ end
123
+
124
+ # SERVER: public_seed=create_random_string()
125
+ # send(public_seed)
126
+ #
127
+ # CLIENT: recv(public_seed)
128
+ # hash_stage1=sha1("password")
129
+ # hash_stage2=sha1(hash_stage1)
130
+ # reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
131
+ #
132
+ # #this three steps are done in scramble()
133
+ #
134
+ # send(reply)
135
+ #
136
+ #
137
+ # SERVER: recv(reply)
138
+ # hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
139
+ # candidate_hash2=sha1(hash_stage1)
140
+ # check(candidate_hash2==hash_stage2)
141
+ def scramble411( password, seed, old_ver )
142
+ raise "old version password is not implemented" if old_ver
143
+
144
+ # print "Seed Bytes = "
145
+ # seed.each_byte { |b| print "0x#{b.to_s( 16 )}, " }
146
+ # puts
147
+
148
+ stage1 = Digest::SHA1.digest( password )
149
+ stage2 = Digest::SHA1.digest( stage1 )
150
+
151
+ dgst = Digest::SHA1.new
152
+ dgst << seed
153
+ dgst << stage2
154
+ stage3 = dgst.digest
155
+
156
+ # stage1.zip( stage3 ).map { |a, b| (a ^ b).chr }.join
157
+ scrambled = ( 0 ... stage3.size ).map { |i| stage3[i] ^ stage1[i] }
158
+ scrambled = scrambled.map { |x| x.chr }
159
+ scrambled.join
160
+ end
161
+
162
+ def change_user(user="", passwd="", db="")
163
+ scrambled_password = version_meets_minimum?( 4, 1, 1 ) ? scramble411( passwd, @scramble_buff, @protocol_version==9 ) : scramble( passwd, @scramble_buff, @protocol_version==9 )
164
+ data = user+"\0"+scrambled_password+"\0"+db
165
+ command COM_CHANGE_USER, data
166
+ @user = user
167
+ @passwd = passwd
168
+ @db = db
169
+ end
170
+
171
+ #
172
+ # The 4.1 protocol changed the length of the END packet
173
+ #
174
+ alias_method :old_read_one_row, :read_one_row
175
+
176
+ def read_one_row( field_count )
177
+ if version_meets_minimum?( 4, 1, 1 )
178
+ read_one_row_41( field_count )
179
+ else
180
+ old_read_one_row( field_count )
181
+ end
182
+ end
183
+
184
+ def read_one_row_41( field_count )
185
+ data = read
186
+ return if data[0] == 254 and data.length < 9
187
+ rec = []
188
+ field_count.times do
189
+ len = get_length data
190
+ if len == nil then
191
+ rec << len
192
+ else
193
+ rec << data.slice!(0,len)
194
+ end
195
+ end
196
+ rec
197
+ end
198
+
199
+ #
200
+ # The 4.1 protocol changed the length of the END packet
201
+ #
202
+ alias_method :old_skip_result, :skip_result
203
+
204
+ def skip_result
205
+ if version_meets_minimum?( 4, 1, 1 )
206
+ skip_result_41
207
+ else
208
+ old_skip_result
209
+ end
210
+ end
211
+
212
+ def skip_result_41()
213
+ if @status == :STATUS_USE_RESULT then
214
+ loop do
215
+ data = read
216
+ break if data[0] == 254 and data.length == 1
217
+ end
218
+ @status = :STATUS_READY
219
+ end
220
+ end
221
+
222
+ # The field description structure is changed for the 4.1 protocol passing
223
+ # more data and a different packing form. NOTE: The 4.1 protocol now passes
224
+ # back a "catalog" name for each field which is a new feature. Since AR has
225
+ # nowhere to put it I'm throwing it away. Possibly this is not the best
226
+ # idea?
227
+ #
228
+ alias_method :old_unpack_fields, :unpack_fields
229
+
230
+ def unpack_fields( data, long_flag_protocol )
231
+ if version_meets_minimum?( 4, 1, 1 )
232
+ unpack_fields_41( data, long_flag_protocol )
233
+ else
234
+ old_unpack_fields( data, long_flag_protocol )
235
+ end
236
+ end
237
+
238
+ def unpack_fields_41( data, long_flag_protocol )
239
+ ret = []
240
+
241
+ data.each do |f|
242
+ catalog_name = f[0]
243
+ database_name = f[1]
244
+ table_name_alias = f[2]
245
+ table_name = f[3]
246
+ column_name_alias = f[4]
247
+ column_name = f[5]
248
+
249
+ charset = f[6][0] + f[6][1]*256
250
+ length = f[6][2] + f[6][3]*256 + f[6][4]*256*256 + f[6][5]*256*256*256
251
+ type = f[6][6]
252
+ flags = f[6][7] + f[6][8]*256
253
+ decimals = f[6][9]
254
+ def_value = f[7]
255
+ max_length = 0
256
+
257
+ ret << Field::new(table_name, table_name, column_name, length, type, flags, decimals, def_value, max_length)
258
+ end
259
+ ret
260
+ end
261
+
262
+ # In this instance the read_query_result method in mysql is bound to read 5 field parameters which
263
+ # is expanded to 7 in the 4.1 protocol. So in this case we redefine this entire method in order
264
+ # to write "read_rows 7" instead of "read_rows 5"!
265
+ #
266
+ alias_method :old_read_query_result, :read_query_result
267
+
268
+ def read_query_result
269
+ if version_meets_minimum?( 4, 1, 1 )
270
+ read_query_result_41
271
+ else
272
+ old_read_query_result
273
+ end
274
+ end
275
+
276
+ def read_query_result_41
277
+ data = read
278
+ @field_count = get_length(data)
279
+ if @field_count == nil then # LOAD DATA LOCAL INFILE
280
+ File::open(data) do |f|
281
+ write f.read
282
+ end
283
+ write "" # mark EOF
284
+ data = read
285
+ @field_count = get_length(data)
286
+ end
287
+ if @field_count == 0 then
288
+ @affected_rows = get_length(data, true)
289
+ @insert_id = get_length(data, true)
290
+ if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
291
+ a = data.slice!(0,2)
292
+ @server_status = a[0]+a[1]*256
293
+ end
294
+ if data.size > 0 and get_length(data) then
295
+ @info = data
296
+ end
297
+ else
298
+ @extra_info = get_length(data, true)
299
+ fields = read_rows 7
300
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
301
+ @status = :STATUS_GET_RESULT
302
+ end
303
+ self
304
+ end
305
+ end
data/rakefile CHANGED
@@ -8,7 +8,7 @@ require 'rake/contrib/rubyforgepublisher'
8
8
 
9
9
  PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
10
10
  PKG_NAME = 'activerecord'
11
- PKG_VERSION = '1.4.0' + PKG_BUILD
11
+ PKG_VERSION = '1.5.0' + PKG_BUILD
12
12
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
13
13
 
14
14
  PKG_FILES = FileList[
@@ -17,7 +17,7 @@ PKG_FILES = FileList[
17
17
 
18
18
 
19
19
  desc "Default Task"
20
- task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_postgresql ]
20
+ task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_sqlite3, :test_postgresql ]
21
21
 
22
22
  # Run the unit tests
23
23
 
@@ -45,6 +45,12 @@ Rake::TestTask.new("test_sqlite") { |t|
45
45
  t.verbose = true
46
46
  }
47
47
 
48
+ Rake::TestTask.new("test_sqlite3") { |t|
49
+ t.libs << "test" << "test/connections/native_sqlite3"
50
+ t.pattern = 'test/*_test.rb'
51
+ t.verbose = true
52
+ }
53
+
48
54
  Rake::TestTask.new("test_sqlserver") { |t|
49
55
  t.libs << "test" << "test/connections/native_sqlserver"
50
56
  t.pattern = 'test/*_test.rb'
@@ -99,6 +105,9 @@ spec = Gem::Specification.new do |s|
99
105
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
100
106
  end
101
107
  s.files.delete "test/fixtures/fixture_database.sqlite"
108
+ s.files.delete "test/fixtures/fixture_database_2.sqlite"
109
+ s.files.delete "test/fixtures/fixture_database.sqlite3"
110
+ s.files.delete "test/fixtures/fixture_database_2.sqlite3"
102
111
  s.require_path = 'lib'
103
112
  s.autorequire = 'active_record'
104
113
 
@@ -1,5 +1,4 @@
1
1
  $:.unshift(File.dirname(__FILE__) + '/../lib')
2
- # $:.unshift(File.dirname(__FILE__) + '/fixtures')
3
2
 
4
3
  require 'test/unit'
5
4
  require 'active_record'
@@ -18,4 +17,4 @@ class Test::Unit::TestCase #:nodoc:
18
17
  end
19
18
  end
20
19
 
21
- Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
20
+ Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
@@ -5,6 +5,7 @@ require 'fixtures/project'
5
5
  require 'fixtures/company'
6
6
  require 'fixtures/topic'
7
7
  require 'fixtures/reply'
8
+ require 'fixtures/computer'
8
9
 
9
10
  # Can't declare new classes in test case methods, so tests before that
10
11
  bad_collection_keys = false
@@ -18,19 +19,19 @@ raise "ActiveRecord should have barked on bad collection keys" unless bad_collec
18
19
 
19
20
  class AssociationsTest < Test::Unit::TestCase
20
21
  def setup
21
- create_fixtures "accounts", "companies", "developers", "projects", "developers_projects"
22
+ create_fixtures "accounts", "companies", "developers", "projects", "developers_projects", "computers"
22
23
  @signals37 = Firm.find(1)
23
24
  end
24
25
 
25
26
  def test_force_reload
26
- firm = Firm.new
27
+ firm = Firm.new("name" => "A New Firm, Inc")
27
28
  firm.save
28
29
  firm.clients.each {|c|} # forcing to load all clients
29
30
  assert firm.clients.empty?, "New firm shouldn't have client objects"
30
31
  assert !firm.has_clients?, "New firm shouldn't have clients"
31
32
  assert_equal 0, firm.clients.size, "New firm should have 0 clients"
32
33
 
33
- client = Client.new("firm_id" => firm.id)
34
+ client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
34
35
  client.save
35
36
 
36
37
  assert firm.clients.empty?, "New firm should have cached no client objects"
@@ -71,12 +72,6 @@ class HasOneAssociationsTest < Test::Unit::TestCase
71
72
  def test_has_one
72
73
  assert_equal @signals37.account, Account.find(1)
73
74
  assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
74
- assert @signals37.has_account?, "37signals should have an account"
75
- assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
76
- assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
77
-
78
- assert !Account.find(2).has_firm?, "Unknown isn't linked"
79
- assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
80
75
  end
81
76
 
82
77
  def test_type_mismatch
@@ -99,43 +94,105 @@ class HasOneAssociationsTest < Test::Unit::TestCase
99
94
  assert_nil Account.find(old_account_id).firm_id
100
95
  end
101
96
 
97
+ def test_dependence
98
+ firm = Firm.find(1)
99
+ assert !firm.account.nil?
100
+ firm.destroy
101
+ assert_equal 1, Account.find_all.length
102
+ end
103
+
102
104
  def test_build
103
105
  firm = Firm.new("name" => "GlobalMegaCorp")
104
106
  firm.save
105
-
106
- account = firm.build_account("credit_limit" => 1000)
107
+
108
+ account = firm.account.build("credit_limit" => 1000)
109
+ assert_equal account, firm.account
107
110
  assert account.save
108
111
  assert_equal account, firm.account
109
112
  end
110
113
 
114
+ def test_build_before_child_saved
115
+ firm = Firm.find(1)
116
+
117
+ account = firm.account.build("credit_limit" => 1000)
118
+ assert_equal account, firm.account
119
+ assert account.new_record?
120
+ assert firm.save
121
+ assert_equal account, firm.account
122
+ assert !account.new_record?
123
+ end
124
+
125
+ def test_build_before_either_saved
126
+ firm = Firm.new("name" => "GlobalMegaCorp")
127
+
128
+ account = firm.account.build("credit_limit" => 1000)
129
+ assert_equal account, firm.account
130
+ assert account.new_record?
131
+ assert firm.save
132
+ assert_equal account, firm.account
133
+ assert !account.new_record?
134
+ end
135
+
111
136
  def test_failing_build_association
112
137
  firm = Firm.new("name" => "GlobalMegaCorp")
113
138
  firm.save
114
139
 
115
- account = firm.build_account
140
+ account = firm.account.build
141
+ assert_equal account, firm.account
116
142
  assert !account.save
143
+ assert_equal account, firm.account
117
144
  assert_equal "can't be empty", account.errors.on("credit_limit")
118
145
  end
119
146
 
120
147
  def test_create
121
148
  firm = Firm.new("name" => "GlobalMegaCorp")
122
149
  firm.save
123
- assert_equal firm.create_account("credit_limit" => 1000), firm.account
150
+ assert_equal firm.account.create("credit_limit" => 1000), firm.account
124
151
  end
125
-
126
- def test_dependence
127
- firm = Firm.find(1)
128
- assert !firm.account.nil?
129
- firm.destroy
130
- assert_equal 1, Account.find_all.length
152
+
153
+ def test_create_before_save
154
+ firm = Firm.new("name" => "GlobalMegaCorp")
155
+ assert_equal firm.account.create("credit_limit" => 1000), firm.account
131
156
  end
132
157
 
133
158
  def test_dependence_with_missing_association
134
159
  Account.destroy_all
135
- firm = Firm.find(1)
136
- assert !firm.has_account?
160
+ firm = Firm.find(1)
161
+ assert firm.account.nil?
137
162
  firm.destroy
138
163
  end
164
+
165
+ def test_assignment_before_parent_saved
166
+ firm = Firm.new("name" => "GlobalMegaCorp")
167
+ firm.account = a = Account.find(1)
168
+ assert firm.new_record?
169
+ assert_equal a, firm.account
170
+ assert firm.save
171
+ assert_equal a, firm.account
172
+ assert_equal a, firm.account(true)
173
+ end
174
+
175
+ def test_assignment_before_child_saved
176
+ firm = Firm.find(1)
177
+ firm.account = a = Account.new("credit_limit" => 1000)
178
+ assert !a.new_record?
179
+ assert_equal a, firm.account
180
+ assert_equal a, firm.account
181
+ assert_equal a, firm.account(true)
182
+ end
183
+
184
+ def test_assignment_before_either_saved
185
+ firm = Firm.new("name" => "GlobalMegaCorp")
186
+ firm.account = a = Account.new("credit_limit" => 1000)
187
+ assert firm.new_record?
188
+ assert a.new_record?
189
+ assert_equal a, firm.account
190
+ assert firm.save
191
+ assert !firm.new_record?
192
+ assert !a.new_record?
193
+ assert_equal a, firm.account
194
+ assert_equal a, firm.account(true)
195
+ end
139
196
  end
140
197
 
141
198
 
@@ -256,16 +313,72 @@ class HasManyAssociationsTest < Test::Unit::TestCase
256
313
  assert_equal 3, @signals37.clients_of_firm(true).size
257
314
  end
258
315
 
316
+ def test_adding_before_save
317
+ no_of_firms = Firm.count
318
+ no_of_clients = Client.count
319
+ new_firm = Firm.new("name" => "A New Firm, Inc")
320
+ new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
321
+ new_firm.clients_of_firm << (c = Client.new("name" => "Apple"))
322
+ assert new_firm.new_record?
323
+ assert c.new_record?
324
+ assert_equal 2, new_firm.clients_of_firm.size
325
+ assert_equal no_of_firms, Firm.count # Firm was not saved to database.
326
+ assert_equal no_of_clients, Client.count # Clients were not saved to database.
327
+ assert new_firm.save
328
+ assert !new_firm.new_record?
329
+ assert !c.new_record?
330
+ assert_equal new_firm, c.firm
331
+ assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
332
+ assert_equal no_of_clients+2, Client.count # Clients were saved to database.
333
+ assert_equal 2, new_firm.clients_of_firm.size
334
+ assert_equal 2, new_firm.clients_of_firm(true).size
335
+ end
336
+
337
+ def test_invalid_adding
338
+ firm = Firm.find(1)
339
+ assert !(firm.clients_of_firm << c = Client.new)
340
+ assert c.new_record?
341
+ assert !firm.save
342
+ assert c.new_record?
343
+ end
344
+
345
+ def test_invalid_adding_before_save
346
+ no_of_firms = Firm.count
347
+ no_of_clients = Client.count
348
+ new_firm = Firm.new("name" => "A New Firm, Inc")
349
+ new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
350
+ assert c.new_record?
351
+ assert !c.valid?
352
+ assert new_firm.valid?
353
+ assert !new_firm.save
354
+ assert c.new_record?
355
+ assert new_firm.new_record?
356
+ end
357
+
259
358
  def test_build
260
359
  new_client = @signals37.clients_of_firm.build("name" => "Another Client")
261
360
  assert_equal "Another Client", new_client.name
262
- assert new_client.save
361
+ assert new_client.new_record?
362
+ assert_equal new_client, @signals37.clients_of_firm.last
363
+ assert @signals37.save
364
+ assert !new_client.new_record?
263
365
  assert_equal 2, @signals37.clients_of_firm(true).size
264
366
  end
367
+
368
+ def test_invalid_build
369
+ new_client = @signals37.clients_of_firm.build
370
+ assert new_client.new_record?
371
+ assert !new_client.valid?
372
+ assert_equal new_client, @signals37.clients_of_firm.last
373
+ assert !@signals37.save
374
+ assert new_client.new_record?
375
+ assert_equal 1, @signals37.clients_of_firm(true).size
376
+ end
265
377
 
266
378
  def test_create
267
379
  force_signal37_to_load_all_clients_of_firm
268
380
  new_client = @signals37.clients_of_firm.create("name" => "Another Client")
381
+ assert !new_client.new_record?
269
382
  assert_equal new_client, @signals37.clients_of_firm.last
270
383
  assert_equal new_client, @signals37.clients_of_firm(true).last
271
384
  end
@@ -277,6 +390,14 @@ class HasManyAssociationsTest < Test::Unit::TestCase
277
390
  assert_equal 0, @signals37.clients_of_firm(true).size
278
391
  end
279
392
 
393
+ def test_deleting_before_save
394
+ new_firm = Firm.new("name" => "A New Firm, Inc.")
395
+ new_client = new_firm.clients_of_firm.build("name" => "Another Client")
396
+ assert_equal 1, new_firm.clients_of_firm.size
397
+ new_firm.clients_of_firm.delete(new_client)
398
+ assert_equal 0, new_firm.clients_of_firm.size
399
+ end
400
+
280
401
  def test_deleting_a_collection
281
402
  force_signal37_to_load_all_clients_of_firm
282
403
  @signals37.clients_of_firm.create("name" => "Another Client")
@@ -366,8 +487,10 @@ class HasManyAssociationsTest < Test::Unit::TestCase
366
487
  end
367
488
 
368
489
  class BelongsToAssociationsTest < Test::Unit::TestCase
490
+ fixtures :accounts, :companies, :developers, :projects, :topics
491
+
369
492
  def setup
370
- create_fixtures "accounts", "companies", "developers", "projects", "developers_projects", "topics"
493
+ create_fixtures "developers_projects"
371
494
  @signals37 = Firm.find(1)
372
495
  end
373
496
 
@@ -418,6 +541,62 @@ class BelongsToAssociationsTest < Test::Unit::TestCase
418
541
  assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
419
542
  end
420
543
 
544
+ def test_assignment_before_parent_saved
545
+ client = Client.find_first
546
+ apple = Firm.new("name" => "Apple")
547
+ client.firm = apple
548
+ assert_equal apple, client.firm
549
+ assert apple.new_record?
550
+ assert client.save
551
+ assert apple.save
552
+ assert !apple.new_record?
553
+ assert_equal apple, client.firm
554
+ assert_equal apple, client.firm(true)
555
+ end
556
+
557
+ def test_assignment_before_child_saved
558
+ final_cut = Client.new("name" => "Final Cut")
559
+ firm = Firm.find(1)
560
+ final_cut.firm = firm
561
+ assert final_cut.new_record?
562
+ assert final_cut.save
563
+ assert !final_cut.new_record?
564
+ assert !firm.new_record?
565
+ assert_equal firm, final_cut.firm
566
+ assert_equal firm, final_cut.firm(true)
567
+ end
568
+
569
+ def test_assignment_before_either_saved
570
+ final_cut = Client.new("name" => "Final Cut")
571
+ apple = Firm.new("name" => "Apple")
572
+ final_cut.firm = apple
573
+ assert final_cut.new_record?
574
+ assert apple.new_record?
575
+ assert final_cut.save
576
+ assert !final_cut.new_record?
577
+ assert !apple.new_record?
578
+ assert_equal apple, final_cut.firm
579
+ assert_equal apple, final_cut.firm(true)
580
+ end
581
+
582
+ def test_new_record_with_foreign_key_but_no_object
583
+ c = Client.new("firm_id" => 1)
584
+ assert_equal @first_firm, c.firm_with_basic_id
585
+ end
586
+
587
+ def test_forgetting_the_load_when_foreign_key_enters_late
588
+ c = Client.new
589
+ assert_nil c.firm_with_basic_id
590
+
591
+ c.firm_id = 1
592
+ assert_equal @first_firm, c.firm_with_basic_id
593
+ end
594
+
595
+ def test_field_name_same_as_foreign_key
596
+ computer = Computer.find 1
597
+ assert_not_nil computer.developer, ":foreign key == attribute didn't lock up"
598
+ end
599
+
421
600
  def xtest_counter_cache
422
601
  apple = Firm.create("name" => "Apple")
423
602
  final_cut = apple.clients.create("name" => "Final Cut")
@@ -508,6 +687,38 @@ class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase
508
687
  assert_equal 2, aridridel.projects.size
509
688
  assert_equal 2, aridridel.projects(true).size
510
689
  end
690
+
691
+ def test_habtm_adding_before_save
692
+ no_of_devels = Developer.count
693
+ no_of_projects = Project.count
694
+ aridridel = Developer.new("name" => "Aridridel")
695
+ aridridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
696
+ assert aridridel.new_record?
697
+ assert p.new_record?
698
+ assert aridridel.save
699
+ assert !aridridel.new_record?
700
+ assert_equal no_of_devels+1, Developer.count
701
+ assert_equal no_of_projects+1, Project.count
702
+ assert_equal 2, aridridel.projects.size
703
+ assert_equal 2, aridridel.projects(true).size
704
+ end
705
+
706
+ def test_build
707
+ devel = Developer.find(1)
708
+ proj = devel.projects.build("name" => "Projekt")
709
+ assert_equal devel.projects.last, proj
710
+ assert proj.new_record?
711
+ devel.save
712
+ assert !proj.new_record?
713
+ assert_equal devel.projects.last, proj
714
+ end
715
+
716
+ def test_create
717
+ devel = Developer.find(1)
718
+ proj = devel.projects.create("name" => "Projekt")
719
+ assert_equal devel.projects.last, proj
720
+ assert !proj.new_record?
721
+ end
511
722
 
512
723
  def test_uniq_after_the_fact
513
724
  @developers["jamis"].find.projects << @projects["active_record"].find