source: main/trunk/greenstone2/perllib/cpan/Mojolicious/Guides/Growing.pod@ 32205

Last change on this file since 32205 was 32205, checked in by ak19, 6 years ago

First set of commits to do with implementing the new 'paged_html' output option of PDFPlugin that uses using xpdftools' new pdftohtml. So far tested only on Linux (64 bit), but things work there so I'm optimistically committing the changes since they work. 2. Committing the pre-built Linux binaries of XPDFtools for both 32 and 64 bit built by the XPDF group. 2. To use the correct bitness variant of xpdftools, setup.bash now exports the BITNESS env var, consulted by gsConvert.pl. 3. All the perl code changes to do with using xpdf tools' pdftohtml to generate paged_html and feed it in the desired form into GS(3): gsConvert.pl, PDFPlugin.pm and its parent ConvertBinaryPFile.pm have been modified to make it all work. xpdftools' pdftohtml generates a folder containing an html file and a screenshot for each page in a PDF (as well as an index.html linking to each page's html). However, we want a single html file that contains each individual 'page' html's content in a div, and need to do some further HTML style, attribute and structure modifications to massage the xpdftool output to what we want for GS. In order to parse and manipulate the HTML 'DOM' to do this, we're using the Mojo::DOM package that Dr Bainbridge found and which he's compiled up. Mojo::DOM is therefore also committed in this revision. Some further changes and some display fixes are required, but need to check with the others about that.

File size: 23.5 KB
Line 
1
2=encoding utf8
3
4=head1 NAME
5
6Mojolicious::Guides::Growing - Growing Mojolicious applications
7
8=head1 OVERVIEW
9
10This document explains the process of starting a L<Mojolicious::Lite> prototype
11from scratch and growing it into a well-structured L<Mojolicious> application.
12
13=head1 CONCEPTS
14
15Essentials every L<Mojolicious> developer should know.
16
17=head2 Model View Controller
18
19MVC is a software architectural pattern for graphical user interface
20programming originating in Smalltalk-80, that separates application logic,
21presentation and input.
22
23 +------------+ +-------+ +------+
24 Input -> | Controller | -> | Model | -> | View | -> Output
25 +------------+ +-------+ +------+
26
27A slightly modified version of the pattern moving some application logic into
28the I<controller> is the foundation of pretty much every web framework these
29days, including L<Mojolicious>.
30
31 +----------------+ +-------+
32 Request -> | | <-> | Model |
33 | | +-------+
34 | Controller |
35 | | +-------+
36 Response <- | | <-> | View |
37 +----------------+ +-------+
38
39The I<controller> receives a request from a user, passes incoming data to the
40I<model> and retrieves data from it, which then gets turned into an actual
41response by the I<view>. But note that this pattern is just a guideline that
42most of the time results in cleaner more maintainable code, not a rule that
43should be followed at all costs.
44
45=head2 REpresentational State Transfer
46
47REST is a software architectural style for distributed hypermedia systems such
48as the web. While it can be applied to many protocols it is most commonly used
49with HTTP these days. In REST terms, when you are opening a URL like
50C<http://mojolicious.org/foo> with your browser, you are basically asking the
51web server for the HTML I<representation> of the C<http://mojolicious.org/foo>
52I<resource>.
53
54 +--------+ +--------+
55 | | -> http://mojolicious.org/foo -> | |
56 | Client | | Server |
57 | | <- <html>Mojo rocks!</html> <- | |
58 +--------+ +--------+
59
60The fundamental idea here is that all resources are uniquely addressable with
61URLs and every resource can have different representations such as HTML, RSS or
62JSON. User interface concerns are separated from data storage concerns and all
63session state is kept client-side.
64
65 +---------+ +------------+
66 | | -> PUT /foo -> | |
67 | | -> Hello World! -> | |
68 | | | |
69 | | <- 201 CREATED <- | |
70 | | | |
71 | | -> GET /foo -> | |
72 | Browser | | Web Server |
73 | | <- 200 OK <- | |
74 | | <- Hello World! <- | |
75 | | | |
76 | | -> DELETE /foo -> | |
77 | | | |
78 | | <- 200 OK <- | |
79 +---------+ +------------+
80
81While HTTP methods such as C<PUT>, C<GET> and C<DELETE> are not directly part
82of REST they go very well with it and are commonly used to manipulate
83I<resources>.
84
85=head2 Sessions
86
87HTTP was designed as a stateless protocol, web servers don't know anything
88about previous requests, which makes user-friendly login systems very tricky.
89Sessions solve this problem by allowing web applications to keep stateful
90information across several HTTP requests.
91
92 GET /login?user=sebastian&pass=s3cret HTTP/1.1
93 Host: mojolicious.org
94
95 HTTP/1.1 200 OK
96 Set-Cookie: sessionid=987654321
97 Content-Length: 10
98 Hello sebastian.
99
100 GET /protected HTTP/1.1
101 Host: mojolicious.org
102 Cookie: sessionid=987654321
103
104 HTTP/1.1 200 OK
105 Set-Cookie: sessionid=987654321
106 Content-Length: 16
107 Hello again sebastian.
108
109Traditionally all session data was stored on the server-side and only session
110ids were exchanged between browser and web server in the form of cookies.
111
112 Set-Cookie: session=hmac-sha1(base64(json($session)))
113
114In L<Mojolicious> however we are taking this concept one step further by
115storing everything JSON serialized and Base64 encoded in HMAC-SHA1 signed
116cookies, which is more compatible with the REST philosophy and reduces
117infrastructure requirements.
118
119=head2 Test-Driven Development
120
121TDD is a software development process where the developer starts writing
122failing test cases that define the desired functionality and then moves on to
123producing code that passes these tests. There are many advantages such as
124always having good test coverage and code being designed for testability, which
125will in turn often prevent future changes from breaking old code. Much of
126L<Mojolicious> was developed using TDD.
127
128=head1 PROTOTYPE
129
130One of the main differences between L<Mojolicious> and other web frameworks is
131that it also includes L<Mojolicious::Lite>, a micro web framework optimized for
132rapid prototyping.
133
134=head2 Differences
135
136You likely know the feeling, you've got a really cool idea and want to try it
137as quickly as possible, that's exactly why L<Mojolicious::Lite> applications
138don't need more than a single file.
139
140 myapp.pl # Templates and even static files can be inlined
141
142Full L<Mojolicious> applications on the other hand are much closer to a well
143organized CPAN distribution to maximize maintainability.
144
145 myapp # Application directory
146 |- script # Script directory
147 | +- my_app # Application script
148 |- lib # Library directory
149 | |- MyApp.pm # Application class
150 | +- MyApp # Application namespace
151 | +- Controller # Controller namespace
152 | +- Example.pm # Controller class
153 |- my_app.conf # Configuration file
154 |- t # Test directory
155 | +- basic.t # Random test
156 |- log # Log directory
157 | +- development.log # Development mode log file
158 |- public # Static file directory (served automatically)
159 | +- index.html # Static HTML file
160 +- templates # Template directory
161 |- layouts # Template directory for layouts
162 | +- default.html.ep # Layout template
163 +- example # Template directory for "Example" controller
164 +- welcome.html.ep # Template for "welcome" action
165
166Both application skeletons can be automatically generated with the commands
167L<Mojolicious::Command::generate::lite_app> and
168L<Mojolicious::Command::generate::app>.
169
170 $ mojo generate lite_app myapp.pl
171 $ mojo generate app MyApp
172
173Feature-wise both are almost equal, the only real differences are
174organizational, so each one can be gradually transformed into the other.
175
176=head2 Foundation
177
178We start our new application with a single executable Perl script.
179
180 $ mkdir myapp
181 $ cd myapp
182 $ touch myapp.pl
183 $ chmod 744 myapp.pl
184
185This will be the foundation for our login manager example application.
186
187 #!/usr/bin/env perl
188 use Mojolicious::Lite;
189
190 get '/' => sub {
191 my $c = shift;
192 $c->render(text => 'Hello World!');
193 };
194
195 app->start;
196
197The built-in development web server makes working on your application a lot of
198fun thanks to automatic reloading.
199
200 $ morbo ./myapp.pl
201 Server available at http://127.0.0.1:3000
202
203Just save your changes and they will be automatically in effect the next time
204you refresh your browser.
205
206=head2 A bird's-eye view
207
208It all starts with an HTTP request like this, sent by your browser.
209
210 GET / HTTP/1.1
211 Host: localhost:3000
212
213Once the request has been received by the web server through the event loop, it
214will be passed on to L<Mojolicious>, where it will be handled in a few simple
215steps.
216
217=over 2
218
219=item 1.
220
221Check if a static file exists that would meet the requirements.
222
223=item 2.
224
225Try to find a route that would meet the requirements.
226
227=item 3.
228
229Dispatch the request to this route, usually reaching one or more actions.
230
231=item 4.
232
233Process the request, maybe generating a response with the renderer.
234
235=item 5.
236
237Return control to the web server, and if no response has been generated yet,
238wait for a non-blocking operation to do so through the event loop.
239
240=back
241
242With our application the router would have found an action in step 2, and
243rendered some text in step 4, resulting in an HTTP response like this being
244sent back to the browser.
245
246 HTTP/1.1 200 OK
247 Content-Length: 12
248 Hello World!
249
250=head2 Model
251
252In L<Mojolicious> we consider web applications simple frontends for existing
253business logic, that means L<Mojolicious> is by design entirely I<model> layer
254agnostic and you just use whatever Perl modules you like most.
255
256 $ mkdir -p lib/MyApp/Model
257 $ touch lib/MyApp/Model/Users.pm
258 $ chmod 644 lib/MyApp/Model/Users.pm
259
260Our login manager will simply use a plain old Perl module abstracting away all
261logic related to matching usernames and passwords. The name
262C<MyApp::Model::Users> is an arbitrary choice, and is simply used to make the
263separation of concerns more visible.
264
265 package MyApp::Model::Users;
266
267 use strict;
268 use warnings;
269
270 use Mojo::Util 'secure_compare';
271
272 my $USERS = {
273 joel => 'las3rs',
274 marcus => 'lulz',
275 sebastian => 'secr3t'
276 };
277
278 sub new { bless {}, shift }
279
280 sub check {
281 my ($self, $user, $pass) = @_;
282
283 # Success
284 return 1 if $USERS->{$user} && secure_compare $USERS->{$user}, $pass;
285
286 # Fail
287 return undef;
288 }
289
290 1;
291
292A simple helper can be registered with the function
293L<Mojolicious::Lite/"helper"> to make our model available to all actions and
294templates.
295
296 #!/usr/bin/env perl
297 use Mojolicious::Lite;
298
299 use lib 'lib';
300 use MyApp::Model::Users;
301
302 # Helper to lazy initialize and store our model object
303 helper users => sub { state $users = MyApp::Model::Users->new };
304
305 # /?user=sebastian&pass=secr3t
306 any '/' => sub {
307 my $c = shift;
308
309 # Query parameters
310 my $user = $c->param('user') || '';
311 my $pass = $c->param('pass') || '';
312
313 # Check password
314 return $c->render(text => "Welcome $user.")
315 if $c->users->check($user, $pass);
316
317 # Failed
318 $c->render(text => 'Wrong username or password.');
319 };
320
321 app->start;
322
323The method L<Mojolicious::Controller/"param"> is used to access query
324parameters, C<POST> parameters, file uploads and route placeholders, all at
325once.
326
327=head2 Testing
328
329In L<Mojolicious> we take testing very serious and try to make it a pleasant
330experience.
331
332 $ mkdir t
333 $ touch t/login.t
334 $ chmod 644 t/login.t
335
336L<Test::Mojo> is a scriptable HTTP user agent designed specifically for
337testing, with many fun state of the art features such as CSS selectors based on
338L<Mojo::DOM>.
339
340 use Test::More;
341 use Test::Mojo;
342
343 # Include application
344 use FindBin;
345 require "$FindBin::Bin/../myapp.pl";
346
347 # Allow 302 redirect responses
348 my $t = Test::Mojo->new;
349 $t->ua->max_redirects(1);
350
351 # Test if the HTML login form exists
352 $t->get_ok('/')
353 ->status_is(200)
354 ->element_exists('form input[name="user"]')
355 ->element_exists('form input[name="pass"]')
356 ->element_exists('form input[type="submit"]');
357
358 # Test login with valid credentials
359 $t->post_ok('/' => form => {user => 'sebastian', pass => 'secr3t'})
360 ->status_is(200)
361 ->text_like('html body' => qr/Welcome sebastian/);
362
363 # Test accessing a protected page
364 $t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);
365
366 # Test if HTML login form shows up again after logout
367 $t->get_ok('/logout')
368 ->status_is(200)
369 ->element_exists('form input[name="user"]')
370 ->element_exists('form input[name="pass"]')
371 ->element_exists('form input[type="submit"]');
372
373 done_testing();
374
375Your application won't pass these tests, but from now on you can use them to
376check your progress with the command L<Mojolicious::Command::test>.
377
378 $ ./myapp.pl test
379 $ ./myapp.pl test t/login.t
380 $ ./myapp.pl test -v t/login.t
381
382Or perform quick requests right from the command line with
383L<Mojolicious::Command::get>.
384
385 $ ./myapp.pl get /
386 Wrong username or password.
387
388 $ ./myapp.pl get -v '/?user=sebastian&pass=secr3t'
389 GET /?user=sebastian&pass=secr3t HTTP/1.1
390 User-Agent: Mojolicious (Perl)
391 Accept-Encoding: gzip
392 Content-Length: 0
393 Host: localhost:59472
394
395 HTTP/1.1 200 OK
396 Date: Sun, 18 Jul 2010 13:09:58 GMT
397 Server: Mojolicious (Perl)
398 Content-Length: 12
399 Content-Type: text/plain
400
401 Welcome sebastian.
402
403=head2 State keeping
404
405Sessions in L<Mojolicious> pretty much just work out of the box once you start
406using the method L<Mojolicious::Controller/"session">, there is no setup
407required, but we suggest setting a more secure passphrase with
408L<Mojolicious/"secrets">.
409
410 $app->secrets(['Mojolicious rocks']);
411
412This passphrase is used by the HMAC-SHA1 algorithm to make signed cookies tamper
413resistant and can be changed at any time to invalidate all existing sessions.
414
415 $c->session(user => 'sebastian');
416 my $user = $c->session('user');
417
418By default all sessions expire after one hour, for more control you can use the
419C<expiration> session value to set an expiration date in seconds from now.
420
421 $c->session(expiration => 3600);
422
423And the whole session can be deleted by using the C<expires> session value to
424set an absolute expiration date in the past.
425
426 $c->session(expires => 1);
427
428For data that should only be visible on the next request, like a confirmation
429message after a C<302> redirect performed with
430L<Mojolicious::Controller/"redirect_to">, you can use the flash, accessible
431through the method L<Mojolicious::Controller/"flash">.
432
433 $c->flash(message => 'Everything is fine.');
434 $c->redirect_to('goodbye');
435
436Just remember that all session data gets serialized with L<Mojo::JSON> and
437stored in HMAC-SHA1 signed cookies, which usually have a C<4096> byte (4KiB)
438limit, depending on browser.
439
440=head2 Final prototype
441
442A final C<myapp.pl> prototype passing all of the tests above could look like
443this.
444
445 #!/usr/bin/env perl
446 use Mojolicious::Lite;
447
448 use lib 'lib';
449 use MyApp::Model::Users;
450
451 # Make signed cookies tamper resistant
452 app->secrets(['Mojolicious rocks']);
453
454 helper users => sub { state $users = MyApp::Model::Users->new };
455
456 # Main login action
457 any '/' => sub {
458 my $c = shift;
459
460 # Query or POST parameters
461 my $user = $c->param('user') || '';
462 my $pass = $c->param('pass') || '';
463
464 # Check password and render "index.html.ep" if necessary
465 return $c->render unless $c->users->check($user, $pass);
466
467 # Store username in session
468 $c->session(user => $user);
469
470 # Store a friendly message for the next page in flash
471 $c->flash(message => 'Thanks for logging in.');
472
473 # Redirect to protected page with a 302 response
474 $c->redirect_to('protected');
475 } => 'index';
476
477 # Make sure user is logged in for actions in this group
478 group {
479 under sub {
480 my $c = shift;
481
482 # Redirect to main page with a 302 response if user is not logged in
483 return 1 if $c->session('user');
484 $c->redirect_to('index');
485 return undef;
486 };
487
488 # A protected page auto rendering "protected.html.ep"
489 get '/protected';
490 };
491
492 # Logout action
493 get '/logout' => sub {
494 my $c = shift;
495
496 # Expire and in turn clear session automatically
497 $c->session(expires => 1);
498
499 # Redirect to main page with a 302 response
500 $c->redirect_to('index');
501 };
502
503 app->start;
504 __DATA__
505
506 @@ index.html.ep
507 % layout 'default';
508 %= form_for index => begin
509 % if (param 'user') {
510 <b>Wrong name or password, please try again.</b><br>
511 % }
512 Name:<br>
513 %= text_field 'user'
514 <br>Password:<br>
515 %= password_field 'pass'
516 <br>
517 %= submit_button 'Login'
518 % end
519
520 @@ protected.html.ep
521 % layout 'default';
522 % if (my $msg = flash 'message') {
523 <b><%= $msg %></b><br>
524 % }
525 Welcome <%= session 'user' %>.<br>
526 %= link_to Logout => 'logout'
527
528 @@ layouts/default.html.ep
529 <!DOCTYPE html>
530 <html>
531 <head><title>Login Manager</title></head>
532 <body><%= content %></body>
533 </html>
534
535And the directory structure should be looking like this now.
536
537 myapp
538 |- myapp.pl
539 |- lib
540 | +- MyApp
541 | +- Model
542 | +- Users.pm
543 +- t
544 +- login.t
545
546Our templates are using quite a few features of the renderer,
547L<Mojolicious::Guides::Rendering> explains them all in great detail.
548
549=head1 WELL-STRUCTURED APPLICATION
550
551Due to the flexibility of L<Mojolicious> there are many variations of the
552actual growing process, but this should give you a good overview of the
553possibilities.
554
555=head2 Inflating templates
556
557All templates and static files inlined in the C<DATA> section can be
558automatically turned into separate files in the C<templates> and C<public>
559directories with the command L<Mojolicious::Command::inflate>.
560
561 $ ./myapp.pl inflate
562
563Those directories have a higher precedence, so inflating can also be a great
564way to allow your users to customize their applications.
565
566=head2 Simplified application class
567
568This is the heart of every full L<Mojolicious> application and always gets
569instantiated during server startup.
570
571 $ touch lib/MyApp.pm
572 $ chmod 644 lib/MyApp.pm
573
574We will start by extracting all actions from C<myapp.pl> and turn them into
575simplified hybrid routes in the L<Mojolicious::Routes> router, none of the
576actual action code needs to be changed.
577
578 package MyApp;
579 use Mojo::Base 'Mojolicious';
580
581 use MyApp::Model::Users;
582
583 sub startup {
584 my $self = shift;
585
586 $self->secrets(['Mojolicious rocks']);
587 $self->helper(users => sub { state $users = MyApp::Model::Users->new });
588
589 my $r = $self->routes;
590
591 $r->any('/' => sub {
592 my $c = shift;
593
594 my $user = $c->param('user') || '';
595 my $pass = $c->param('pass') || '';
596 return $c->render unless $c->users->check($user, $pass);
597
598 $c->session(user => $user);
599 $c->flash(message => 'Thanks for logging in.');
600 $c->redirect_to('protected');
601 } => 'index');
602
603 my $logged_in = $r->under(sub {
604 my $c = shift;
605 return 1 if $c->session('user');
606 $c->redirect_to('index');
607 return undef;
608 });
609 $logged_in->get('/protected');
610
611 $r->get('/logout' => sub {
612 my $c = shift;
613 $c->session(expires => 1);
614 $c->redirect_to('index');
615 });
616 }
617
618 1;
619
620The C<startup> method gets called right after instantiation and is the place
621where the whole application gets set up. Since full L<Mojolicious> applications
622can use nested routes they have no need for C<group> blocks.
623
624=head2 Simplified application script
625
626C<myapp.pl> itself can now be turned into a simplified application script to
627allow running tests again.
628
629 #!/usr/bin/env perl
630
631 use strict;
632 use warnings;
633
634 use lib 'lib';
635 use Mojolicious::Commands;
636
637 # Start command line interface for application
638 Mojolicious::Commands->start_app('MyApp');
639
640And the directory structure of our hybrid application should be looking like
641this.
642
643 myapp
644 |- myapp.pl
645 |- lib
646 | |- MyApp.pm
647 | +- MyApp
648 | +- Model
649 | +- Users.pm
650 |- t
651 | +- login.t
652 +- templates
653 |- layouts
654 | +- default.html.ep
655 |- index.html.ep
656 +- protected.html.ep
657
658=head2 Controller class
659
660Hybrid routes are a nice intermediate step, but to maximize maintainability it
661makes sense to split our action code from its routing information.
662
663 $ mkdir lib/MyApp/Controller
664 $ touch lib/MyApp/Controller/Login.pm
665 $ chmod 644 lib/MyApp/Controller/Login.pm
666
667Once again the actual action code does not need to change, we just rename C<$c>
668to C<$self> since the controller is now the invocant.
669
670 package MyApp::Controller::Login;
671 use Mojo::Base 'Mojolicious::Controller';
672
673 sub index {
674 my $self = shift;
675
676 my $user = $self->param('user') || '';
677 my $pass = $self->param('pass') || '';
678 return $self->render unless $self->users->check($user, $pass);
679
680 $self->session(user => $user);
681 $self->flash(message => 'Thanks for logging in.');
682 $self->redirect_to('protected');
683 }
684
685 sub logged_in {
686 my $self = shift;
687 return 1 if $self->session('user');
688 $self->redirect_to('index');
689 return undef;
690 }
691
692 sub logout {
693 my $self = shift;
694 $self->session(expires => 1);
695 $self->redirect_to('index');
696 }
697
698 1;
699
700All L<Mojolicious::Controller> controllers are plain old Perl classes and get
701instantiated on demand.
702
703=head2 Application class
704
705The application class C<lib/MyApp.pm> can now be reduced to model and routing
706information.
707
708 package MyApp;
709 use Mojo::Base 'Mojolicious';
710
711 use MyApp::Model::Users;
712
713 sub startup {
714 my $self = shift;
715
716 $self->secrets(['Mojolicious rocks']);
717 $self->helper(users => sub { state $users = MyApp::Model::Users->new });
718
719 my $r = $self->routes;
720 $r->any('/')->to('login#index')->name('index');
721
722 my $logged_in = $r->under('/')->to('login#logged_in');
723 $logged_in->get('/protected')->to('login#protected');
724
725 $r->get('/logout')->to('login#logout');
726 }
727
728 1;
729
730The router allows many different route variations,
731L<Mojolicious::Guides::Routing> explains them all in great detail.
732
733=head2 Templates
734
735Templates are our views, and usually bound to controllers, so they need to be
736moved into the appropriate directories.
737
738 $ mkdir templates/login
739 $ mv templates/index.html.ep templates/login/index.html.ep
740 $ mv templates/protected.html.ep templates/login/protected.html.ep
741
742=head2 Script
743
744Finally C<myapp.pl> can be moved into a C<script> directory and renamed to
745C<my_app> to follow the CPAN standard.
746
747 $ mkdir script
748 $ mv myapp.pl script/my_app
749
750Just a few small details change, instead of L<lib> we now use L<FindBin> and
751C<@INC>, allowing us to start the application from outside its home directory.
752
753 #!/usr/bin/env perl
754
755 use strict;
756 use warnings;
757
758 use FindBin;
759 BEGIN { unshift @INC, "$FindBin::Bin/../lib" }
760 use Mojolicious::Commands;
761
762 # Start command line interface for application
763 Mojolicious::Commands->start_app('MyApp');
764
765=head2 Simplified tests
766
767Full L<Mojolicious> applications are a little easier to test, so C<t/login.t>
768can be simplified.
769
770 use Test::More;
771 use Test::Mojo;
772
773 # Load application class
774 my $t = Test::Mojo->new('MyApp');
775 $t->ua->max_redirects(1);
776
777 $t->get_ok('/')
778 ->status_is(200)
779 ->element_exists('form input[name="user"]')
780 ->element_exists('form input[name="pass"]')
781 ->element_exists('form input[type="submit"]');
782
783 $t->post_ok('/' => form => {user => 'sebastian', pass => 'secr3t'})
784 ->status_is(200)
785 ->text_like('html body' => qr/Welcome sebastian/);
786
787 $t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);
788
789 $t->get_ok('/logout')
790 ->status_is(200)
791 ->element_exists('form input[name="user"]')
792 ->element_exists('form input[name="pass"]')
793 ->element_exists('form input[type="submit"]');
794
795 done_testing();
796
797And our final directory structure should be looking like this.
798
799 myapp
800 |- script
801 | +- my_app
802 |- lib
803 | |- MyApp.pm
804 | +- MyApp
805 | |- Controller
806 | | +- Login.pm
807 | +- Model
808 | +- Users.pm
809 |- t
810 | +- login.t
811 +- templates
812 |- layouts
813 | +- default.html.ep
814 +- login
815 |- index.html.ep
816 +- protected.html.ep
817
818Test-driven development takes a little getting used to, but can be a very
819powerful tool.
820
821=head1 MORE
822
823You can continue with L<Mojolicious::Guides> now or take a look at the
824L<Mojolicious wiki|http://github.com/kraih/mojo/wiki>, which contains a lot more
825documentation and examples by many different authors.
826
827=head1 SUPPORT
828
829If you have any questions the documentation might not yet answer, don't
830hesitate to ask on the
831L<mailing list|http://groups.google.com/group/mojolicious> or the official IRC
832channel C<#mojo> on C<irc.perl.org>
833(L<chat now!|https://chat.mibbit.com/?channel=%23mojo&server=irc.perl.org>).
834
835=cut
Note: See TracBrowser for help on using the repository browser.