public
Description: Markdown sources for http://hobocentral.net
Homepage: http://hobocentral.net
Clone URL: git://github.com/tablatom/hobocentral.git
Big extension to Agility tutorial
tablatom (author)
Tue Jul 01 05:12:02 -0700 2008
commit  91fa9460070eea2ceec5eeb27f5380e1db22c506
tree    73bac9357507489e06ff7b7618b029fa3fac2d46
parent  24cd7ac99a5110568ed54667d40c8c02d14cf989
...
1
2
3
 
4
5
 
6
7
8
9
 
10
11
12
...
36
37
38
39
 
40
41
42
 
43
44
45
46
47
48
 
49
50
51
52
53
54
 
 
55
56
 
57
58
59
...
66
67
68
69
 
70
71
72
...
80
81
82
83
 
84
85
86
...
102
103
104
105
106
107
 
108
109
110
...
133
134
135
136
 
137
138
139
...
164
165
166
167
 
168
169
170
171
172
173
174
 
175
176
177
178
 
179
180
181
...
187
188
189
190
 
191
192
193
...
206
207
208
209
 
210
211
212
 
213
214
215
 
216
217
218
...
234
235
236
237
 
238
239
240
...
243
244
245
246
 
247
248
 
249
250
251
 
252
253
 
 
254
255
256
257
 
258
259
260
261
262
263
264
265
 
266
267
268
 
269
270
271
272
 
273
274
275
...
279
280
281
282
 
283
284
285
286
 
 
287
288
289
...
298
299
300
301
 
302
303
304
...
309
310
311
312
 
313
314
315
316
317
 
318
319
320
...
328
329
330
331
 
332
333
334
...
356
357
358
359
 
360
361
362
...
368
369
370
371
372
373
 
374
375
376
377
 
378
379
380
...
442
443
444
445
 
446
447
448
449
 
450
451
452
453
454
 
455
456
457
...
487
488
489
490
 
491
492
 
493
494
495
...
513
514
515
516
517
518
 
 
 
 
 
519
520
521
522
 
523
524
525
...
546
547
548
549
 
550
551
 
552
553
554
 
555
556
557
...
580
581
582
583
 
584
585
 
586
587
 
588
589
 
 
 
590
 
591
592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
597
598
599
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
 
3
4
 
5
6
 
 
 
7
8
9
10
...
34
35
36
 
37
38
 
 
39
40
41
42
 
 
 
43
44
 
 
 
 
 
45
46
47
 
48
49
50
51
...
58
59
60
 
61
62
63
64
...
72
73
74
 
75
76
77
78
...
94
95
96
 
 
 
97
98
99
100
...
123
124
125
 
126
127
128
129
...
154
155
156
 
157
158
159
160
161
162
163
 
164
165
166
167
 
168
169
170
171
...
177
178
179
 
180
181
182
183
...
196
197
198
 
199
200
201
 
202
203
204
 
205
206
207
208
...
224
225
226
 
227
228
229
230
...
233
234
235
 
236
237
 
238
239
240
 
241
242
243
244
245
246
247
248
 
249
250
 
 
 
251
252
253
 
254
255
256
 
257
258
259
260
 
261
262
263
264
...
268
269
270
 
271
272
273
 
 
274
275
276
277
278
...
287
288
289
 
290
291
292
293
...
298
299
300
 
301
302
303
304
305
 
306
307
308
309
...
317
318
319
 
320
321
322
323
...
345
346
347
 
348
349
350
351
...
357
358
359
 
 
 
360
361
362
363
 
364
365
366
367
...
429
430
431
 
432
433
434
435
 
436
437
438
439
440
 
441
442
443
444
...
474
475
476
 
477
478
 
479
480
481
482
...
500
501
502
 
503
504
505
506
507
508
509
510
511
512
 
513
514
515
516
...
537
538
539
 
540
541
 
542
543
544
 
545
546
547
548
...
571
572
573
 
574
575
 
576
577
 
578
579
 
580
581
582
583
584
585
 
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
 
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
0
@@ -1,12 +1,10 @@
0
 # Hobo Tutorial -- Agility
0
 
