Misc updates to factor out find()'ing an object into a common 'object' method and...
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / BasicCRUD.pod
index 7da68dc..0aa7e2c 100644 (file)
@@ -66,9 +66,17 @@ focus on the Create and Delete aspects of CRUD.  More advanced
 capabilities, including full Update functionality, will be addressed in
 Part 9.
 
+Although this part of the tutorial will show you how to build CRUD 
+functionality yourself, another option is to use a "CRUD builder" type 
+of tool to automate the process.  You get less control, but it's quick 
+and easy.  For example, see 
+L<CatalystX::ListFramework::Builder|CatalystX::ListFramework::Builder>,
+L<CatalystX::CRUD|CatalystX::CRUD>, and 
+L<CatalystX::CRUD::YUI|CatalystX::CRUD::YUI>.
+
 You can checkout the source code for this example from the catalyst
 subversion repository as per the instructions in
-L<Catalyst::Manual::Tutorial::Intro>
+L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>.
 
 
 =head1 FORMLESS SUBMISSION
@@ -172,22 +180,20 @@ Edit C<root/src/books/create_done.tt2> and then enter:
     [% Dumper.dump(book) %]
     </pre>
 
-The TT C<USE> directive allows access to a variety of plugin modules (TT
-plugins, that is, not Catalyst plugins) to add extra functionality to
-the base TT capabilities.  Here, the plugin allows L<Data::Dumper>
-"pretty printing" of objects and variables.  Other than that, the rest
-of the code should be familiar from the examples in Part 3.
+The TT C<USE> directive allows access to a variety of plugin modules 
+(TT plugins, that is, not Catalyst plugins) to add extra functionality 
+to the base TT capabilities.  Here, the plugin allows 
+L<Data::Dumper|Data::Dumper> "pretty printing" of objects and 
+variables.  Other than that, the rest of the code should be familiar 
+from the examples in Part 3.
 
-B<IMPORTANT NOTE> As mentioned earlier, the C<MyApp::View::TT.pm> view
-class created by TTSite redefines the name used to access the Catalyst
-context object in TT templates from the usual C<c> to C<Catalyst>.
 
 =head2 Try the C<url_create> Feature
 
 If the application is still running from before, use C<Ctrl-C> to kill
 it. Then restart the server:
 
-    $ script/myapp_server.pl
+    $ DBIC_TRACE=1 script/myapp_server.pl
 
 Note that new path for C</books/url_create> appears in the startup debug
 output.
@@ -203,7 +209,7 @@ Next, use your browser to enter the following URL:
 
     http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
 
-Your browser should display " Added book 'TCPIP_Illustrated_Vol-2' by
+Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by
 'Stevens' with a rating of 5." along with a dump of the new book model
 object.  You should also see the following DBIC debug messages displayed
 in the development server log messages if you have DBIC_TRACE set:
@@ -218,19 +224,236 @@ The C<INSERT> statements are obviously adding the book and linking it to
 the existing record for Richard Stevens.  The C<SELECT> statement results
 from DBIC automatically fetching the book for the C<Dumper.dump(book)>.
 
-If you then click the "Return to list" link, you should find that there
-are now six books shown (if necessary, Shift-Reload your browser at the
-C</books/list> page).
+If you then click the "Return to list" link, you should find that 
+there are now six books shown (if necessary, Shift+Reload or 
+Ctrl+Reload your browser at the C</books/list> page).
 
-Then I<add 2 more copies of the same book> so that we have some extras for
-our delete logic that will be coming up soon.  Enter the same URL above
-two more times (or refresh your browser twice if it still contains this
-URL):
 
