Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その44 )( 貸出承認画面の作成4 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その43 )( 貸出承認画面の作成3 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 貸出承認画面の作成
- 「確定」ボタンをクリックした時の処理の実装。
- 2回目の更新処理&メール送信処理を実装します。
- 貸出承認画面の作成
参照したサイト・書籍
Velocity - User Guide
https://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html- メールの中に書籍名と承認・却下の結果を出力するための Velocity のテンプレートでの記述方法を調べた時に参照しました。
目次
- LendingBookDao インターフェースの変更
- Mail003Helper クラスの作成
- LendingapprovalService クラスの変更
- LendingapprovalController クラスの変更
- lendingapproval.html の変更
- 動作確認
- 次回は。。。
手順
LendingBookDao インターフェースの変更
- lending_book テーブルの approval_result, approval_reason だけを更新するメソッドを作成します。src/main/java/ksbysample/webapp/lending/dao の下の LendingBookDao.java を リンク先の内容 に変更します。
Mail003Helper クラスの作成
src/main/java/ksbysample/webapp/lending/helper/mail の下に Mail003Helper.java を作成します。作成後、リンク先の内容 に変更します。
src/main/resources/templates/mail の下に mail003-body.vm を作成します。作成後、リンク先の内容 に変更します。
LendingapprovalService クラスの変更
- 更新処理を実装します。src/main/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalService.java を リンク先の内容 に変更します。
LendingapprovalController クラスの変更
- src/main/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalController.java を リンク先の内容 に変更します。
lendingapproval.html の変更
- src/main/resources/templates/lendingapproval の下の lendingapproval.html を リンク先の内容 に変更します。
動作確認
動作確認します。データは以下の状態です。
メールを受信するので smtp4dev が起動していない場合には起動します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/lendingapproval?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
貸出承認画面が表示されます。
まずは全て「承認」を選択した後「確定」ボタンをクリックしてみます。
画面は貸出承認画面のままですが、画面上部に「確定しました」のメッセージが表示され、「承認/却下」「却下理由」欄は表示状態になりました。
DB のデータも正常に更新されていることが確認できます。
smtp4dev にもメールが送信されており、
受信したメールを表示すると承認対象の書籍一覧と、「承認/却下」欄が全て承認で表示されていることが確認できます。
データを version カラム以外は元の状態に戻します。
今度は全て「却下」を選択して「却下理由」を入力した後「確定」ボタンをクリックしてみます。
画面上部に「確定しました」のメッセージが表示され、「承認/却下」「却下理由」欄は表示状態になりました。
DB のデータも正常に更新されていることが確認できます。
smtp4dev にもメールが送信されており、
受信したメールを表示すると承認対象の書籍一覧と、「承認/却下」欄が全て却下で表示されていることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
一旦 commit します。
次回は。。。
承認権限 ( ROLE_APPROVER ) を付与されていないユーザが /lendingapproval にアクセスした場合にはエラーにする処理を実装します。
ソースコード
LendingBookDao.java
package ksbysample.webapp.lending.dao; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig; import org.seasar.doma.Dao; import org.seasar.doma.Delete; import org.seasar.doma.Insert; import org.seasar.doma.Select; import org.seasar.doma.Update; import org.seasar.doma.jdbc.SelectOptions; import java.util.List; /** */ @Dao @ComponentAndAutowiredDomaConfig public interface LendingBookDao { /** * @param lendingBookId * @return the LendingBook entity */ @Select LendingBook selectById(Long lendingBookId); @Select(ensureResult = true) LendingBook selectByIdAndVersion(Long lendingBookId, Long version); @Select List<LendingBook> selectByLendingAppId(Long lendingAppId); @Select List<LendingBook> selectByLendingAppId(Long lendingAppId, SelectOptions options); @Select List<LendingBook> selectByLendingAppIdAndLendingAppFlg(Long lendingAppId, String lendingAppFlg); @Select List<LendingBook> selectByLendingAppIdAndLendingAppFlg(Long lendingAppId, String lendingAppFlg, SelectOptions options); /** * @param entity * @return affected rows */ @Insert int insert(LendingBook entity); /** * @param entity * @return affected rows */ @Update int update(LendingBook entity); @Update(include = {"lendingState"}) int updateLendingState(LendingBook entity); @Update(include = {"lendingAppFlg", "lendingAppReason"}) int updateLendingAppFlgAndReason(LendingBook entity); @Update(include = {"approvalResult", "approvalReason"}) int updateApprovalResultAndReason(LendingBook entity); /** * @param entity * @return affected rows */ @Delete int delete(LendingBook entity); }
@Update(include = {"approvalResult", "approvalReason"}) int updateApprovalResultAndReason(LendingBook entity);
を追加します。
Mail003Helper.java
package ksbysample.webapp.lending.helper.mail; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.util.velocity.VelocityUtils; import ksbysample.webapp.lending.values.ValuesHelper; import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Component public class Mail003Helper { private final String TEMPLATE_LOCATION_TEXTMAIL = "mail/mail003-body.vm"; private final String FROM_ADDR = "lendingapp@sample.com"; private final String SUBJECT = "貸出申請が承認・却下されました"; @Autowired private VelocityUtils velocityUtils; @Autowired private JavaMailSender mailSender; @Autowired private ValuesHelper<?> vh; public MimeMessage createMessage(String toAddr, Long lendingAppId, List<LendingBook> lendingBookList) throws MessagingException { List<Mail003BookData> mail003BookDataList = lendingBookList.stream() .map(Mail003BookData::new) .collect(Collectors.toList()); MimeMessage mimeMessage = this.mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8"); message.setFrom(FROM_ADDR); message.setTo(toAddr); message.setSubject(SUBJECT); message.setText(generateTextUsingVelocity(lendingAppId, mail003BookDataList), false); return message.getMimeMessage(); } private String generateTextUsingVelocity(Long lendingAppId, List<Mail003BookData> mail003BookDataList) { Map<String, Object> model = new HashMap<>(); model.put("lendingAppId", lendingAppId); model.put("mail003BookDataList", mail003BookDataList); return velocityUtils.merge(this.TEMPLATE_LOCATION_TEXTMAIL, model); } @Data public class Mail003BookData { private String approvalResultStr; private String bookName; public Mail003BookData(LendingBook lendingBook) { try { this.approvalResultStr = vh.getText("LendingBookApprovalResultValues", lendingBook.getApprovalResult()); this.bookName = lendingBook.getBookName(); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } } }
mail003-body.vm
貸出申請が承認・却下されました。 ======================================================================== 承認/却下 書籍 ------------------------------------------------------------------------ #foreach ($bookData in $mail003BookDataList) $bookData.approvalResultStr $bookData.bookName #end ======================================================================== 詳細は以下のURLから確認してください。 http://localhost:8080/confirmresult?lendingAppId=${lendingAppId}
LendingapprovalService.java
package ksbysample.webapp.lending.web.lendingapproval; import ksbysample.webapp.lending.dao.LendingAppDao; import ksbysample.webapp.lending.dao.LendingBookDao; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.helper.mail.EmailHelper; import ksbysample.webapp.lending.helper.mail.Mail003Helper; 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.ArrayList; import java.util.Arrays; import java.util.List; import static ksbysample.webapp.lending.values.LendingAppStatusValues.APPLOVED; import static ksbysample.webapp.lending.values.LendingAppStatusValues.PENDING; import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY; @Service public class LendingapprovalService { @Autowired private LendingAppDao lendingAppDao; @Autowired private UserInfoDao userInfoDao; @Autowired private LendingBookDao lendingBookDao; @Autowired private Mail003Helper mail003Helper; @Autowired private EmailHelper emailHelper; public void setDispData(Long lendingAppId, LendingapprovalForm lendingapprovalForm) { LendingApp lendingApp = lendingAppDao.selectByIdAndStatus(lendingAppId , Arrays.asList(PENDING.getValue(), APPLOVED.getValue())); String username = ""; if (lendingApp != null) { UserInfo userInfo = userInfoDao.selectById(lendingApp.getLendingUserId()); username = userInfo.getUsername(); } List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppIdAndLendingAppFlg(lendingAppId, APPLY.getValue()); lendingapprovalForm.setLendingApp(lendingApp); lendingapprovalForm.setUsername(username); lendingapprovalForm.setApplyingBookFormList(lendingBookList); } public void complete(LendingapprovalForm lendingapprovalForm) throws MessagingException { // 更新対象のデータを取得する(ロックする) Long lendingAppId = lendingapprovalForm.getLendingApp().getLendingAppId(); LendingApp lendingApp = lendingAppDao.selectById(lendingAppId, SelectOptions.get().forUpdate()); List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppId(lendingAppId, SelectOptions.get().forUpdate()); // lending_app.status を 4(承認済) にする lendingApp.setStatus(APPLOVED.getValue()); lendingAppDao.update(lendingApp); // lending_book の approval_result, approval_reason を更新する lendingBookList = new ArrayList<>(); for (ApplyingBookForm applyingBookForm : lendingapprovalForm.getApplyingBookFormList()) { LendingBook lendingBook = new LendingBook(); BeanUtils.copyProperties(applyingBookForm, lendingBook); lendingBookDao.updateApprovalResultAndReason(lendingBook); lendingBookList.add(lendingBook); } // 申請者にメールを送信する UserInfo userInfo = userInfoDao.selectById(lendingApp.getLendingUserId()); MimeMessage mimeMessage = mail003Helper.createMessage(userInfo.getMailAddress(), lendingAppId, lendingBookList); emailHelper.sendMail(mimeMessage); } }
- complete メソッドを追加し、中の処理を実装します。
LendingapprovalController.java
package ksbysample.webapp.lending.web.lendingapproval; import ksbysample.webapp.lending.exception.WebApplicationRuntimeException; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import ksbysample.webapp.lending.helper.thymeleaf.SuccessMessagesHelper; 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; @Controller @RequestMapping("/lendingapproval") public class LendingapprovalController { @Autowired private LendingapprovalService lendingapprovalService; @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Autowired private LendingapprovalFormValidator lendingapprovalFormValidator; @InitBinder(value = "lendingapprovalForm") public void initBinder(WebDataBinder binder) { binder.addValidators(lendingapprovalFormValidator); } @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); // 指定された貸出申請IDで申請中、承認済のデータがない場合には、貸出承認画面上にエラーメッセージを表示する if (lendingapprovalForm.getLendingApp() == null) { bindingResultOfLendingapprovalForm.reject("LendingapprovalForm.lendingApp.nodataerr"); } return "lendingapproval/lendingapproval"; } @RequestMapping(value = "/complete", method = RequestMethod.POST) public String complete(@Validated LendingapprovalForm lendingapprovalForm , BindingResult bindingResult , Model model) throws MessagingException { if (bindingResult.hasErrors()) { return "lendingapproval/lendingapproval"; } try { // データを更新し、承認完了メールを送信する lendingapprovalService.complete(lendingapprovalForm); // 画面に表示するデータを取得する lendingapprovalService.setDispData(lendingapprovalForm.getLendingApp().getLendingAppId(), lendingapprovalForm); // 画面に表示する通常メッセージをセットする SuccessMessagesHelper successMessagesHelper = new SuccessMessagesHelper("確定しました"); successMessagesHelper.setToModel(model); } catch (OptimisticLockException e) { bindingResult.reject("Global.optimisticLockException"); } return "lendingapproval/lendingapproval"; } }
- complete メソッド内に「データを更新し、承認完了メールを送信する」~「画面に表示する通常メッセージをセットする」の処理を追加します。更新時の楽観的排他制御でエラーが発生した場合に、入力された内容を維持したまま画面にエラーメッセージが表示されるよう
try { ... } catch (OptimisticLockException e) { bindingResult.reject("Global.optimisticLockException"); }
で処理を囲みます。
lendingapproval.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; } .btn-approval.active { background-color: #00a65a; } .btn-reject.active { background-color: #dd4b39; } .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="lendingapprovalForm" method="post" action="/lendingapproval/complete" th:action="@{/lendingapproval/complete}" th:object="${lendingapprovalForm}"> <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" th:if="*{lendingApp != null}"> <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> <td th:text="*{lendingApp.lendingAppId}">1</td> </tr> <tr> <th class="bg-purple">ステータス</th> <td th:text="${@vh.getText('LendingAppStatusValues', lendingapprovalForm.lendingApp.status)}">申請中</td> </tr> <tr> <th class="bg-purple">申請者</th> <td th:text="*{username}">田中 太郎</td> </tr> <input type="hidden" th:field="*{lendingApp.lendingAppId}"/> <input type="hidden" th:field="*{lendingApp.status}"/> <input type="hidden" th:field="*{username}"/> </table> </div> <br/> <table class="table"> <colgroup> <col width="5%"/> <col width="15%"/> <col width="15%"/> <col width="20%"/> <col width="20%"/> <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="applyingBookForm, iterStat : *{applyingBookFormList}"> <td th:text="${iterStat.count}">1</td> <td th:text="${applyingBookForm.isbn}">978-4-7741-6366-6</td> <td th:text="${applyingBookForm.bookName}">GitHub実践入門</td> <td th:text="${applyingBookForm.lendingAppReason}">開発で使用する為</td> <td> <div class="btn-group-sm" data-toggle="buttons" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"> <label class="btn btn-default btn-approval" th:classappend="*{applyingBookFormList[__${iterStat.index}__].approvalResult} == ${@vh.getValue('LendingBookApprovalResultValues', 'APPROVAL')} ? 'active' : ''"> <input type="radio" th:field="*{applyingBookFormList[__${iterStat.index}__].approvalResult}" th:value="${@vh.getValue('LendingBookApprovalResultValues', 'APPROVAL')}"/> 承認 </label> <label class="btn btn-default btn-reject" th:classappend="*{applyingBookFormList[__${iterStat.index}__].approvalResult} == ${@vh.getValue('LendingBookApprovalResultValues', 'REJECT')} ? 'active' : ''"> <input type="radio" th:field="*{applyingBookFormList[__${iterStat.index}__].approvalResult}" th:value="${@vh.getValue('LendingBookApprovalResultValues', 'REJECT')}"/> 却下 </label> </div> <span th:text="${@vh.getText('LendingBookApprovalResultValues', applyingBookForm.approvalResult)}" th:if="*{lendingApp.status} != ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"> </span> </td> <td> <input type="text" class="form-control input-sm" th:classappend="${#fields.hasErrors('*{applyingBookFormList[__${iterStat.index}__].approvalReason}')} ? 'has-error' : ''" th:field="*{applyingBookFormList[__${iterStat.index}__].approvalReason}" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"/> <span th:text="${applyingBookForm.approvalReason}" th:if="*{lendingApp.status} != ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"> </span> </td> <input type="hidden" th:field="*{applyingBookFormList[__${iterStat.index}__].lendingBookId}"/> <input type="hidden" th:field="*{applyingBookFormList[__${iterStat.index}__].isbn}"/> <input type="hidden" th:field="*{applyingBookFormList[__${iterStat.index}__].bookName}"/> <input type="hidden" th:field="*{applyingBookFormList[__${iterStat.index}__].lendingAppReason}"/> <input type="hidden" th:field="*{applyingBookFormList[__${iterStat.index}__].version}"/> </tr> </tbody> </table> <div class="text-center" th:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"> <button class="btn bg-blue js-btn-complete"><i class="fa fa-check-square-o"></i> 確定</button> </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-complete").click(function(){ $("#lendingapprovalForm").submit(); return false; }); }); --> </script> </body> </html>
- 通常メッセージ表示エリアの
<div class="alert alert-success" th:if="${successMessages}">...</div>
を追加します。 - 「承認/却下」欄はステータスが申請中の時のみ変更可能にするために以下の点を変更します。
<div class="btn-group-sm" data-toggle="buttons">
にth:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"
を追加します。<span th:text="${@vh.getText('LendingBookApprovalResultValues', applyingBookForm.approvalResult)}" ...></span>
を追加します。
- 「却下理由」欄もステータスが申請中の時のみ変更可能にするために以下の点を変更します。
<input type="text" class="form-control input-sm" .../>
にth:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"
を追加します。<span th:text="${applyingBookForm.approvalReason}" ...></span>
を追加します。
- 「確定」ボタンはステータスが申請中の時のみ表示されるよう
<div class="text-center">
にth:if="*{lendingApp.status} == ${@vh.getValue('LendingAppStatusValues', 'PENDING')}"
を追加します。
履歴
2016/01/17
初版発行。