Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その48 )( 気になった点を修正 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その47 )( 貸出承認画面の作成7 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 前に作成した画面と後から作成した画面で実装方法を変えた部分について、前の画面の実装を修正します。
- 他に修正した方が良い点を何点か見つけたので修正します。
- 貸出申請画面の lendingapp.html でデータを表示する部分のタグが td ではなく th になっているので修正する。
- Thymeleaf テンプレートの共通エラーメッセージ表示エリア、通常メッセージ表示エリアを外部ファイルにして共通部品にする。
参照したサイト・書籍
目次
- AdminLibraryController の権限チェックを hasAuthority → hasRole に変更する
- LendingappController クラスの setDispData メソッドを LendingappService クラスへ移動する
- LendingappController の index メソッドの bindingResult.hasErrors() の if 文内は常に lendingappParamForm.getLendingAppId() == null なので else の方のコードを削除する
- 貸出申請画面の lendingapp.html でデータを表示する部分のタグが td ではなく th になっているので修正する
- Thymeleaf テンプレートの共通エラーメッセージ表示エリア、通常メッセージ表示エリアを外部ファイルにして共通部品にする
- 次回は。。。
手順
AdminLibraryController の権限チェックを hasAuthority → hasRole に変更する
feature/58-issue ブランチを作成します。
src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryController.java を リンク先の内容 に変更します。
テストを実行します。AdminLibraryControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'AdminLibraryControllerTest' with Coverage」を選択します。
テストが成功することが確認できます。
commit、GitHub へ Push、feature/58-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/58-issue ブランチを削除、をします。
LendingappController クラスの setDispData メソッドを LendingappService クラスへ移動する
貸出承認画面で setDispData メソッドを Service クラス側に配置するようにしたので、貸出申請画面の方を修正します。今回は IntelliJ IDEA のリファクタリングを利用して修正してみます。
feature/55-issue ブランチを作成します。
src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappController.java を開き、setDispData メソッドのメソッド名にカーソルを移動してコンテキストメニューを表示した後、「Refactor」-「Move...」を選択します。
「Move Instance Method」ダイアログが表示されます。「Select an instance parameter」で移動先の LendingappService lendingappService を選択し、「Visibility」で Public を選択した後、「Refactor」ボタンをクリックします。
これで setDispData メソッドが LendingappService へ移動します。
■LendingappService
- LendingappService クラスに setDisp メソッドが移動しています。引数に
LendingappController lendingappController
が追加されていました。
■LendingappController
- LendingappController クラス内の setDisp メソッドを呼び出しているコードが
lendingappService.setDispData
に変更されています。
- LendingappService クラスに setDisp メソッドが移動しています。引数に
LendingappService へ移動した setDispData メソッドの引数から追加された LendingappController lendingappController を削除します。src/test/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappService.java を開き、setDisp メソッドの引数にカーソルを移動してコンテキストメニューをを表示した後、「Refactor」-「Change Signature...」を選択します。
「Change Signature」ダイアログが表示されます。画面中央の「Parameters」のリストで LendingappController lendingappController を削除した後、「OK」ボタンをクリックします。
setDisp メソッドが以下のように変更されます。
■LendingappService
■LendingappController
最後に ksbysample/webapp/lending/web/lendingapp の下の LendingappService.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/lendingapp の LendingappController.java は リンク先のその1の内容 になっています。
テストを実行します。LendingappControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'LendingappControllerTest' with Coverage」を選択します。
テストが成功することが確認できます。
commit、GitHub へ Push、feature/55-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/55-issue ブランチを削除、をします。
LendingappController の index メソッドの bindingResult.hasErrors() の if 文内は常に lendingappParamForm.getLendingAppId() == null なので else の方のコードを削除する
feature/56-issue ブランチを作成します。
src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappController.java を リンク先のその2の内容 に変更します。
テストを実行します。LendingappControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'LendingappControllerTest' with Coverage」を選択します。
テストが成功することが確認できます。
commit、GitHub へ Push、feature/56-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/56-issue ブランチを削除、をします。
貸出申請画面の lendingapp.html でデータを表示する部分のタグが td ではなく th になっているので修正する
feature/54-issue ブランチを作成します。
src/main/resources/templates/lendingapp の下の lendingapp.html を リンク先の内容 に変更します。
動作を確認します。ksbysample.common.test.rule.db.TestDataLoader クラスを利用して ksbysample/webapp/lending/web/lendingapp/testdata/001 のデータを load します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
貸出申請画面が表示されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
commit、GitHub へ Push、feature/54-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/54-issue ブランチを削除、をします。
Thymeleaf テンプレートの共通エラーメッセージ表示エリア、通常メッセージ表示エリアを外部ファイルにして共通部品にする
feature/57-issue ブランチを作成します。
src/main/resources/templates/common の下の mainparts.html を リンク先の内容 に変更します。
src/main/resources/templates/lendingapp の下の lendingapp.html を リンク先のその2の内容 に変更します。
src/main/resources/templates/lendingapproval の下の lendingapproval.html を リンク先の内容 に変更します。
動作を確認します。ksbysample.common.test.rule.db.TestDataLoader クラスを利用して ksbysample/webapp/lending/web/lendingapp/testdata/001 のデータを load します。以下のようなテストメソッドを作成して実行します。ただしバックアップ&リストアは不要なので TestDataResource をテストクラス内には記述しません。
@Test
public void loadTestData() {
testDataLoader.load("src/test/resources/ksbysample/webapp/lending/web/lendingapp/testdata/001");
}メールを受信するので smtp4dev が起動していない場合には起動します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
貸出申請画面が表示されます。
何も入力せずに「申請」ボタンをクリックします。画面上部にエラーメッセージが表示されることが確認できます。
何も入力せずに「一時保存」ボタンをクリックします。画面上部に通常メッセージが表示されることが確認できます。
ksbysample.common.test.rule.db.TestDataLoader クラスを利用して src/test/resources/ksbysample/webapp/lending/web/lendingapproval/testdata/001 のデータを load します。
http://localhost:8080/lendingapproval?lendingAppId=105 へアクセスします。
貸出承認画面が表示されます。
何も入力せずに「確定」ボタンをクリックします。画面上部にエラーメッセージが表示されることが確認できます。
全ての書籍で「承認」を選択した後「確定」ボタンをクリックします。画面上部に通常メッセージが表示されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
動作確認をしていてテストで確認できるようになっていた方がよいように思えたので、テストクラスを変更します。
src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappControllerTest.java を リンク先の内容 に変更します。
テストを実行します。LendingappControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'LendingappControllerTest' with Coverage」を選択します。
テストが成功することが確認できます。
src/main/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalControllerTest.java を リンク先の内容 に変更します。
テストを実行します。LendingapprovalControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'LendingapprovalControllerTest' with Coverage」を選択します。
テストが成功することが確認できます。
commit、GitHub へ Push、feature/57-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/57-issue ブランチを削除、をします。
次回は。。。
貸出申請結果確認画面の作成に進みます。
ソースコード
AdminLibraryController.java
package ksbysample.webapp.lending.web.admin.library; import ksbysample.webapp.lending.config.Constant; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @PreAuthorize("hasRole('ROLE_ADMIN')") @RequestMapping("/admin/library") public class AdminLibraryController { @Autowired private AdminLibraryService adminLibraryService; @RequestMapping public String index() { return "admin/library/library"; } @RequestMapping("/addSearchLibrary") public String addSearchLibrary(SetSelectedLibraryForm setSelectedLibraryForm) { adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm); return "redirect:" + Constant.URL_ADMIN_LIBRARY; } }
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
→@PreAuthorize("hasRole('ROLE_ADMIN')")
へ変更します。
LendingappService.java
package ksbysample.webapp.lending.web.lendingapp; import ksbysample.webapp.lending.dao.LendingAppDao; import ksbysample.webapp.lending.dao.LendingBookDao; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.exception.WebApplicationRuntimeException; import ksbysample.webapp.lending.helper.mail.EmailHelper; import ksbysample.webapp.lending.helper.mail.Mail002Helper; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import ksbysample.webapp.lending.helper.user.UserHelper; import org.apache.commons.lang3.StringUtils; import org.seasar.doma.jdbc.SelectOptions; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.List; import static ksbysample.webapp.lending.values.LendingAppStatusValues.PENDING; import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY; @Service public class LendingappService { @Autowired private LendingAppDao lendingAppDao; @Autowired private LendingBookDao lendingBookDao; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Autowired private EmailHelper emailHelper; @Autowired private Mail002Helper mail002Helper; @Autowired private UserHelper userHelper; public void setDispData(Long lendingAppId, LendingappForm lendingappForm) { LendingApp lendingApp = lendingAppDao.selectById(lendingAppId); if (lendingApp == null) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingApp.nodataerr", null)); } List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingAppId); lendingappForm.setLendingApp(lendingApp); lendingappForm.setLendingBookList(lendingBookList); } public void apply(LendingappForm lendingappForm) throws MessagingException { // 更新対象のデータを取得する(ロックする) LendingApp lendingApp = lendingAppDao.selectById(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); // lending_app.status を 3(申請中) にする lendingApp.setStatus(PENDING.getValue()); lendingAppDao.update(lendingApp); // lending_book.lending_app_flg を 1(する) に、lending_app_reason に画面に入力された申請理由をセットする lendingappForm.getLendingBookDtoList().stream() .filter(lendingBookDto -> StringUtils.equals(lendingBookDto.getLendingAppFlg(), APPLY.getValue())) .forEach(lendingBookDto -> { LendingBook lendingBook = new LendingBook(); BeanUtils.copyProperties(lendingBookDto, lendingBook); lendingBookDao.updateLendingAppFlgAndReason(lendingBook); }); // 承認者にメールを送信する String[] approverMailAddrList = userHelper.getApprovalMailAddrList(); MimeMessage mimeMessage = mail002Helper.createMessage(approverMailAddrList, lendingApp.getLendingAppId()); emailHelper.sendMail(mimeMessage); } public void temporarySave(LendingappForm lendingappForm) { // 更新対象のデータを取得する(ロックする) LendingApp lendingApp = lendingAppDao.selectById(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingappForm.getLendingApp().getLendingAppId() , SelectOptions.get().forUpdate()); // lending_book.lending_app_flg, lending_app_reason に画面に入力された内容をセットする lendingappForm.getLendingBookDtoList().stream() .forEach(lendingBookDto -> { LendingBook lendingBook = new LendingBook(); BeanUtils.copyProperties(lendingBookDto, lendingBook); lendingBookDao.updateLendingAppFlgAndReason(lendingBook); }); } }
- setDispData メソッドを apply メソッドの上に移動します。
- getLendingApp, getLendingBookList メソッド内の実装を setDispData メソッドへ移動した後、getLendingApp, getLendingBookList メソッドを削除します。
LendingappController.java
■その1
package ksbysample.webapp.lending.web.lendingapp; import ksbysample.webapp.lending.cookie.CookieLastLendingAppId; import ksbysample.webapp.lending.exception.WebApplicationRuntimeException; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import ksbysample.webapp.lending.helper.thymeleaf.SuccessMessagesHelper; import ksbysample.webapp.lending.util.cookie.CookieUtils; import org.apache.commons.lang.StringUtils; import org.seasar.doma.jdbc.OptimisticLockException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; import static ksbysample.webapp.lending.values.LendingAppStatusValues.UNAPPLIED; @Controller @RequestMapping("/lendingapp") public class LendingappController { @Autowired private LendingappService lendingappService; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Autowired private LendingappFormValidator lendingappFormValidator; @InitBinder(value = "lendingappForm") public void initBinder(WebDataBinder binder) { binder.addValidators(lendingappFormValidator); } @RequestMapping public String index(@Validated LendingappParamForm lendingappParamForm , BindingResult bindingResult , LendingappForm lendingappForm , HttpServletResponse response) { if (bindingResult.hasErrors()) { if (lendingappParamForm.getLendingAppId() == null) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingAppId.emptyerr", null)); } else { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingApp.nodataerr", null)); } } // 画面に表示するデータを取得する lendingappService.setDispData(lendingappParamForm.getLendingAppId(), lendingappForm); // 未申請の場合には LastLendingAppId Cookie に貸出申請ID をセットする if (StringUtils.equals(lendingappForm.getLendingApp().getStatus(), UNAPPLIED.getValue())) { CookieUtils.addCookie(CookieLastLendingAppId.class , response, String.valueOf(lendingappParamForm.getLendingAppId())); } return "lendingapp/lendingapp"; } @RequestMapping(value = "/apply", method = RequestMethod.POST) public String apply(@Validated LendingappForm lendingappForm , BindingResult bindingResult , HttpServletResponse response) throws MessagingException { if (bindingResult.hasErrors()) { return "lendingapp/lendingapp"; } try { // 入力された内容で申請する lendingappService.apply(lendingappForm); // 画面に表示するデータを取得する lendingappService.setDispData(lendingappForm.getLendingApp().getLendingAppId(), lendingappForm); // LastLendingAppId Cookie を削除する CookieUtils.removeCookie(CookieLastLendingAppId.class, response); } catch (OptimisticLockException e) { bindingResult.reject("Global.optimisticLockException"); } return "lendingapp/lendingapp"; } @RequestMapping(value = "/temporarySave", method = RequestMethod.POST) public String temporarySave(@Validated LendingappForm lendingappForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { return "lendingapp/lendingapp"; } try { // 入力された内容を一時保存する lendingappService.temporarySave(lendingappForm); // 画面に表示する通常メッセージをセットする SuccessMessagesHelper successMessagesHelper = new SuccessMessagesHelper("一時保存しました"); successMessagesHelper.setToModel(model); } catch (OptimisticLockException e) { bindingResult.reject("Global.optimisticLockException"); } return "lendingapp/lendingapp"; } }
■その2
@RequestMapping public String index(@Validated LendingapprovalParamForm lendingapprovalParamForm , BindingResult bindingResult , LendingapprovalForm lendingapprovalForm , BindingResult bindingResultOfLendingapprovalForm) { if (bindingResult.hasErrors()) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingapprovalParamForm.lendingAppId.emptyerr", null)); } // 画面に表示するデータを取得する lendingapprovalService.setDispData(lendingapprovalParamForm.getLendingAppId(), lendingapprovalForm);
- index メソッドの
if (bindingResult.hasErrors()) { ... }
内のif (lendingappParamForm.getLendingAppId() == null) { ... } else { ... }
を削除しthrow new WebApplicationRuntimeException(messagesPropertiesHelper.getMessage("LendingapprovalParamForm.lendingAppId.emptyerr", null));
だけになるように変更します。
lendingapp.html
■その1
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出申請</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <link th:replace="common/head-cssjs"/> <style> .content-wrapper { background-color: #fffafa; } .selected-library { color: #ffffff !important; font-size: 100%; font-weight: 700; } .box-body.no-padding { padding-bottom: 10px !important; } .table>tbody>tr>td , .table>tbody>tr>th , .table>tfoot>tr>td , .table>tfoot>tr>th , .table>thead>tr>td , .table>thead>tr>th { padding: 5px; font-size: 90%; } .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .has-error { background-color: #ffcccc; } </style> </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Main Header --> <div th:replace="common/mainparts :: main-header"></div> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Content Header (Page header) --> <section class="content-header"> <h1>貸出申請</h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <form id="lendingappForm" method="post" action="/lendingapp/apply" th:action="@{/lendingapp/apply}" th:object="${lendingappForm}"> <div class="alert alert-danger" th:if="${#fields.hasGlobalErrors()}"> <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">共通エラーメッセージ表示エリア</p> </div> <div class="alert alert-success" th:if="${successMessages}"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <p th:each="msg : ${successMessages}" th:text="${msg}">通常メッセージ表示エリア</p> </div> <div class="box"> <div class="box-body no-padding"> <div class="col-xs-6 no-padding"> <table class="table table-bordered"> <colgroup> <col width="30%"/> <col width="70%"/> </colgroup> <tr> <th class="bg-purple">貸出申請ID</th> <th th:text="*{lendingApp.lendingAppId}">1</th> </tr> <tr> <th class="bg-purple">ステータス</th> <th th:text="${@vh.getText('LendingAppStatusValues', lendingappForm.lendingApp.status)}">申請中</th> </tr> </table> <input type="hidden" th:field="*{lendingApp.lendingAppId}"/> <input type="hidden" th:field="*{lendingApp.status}"/> <input type="hidden" th:field="*{lendingApp.version}"/> </div> <br/> <table class="table table-hover"> <colgroup> <col width="5%"/> <col width="20%"/> <col width="20%"/> <col width="15%"/> <col width="15%"/> <col width="25%"/> </colgroup> <thead class="bg-purple"> <tr> <th>No.</th> <th>ISBN</th> <th>書名</th> <th>貸出状況</th> <th>申請</th> <th>申請理由</th> </tr> </thead> <tbody class="jp-gothic"> <tr th:each="lendingBookDto, iterStat : *{lendingBookDtoList}"> <td th:text="${iterStat.count}">1</td> <td th:text="${lendingBookDto.isbn}">978-1-4302-5908-4</td> <td th:text="${lendingBookDto.bookName}">Spring Recipes</td> <td th:text="${lendingBookDto.lendingState}">蔵書なし</td> <td> <select class="form-control input-sm" th:classappend="${#fields.hasErrors('*{lendingBookDtoList[__${iterStat.index}__].lendingAppFlg}')} ? 'has-error' : ''" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingAppFlg}" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} and ${lendingBookDto.lendingState} == '蔵書あり'"> <option th:each="lendingAppFlg : ${@vh.values('LendingBookLendingAppFlgValues')}" th:value="${lendingAppFlg.getValue()}" th:text="${lendingAppFlg.getText()}">しない</option> </select> <span th:text="${@vh.getText('LendingBookLendingAppFlgValues', lendingBookDto.lendingAppFlg)}" th:if="*{lendingApp.status} != ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}">しない </span> </td> <td> <input type="text" class="form-control input-sm" th:classappend="${#fields.hasErrors('*{lendingBookDtoList[__${iterStat.index}__].lendingAppReason}')} ? 'has-error' : ''" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingAppReason}" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')} and ${lendingBookDto.lendingState} == '蔵書あり'"/> <span th:text="${lendingBookDto.lendingAppReason}" th:if="*{lendingApp.status} != ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}">入力された申請理由を表示する </span> </td> <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingBookId}"/> <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].isbn}"/> <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].bookName}"/> <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].lendingState}"/> <input type="hidden" th:field="*{lendingBookDtoList[__${iterStat.index}__].version}"/> </tr> </tbody> </table> <div class="text-center" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'UNAPPLIED')}"> <button class="btn bg-blue js-btn-apply"><i class="fa fa-thumbs-up"></i> 申請</button> <button class="btn bg-orange js-btn-temporarySave"><i class="fa fa-tag"></i> 一時保存(まだ申請しない)</button> <input type="hidden" th:field="*{btn}"/> </div> </div> </div> </form> </div> </div> </section> <!-- /.content --> </div> <!-- /.container --> </div> </div> <!-- ./wrapper --> <script th:replace="common/bottom-js"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $(".js-btn-apply").click(function(){ $("#btn").val("apply"); $("#lendingappForm").submit(); return false; }); $(".js-btn-temporarySave").click(function(){ $("#btn").val("temporarySave"); $("#lendingappForm").attr("action", "/lendingapp/temporarySave"); $("#lendingappForm").submit(); return false; }); }); --> </script> </body> </html>
<tbody class="jp-gothic"> ... </tbody>
の中の th タグを td タグに変更します。IntelliJ IDEA Ulitimate Edition だと<th>
→<td>
に変更すると自動的に対応する終了タグも</th>
→</td>
に変更してくれて、何気に便利でした。
■その2
<!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <form id="lendingappForm" method="post" action="/lendingapp/apply" th:action="@{/lendingapp/apply}" th:object="${lendingappForm}"> <div th:replace="common/mainparts :: alert-danger"></div> <div th:replace="common/mainparts :: alert-success"></div> <div class="box">
<form id="lendingappForm" method="post" action="/lendingapp/apply" th:action="@{/lendingapp/apply}" th:object="${lendingappForm}">
の下の共通エラーメッセージ表示エリア、通常メッセージ表示エリアを<div th:replace="common/mainparts :: alert-danger"></div>
、<div th:replace="common/mainparts :: alert-success"></div>
に書き換えます。
mainparts.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>検索対象図書館登録</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style> .noselected-library { color: #ff8679 !important; font-size: 100%; font-weight: 700; } .selected-library { color: #ffffff !important; font-size: 100%; font-weight: 700; } .content-wrapper { background-color: #fffafa; } </style> </head> <!-- ADD THE CLASS layout-top-nav TO REMOVE THE SIDEBAR. --> <body class="skin-blue layout-top-nav"> <div class="wrapper"> <!-- Main Header --> <header class="main-header" th:fragment="main-header"> <nav class="navbar navbar-static-top"> <div class="container"> <div class="navbar-header"> <a href="#" class="navbar-brand"><b>ksbysample-lending</b></a> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse"> <i class="fa fa-bars"></i> </button> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse pull-left" id="navbar-collapse"> <ul class="nav navbar-nav"> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">メニュー <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="/booklist">貸出希望書籍登録</a></li> <li><a href="/lendingapp">貸出申請</a></li> <li><a href="/confirmresult">貸出申請結果確認</a></li> <li class="divider"></li> <li><a href="/lendingapproval">貸出承認</a></li> <li class="divider"></li> <li><a href="/admin/library">検索対象図書館登録</a></li> </ul> </li> </ul> </div> <!-- /.navbar-collapse --> <!-- Navbar Right Menu --> <div class="navbar-custom-menu"> <p class="navbar-text" th:classappend="${#strings.startsWith(@libraryHelper.getSelectedLibrary(), '※')} ? 'noselected-library' : 'selected-library'" th:text="${@libraryHelper.getSelectedLibrary()}">※図書館が選択されていません</p> <ul class="nav navbar-nav"> <li><a href="/logout">ログアウト</a></li> </ul> </div> <!-- /.navbar-custom-menu --> </div> <!-- /.container-fluid --> </nav> </header> <!-- Full Width Column --> <div class="content-wrapper"> <div class="container"> <!-- Content Header (Page header) --> <section class="content-header"> <h1>画面名</h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <form id="lendingapprovalForm" method="post" action="/lendingapproval/complete" th:action="@{/lendingapproval/complete}" th:object="${lendingapprovalForm}"> <div class="alert alert-danger" th:fragment="alert-danger" th:if="${#fields.hasGlobalErrors()}"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">共通エラーメッセージ表示エリア</p> </div> <div class="alert alert-success" th:fragment="alert-success" th:if="${successMessages}"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <p th:each="msg : ${successMessages}" th:text="${msg}">通常メッセージ表示エリア</p> </div> </form> </div> </div> </section> <!-- /.content --> </div> <!-- /.container --> </div> </div> <!-- ./wrapper --> <!-- jQuery 2.1.4 --> <script src="/js/jQuery-2.1.4.min.js" type="text/javascript"></script> <!-- Bootstrap 3.3.2 JS --> <script src="/js/bootstrap.min.js" type="text/javascript"></script> <!-- AdminLTE App --> <script src="/js/app.min.js" type="text/javascript"></script> </body> </html>
<section class="content"> ... </section>
の中に上記の実装を追加します。この中に以下の2つの th:fragment を記述しています。- 共通エラーメッセージ表示エリアの
<div class="alert alert-danger" th:fragment="alert-danger" ...> ... </div>
。呼び出し元では<div th:replace="common/mainparts :: alert-danger"></div>
と記述します。 - 通常メッセージ表示エリアの
<div class="alert alert-success" th:fragment="alert-success" ...> ... </div>
。呼び出し元では<div th:replace="common/mainparts :: alert-success"></div>
と記述します。
- 共通エラーメッセージ表示エリアの
lendingapproval.html
<!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <form id="lendingapprovalForm" method="post" action="/lendingapproval/complete" th:action="@{/lendingapproval/complete}" th:object="${lendingapprovalForm}"> <div th:replace="common/mainparts :: alert-danger"></div> <div th:replace="common/mainparts :: alert-success"></div> <div class="box" th:if="*{lendingApp != null}">
<form id="lendingapprovalForm" method="post" action="/lendingapproval/complete" th:action="@{/lendingapproval/complete}" th:object="${lendingapprovalForm}">
の下の共通エラーメッセージ表示エリア、通常メッセージ表示エリアを<div th:replace="common/mainparts :: alert-danger"></div>
、<div th:replace="common/mainparts :: alert-success"></div>
に書き換えます。
LendingappControllerTest.java
package ksbysample.webapp.lending.web.lendingapp; import com.google.common.base.Charsets; import ksbysample.common.test.rule.db.TableDataAssert; import ksbysample.common.test.helper.TestHelper; import ksbysample.common.test.rule.mail.MailServerResource; import ksbysample.common.test.rule.mockmvc.SecurityMockMvcResource; import ksbysample.common.test.rule.db.TestData; import ksbysample.common.test.rule.db.TestDataResource; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MvcResult; import org.yaml.snakeyaml.Yaml; import javax.mail.internet.MimeMessage; import javax.sql.DataSource; import java.io.File; import java.util.List; import static ksbysample.common.test.matcher.ErrorsResultMatchers.errors; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(Enclosed.class) public class LendingappControllerTest { .......... @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 貸出申請画面の入力チェックエラーのテスト { // テストデータ private LendingappForm lendingappForm_002 = (LendingappForm) new Yaml().load(getClass().getResourceAsStream("LendingappForm_002.yaml")); private LendingappForm lendingappForm_003 = (LendingappForm) new Yaml().load(getClass().getResourceAsStream("LendingappForm_003.yaml")); private LendingappForm lendingappForm_005 = (LendingappForm) new Yaml().load(getClass().getResourceAsStream("LendingappForm_005.yaml")); @Rule @Autowired public SecurityMockMvcResource mvc; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Test public void 申請するが1つも選択されていない場合は入力チェックエラー() throws Exception { mvc.authTanakaTaro.perform(TestHelper.postForm("/lendingapp/apply", this.lendingappForm_002).with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("lendingapp/lendingapp")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(4)) .andExpect(errors().hasGlobalError("lendingappForm", "LendingappForm.lendingBookDtoList.notExistApply")) .andExpect(errors().hasFieldError("lendingappForm", "lendingBookDtoList[0].lendingAppFlg", "")) .andExpect(errors().hasFieldError("lendingappForm", "lendingBookDtoList[1].lendingAppFlg", "")) .andExpect(errors().hasFieldError("lendingappForm", "lendingBookDtoList[2].lendingAppFlg", "")) .andExpect(xpath("//*[@class=\"alert alert-danger\"]/p") .string(messagesPropertiesHelper.getMessage("LendingappForm.lendingBookDtoList.notExistApply", null))); } .......... } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 貸出申請画面の正常処理時のテスト { .......... @Test @TestData("src/test/resources/ksbysample/webapp/lending/web/lendingapp/testdata/001") public void 一時保存ボタンをクリックした場合() throws Exception { mvc.authTanakaTaro.perform(TestHelper.postForm("/lendingapp/temporarySave", this.lendingappForm_007).with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("lendingapp/lendingapp")) .andExpect(model().hasNoErrors()) .andExpect(xpath("//*[@class=\"alert alert-success\"]/p").string("一時保存しました")); // then ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました ) // DB IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/lendingapp/assertdata/002")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id", "approval_user_id"}); tableDataAssert.assertEquals("lending_book", new String[]{"lending_book_id", "lending_app_id", "lending_state", "lending_app_flg", "lending_app_reason", "approval_result", "approval_reason"}); } } }
- 「貸出申請画面の入力チェックエラーのテスト」テストクラスの以下の点を変更します。
@Autowired private MessagesPropertiesHelper messagesPropertiesHelper;
を追加します。- 「申請するが1つも選択されていない場合は入力チェックエラー()」テストメソッドの内の mvc を呼び出す時の処理に
.andExpect(xpath("//*[@class=\"alert alert-danger\"]/p").string(messagesPropertiesHelper.getMessage("LendingappForm.lendingBookDtoList.notExistApply", null)))
を追加します。
- 「貸出申請画面の正常処理時のテスト」テストクラスの以下の点を変更します。
LendingapprovalControllerTest.java
package ksbysample.webapp.lending.web.lendingapproval; import com.google.common.base.Charsets; import ksbysample.common.test.rule.db.AssertOptions; import ksbysample.common.test.rule.db.TableDataAssert; import ksbysample.common.test.helper.TestHelper; import ksbysample.common.test.rule.mail.MailServerResource; import ksbysample.common.test.rule.mockmvc.SecurityMockMvcResource; import ksbysample.common.test.rule.db.TestData; import ksbysample.common.test.rule.db.TestDataResource; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MvcResult; import org.yaml.snakeyaml.Yaml; import javax.mail.internet.MimeMessage; import javax.sql.DataSource; import java.io.File; import static ksbysample.common.test.matcher.ErrorsResultMatchers.errors; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(Enclosed.class) public class LendingapprovalControllerTest { .......... @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 貸出承認画面の入力チェックエラーのテスト { // テストデータ private LendingapprovalForm lendingapprovalForm_003 = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_003.yaml")); private LendingapprovalForm lendingapprovalForm_006 = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_006.yaml")); @Rule @Autowired public SecurityMockMvcResource mvc; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; // FormValidator の入力チェックを呼び出せているかチェックできればよいので、1パターンだけテストする @Test public void 一部の書籍は承認却下未選択で一部の書籍は却下理由未入力の場合は入力チェックエラー() throws Exception { mvc.authTanakaTaro.perform(TestHelper.postForm("/lendingapproval/complete", this.lendingapprovalForm_003).with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("lendingapproval/lendingapproval")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(2)) .andExpect(errors().hasGlobalError("lendingapprovalForm", "LendingapprovalForm.applyingBookFormList.approvalResult.notAllCheckedErr")) .andExpect(errors().hasFieldError("lendingapprovalForm", "applyingBookFormList[2].approvalReason", "")) .andExpect(xpath("//*[@class=\"alert alert-danger\"]/p") .string(messagesPropertiesHelper.getMessage("LendingapprovalForm.applyingBookFormList.approvalResult.notAllCheckedErr", null))); } @Test public void LendingapprovalForm_BeanValidationのテスト() throws Exception { mvc.authTanakaTaro.perform(TestHelper.postForm("/lendingapproval/complete", this.lendingapprovalForm_006).with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("lendingapproval/lendingapproval")) .andExpect(model().hasErrors()) .andExpect(model().errorCount(2)) .andExpect(errors().hasFieldError("lendingapprovalForm", "applyingBookFormList[0].approvalResult", "ValuesEnum")) .andExpect(errors().hasFieldError("lendingapprovalForm", "applyingBookFormList[1].approvalReason", "Size")); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 貸出承認画面の正常処理時のテスト { // テストデータ private LendingapprovalForm lendingapprovalForm_004 = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_004.yaml")); private LendingapprovalForm lendingapprovalForm_005 = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_005.yaml")); @Rule @Autowired public TestDataResource testDataResource; @Autowired private DataSource dataSource; @Rule @Autowired public MailServerResource mailServerResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test @TestData("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/testdata/001") public void 確定ボタンをクリックした場合_承認() throws Exception { // when ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました ) mvc.authTanakaTaro.perform(TestHelper.postForm("/lendingapproval/complete", this.lendingapprovalForm_004).with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("lendingapproval/lendingapproval")) .andExpect(model().hasNoErrors()) .andExpect(xpath("//*[@class=\"alert alert-success\"]/p").string("確定しました")) .andExpect(xpath("//*[@id=\"lendingapprovalForm\"]/div/div/table/tbody/tr[1]/td[6]/input[@type=\"text\"]").doesNotExist()); // then ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました ) // DB IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/001")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id", "approval_user_id"}); tableDataAssert.assertEquals("lending_book", new String[]{"lending_app_id", "isbn,book_name", "lending_state", "lending_app_flg", "lending_app_reason", "approval_reason"}); // メール assertThat(mailServerResource.getMessagesCount()).isEqualTo(1); MimeMessage mimeMessage = mailServerResource.getFirstMessage(); assertThat(mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO)) .extracting(Object::toString) .containsOnly("tanaka.taro@sample.com"); assertThat(mimeMessage.getContent()) .isEqualTo(com.google.common.io.Files.toString( new File("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/001/message.txt") , Charsets.UTF_8)); } .......... } }
- 「貸出承認画面の入力チェックエラーのテスト」テストクラスの以下の点を変更します。
@Autowired private MessagesPropertiesHelper messagesPropertiesHelper;
を追加します。- 「一部の書籍は承認却下未選択で一部の書籍は却下理由未入力の場合は入力チェックエラー()」テストメソッドの内の mvc を呼び出す時の処理に
.andExpect(xpath("//*[@class=\"alert alert-danger\"]/p").string(messagesPropertiesHelper.getMessage("LendingapprovalForm.applyingBookFormList.approvalResult.notAllCheckedErr", null)))
を追加します。
- 「貸出承認画面の正常処理時のテスト」テストクラスの以下の点を変更します。
履歴
2016/01/24
初版発行。