96fb811de10ef52b81a7463ab5b0cb0765db2924
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Manual / Tutorial / AdvancedCRUD.pod
1 =head1 NAME
2
3 Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8: Advanced CRUD
4
5 =head1 OVERVIEW
6
7 This is B<Part 8 of 9> for the Catalyst tutorial.
8
9 L<Tutorial Overview|Catalyst::Manual::Tutorial>
10
11 =over 4
12
13 =item 1
14
15 L<Introduction|Catalyst::Manual::Tutorial::Intro>
16
17 =item 2
18
19 L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics>
20
21 =item 3
22
23 L<Basic CRUD|Catalyst::Manual::Tutorial_BasicCRUD>
24
25 =item 4
26
27 L<Authentication|Catalyst::Manual::Tutorial::Authentication>
28
29 =item 5
30
31 L<Authorization|Catalyst::Manual::Tutorial::Authorization>
32
33 =item 6
34
35 L<Debugging|Catalyst::Manual::Tutorial::Debugging>
36
37 =item 7
38
39 L<Testing|Catalyst::Manual::Tutorial::Testing>
40
41 =item 8
42
43 B<AdvancedCRUD>
44
45 =item 9
46
47 L<Appendices|Catalyst::Manual::Tutorial::Appendices>
48
49 =back
50
51 =head1 DESCRIPTION
52
53 This part of the tutorial explores more advanced functionality for
54 Create, Read, Update, and Delete (CRUD) than we saw in Part 3.  In
55 particular, it looks at a number of techniques that can be useful for
56 the Update portion of CRUD, such as automated form generation,
57 validation of user-entered data, and automated transfer of data between
58 forms and model objects.
59
60 In keeping with the Catalyst (and Perl) spirit of flexibility, there are
61 many different ways to approach advanced CRUD operations in a Catalyst
62 environment.  One alternative is to use
63 L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold> 
64 to instantly construct a set of Controller methods and templates for 
65 basic CRUD operations.  Although a popular subject in Quicktime 
66 movies that serve as promotional material for various frameworks, 
67 real-world applications generally require more control.  Other 
68 options include L<Data::FormValidator|Data::FormValidator> and
69 L<HTML::FillInForm|HTML::FillInForm>.
70
71 Here, we will make use of the L<HTML::Widget|HTML::Widget> to not only 
72 ease form creation, but to also provide validation of the submitted 
73 data.  The approached used by this part of the tutorial is to slowly 
74 incorporate additional L<HTML::Widget|HTML::Widget> functionality in a 
75 step-wise fashion (we start with fairly simple form creation and then 
76 move on to more complex and "magical" features such as validation and 
77 auto-population/auto-saving).
78
79 B<Note:> Part 8 of the tutorial is optional.  Users who do not wish to
80 use L<HTML::Widget|HTML::Widget> may skip this part.
81
82 You can checkout the source code for this example from the catalyst subversion repository as per the instructions in L<Catalyst::Manual::Tutorial::Intro>
83
84 =head1 C<HTML::WIDGET> FORM CREATION
85
86 This section looks at how L<HTML::Widget|HTML::Widget> can be used to
87 add additional functionality to the manually created form from Part 3.
88
89 =head2 Add the C<HTML::Widget> Plugin
90
91 Open C<lib/MyApp.pm> in your editor and add the following to the list of
92 plugins (be sure to leave the existing plugins enabled):
93
94     HTML::Widget
95
96 =head2 Add a Form Creation Helper Method
97
98 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
99 following method:
100
101     =head2 make_book_widget
102     
103     Build an HTML::Widget form for book creation and updates
104     
105     =cut
106     
107     sub make_book_widget {
108         my ($self, $c) = @_;
109     
110         # Create an HTML::Widget to build the form
111         my $w = $c->widget('book_form')->method('post');
112     
113         # Get authors
114         my @authorObjs = $c->model("MyAppDB::Author")->all();
115         my @authors = map {$_->id => $_->last_name }
116                            sort {$a->last_name cmp $b->last_name} @authorObjs;
117     
118         # Create the form feilds
119         $w->element('Textfield', 'title'  )->label('Title')->size(60);
120         $w->element('Textfield', 'rating' )->label('Rating')->size(1);
121         $w->element('Select',    'authors')->label('Authors')
122             ->options(@authors);
123         $w->element('Submit',    'submit' )->value('submit');
124     
125         # Return the widget    
126         return $w;
127     }
128
129 This method provides a central location that builds an 
130 HTML::Widget-based form with the appropriate fields.  The "Get authors" 
131 code uses DBIC to retrieve a list of model objects and then uses C<map> 
132 to create a hash where the hash keys are the database primary keys from 
133 the authors table and the associated values are the last names of the 
134 authors.
135
136 =head2 Add Actions to Display and Save the Form
137
138 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
139 following methods:
140
141     =head2 hw_create
142     
143     Build an HTML::Widget form for book creation and updates
144     
145     =cut
146     
147     sub hw_create : Local {
148         my ($self, $c) = @_;
149     
150         # Create the widget and set the action for the form
151         my $w = $self->make_book_widget($c);
152         $w->action($c->uri_for('hw_create_do'));
153     
154         # Write form to stash variable for use in template
155         $c->stash->{widget_result} = $w->result;
156     
157         # Set the template
158         $c->stash->{template} = 'books/hw_form.tt2';
159     }
160     
161     
162     =head2 hw_create_do
163     
164     Build an HTML::Widget form for book creation and updates
165     
166     =cut
167     
168     sub hw_create_do : Local {
169         my ($self, $c) = @_;
170     
171         # Retrieve the data from the form
172         my $title   = $c->request->params->{title};
173         my $rating  = $c->request->params->{rating};
174         my $authors = $c->request->params->{authors};
175     
176         # Call create() on the book model object. Pass the table 
177         # columns/field values we want to set as hash values
178         my $book = $c->model('MyAppDB::Book')->create({
179                 title   => $title,
180                 rating  => $rating
181             });
182         
183         # Add a record to the join table for this book, mapping to 
184         # appropriate author
185         $book->add_to_book_authors({author_id => $authors});
186         
187         # Set a status message for the user
188         $c->stash->{status_msg} = 'Book created';
189     
190         # Use 'hw_create' to redisplay the form.  As discussed in 
191         # Part 3, 'detach' is like 'forward', but it does not return
192         $c->detach('hw_create');
193     }
194
195 Note how we use C<make_book_widget> to build the core parts of the form
196 in one location, but we set the action (the URL the form is sent to when
197 the user clicks the 'Submit' button) separately in C<hw_create>.  Doing
198 so allows us to have the same form submit the data to different actions
199 (e.g., C<hw_create_do> for a create operation but C<hw_update_do> to
200 update an existing book object).
201
202 =head2 Update the CSS
203
204 Edit C<root/src/ttsite.css> and add the following lines to the bottom of
205 the file:
206
207     label {
208         display: block;
209         width: 10em;
210         position: relative;
211         margin: .5em 0em;
212     }
213     label input {
214         position: absolute;
215         left: 100%;
216     }
217     label select {
218         position: absolute;
219         left: 100%;
220     }
221     .submit {
222         margin-top: 2em;;
223     }
224     .error_messages {
225         color: [% site.col.error %];
226     }
227
228 These changes will display form elements vertically and also show error
229 messages in red.  Note that we are pulling the color scheme settings
230 from the C<root/lib/config/col> file that was created by the TTSite
231 helper.  This allows us to change the color used by various error styles
232 in the CSS from a single location.
233
234 =head2 Create a Template Page To Display The Form
235
236 Open C<root/src/books/hw_form.tt2> in your editor and enter the following:
237
238     [% META title = 'Create/Update Book' %]
239     
240     [% widget_result.as_xml %]
241     
242     <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
243
244 =head2 Add Links for Create and Update via C<HTML::Widget>
245
246 Open C<root/src/books/list.tt2> in your editor and add the following to
247 the bottom of the existing file:
248
249     <p>
250       HTML::Widget:
251       <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
252       <a href="[% Catalyst.uri_for('hw_update') %]">Update</a>
253     </p>
254
255 =head2 Test The <HTML::Widget> Create Form
256
257 Press C<Ctrl-C> to kill the previous server instance (if it's still
258 running) and restart it:
259
260     $ script/myapp_server.pl
261
262 Login as C<test01>.  Once at the Book List page, click the HTML::Widget
263 "Create" link to display for form produced by C<make_book_widget>.  Fill
264 out the form with the following values: Title = "Internetworking with
265 TCP/IP Vol. II", Rating = "4", and Author = "Comer".  Click Submit, and
266 you will be returned to the Create/Update Book page with a "Book
267 created" status message displayed.  Click "Return to book list" to view
268 the newly created book on the main list.
269
270 Also note that this implementation allows you to can create books with
271 bogus information.  Although we have constrained the authors with the
272 drop-down list, there are no restrictions on items such as the length of
273 the title (for example, you can create a one-letter title) and value for
274 the rating (you can use any number you want, and even non-numeric values
275 with SQLite).  The next section will address this concern.
276
277 B<Note:> Depending on the database you are using and how you established
278 the columns in your tables, the database could obviously provide various
279 levels of "type enforcement" on your data.  The key point being made in
280 the previous paragraph is that the I<web application> itself is not
281 performing any validation.
282
283 =head1 C<HTML::WIDGET> VALIDATION AND FILTERING
284
285 Although the use of L<HTML::Widget|HTML::Widget> in the previous section
286 did provide an automated mechanism to build the form, the real power of
287 this module stems from functionality that can automatically validate and
288 filter the user input.  Validation uses constraints to be sure that
289 users input appropriate data (for example, that the email field of a
290 form contains a valid email address).  Filtering can be used to remove
291 extraneous whitespace from fields or to escape meta-characters in user
292 input.
293
294 =head2 Add Constraints and Filters to the Widget Creation Method
295
296 Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
297 C<make_book_widget> method to match the following (new sections have
298 been marked with a C<*** NEW:> comment):
299
300     sub make_book_widget {
301         my ($self, $c) = @_;
302     
303         # Create an HTML::Widget to build the form
304         my $w = $c->widget('book_form')->method('post');
305             
306         # Get authors
307         my @authorObjs = $c->model("MyAppDB::Author")->all();
308         my @authors = map {$_->id => $_->last_name }
309                            sort {$a->last_name cmp $b->last_name} @authorObjs;
310     
311         # Create the form feilds
312         $w->element('Textfield', 'title'  )->label('Title')->size(60);
313         $w->element('Textfield', 'rating' )->label('Rating')->size(1);
314         # ***NEW: Convert to multi-select list
315         $w->element('Select',    'authors')->label('Authors')
316             ->options(@authors)->multiple(1)->size(3);
317         $w->element('Submit',    'submit' )->value('submit');
318     
319         # ***NEW: Set constraints
320         $w->constraint(All     => qw/title rating authors/)
321             ->message('Required. ');
322         $w->constraint(Integer => qw/rating/)
323             ->message('Must be an integer. ');
324         $w->constraint(Range   => qw/rating/)->min(1)->max(5)
325             ->message('Must be a number between 1 and 5. ');
326         $w->constraint(Length  => qw/title/)->min(5)->max(50)
327             ->message('Must be between 5 and 50 characters. ');
328     
329         # ***NEW: Set filters
330         for my $column (qw/title rating authors/) {
331             $w->filter( HTMLEscape => $column );
332             $w->filter( TrimEdges  => $column );
333         }
334     
335         # Return the widget    
336         return $w;
337     }
338
339 The main changes are:
340
341 =over 4
342
343 =item *
344
345 The C<Select> element for C<authors> is changed from a single-select
346 drop-down to a multi-select list by adding calls to C<multiple> (set to
347 C<true>) and C<size> (set to the number of rows to display).
348
349 =item *
350
351 Four sets of constraints are added to provide validation of the user input.
352
353 =item *
354
355 Two filters are run on every field to remove and escape unwanted input.
356
357 =back
358
359 =head2 Rebuild the Form Submission Method to Include Validation
360
361 Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
362 match the following code (enough of the code is different that you
363 probably want to cut and paste this over code the existing method):
364
365     sub hw_create_do : Local {
366         my ($self, $c) = @_;
367     
368         # Retrieve the data from the form
369         my $title   = $c->request->params->{title};
370         my $rating  = $c->request->params->{rating};
371         my $authors = $c->request->params->{authors};
372         
373         # Create the widget and set the action for the form
374         my $w = $self->make_book_widget($c);
375         $w->action($c->uri_for('hw_create_do'));
376     
377         # Validate the form parameters
378         my $result = $w->process($c->req);
379     
380         # Write form (including validation error messages) to
381         # stash variable for use in template
382         $c->stash->{widget_result} = $result;
383     
384         # Were their validation errors?
385         if ($result->has_errors) {
386             # Warn the user at the top of the form that there were errors.
387             # Note that there will also be per-field feedback on
388             # validation errors because of '$w->process($c->req)' above.
389             $c->stash->{error_msg} = 'Validation errors!';
390         } else {
391             # Everything validated OK, so do the create
392             # Call create() on the book model object. Pass the table
393             # columns/field values we want to set as hash values
394             my $book = $c->model('MyAppDB::Book')->create({
395                     title   => $title,
396                     rating  => $rating
397                 });
398     
399             # Add a record to the join table for this book, mapping to
400             # appropriate author.  Note that $authors will be 1 author as
401             # a scalar or ref to list of authors depending on how many the
402             # user selected; the 'ref $authors ?...' handles both cases
403             foreach my $author (ref $authors ? @$authors : $authors) {
404                 $book->add_to_book_authors({author_id => $author});
405             }    
406             # Set a status message for the user
407             $c->stash->{status_msg} = 'Book created';
408         }
409     
410         # Set the template
411         $c->stash->{template} = 'books/hw_form.tt2';
412     }
413
414 The key changes to C<hw_create_do> are:
415
416 =over 4
417
418 =item *
419
420 C<hw_create_do> no longer does a C<detach> to C<hw_create> to redisplay
421 the form.  Now that C<hw_create_do> has to process the form in order to
422 perform the validation, we go ahead and build a complete set of form
423 presentation logic into C<hw_create_do> (for example, C<hw_create_do>
424 now has a C<$c-E<gt>stash-E<gt>{template}> line).  Note that if we
425 process the form in C<hw_create_do> I<and> forward/detach back to
426 <hw_create>, we would end up with C<make_book_widget> being called
427 twice, resulting in a duplicate set of elements being added to the form.
428 (There are other ways to address the "duplicate form rendering" issue --
429 just be aware that it exists.)
430
431 =item *
432
433 C<$w-E<gt>process($c-E<gt>req)> is called to run the validation logic.
434 Not only does this set the C<has_errors> flag if validation errors are
435 encountered, it returns a string containing any field-specific warning
436 messages.
437
438 =item *
439
440 An C<if> statement checks if any validation errors were encountered.  If
441 so, C<$c-E<gt>stash-E<gt>{error_msg}> is set and the input form is
442 redisplayed.  If no errors were found, the object is created in a manner
443 similar to the prior version of the C<hw_create_do> method.
444
445 =back
446
447 =head2 Try Out the Form
448
449 Press C<Ctrl-C> to kill the previous server instance (if it's still 
450 running) and restart it:
451
452     $ script/myapp_server.pl
453
454 Now try adding a book with various errors: title less than 5 characters,
455 non-numeric rating, a rating of 0 or 6, etc.  Also try selecting one,
456 two, and zero authors.  When you click Submit, the HTML::Widget
457 C<constraint> items will validate the logic and insert feedback as
458 appropriate.
459
460
461 =head1 Enable C<DBIx::Class::HTMLWidget> Support
462
463 In this section we will take advantage of some of the "auto-population"
464 features of C<DBIx::Class::HTMLWidget>.  Enabling
465 C<DBIx::Class::HTMLWidget> provides two additional methods to your DBIC
466 model classes:
467
468 =over 4
469
470 =item *
471
472 fill_widget()
473
474 Takes data from the database and transfers it to your form widget.
475
476 =item *
477
478 populate_from_widget()
479
480 Takes data from a form widget and uses it to update the corresponding
481 records in the database.
482
483 =back
484
485 In other words, the two methods are a mirror image of each other: one
486 reads from the database while the other writes to the database.
487
488 =head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
489
490 In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
491 need to add C<HTMLWidget> to the C<load_components> line of DBIC result
492 source files that need to use the C<fill_widget> and
493 C<populate_from_widget> methods.  In this case, open
494 C<lib/MyAppDB/Book.pm> and update the C<load_components> line to match:
495
496         __PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
497
498 =head2 Use C<populate_from_widget> in C<hw_create_do>
499
500 Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
501 match the following code:
502
503     =head2 hw_create_do
504     
505     Build an HTML::Widget form for book creation and updates
506     
507     =cut
508     
509     sub hw_create_do : Local {
510         my ($self, $c) = @_;
511     
512         # Create the widget and set the action for the form
513         my $w = $self->make_book_widget($c);
514         $w->action($c->uri_for('hw_create_do'));
515     
516         # Validate the form parameters
517         my $result = $w->process($c->req);
518     
519         # Write form (including validation error messages) to
520         # stash variable for use in template
521         $c->stash->{widget_result} = $result;
522     
523         # Were their validation errors?
524         if ($result->has_errors) {
525             # Warn the user at the top of the form that there were errors.
526             # Note that there will also be per-field feedback on
527             # validation errors because of '$w->process($c->req)' above.
528             $c->stash->{error_msg} = 'Validation errors!';
529         } else {
530             my $book = $c->model('MyAppDB::Book')->new({});
531             $book->populate_from_widget($result);
532     
533             # Add a record to the join table for this book, mapping to
534             # appropriate author.  Note that $authors will be 1 author as
535             # a scalar or ref to list of authors depending on how many the
536             # user selected; the 'ref $authors ?...' handles both cases
537             my $authors = $c->request->params->{authors};
538             foreach my $author (ref $authors ? @$authors : $authors) {
539                 $book->add_to_book_authors({author_id => $author});
540             }
541     
542             # Set a status message for the user
543             $c->stash->{status_msg} = 'Book created';
544             
545             # Redisplay an empty form for another
546             $c->stash->{widget_result} = $w->result;
547         }
548     
549         # Set the template
550         $c->stash->{template} = 'books/hw_form.tt2';
551     }
552
553 In this version of C<hw_create_do> we removed the logic that manually
554 pulled the form variables and used them to call
555 C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
556 single call to C<$book-E<gt>populate_from_widget>.  Note that we still
557 have to call C<$book-E<gt>add_to_book_authors> once per author because
558 C<populate_from_widget> does not currently handle the relationships
559 between tables.  Also, we reset the form to an empty fields by adding
560 another call to C<$w-E<gt>result> and storing the output in the stash 
561 (if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
562 the form values already entered will be retained on redisplay --
563 although this could be desirable for some applications, we avoid it
564 here to help avoid the creation of duplicate records).
565
566
567 =head2 Try Out the Form
568
569 Press C<Ctrl-C> to kill the previous server instance (if it's still 
570 running) and restart it:
571
572     $ script/myapp_server.pl
573
574 Try adding a book that validate.  Return to the book list and the book 
575 you added should be visible.
576
577
578
579 =head1 Rendering C<HTMLWidget> Forms in a Table
580
581 Some developers my wish to use the "old-fashioned" table style of 
582 rendering a form in lieu of the default C<HTML::Widget> rendering that 
583 assumes you will use CSS for formatting.  This section demonstrates
584 some techniques that can override the default rendering with a 
585 custom class.
586
587
588 =head2 Add a New "Element Container"
589
590 Open C<lib/FormElementContainer.pm> in your editor and enter:
591
592     package FormElementContainer;
593     
594     use base 'HTML::Widget::Container';
595     
596     sub _build_element {
597         my ($self, $element) = @_;
598     
599         return () unless $element;
600         if (ref $element eq 'ARRAY') {
601             return map { $self->_build_element($_) } @{$element};
602         }
603         my $e = $element->clone;
604         $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
605             if $self->error && $e->tag eq 'input';
606     
607         return $e ? ($e) : ();
608     }
609     
610     1;
611
612 This simply dumps the HTML code for a given form element, followed by a 
613 C<span> that can contain validation error message.
614
615
616 =head2 Enable the New Element Container When Building the Form
617
618 Open C<lib/MyApp/Controller/Books.pm> in your editor.  First add a
619 C<use> for your element container class:
620
621     use FormElementContainer;
622
623 B<Note:> If you forget to C<use> your container class in your 
624 controller, then your form will not be displayed and no error messages 
625 will be generated. Don't forget this important step!
626
627 Then tell C<HTML::Widget> to use that class during rendering by updating
628 C<make_book_widget> to match the following:
629
630     sub make_book_widget {
631         my ($self, $c) = @_;
632     
633         # Create an HTML::Widget to build the form
634         my $w = $c->widget('book_form')->method('post');
635     
636         # ***New: Use custom class to render each element in the form    
637         $w->element_container_class('FormElementContainer');
638         
639         # Get authors
640         my @authorObjs = $c->model("MyAppDB::Author")->all();
641         my @authors = map {$_->id => $_->last_name }
642                            sort {$a->last_name cmp $b->last_name} @authorObjs;
643     
644         # Create the form feilds
645         $w->element('Textfield', 'title'  )->label('Title')->size(60);
646         $w->element('Textfield', 'rating' )->label('Rating')->size(1);
647         # Convert to multi-select list
648         $w->element('Select',    'authors')->label('Authors')
649             ->options(@authors)->multiple(1)->size(3);
650         $w->element('Submit',    'submit' )->value('submit');
651     
652         # Set constraints
653         $w->constraint(All     => qw/title rating authors/)
654             ->message('Required. ');
655         $w->constraint(Integer => qw/rating/)
656             ->message('Must be an integer. ');
657         $w->constraint(Range   => qw/rating/)->min(1)->max(5)
658             ->message('Must be a number between 1 and 5. ');
659         $w->constraint(Length  => qw/title/)->min(5)->max(50)
660             ->message('Must be between 5 and 50 characters. ');
661     
662         # Set filters
663         for my $column (qw/title rating authors/) {
664             $w->filter( HTMLEscape => $column );
665             $w->filter( TrimEdges  => $column );
666         }
667     
668         # Return the widget    
669         return $w;
670     }
671
672 The two new lines are marked with C<***New:>.
673
674
675 =head2 Update the TT Template
676
677 Open C<root/src/books/hw_form.tt2> and edit it to match:
678
679     [% META title = 'Create/Update Book' %]
680     
681     [%# Comment out the auto-rendered form %]
682     [%# widget_result.as_xml %]
683     
684     
685     [%# Iterate over the form elements and display each -%]
686     <form name="book_form" action="[% widget_result.action %]" method="post">
687     <table border="0">
688     [% FOREACH element = widget_result.elements %]
689       <tr>
690         <td class="form-label">
691           [% element.label.as_text %]
692         </td>
693         <td class="form-element">
694           [% element.element_xml %]
695           <span class="form-error">
696             [% element.error_xml %]
697           </span>
698         </td>
699       </tr>
700     [% END %]
701     </table>
702     </form>
703     
704     
705     <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
706     
707     
708     [%# A little JavaScript to move the cursor to the first field %]
709     <script LANGUAGE="JavaScript">
710     document.book_form.book_form_title.focus();
711     </script>
712
713 This represents three changes:
714
715 =over 4
716
717 =item *
718
719 The existing C<widget_result.as_xml> has been commented out.
720
721 =item *
722
723 It loops through each form element, displaying the field name in the 
724 first table cell along with the form element and validation errors in 
725 the second field.
726
727 =item *
728
729 JavaScript to position the user's cursor in the first field of the form.
730
731 =back
732
733
734 =head2 Try Out the Form
735
736 Press C<Ctrl-C> to kill the previous server instance (if it's still 
737 running) and restart it:
738
739     $ script/myapp_server.pl
740
741 Try adding a book that validate.  Return to the book list and the book 
742 you added should be visible.
743
744
745 =head1 AUTHOR
746
747 Kennedy Clark, C<hkclark@gmail.com>
748
749 Please report any errors, issues or suggestions to the author.  The
750 most recent version of the Catalyst Tutorial can be found at
751 L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Runtime/lib/Catalyst/Manual/Tutorial/>.
752
753 Copyright 2006, Kennedy Clark, under Creative Commons License
754 (L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).