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"> © 2002 - ${Calendar.getInstance().get(Calendar.YEAR)}, FakeCo Inc. All Rights Reserved. </div> </div> </body> </html> ||< これを、<div class="body">...</div>の後に入れると、表示される位置が一番下ではなく、リスト表示テーブルの右横上になってしまいました。 著作権表記を、すべてのページのページ下部に書け、ということになると、非常に面倒です。 ** カスタム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"> © 2002 - <g:thisYear />, FakeCo Inc. All Rights Reserved. </div> </body> </html>
でました。でも、ページごとに埋めていくのは面倒ですね。
カスタムTagLibのテスト
書籍「レガシーコード改善ガイド」
記事中で触れられている書籍「Working Effectively with Legacy Code」の日本語訳が出版されました。

レガシーコード改善ガイド (Object Oriented SELECTION)
- 作者: マイケル・C・フェザーズ,ウルシステムズ株式会社,平澤章,越智典子,稲葉信之,田村友彦,小堀真義
- 出版社/メーカー: 翔泳社
- 発売日: 2009/07/14
- メディア: 大型本
- 購入: 45人 クリック: 673回
- この商品を含むブログ (157件) を見る
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テンプレートに加えた修正が反映されていることが分かります。