Merge lp:~schuller-lunatech/play/1.1-scala-tutorial into lp:play/1.1

Proposed by Bart Schuller
Status: Merged
Merged at revision: not available
Proposed branch: lp:~schuller-lunatech/play/1.1-scala-tutorial
Merge into: lp:play/1.1
Diff against target: 779 lines (+718/-2)
5 files modified
documentation/manual/scguide2.textile (+5/-1)
documentation/manual/scguide3.textile (+329/-0)
documentation/manual/scguide4.textile (+245/-0)
documentation/manual/scguide5.textile (+137/-0)
modules/scala/src/play/scalasupport/core/ScalaPlugin.scala (+2/-1)
To merge this branch: bzr merge lp:~schuller-lunatech/play/1.1-scala-tutorial
Reviewer Review Type Date Requested Status
play framework developers Pending
Review via email: mp+15236@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Bart Schuller (schuller-lunatech) wrote :

A few more Scala tutorial chapters. Also enables printing of deprecation warnings, which is better than just being told there are some.

This may be my last contribution for a while, so feel free to continue without me.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'documentation/manual/scguide2.textile'
2--- documentation/manual/scguide2.textile 2009-11-09 21:58:51 +0000
3+++ documentation/manual/scguide2.textile 2009-11-25 08:20:26 +0000
4@@ -32,8 +32,9 @@
5 var fullname: String
6 ) extends Model {
7
8- var isAdmin: boolean = false
9+ var isAdmin: Boolean = false
10
11+ def this() = this(null, null, null)
12 }
13
14 If you're new to Scala then this class definition might look a bit strange. What is happening here is that the definition of 3 fields is combined with the definition of the primary constructor. Then there is a fourth field which is set to false by default.
15@@ -42,6 +43,7 @@
16
17 * a public class
18 * a public 3-argument constructor
19+* a public 0-argument constructor
20 * 4 private fields
21 * 4 public accessors and mutators
22
23@@ -159,6 +161,7 @@
24
25 var postedAt: Date = new Date()
26
27+ def this() = this(null, null, null)
28 }
29
30 Here we use the **@Lob** annotation to tell JPA to use a large text database type to store the post content. We have declared the relation to the User class using **@ManyToOne**. That means that each Post is authored by a single User, and that each User can author several Posts.
31@@ -230,6 +233,7 @@
32
33 var postedAt: Date = new Date()
34
35+ def this() = this(null, null, null)
36 }
37
38 Let's write a first test case:
39
40=== added file 'documentation/manual/scguide3.textile'
41--- documentation/manual/scguide3.textile 1970-01-01 00:00:00 +0000
42+++ documentation/manual/scguide3.textile 2009-11-25 08:20:26 +0000
43@@ -0,0 +1,329 @@
44+h1. Building the first screen (Scala version)
45+
46+Now that we have built a first data model, it's time to start to create the first page of the application. This page will just show the most recent posts, as well as a list of older posts.
47+
48+Here is a mock of what we want to achieve:
49+
50+!images/guide-mock1!
51+
52+h2. <a>Bootstrapping with default data</a>
53+
54+In fact before coding the first screen we need one more thing. Working on a web application without test data is not fun. You can't even test what you're doing. But because we haven't developed the contribution screens yet, we can't populate the blog with posts ourselves.
55+
56+One way to inject default data into the blog is to load a fixture file at application load time. To do that we will create a Bootstrap Job. A play job is something that executes itself outside of any HTTP request, for example at the application start or at specific interval using a CRON job.
57+
58+Let's create the **/yabe/app/Bootstrap.java** job that will load a set of default data using fixture:
59+
60+bc. import play._
61+import play.jobs._
62+import play.test._
63+import play.db.jpa.QueryFunctions._
64+
65+import models._
66+
67+@OnApplicationStart
68+class Bootstrap extends Job {
69+
70+ override def doJob() {
71+ // Check if the database is empty
72+ if(count[User] == 0) {
73+ Fixtures.load("initial-data.yml")
74+ }
75+ }
76+
77+}
78+
79+We have annotated this Job with the **@OnApplicationStart** annotation to tell play that we want to run this jobs synchronously at the application start.
80+
81+p(note). In fact this job will be run differently in DEV or PROD modes. In DEV mode, play waits for a first request to start. So this job will be executed synchronously at the first request. This way, if the job fail, you will get the error message in your browser. In PROD mode however, the job will be executed at application start (synchrously with the **'play run'** command) and will prevent the application to start in case of error.
82+
83+You have to create a initial-data.yml in the **/yabe/conf** directory. You can of course reuse the **data.yml** content that we just used for tests previously.
84+
85+Now run the application using **play run** and display the "http://localhost:9000":http://localhost:9000 page in the browser.
86+
87+h2. <a>The blog home page</a>
88+
89+This time, we can really start to code the home page.
90+
91+Do you remember how the first page is displayed? First the routes file defines that the **/** URL will invoke the **controllers.Application.index()** action method. Then this method calls **render()** and execute the **/yabe/app/views/Application/index.html** template.
92+
93+We will keep these components but add code to them to load the posts list and display them.
94+
95+Open the **/yabe/app/controllers/Application.java** controller and modify the **index()** action to load the posts list, as is:
96+
97+bc. package controllers
98+
99+import play._
100+import play.mvc._
101+import play.db.jpa.QueryFunctions._
102+
103+import models._
104+
105+object Application extends Actions {
106+
107+ def index() {
108+ val frontPost = find[Post]("order by postedAt desc").first
109+ val olderPosts = find[Post](
110+ "order by postedAt desc"
111+ ).from(1).fetch(10)
112+
113+ render(frontPost, olderPosts)
114+ }
115+
116+}
117+
118+Can you see how we pass objects to the render method? It will allow us to access them from the template using the same name.
119+
120+Open the **/yabe/app/views/Application/index.html** and modify it to display these objects:
121+
122+bc. #{extends 'main.html' /}
123+#{set title:'Home' /}
124+
125+#{if frontPost}
126+ <div class="post">
127+ <h2 class="post-title">
128+ <a href="#">${frontPost.title}</a>
129+ </h2>
130+ <div class="post-metadata">
131+ <span class="post-author">by ${frontPost.author.fullname}</span>
132+ <span class="post-date">${frontPost.postedAt.format('MMM dd')}</span>
133+ <span class="post-comments">
134+ &nbsp;|&nbsp;
135+ ${frontPost.comments.size() ?: 'no'}
136+ comment${frontPost.comments.size().pluralize()}</a>
137+ #{if frontPost.comments}
138+ , latest by ${frontPost.comments[0].author}
139+ #{/if}
140+ </span>
141+ </div>
142+ <div class="post-content">
143+ ${frontPost.content.nl2br()}
144+ </div>
145+ </div>
146+
147+ #{if olderPosts.size() > 1}
148+ <div class="older-posts">
149+ <h3>Older posts <span class="from">from this blog</span></h3>
150+
151+ #{list items:olderPosts, as:'oldPost'}
152+ <div class="post">
153+ <h2 class="post-title">
154+ <a href="#">${oldPost.title}</a>
155+ </h2>
156+ <div class="post-metadata">
157+ <span class="post-author">
158+ by ${oldPost.author.fullname}
159+ </span>
160+ <span class="post-date">
161+ ${oldPost.postedAt.format('dd MMM yy')}
162+ </span>
163+ <div class="post-comments">
164+ ${oldPost.comments.size() ?: 'no'}
165+ comment${oldPost.comments.size().pluralize()}
166+ #{if oldPost.comments}
167+ - latest by ${oldPost.comments[0].author}
168+ #{/if}
169+ </div>
170+ </div>
171+ </div>
172+ #{/list}
173+ </div>
174+
175+ #{/if}
176+
177+#{/if}
178+
179+#{else}
180+ <div class="empty">
181+ There is currently nothing to read here.
182+ </div>
183+#{/else}
184+
185+You can read more about the "template language here":templates. Basically, it allows you to access your java objects dynamically. Under the hood we use Groovy. Most of the pretty constructs you see (like the ?: operator) come from Groovy. But you don't really need to learn groovy to write play templates. If you're already familiar with another template language like JSP with JSTL you won't be lost.
186+
187+OK, now refresh the blog home page.
188+
189+!images/guide3-0!
190+
191+Not pretty but it works!
192+
193+However you see you have already started to duplicate code. Because we will display posts in several fashions (full, full with comment, teaser) we should create something like a function that we could call from several templates. This is exactly what a play tag does!
194+
195+To create a tag, just create the new **/yabe/app/views/tags/display.html** file. A tag is just another template. It has parameters (like a function). The **#{display /}** tag will have 2 parameters: the Post object to display and the display mode which will be one of 'home', 'teaser' or 'full'.
196+
197+bc. *{ Display a post in one of these modes: 'full', 'home' or 'teaser' }*
198+
199+<div class="post ${_as == 'teaser' ? 'teaser' : ''}">
200+ <h2 class="post-title">
201+ <a href="#">${_post.title}</a>
202+ </h2>
203+ <div class="post-metadata">
204+ <span class="post-author">by ${_post.author.fullname}</span>,
205+ <span class="post-date">${_post.postedAt.format('dd MMM yy')}</span>
206+ #{if _as != 'full'}
207+ <span class="post-comments">
208+ &nbsp;|&nbsp; ${_post.comments.size() ?: 'no'}
209+ comment${_post.comments.size().pluralize()}
210+ #{if _post.comments}
211+ , latest by ${_post.comments[0].author}
212+ #{/if}
213+ </span>
214+ #{/if}
215+ </div>
216+ #{if _as != 'teaser'}
217+ <div class="post-content">
218+ <div class="about">Detail: </div>
219+ ${_post.content.nl2br()}
220+ </div>
221+ #{/if}
222+</div>
223+
224+#{if _as == 'full'}
225+ <div class="comments">
226+ <h3>
227+ ${_post.comments.size() ?: 'no'}
228+ comment${_post.comments.size().pluralize()}
229+ </h3>
230+
231+ #{list items:_post.comments, as:'comment'}
232+ <div class="comment">
233+ <div class="comment-metadata">
234+ <span class="comment-author">by ${comment.author},</span>
235+ <span class="comment-date">
236+ ${comment.postedAt.format('dd MMM yy')}
237+ </span>
238+ </div>
239+ <div class="comment-content">
240+ <div class="about">Detail: </div>
241+ ${comment.content.escape().nl2br()}
242+ </div>
243+ </div>
244+ #{/list}
245+
246+ </div>
247+#{/if}
248+
249+Now using this tag we can rewrite the home page without code duplication :
250+
251+bc. #{extends 'main.html' /}
252+#{set title:'Home' /}
253+
254+#{if frontPost}
255+
256+ #{display post:frontPost, as:'home' /}
257+
258+ #{if olderPosts.size()}
259+
260+ <div class="older-posts">
261+ <h3>Older posts <span class="from">from this blog</span></h3>
262+
263+ #{list items:olderPosts, as:'oldPost'}
264+ #{display post:oldPost, as:'teaser' /}
265+ #{/list}
266+ </div>
267+
268+ #{/if}
269+
270+#{/if}
271+
272+#{else}
273+ <div class="empty">
274+ There is currently nothing to read here.
275+ </div>
276+#{/else}
277+
278+Reload the page and check that all is fine.
279+
280+h2. <a>Improving the layout</a>
281+
282+As you can see, the **index.html** template extends **main.html**. Because we want to provide a common layout to all blog pages with the blog title and authentication links, we need to modify this file.
283+
284+Edit the **/yabe/app/views/main.html** file :
285+
286+bc. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
287+
288+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
289+ <head>
290+ <title>#{get 'title' /}</title>
291+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
292+ <link rel="stylesheet" type="text/css" media="screen"
293+ href="@{'/public/stylesheets/main.css'}" />
294+ <link rel="shortcut icon" type="image/png"
295+ href="@{'/public/images/favicon.png'}" />
296+ </head>
297+ <body>
298+
299+ <div id="header">
300+ <div id="logo">
301+ yabe.
302+ </div>
303+ <ul id="tools">
304+ <li>
305+ <a href="#">Log in to write something</a>
306+ </li>
307+ </ul>
308+ <div id="title">
309+ <span class="about">About this blog</span>
310+ <h1><a href="#">${blogTitle}</a></h1>
311+ <h2>${blogBaseline}</h2>
312+ </div>
313+ </div>
314+
315+ <div id="main">
316+ #{doLayout /}
317+ </div>
318+
319+ <p id="footer">
320+ Yabe is a (not that) powerful blog engine built with the
321+ <a href="http://www.playframework.org">play framework</a>
322+ as a tutorial application.
323+ </p>
324+
325+ </body>
326+</html>
327+
328+Refresh and check the result. It seems to work, except that the **blogTitle** and the **blogBaseLine** variables are not displayed. This is because we didn't pass them during the **render(...)** call. Of course we could add them to the **render()** call in the index action. But because the **main.html** file will be used as main template for all application actions, we don't want to add them every time.
329+
330+One way to execute the same code for each action of a controller (or a hierarchy of controllers) is to define a **@Before** interceptor.
331+
332+Let's add the **addDefaults()** method to the Application controller :
333+
334+bc. @Before
335+def addDefaults() {
336+ renderArgs.put("blogTitle", Play.configuration.getProperty("blog.title"))
337+ renderArgs.put("blogBaseline", Play.configuration.getProperty("blog.baseline"))
338+}
339+
340+All variables added to the **renderArgs** scope will be available from the templates. And you can see that the method reads the variable's values from the **Play.configuration** object. This object contains all configuration keys from the **/yabe/conf/application.conf** file.
341+
342+Add these 2 keys to the configuration file:
343+
344+bc. # Configuration of the blog engine
345+# ~~~~~
346+blog.title=Yet another blog
347+blog.baseline=We won't write about anything
348+
349+Reload the home page and check that it works!
350+
351+!images/guide3-1!
352+
353+h2. <a>Adding some style</a>
354+
355+Now the blog home page is almost done, but it's not really pretty. We'll add some style to make it shinier. As you have seen, the main template file main.html includes the **/public/stylesheets/main.css** stylesheet. We'll keep it but add more style rules to it.
356+
357+You can "download it here":files/main.css, and copy it to the **/public/stylsheets/main.css** file.
358+
359+Refresh the home page and you should see a styled page now.
360+
361+!images/guide3-2!
362+
363+h2. <a>Commit your work</a>
364+
365+This time the blog home page is finished. As usual we can commit this blog version to bazaar:
366+
367+bc. $ bzr st
368+$ bzr add
369+$ bzr commit -m 'Home page'
370+
371+p(note). Go to the "next part":scguide4.
372+
373
374=== added file 'documentation/manual/scguide4.textile'
375--- documentation/manual/scguide4.textile 1970-01-01 00:00:00 +0000
376+++ documentation/manual/scguide4.textile 2009-11-25 08:20:26 +0000
377@@ -0,0 +1,245 @@
378+h1. Viewing and posting comments (Scala version)
379+
380+The blog home page is now set, and we will continue by writing the post details page. This page will show all the comments about the current post, and will include a form to post new comments.
381+
382+h2. <a>Creating the 'show' action</a>
383+
384+To display the post details page, we will need a new action method to the Application controller. Let's call it **show()**:
385+
386+bc. def show(id: Long) {
387+ val post = findById[Post](id)
388+ render(post)
389+}
390+
391+As you can see this action is pretty simple. We declare the **id** method parameter to automatically retrieve the HTTP **'id'** parameter as a Long object. This parameter will be extracted either from the queryString, from the URL path or from the request body.
392+
393+p(note). If we try to send an **id** HTTP parameter that is not a valid number, the id variable value will be null and play will automatically add a validation error to the **errors** container.
394+
395+This action will display the **/yabe/app/views/Application/show.html** template :
396+
397+bc. #{extends 'main.html' /}
398+#{set title:post.title /}
399+
400+#{display post:post, as:'full' /}
401+
402+Because we've already written the display tag, this page is really simple to write.
403+
404+h2. <a>Adding links to the details page</a>
405+
406+In the display tag we've left all links empty (using #). It's now time to make these links pointing to the **Application.show** action. With play you can easily build links in a template using the **@{...} notation**. This syntax uses the router to 'reverse' the URL needed to call the specified action.
407+
408+Let's edit the **/yabe/app/views/tags/display.html** tag :
409+
410+bc. ...
411+<h2 class="post-title">
412+ <a href="@{Application.show(_post.id)}">${_post.title}</a>
413+</h2>
414+...
415+
416+You can new refresh the home page, and click on a post title to display it.
417+
418+!images/guide4-0!
419+
420+It's great, but it lacks a link to go back to the home page. Edit the **/yabe/app/views/main.html** template to complete the title link:
421+
422+bc. ...
423+<div id="title">
424+ <span class="about">About this blog</span>
425+ <h1><a href="@{Application.index()}">${blogTitle}</a></h1>
426+ <h2>${blogBaseline}</h2>
427+</div>
428+...
429+
430+We can now navigate between the home page and the post detail pages.
431+
432+h2. <a>Specifying a better URL</a>
433+
434+As you can see, the post detail page URL looks like
435+
436+bc. /application/show?id=1
437+
438+This is because play has used the default 'catch all' route.
439+
440+bc. * /{controller}/{action} {controller}.{action}
441+
442+We can have a better URL by specifying a custom path for the **Application.show** action. Edit the **/yabe/conf/routes** file and add this route after the first one:
443+
444+bc. GET /posts/{id} Application.show
445+
446+p(note). This way the id parameter will be extracted from the URL path.
447+
448+Refresh the browser and check that it now uses the correct URL.
449+
450+h2. <a>Adding some pagination</a>
451+
452+To allow users to navigate easily through posts, we will add a pagination mechanism. We'll extend the Post class to be able to fetch the previous and next post as required:
453+
454+bc. import play.db.jpa.QueryFunctions._
455+...
456+def previous(): Post = {
457+ find[Post]("postedAt < ? order by postedAt desc", postedAt).first
458+}
459+
460+def next(): Post = {
461+ find[Post]("postedAt > ? order by postedAt asc", postedAt).first
462+}
463+
464+Since we will call these methods several times during a request it could be optimized, but it's good enough for now. Also, add the pagination links in top of the show.html template (before the **#{display/}** tag):
465+
466+bc. <ul id="pagination">
467+ #{if post.previous()}
468+ <li id="previous">
469+ <a href="@{Application.show(post.previous().id)}">
470+ ${post.previous().title}
471+ </a>
472+ </li>
473+ #{/if}
474+ #{if post.next()}
475+ <li id="next">
476+ <a href="@{Application.show(post.next().id)}">
477+ ${post.next().title}
478+ </a>
479+ </li>
480+ #{/if}
481+</ul>
482+
483+It's better now.
484+
485+h2. <a>Adding the comment form</a>
486+
487+Now it's time to set up a comments form. We'll start by adding the **postComment** action method to the Application controller.
488+
489+bc. def postComment(postId: Long, author: String, content: String) {
490+ val post = findById[Post](postId)
491+ post.addComment(author, content)
492+ show(postId)
493+}
494+
495+As you see we just reuse the **addComment()** method we previously added to the Post class.
496+
497+Let's write the HTML form in the show.html template (after the **#{display /}** tag in fact):
498+
499+bc. <h3>Post a comment</h3>
500+
501+#{form @Application.postComment(post.id)}
502+ <p>
503+ <label for="author">Your name: </label>
504+ <input type="text" name="author" id="author" />
505+ </p>
506+ <p>
507+ <label for="content">Your message: </label>
508+ <textarea name="content" id="content"></textarea>
509+ </p>
510+ <p>
511+ <input type="submit" value="Submit your comment" />
512+ </p>
513+#{/form}
514+
515+You can try to post a new comment. It should just work.
516+
517+!images/guide4-1!
518+
519+h2. <a>Adding validation</a>
520+
521+Currently we don't validate the form content before creating the comment. We would like to set both fields as required. We can easily use the play validation mechanism to ensure that HTTP parameters are correctly filled. Modify the **postComment** action to add validation annotations and check that no error occurs:
522+
523+bc. def postComment(postId: Long, @Required author: String, @Required content: String) {
524+ val post = findById[Post](postId)
525+ if (Validation.hasErrors()) {
526+ render("Application/show.html", post)
527+ }
528+ post.addComment(author, content)
529+ show(postId)
530+}
531+
532+p(note). **Don't forget** to import play.data.validation._ as well.
533+
534+As you see, in case of validation errors, we re-display the post detail page. We have to modify the form code to display the error message:
535+
536+bc. <h3>Post a comment</h3>
537+
538+#{form @Application.postComment(post.id)}
539+
540+ #{ifErrors}
541+ <p class="error">
542+ All fields are required!
543+ </p>
544+ #{/ifErrors}
545+
546+ <p>
547+ <label for="author">Your name: </label>
548+ <input type="text" name="author" id="author" value="${params.author}" />
549+ </p>
550+ <p>
551+ <label for="content">Your message: </label>
552+ <textarea name="content" id="content">${params.content}</textarea>
553+ </p>
554+ <p>
555+ <input type="submit" value="Submit your comment" />
556+ </p>
557+#{/form}
558+
559+Note that we reuse the posted parameters to fill the HTML input values.
560+
561+To make the UI feedback more pleasant for the poster, we will add a little Javascript to automatically focus the comment form in case of an error. As this script uses "JQuery":files/jquery-1.3.2.min.js and "JQuery Tools":files/jquery.tools.min.js as support libraries, you have to include them. Download these 2 libraries to the **/yabe/public/javascripts** directory and modify the **main.html** template to include them:
562+
563+bc. ...
564+ <script src="@{'/public/javascripts/jquery-1.3.2.min.js'}"></script>
565+ <script src="@{'/public/javascripts/jquery.tools.min.js'}"></script>
566+</head>
567+
568+Now you can add this script to the **show.html** template (add it at the end of the page):
569+
570+bc. <script type="text/javascript" charset="utf-8">
571+ $(function() {
572+ // Expose the form
573+ $('form').click(function() {
574+ $('form').expose({api: true}).load();
575+ });
576+
577+ // If there is an error, focus to form
578+ if($('form .error').size()) {
579+ $('form').expose({api: true, loadSpeed: 0}).load();
580+ $('form input').get(0).focus();
581+ }
582+ });
583+</script>
584+
585+!images/guide4-2!
586+
587+The comment form looks pretty cool now. We will add two more things.
588+
589+First we will display a success message after a comment is successfully posted. For that, we use the flash scope that allows to dispatch messages from one action call to the next one.
590+
591+Modify the **postComment** action to add a success message:
592+
593+bc. public static void postComment(Long postId, @Required String author, @Required String content) {
594+ Post post = Post.findById(postId);
595+ if(validation.hasErrors()) {
596+ render("Application/show.html", post);
597+ }
598+ post.addComment(author, content);
599+ flash.success("Thanks for posting, %s", author);
600+ show(postId);
601+}
602+
603+and display the success message in the **show.html** if present (add it at the top the page):
604+
605+bc. ...
606+#{if flash.success}
607+ <p class="success">${flash.success}</p>
608+#{/if}
609+
610+#{display post:post, as:'full' /}
611+...
612+
613+!images/guide4-3!
614+
615+The last thing we will adjust in this form is the URL used for the **postComment** action. As always it uses the default catch all route because we didn't define any specific route. So add this route to the application routes file:
616+
617+bc. POST /posts/{postId}/comments Application.postComment
618+
619+That's done. As always, commit the version to bazaar.
620+
621+p(note). Go to the "next part":scguide5.
622+
623
624=== added file 'documentation/manual/scguide5.textile'
625--- documentation/manual/scguide5.textile 1970-01-01 00:00:00 +0000
626+++ documentation/manual/scguide5.textile 2009-11-25 08:20:26 +0000
627@@ -0,0 +1,137 @@
628+h1. Setting up a captcha (Scala version)
629+
630+Because anyone can post a comment to our blog engine, we should protect it a little to avoid automated spam. A simple way to protect a form from this is to add a captcha image.
631+
632+h2. <a>Generating the captcha image</a>
633+
634+We'll start to see how we can easily generate a captcha image using play. Basically we will just use another action, except that it will return a binary stream instead of HTML responses like what we've returned so far.
635+
636+Play being a **full-stack** web framework, we try to include built-in constructs for a web application's most typical needs; generating a captcha is one of them. We can use the **play.libs.Images** utility to simply generate a captcha image, and then write it to the HTTP response.
637+
638+As usual, we will start with a simple implementation. Add the **captcha** action to the **Application** controller:
639+
640+bc. def captcha() {
641+ val captcha = Images.captcha()
642+ renderBinary(captcha)
643+}
644+
645+Note that we can pass the captcha object directly to the renderBinary() method because the Images.Captcha class implements java.io.InputStream.
646+
647+p(note). **Don't forget** to import play.libs._
648+
649+Now add a new route to the **/yabe/conf/routes** file:
650+
651+bc. GET /captcha Application.captcha
652+
653+And try the **captcha** action by opening "http://localhost:9000/captcha":http://localhost:9000/captcha.
654+
655+!images/guide5-1!
656+
657+It should generate a random text for each refresh.
658+
659+h2. <a>How to manage the state?</a>
660+
661+Until now it was easy, but the most complicated part is coming. To validate the captcha we need to save the random text written to the captcha image and then check it at the form submission time.
662+
663+Of course we could just put the text in the user session at image generation time and then retrieve it later. But this solution has two drawbacks:
664+
665+**First** the play session is stored as a cookie. It solves a lot of problems in terms of architecture but has a lot of implications. Data written to the session cookie are signed (so the user can't modify them) but not encrypted. If we write the captcha code to the session any bot could easily resolve it by reading the session cookie.
666+
667+**Then** remember that play is a **stateless** framework. We want to manage things in a pure stateless way. Typically, what happens if a user simultaneously opens two different blog pages with two different captcha images? We have to track the captcha code for each form.
668+
669+So to resolve the problem we need two things. We will store the captcha secret key on the server side. Because it is transient data we can easily use the play **Cache**. Moreover because cached data have a limited life time it will add one more security mechanism (let's say that a captcha code will be available for only 10mn). Then to resolve the code later we need to generate a **unique ID**. This unique ID will be added to each form as a hidden field and implicitly references a generated captcha code.
670+
671+This way we elegantly solve our state problem.
672+
673+Modify the **captcha** action as follows:
674+
675+bc. def captcha(id: String) {
676+ val captcha = Images.captcha()
677+ val code = captcha.getText("#E4EAFD")
678+ Cache.set(id, code, "10mn")
679+ renderBinary(captcha)
680+}
681+
682+Note that the **getText()** method takes any color as parameter. It will use this color to draw the text.
683+
684+p(note). **Don't forget** to import play.cache._
685+
686+h2. <a>Adding the captcha image to the comment form</a>
687+
688+Now, before displaying a comment form we will generate a unique ID. Then we will modify the HTML form to integrate a captcha image using this ID, and add the ID to another hidden field.
689+
690+Let's rewrite the **Application.show** action:
691+
692+bc. public static void show(Long id) {
693+ Post post = Post.findById(id);
694+ String randomID = Codec.UUID();
695+ render(post, randomID);
696+}
697+
698+And now the form in the **/yable/app/views/Application/show.html** template:
699+
700+bc. ...
701+<p>
702+ <label for="content">Your message: </label>
703+ <textarea name="content" id="content">${params.content}</textarea>
704+</p>
705+<p>
706+ <label for="code">Please type the code below: </label>
707+ <img src="@{Application.captcha(randomID)}" />
708+ <br />
709+ <input type="text" name="code" id="code" size="18" value="" />
710+ <input type="hidden" name="randomID" value="${randomID}" />
711+</p>
712+<p>
713+ <input type="submit" value="Submit your comment" />
714+</p>
715+...
716+
717+Good start. The comment form now has a captcha image.
718+
719+!images/guide5-2!
720+
721+h2. <a>Validating the captcha</a>
722+
723+Now we just have to validate the captcha. We have added the **randomID** as an hidden field right ? So we can retrieve it in the **postComment** action, then retrieve the actual code from Cache and finally compare it to the submitted code.
724+
725+Not that difficult. Let's modify the **postComment** action.
726+
727+bc. public static void postComment(
728+ Long postId,
729+ @Required(message="Author is required") String author,
730+ @Required(message="A message is required") String content,
731+ @Required(message="Please type the code") String code,
732+ String randomID)
733+{
734+ Post post = Post.findById(postId);
735+ validation.equals(
736+ code, Cache.get(randomID)
737+ ).message("Invalid code. Please type it again");
738+ if(validation.hasErrors()) {
739+ render("Application/show.html", post, randomID);
740+ }
741+ post.addComment(author, content);
742+ flash.success("Thanks for posting %s", author);
743+ show(postId);
744+}
745+
746+Because we have now more error messages, modify the way we display errors in the **show.html** template (yes we will just display the first error, it's good enough):
747+
748+bc. ..
749+#{ifErrors}
750+ <p class="error">
751+ ${errors[0]}
752+ </p>
753+#{/ifErrors}
754+...
755+
756+p(note). Typically for more complex forms, error messages are not managed this way but externalized in the **messages** file and each error is printed in side of the corresponding field.
757+
758+Check that the captcha is now fully functional.
759+
760+!images/guide5-3!
761+
762+Great!
763+
764+p(note). Go to the "next part":guide6.
765\ No newline at end of file
766
767=== modified file 'modules/scala/src/play/scalasupport/core/ScalaPlugin.scala'
768--- modules/scala/src/play/scalasupport/core/ScalaPlugin.scala 2009-11-24 20:05:47 +0000
769+++ modules/scala/src/play/scalasupport/core/ScalaPlugin.scala 2009-11-25 08:20:26 +0000
770@@ -85,7 +85,8 @@
771 private val virtualDirectory = new VirtualDirectory("(memory)", None)
772 private val settings = new Settings()
773 settings.debuginfo.level = 3
774- settings.outputDirs setSingleOutput virtualDirectory
775+ settings.outputDirs setSingleOutput virtualDirectory
776+ settings.deprecation.value = true
777 private val compiler = new Global(settings, reporter)
778
779 def compile(sources: List[VFile]) = {

Subscribers

People subscribed via source and target branches