has_many_through_generator 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ 1.0.0 ==========
2
+
3
+ * Initial release of generator
data/HOWTO ADDED
@@ -0,0 +1,473 @@
1
+ By Dr Nic
2
+
3
+ If you want a model, a migration file, and a unit test file, you use the <tt>model</tt> generator.
4
+ So if your object model consists of <tt>users</tt> and <tt>groups</tt>, with a <tt>memberships</tt>
5
+ table in the middle to represent the many-to-many relationship, you have to do the following:
6
+
7
+ 1. <tt>ruby script/generate model User</tt> -- generates user.rb, 001_create_user.rb, user_test.rb
8
+ 1. <tt>ruby script/generate model Group</tt> -- generates group.rb, 002_create_group.rb, group_test.rb
9
+ 1. <tt>ruby script/generate model Membership</tt> -- generates membership.rb, 003_create_membership.rb, membership_test.rb
10
+ 1. Add foreign keys to 003_create_membership.rb:
11
+ * <tt>t.add_column :group_id, :integer</tt>
12
+ * <tt>t.add_column :user_id, :integer</tt>
13
+ 1. Add belongs_to to Membership class, in membership.rb:
14
+ * <tt>belongs_to :group</tt>
15
+ * <tt>belongs_to :user</tt>
16
+ 1. Add has_manys to User class, in user.rb:
17
+ * <tt>has_many :memberships</tt>
18
+ * <tt>has_many :groups, :through => :memberships</tt>
19
+ 1. Repeat for Group class, in group.rb:
20
+ * <tt>has_many :memberships</tt>
21
+ * <tt>has_many :users, :through => :memberships</tt>
22
+ 1. Run migration to create the tables in your database:
23
+ * <tt>rake migrate</tt>
24
+
25
+ In a few minutes you have created your tables in the database, and setup your model classes
26
+ to reference each other. Lovely.
27
+
28
+ And then you start to write test cases to make sure everything is hunky doory. Even if you have
29
+ some similar code lying around it'll take an hour or more to refactor it for your new classes.
30
+
31
+ If your a new Rails programmer, you probably don't have any similar code nor do you know
32
+ what test cases to add to your unit tests.
33
+
34
+ I became so sick of this process that I wrote a new generator - similar to <tt>model</tt>,
35
+ <tt>controller</tt>, and <tt>scaffold</tt> - to generate all the code above and
36
+ a set of unit tests to show that its all stuck together nicely.
37
+
38
+ Below is a tutorial for how to:
39
+ * install the generator
40
+ * generate code for two new models and the many-to-many model
41
+ * generate code where one of the models already exists in your application
42
+
43
+ Create a new Rails application for the tutorial
44
+ <pre>
45
+ \> rails hmt_tutorial
46
+ create
47
+ create app/controllers
48
+ create app/helpers
49
+ create app/models
50
+ create app/views/layouts
51
+ ...etc...
52
+ > cd hmt_tutorial
53
+ </pre>
54
+
55
+ To install the generator run the following:
56
+ <pre>
57
+ > gem install hasmanythrough
58
+ </pre>
59
+
60
+ To create our User, Group and Membership models and tests:
61
+ <pre>
62
+ > ruby script/generate has_many_through User Group Membership
63
+ exists app/models/
64
+ exists test/unit/
65
+ exists test/fixtures/
66
+ create app/models/group.rb
67
+ create test/unit/group_test.rb
68
+ create test/fixtures/groups.yml
69
+ create db/migrate
70
+ create db/migrate/001_create_groups.rb
71
+ create app/models/user.rb
72
+ create test/unit/user_test.rb
73
+ create test/fixtures/users.yml
74
+ exists db/migrate
75
+ create db/migrate/002_create_users.rb
76
+ create app/models/membership.rb
77
+ create test/unit/membership_test.rb
78
+ create test/fixtures/memberships.yml
79
+ exists db/migrate
80
+ create db/migrate/003_create_memberships.rb
81
+ </pre>
82
+
83
+ The generator command is <tt>has_many_through</tt> and requires 2 parameters:
84
+ the two class names for your two models, which is User and Group.
85
+ The 3rd parameter is optional: the explicit name of the many-to-many model,
86
+ which is Membership in this example. A later example will show what
87
+ happens if you omit to name the many-to-many model.
88
+
89
+ The generated code for the Group class is:
90
+ <pre>
91
+ class Group < ActiveRecord::Base
92
+ has_many :memberships
93
+ has_many :users, :through => :memberships
94
+ end
95
+ </pre>
96
+
97
+ It includes the relationships to the other two classes. The User class will look similar,
98
+ but with a <tt>has_many :groups, :through => :memberships</tt> statement.
99
+
100
+ The generated code for the Membership class is:
101
+ <pre>
102
+ class Membership < ActiveRecord::Base
103
+ belongs_to :group
104
+ belongs_to :user
105
+
106
+ validates_presence_of :group_id, :user_id
107
+ validates_uniqueness_of :group_id, :scope => :user_id
108
+ validates_uniqueness_of :user_id, :scope => :group_id
109
+ end
110
+ </pre>
111
+
112
+ It includes the two belongs_to statements to the primary models, and also includes
113
+ some validation. A user would never expect to see the error messages from
114
+ these validations - there isn't much a user can do if your controller code
115
+ doesn't correctly create Membership objects - but it will ensure that the
116
+ database relationships are correct.
117
+
118
+ The two <tt>validates_uniqueness_of</tt> exist to ensure that there is only
119
+ one Membership object for any [User, Group] pair. If your application
120
+ allows for duplicates, then remove these two statements.
121
+
122
+ Before we create the database tables using migrations, you'll want to add some useful
123
+ fields to the User and Group tables.
124
+
125
+ In 001_create_groups.rb, update the self.up method:
126
+ <pre>
127
+ def self.up
128
+ create_table :groups do |t|
129
+ t.column :name, :string
130
+ end
131
+ end
132
+ </pre>
133
+
134
+ In 002_create_users.rb, also update the self.up method:
135
+ <pre>
136
+ def self.up
137
+ create_table :users do |t|
138
+ t.column :firstname, :string
139
+ t.column :lastname, :string
140
+ t.column :email, :string
141
+ end
142
+ end
143
+ </pre>
144
+
145
+ No changes are required to the memberships table (unless you want to add
146
+ additional fields in your model):
147
+ <pre>
148
+ def self.up
149
+ create_table :memberships do |t|
150
+ t.column :group_id, :integer
151
+ t.column :user_id, :integer
152
+ end
153
+ end
154
+ </pre>
155
+
156
+ To setup your database (mysql example given):
157
+ <pre>
158
+ > mysql -u root
159
+ mysql> create database hmt_tutorial_development;
160
+ Query OK, 1 row affected (0.06 sec)
161
+
162
+ mysql> create database hmt_tutorial_test;
163
+ Query OK, 1 row affected (0.00 sec)
164
+
165
+ mysql> create database hmt_tutorial_production;
166
+ Query OK, 1 row affected (0.00 sec)
167
+
168
+ mysql> exit
169
+ </pre>
170
+
171
+ You can now run migrations:
172
+ <pre>
173
+ > rake migrate
174
+ == CreateGroups: migrating ====================================================
175
+ -- create_table(:groups)
176
+ -> 0.0780s
177
+ == CreateGroups: migrated (0.0780s) ===========================================
178
+
179
+ == CreateUsers: migrating =====================================================
180
+ -- create_table(:users)
181
+ -> 0.0780s
182
+ == CreateUsers: migrated (0.0780s) ============================================
183
+
184
+ == CreateMemberships: migrating ===============================================
185
+ -- create_table(:memberships)
186
+ -> 0.1100s
187
+ == CreateMemberships: migrated (0.1100s) ======================================
188
+ </pre>
189
+
190
+ Now you can run the unit tests:
191
+ <pre>
192
+ > rake test:units
193
+ Started
194
+ ......................
195
+ Finished in 0.484 seconds.
196
+
197
+ 22 tests, 33 assertions, 0 failures, 0 errors
198
+ </pre>
199
+
200
+ Huzzah! You've got 22 tests and 33 successful assertions.
201
+
202
+ Before we look at the generated test code, let's break it. Make the email attribute required for
203
+ all Users, and also unique so no two Users can have the same email address.
204
+
205
+ <pre>
206
+ class User < ActiveRecord::Base
207
+ has_many :memberships
208
+ has_many :groups, :through => :memberships
209
+
210
+ validates_presence_of :email
211
+ validates_uniqueness_of :email
212
+ end
213
+ </pre>
214
+
215
+ And the tests should hopefully break:
216
+ <pre>
217
+ > rake test:units
218
+ Started
219
+ ..................FF..
220
+ Finished in 0.641 seconds.
221
+
222
+ 1) Failure:
223
+ test_new(UserTest) [./test/unit/user_test.rb:28]:
224
+ User should be valid.
225
+ <false> is not true.
226
+
227
+ 2) Failure:
228
+ test_raw_validation(UserTest) [./test/unit/user_test.rb:18]:
229
+ User should be valid without initialisation parameters.
230
+ <false> is not true.
231
+
232
+ 22 tests, 33 assertions, 2 failures, 0 errors
233
+ </pre>
234
+
235
+ We need to update the unit test for User to consider the required email field.
236
+ The user_test.rb file starts with something like this:
237
+
238
+ <pre>
239
+ class UserTest < Test::Unit::TestCase
240
+ fixtures :users, :groups, :memberships
241
+
242
+ NEW_USER = {} # e.g. {:name => 'Test User', :description => 'Dummy'}
243
+ REQ_ATTR_NAMES = %w( ) # name of fields that must be present, e.g. %(name description)
244
+ DUPLICATE_ATTR_NAMES = %w( ) # name of fields that cannot be a duplicate, e.g. %(name description)
245
+
246
+ def test_raw_validation
247
+ user = User.new
248
+ if REQ_ATTR_NAMES.blank?
249
+ assert user.valid?, "User should be valid without initialisation parameters"
250
+ else
251
+ # If User has validation, then use the following:
252
+ assert !user.valid?, "User should not be valid without initialisation parameters"
253
+ REQ_ATTR_NAMES.each {|attr_name| assert user.errors.invalid?(attr_name.to_sym), "Should be an error message for :#{attr_name}"}
254
+ end
255
+ end
256
+ </pre>
257
+
258
+ The constants NEW_USER, REQ_ATTR_NAMES, and DUPLICATE_ATTR_NAMES need to
259
+ be set by you to match your class's validation requirements.
260
+ Let's change these three lines to:
261
+
262
+ <pre>
263
+ NEW_USER = {:firstname => 'Test', :lastname => 'Person', :email => 'test@person.com'} # e.g. {:name => 'Test User', :description => 'Dummy'}
264
+ REQ_ATTR_NAMES = %w( email ) # name of fields that must be present, e.g. %(name description)
265
+ DUPLICATE_ATTR_NAMES = %w( email ) # name of fields that cannot be a duplicate, e.g. %(name description)
266
+ </pre>
267
+
268
+ And run the tests again:
269
+
270
+ <pre>
271
+ > rake test:units
272
+ Started
273
+ ......................
274
+ Finished in 0.625 seconds.
275
+
276
+ 22 tests, 41 assertions, 0 failures, 0 errors
277
+ </pre>
278
+
279
+ More assertions were tested this time to check the required and unique fields (email).
280
+ Now, a raw User.new object will be invalid, where as initially it was assumed to be valid.
281
+
282
+
283
+ The last step of this tutorial is to create another many-to-many model pair where
284
+ one of the classes already exists in the application. Let's consider we need
285
+ <tt>users</tt> and <tt>forums</tt>, and we need a many-to-many table as well,
286
+ but we don't care what its called.
287
+
288
+ <pre>
289
+ > ruby script/generate has_many_through User Forum
290
+ exists app/models/
291
+ exists test/unit/
292
+ exists test/fixtures/
293
+ create app/models/forum.rb
294
+ create test/unit/forum_test.rb
295
+ create test/fixtures/forums.yml
296
+ exists db/migrate
297
+ create db/migrate/004_create_forums.rb
298
+ overwrite app/models/user.rb? [Ynaq] y
299
+ force app/models/user.rb
300
+ overwrite test/unit/user_test.rb? [Ynaq] y
301
+ force test/unit/user_test.rb
302
+ identical test/fixtures/users.yml
303
+ exists db/migrate
304
+ Skipping migration create_users, as already exists: db/migrate/002_create_users.rb
305
+ create app/models/forum_user.rb
306
+ create test/unit/forum_user_test.rb
307
+ create test/fixtures/forum_users.yml
308
+ exists db/migrate
309
+ create db/migrate/005_create_forum_users.rb
310
+ </pre>
311
+
312
+ The generator does a number of things of interest:
313
+ 1. Automatically names your many-to-many table forum_users, and the class ForumUser.
314
+ 1. Skips the generation of the create_users migration as one already exists
315
+ 1. Allows you to re-create the user.rb and user_test.rb files to support
316
+ the new relationships.
317
+
318
+ During the process we regenerated both the user.rb and user_test.rb files. This probably
319
+ wasn't clever as we lost our email validations and our unit test changes.
320
+
321
+ But before we add those changes back, let's run the migration and unit tests:
322
+
323
+ <pre>
324
+ > rake migration
325
+ == CreateForums: migrating ====================================================
326
+ -- create_table(:forums)
327
+ -> 0.1090s
328
+ == CreateForums: migrated (0.1090s) ===========================================
329
+
330
+ == CreateForumUsers: migrating ================================================
331
+ -- create_table(:forum_users)
332
+ -> 0.1090s
333
+ == CreateForumUsers: migrated (0.1090s) =======================================
334
+
335
+ > rake test:units
336
+ Started
337
+ ............................E........
338
+ Finished in 0.937 seconds.
339
+
340
+ 1) Error:
341
+ test_user_to_memberships(MembershipTest):
342
+ NoMethodError: undefined method `memberships' for #<User:0x3b3bea8>
343
+ D:/InstantRails-1.0/ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.3/lib/active_record/base.rb:1792:in `method_missing'
344
+ ./test/unit/membership_test.rb:64:in `test_user_to_memberships'
345
+
346
+ 37 tests, 56 assertions, 0 failures, 1 errors
347
+ </pre>
348
+
349
+ Our units test - which should work straight out-of-the-box - have failed. The Membership unit test,
350
+ from the first example, which used to pass now fails.
351
+
352
+ The failing test method is:
353
+ <pre>
354
+ def test_user_to_memberships
355
+ assert_not_nil users(:first).memberships, "User.memberships should not be nil"
356
+ assert_equal Membership, users(:first).memberships[0].class, "User.memberships should be an array of Membership"
357
+ assert_equal 1, users(:first).memberships.length, "Incorrect number of Membership"
358
+ end
359
+ </pre>
360
+
361
+ Line 64 is the first line of the method. Apparently our User class no longer has
362
+ a memberships method?! Aah... we must have lost it too when we re-generated the
363
+ User class above.
364
+
365
+ Our User class now looks like:
366
+
367
+ <pre>
368
+ class User < ActiveRecord::Base
369
+ has_many :forum_users
370
+ has_many :forums, :through => :forum_users
371
+ end
372
+ </pre>
373
+
374
+ And needs to include the <tt>has_many</tt>s from the Group example, plus our
375
+ missing validations.
376
+
377
+ <pre>
378
+ class User < ActiveRecord::Base
379
+ has_many :forum_users
380
+ has_many :forums, :through => :forum_users
381
+ has_many :memberships
382
+ has_many :groups, :through => :memberships
383
+
384
+ validates_presence_of :email
385
+ validates_uniqueness_of :email
386
+ end
387
+ </pre>
388
+
389
+ Running the unit tests:
390
+
391
+ <pre>
392
+ Started
393
+ .................................FF..
394
+ Finished in 0.938 seconds.
395
+
396
+ 1) Failure:
397
+ test_new(UserTest) [./test/unit/user_test.rb:28]:
398
+ User should be valid.
399
+ <false> is not true.
400
+
401
+ 2) Failure:
402
+ test_raw_validation(UserTest) [./test/unit/user_test.rb:18]:
403
+ User should be valid without initialisation parameters.
404
+ <false> is not true.
405
+
406
+ 37 tests, 59 assertions, 2 failures, 0 errors
407
+ </pre>
408
+
409
+ Damn. Our validation errors are back. Let's re-add the user_test.rb requirement
410
+ and uniqueness details again:
411
+
412
+ <pre>
413
+ NEW_USER = {:firstname => 'Test', :lastname => 'Person', :email => 'test@person.com'} # e.g. {:name => 'Test User', :description => 'Dummy'}
414
+ REQ_ATTR_NAMES = %w( email ) # name of fields that must be present, e.g. %(name description)
415
+ DUPLICATE_ATTR_NAMES = %w( email ) # name of fields that cannot be a duplicate, e.g. %(name description)
416
+ </pre>
417
+
418
+ And run the tests again:
419
+
420
+ <pre>
421
+ > rake test:units
422
+ Started
423
+ .....................................
424
+ Finished in 0.938 seconds.
425
+
426
+ 37 tests, 67 assertions, 0 failures, 0 errors
427
+ </pre>
428
+
429
+ Now we have 67 happy assertions.
430
+
431
+ But wait! There is something missing still. The has_many_through generator
432
+ can generate new files but cannot insert code/text into existing files.
433
+ When we regenerated our user_test.rb file, there were 3 test methods
434
+ associated with the Membership and Group classes that were lost.
435
+
436
+ By looking at the last 3 methods of the user_test.rb file for
437
+ Forum and ForumUser, we can recreate the Group and Membership tests.
438
+
439
+ Add the following to your user_test.rb file:
440
+
441
+ <pre>
442
+ def test_memberships_to_user
443
+ assert_equal users(:first), Membership.find_first.user, "Membership.user should be a User"
444
+ end
445
+
446
+ def test_user_to_memberships
447
+ assert_not_nil users(:first).memberships, "User.users should not be nil"
448
+ assert_equal Membership, users(:first).memberships[0].class, "User.memberships should be an array of Membership"
449
+ end
450
+
451
+ def test_groups
452
+ assert_not_nil users(:first).groups, "User.groups should not be nil"
453
+ assert_equal Group, users(:first).groups[0].class, "User.groups should be an array of Group"
454
+ end
455
+ </pre>
456
+
457
+ And run the tests for the final time:
458
+
459
+ <pre>
460
+ Started
461
+ ........................................
462
+ Finished in 1.0 seconds.
463
+
464
+ 40 tests, 72 assertions, 0 failures, 0 errors
465
+ </pre>
466
+
467
+ Lovely.
468
+
469
+ It is now very easy to start your Rails apps or extend your old apps with common
470
+ many-to-many relationships using the has_many_through generator, and you
471
+ get a whole bunch of unit tests for free.
472
+
473
+ Cheap at half the price.