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 +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.
|