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