-    http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+=head1 CONVERT TO A CHAINED ACTION
+
+Although the example above uses the same C<Local> action type for the 
+method that we saw in the previous part of the tutorial, there is an 
+alternate approach that allows us to be more specific while also 
+paving the way for more advanced capabilities.  Change the method 
+declaration for C<url_create> in C<lib/MyApp/Controller/Books.pm> you 
+entered above to match the following:
+
+    sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) {
+
+This converts the method to take advantage of the Chained 
+action/dispatch type. Chaining let's you have a single URL 
+automatically dispatch to several controller methods, each of which 
+can have precise control over the number of arguments that it will 
+receive.  A chain can essentially be thought of having three parts --
+a beginning, a middle and an end.  The bullets below summarize the key 
+points behind each of these parts of a chain:
+
+
+=over 4
+
+
+=item *
+
+Beginning
+
+=over 4
+
+=item *
+
+B<Use "C<:Chained('/')>" to start a chain>
+
+=item *
+
+Get arguments through C<CaptureArgs()>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=item *
+
+Middle
+
+=over 4
+
+=item *
+
+Link to previous part of the chain with C<:Chained('_name_')>
+
+=item *
+
+Get arguments through C<CaptureArgs()>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=item *
+
+End
+
+=over 4
+
+=item *
+
+Link to previous part of the chain with C<:Chained('_name_')>
 
-You should be able to click "Return to list" and now see 3 copies of 
-"TCP_Illustrated_Vol-2".
+=item *
+
+B<Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=back
+
+In our C<url_create> method above, we have combined all 3 parts into a 
+single method: C<:Chained('/')> to start the chain, 
+C<:PathPart('books/url_create')> to specify the base URL to match, 
+along with C<:Args(3)> to capture exactly 3 arguments and also end the 
+chain.
+
+As we will see shortly, a chain can consist of as many "links" as you 
+wish, with each part capturing some arguments and doing some work 
+along the way.  We will continue to use the Chained action type in this 
+part of the tutorial and explore slightly more advanced capabilities 
+with the base method and delete feature below.  But Chained dispatch 
+is capable of far more.  For additional information, see 
+L<Catalyst::Manual::Intro/Action types>, 
+L<Catalyst::DispatchType::Chained|Catalyst::DispatchType::Chained>, 
+and the 2006 advent calendar entry on the subject: 
+L<http://www.catalystframework.org/calendar/2006/10>.
+
+
+=head2 Try the Chained Action
+
+If you look back at the development server startup logs from your 
+initial version of the C<url_create> method (the one using the 
+C<:Local> attribute), you will notice that it produced output similar
+to the following:
+
+       [debug] Loaded Path actions:
+       .-------------------------------------+--------------------------------------.
+       | Path                                | Private                              |
+       +-------------------------------------+--------------------------------------+
+       | /                                   | /default                             |
+       | /                                   | /index                               |
+       | /books                              | /books/index                         |
+       | /books/list                         | /books/list                          |
+       | /books/url_create                   | /books/url_create                    |
+       '-------------------------------------+--------------------------------------'
+
+Now start the development server with our basic chained method in 
+place and the startup debug output should change to something along 
+the lines of the following:
+
+       [debug] Loaded Path actions:
+       .-------------------------------------+--------------------------------------.
+       | Path                                | Private                              |
+       +-------------------------------------+--------------------------------------+
+       | /                                   | /default                             |
+       | /                                   | /index                               |
+       | /books                              | /books/index                         |
+       | /books/list                         | /books/list                          |
+       '-------------------------------------+--------------------------------------'
+       
+       [debug] Loaded Chained actions:
+       .-------------------------------------+--------------------------------------.
+       | Path Spec                           | Private                              |
+       +-------------------------------------+--------------------------------------+
+       | /books/url_create/*/*/*             | /books/url_create                    |
+       '-------------------------------------+--------------------------------------'
+
+C<url_create> has disappeared form the "Loaded Path actions" section 
+but it now shows up under the newly created "Loaded Chained actions" 
+section.  And, the "/*/*/*" portion clearly shows that we have 
+specified that 3 arguments are required.
+
+As with our non-chained version of C<url_create>, use your browser to 
+enter the following URL:
+
+       http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+
+You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by 
+'Stevens' with a rating of 5." along with a dump of the new book model 
+object.  Click the "Return to list" link, you should find that there 
+are now seven books shown (two copies of TCPIP_Illustrated_Vol-2).
+
+
+=head2 Refactor to Use a "Base" Method to Start the Chains
+
+Let's make a quick update to our initial Chained action to show a 
+little more of the power of chaining.  First, open 
+C<lib/MyApp/Controller/Books.pm> in your editor and add the following
+method:
+
+       =head2 base
+       
+       Can place common logic to start chained dispatch here
+       
+       =cut
+       
+       sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
+           my ($self, $c) = @_;
+           
+           # Store the resultset in stash so it's available for other methods
+           $c->stash->{resultset} = $c->model('DB::Books');
+       
+           # Print a message to the debug log
+           $c->log->debug('*** INSIDE BASE METHOD ***');
+       }
+
+Here we print a log message and store the resultset in 
+C<$c-E<gt>stash-E<gt>{resultset}> so that it's automatically available 
+for other actions that chain off C<base>.  If your controller always 
+needs a book ID as it's first argument, you could have the base method 
+capture that argument (with C<:CaptureArgs(1)>) and use it to pull the 
+book object with that ID from the database and leave it in the stash for 
+later parts of your chains to then act upon. Because we have several 
+actions that don't need to retrieve a book (such as the C<url_create>
+we are working with now), we will instead add that functionality
+to a common C<object> action shortly.
+
+As for C<url_create>, let's modify it to first dispatch to C<base>. 
+Open up C<lib/MyApp/Controller/Books.pm> and edit the declaration for 
+C<url_create> to match the following:
+
+    sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
+
+Next, let's try out our refactored chain.  Restart the development
+server and notice that our "Loaded Chained actions" section has 
+changed slightly:
+       
+       [debug] Loaded Chained actions:
+       .-------------------------------------+--------------------------------------.
+       | Path Spec                           | Private                              |
+       +-------------------------------------+--------------------------------------+
+       | /books/url_create/*/*/*             | /books/base (0)                      |
+       |                                     | => /books/url_create                 |
+       '-------------------------------------+--------------------------------------'
+
+The "Path Spec" is the same, but now it maps to two Private actions as 
+we would expect.
+
+Once again, enter the following URL into your browser:
+
+       http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+
+The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a 
+rating of 5." and dump of the new book object should appear.  Also 
+notice the extra debug message in the development server output from 
+the C<base> method.  Click the "Return to list" link, you should find 
+that there are now eight books shown. 
 
 
 =head1 MANUALLY BUILDING A CREATE FORM
