yabeチュートリアル その3 最初の画面を作る

※ 本記事では、Page not found — Playframeworkを和訳しています。

さて、最初のデータモデル作成が完了しているので、アプリケーションの画面開発に入ります。この画面は、古いPostだけでなく、最近のPostを表示します。

こちらはモックアップです。

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




デフォルトデータではじめてみる

実は、画面の開発を始める前に、もうひとつやらなければならないことがあります。テストデータ無しで動くWebアプリは面白くありません。挙動のテストもできません。しかし画面を開発中のため、自分でデータを登録していくことはできません。

ブログにデフォルトデータを投入するひとつの方法は、固定ファイルをアプリケーション開始時にロードすることです。まずやらなければいけないことはBootstrapジョブを作ることです。Playジョブは、アプリケーション開始のタイミングや、Cronジョブを使った特定の間隔といった、HTTPリクエストが無いタイミングで何かを実行させるものです。

/yabe/app/bootsrap.scalaファイルを作り、Fixturesを使ってデフォルトデータをロードするジョブを定義しましょう。

import play.jobs._
    
@OnApplicationStart class BootStrap extends Job {
    
    override def doJob {
        
        import models._
        import play.test._
        
        // Import initial data if the database is empty
        if(User.count().single() == 0) {
            Yaml[List[Any]]("initial-data.yml").foreach { 
                _ match {
                    case u:User => User.create(u)
                    case p:Post => Post.create(p)
                    case c:Comment => Comment.create(c)
                }
            }
        }        
        
    }
    
}

このジョブでは、@OnApplicationStartアノテーションを使って、アプリケーション起動と同期をとって実行したいということをPlayフレームワークに伝えています。

実際にはこのジョブはDEVモードとPRODモードで違う挙動をします。DEVモードでは、最初のリクエストが来るまで待ちます。なので、このジョブは初回リクエストと同期をとって稼働することになります。このため、もしジョブが失敗すると、ブラウザにエラーメッセージが表示されます。しかしながらPROVモードの時は、アプリケーション稼働と同期をとって(正確に言うと、play runコマンド実行時に)稼働するので、エラー状態でアプリケーションが稼働することを防ぎます。

yabe/conf/ディレクトリに、initial-data.ymlファイルを作成しなければなりません。もちろん前回使った、data.ymlを使うことも可能です。

では、play run コマンドを実行して、ブラウザで http://localhost:9000/ を参照してください。

Bootstrapジョブを適用させるために、アプリケーションの再起動が必要です。SQLコンソールにアクセスすることで、初期データが正しくデータベースに投入されているかを確認することができます

ブログホームページ

いよいよホームページのコーディングを開始します。

最初のページがどのように表示されていたか覚えていますか? まず、/ URLが、controllers.Application.indexアクションメソッドを呼び出すようにroutesファイルで設定されています。このメソッドはTemplateクラスを応答し、/yabe/app/views/Application/index.html テンプレートを実行します。

このコンポーネントを維持したまま、Postデータをリスト表示するコードを追加します。

/yabe/app/controllers.scalaコントローラを開き、indexアクションPostリストをロードするように編集します。

def index = {
    val allPosts = Post.allWithAuthorAndComments
    html.index(
        front = allPosts.headOption, 
        older = allPosts.drop(1)
    )
}

Application controllerクラスに、models._ をインポートするのをお忘れなく。

どのようにviews.Application.html.indexを呼んでいるか分かりますか?Symbolによって定義された名前を使ってテンプレートからアクセスしているのです。このケースでは、fron変数とolder変数がテンプレートで使えるようになります。

/yabe/app/views/Application/index.scala.htmlを開き、これらのオブジェクトを表示するように編集してください。

@(
    front:Option[(models.Post,models.User,Seq[models.Comment])], 
    older:Seq[(models.Post,models.User,Seq[models.Comment])]
)
 
@main(title = "Home") {
    
    @front.map { front =>
 
        <div class="post">
            <h2 class="post-title">
                <a href="#">@front._1.title</a>
            </h2>
            <div class="post-metadata">
                <span class="post-author">by @front._2.fullname</span>
                <span class="post-date">
                    @front._1.postedAt.format("MMM dd")
                </span>
                <span class="post-comments">
                    &nbsp;|&nbsp; 
 
                    @if(front._3) {
                        @front._3.size comments, 
                        latest by @front._3(0).author
                    } else {
                        no comments
                    }
 
                </span>
            </div>
            <div class="post-content">
                @Html(front._1.content.replace("\n", "<br>"))
            </div>
        </div>
 
        @Option(older).filterNot(_.isEmpty).map { posts =>
 
            <div class="older-posts">    
                <h3>Older posts <span class="from">from this blog</span></h3>
 
                @posts.map { post =>
                    <div class="post">
                       <h2 class="post-title">
                           <a href="#">@post._1.title</a>
                       </h2>
                       <div class="post-metadata">
                           <span class="post-author">
                               by @post._2.fullname
                           </span>
                           <span class="post-date">
                               @post._1.postedAt.format("dd MMM yy")
                           </span>
                           <div class="post-comments">
                               @if(post._3) {
                                   @post._3.size comments, 
                                   latest by @post._3(0).author
                               } else {
                                   no comments
                               }
                           </div>
                       </div>
                   </div>
                }
 
            </div> 
 
        }
 
    }.getOrElse {
 
        <div class="empty">
            There is currently nothing to read here.
        </div>
 
    }
    
}

