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 +3 -0
- data/HOWTO +473 -0
- data/MIT-LICENSE +20 -0
- data/README +18 -0
- data/has_many_through_generator.rb +330 -0
- data/templates/controller.rb +116 -0
- data/templates/controller_manytomany.rb +56 -0
- data/templates/fixtures.yml +8 -0
- data/templates/fixtures_manytomany.yml +13 -0
- data/templates/functional_test.rb +78 -0
- data/templates/functional_test_manytomany.rb +75 -0
- data/templates/helper.rb +2 -0
- data/templates/layout.rhtml +15 -0
- data/templates/migration.rb +11 -0
- data/templates/migration_manytomany.rb +12 -0
- data/templates/model.rb +4 -0
- data/templates/model_manytomany.rb +8 -0
- data/templates/partial_form.rhtml +3 -0
- data/templates/stylesheet.css +5 -0
- data/templates/unit_test.rb +67 -0
- data/templates/unit_test_manytomany.rb +68 -0
- data/templates/view_index.rhtml +1 -0
- metadata +67 -0
data/CHANGELOG
ADDED
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.
|