Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8: Advanced CRUD
+
=head1 OVERVIEW
This is B<Part 8 of 9> for the Catalyst tutorial.
(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
<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
}
# 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;
$ 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.
$ 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.
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>
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
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:
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
[% # 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(', ') %]
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
$ 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
'----------------------+--------------------------------------+--------------'
[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
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);
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
'-------------------------------------+--------------------------------------'
[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:
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
# 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
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
Catalyst::Manual::Tutorial::Testing - Catalyst Tutorial - Part 7: Testing
+
=head1 OVERVIEW
This is B<Part 7 of 9> for the Catalyst tutorial.
"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;
$_->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",
$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");
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.