Moving tutorial POD here
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / Tutorial / AdvancedCRUD.pod
diff --git a/lib/Catalyst/Manual/Tutorial/Tutorial/AdvancedCRUD.pod b/lib/Catalyst/Manual/Tutorial/Tutorial/AdvancedCRUD.pod
new file mode 100644 (file)
index 0000000..77b81cd
--- /dev/null
@@ -0,0 +1,764 @@
+=head1 NAME
+
+Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8: Advanced CRUD
+
+
+=head1 OVERVIEW
+
+This is B<Part 8 of 9> for the Catalyst tutorial.
+
+L<Tutorial Overview|Catalyst::Manual::Tutorial>
+
+=over 4
+
+=item 1
+
+L<Introduction|Catalyst::Manual::Tutorial::Intro>
+
+=item 2
+
+L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics>
+
+=item 3
+
+L<Basic CRUD|Catalyst::Manual::Tutorial_BasicCRUD>
+
+=item 4
+
+L<Authentication|Catalyst::Manual::Tutorial::Authentication>
+
+=item 5
+
+L<Authorization|Catalyst::Manual::Tutorial::Authorization>
+
+=item 6
+
+L<Debugging|Catalyst::Manual::Tutorial::Debugging>
+
+=item 7
+
+L<Testing|Catalyst::Manual::Tutorial::Testing>
+
+=item 8
+
+B<AdvancedCRUD>
+
+=item 9
+
+L<Appendices|Catalyst::Manual::Tutorial::Appendices>
+
+=back
+
+=head1 DESCRIPTION
+
+This part of the tutorial explores more advanced functionality for
+Create, Read, Update, and Delete (CRUD) than we saw in Part 3.  In
+particular, it looks at a number of techniques that can be useful for
+the Update portion of CRUD, such as automated form generation,
+validation of user-entered data, and automated transfer of data between
+forms and model objects.
+
+In keeping with the Catalyst (and Perl) spirit of flexibility, there are
+many different ways to approach advanced CRUD operations in a Catalyst
+environment.  One alternative is to use
+L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold> 
+to instantly construct a set of Controller methods and templates for 
+basic CRUD operations.  Although a popular subject in Quicktime 
+movies that serve as promotional material for various frameworks, 
+real-world applications generally require more control.  Other 
+options include L<Data::FormValidator|Data::FormValidator> and
+L<HTML::FillInForm|HTML::FillInForm>.
+
+Here, we will make use of the L<HTML::Widget|HTML::Widget> to not only 
+ease form creation, but to also provide validation of the submitted 
+data.  The approached used by this part of the tutorial is to slowly 
+incorporate additional L<HTML::Widget|HTML::Widget> functionality in a 
+step-wise fashion (we start with fairly simple form creation and then 
+move on to more complex and "magical" features such as validation and 
+auto-population/auto-saving).
+
+B<Note:> Part 8 of the tutorial is optional.  Users who do not wish to
+use L<HTML::Widget|HTML::Widget> may skip this part.
+
+You can checkout the source code for this example from the catalyst subversion repository as per the instructions in L<Catalyst::Manual::Tutorial::Intro>
+
+=head1 C<HTML::WIDGET> FORM CREATION
+
+This section looks at how L<HTML::Widget|HTML::Widget> can be used to
+add additional functionality to the manually created form from Part 3.
+
+=head2 Add the C<HTML::Widget> Plugin
+
+Open C<lib/MyApp.pm> in your editor and add the following to the list of
+plugins (be sure to leave the existing plugins enabled):
+
+    HTML::Widget
+
+=head2 Add a Form Creation Helper Method
+
+Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
+following method:
+
+    =head2 make_book_widget
+    
+    Build an HTML::Widget form for book creation and updates
+    
+    =cut
+    
+    sub make_book_widget {
+        my ($self, $c) = @_;
+    
+        # Create an HTML::Widget to build the form
+        my $w = $c->widget('book_form')->method('post');
+    
+        # Get authors
+        my @authorObjs = $c->model("MyAppDB::Author")->all();
+        my @authors = map {$_->id => $_->last_name }
+                           sort {$a->last_name cmp $b->last_name} @authorObjs;
+    
+        # Create the form feilds
+        $w->element('Textfield', 'title'  )->label('Title')->size(60);
+        $w->element('Textfield', 'rating' )->label('Rating')->size(1);
+        $w->element('Select',    'authors')->label('Authors')
+            ->options(@authors);
+        $w->element('Submit',    'submit' )->value('submit');
+    
+        # Return the widget    
+        return $w;
+    }
+
+This method provides a central location that builds an 
+HTML::Widget-based form with the appropriate fields.  The "Get authors" 
+code uses DBIC to retrieve a list of model objects and then uses C<map> 
+to create a hash where the hash keys are the database primary keys from 
+the authors table and the associated values are the last names of the 
+authors.
+
+=head2 Add Actions to Display and Save the Form
+
+Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
+following methods:
+
+    =head2 hw_create
+    
+    Build an HTML::Widget form for book creation and updates
+    
+    =cut
+    
+    sub hw_create : Local {
+        my ($self, $c) = @_;
+    
+        # Create the widget and set the action for the form
+        my $w = $self->make_book_widget($c);
+        $w->action($c->uri_for('hw_create_do'));
+    
+        # Write form to stash variable for use in template
+        $c->stash->{widget_result} = $w->result;
+    
+        # Set the template
+        $c->stash->{template} = 'books/hw_form.tt2';
+    }
+    
+    
+    =head2 hw_create_do
+    
+    Build an HTML::Widget form for book creation and updates
+    
+    =cut
+    
+    sub hw_create_do : Local {
+        my ($self, $c) = @_;
+    
+        # Retrieve the data from the form
+        my $title   = $c->request->params->{title};
+        my $rating  = $c->request->params->{rating};
+        my $authors = $c->request->params->{authors};
+    
+        # Call create() on the book model object. Pass the table 
+        # columns/field values we want to set as hash values
+        my $book = $c->model('MyAppDB::Book')->create({
+                title   => $title,
+                rating  => $rating
+            });
+        
+        # Add a record to the join table for this book, mapping to 
+        # appropriate author
+        $book->add_to_book_authors({author_id => $authors});
+        
+        # Set a status message for the user
+        $c->stash->{status_msg} = 'Book created';
+    
+        # Use 'hw_create' to redisplay the form.  As discussed in 
+        # Part 3, 'detach' is like 'forward', but it does not return
+        $c->detach('hw_create');
+    }
+
+Note how we use C<make_book_widget> to build the core parts of the form
+in one location, but we set the action (the URL the form is sent to when
+the user clicks the 'Submit' button) separately in C<hw_create>.  Doing
+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
+the file:
+
+    label {
+        display: block;
+        width: 10em;
+        position: relative;
+        margin: .5em 0em;
+    }
+    label input {
+        position: absolute;
+        left: 100%;
+    }
+    label select {
+        position: absolute;
+        left: 100%;
+    }
+    .submit {
+        margin-top: 2em;;
+    }
+    .error_messages {
+        color: [% site.col.error %];
+    }
+
+These changes will display form elements vertically and also show error
+messages in red.  Note that we are pulling the color scheme settings
+from the C<root/lib/config/col> file that was created by the TTSite
+helper.  This allows us to change the color used by various error styles
+in the CSS from a single location.
+
+=head2 Create a Template Page To Display The Form
+
+Open C<root/src/books/hw_form.tt2> in your editor and enter the following:
+
+    [% META title = 'Create/Update Book' %]
+    
+    [% widget_result.as_xml %]
+    
+    <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
+
+=head2 Add Links for Create and Update via C<HTML::Widget>
+
+Open C<root/src/books/list.tt2> in your editor and add the following to
+the bottom of the existing file:
+
+    <p>
+      HTML::Widget:
+      <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
+    </p>
+
+
+=head2 Test The <HTML::Widget> Create Form
+
+Press C<Ctrl-C> to kill the previous server instance (if it's still
+running) and restart it:
+
+    $ script/myapp_server.pl
+
+Login as C<test01>.  Once at the Book List page, click the HTML::Widget
+"Create" link to display for form produced by C<make_book_widget>.  Fill
+out the form with the following values: Title = "Internetworking with
+TCP/IP Vol. II", Rating = "4", and Author = "Comer".  Click Submit, and
+you will be returned to the Create/Update Book page with a "Book
+created" status message displayed.  Click "Return to book list" to view
+the newly created book on the main list.
+
+Also note that this implementation allows you to can create books with
+bogus information.  Although we have constrained the authors with the
+drop-down list, there are no restrictions on items such as the length of
+the title (for example, you can create a one-letter title) and value for
+the rating (you can use any number you want, and even non-numeric values
+with SQLite).  The next section will address this concern.
+
+B<Note:> Depending on the database you are using and how you established
+the columns in your tables, the database could obviously provide various
+levels of "type enforcement" on your data.  The key point being made in
+the previous paragraph is that the I<web application> itself is not
+performing any validation.
+
+=head1 C<HTML::WIDGET> VALIDATION AND FILTERING
+
+Although the use of L<HTML::Widget|HTML::Widget> in the previous section
+did provide an automated mechanism to build the form, the real power of
+this module stems from functionality that can automatically validate and
+filter the user input.  Validation uses constraints to be sure that
+users input appropriate data (for example, that the email field of a
+form contains a valid email address).  Filtering can be used to remove
+extraneous whitespace from fields or to escape meta-characters in user
+input.
+
+=head2 Add Constraints and Filters to the Widget Creation Method
+
+Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
+C<make_book_widget> method to match the following (new sections have
+been marked with a C<*** NEW:> comment):
+
+    sub make_book_widget {
+        my ($self, $c) = @_;
+    
+        # Create an HTML::Widget to build the form
+        my $w = $c->widget('book_form')->method('post');
+            
+        # Get authors
+        my @authorObjs = $c->model("MyAppDB::Author")->all();
+        my @authors = map {$_->id => $_->last_name }
+                           sort {$a->last_name cmp $b->last_name} @authorObjs;
+    
+        # Create the form feilds
+        $w->element('Textfield', 'title'  )->label('Title')->size(60);
+        $w->element('Textfield', 'rating' )->label('Rating')->size(1);
+        # ***NEW: Convert to multi-select list
+        $w->element('Select',    'authors')->label('Authors')
+            ->options(@authors)->multiple(1)->size(3);
+        $w->element('Submit',    'submit' )->value('submit');
+    
+        # ***NEW: Set constraints
+        $w->constraint(All     => qw/title rating authors/)
+            ->message('Required. ');
+        $w->constraint(Integer => qw/rating/)
+            ->message('Must be an integer. ');
+        $w->constraint(Range   => qw/rating/)->min(1)->max(5)
+            ->message('Must be a number between 1 and 5. ');
+        $w->constraint(Length  => qw/title/)->min(5)->max(50)
+            ->message('Must be between 5 and 50 characters. ');
+    
+        # ***NEW: Set filters
+        for my $column (qw/title rating authors/) {
+            $w->filter( HTMLEscape => $column );
+            $w->filter( TrimEdges  => $column );
+        }
+    
+        # Return the widget    
+        return $w;
+    }
+
+The main changes are:
+
+=over 4
+
+=item *
+
+The C<Select> element for C<authors> is changed from a single-select
+drop-down to a multi-select list by adding calls to C<multiple> (set to
+C<true>) and C<size> (set to the number of rows to display).
+
+=item *
+
+Four sets of constraints are added to provide validation of the user input.
+
+=item *
+
+Two filters are run on every field to remove and escape unwanted input.
+
+=back
+
+=head2 Rebuild the Form Submission Method to Include Validation
+
+Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
+match the following code (enough of the code is different that you
+probably want to cut and paste this over code the existing method):
+
+    sub hw_create_do : Local {
+        my ($self, $c) = @_;
+    
+        # Retrieve the data from the form
+        my $title   = $c->request->params->{title};
+        my $rating  = $c->request->params->{rating};
+        my $authors = $c->request->params->{authors};
+        
+        # Create the widget and set the action for the form
+        my $w = $self->make_book_widget($c);
+        $w->action($c->uri_for('hw_create_do'));
+    
+        # Validate the form parameters
+        my $result = $w->process($c->req);
+    
+        # Write form (including validation error messages) to
+        # stash variable for use in template
+        $c->stash->{widget_result} = $result;
+    
+        # Were their validation errors?
+        if ($result->has_errors) {
+            # Warn the user at the top of the form that there were errors.
+            # Note that there will also be per-field feedback on
+            # validation errors because of '$w->process($c->req)' above.
+            $c->stash->{error_msg} = 'Validation errors!';
+        } else {
+            # Everything validated OK, so do the create
+            # Call create() on the book model object. Pass the table
+            # columns/field values we want to set as hash values
+            my $book = $c->model('MyAppDB::Book')->create({
+                    title   => $title,
+                    rating  => $rating
+                });
+    
+            # Add a record to the join table for this book, mapping to
+            # appropriate author.  Note that $authors will be 1 author as
+            # a scalar or ref to list of authors depending on how many the
+            # user selected; the 'ref $authors ?...' handles both cases
+            foreach my $author (ref $authors ? @$authors : $authors) {
+                $book->add_to_book_authors({author_id => $author});
+            }    
+            # Set a status message for the user
+            $c->stash->{status_msg} = 'Book created';
+        }
+    
+        # Set the template
+        $c->stash->{template} = 'books/hw_form.tt2';
+    }
+
+The key changes to C<hw_create_do> are:
+
+=over 4
+
+=item *
+
+C<hw_create_do> no longer does a C<detach> to C<hw_create> to redisplay
+the form.  Now that C<hw_create_do> has to process the form in order to
+perform the validation, we go ahead and build a complete set of form
+presentation logic into C<hw_create_do> (for example, C<hw_create_do>
+now has a C<$c-E<gt>stash-E<gt>{template}> line).  Note that if we
+process the form in C<hw_create_do> I<and> forward/detach back to
+<hw_create>, we would end up with C<make_book_widget> being called
+twice, resulting in a duplicate set of elements being added to the form.
+(There are other ways to address the "duplicate form rendering" issue --
+just be aware that it exists.)
+
+=item *
+
+C<$w-E<gt>process($c-E<gt>req)> is called to run the validation logic.
+Not only does this set the C<has_errors> flag if validation errors are
+encountered, it returns a string containing any field-specific warning
+messages.
+
+=item *
+
+An C<if> statement checks if any validation errors were encountered.  If
+so, C<$c-E<gt>stash-E<gt>{error_msg}> is set and the input form is
+redisplayed.  If no errors were found, the object is created in a manner
+similar to the prior version of the C<hw_create_do> method.
+
+=back
+
+=head2 Try Out the Form
+
+Press C<Ctrl-C> to kill the previous server instance (if it's still 
+running) and restart it:
+
+    $ script/myapp_server.pl
+
+Now try adding a book with various errors: title less than 5 characters,
+non-numeric rating, a rating of 0 or 6, etc.  Also try selecting one,
+two, and zero authors.  When you click Submit, the HTML::Widget
+C<constraint> items will validate the logic and insert feedback as
+appropriate.
+
+
+=head1 Enable C<DBIx::Class::HTMLWidget> Support
+
+In this section we will take advantage of some of the "auto-population"
+features of C<DBIx::Class::HTMLWidget>.  Enabling
+C<DBIx::Class::HTMLWidget> provides two additional methods to your DBIC
+model classes:
+
+=over 4
+
+=item *
+
+fill_widget()
+
+Takes data from the database and transfers it to your form widget.
+
+=item *
+
+populate_from_widget()
+
+Takes data from a form widget and uses it to update the corresponding
+records in the database.
+
+=back
+
+In other words, the two methods are a mirror image of each other: one
+reads from the database while the other writes to the database.
+
+=head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
+
+In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
+need to add C<HTMLWidget> to the C<load_components> line of DBIC result
+source files that need to use the C<fill_widget> and
+C<populate_from_widget> methods.  In this case, open
+C<lib/MyAppDB/Book.pm> and update the C<load_components> line to match:
+
+       __PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
+
+=head2 Use C<populate_from_widget> in C<hw_create_do>
+
+Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
+match the following code:
+
+    =head2 hw_create_do
+    
+    Build an HTML::Widget form for book creation and updates
+    
+    =cut
+    
+    sub hw_create_do : Local {
+        my ($self, $c) = @_;
+    
+        # Create the widget and set the action for the form
+        my $w = $self->make_book_widget($c);
+        $w->action($c->uri_for('hw_create_do'));
+    
+        # Validate the form parameters
+        my $result = $w->process($c->req);
+    
+        # Write form (including validation error messages) to
+        # stash variable for use in template
+        $c->stash->{widget_result} = $result;
+    
+        # Were their validation errors?
+        if ($result->has_errors) {
+            # Warn the user at the top of the form that there were errors.
+            # Note that there will also be per-field feedback on
+            # validation errors because of '$w->process($c->req)' above.
+            $c->stash->{error_msg} = 'Validation errors!';
+        } else {
+            my $book = $c->model('MyAppDB::Book')->new({});
+            $book->populate_from_widget($result);
+    
+            # Add a record to the join table for this book, mapping to
+            # appropriate author.  Note that $authors will be 1 author as
+            # a scalar or ref to list of authors depending on how many the
+            # user selected; the 'ref $authors ?...' handles both cases
+            my $authors = $c->request->params->{authors};
+            foreach my $author (ref $authors ? @$authors : $authors) {
+                $book->add_to_book_authors({author_id => $author});
+            }
+    
+            # Set a status message for the user
+            $c->flash->{status_msg} = 'Book created';
+            
+            # Redisplay an empty form for another
+            $c->stash->{widget_result} = $w->result;
+        }
+    
+        # Set the template
+        $c->stash->{template} = 'books/hw_form.tt2';
+    }
+
+In this version of C<hw_create_do> we removed the logic that manually
+pulled the form variables and used them to call
+C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
+single call to C<$book-E<gt>populate_from_widget>.  Note that we still
+have to call C<$book-E<gt>add_to_book_authors> once per author because
+C<populate_from_widget> does not currently handle the relationships
+between tables.  Also, we reset the form to an empty fields by adding
+another call to C<$w-E<gt>result> and storing the output in the stash 
+(if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
+the form values already entered will be retained on redisplay --
+although this could be desirable for some applications, we avoid it
+here to help avoid the creation of duplicate records).
+
+
+=head2 Try Out the Form
+
+Press C<Ctrl-C> to kill the previous server instance (if it's still 
+running) and restart it:
+
+    $ script/myapp_server.pl
+
+Try adding a book that validates.  Return to the book list and the book 
+you added should be visible.
+
+
+
+=head1 Rendering C<HTMLWidget> Forms in a Table
+
+Some developers my wish to use the "old-fashioned" table style of 
+rendering a form in lieu of the default C<HTML::Widget> rendering that 
+assumes you will use CSS for formatting.  This section demonstrates
+some techniques that can override the default rendering with a 
+custom class.
+
+
+=head2 Add a New "Element Container"
+
+Open C<lib/FormElementContainer.pm> in your editor and enter:
+
+    package FormElementContainer;
+    
+    use base 'HTML::Widget::Container';
+    
+    sub _build_element {
+        my ($self, $element) = @_;
+    
+        return () unless $element;
+        if (ref $element eq 'ARRAY') {
+            return map { $self->_build_element($_) } @{$element};
+        }
+        my $e = $element->clone;
+        $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
+            if $self->error && $e->tag eq 'input';
+    
+        return $e ? ($e) : ();
+    }
+    
+    1;
+
+This simply dumps the HTML code for a given form element, followed by a 
+C<span> that can contain validation error message.
+
+
+=head2 Enable the New Element Container When Building the Form
+
+Open C<lib/MyApp/Controller/Books.pm> in your editor.  First add a
+C<use> for your element container class:
+
+    use FormElementContainer;
+
+B<Note:> If you forget to C<use> your container class in your 
+controller, then your form will not be displayed and no error messages 
+will be generated. Don't forget this important step!
+
+Then tell C<HTML::Widget> to use that class during rendering by updating
+C<make_book_widget> to match the following:
+
+    sub make_book_widget {
+        my ($self, $c) = @_;
+    
+        # Create an HTML::Widget to build the form
+        my $w = $c->widget('book_form')->method('post');
+    
+        # ***New: Use custom class to render each element in the form    
+        $w->element_container_class('FormElementContainer');
+        
+        # Get authors
+        my @authorObjs = $c->model("MyAppDB::Author")->all();
+        my @authors = map {$_->id => $_->last_name }
+                           sort {$a->last_name cmp $b->last_name} @authorObjs;
+    
+        # Create the form feilds
+        $w->element('Textfield', 'title'  )->label('Title')->size(60);
+        $w->element('Textfield', 'rating' )->label('Rating')->size(1);
+        # Convert to multi-select list
+        $w->element('Select',    'authors')->label('Authors')
+            ->options(@authors)->multiple(1)->size(3);
+        $w->element('Submit',    'submit' )->value('submit');
+    
+        # Set constraints
+        $w->constraint(All     => qw/title rating authors/)
+            ->message('Required. ');
+        $w->constraint(Integer => qw/rating/)
+            ->message('Must be an integer. ');
+        $w->constraint(Range   => qw/rating/)->min(1)->max(5)
+            ->message('Must be a number between 1 and 5. ');
+        $w->constraint(Length  => qw/title/)->min(5)->max(50)
+            ->message('Must be between 5 and 50 characters. ');
+    
+        # Set filters
+        for my $column (qw/title rating authors/) {
+            $w->filter( HTMLEscape => $column );
+            $w->filter( TrimEdges  => $column );
+        }
+    
+        # Return the widget    
+        return $w;
+    }
+
+The two new lines are marked with C<***New:>.
+
+
+=head2 Update the TT Template
+
+Open C<root/src/books/hw_form.tt2> and edit it to match:
+
+    [% META title = 'Create/Update Book' %]
+    
+    [%# Comment out the auto-rendered form %]
+    [%# widget_result.as_xml %]
+    
+    
+    [%# Iterate over the form elements and display each -%]
+    <form name="book_form" action="[% widget_result.action %]" method="post">
+    <table border="0">
+    [% FOREACH element = widget_result.elements %]
+      <tr>
+        <td class="form-label">
+          [% element.label.as_text %]
+        </td>
+        <td class="form-element">
+          [% element.element_xml %]
+          <span class="form-error">
+            [% element.error_xml %]
+          </span>
+        </td>
+      </tr>
+    [% END %]
+    </table>
+    </form>
+    
+    
+    <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
+    
+    
+    [%# A little JavaScript to move the cursor to the first field %]
+    <script LANGUAGE="JavaScript">
+    document.book_form.book_form_title.focus();
+    </script>
+
+This represents three changes:
+
+=over 4
+
+=item *
+
+The existing C<widget_result.as_xml> has been commented out.
+
+=item *
+
+It loops through each form element, displaying the field name in the 
+first table cell along with the form element and validation errors in 
+the second field.
+
+=item *
+
+JavaScript to position the user's cursor in the first field of the form.
+
+=back
+
+
+=head2 Try Out the Form
+
+Press C<Ctrl-C> to kill the previous server instance (if it's still 
+running) and restart it:
+
+    $ script/myapp_server.pl
+
+Try adding a book that validates.  Return to the book list and the book 
+you added should be visible.
+
+
+=head1 AUTHOR
+
+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-Runtime/lib/Catalyst/Manual/Tutorial/>.
+
+Copyright 2006, Kennedy Clark, under Creative Commons License
+(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).