torutkのブログ

ソフトウェア・エンジニアのブログ

「Grailsをマスターする:Groovy Server Pagesによるビューの変更」を読みながら動かしてみる

Grails をマスターする: Groovy Server Pages によるビューの変更の記事に沿ってGrailsの習得を進めます。

id:torutk:20090718の続きです。

コントローラとビューの生成を実行時に動的ではなく事前生成に変更し、生成された内容を読む

前回までは、Grailsの簡単CRUD機能実現で大きな役割を果たすscaffold指定だけをコントローラに記述し、ドメインクラスを定義するだけでWebアプリケーションのコントローラ処理、ビュー処理は実行時に動的に自動生成させていました。

今回の記事は、Grailsの内容を理解するため、事前にコマンドでコントローラ処理とビュー処理のGroovyコードを生成させ、その中を読むというものです。

前回作成したtrip-plannerディレクトリで、grails generate-allコマンドを実行します。

trip-planner$ grails generate-all Trip
    :(中略)
Generating views for domain class Trip ...
Generating controller for domain class Trip ...
File /home/torutk/work/trip-planner/grails-app/controllers/TripController.groovy already exists. Overwrite?y,n,a
a
Finished generation for domain class Trip
trip-planner$ 

Scaffold記述をしたコントローラ(TripController.groovy)が既にあるので、上書きするか聞いてきます。上書きします。

TripController.groovyのlist処理

生成されたTripControllerクラスのリスト処理は以下です。

def list = {
    params.max = Math.min( params.max ? params.max.toInteger() : 10,  100)
    [ tripInstanceList: Trip.list( params ), tripInstanceTotal: Trip.count() ]
}

変数名ほか少し記事とはソースコードが違っていますが、意図はほぼ一緒です。

trip/list.gsp

生成されたビューのリスト表示(抜粋)は以下です。

  <g:each in="${tripInstanceList}" status="i" var="tripInstance">
    <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
      <td>
        <g:link action="show" id="${tripInstance.id}">${fieldValue(bean:tripInstance, field:'id')}</g:link>
      </td>                       
      <td>${fieldValue(bean:tripInstance, field:'airline')}</td>
      <td>${fieldValue(bean:tripInstance, field:'city')}</td>
      <td>${fieldValue(bean:tripInstance, field:'endDate')}</td>
      <td>${fieldValue(bean:tripInstance, field:'name')}</td>
      <td>${fieldValue(bean:tripInstance, field:'notes')}</td>
    </tr>
  </g:each>

記事と違うところは、<g:link>要素のデータです。記事の方では以下のようになっていました。

<g:link action="show" id="${trip.id}">${trip.id?.encodeAsHTML()}</g:link>

fieldValueは、GSPのTagとして存在しているようです。

スクリプトレット copyrightの追加

スクリプトレットは簡単だが不適切な手段といっていながら、スクリプトレットcopyrightの説明があるので、動かして見たいと思います。

うーん、うーん、記事で断片がリストとして上がっているのですが、これはどこに書けばいいのだろうか? とりあえず、views/trip/list.gspに追加してみることにする。

list.gspに追加

どこに追加するのかよく分からず、まずはtrip/list.gspの終わりの方、<div class="body">の最後の要素として追加しました。

  :(前略)
  <div class="body">
    :(中略)
    <div id="copyright">
      &copy; 2002 - ${Calendar.getInstance().get(Calendar.YEAR)},
    	  FakeCo Inc. All Rights Reserved.
    </div>
  </div>
 </body>
</html>
||< 
これを、&lt;div class="body"&gt;...&lt;/div&gt;の後に入れると、表示される位置が一番下ではなく、リスト表示テーブルの右横上になってしまいました。

著作権表記を、すべてのページのページ下部に書け、ということになると、非常に面倒です。

** カスタムGrailsタグの作成
>||
trip-planner$ grails create-tag-lib Date
    :(中略)
Created TagLib for Date
Created Tests for Date
trip-planner$ 

すると、grails-app/taglib/DateTagLib.groovyファイルが生成されます。

class DateTagLib {

}

ガラだけ生成されるので、DateTagLibの中身を記述します。

    def thisYear = {
        out << Calendar.getInstance().get(Calendar.YEAR)
    }

今回は、トップページを生成するviews/index.gspに著作権表示を追加します。

    <div id="copyright">
      &copy; 2002 - <g:thisYear />, FakeCo Inc. All Rights Reserved.
    </div>
  </body>
</html>

でました。でも、ページごとに埋めていくのは面倒ですね。

カスタムTagLibのテスト

書籍「レガシーコード改善ガイド」

記事中で触れられている書籍「Working Effectively with Legacy Code」の日本語訳が出版されました。

レガシーコード改善ガイド (Object Oriented SELECTION)

レガシーコード改善ガイド (Object Oriented SELECTION)

DateTagLibのテスト(記事の通りに)

grails create-tag-libコマンドで生成すると、test/unit/DateTagLibTests.groovyファイルにテストコードの雛形が生成されます。

import grails.test.*

class DateTagLibTests extends TagLibUnitTestCase {
    protected void setUp() {
        super.setUp()
    }

    protected void tearDown() {
        super.tearDown()
    }

    void testSomething() {

    }
}

DateTagLibTests.groovyファイルにtestThisYearテストコードを追加します。

    void testThisYear() {
        String expected = Calendar.getInstance().get(Calendar.YEAR)
        assertEquals("the years don't match", expected, dateTagLib.thisYear())
    }

テストを実行してみると、

trip-planner$ grails test-app
    :(中略)
