Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その34 )( 貸出申請画面の作成5 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その33 )( 貸出申請画面の作成4 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 貸出申請画面の作成
- 「一時保存」ボタンクリック時の処理の実装
- 貸出申請画面の作成
参照したサイト・書籍
Thymeleaf multiple submit button in one form
http://stackoverflow.com/questions/31401669/thymeleaf-multiple-submit-button-in-one-form- 複数のボタンを画面上に表示している時に、どのボタンが押されたのかを判別する方法を調査した際に参照しました。
この記事を見て初めて知ったのですが、Thymeleaf では
th:object="${lendingappForm}"
と記載する以外にdata-th-object="${lendingappForm}"
と記載する方法があるようです。確かに IntelliJ IDEA の補完一覧に出てきますね。出力される HTML を比較してみました。まずは
th:object="${lendingappForm}"
の場合です。次に
data-th-object="${lendingappForm}"
の場合です。出力される HTML には違いはないようです。おそらくどちらでも好きな方を使えばよいということでしょう。自分は th: の方で進めたいと思います。
目次
- Bean Validation のみ実行し Validator クラスの validate メソッドは実行しない方法があるか?
- LendingappForm クラスの変更
- lendingapp.html の変更
- LendingappFormValidator クラスの変更
- LendingappService クラスの変更
- 通常メッセージ表示用ユーティリティクラス SuccessMessagesHelper の作成
- LendingappController クラスの変更
- 動作確認
- 次回は。。。
手順
Bean Validation のみ実行し Validator クラスの validate メソッドは実行しない方法があるか?
一時保存するのに Validator クラスで実施している以下の入力チェックは不要なので、
- 最低1つ「申請する」が選択されているか。
- 「申請する」が選択されている場合に申請理由が入力されているか。
Bean Validation のみ実行し Validator クラスの validate メソッドは実行しないようにします。
特定の Validator クラスの validate メソッドを実行しないようにする方法がないか探してみたのですが、見つかりませんでした。押されたボタンの種類を <input type="hidden" ...
にセットして Form オブジェクトに渡せば Validator クラス内で「申請」「一時保存」のどちらのボタンが押されたのか判別できそうなので、その方法で進めます。
LendingappForm クラスの変更
lendingapp.html の変更
- src/main/resources/templates/lendingapp の下の lendingapp.html を リンク先の内容 に変更します。
LendingappFormValidator クラスの変更
- src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappFormValidator.java を リンク先の内容 に変更します。
LendingappService クラスの変更
通常メッセージ表示用ユーティリティクラス SuccessMessagesHelper の作成
src/main/java/ksbysample/webapp/lending/helper の下に thymeleaf パッケージを作成します。
src/main/java/ksbysample/webapp/lending/helper/thymeleaf の下に SuccessMessagesHelper.java を作成します。作成後、リンク先の内容 に変更します。
LendingappController クラスの変更
- src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappController.java を リンク先の内容 に変更します。
動作確認
動作確認します。データは Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その31 )( 貸出申請画面の作成2 ) で作成した貸出申請ID = 105 のデータを使用します。lending_app.status の値が 3 になっている場合には 2 に変更し、lending_book.lending_app_flg, lending_app_reason をクリアしておきます。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。貸出申請画面が表示されます。
「申請する/しない」欄は全て「申請しない」にしたまま、何件かの申請理由を記入した後「一時保存」ボタンをクリックします。
画面上部にエラーメッセージではなく「一時保存しました」のメッセージが表示され、画面上の入力されたデータが保存されます。
一度ブラウザを終了して起動し直した後、再度 http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。
貸出申請画面が一時保存した内容で表示されることが確認できます。
今度は各書籍毎に入力パターンを変えてから「一時保存」ボタンをクリックします。
もう一度ブラウザを終了して起動し直した後、再度 http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。
貸出申請画面が一時保存した内容で表示されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
一旦 commit します。
次回は。。。
他で書いていた以下の内容を ksbysample-webapp-lending にまだ反映していなかったので反映します。他にも修正したい点があれば修正します。
- Spring Boot で Doma 2 を使用するには で記載した JPA の AutoConfiguration を無効する設定を反映します。
- Qiita に書いた Spring Boot + Thymeleaf + Enum(列挙型) でドロップダウンリストを表示する の内容を反映します。
次に承認者にメールを送信する機能を実装します。
次に他のブラウザから既にデータが更新されていた場合に更新させないようにする処理を実装する予定です。
その後テストを書く予定です。
あと「一時保存」はセッションに保存する予定だったのですが、何も考えずに DB に保存してしまいました。他に実装するところを考えないと。。。
ソースコード
LendingappForm.java
package ksbysample.webapp.lending.web.lendingapp; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import lombok.Data; import javax.validation.Valid; import java.util.ArrayList; import java.util.List; @Data public class LendingappForm { private LendingApp lendingApp; private String btn; @Valid private List<LendingBookDto> lendingBookDtoList; public void setLendingBookList(List<LendingBook> lendingBookList) { this.lendingBookDtoList = new ArrayList<>(); lendingBookList.stream() .forEach(lendingBook -> { LendingBookDto lendingBookDto = new LendingBookDto(lendingBook); this.lendingBookDtoList.add(lendingBookDto); }); } }
private String btn;
を追加します。
lendingapp.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"/> <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}"/> </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}"> <th th:text="${iterStat.count}">1</th> <th th:text="${lendingBookDto.isbn}">978-1-4302-5908-4</th> <th th:text="${lendingBookDto.bookName}">Spring Recipes</th> <th th:text="${lendingBookDto.lendingState}">蔵書なし</th> <th> <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> </th> <th> <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> </th> <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}"/> </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>
- 共通エラーメッセージ表示エリアのスタイルを通常エラーメッセージ表示エリアと合わせたいので CSS の設定を変更します。
<div class="callout callout-danger" th:if="${#fields.hasGlobalErrors()}">
→<div class="alert alert-danger" th:if="${#fields.hasGlobalErrors()}">
へ変更します。 - 通常エラーメッセージ表示エリアである
<div class="alert alert-success" th:if="${successMessages}"> ... </div>
を追加します。 <input type="hidden" th:field="*{btn}"/>
を追加します。$(".js-btn-apply").click(function(){ ... });
の中に$("#btn").val("apply");
を追加します。$(".js-btn-temporarySave").click(function(){ ... });
の中に$("#btn").val("temporarySave");
を追加します。
LendingappFormValidator.java
package ksbysample.webapp.lending.web.lendingapp; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY; @Component public class LendingappFormValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return clazz.equals(LendingappForm.class); } @Override public void validate(Object target, Errors errors) { LendingappForm lendingappForm = (LendingappForm) target; // 「一時保存」ボタンが押された時は validate メソッドの入力チェックは実行しない if (StringUtils.equals(lendingappForm.getBtn(), "temporarySave")) { return; } // 以下の点をチェックする // ・最低1つ「申請する」が選択されているか // ・「申請する」が選択されている場合に申請理由が入力されているか boolean existApply = false; boolean rejectEmptyReason = false; int i = 0; for (LendingBookDto lendingBookDto : lendingappForm.getLendingBookDtoList()) { if (StringUtils.equals(lendingBookDto.getLendingAppFlg(), APPLY.getValue())) { existApply = true; if (StringUtils.isBlank(lendingBookDto.getLendingAppReason())) { errors.rejectValue(String.format("lendingBookDtoList[%d].lendingAppReason", i), null); if (!rejectEmptyReason) { errors.reject("LendingappForm.lendingBookDtoList.emptyReason", null); rejectEmptyReason = true; } } } i++; } if (!existApply) { i = 0; for (LendingBookDto lendingBookDto : lendingappForm.getLendingBookDtoList()) { errors.rejectValue(String.format("lendingBookDtoList[%d].lendingAppFlg", i), null); i++; } errors.reject("LendingappForm.lendingBookDtoList.notExistApply", null); } } }
- validate メソッドの最初に
if (StringUtils.equals(lendingappForm.getBtn(), "temporarySave")) { return; }
を追加します。
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.message.MessagesPropertiesHelper; 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 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; public LendingApp getLendingApp(Long lendingAppId) { LendingApp lendingApp = lendingAppDao.selectById(lendingAppId); if (lendingApp == null) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingApp.nodataerr", null)); } return lendingApp; } public List<LendingBook> getLendingBookList(Long lendingAppId) { List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingAppId); return lendingBookList; } public void apply(LendingappForm lendingappForm) { // 更新対象のデータを取得する(ロックする) 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); }); } 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); }); } }
- temporarySave メソッドを追加します。
SuccessMessagesHelper.java
package ksbysample.webapp.lending.helper.thymeleaf; import org.springframework.ui.Model; import java.util.ArrayList; import java.util.List; public class SuccessMessagesHelper { private List<String> successMessages = new ArrayList<>(); public SuccessMessagesHelper() {} public SuccessMessagesHelper(String msg) { this.successMessages.add(msg); } public void addMsg(String msg) { this.successMessages.add(msg); } public void setToModel(Model model) { model.addAttribute("successMessages", this.successMessages); } }
LendingappController.java
package ksbysample.webapp.lending.web.lendingapp; import ksbysample.webapp.lending.cookie.CookieLastLendingAppId; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; 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.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.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; 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 bindingResultForLendingappParamForm , LendingappForm lendingappForm , HttpServletResponse response) { if (bindingResultForLendingappParamForm.hasErrors()) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("LendingappForm.lendingAppId.emptyerr", null)); } // 画面に表示するデータを取得する 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) { if (bindingResult.hasErrors()) { return "lendingapp/lendingapp"; } // 入力された内容で申請する lendingappService.apply(lendingappForm); // 画面に表示するデータを取得する setDispData(lendingappForm.getLendingApp().getLendingAppId(), lendingappForm); // LastLendingAppId Cookie を削除する CookieUtils.removeCookie(CookieLastLendingAppId.class, response); 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"; } // 入力された内容を一時保存する lendingappService.temporarySave(lendingappForm); // 画面に表示する通常メッセージをセットする SuccessMessagesHelper successMessagesHelper = new SuccessMessagesHelper("一時保存しました"); successMessagesHelper.setToModel(model); return "lendingapp/lendingapp"; } private void setDispData(Long lendingAppId, LendingappForm lendingappForm) { LendingApp lendingApp = lendingappService.getLendingApp(lendingAppId); List<LendingBook> lendingBookList = lendingappService.getLendingBookList(lendingAppId); lendingappForm.setLendingApp(lendingApp); lendingappForm.setLendingBookList(lendingBookList); } }
- temporarySave メソッドの以下の点を変更します。
- 引数に
@Validated LendingappForm lendingappForm
,BindingResult bindingResult
を追加します。 - メソッド内の処理を実装します。
- 引数に
履歴
2015/11/30
初版発行。
2015/12/02
* 「次回は。。。」に申請者にメールを送信する件と、セッションに保存する件を忘れていた件を追記しました。