0
-**NOTE: This is the tutorial that was recently delivered at the Ruby Fools conference in Copenhagen. It hasn't been properly chapterised for the web yet.**
0
+The full source code for the Agility app is available at [http://github.com/tablatom/agility](http://github.com/tablatom/agility/tree/master).
0
 
0
-The full source code for the Agility app is available [here](http://github.com/tablatom/agility/tree/master).
0
+In this tutorial we'll be creating a simple "Agile Development" application -- _Agility_. The application tracks projects which consist of a number of user stories. Stories have a status (e.g. accepted, under development...) as well as a number of associated tasks. Tasks can be assigned to users, and each user can see a heads-up of all the tasks they've been assigned to on their home page.
0
 
0
-In this tutorial we'll be creating a simple "Agile Development" application -- _Agillity_. The application tracks projects which consist of a number of user stories. Stories have a status (e.g. accepted, under development...) as well as a number of associated tasks. Tasks can be assigned to users, and each user can see a heads-up of all the tasks they've been assigned to on their home page.
0
-
0
-This is a bit of an in-at-the-deep-end tutorial -- we build the app the way you would assuming you had already got the hang of the way Hobo works. In the later stages new concepts start coming thick and fast. The goal here is to show you what's possible, and give you a flavour of Hobo-style application development, rather that to provide detailed background on how everything workds. Don't worry about it, it's fun! If you'd rather take things a bit slower, you might prefer the [POD tutorial](http://hobocentral.net/pod-tutorial).
0
+This is a bit of an in-at-the-deep-end tutorial -- we build the app the way you would assuming you had already got the hang of the way Hobo works. In the later stages new concepts start coming thick and fast. The goal here is to show you what's possible, and give you a flavour of Hobo-style application development, rather than to provide detailed background on how everything workds. Don't worry about it, it's fun! If you'd rather take things a bit slower, you might prefer the [POD tutorial](http://hobocentral.net/pod-tutorial).
0
 
0
 
0
 # Part 1 -- Getting Started
0
@@ -36,24 +34,18 @@ The `hobo` command is like the Rails command, it will create a new blank Rails a
0
 
0
     $ hobo agility
0
 
0
-With Hobo, you get a bare-bones application immediately. Let's run it:
0
+With Hobo, you get a bare-bones application immediately. Let's create the database and then run the app. We first need to use the migration generator to create the basic database:
0
 
0
- $ cd agility
0
- $ ./script/server
0
+ $ ./script/generate hobo_migration
0
 
0
 (Tip: Windows users: Whenever you see `./script/something`, you will need to instead do `ruby script/something`)
0
 
0
-If you try and sign up you'll see that part is not working yet - because we haven't created the `users` table. There's a good reason why the `hobo` command doesn't do that automatically but we'll gloss over that for now. To create the table, use Hobo's migration generator:
0
-
0
-(Tip: leave the server running and open a new terminal window)
0
+Respond to the prompt with `m` and then give the migration a name. Then:
0
 
0
- $ ./script/generate hobo_migration
0
-
0
-Respond to the prompt with `m` and then give the migration a name
0
-
0
-You should now be able to sign up. By default, Hobo gives administrator status to the first user that signs in. (The concept of an administrator is built into Hobo, but if it doesn't make sense for your application it's trivial to do away with).
0
+ $ cd agility
0
+ $ ./script/server
0
 
0
-In the next section we'll be starting to flesh out the basics of the app.
0
+You should be able to sign up. In the next section we'll be starting to flesh out the basics of the app.
0
 
0
 
0
 ## Interface first Hobo style
0
@@ -66,7 +58,7 @@ The reason is, we think we've rewritten this rule:
0
 
0
 > Design is relatively light. A paper sketch is cheap and easy to change. html designs are still relatively simple to modify (or throw out). That's not true of programming. Designing first keeps you flexible. Programming first fences you in and sets you up for additional costs.
0
 
0
-In our experience, experimenting with an app by actually building a prototype with Hobo, is _far_ quicker than creating html designs. How's that for getting real? We could waffle for a good while on this point, but that's probably best saved for a blog post. For now let's dive in and get this app running.
0
+In our experience, experimenting with an app by actually building a prototype with Hobo, is actually quicker than creating html designs. How's that for getting real? We could waffle for a good while on this point, but that's probably best saved for a blog post. For now let's dive in and get this app running.
0
 
0
 
0
 ## The models
0
@@ -80,7 +72,7 @@ Let's review what we want this app to do:
0
  * Tasks can be assigned to users
0
  * Users can get an easy heads up of the tasks they are assigned to
0
 
0
-Sounds to me like we just designed our models. We'll need:
0
+Sounds to me like we just sketched a first-cut of our models. We'll start with:
0
 
0
  * `Project` (with a name)
0
   has many stories
0
@@ -102,9 +94,7 @@ Here's how we create these with a Hobo generator:
0
   $ ./script/generate hobo_model_resource task description:string
0
   $ ./script/generate hobo_model_resource task_assignment
0
   
0
-The field declarations have been created by the generators, but not the associations. Go ahead and edit the associations in the models to reflect the description above.
0
-
0
-The associations go just below the `fields do ... end` declaration in each model.
0
+The field declarations have been created by the generators, but not the associations. Go ahead and add the associations, just below the `fields do ... end` declaration in each model, as follows:
0
 
0
 #### `app/models/project.rb`
0
 
0
@@ -133,7 +123,7 @@ The associations go just below the `fields do ... end` declaration in each model
0
       belongs_to :story
0
 
0
       has_many :task_assignments, :dependent => :destroy
0
- has_many :users, :through => :task_assignments, :managed => true
0
+ has_many :users, :through => :task_assignments
0
       ...
0
     end
0
 {: .ruby}
0
@@ -164,18 +154,18 @@ Now watch how Hobo can create a single migration for all of these:
0
   
0
 Note: Hobo's automatic routing happens when the application starts. You'll need to stop and start the web-server in order for the application to reflect all these new models and controllers.
0
   
0
-Fire up the app. It's kind of a weird UI at this stage, but we do actually have a working application. Make sure you are logged in as the user who signed up first, and spend a few minutes populating the app with projects, stories and tasks.
0
+Fire up the app. It's kind of a weird UI at this stage, but we do actually have a working application. Make sure you are logged in as an administrator (e.g. the user who signed up first), and spend a few minutes populating the app with projects, stories and tasks.
0
 
0
 With some more very simple changes, and without even touching the views, we can get surprisingly close to a decent UI.
0
 
0
 
0
 # Part 2 -- Removing actions
0
 
0
-By default Hobo has given us a full set of restful actions routes for every single model/controller pair. Many of these routes are inappropriate for our application. For example, why would be want an index page listing every Task in the database? We only really want to see tasks listed against stories and users. We need to disable the routes we don't want.
0
+By default Hobo has given us a full set of restful actions for every single model/controller pair. Many of these routes are inappropriate for our application. For example, why would be want an index page listing every Task in the database? We only really want to see tasks listed against stories and users. We need to disable the routes we don't want.
0
 
0
 There's an interesting change of approach here that often crops up with Hobo development. Normally you'd expect to have to build everything yourself. With Hobo, you often get given everything you want and more besides. Your job is to take away the parts that you *don't* want.
0
 
0
-Taking that example -- removing the index action from TasksController, here's how we'd do that. in `app/controllers/tasks_controller.rb`, change
0
+Here's how we would remove, for example, the index action from TasksController. In `app/controllers/tasks_controller.rb`, change
0
 
0
   auto_actions :all
0
 {: .ruby}
0
@@ -187,7 +177,7 @@ To
0
   
0
 Restart the server and you'll notice that Tasks has been removed from the main nav-bar. Hobo's generic pages (which are just clever defaults that you can override as much or as little as you like) know how to adapt to changes in the actions that you make available.
0
 
0
-Here's another similar trick. Browse to one of your stories. See that "New Task" link at the bottom? That's kind of clunky for the user -- it would be much nicer if the new task form (which only has one field after all) was in-line in the same page. Edit the `auto_actions` declaration in `stories_controller` to look like this:
0
+Here's another similar trick. Browse to one of your stories. See that "New Task" link at the bottom? That's kind of clunky for the user -- it would be much nicer if the new task form (which only has one field after all) was in-line in the same page. Edit the `auto_actions` declaration in `stories_controller.rb` to look like this:
0
 
0
   auto_actions :all, :except => :new_task
0
 {: .ruby}
0
@@ -206,13 +196,13 @@ There's also a handy shortcut to get just the read-only routes (i.e. the ones th
0
   
0
 The opposite is handy for things that are manipulated by ajax but never viewed directly:
0
 
0
- auto_actions :write_only
0
+ auto_actions :write_only # short for -- :create, :update, :destroy
0
 {: .ruby}
0
 
0
-Work through your controllers and have a think about which actions you want. You might end up with something like:
0
+Work through your controllers and have a think about which actions you want. You need to end up with:
0
 
0
  * Projects: `:all`
0
- * Stories `:write_only, :show, :edit, :new`
0
+ * Stories `:write_only, :show, :edit, :new, :create_task`
0
  * Tasks: `:write_only, :edit`
0
  * TaskAssignments: `:write_only` (or maybe we don't even need this controller?)
0
 
0
@@ -234,7 +224,7 @@ You might have noticed methods like this one in the generated models:
0
 
0
 That tells Hobo that only administrators are allowed to create this kind of model. Before every create, update and delete (destroy) operation, Hobo calls one of these methods passing the current user. Only if the method returns true is the operation allowed to continue.
0
 
0
-Furthermore, the *Rapid* DRYML tag library (that's the part of Hobo that creates the UI automatically for you) knows how to interrogate the models permissions and adapt accordingly. For example, Rapid won't generate a "New Project" link if the current user does not have permission to create a project.
0
+Furthermore, the *Rapid* DRYML tag library (that's the part of Hobo that creates the UI automatically for you) knows how to interrogate the permissions and adapt accordingly. For example, Rapid won't generate a "New Project" link if the current user does not have permission to create a project.
0
 
0
 You can see this feature in action by logging out and browsing around the app. You'll notice that all the 'new' and 'edit' links have disappeared. If you experiment by change `user.administrator?` to `true` in some permission methods, you'll see operations start to become available.
0
 
0
@@ -243,33 +233,32 @@ You can see this feature in action by logging out and browsing around the app. Y
0
 For the purposes of the tutorial you can make your own decisions about who should be allowed to do what. In the spirit of agile methods, we probably don't want to lock things down too much. Here's a suggestion:
0
 
0
  * Only administrators can create, edit and delete projects
0
- * Stories and tasks are open to change by all logged in users.
0
+ * Stories and tasks are open to change by all signed up users.
0
 
0
-A permission that says "only logged in users" looks like this:
0
+A permission that says "only signed up users" looks like this:
0
 
0
   def creatable_by?(user)
0
- !user.guest?
0
+ user.signed_up?
0
   end
0
 {: .ruby}
0
+
0
+(There's also `user.guest?`)
0
   
0
 You might need to sign up a new user so you've got a non-admin to test things with.
0
 
0
-Tip: Hobo provides an easy way to switch user so you can see how things look to different people. Just visit (substituting `localhost` and `3000` as appropriate):
0
+Tip: Hobo provides an easy way to switch user so you can see how things look to different people. In development mode you should see the change-user menu in the top-left.
0
 
0
- http://localhost:3000/dev/set_current_user?name=Fred
0
-
0
-*Not available in production mode!* Once you've typed this once, your can use your browser's history completion to quickly flick between different users.
0
 
0
 ## Permissions for data integrity
0
 
0
-The permissions system is not just for opening operations for some users but not others, it is also used to prevent operations that don't make sense for anyone. For example, you've probably noticed that the default UI allows stories to be moved from one project to another. That's probably not a sensible operation for *anyone* to be doing. We can prevent it with this method in `story.rb`:
0
+The permissions system is not just for opening operations for some users but not others, it is also used to prevent operations that don't make sense for anyone. For example, you've probably noticed that the default UI allows stories to be moved from one project to another. That's arguably not a sensible operation for *anyone* to be doing. We can prevent it with this method in `story.rb`:
0
 
0
   def updatable_by?(user, new)
0
- !user.guest? && same_fields?(new, :project)
0
+ user.signed_up? && same_fields?(new, :project)
0
   end
0
 {: .ruby}
0
 
0
-Note that the `updatable_by?` method is called with `self` in the current state, and the `new` argument is an instance of the same class in the proposed new state. The `same_fields?` helper is a convenient way to assert that certain fields have not been changed. There's also `only_changed_fields?` which is more convenient if you want to prevent changes to all but a certain few fields.
0
+The `updatable_by?` method has a parameter: `new`. This is a duplicate instance of `self` with the proposed changes applied. So for example, if you wanted to know if the user is clearing the association to the project, you could test for `new.project.nil?`. The `same_fields?` helper is a convenient way to assert that certain fields have not been changed. There's also `only_changed_fields?` which is more convenient if you want to prevent changes to all but a certain few fields.
0
 
0
 Rapid will respond to this change by removing the project selector from the edit-story page.
0
 
0
@@ -279,11 +268,11 @@ Make a similar change to prevent tasks being moved from one story to another.
0
 
0
 One permission method that is not present by default is`editable_by?`. This method tells Rapid if the user is allowed to see an edit control for a given field. The reason this method is often omitted is that Hobo does a pretty good job of figuring out edit permission automatically from the rules you've given for update permission (if you think about it, these are two sides of the same coin).
0
 
0
-Sometimes however, Hobo can't figure out edit permission unless you tell it explicitly. A common example is `has_many` associations. If you don't specify edit permission for these, Hobo just defaults to not-editable. That's why, up until now, the task assignments are completely absent from the UI. We can fix that by adding this to `task.rb`:
0
+Sometimes however, Hobo can't figure out edit permission unless you tell it explicitly. A common case is `has_many` associations. If you don't specify edit permission for these, Hobo just defaults to not-editable. That's why, up until now, the task assignments are completely absent from the UI. We can fix that by adding this to `task.rb`:
0
   
0
   def users_editable_by?(user)
0
- !user.guest?
0
- end
0
+ user.signed_up?
0
+ end
0
 {: .ruby}
0
   
0
 We also need to tell Hobo that it's ok for the TaskAssigment objects can be created and deleted automatically as a side effect of saving changes to the Task. Change the `has_many :users` declaration in `task.rb` to:
0
@@ -298,7 +287,7 @@ You should now get a nice javascript powered control for assigning users in the
0
 
0
 It's pretty surprising how far you can get without even touching the view layer. That's the way we like to work with Hobo - get the models and controllers right and the view will probably get close to what you want. From there you can override just those parts of the view that you need to.
0
 
0
-Hobo uses the DRYML template language for the views. DRYML is a tag based template language -- it allows you to define and use your own tags right alongside the regular HTML tags. Tags are like helpers, but a lot more powerful. DRYML is quite different to other tag-based template languages, thanks to features like the implicit context and nestable parameters. DRYML also an extension of ERB so you can still use the ERB syntax you're familiar with from Rails.
0
+We do that using the DRYML template language which is part of Hobo. DRYML is tag based -- it allows you to define and use your own tags right alongside the regular HTML tags. Tags are like helpers, but a lot more powerful. DRYML is quite different to other tag-based template languages, thanks to features like the implicit context and nestable parameters. DRYML is also an extension of ERB so you can still use the ERB syntax you're familiar with from Rails.
0
 
0
 DRYML is probably the single best part of Hobo. It's very good at high-level re-use because it allows you to make very focussed changes if a given piece of pre-packaged HTML is not *quite* what you want.
0
 
0
@@ -309,12 +298,12 @@ A full coverage of DRYML is well beyond the scope of this tutorial. Instead we'r
0
 
0
 At the moment, the only way to see who's assigned to a task is to click the task's edit link. Not good. Let's add a list of the assigned users to each task when we're looking at a story.
0
 
0
-DRYML has a feature called *polymorphic tags*. These are tags that are defined differently for different types of object. Rapid makes use of this feature with a system of cards. The tasks that are displayed on the story page are rendered by the `<card>` tag that Rapid provides. You can define custom cards for particular models. Furthermore, if you call `<base-card>` you can define your card by tweaking the default, rather than starting from scratch. This is what DRYML is all about. It's like a smart-bomb, capable of taking out little bits of unwanted HTML with pin-point strikes and no collateral damage.
0
+DRYML has a feature called *polymorphic tags*. These are tags that are defined differently for different types of object. Rapid makes use of this feature with a system of "cards". The tasks that are displayed on the story page are rendered by the `<card>`. You can define custom cards for particular models. Furthermore, if you call `<base-card>` you can define your card by tweaking the default, rather than starting from scratch. This is what DRYML is all about. It's like a smart-bomb, capable of taking out little bits of unwanted HTML with pin-point strikes and no collateral damage.
0
 
0
 The file `app/views/taglibs/application.dryml` is a place to put tag definitions that will be available throughout the site. Add this definition to that file:
0
 
0
   <def tag="card" for="Task">
0
- <base-card>
0
+ <base-card merge>
0
    <creation-details: replace>
0
    <div class="users">
0
    Assigned users: <repeat:users join=", "><a/></repeat><else>None</else>
0
@@ -328,7 +317,7 @@ OK there's a lot of new concepts thrown at you at once there :-)
0
   
0
 First off, refresh the story page. You'll see that in the cards for each task, we've replaced the creation time and date (which we don't really want to see) with the list of assigned users. The users are clickable - they link to each users home page (which doesn't have much on it at the moment).
0
 
0
-The card that we defined calls `<base-card>` which gives us the default card from Rapid, but it overrides some of the content using DRYML's named parameters (`<creation-details:>`). The `replace` attribute means we want to remove the creation-details entirely. For the replacement we insert a `<div>` and use the `<repeat>` tag to insert the list of links. Some things to note:
0
+The card that we defined calls `<base-card>`, but it overrides some of the content using DRYML's named parameters (`<creation-details:>`). The `replace` attribute means we want to remove the creation-details entirely. For the replacement we insert a `<div>` and use the `<repeat>` tag to insert the list of links. Some things to note:
0
   
0
  * The `<repeat>` tag provides a `join` attribute which we use to insert the commas
0
  * The link is created with a simple empty `<a/>`. It links to the 'current context' which, in this case, is the user.
0
@@ -356,7 +345,7 @@ Now let's get the content we're after - the user's assigned tasks, grouped by st
0
   <show-page>
0
    <content-body:>
0
    <h2><Your/> Assigned Tasks</h2>
0
- <repeat with="&this.tasks.group_by(&:story)">
0
+ <repeat with="&@user.tasks.group_by(&:story)">
0
    <h3><a with="&this_key"/></h3>
0
    <collection class="tasks"/>
0
    </repeat>
0
@@ -368,13 +357,11 @@ Again - lots of new stuff there. Let's quickly run over what's going on
0
 
0
  * The `<Your>` tag is a handy little gadget. It outputs "Your" if the context is the current user, otherwise it outputs the user's name. You'll see "Your Assigned Tasks" when looking at yourself, and "Fred's Assigned Tasks" when you're looking at Fred.
0
   
0
- * We're using `<repeat>` again, but this time we're setting the context to the result of a Ruby expression (`with="&...expr..."`).
0
-
0
- * The expression `this.tasks.group_by(&:story)` gives us the grouped tasks. (`this` will be the user, because we're on the `users/show` page)
0
+ * We're using `<repeat>` again, but this time we're setting the context to the result of a Ruby expression (`with="&...expr..."`). The expression `@user.tasks.group_by(&:story)` gives us the grouped tasks.
0
 
0
  * We're repeating on a hash this time. Inside the repeat `this` (the implicit context) will be an array of tasks, and `this_key` will be the story. So `<a with="&this_key">` gives us a link to the story.
0
   
0
- * The `<collection>` is used to render a collection of anything. By default it renders `<card>` tags in a `<ul>` list. Like `<card>` it can be overridden for specific types.
0
+ * `<collection>` is used to render a collection of anything. By default it renders `<card>` tags in a `<ul>` list. Like `<card>` it can be overridden for specific types. For example, you could chose that a collection of projects should be rendered as a table.
0
   
0
 That's probably a lot to take in all at once -- the main idea here is to throw you in and give you an overview of what's possible. The [DRYML Guide][] will shed more light.
0
 
0
@@ -442,16 +429,16 @@ Off we go.
0
 
0
 ## Story status menu.
0
 
0
-We're going to do this in two stages - first a fixed menu that requires a source-code change to alter the available statuses. We'll then remove that restriction by adding a StoryStatus model. We'll also see the migration generator in action again.
0
+We're going to do this in two stages - first a fixed menu that would require a source-code change if you ever need to alter the available statuses. We'll then remove that restriction by adding a StoryStatus model. We'll also see the migration generator in action again.
0
 
0
 The fixed menu is brain-dead simple. Track down the declaration of the status field in `story.rb` (it's in the `fields do ... end` block), and change it to read something like:
0
 
0
- status enum_string(:new, :accepted, :discussion, :implementation, :user_testing, :deployed, :rejected)
0
+ status enum_string(:new, :accepted, :discussion, :implementation) # etc.
0
 {: .ruby}
0
   
0
 Job done. If you want the gory details, `enum_string` is a *type constructor*. It creates an anonymous class that represents this enumerated type (a subclass of String). You can see this in action by trying this in the console:
0
 
0
- >>> Story.find(:first).status.class
0
+ >> Story.find(:first).status.class
0
 {: .ruby}
0
 
0
 The menu is working in the edit-story page now. It would be nice though if we had a ajaxified editor right on the story page. Edit `app/views/stories/show.dryml` to be:
0
@@ -487,9 +474,9 @@ Now run the migration generator
0
 You'll see that the migration generator considers this change to be ambiguous. Whenever there are columns removed *and* columns added, the migration generator can't tell whether you're actually removing one column and
0
 adding another, or if you are renaming the old column. It's also pretty fussy about what it makes you type. We really don't want to play fast and lose with your precious data, so to confirm that you want to drop the 'status' column, you have to type in full: "drop status".
0
 
0
-Once you've done that you'll see that the generated migration includes the creation of the new foreign key and the removal of the old status column. If you wanted to do something clever with the existing status values you're free to edit the migration and add some data processing before you run it.
0
+Once you've done that you'll see that the generated migration includes the creation of the new foreign key and the removal of the old status column.
0
 
0
-For example, within your migration, console or a rake task, you could create some initial story statuses
0
+You can always edit the migration before running it. For example you could create some initial story statuses:
0
 
0
     [:new, :accepted, :discussion, :implementation,
0
      :user_testing, :deployed, :rejected].each do |status|
0
@@ -513,13 +500,17 @@ First we'll add the filter control to the header of the table-plus. Rapid provid
0
    Display by status: <filter-menu param-name="status" options="&StoryStatus.all"/>
0
    </div>
0
    </prepend-header:>
0
- <empty-message:>No stoires match your criteria</empty-message:>
0
   </table-plus>
0
 {: .dryml}
0
+
0
+To make the filter look right, add this to `public/application.css`
0
+
0
+ .show-page.project .filter {float: left;}
0
+ .show-page.project .filter form {display: inline;}
0
   
0
 If you try to use the filter, you'll see it adds a `status` parameter in the query string. We need to pick that up and do something useful with it in the controller. We can use the `apply_scopes` method again, which is already being used, so it's just a matter of adding one more keyword argument:
0
 
0
-Needs support in the controller. Add this option to `apply_scopes`:
0
+Needs support in the controller. Add this option to the `apply_scopes` call in `ProjectsController#show`:
0
 
0
   :status_is => params[:status]
0
 {: .ruby}
0
@@ -546,12 +537,12 @@ The migration generator knows about `acts_as_list`, so you can just run it and y
0
   
0
 And that's it! You will need to restart the server because there's a new route for the reorder action.
0
 
0
-You'll notice a slight glitch -- the tasks position has been added to the new-task and edit-task forms. We don't want that. The easiest way to fix this is slightly different for the new form and the edit form.
0
+You'll notice a slight glitch -- the tasks position has been added to the new-task and edit-task forms. We don't want that. The easiest way to fix this differs slightly for the new form and the edit form.
0
 
0
-For the new form, we actually don't ever want the position to be set when the task is created -- that's handled by acts-as-list. We can reflect this in the Task's create permissions:
0
+For the new form, we actually never want the position to be set when the task is created -- that's handled by acts-as-list. We can reflect this in the Task's create permissions:
0
 
0
   def creatable_by?(user)
0
- !user.guest? && position.nil?
0
+ user.signed_up? && position.nil?
0
   end
0
 {: .ruby}
0
   
0
@@ -580,20 +571,338 @@ To
0
 
0
 You may need to install the relevant ruby gem: either BlueCloth (markdown) or RedCloth (textile)
0
 
0
-That's it! Hope you had fun. I'm sure there are lots of ways you'd like to take this app forward. Have a play and don't forget to take advantage of the forums and IRC channel (#hobo on freenode) if you get stuck. Check the next section if you want some ideas
0
+# Part 6 -- Project Ownership
0
 
0
-# Part 6 -- Ideas for extending the app.
0
+The next goal for Agility is to move to a full mutli-user application, where users can create their own projects and control who has access to them. Rather than make this change in one go, we'll start with a small change that doesn't do much by itself, but is a step in the right direction: making projects be owned by users.
0
 
0
-## More access controls
0
+Add the following to the Project model:
0
 
0
-It might make sense if users can create their own projects, and control who can view them and who can edit them. For example, you might want to invite a client to have a read-only view of how the project is progressing.
0
+ belongs_to :owner, :class_name => "User", :creator => true
0
+
0
+There's a Hobo extension there: `:creator => true` tells Hobo that when creating one of these things, the `owner` association should be automatically set up to be the user doing the create.
0
 
0
+We also need the other end of this assocaition, in the User model:
0
 
0
-## Milestones
0
+ has_many :projects, :class_name => "Project", :foreign_key => "owner_id"
0
+
0
+How should this effect the permissions? Certain operations on the project should probably be restricted to its owner:
0
+
0
+ def creatable_by?(user)
0
+ user.signed_up? && owner == user
0
+ end
0
+
0
+ def updatable_by?(user, new)
0
+ user.administrator? || (user == owner && same_fields?(new, :owner))
0
+ end
0
+
0
+ def deletable_by?(user)
0
+ user.administrator? || user == owner
0
+ end
0
+
0
+One thing worth noting in the `creatable_by?` method. We assert that `owner == user`. This is very often found in conjunction with `:creator => true`. Together, these mean that the current user can create their own projects only, and the "Owner" form field will be automatically removed from the new project form.
0
 
0
-A pretty obvious addition is to have project milestones, and be able to associate tickets with milestones.
0
+Tip: Now that the projects are not viewable by everyone, projects/index won't be working well. You might want to disable the index action on ProjectsController. As we're moving Agility to be more of a social app, you might want to enable the index action on UsersController instead. Restart the server to see the change in the nav-bar.
0
+
0
+
0
+# Part 7 -- Granting read access to others
0
+
0
+Now that we've got users owning their own projects, it seems wrong that any signed up user can view any project. On the other hand it wouldn't make any sense to hide the project from everyone. What we need is a way for the project owner to grant others access.
0
+
0
+We can model this with a ProjectMembership model that represents access for a specific user and project:
0
+
0
+ $ ./script/generate hobo_model_resource project_membership
0
+
0
+## The model layer
0
+
0
+First we'll add the associations to the model:
0
+
0
+ belongs_to :project
0
+ belongs_to :user
0
+
0
+Then permissions -- only the project owner (and admins) can manipulate these:
0
 
0
+ def creatable_by?(user)
0
+ user.administrator? || user == project.owner
0
+ end
0
+
0
+ def updatable_by?(user, new)
0
+ user.administrator? || user == project.owner
0
+ end
0
+
0
+ def deletable_by?(user)
0
+ user.administrator? || user == project.owner
0
+ end
0
+
0
+ def viewable_by?(user, field)
0
+ true
0
+ end
0
+
0
+Let's do the other ends of those two belongs-to associations. In the Project model:
0
+
0
+ has_many :memberships, :class_name => "ProjectMembership", :dependent => :destroy
0
+ has_many :members, :through => :memberships, :source => :user, :managed => true
0
+
0
+And in the User model (remember that User already has an association called `projects` so we need a new name):
0
+
0
+ has_many :project_memberships, :dependent => :destroy
0
+ has_many :joined_projects, :through => :project_memberships, :source => :project
0
+
0
+Now we can define view permission on projects, stories and tasks according to project membership.
0
+
0
+In Project:
0
+
0
+ def viewable_by?(user, field=nil)
0
+ user.administrator? || user == owner || user.in?(members)
0
+ end
0
+
0
+In Story:
0
+
0
+ def viewable_by?(user, field=nil)
0
+ project.viewable_by?(user)
0
+ end
0
+
0
+In Task:
0
+
0
+ def viewable_by?(user, field)
0
+ story.viewable_by?(user)
0
+ end
0
+
0
+## The view layer
0
+
0
+The last step is that we need a UI to manage these memberships. We're going to do it all ajax-style on the project show page. First up, we'll add a side-bar to the projects/show page that lists the members of the projects. The Rapid pages and the clean theme already have support for sidebars through the "aside layout". To turn in on, edit projects/show.dryml as follows (don't get rid of the `<primary-collection:>` parameter that's there already):
0
+
0
+ <show-page layout="aside">
0
+ <aside:>Aside Content</aside:>
0
+ ...
0
+ </show-page>
0
+
0
+Refresh the page and you'll see the side-bar appear.
0
+
0
+Now we'll display the members of the project in the side-bar using the `<collection>` tag which we've seen before:
0
+
0
+ <show-page layout="aside">
0
+ <aside:>
0
+ <h2>Project Members</h2>
0
+ <collection:members/>
0
+ </aside:>
0
+ ...
0
+ </show-page>
0
+
0
+## A form with auto-completion
0
+
0
+Finally we'll add the form to add a new person to the project. We'll set it up so that you can type the user's name, with autocompletion, in order to add someone to the project.
0
+
0
+First we need the controller side of the auto-complete. We're going to add an auto-completer to ProjectsController that will only complete the names of people that are not already members of the project. Hobo's automatically scopes come very handy. Add this declaration to `projects_controller.rb`:
0
+
0
+ autocomplete :new_member_name do
0
+ project = find_instance
0
+ hobo_completions :username, User.without_project(project).is_not(project.owner)
0
+ end
0
+
0
+You can read this as: create an auto-complete action '`new_member_name`' that finds users that are not already members of the project, and not the owner of the project and completes the `:username` field.
0
+
0
+Now the form in projects/show.dryml. We'll use Hobo's ajax part mechanism to refresh the collection without reloading the page:
0
+
0
+ <show-page layout="aside">
0
+
0
+ <aside:>
0
+ <h2>Project Members</h2>
0
+ <collection:members part="members"/>
0
+
0
+ <form:memberships.new update="members" reset-form refocus-form>
0
+ <div>
0
+ Add a member:
0
+ <name-one:user complete-target="&@project" completer="new_member_name"/>
0
+ </div>
0
+ </form:memberships.new>
0
+ </aside:>
0
+ ...
0
+ </show-page>
0
+
0
+Some things to note:
0
+
0
+ - The `<collection>` tag has `part="members"`. This creates a re-loadable section of the page, much as you would achieve with partials in regular Rails.
0
+
0
+ - The `<form>` tag has `update="members"`. The presence of this attribute turns the form into an ajax form. Submitting the form will cause the "members" part to be updated.
0
+
0
+ - The `<name-one>` tag creates an input field for the user association with auto-completion. The `complete-target` and `completer` attributes are used to determine the URL of the completer action.
0
+
0
+## Removing members
0
+
0
+The sidebar we just implemented has an obvious draw-back -- there's no way to remove members. In typical RESTful style, removing a member is achieved by deleting a membership. What we'd like is a delete button on each card that removes the membership. That means what we really want are *Membership* cards in the sidebar (at the moment they are User cards).
0
+
0
+The first step is to define a Membership card. We'll define it in terms of base-card, with the single change that the card heading should be the name of the user (and we'll make it a link to the user while we're at it ):
0
+
0
+ <def tag="card" for="ProjectMembership">
0
+ <base-card merge>
0
+ <heading:><a:user/></heading:>
0
+ </base-card>
0
+ </def>
0
+
0
+Now we can change projects/show.dryml so that it renders these cards. We'll also show a little with DRYML's implicit context -- we set the context on the whole section of the page so that we don't have to set it multiple times:
0
+
0
+ <aside:>
0
+ <section:memberships class="memberships">
0
+ <h2>Project Memberships</h2>
0
+ <collection part="members"/>
0
+
0
+ <form:new update="members" reset-form refocus-form>
0
+ <div>
0
+ Add a member:
0
+ <name-one:user complete-target="&@project" completer="new_member_name"/>
0
+ </div>
0
+ </form:new>
0
+ </section:memberships>
0
+ </aside:>
0
+
0