Starting unit tests ...
Running tests of type 'unit'
  [groovyc] Compiling 1 source file to /home/torutk/.grails/1.1.1/projects/trip-planner/test-classes/unit
-------------------------------------------------------
Running 4 unit tests...
Running test AirlineControllerTests...PASSED
Running test DateTagLibTests...
                    testThisYear...FAILED
Running test AirlineTests...PASSED
Running test TripTests...PASSED
Tests Completed in 1128ms ...
-------------------------------------------------------
Tests passed: 3
Tests failed: 1
-------------------------------------------------------
    :(略)

おや、失敗してしまいました。テストの実行結果は、test/reportsディレクトリに出力されます。DateTagTestについては、以下のファイルが生成されています。

 test
  +-- reports
       +-- TEST-DateTagLibTests.xml
       +-- html
       |    +-- 2_DateTagLibTests-err.txt
       |    +-- 2_DateTagLibTests-error.html
       |    +-- 2_DateTagLibTests-out.txt
       |    +-- 2_DateTagLibTests.html
       +-- plain
            +-- TEST-DateTagLibTests-err.txt
            +-- TEST-DateTagLibTests-out.txt
            +-- TEST-DateTagLibTests.txt

ブラウザで、2_DateTagLibTests.htmlを開くと、テスト結果とともに、テストで発生したエラー内容が表示されます。

 No such property: dateTagLib for class: DateTagLibTests
 groovy.lang.MissingPropertyException: No such property: dateTagLib
 for class: DateTagLibTests
 at DateTagLibTests.testThisYear(DateTagLibTests.groovy:14)
 at _GrailsTest_groovy$_run_closure4.doCall(_GrailsTest_groovy:203)
 : (略)

DateTagLibTests.groovyの14行目は以下です。

    assertEquals("the years don't match", expected, dateTagLib.thisYear())

このdateTagLib.thisYear()がダメなようです。
後の記事Grails をマスターする: Grailsアプリケーションのテストでは、カスタムTagLibsのユニットテスト

  def output = 
     new DateTagLib().customDateFormat(format:null, body:"2008-10-01 00:00:00.0")

のように記述しているので、同じくnewして呼び出す記述に変えてみました。

void testThisYear1() {
    def expected = Calendar.getInstance().get(Calendar.YEAR)
    assertEquals "the years don't match", expected, new DateTagLib().thisYear()
}

しかし、このテスト結果は失敗となってしまいました。

the years don't match expected:<2009> but was:<2009>

junit.framework.AssertionFailedError: the years don't match expected:<2009> but was:<2009>

妙な結果です。JUnitは、期待値と結果が違うと言っていますが、ともに2009に見えます。

ここで、型を明示的に指定して呼び出すコードを書いてみます。

void testThisYear3() {
    String expected = Calendar.getInstance().get(Calendar.YEAR)
    String measured = new DateTagLib().thisYear()
    assertEquals "the years don't match", expected, measured
}

これならば、「testThisYear3 Success 」となって成功です。

試行錯誤のおり、"new DateTagLib().thisYear()"がStringではなくStringWriterだと怒られたので、ひょっとして、と思いString型の変数同士をassertEqualsに指定してみたらうまくいったという次第です。

なお、試行錯誤の際、printデバッグをしようとprintlnをあちこち埋め込んでも実行結果には何も表示されませんでした。これは、grails test-appを実行すると、ユニットテストコードの中で標準出力・標準エラー出力を行うと、コンソールではなくファイルに吐かれるようになっていたからです。
test/reports/の下に、XXXX-out.txtやXXXX-err.txtといったファイルが生成されていますが、これがXXXXテストクラスを実行した際の標準出力と標準エラー出力が格納されるファイルとなります。

部分テンプレート

複数のWebページで共有するGSPコード塊を指すのが「部分テンプレート」だそうです。

さて、記事中の部分テンプレートですが、grails-app/views/ディレクトリに保存するとありますが、ファイル名は何だろう?

とりあえず、viewsの下に、footer.gspという名前で作成します。

<div id="footer">
  <g:copyright startYear='2002'>FakeCo, Inc.</g:copyright>

  <div id="powered-by">
    <img src="${createLinkTo(dir:'images', file:'grails-powered.gif')}" />
  </div>
</div>

画像ファイル名は、現時点でGrailsのWebページからダウンロードできるアイコンに合わせて修正しています。

GrailsのWebページからPowered-byアイコン画像をダウンロードします。現時点では以下ページにあるようです。
http://docs.codehaus.org/display/GRAILS/Brand+2.0
今回は、grails-powered.gifを、プロジェクトディレクトリ直下のweb-app/imagesディレクトリ下にコピーします。記事では、grails-app/web-app/imagesとなっていますが、修正します。

さて、実行するとエラーとなりました。
やはり、ファイル名が違ったようです。
後の記事Grails をマスターする: Grails アプリケーションの見栄えを良くするに部分テンプレートの解説がありますが、ファイル名は、アンダースコアが必要なようです。footer.gspではなく、_footer.gspにしてみたらOKでした。

scaffoldのカスタマイズ

今回の記事の最後の課題が、scaffoldのカスタマイズです。

trip-planner$ grails install-templates
    :

src/templatesの下に、いくつかディレクトリとファイルが生成されました。記事では、grails-app/src/templatesとありますが、実際には、プロジェクト直下のsrc/templatesになります。

記事に沿って、src/templates/scaffold/list.gspを編集します。

    </div>
    <g:render template="/footer" />
  </body>
</html>

grailsサーバを再起動して、Airplaneのリストページを表示します。Airlineはscaffoldのままなので、scaffoldテンプレートに加えた修正が反映されていることが分かります。