Updates and additions to the tutorial.
Kennedy Clark [Thu, 21 Sep 2006 02:07:39 +0000 (02:07 +0000)]
lib/Catalyst/Manual/Tutorial/AdvancedCRUD.pod
lib/Catalyst/Manual/Tutorial/Authentication.pod
lib/Catalyst/Manual/Tutorial/BasicCRUD.pod
lib/Catalyst/Manual/Tutorial/CatalystBasics.pod
lib/Catalyst/Manual/Tutorial/Testing.pod

index 96fb811..22d4ecc 100644 (file)
@@ -2,6 +2,7 @@
 
 Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8: Advanced CRUD
 
+
 =head1 OVERVIEW
 
 This is B<Part 8 of 9> for the Catalyst tutorial.
@@ -199,6 +200,15 @@ so allows us to have the same form submit the data to different actions
 (e.g., C<hw_create_do> for a create operation but C<hw_update_do> to
 update an existing book object).
 
+B<NOTE:> If you receive an error about Catalyst not being able to find
+the template C<hw_create_do.tt2>, please verify that you followed the
+instructions in the final section of
+L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> where
+you returned to a manually specified template.  You can either use 
+C<forward>/C<detach> B<OR> default template names, but the two cannot
+be used together.
+
+
 =head2 Update the CSS
 
 Edit C<root/src/ttsite.css> and add the following lines to the bottom of
@@ -249,9 +259,9 @@ the bottom of the existing file:
     <p>
       HTML::Widget:
       <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
-      <a href="[% Catalyst.uri_for('hw_update') %]">Update</a>
     </p>
 
+
 =head2 Test The <HTML::Widget> Create Form
 
 Press C<Ctrl-C> to kill the previous server instance (if it's still
@@ -540,7 +550,7 @@ match the following code:
             }
     
             # Set a status message for the user
-            $c->stash->{status_msg} = 'Book created';
+            $c->flash->{status_msg} = 'Book created';
             
             # Redisplay an empty form for another
             $c->stash->{widget_result} = $w->result;
@@ -571,7 +581,7 @@ running) and restart it:
 
     $ script/myapp_server.pl
 
-Try adding a book that validate.  Return to the book list and the book 
+Try adding a book that validates.  Return to the book list and the book 
 you added should be visible.
 
 
@@ -738,7 +748,7 @@ running) and restart it:
 
     $ script/myapp_server.pl
 
-Try adding a book that validate.  Return to the book list and the book 
+Try adding a book that validates.  Return to the book list and the book 
 you added should be visible.
 
 
index bde0033..b330fd7 100644 (file)
@@ -797,6 +797,71 @@ of this module on your system:
     perl -MCatalyst::Plugin::Authorization::ACL -e 'print $Catalyst::Plugin::Authorization::ACL::VERSION, "\n";'
 
 
+=head1 USING THE SESSION FOR FLASH
+
+As discussed in Part 3 of the tutorial, C<flash> allows you to set
+variables in a way that is very similar to C<stash>, but it will 
+remain set across multiple requests.  Once the value is read, it
+is cleared (unless reset).  Although C<flash> has nothing to do with
+authentication, it does leverage the same session plugins.  Now that
+those plugins are enabled, let's go back and improve the "delete
+and redirect with query parameters" code seen at the end of the
+C<BasicCRUD> part of the tutorial.
+
+First, open C<lib/MyApp/Controller/Books.pm> and modify C<sub delete>
+to match the following:
+
+    =head2 delete 
+    
+    Delete a book
+        
+    =cut
+    
+    sub delete : Local {
+        # $id = primary key of book to delete
+        my ($self, $c, $id) = @_;
+    
+        # Search for the book and then delete it
+        $c->model('MyAppDB::Book')->search({id => $id})->delete_all;
+    
+        # Use 'flash' to save information across requests util it's read
+        $c->flash->{status_msg} = "Book deleted";
+            
+        # Redirect the user back to the list page with status msg as an arg
+        $c->response->redirect($c->uri_for('/books/list'));
+    }
+
+Next, open C<root/lib/site/layout> update the TT code to pull from flash
+vs. the C<status_msg> query parameter:
+
+    <div id="header">[% PROCESS site/header %]</div>
+    
+    <div id="content">
+    <span class="message">[% status_msg || Catalyst.flash.status_msg %]</span>
+    <span class="error">[% error_msg %]</span>
+    [% content %]
+    </div>
+    
+    <div id="footer">[% PROCESS site/footer %]</div>
+
+
+=head2 Try Out Flash
+
+Restart the development server and point your browser to 
+L<http://localhost:3000/books/url_create/Test/1/4> to create an extra
+book.  Click the "Return to list" link and delete this "Test" book.  
+The C<flash> mechanism should retain our "Book deleted" status message
+across the redirect.
+
+B<NOTE:> While C<flash> will save information across multiple requests,
+it does get cleared the first time it is read.  In general, this is
+exactly what you want -- the C<flash> message will get displayed on
+the next screen where it's appropriate, but it won't "keep showing up"
+after that first time (unless you reset it).  Please refer to
+L<Catalyst::Plugin::Session|Catalyst::Plugin::Session> for additional
+information.
+
+
 =head1 AUTHOR
 
 Kennedy Clark, C<hkclark@gmail.com>