@@ -251,7 +474,7 @@ Edit C<lib/MyApp/Controller/Books.pm> and add the following method:
     
     =cut
     
-    sub form_create : Local {
+    sub form_create :Chained('base') :PathPart('form_create') :Args(0) {
         my ($self, $c) = @_;
     
         # Set the TT template to use
@@ -260,6 +483,7 @@ Edit C<lib/MyApp/Controller/Books.pm> and add the following method:
 
 This action simply invokes a view containing a book creation form.
 
+
 =head2 Add a Template for the Form
 
 Open C<root/src/books/form_create.tt2> in your editor and enter:
@@ -278,6 +502,7 @@ Open C<root/src/books/form_create.tt2> in your editor and enter:
 Note that we have specified the target of the form data as
 C<form_create_do>, the method created in the section that follows.
 
+
 =head2 Add a Method to Process Form Values and Update Database
 
 Edit C<lib/MyApp/Controller/Books.pm> and add the following method to
@@ -289,7 +514,7 @@ save the form information to the database:
     
     =cut
     
-    sub form_create_do : Local {
+    sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) {
         my ($self, $c) = @_;
     
         # Retrieve the values from the form
@@ -324,9 +549,24 @@ it.  Then restart the server:
 
     $ script/myapp_server.pl
 
+Notice that the server startup log reflects the two new chained 
+methods that we added:
+
+       [debug] Loaded Chained actions:
+       .-------------------------------------+--------------------------------------.
+       | Path Spec                           | Private                              |
+       +-------------------------------------+--------------------------------------+
+       | /books/form_create                  | /books/base (0)                      |
+       |                                     | => /books/form_create                |
+       | /books/form_create_do               | /books/base (0)                      |
+       |                                     | => /books/form_create_do             |
+       | /books/url_create/*/*/*             | /books/base (0)                      |
+       |                                     | => /books/url_create                 |
+       '-------------------------------------+--------------------------------------'
+
 Point your browser to L<http://localhost:3000/books/form_create> and
 enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an
-author ID of 4.  You should then be forwarded to the same
+author ID of 4.  You should then see the output of the same
 C<create_done.tt2> template seen in earlier examples.  Finally, click
 "Return to list" to view the full list of books.
 
@@ -380,33 +620,92 @@ and 2) the four lines for the Delete link near the bottom).
         </td>
         <td>
           [% # Add a link to delete a book %]
-          <a href="[% c.uri_for('delete', book.id) %]">Delete</a>
+          <a href="[% c.uri_for('id', book.id, 'delete') %]">Delete</a>
         </td>
       </tr>
     [% END -%]
     </table>
 
-The additional code is obviously designed to add a new column to the
-right side of the table with a C<Delete> "button" (for simplicity, links
-will be used instead of full HTML buttons).
+The additional code is obviously designed to add a new column to the 
+right side of the table with a C<Delete> "button" (for simplicity, 
+links will be used instead of full HTML buttons).
+
+B<Note:> You should use more than just a simple link with your 
+applications. Consider using some sort of of confirmation page 
+(typically with unique actions in your controller for both the 
+confirmation and the actual delete operation).  Also, you should try 
+to use an HTTP POST operation (versus the GET used here) for 
+operations that change the state of your application (e.g., the 
+database).
+
+
+=head2 Add a Common Method to Retrieve a Book for the Chain
+
+As mentioned earlier, since we have a mixture of actions that operate on 
+a single book ID and others that do no, we should not have C<base> 
+capture the book ID, find the corresponding book in the database and 
+save it in the stash for later links in the chain.  However, just 
+because that logic does not belong in C<base> doesn't mean that we can't 
+create another location to centralize that logic.  In our case, we will 
+create a method called C<object> that will store the specific book in 
+the stash. Chains that always operate on a single existing book can 
+chain off this method, but methods such as C<url_create> that don't 
+operate on an existing book can chain directly off base.
+
+To add the C<object> method, edit C<lib/MyApp/Controller/Books.pm>
+and add the following code:
+
+    sub object :Chained('base') :PathPart('id') :CaptureArgs(1) {
+        my ($self, $c, $id) = @_;
+        
+        # Find the book object and store it in the stash
+        $c->stash(object => $c->stash->{resultset}->find($id));
+        
+        # Make sure the lookup was successful.  You would probably
+        # want to do something like this in a real app:
+        #   $c->detach('/error_404') if !$c->stash->{object};
+        die "Book $id not found!" if !$c->stash->{object};
+    }
+
+Now, any other method that chains off C<object> will automatically
+have the appropriate book waiting for it in 
+C<$c-E<gt>stash-Egt>{object}>.
+
+Also note that we are using different technique for setting
+C<$c-E<gt>stash>.  The advantage of this style is that it let's you
+set multiple stash variables at a time.  For example:
+
+    $c->stash(object => $c->stash->{resultset}->find($id),
+              another_thing => 1);
+
+or as a hashref:
+
+    $c->stash({object => $c->stash->{resultset}->find($id),
+              another_thing => 1});
+
+Either format works, but the C<$c-E<gt>stash(name => value);>
+style is growing in popularity -- you may which to use it all
+the time (even when you are only setting a single value).
+
 
 =head2 Add a Delete Action to the Controller
 
 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
 following method:
 
-    =head2 delete 
+    =head2 delete
     
     Delete a book
         
     =cut
     
-    sub delete : Local {
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
         # $id = primary key of book to delete
-        my ($self, $c, $id) = @_;
+        my ($self, $c) = @_;
     
-        # Search for the book and then delete it
-        $c->model('DB::Books')->search({id => $id})->delete_all;
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_authors' entries
+        $c->stash->{object}->delete;
     
         # Set a status message to be displayed at the top of the view
         $c->stash->{status_msg} = "Book deleted.";
@@ -415,12 +714,10 @@ following method:
         $c->forward('list');
     }
 
-This method first deletes the book with the specified primary key ID.
-However, it also removes the corresponding entry from the
-C<book_authors> table.  Note that C<delete_all> was used instead of
-C<delete>: whereas C<delete_all> also removes the join table entries in
-C<book_authors>, C<delete> does not (only use C<delete_all> if you
-really need the cascading deletes... otherwise you are wasting resources).
+This method first deletes the book object saved by the C<object> method. 
+However, it also removes the corresponding entry from the 
+C<book_authors> table.  Note that C<delete> will cascade to also delete 
+the related join table entries in C<book_authors>.
 
 Then, rather than forwarding to a "delete done" page as we did with the
 earlier create example, it simply sets the C<status_msg> to display a
@@ -438,12 +735,35 @@ equivalent.
 If the application is still running from before, use C<Ctrl-C> to kill
 it.  Then restart the server:
 
-    $ script/myapp_server.pl
+    $ DBIC_TRACE=1 script/myapp_server.pl
+
+The C<delete> method now appears in the "Loaded Chained actions" section
+of the startup debug output:
+
+       [debug] Loaded Chained actions:
+    .-------------------------------------+--------------------------------------.
+    | Path Spec                           | Private                              |
+    +-------------------------------------+--------------------------------------+
+    | /books/id/*/delete                  | /books/base (0)                      |
+    |                                     | -> /books/object (1)                 |
+    |                                     | => /books/delete                     |
+    | /books/form_create                  | /books/base (0)                      |
+    |                                     | => /books/form_create                |
+    | /books/form_create_do               | /books/base (0)                      |
+    |                                     | => /books/form_create_do             |
+    | /books/url_create/*/*/*             | /books/base (0)                      |
+    |                                     | => /books/url_create                 |
+    '-------------------------------------+--------------------------------------'
 
 Then point your browser to L<http://localhost:3000/books/list> and click
 the "Delete" link next to the first "TCPIP_Illustrated_Vol-2".  A green 
 "Book deleted" status message should display at the top of the page, 
-along with a list of the eight remaining books.
+along with a list of the eight remaining books.  You will also see the
+cascading delete operation via the DBIC_TRACE output:
+
+    DELETE FROM books WHERE ( id = ? ): '6'
+    SELECT me.book_id, me.author_id FROM book_authors me WHERE ( me.book_id = ? ): '6'
+    DELETE FROM book_authors WHERE ( author_id = ? AND book_id = ? ): '4', '6'
 
 
 =head2 Fixing a Dangerous URL
@@ -468,18 +788,19 @@ To convert the forward used in the previous section to a redirect,
 open C<lib/MyApp/Controller/Books.pm> and edit the existing 
 C<sub delete> method to match:
 
-    =head2 delete 
+    =head2 delete
     
     Delete a book
-        
+    
     =cut
     
-    sub delete : Local {
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
         # $id = primary key of book to delete
         my ($self, $c, $id) = @_;
     
-        # Search for the book and then delete it
-        $c->model('DB::Books')->search({id => $id})->delete_all;
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_authors' entries
+        $c->stash->{object}->delete;
     
         # Set a status message to be displayed at the top of the view
         $c->stash->{status_msg} = "Book deleted.";
@@ -492,23 +813,25 @@ C<sub delete> method to match:
 =head2 Try the Delete and Redirect Logic
 
 Restart the development server and point your browser to 
-L<http://localhost:3000/books/list> and delete the first copy of 
-"TCPIP_Illustrated_Vol-2".  The URL in your browser should return to 
-the L<http://localhost:3000/books/list> URL, so that is an 
-improvement, but notice that I<no green "Book deleted" status message 
-is displayed>.  Because the stash is reset on every request (and a 
-redirect involves a second request), the C<status_msg> is cleared 
-before it can be displayed.
+L<http://localhost:3000/books/list> (don't just hit "Refresh" in your 
+browser since we left the URL in an invalid state in the previous 
+section!) and delete the first copy of the remaining two 
+"TCPIP_Illustrated_Vol-2" books.  The URL in your browser should return 
+to the L<http://localhost:3000/books/list> URL, so that is an 
+improvement, but notice that I<no green "Book deleted" status message is 
+displayed>.  Because the stash is reset on every request (and a redirect 
+involves a second request), the C<status_msg> is cleared before it can 
+be displayed.
 
 
 =head2 Using C<uri_for> to Pass Query Parameters
 
-There are several ways to pass information across a redirect. 
-In general, the best option is to use the C<flash> technique that we
-will see in Part 5 of the tutorial; however, here we will pass the
-information via query parameters on the redirect itself.  Open 
-C<lib/MyApp/Controller/Books.pm> and update the existing 
-C<sub delete> method to match the following:
+There are several ways to pass information across a redirect. One 
+option is to use the C<flash> technique that we will see in Part 5 of 
+the tutorial; however, here we will pass the information via query 
+parameters on the redirect itself.  Open 
+C<lib/MyApp/Controller/Books.pm> and update the existing C<sub delete> 
+method to match the following:
 
     =head2 delete 
     
@@ -516,12 +839,13 @@ C<sub delete> method to match the following:
         
     =cut
     
-    sub delete : Local {
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
         # $id = primary key of book to delete
         my ($self, $c, $id) = @_;
     
-        # Search for the book and then delete it
-        $c->model('DB::Books')->search({id => $id})->delete_all;
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_authors' entries
+        $c->stash->{object}->delete;
     
         # Redirect the user back to the list page with status msg as an arg
         $c->response->redirect($c->uri_for('/books/list', 
@@ -530,35 +854,46 @@ C<sub delete> method to match the following:
 
 This modification simply leverages the ability of C<uri_for> to include
 an arbitrary number of name/value pairs in a hash reference.  Next, we 
-need to update C<root/lib/site/layout> to handle C<status_msg> as a 
+need to update C<root/src/wrapper.tt2> to handle C<status_msg> as a 
 query parameter:
 
-    <div id="header">[% PROCESS site/header %]</div>
-    
+    ...
     <div id="content">
-    <span class="message">[% status_msg || c.request.params.status_msg %]</span>
-    <span class="error">[% error_msg %]</span>
-    [% content %]
-    </div>
-    
-    <div id="footer">[% PROCESS site/footer %]</div>
+        [%# Status and error messages %]
+        <span class="message">[% status_msg || c.request.params.status_msg %]</span>
+        <span class="error">[% error_msg %]</span>
+        [%# This is where TT will stick all of your template's contents. -%]
+        [% content %]
+    </div><!-- end content -->
+    ...
+
+Although the sample above only shows the C<content> div, leave the 
+rest of the file intact -- the only change we made to the C<wrapper.tt2>
+was to add "C<|| c.request.params.status_msg>" to the 
+C<E<lt>span class="message"E<gt>> line.
 
 
 =head2 Try the Delete and Redirect With Query Param Logic
 
 Restart the development server and point your browser to 
-L<http://localhost:3000/books/list>.  Then delete the remaining copy 
-of "TCPIP_Illustrated_Vol-2".  The green "Book deleted" status message
+L<http://localhost:3000/books/list> (you should now be able to safely 
+hit "refresh" in your browser).  Then delete the remaining copy of 
+"TCPIP_Illustrated_Vol-2".  The green "Book deleted" status message 
 should return.
 
-B<NOTE:> Although this did present an opportunity to show a handy
-capability of C<uri_for>, it would be much better to use Catalyst's
-C<flash> feature in this situation.  Although the technique here is 
-less dangerous than leaving the delete URL in the client's browser, 
-we have still exposed the status message to the user.  With C<flash>, 
-this message returns to its rightful place as a service-side 
-mechanism (we will migrate this code to C<flash> in the next part 
-of the tutorial).
+B<NOTE:> Another popular method for maintaining server-side 
+information across a redirect is to use the C<flash> technique we 
+discuss in the next part of the tutorial, 
+L<Authentication|Catalyst::Manual::Tutorial::Authentication>. While 
+C<flash> is a "slicker" mechanism in that it's all handled by the 
+server and doesn't "pollute" your URLs, B<it is important to note that 
+C<flash> can lead to situations where the wrong information shows up 
+in the wrong browser window if the user has multiple windows or 
+browser tabs open.>  For example, Window A causes something to be 
+placed in the stash, but before that window performs a redirect, 
+Window B makes a request to the server and gets the status information 
+that should really go to Window A.  For this reason, you may wish
+to use the "query param" technique shown here in your applications.
 
 
 =head1 AUTHOR
@@ -567,8 +902,7 @@ Kennedy Clark, C<hkclark@gmail.com>
 
 Please report any errors, issues or suggestions to the author.  The
 most recent version of the Catalyst Tutorial can be found at
-L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
+L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>.
 
 Copyright 2006-2008, Kennedy Clark, under Creative Commons License
-(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).
-
+(L<http://creativecommons.org/licenses/by-sa/3.0/us/>).