has_many_through_generator 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.