index b066395..75eb95e 100644 (file)
@@ -206,11 +206,28 @@ in the development server log messages:
 
     INSERT INTO books (rating, title) VALUES (?, ?): `5', `TCPIP_Illustrated_Vol-2'
     INSERT INTO book_authors (author_id, book_id) VALUES (?, ?): `4', `6'
+    SELECT author.id, author.first_name, author.last_name 
+        FROM book_authors me  JOIN authors author 
+        ON ( author.id = me.author_id ) WHERE ( me.book_id = ? ): '6'
+
+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).
 
+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
+
+You should be able to click "Return to list" and now see 3 copies of 
+"TCP_Illustrated_Vol-2".
+
 
 =head1 MANUALLY BUILDING A CREATE FORM
 
@@ -257,7 +274,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 Method to Process Form Values and Update Database
+=head2 Add a Method to Process Form Values and Update Database
 
 Edit C<lib/MyApp/Controller/Books.pm> and add the following method to
 save the form information to the database:
@@ -313,6 +330,7 @@ B<Note:> Having the user enter the primary key ID for the author is
 obviously crude; we will address this concern with a drop-down list in
 Part 8.
 
+
 =head1 A SIMPLE DELETE FEATURE
 
 Turning our attention to the delete portion of CRUD, this section
@@ -351,7 +369,7 @@ and 2) the four lines for the Delete link near the bottom).
           [% # call it and discard the return value.                                 -%]
           [% tt_authors = [ ];
              tt_authors.push(author.last_name) FOREACH author = book.authors %]
-          [% # Now use a TT 'virtual method' to display the author count             -%]
+          [% # Now use a TT 'virtual method' to display the author count in parens   -%]
           ([% tt_authors.size %])
           [% # Use another TT vmethod to join & print the names & comma separators   -%]
           [% tt_authors.join(', ') %]
@@ -409,15 +427,6 @@ Whereas C<forward> I<returns> to the original action once it is
 completed, C<detach> does I<not> return.  Other than that, the two are
 equivalent.
 
-Another alternative to C<forward> would be to use
-C<$c-E<gt>response-E<gt>redirect($c-E<gt>uri_for('/books/list'))>.  The
-C<forward> and C<redirect> operations differ in several important
-respects that stem from the fact that redirects cause the client browser
-to issue an entirely new HTTP request.  In doing so, this results in a
-new URL showing in the browser window.  And, because the stash
-information is reset for every request, the "Book deleted" message would
-not be displayed.
-
 
 =head2 Try the Delete Feature
 
@@ -427,9 +436,120 @@ it.  Then restart the server:
     $ script/myapp_server.pl
 
 Then point your browser to L<http://localhost:3000/books/list> and click
-the "Delete" link next to "TCPIP_Illustrated_Vol-2".  A green "Book
-deleted" status message should display at the top of the page, along
-with a list of the six remaining books.
+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.
+
+
+=head2 Fixing a Dangerous URL
+
+Note the URL in your browser once you have performed the deleted in the 
+prior step -- it is still referencing the delete action:
+
+    http://localhost:3000/books/delete/6
+
+What if the user were to press reload with this URL still active?  In
+this case the redundant delete is harmless, but in other cases this
+could clearly be extremely dangerous.
+
+We can improve the logic by converting to a redirect.  Unlike
+C<$c-E<gt>forward('list'))> or C<$c-E<gt>detach('list'))> that perform
+a server-side alteration in the flow of processing, a redirect is a
+client-side mechanism that causes the brower to issue an entirely
+new request.  As a result, the URL in the browser is updated to match
+the destination of the redirection URL.
+
+To convert the forward used in the previous section to a redirect,
+open C<lib/MyApp/Controller/Books.pm> and the existing C<sub delete>
+method to match:
+
+    =head2 delete 
+    
+    Delete a book
+        
+    =cut
+    
+    sub delete : Local {
+        # $id = primary key of book to delete
+        my ($self, $c, $id) = @_;
+    
+        # Search for the book and then delete it
+        $c->model('MyAppDB::Book')->search({id => $id})->delete_all;
+    
+        # Set a status message to be displayed at the top of the view
+        $c->stash->{status_msg} = "Book deleted.";
+    
+        # Redirect the user back to the list page
+        $c->response->redirect($c->uri_for('/books/list'));
+    }
+
+
+=head2 Try the Delete and Redirect Logic
+
+Restart the development server and point your browser to 
+L<http://localhost:3000/books/list>.  Delete the first copy of 
+"TCPIP_Illustrated_Vol-2", but notice that I<no green "Book deleted" 
+status message is displayed>.  Because the stash is reset on every
+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 4 of the tutorial; however, here we will pass the
+information via the redirect itself.  Open 
+C<lib/MyApp/Controller/Books.pm> and update the existing 
+C<sub delete> method to match the following:
+
+    =head2 delete 
+    
+    Delete a book
+        
+    =cut
+    
+    sub delete : Local {
+        # $id = primary key of book to delete
+        my ($self, $c, $id) = @_;
+    
+        # Search for the book and then delete it
+        $c->model('MyAppDB::Book')->search({id => $id})->delete_all;
+    
+        # Redirect the user back to the list page with status msg as an arg
+        $c->response->redirect($c->uri_for('/books/list', 
+            {status_msg => "Book deleted."}));
+    }
+
+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 
+query parameter:
+
+    <div id="header">[% PROCESS site/header %]</div>
+    
+    <div id="content">
+    <span class="message">[% status_msg || Catalyst.request.params.status_msg %]</span>
+    <span class="error">[% error_msg %]</span>
+    [% content %]
+    </div>
+    
+    <div id="footer">[% PROCESS site/footer %]</div>
+
+
+=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> and 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 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).
 
 
 =head1 AUTHOR
index 97ff614..d1303bb 100644 (file)
@@ -163,7 +163,7 @@ application with the built-in development web server:
     '----------------------+--------------------------------------+--------------'
     
     [info] MyApp powered by Catalyst 5.7002
-    You can connect to your server at http://localhost.localdomain:3000
+    You can connect to your server at http://localhost:3000
 
 B<NOTE>: Be sure you run the C<script/myapp_server.pl> command from the
 'base' directory of your application, not inside the C<script> directory 
@@ -228,7 +228,7 @@ in your editor and enter:
     INSERT INTO authors VALUES (4, 'Richard', 'Stevens');
     INSERT INTO authors VALUES (5, 'Douglas', 'Comer');
     INSERT INTO authors VALUES (6, 'Tom', 'Christiansen');
-    INSERT INTO authors VALUES (7, ' Nathan', 'Torkington');
+    INSERT INTO authors VALUES (7, 'Nathan', 'Torkington');
     INSERT INTO authors VALUES (8, 'Jeffrey', 'Zeldman');
     INSERT INTO book_authors VALUES (1, 1);
     INSERT INTO book_authors VALUES (1, 2);
@@ -442,6 +442,10 @@ B<Note:> C<__PACKAGE__> is just a shorthand way of referencing the name
 of the package where it is used.  Therefore, in C<MyAppDB.pm>,
 C<__PACKAGE__> is equivalent to C<MyAppDB>.
 
+B<Note:> As with any Perl package, we need to end the last line with
+a statement that evaluates to C<true>.  This is customarily done with
+C<1> on a line by itself as shown above.
+
 
 =head2 Create the DBIC "Result Source" Files
 
@@ -1100,7 +1104,7 @@ Your development server log output should display something like:
     '-------------------------------------+--------------------------------------'
     
     [info] MyApp powered by Catalyst 5.7002
-    You can connect to your server at http://localhost.localdomain:3000
+    You can connect to your server at http://localhost:3000
 
 Some things you should note in the output above:
 
@@ -1152,11 +1156,12 @@ information for each book.
 By default, C<Catalyst::View::TT> will look for a template that uses the 
 same name as your controller action, allowing you to save the step of 
 manually specifying the template name in each action.  For example, this 
-would allow us to remove (or comment out) the 
+would allow us to remove the 
 C<$c-E<gt>stash-E<gt>{template} = 'books/list.tt2';> line of our 
 C<list> action in the Books controller.  Open 
-C<lib/MyApp/Controller/Books.pm> in your editor and update it to 
-match the following:
+C<lib/MyApp/Controller/Books.pm> in your editor and comment out this line
+to match the following (only the C<$c-E<gt>stash-E<gt>{template}> line
+has changed):
 
     =head2 list
     
@@ -1174,8 +1179,10 @@ match the following:
         # stash where they can be accessed by the TT template
         $c->stash->{books} = [$c->model('MyAppDB::Book')->all];
     
-        # Automatically look for a template of 'books/list.tt2' template
-        # (if TEMPLATE_EXTENSION is set to '.tt2')
+        # Set the TT template to use.  You will almost always want to do this
+        # in your action methods (actions methods respond to user input in
+        # your controllers).
+        #$c->stash->{template} = 'books/list.tt2';
     }
 
 C<Catalyst::View::TT> defaults to looking for a template with no 
