Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その50 )( 貸出申請結果確認画面の作成2 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その49 )( 貸出申請結果確認画面の作成 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 貸出申請結果確認画面の作成
- パラメータで指定されている貸出申請 ID とそのステータス、貸出申請したユーザ名、承認/却下したユーザ名、及び「申請する」が選択された書籍、申請理由、承認/却下、却下理由の一覧を表示する処理の実装
- 貸出申請結果確認画面の作成
参照したサイト・書籍
目次
- ConfirmresultParamForm, ApprovedBookForm, ConfirmresultForm クラスの作成
- messages_ja_JP.properties にエラーメッセージを定義する
- ConfirmresultService クラスの作成
- ConfirmresultController クラスの変更
- confirmresult.html の変更
- LendingapprovalService クラスの変更
- 動作確認
- 次回は。。。
手順
ConfirmresultParamForm, ApprovedBookForm, ConfirmresultForm クラスの作成
src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappParamForm.java を src/main/java/ksbysample/webapp/lending/web/confirmresult の下へ ConfirmresultParamForm.java というファイル名でコピーします。リンク先の内容 になります。
src/main/java/ksbysample/webapp/lending/web/confirmresult の下に ApprovedBookForm.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/confirmresult の下に ConfirmresultForm.java を作成します。作成後、リンク先の内容 に変更します。
messages_ja_JP.properties にエラーメッセージを定義する
- エラーメッセージを定義します。src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。
ConfirmresultService クラスの作成
- src/main/java/ksbysample/webapp/lending/web/confirmresult の下に ConfirmresultService.java を作成します。作成後、リンク先の内容 に変更します。
ConfirmresultController クラスの変更
- src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultController.java を リンク先の内容 に変更します。
confirmresult.html の変更
- src/main/resources/templates/confirmresult の下の confirmresult.html を リンク先の内容 に変更します。
LendingapprovalService クラスの変更
貸出承認画面で「確定」ボタンクリック時に lending_app.approval_user_id にユーザID をセットしていないことに気づいたので修正します。
一旦 commit します。
1.0.x ブランチをチェックアウトします。
feature/85-issue ブランチを作成します。
src/main/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalService.java を リンク先の内容 に変更します。
申請者と承認者が別の人になるようにします。src/test/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalControllerTest.java を リンク先の内容 に変更します。
src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/001 の下の lending_app.csv を リンク先の内容 へ変更します。
src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/002 の下の lending_app.csv を リンク先の内容 へ変更します。
clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが実行され、全て成功することを確認します。
commit、GitHub へ Push、feature/85-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/85-issue ブランチを削除、をします。
feature/81-issue ブランチをチェックアウトします。
以下のコマンドを実行します。
> git rebase 1.0.x
動作確認
動作確認します。データは以下の状態です。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
メールの URL をクリックします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
貸出申請結果確認画面が表示されることが確認できます。
http://localhost:8080/confirmresult?lendingAppId=106 にアクセスします。存在しない貸出申請ID なので貸出申請結果確認画面上にエラーメッセージが表示されることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
一旦 commit します。
次回は。。。
CSVファイルダウンロード処理を実装します。
ソースコード
ConfirmresultParamForm.java
package ksbysample.webapp.lending.web.confirmresult; import lombok.Data; import javax.validation.constraints.DecimalMax; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.NotNull; @Data public class ConfirmresultParamForm { @NotNull @DecimalMin(value = "1") @DecimalMax(value = "9223372036854775807") private Long lendingAppId; }
ApprovedBookForm.java
package ksbysample.webapp.lending.web.confirmresult; import ksbysample.webapp.lending.entity.LendingBook; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.beans.BeanUtils; @Data @NoArgsConstructor public class ApprovedBookForm { private Long lendingBookId; private String isbn; private String bookName; private String lendingAppReason; private String approvalResult; private String approvalReason; public ApprovedBookForm(LendingBook lendingBook) { BeanUtils.copyProperties(lendingBook, this); } }
ConfirmresultForm.java
package ksbysample.webapp.lending.web.confirmresult; import ksbysample.webapp.lending.entity.LendingApp; import ksbysample.webapp.lending.entity.LendingBook; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; import java.util.stream.Collectors; @Data @NoArgsConstructor public class ConfirmresultForm { private LendingApp lendingApp; private String lendingUserName; private String approvalUserName; private List<ApprovedBookForm> approvedBookFormList; public void setApprovedBookFormListFromLendingBookList(List<LendingBook> lendingBookList) { this.approvedBookFormList = null; if (lendingBookList != null) { this.approvedBookFormList = lendingBookList.stream() .map(ApprovedBookForm::new) .collect(Collectors.toList()); } } }
messages_ja_JP.properties
ConfirmresultParamForm.lendingAppId.emptyerr=貸出申請IDが指定されていません。 ConfirmresultForm.lendingApp.nodataerr=指定された貸出申請IDでは貸出承認されておりません。
- messages_ja_JP.properties の最後の上の2つのメッセージを追加します。
ConfirmresultService.java
package ksbysample.webapp.lending.web.confirmresult; 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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; import static ksbysample.webapp.lending.values.LendingAppStatusValues.APPLOVED; import static ksbysample.webapp.lending.values.LendingBookLendingAppFlgValues.APPLY; @Service public class ConfirmresultService { @Autowired private LendingAppDao lendingAppDao; @Autowired private UserInfoDao userInfoDao; @Autowired private LendingBookDao lendingBookDao; public void setDispData(Long lendingAppId, ConfirmresultForm confirmresultForm) { LendingApp lendingApp = lendingAppDao.selectByIdAndStatus(lendingAppId, Arrays.asList(APPLOVED.getValue())); String lendingUserName = ""; String approvalUserName = ""; if (lendingApp != null) { UserInfo lendingUserInfo = userInfoDao.selectById(lendingApp.getLendingUserId()); lendingUserName = lendingUserInfo.getUsername(); UserInfo approvalUserInfo = userInfoDao.selectById(lendingApp.getApprovalUserId()); approvalUserName = approvalUserInfo.getUsername(); } List<LendingBook> lendingBookList = lendingBookDao.selectByLendingAppIdAndLendingAppFlg(lendingAppId, APPLY.getValue()); confirmresultForm.setLendingApp(lendingApp); confirmresultForm.setLendingUserName(lendingUserName); confirmresultForm.setApprovalUserName(approvalUserName); confirmresultForm.setApprovedBookFormListFromLendingBookList(lendingBookList); } }
confirmresult.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; } .buttom-btn-area { padding-top: 10px; } .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; } </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="confirmresultForm" method="post" action="/confirmresult/filedownload" th:action="@{/confirmresult/filedownload}" th:object="${confirmresultForm}"> <div th:replace="common/mainparts :: alert-danger"></div> <div th:replace="common/mainparts :: alert-success"></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', confirmresultForm.lendingApp.status)}">承認済</td> </tr> <tr> <th class="bg-purple">申請者</th> <td th:text="*{lendingUserName}">田中 太郎</td> </tr> <tr> <th class="bg-purple">承認者</th> <td th:text="*{approvalUserName}">鈴木 花子</td> </tr> </table> <input type="hidden" th:field="*{lendingApp.lendingAppId}"/> </div> <br/> <table class="table"> <colgroup> <col width="5%"/> <col width="20%"/> <col width="20%"/> <col width="20%"/> <col width="15%"/> <col width="20%"/> </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="approvedBookForm, iterStat : *{approvedBookFormList}"> <td th:text="${iterStat.count}">1</td> <td th:text="${approvedBookForm.isbn}">978-4-7741-6366-6</td> <td th:text="${approvedBookForm.bookName}">GitHub実践入門</td> <td th:text="${approvedBookForm.lendingAppReason}">開発で使用する為</td> <td th:text="${@vh.getText('LendingBookApprovalResultValues', approvedBookForm.approvalResult)}">承認</td> <td th:text="${approvedBookForm.approvalReason}"></td> </tr> </tbody> </table> <div class="buttom-btn-area text-center"> <button class="btn bg-blue js-btn-filedownload"><i class="fa fa-download"></i> CSVダウンロード</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-filedownload").click(function(){ $("#confirmresultForm").submit(); return false; }); }); --> </script> </body> </html>
<form id="lendingapprovalForm" ...>
の下に共通エラーメッセージ表示エリアである<div th:replace="common/mainparts :: alert-danger"></div>
、通常メッセージ表示エリアである<div th:replace="common/mainparts :: alert-success"></div>
を追加します。- 貸出申請ID, ステータス, 申請者, 承認者にデータが表示されるよう
th:text="..."
を追加します。ステータスは ValuesHelper クラスを利用してth:text="${@vh.getText('LendingAppStatusValues', confirmresultForm.lendingApp.status)}"
でコードに対応する文字列が表示されるようにします。 - CSVダウンロード時に貸出申請IDが必要になるので、
<input type="hidden" th:field="*{lendingApp.lendingAppId}"/>
を追加します。 <tbody class="jp-gothic">
の下の<tr>
→<tr th:each="applyingBookForm, iterStat : *{applyingBookFormList}">
へ変更します。イテレーション処理中のステータスを取るための変数 iterStat を記述します。- 一覧表の各行にデータが表示されるように th:text="..." を追加します。承認/却下はコードに対応する文字を表示するために
th:text="${@vh.getText('LendingBookApprovalResultValues', approvedBookForm.approvalResult)}"
と記述します。
ConfirmresultController.java
package ksbysample.webapp.lending.web.confirmresult; import ksbysample.webapp.lending.exception.WebApplicationRuntimeException; import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper; import ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm; import ksbysample.webapp.lending.web.lendingapproval.LendingapprovalParamForm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/confirmresult") public class ConfirmresultController { @Autowired private MessagesPropertiesHelper messagesPropertiesHelper; @Autowired private ConfirmresultService confirmresultService; @RequestMapping public String index(@Validated ConfirmresultParamForm confirmresultParamForm , BindingResult bindingResult , ConfirmresultForm confirmresultForm , BindingResult bindingResultOfConfirmresultForm) { if (bindingResult.hasErrors()) { throw new WebApplicationRuntimeException( messagesPropertiesHelper.getMessage("ConfirmresultParamForm.lendingAppId.emptyerr", null)); } // 画面に表示するデータを取得する confirmresultService.setDispData(confirmresultParamForm.getLendingAppId(), confirmresultForm); // 指定された貸出申請IDで承認済のデータがない場合には、貸出申請結果確認画面上にエラーメッセージを表示する if (confirmresultForm.getLendingApp() == null) { bindingResultOfConfirmresultForm.reject("ConfirmresultForm.lendingApp.nodataerr"); } return "confirmresult/confirmresult"; } @RequestMapping(value = "/filedownload", method = RequestMethod.POST) public String filedownload() { return "confirmresult/confirmresult"; } }
@Autowired private MessagesPropertiesHelper messagesPropertiesHelper;
を追加します。@Autowired private ConfirmresultService confirmresultService;
を追加します。- index メソッドの以下の点を変更します。
- 引数に confirmresultParamForm, bindingResult, confirmresultForm, bindingResultOfConfirmresultForm の4つを追加します。
- 内部の処理を実装します。
LendingapprovalService.java
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()); lendingApp.setApprovalUserId(LendingUserDetailsHelper.getLoginUserId()); 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 メソッドの処理に
lendingApp.setApprovalUserId(LendingUserDetailsHelper.getLoginUserId());
を追加します。
LendingapprovalControllerTest.java
@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.authSuzukiHanako.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"}); 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)); } @Test @TestData("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/testdata/001") public void 確定ボタンをクリックした場合_却下と却下理由() throws Exception { // when ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました ) mvc.authSuzukiHanako.perform(TestHelper.postForm("/lendingapproval/complete", this.lendingapprovalForm_005).with(csrf())) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("lendingapproval/lendingapproval")) .andExpect(model().hasNoErrors()) .andExpect(xpath("//*[@id=\"lendingapprovalForm\"]/div/div/table/tbody/tr[1]/td[6]/input[@type=\"text\"]").doesNotExist()) .andExpect(xpath("//*[@id=\"lendingapprovalForm\"]/div[2]/div/table/tbody/tr[1]/td[6]/span").string("購入済です")); // then ( Spock Framework のブロックの区分けが分かりやすかったので、同じ部分にコメントで付けてみました ) // DB IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/lendingapproval/assertdata/002")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id"}); tableDataAssert.assertEquals("lending_book", new String[]{"approval_result", "approval_reason", "version"} , AssertOptions.INCLUDE_COLUMN); // メール 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/002/message.txt") , Charsets.UTF_8)); } }
- 「貸出承認画面の正常処理時のテスト」テストクラス内のテストメソッド「確定ボタンをクリックした場合承認()」「確定ボタンをクリックした場合却下と却下理由()」の以下2点を変更します。
mvc.authTanakaTaro.perform(...
→mvc.authSuzukiHanako.perform(...
へ変更します。tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id", "approval_user_id"});
→tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id"});
へ変更します。
web/lendingapproval/assertdata/001/lending_app.csv
lending_app_id,status,lending_user_id,approval_user_id,version 105,4,1,2,2
- approval_user_id の値を
[null]
→2
へ変更します。
web/lendingapproval/assertdata/002/lending_app.csv
lending_app_id,status,lending_user_id,approval_user_id,version 105,4,1,2,2
- approval_user_id の値を
[null]
→2
へ変更します。
履歴
2016/01/30
初版発行。