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