@@ -1200,11 +1207,26 @@ You should now be able to restart the development server as per the
 previous section and access the L<http://localhost:3000/books/list>
 as before.
 
-Although this can be a valuable technique to establish a default 
-template for each of your actions, the remainder of the tutorial
-will manually assign the template name to 
-C<$c-E<gt>stash-E<gt>{template}> in each action in order to make 
-the logic as conspicuous as possible.
+B<NOTE:> Please note that if you use the default template technique,
+you will B<not> be able to use either the C<$c-E<gt>forward> or
+the C<$c-E<gt>detach> mechanisms (these are discussed in Part 2 and 
+Part 8 of the Tutorial).
+
+
+=head1 RETURN TO A MANUALLY SPECIFIED TEMPLATE
+
+In order to be able to use C<$c-E<gt>forward> and C<$c-E<gt>detach>
+later in the tutorial, you should remove the comment from the
+statement in C<sub list>:
+
+    $c->stash->{template} = 'books/list.tt2';
+
+Then delete the C<TEMPLATE_EXTENSION> line in  
+C<lib/MyApp/View/TT.pm>.
+
+You should then be able to restart the development server and 
+access L<http://localhost:3000/books/list> in the same manner as
+with earlier sections.
 
 
 =head1 AUTHOR
index ee3666b..f3b2b64 100644 (file)
@@ -2,6 +2,7 @@
 
 Catalyst::Manual::Tutorial::Testing - Catalyst Tutorial - Part 7: Testing
 