テンプレートの働き方については、Page not found — Playframeworkで知ることができます。基本的には、シンプルなテキストファイルにScalaコードをかけるようになるというものです。内部的には、テンプレートは標準的なScala関数にコンパイルされています。

では、ブログホームページを再表示してみてください。

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

今ひとつだけど、動きました!

しかしながら、既にコードが重複し始めていることが分かります。Postをいくつかの方法(全部、コメント付き全部、短い一行)で表示するので、いくつかのテンプレートから呼び出せる別の関数を作る必要があります。テンプレートが単なる関数であったように、いくつかの方法で簡単に組み合わせることができます。

/yabe/app/views/Application/display.scala.htmlを作ってください。

@(post:(models.Post,models.User,Seq[models.Comment]), mode: String = "full")
 
@commentsTitle = {
    @if(post._3) {
        @post._3.size comments, latest by @post._3(0).author
    } else {
        no comments
    }
}
  
<div class="post @mode">
    <h2 class="post-title">
        <a href="#">@post._1.title</a>
    </h2>
    <div class="post-metadata">
        <span class="post-author">by @post._2.fullname</span>,
        <span class="post-date">
            @post._1.postedAt.format("dd MMM yy")
        </span>
        @if(mode != "full") {
            <span class="post-comments">
                @commentsTitle
            </span>
        }
    </div>
    @if(mode != "teaser") {
        <div class="post-content">
            <div class="about">Detail: </div>
            @Html(post._1.content.replace("\n", "<br>"))
        </div>
    }
</div>
 
@if(mode == "full") {
    
    <div class="comments">
        <h3>
            @commentsTitle
        </h3>
        
        @post._3.map { comment =>
            <div class="comment">
                <div class="comment-metadata">
                    <span class="comment-author">by @comment.author,</span>
                    <span class="comment-date">
                        @comment.postedAt.format("dd MMM yy")
                    </span>
                </div>
                <div class="comment-content">
                    <div class="about">Detail: </div>
                    @Html(comment.content.replace("\n", "<br>"))
                </div>
            </div>
        }
        
    </div>
    
}

このタグを使って、重複なくホームページを書き換えることができます。

@(
    front:Option[(models.Post,models.User,Seq[models.Comment])], 
    older:Seq[(models.Post,models.User,Seq[models.Comment])]
)
 
@main(title = "Home") {
    
    @front.map { front =>
        
        @display(front, mode = "home")
 
        @Option(older).filterNot(_.isEmpty).map { posts =>
 
            <div class="older-posts">    
                <h3>Older posts <span class="from">from this blog</span></h3>
 
                @posts.map { post =>
                    @display(post, mode = "teaser")
                }
 
            </div> 
 
        }
 
    }.getOrElse {
 
        <div class="empty">
            There is currently nothing to read here.
        </div>
 
    }
    
}

ページをリロードして、動きを確認してください。



レイアウトを改善する

見て分かるように、index.scala.htmlテンプレートは、main.scala.htmlテンプレートを継承しています。全てのブログページを、ブログタイトルと著者リンクのところを共通レイアウトとしたいので、このファイルを編集する必要があります。

/yabe/app/views/main.scala.htmlファイルを編集してください。

@(title:String = "")(body: => Html)
 
<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <link rel="stylesheet" media="screen" href="@asset("public/stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@asset("public/images/favicon.png")">
        <script src="@asset("public/javascripts/jquery-1.5.2.min.js")" type="text/javascript"></script>
    </head>
    <body>
         
        <div id="header">
            <div id="logo">
                yabe.
            </div>
            <ul id="tools">
                <li>
                    <a href="#">Log in to write something</a>
                </li>
            </ul>
            <div id="title">
                <span class="about">About this blog</span>
                <h1><a href="#">@play.Play.configuration.get("blog.title")</a></h1>
                <h2>@play.Play.configuration.get("blog.baseline")</h2>
            </div>
        </div>
        
        <div id="main">
            @body
        </div>
        
        <p id="footer">
            Yabe is a (not that) powerful blog engine built with the 
            <a href="http://www.playframework.org">Play framework</a>
            as a tutorial application.
        </p>
        
    </body>
</html>

また、以下ふたつの定義を、設定ファイルに追加してください。

# Blog engine configuration
# ~~~~~
blog.title=Yet another blog
blog.baseline=We won't write about anything

リロードして結果を確認してください。

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



Styleを追加

ブログホームページはほとんどできあがりましたが、あまりかわいくありません。いくつかスタイルを設定して、多少かっこ良くしましょう。これまで見てきたように、mainテンプレートファイルは、/public/stylesheets/main.cssスタイルシートをインクルードしています。これを保持しつつ、スタイルルールを加えて行きましょう。

スタイルシートをダウンロードして、 /public/stylesheets/main.cssファイルにコピーしてください。

ページを再表示すると、スタイルが設定された画面を確認することができます。

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