yabeチュートリアル その4 コメントの投稿と表示

※ 本記事はPage not found — Playframeworkを和訳したものです。

ブログのホームページは出来上がっているので、続いてPost詳細ページをコーディングします。このページは今のPostに対する全てのコメントを表示し、新しいコメントを投稿するフィールドを含めます。



showアクション作成

Post詳細ページを表示するために、Applicationコントローラーに新しいActionが必要です。showアクションとしてこれを作りましょう。

def show(id: Long) = {
    Post.byIdWithAuthorAndComments(id).map {
        html.show(_)
    } getOrElse {
        NotFound("No such Post")
    }
}

このようにshowアクションはとてもシンプルです。Long型のScala値としてHTTPのidパラメータを自動的に受け取るメソッドパラメータをidとして宣言しています。このパラメータは、クエリストリングか、URLパスか、リクエスト電文のBody部かのいずれかから引き抜かれます。

もし送信したHTTPパラメータのidが、数値型でなければあ、errorsコンテナにバリデーションエラーを自動的に加えます。

このアクションは/yabe/app/views/Application/show.scala.htmlテンプレートを表示します。

@(post:(models.Post,models.User,Seq[models.Comment]))
 
@main(title = post._1.title) {
    
    @display(post, mode = "full")
    
}

display関数は既に作ってあるので、このページはとてもシンプルに書けます。



詳細ページにリンクを追加

displayタグの中では、全てのリンクは#を使って空にしてあります。Application.showアクションにリンクポイントを設定しましょう。Playフレームワークでは、テンプレートの中でaction
ヘルパーを使うことで簡単にリンクを構築することができます。

...
<h2 class="post-title">
    <a href="@action(controllers.Application.show(post._1.id()))">
        @post._1.title
    </a>
</h2>
...

ホームページをリロードし、Postタイトルをクリックし、Postページを表示してください。

f:id:teppei-studio:20110612115505p:image

すばらしい、ですが、ホームページに戻るリンクがありませんね。/yabe/app/views/main.scala.htmlテンプレートを編集し、タイトルリンクを完成させましょう。

...
<div id="title">
    <span class="about">About this blog</span>
    <h1>
        <a href="@action(controllers.Application.index)">
            @play.Play.configuration.get("blog.title")
        </a>
    </h1>
    <h2>@play.Play.configuration.get("blog.baseline")</h2>
</div>
... 

これでホームページとPost詳細ページの間をナビゲートすることができるようになりました。



URLをもう少しよくしましょう。

見て分かるように、Post詳細ページのURLは以下のような具合です。

/application/show?id=1

Playフレームワークはデフォルトの、「catch all」ルートを使っているからです。

*       /{controller}/{action}                  {controller}.{action}

Application.showアクションへのカスタムパス特定することによって、URLを良くすることができます。/yabe/conf/routes ファイルを編集し、最初のものの後ろに、以下のルートを追加してください。

GET     /posts/{id}                             Application.show

この方法では、URLパスからidが取得されます。

ブラウザを再読み込みして、挙動を確認してください。



ページングを追加

Postを簡単にナビゲーションするために、ページングの仕掛けを追加しましょう。Postクラスを拡張して、前と次をくくり付けられるようにします。

ここにあるSQLクエリは、特定のエレメントから前と次のPostを括り付けるために使うものです。

(
    select *, 'next' as pos from post 
    where postedAt < {date} order by postedAt desc limit 1
)
    union
(
    select *, 'prev' as pos from post 
    where postedAt > {date} order by postedAt asc limit 1
)
 
order by postedAt desc

そしてクエリ結果を(Option[Post], Option[Post])の値に変換させます。このふたつのうち、一つ目が前の要素がPostで、二つ目の要素が次のPostに該当します。

opt('pos.is("prev") ~> Post.on("")) ~ opt('pos.is("next") ~> Post.on(""))

これがPostクラスに追加する新しいprevNextメソッドです。

case class Post(
  id: Pk[Long],
  title: String, content: String, postedAt: Date, author_id: Long
){

def prevNext = {        
    SQL(
        """
            (
                select *, 'next' as pos from post 
                where postedAt < {date} order by postedAt desc limit 1
            )
                union
            (
                select *, 'prev' as pos from post 
                where postedAt > {date} order by postedAt asc limit 1
            )
 
            order by postedAt desc
            
        """
    ).on("date" -> postedAt).as( 
        opt('pos.is("prev")~>Post.on("")) ~ opt('pos.is("next")~>Post.on("")) 
        ^^ flatten
    )
}
}

そしてshow.htmlテンプレートに、以下の情報を追加しましょう。

def show(id: Long) = {
    Post.byIdWithAuthorAndComments(id).map { post =>
        html.show(post, post._1.prevNext)
    } getOrElse {
        NotFound("No such Post")
    }
}

これらのメソッドをリクエストの間、何回か呼ぶことになりますが、まだ十分ではありません。show.scala.htmlテンプレートのトップにページングリンクを追加しましょう。

@(
    post:(models.Post,models.User,Seq[models.Comment]),
    pagination:(Option[models.Post],Option[models.Post])
)
 
