git clone git@github.com:Taack/myLibrary.git
Set Up
The solution and skeletons for this task are available to clone on this repository
myLibrary) and copy the following line.git checkout part-2-skeleton
Or in IntelliJ you can select the wanted branch directly. Select in the top left corner main/myLibrary/remote, and you will see all the branches. Select a branch add checkout.
Introduction
You have now created a personal library, let’s add some features. We want to add a borrowing system. After this part, you will be able to borrow books from your own library, approve the request and return the books, and you will also have access to your borrowing request history.
We have updated the code in myLibrary. Pull this version to have access to the new skeleton code.
Define and Update Domain Classes
Edit: app/library/src/main/groovy/myLibrary/domain/
Pro Mode: [TODO 1.1, 1.2 & 1.3]
MyLibraryBook class and add the following method (TODO 1.1):int getNumberOfBooksBorrowable() {
return MyLibraryBookInstance.countByIsAvailableBAndIsActiveAndBook(true, true, this) //(1)
}
-
countByIsAvailableBAndIsActiveAndBookis a dynamic finder method in Grails/GORM that automatically counts the number of records in a domain class matching the given conditions for the fieldsisAvailableB,isActive, andbook. For more information, Grails Documentation (countBy).
MyLibraryBookInstance class and add the following variables and hasMany relationship (TODO 1.2):List<MyLibraryBorrowed> borrowHistoryOfBook
Boolean isAvailableB = true
static hasMany = [borrowHistoryOfBook: MyLibraryBorrowed]
MyLibraryBorrowed class (TODO 1.3):enum ApprovalStatus {
PENDING, APPROVED, REJECTED
}
@GrailsCompileStatic
@TaackFieldEnum
class MyLibraryBorrowed {
MyLibraryBookInstance bookInstance
User user
Date requestDate
Date approvalDate
Date returnDate
ApprovalStatus statusOfApproval= ApprovalStatus.PENDING
static constraints = {
approvalDate nullable: true
returnDate nullable: true
}
}
You have now implemented all the necessary classes for this section!
Define Menus
For this section, we need to create two new menus. The user will request to borrow a bookInstance in the Book menu. The bookInstances that have been requested and not yet returned (either pending requests or approved requests) will be in the borrowed menu. The bookInstances that have been returned will be in the History menu.
Let’s start by adding these menus to the buildMenu.
Pro Mode: Add the menus in buildMenu. [TODO 2]
buildMenu:menu MyLibraryController.&listBooksBorrowed as MC
menu MyLibraryController.&listBooksCurrentlyBorrowed as MC
Define Borrowed Menu
Borrowed Table
We want to display the borrowed (not yet returned) in a table. For that we need to create a UiTableSpecifier. Go to app/library/services/myLibrary/MyLibraryUiService in Author Menu.
Note: the only differences between the borrowed and history menus are the returnDate and StatusOfApproval.
Therefore, we will use the same TableSpecifier for both menus; this is one of the big advantages of using a separate file (MyLibraryUiService) to implement the Specifiers: they can be reused in different contexts with small modifications.
|
Note
|
You will need a boolean flag to check if a book is currently borrowed, we’ll call it isCurrentlyBorrowed. |
Pro Mode: implement the Table by yourself [TODOS 3.1].
Solution
buildBorrowedTable:MyLibraryBook book = new MyLibraryBook()
MyLibraryBorrowed borrowed = new MyLibraryBorrowed()
UiTableSpecifier buildUserBorrowsSpecifier = new UiTableSpecifier()
MyLibraryBookInstance bookInstance = new MyLibraryBookInstance()
buildUserBorrowsSpecifier.ui {
header {
sortableFieldHeader borrowed.bookInstance_,bookInstance.book_,book.title_
sortableFieldHeader borrowed.bookInstance_,bookInstance.book_,book.author_
if (isCurrently) {label borrowed.statusOfApproval_}
label borrowed.requestDate_
label borrowed.approvalDate_
column {label borrowed.user_}
}
TaackFilter.FilterBuilder filter = taackFilterService.getBuilder(MyLibraryBorrowed)
.setMaxNumberOfLine(10)
.setSortOrder(
TaackFilter.Order.ASC,
borrowed.bookInstance_,
bookInstance.book_,book.title_)
if(isCurrently) {filter.addFilter(new FilterExpression(null, Operator.EQ, borrowed.returnDate_))} //(1)
else {filter.addFilter(new FilterExpression(null, Operator.NE, borrowed.returnDate_))} //(1)
iterate(
filter.build()) { MyLibraryBorrowed borrowedIterator ->
rowColumn {
rowField borrowedIterator.bookInstance.book.title
}
rowField borrowedIterator.bookInstance.book.author.toString()
if(isCurrently) {rowField borrowedIterator.statusOfApproval_}
rowField borrowedIterator.requestDate_
rowField borrowedIterator.approvalDate_
rowColumn {rowField borrowedIterator.user.username_}
}
}
-
This is another way to create a filter if you are not planning on reusing it somewhere else.
We also want to add a filter that filters the authors by last name. Let’s implement this filter.
Pro Mode: implement the Filter by yourself [TODO 3.2].
buildUserBorrowsFilter:UiFilterSpecifier buildUserBorrowsFilter(User user = null) {
MyLibraryBook book = new MyLibraryBook()
UiFilterSpecifier UserBorrowsFilterSpecifier = new UiFilterSpecifier()
MyLibraryBorrowed borrowed = new MyLibraryBorrowed()
MyLibraryBookInstance bookInstance = new MyLibraryBookInstance()
UserBorrowsFilterSpecifier.ui MyLibraryBorrowed, {
if (user) hiddenId(user.id)
section "Borrows Filter", {
filterField borrowed.bookInstance_,bookInstance.book_,book.title_
}
}
}
Your table is now complete; we just need to render it on the page. We will do this in a module called listBooksCurrentlyBorrowed. Go to app/library/controller/myLibrary/MyLibraryController in Borrowed Menu.
Pro Mode: implement listBooksCurrentlyBorrowed by yourself [TODO 3.3].
listBooksCurrentlyBorrowed method:def listBooksCurrentlyBorrowed() {
UiTableSpecifier tableUserBorrowsSpecifier = myLibraryUiService.buildUserBorrowsTable(true)
UiFilterSpecifier filterUserBorrowsSpecifier = myLibraryUiService.buildUserBorrowsFilter()
taackUiService.show(new UiBlockSpecifier().ui {
tableFilter filterUserBorrowsSpecifier, tableUserBorrowsSpecifier
}, myLibraryUiService.buildMenu())
}
You can now start the server. The borrowed table should be functional, but currently you will only see the table headers since there are no borrows in your database. So let’s proceed with creating a form and saving objects into the database. We now have to implement requestBookInstance, which requires creating a UiFormSpecifier.
Let’s go back to MyLibraryUiService.
Borrowed Form
Pro Mode: implement buildRequestBookForm [TODO 3.4].
buildRequestBookForm:UiFormSpecifier buildRequestBookForm(MyLibraryBook book) {
User user = springSecurityService.currentUser as User
MyLibraryBorrowed borrowed = new MyLibraryBorrowed()
borrowed.user = user
book ?= new MyLibraryBook(params)
UiFormSpecifier requestBookFormSpecifier = new UiFormSpecifier()
requestBookFormSpecifier.ui borrowed, {
section "Request Book Form", {
hiddenField borrowed.user_ //(1)
field borrowed.requestDate_
ajaxField borrowed.bookInstance_, MyLibraryController.&selectBookInstanceOne as MC, book.id
}
formAction MyLibraryController.&saveBookForm as MC
}
}
-
Passes parameters in the form without the user seeing.
Let’s go back to MyLibraryController. We will render this form in a modal.
Pro Mode: implement requestBookInstance [TODO 3.5]
requestBookInstance:def requestBookInstance(MyLibraryBook book){
UiFormSpecifier requestBookInstanceForm = myLibraryUiService.buildRequestBookForm(book)
taackUiService.show new UiBlockSpecifier().ui {
modal {
form requestBookInstanceForm
}
}
}
We now need to save the borrowed record, so let’s implement saveBookForm.
Pro Mode: implement saveBookForm [TODO 3.6].
@Transactional
def saveBookForm() {
MyLibraryBorrowed borrowed = taackSaveService.save(MyLibraryBorrowed)
borrowed.bookInstance?.isAvailableB = false
taackSaveService.redirectOrRenderErrors(borrowed)
}
We now need to implement selectBookInstanceOne which will use buildInstanceBookTable, we need to modify this Specifier first.
Pro Mode: modify buildInstanceBookTable [TODO 3.7].
column {
if (isOne) {
label "Select Book Instance"
} else {
label "Delete"
}
}
filter.addFilter(buildIsAvailableBookInstances(book))
rowColumn {
if(isOne) {
rowAction tr('default.serialNumber.label'), ActionIcon.SELECT * IconStyle.SCALE_DOWN, bookInstanceIterator.id, bookInstanceIterator.serialNumber.toString()
} else {
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&deleteBookInstances as MC, bookInstanceIterator.id, [bookId:book.id]
}
}
We have used the buildIsAvailableBookInstances filter; let’s implement it.
Pro Mode: Implement buildIsAvailableBookInstances [TODO 3.8].
buildIsAvailableBookInstances:UiFilterSpecifier buildIsAvailableBookInstances(MyLibraryBook book) {
MyLibraryBookInstance bookInstance = new MyLibraryBookInstance()
UiFilterSpecifier bookInstanceFilterSpecifier = new UiFilterSpecifier()
bookInstanceFilterSpecifier.sec MyLibraryBookInstance, {
filterFieldExpressionBool new FilterExpression(true, Operator.EQ, bookInstance.isAvailableB_)
}
}
Pro Mode: Implement selectBookInstanceOne [TODO 3.9].
selectBookInstanceOne:def selectBookInstanceOne(MyLibraryBook book) {
UiTableSpecifier bookInstanceTable = myLibraryUiService.buildInstanceBookTable(book, true)
taackUiService.show new UiBlockSpecifier().ui {
modal {
table bookInstanceTable
}
}
}
Let’s now add the button to access the request form. We also want to know how many books are available (can be borrowed), we will implement this in buildBookTable.
Pro Mode: Modify buildBookTable [TODO 3.10]
label "Number of Available Book Instances"
label "Request Form"
rowField bookIterator.numberOfBooksBorrowable_
rowColumn {
rowAction ActionIcon.CREATE * IconStyle.SCALE_DOWN, MyLibraryController.&requestBookInstance as MC, bookIterator.id
}
You have a fully functional table where we can display and create borrow requests. Start the server again, you should now be able to click on the button and fill out the form.
We now want to be able to approve requests and return borrowed bookInstances. Let’s implement the approveBook and returnBook methods. Let’s navigate to MyLibraryUiService in the buildUserBorrowsTable method.
Approve Requests and Return BookInstances
Go to myLibraryUiService in the buildUserBorrowsTable.
Pro Mode: add the buttons [TODO 3.11].
if (isCurrently) column {label "Return Book"}
if (isCurrently) {
rowColumn {
if (borrowedIterator.statusOfApproval == ApprovalStatus.APPROVED) {
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&returnBook as MC, borrowedIterator.id
}
}
}
if (isCurrently) {label "Approve Book"}
if (isCurrently) {
rowColumn {
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&approveBook as MC, borrowedIterator.id
}
}
Let’s now implement the forms for approveBook and returnBook.
Pro Mode implement buildApproveBookTable [TODO 3.12]
buildApproveBookTable:UiFormSpecifier buildApproveBookTable(MyLibraryBorrowed borrowed) {
UiFormSpecifier approveBookSpecifier = new UiFormSpecifier()
approveBookSpecifier.ui borrowed, {
section "Approve Book Form", {
field borrowed.approvalDate_
field borrowed.statusOfApproval_
}
formAction MyLibraryController.&saveApprovalBookForm as MC
}
}
Pro Mode implement buildRequestReturnBookForm [TODO 3.13]
buildRequestReturnBookForm:UiFormSpecifier buildRequestReturnBookForm(MyLibraryBorrowed borrowed) {
UiFormSpecifier requestBookFormSpecifier = new UiFormSpecifier()
requestBookFormSpecifier.ui borrowed, {
section "Request Return Book Form", {
field borrowed.returnDate_
}
formAction MyLibraryController.&saveReturnBookForm as MC
}
}
Navigate back to MyLibraryController to implement the methods approveBook & returnBook.
Pro Mode: implement the methods [TODO 3.14 & 3.15]
approveBook:def approveBook(MyLibraryBorrowed borrowed) {
UiFormSpecifier approveBookSpecifier = myLibraryUiService.buildApproveBookTable(borrowed)
taackUiService.show(new UiBlockSpecifier().ui {
modal {
form approveBookSpecifier
}
})
}
returnBook:def returnBook(MyLibraryBorrowed borrowed) {
UiFormSpecifier requestReturnBookInstanceForm = myLibraryUiService.buildRequestReturnBookForm(borrowed)
taackUiService.show new UiBlockSpecifier().ui {
modal {
form requestReturnBookInstanceForm
}
}
}
Let’s implement the saves, saveApprovalBookForm & saveReturnBookForm.
Pro Mode: implement the methods [TODO 3.16 & 3.17]
saveApprovalBookForm: @Transactional
def saveApprovalBookForm() {
Calendar cal = Calendar.getInstance()
cal.set(999999, Calendar.DECEMBER, 31)
Date date = cal.time
MyLibraryBorrowed borrowed = taackSaveService.save(MyLibraryBorrowed)
if(borrowed.statusOfApproval == ApprovalStatus.REJECTED) {
borrowed.returnDate = date
}
taackSaveService.redirectOrRenderErrors(borrowed)
}
Note: The Calendar is set to a far future date (year 999999) to indicate rejection as permanent.
saveReturnBookForm:@Transactional
def saveReturnBookForm() {
MyLibraryBorrowed borrowed = taackSaveService.save(MyLibraryBorrowed)
borrowed.bookInstance?.isAvailableB = true
taackSaveService.redirectOrRenderErrors(borrowed)
}
You should now be able to approve and return books. Run the server and try it!
We want to be able to display more information about the request in a modal.
Let’s now implement the showBorrowed method. We first need to create a link to this method and a UiShowSpecifier in myLibraryUiService.
Let’s navigate to MyLibraryUiService in buildUserBorrowsTable.
Pro Mode: add the buttons [TODO 3.18].
Add the show action. Replace the TODO 3.18:
rowAction ActionIcon.SHOW * IconStyle.SCALE_DOWN, MyLibraryController.&showBorrowed as MC, borrowedIterator.id
Pro Mode: implement buildBorrowedShow [TODO 3.19]
buildBorrowedShow:UiShowSpecifier buildBorrowedShow(MyLibraryBorrowed borrowed) {
UiShowSpecifier borrowedShowSpecifier = new UiShowSpecifier()
borrowedShowSpecifier.ui(borrowed, {
fieldLabeled borrowed.bookInstance.book.title_
fieldLabeled borrowed.bookInstance.book.author_
fieldLabeled borrowed.user.username_
fieldLabeled borrowed.statusOfApproval_
fieldLabeled borrowed.requestDate_
fieldLabeled borrowed.approvalDate_
fieldLabeled borrowed.returnDate_
})
}
We will render the buildBorrowedShow in a modal using showBorrowed.
Pro Mode: implement showBorrowed [TODO 3.20]
showBorrowed:def showBorrowed(MyLibraryBorrowed borrowed) {
UiShowSpecifier showSpec = myLibraryUiService.buildBorrowedShow(borrowed)
taackUiService.show(new UiBlockSpecifier().ui {
modal {
show showSpec
}
})
}
Congratulations! You now have implemented everything you need to request, approve, and return books, as well as display all the requests in progress. Start the server again and test all the new implementations.
Define History Menu
We now want to display the previous results in a table. As explained earlier, this is simple and fast since we already have our builder, and we have implemented the isCurrently. Let’s go to MyLibraryController and render the past requests.
Pro Mode: implement listBooksBorrowed [TODOS 4.1].
listBooksBorrowed:def listBooksBorrowed() {
UiTableSpecifier tableUserBorrowsSpecifier = myLibraryUiService.buildUserBorrowsTable()
UiFilterSpecifier filterUserBorrowsSpecifier = myLibraryUiService.buildUserBorrowsFilter()
taackUiService.show(new UiBlockSpecifier().ui {
tableFilter filterUserBorrowsSpecifier, tableUserBorrowsSpecifier
}, myLibraryUiService.buildMenu())
}
|
Note
|
Testing borrowers
You can test the borrowers by either : → Creating a ROLE_BORROWER in the ROLE table of the h2 database → Logging in with a created user. Refer to Section B for more information. |
Congratulations! You have completed the second part of this project.
In the next part, we will dive into Security and Constraints.