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