@main(title = post._1.title) {
    
    <ul id="pagination">
        @pagination._1.map { post =>
            <li id="previous">
                <a href="@action(controllers.Application.show(post.id()))">
                    @post.title
                </a>
            </li>
        }
        @pagination._2.map { post =>
            <li id="next">
                <a href="@action(controllers.Application.show(post.id()))">
                    @post.title
                </a>
            </li>
        }
    </ul>
    
    @display(post, mode = "full")
    
}

コメントフォームを追加

コメントフォームをセットアップしましょう。postCommentアクションをApplicationContorllerに追加します。

def postComment(postId:Long) = {
    val author = params.get("author")
    val content = params.get("content")
    Comment.create(Comment(postId, author, content))
    Action(show(postId))
}

Action値を返すようにしたのは、show(postId)にリダイレクトさせたいということを指し示しています。

show.htmlテンプレートの、#{display /} タグの後ろに、HTMLフォームを書きましょう。

<h3>Post a comment</h3>
 
@form(controllers.Application.postComment(post._1.id())) {
    <p>
        <label for="author">Your name: </label>
        <input type="text" name="author" />
    </p>
    <p>
        <label for="content">Your message: </label>
        <textarea name="content"></textarea>
    </p>
    <p>
        <input type="submit" value="Submit your comment" />
    </p>
}

さて、新しいコメントをPostしてみましょう。

f:id:teppei-studio:20110612131340p:image




バリデーションを追加

現在、コメント登録前にコンテンツのバリデーションをしていません。両方の入力を必須にしたいと思います。Playバリデーション機能を使えば、HTTPパラメータが埋められていることを簡単に確実にすることができます。postCommentアクションを編集して、バリデーションを行い、エラーが起きていないことを確認てください。

def postComment(postId:Long) = {
    val author = params.get("author")
    val content = params.get("content")
    Validation.required("author", author)
    Validation.required("content", content)
    if(Validation.hasErrors) {
        show(postId)
    } else {
        Comment.create(Comment(postId, author, content))
        Action(show(postId))
    }
}

play.data.validation._ をImportするのを忘れないでください。

見て分かるように、バリデーションエラーになった場合、Post詳細ページを再表示しています。エラーメッセージを表示するように修正しましょう。

<h3>Post a comment</h3>
 
@form(controllers.Application.postComment(post._1.id())) {
    
    @if(errors) {
        <p class="error">
            All fields are required!
        </p>
    }
    
    <p>
        <label for="author">Your name: </label>
        <input type="text" name="author" value="@params.get("author")">
    </p>
    <p>
        <label for="content">Your message: </label>
        <textarea name="content">@params.get("content")</textarea>
    </p>
    <p>
        <input type="submit" value="Submit your comment" />
    </p>
}

errorsparamsを参照しています。それをテンプレートパラメータリストに加えて、暗黙値としてマークする必要があります。

@(
    post:(models.Post,models.User,Seq[models.Comment]),
    pagination:(Option[models.Post],Option[models.Post])
)(
    implicit 
    params:play.mvc.Scope.Params,
    flash:play.mvc.Scope.Flash,
    errors:Map[String,play.data.validation.Error]
)
 
…

投稿者に対してもっと分かりやすいフィードバックUIを作るために、エラー時に自動的に入力欄にフォーカスが当たる小さなJavaScriptを追加します。このスクリプトはサポートライブラリとしてJQuery Tools Exposeを使っているので、これをインクルードしておきます。このライブラリをダウンロードして、yabe/public/javascripts/ディレクトリに格納して、main.htmlテンプレートを以下のインクルード文を含めるように修正してください。

<script src="@asset("public/javascripts/jquery-1.5.2.min.js")"></script>
<script src="@asset("public/javascripts/jquery.tools.min.js")"></script>

これで以下のスクリプトを、show.scala.htmlテンプレートに追加できるようになりました。(ページの最後に追加するように御願いします。)

<script type="text/javascript" charset="utf-8">
    $(function() {         
        // Expose the form 
        $('form').click(function() { 
            $('form').expose({api: true}).load(); 
        }); 
        
        // If there is an error, focus to form
        if($('form .error').size()) {
            $('form').expose({api: true, loadSpeed: 0}).load(); 
            $('form input[type=text]').get(0).focus();
        }
    });
</script>

f:id:teppei-studio:20110612183517p:image

コメントフォームはとってもかっこ良くなりました。もう2つやりましょう。

ひとつは、Postが成功した時に、成功メッセージを出す処理を追加します。このためにflash scopeを使います。

postCommentを編集し、成功メッセージを追加してください。

def postComment(postId:Long) = {
    val author = params.get("author")
    val content = params.get("content")
    Validation.required("author", author)
    Validation.required("content", content)
    if(Validation.hasErrors) {
        show(postId)
    } else {
        Comment.create(Comment(postId, author, content))
        flash += "success" -> ("Thanks for posting " + author)
        Action(show(postId))
    }
}

show.htmlに表示を追加します。

…
@if(flash.get("success")) {
    <p class="success">@flash.get("success")</p>
}
…

f:id:teppei-studio:20110612191745p:image

最後にURLをシンプルにしましょう。ルートファイルに以下定義を追加してください。

POST    /posts/{postId}/comments                Application.postComment

以上。