+
 =head1 OVERVIEW
 
 This is B<Part 7 of 9> for the Catalyst tutorial.
@@ -176,8 +177,14 @@ editor and enter the following:
         "Check we are NOT logged in") for $ua1, $ua2;
     
     # Log in as each user
+    # Specify username and password on the URL
     $ua1->get_ok("http://localhost/login?username=test01&password=mypass", "Login 'test01'");
-    $ua2->get_ok("http://localhost/login?username=test02&password=mypass", "Login 'test02'");
+    # Use the form for user 'test02'; note there is no description here
+    $ua2->submit_form(
+        fields => {
+            username => 'test02',
+            password => 'mypass',
+        });
     
     # Go back to the login page and it should show that we are already logged in
     $_->get_ok("http://localhost/login", "Return to '/login'") for $ua1, $ua2;
@@ -185,7 +192,7 @@ editor and enter the following:
     $_->content_contains("Please Note: You are already logged in as ",
         "Check we ARE logged in" ) for $ua1, $ua2;
     
-    # 'Click' the 'Logout' link
+    # 'Click' the 'Logout' link (see also 'text_regex' and 'url_regex' options)
     $_->follow_link_ok({n => 1}, "Logout via first link on page") for $ua1, $ua2;
     $_->title_is("Login", "Check for login title") for $ua1, $ua2;
     $_->content_contains("You need to log in to use this application",
@@ -233,7 +240,7 @@ editor and enter the following:
     $ua1->get_ok($delLinks[$#delLinks]->url, 'Delete last book');
     # Check that delete worked
     $ua1->content_contains("Book List", "Book List page test");
-    $ua1->content_contains("Book deleted.", "Book was deleted");
+    $ua1->content_contains("Book deleted", "Book was deleted");
     
     # User 'test02' should not be able to add a book
     $ua2->get_ok("http://localhost/books/url_create/TestTitle2/2/5", "'test02' add");
@@ -277,12 +284,31 @@ Note that although this tutorial uses a single custom test case for
 simplicity, you may wish to break your tests into different files for
 better organization.
 
+B<TIP:> If you have a test case that fails, you will receive an error
+similar to the following:
+
+    #   Failed test 'Check we are NOT logged in'
+    #   in t/live_app01.t at line 31.
+    #     searched: "\x{0a}<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Tran"...
+    #   can't find: "You need to log in to use this application."
+
+Unfortunately, this only shows us the first 50 characters of the HTML
+returned by the request -- not enough to determine where the problem
+lies.  A simple technique that can be used in such situations is to 
+temporarily insert a line similar to the following right after the 
+failed test:
+
+    warn $ua1->content;
+
+This will cause the full HTML returned by the request to be displayed.
+
+
 =head1 SUPPORTING BOTH PRODUCTION AND TEST DATABASES
 
 You may wish to leverage the techniques discussed in this tutorial to
 maintain both a "production database" for your live application and a
 "testing database" for your test cases.  One advantage to
-L<Test::WWW::Mechanize::Catalyst> is that
+L<Test::WWW::Mechanize::Catalyst|Test::WWW::Mechanize::Catalyst> is that
 it runs your full application; however, this can complicate things when
 you want to support multiple databases.  One solution is to allow the
 database specification to be overridden with an environment variable.