Skip to content

Commit 1878f69

Browse files
meysholdtona-agent
andcommitted
feat(CX-215): add first name search to find owners form
Add a first name input field to the find owners form so users can narrow results when multiple owners share a surname. Both fields are optional and use prefix matching. - Add findByFirstNameStartingWithAndLastNameStartingWith to OwnerRepository - Update OwnerController to filter by both first and last name - Add first name input to findOwners.html template - Preserve both query params in ownersList.html pagination links - Add tests for first-name-only, combined, and no-results scenarios Co-authored-by: Ona <[email protected]>
1 parent cf2971a commit 1878f69

5 files changed

Lines changed: 78 additions & 13 deletions

File tree

src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,13 @@ public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner
9999
if (lastName == null) {
100100
lastName = ""; // empty string signifies broadest possible search
101101
}
102+
String firstName = owner.getFirstName();
103+
if (firstName == null) {
104+
firstName = ""; // empty string signifies broadest possible search
105+
}
102106

103-
// find owners by last name
104-
Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, lastName);
107+
// find owners by first name and last name
108+
Page<Owner> ownersResults = findPaginatedForOwners(page, firstName, lastName);
105109
if (ownersResults.isEmpty()) {
106110
// no owners found
107111
result.rejectValue("lastName", "notFound", "not found");
@@ -115,6 +119,8 @@ public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner
115119
}
116120

117121
// multiple owners found
122+
model.addAttribute("firstName", firstName);
123+
model.addAttribute("lastName", lastName);
118124
return addPaginationModel(page, model, ownersResults);
119125
}
120126

@@ -127,10 +133,10 @@ private String addPaginationModel(int page, Model model, Page<Owner> paginated)
127133
return "owners/ownersList";
128134
}
129135

130-
private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
136+
private Page<Owner> findPaginatedForOwners(int page, String firstName, String lastName) {
131137
int pageSize = 5;
132138
Pageable pageable = PageRequest.of(page - 1, pageSize);
133-
return owners.findByLastNameStartingWith(lastname, pageable);
139+
return owners.findByFirstNameStartingWithAndLastNameStartingWith(firstName, lastName, pageable);
134140
}
135141

136142
@GetMapping("/owners/{ownerId}/edit")

src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ public interface OwnerRepository extends JpaRepository<Owner, Integer> {
4444
*/
4545
Page<Owner> findByLastNameStartingWith(String lastName, Pageable pageable);
4646

47+
/**
48+
* Retrieve {@link Owner}s from the data store by first name and last name, returning
49+
* all owners whose first name <i>starts</i> with the given first name and whose last
50+
* name <i>starts</i> with the given last name.
51+
* @param firstName Value to search for in first name
52+
* @param lastName Value to search for in last name
53+
* @return a Collection of matching {@link Owner}s (or an empty Collection if none
54+
* found)
55+
*/
56+
Page<Owner> findByFirstNameStartingWithAndLastNameStartingWith(String firstName, String lastName,
57+
Pageable pageable);
58+
4759
/**
4860
* Retrieve an {@link Owner} from the data store by id.
4961
* <p>

src/main/resources/templates/owners/findOwners.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
<h2 th:text="#{findOwners}">Find Owners</h2>
88

99
<form th:object="${owner}" th:action="@{/owners}" method="get" class="form-horizontal" id="search-owner-form">
10+
<div class="form-group">
11+
<div class="control-group" id="firstNameGroup">
12+
<label class="col-sm-2 control-label" th:text="#{firstName}">First name </label>
13+
<div class="col-sm-10">
14+
<input class="form-control" th:field="*{firstName}" size="30" maxlength="80" />
15+
</div>
16+
</div>
17+
</div>
1018
<div class="form-group">
1119
<div class="control-group" id="lastNameGroup">
1220
<label class="col-sm-2 control-label" th:text="#{lastName}">Last name </label>

src/main/resources/templates/owners/ownersList.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,26 @@ <h2 th:text="#{owners}">Owners</h2>
3232
<span th:text="#{pages}">Pages:</span>
3333
<span>[</span>
3434
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
35-
<a th:if="${currentPage != i}" th:href="@{'/owners?page=' + ${i}}">[[${i}]]</a>
35+
<a th:if="${currentPage != i}" th:href="@{/owners(page=${i},firstName=${firstName},lastName=${lastName})}">[[${i}]]</a>
3636
<span th:unless="${currentPage != i}">[[${i}]]</span>
3737
</span>
3838
<span>]&nbsp;</span>
3939
<span>
40-
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" th:title="#{first}" class="fa fa-fast-backward"></a>
40+
<a th:if="${currentPage > 1}" th:href="@{/owners(page=1,firstName=${firstName},lastName=${lastName})}" th:title="#{first}" class="fa fa-fast-backward"></a>
4141
<span th:unless="${currentPage > 1}" th:title="#{first}" class="fa fa-fast-backward"></span>
4242
</span>
4343
<span>
44-
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=__${currentPage - 1}__'}" th:title="#{previous}"
44+
<a th:if="${currentPage > 1}" th:href="@{/owners(page=${currentPage - 1},firstName=${firstName},lastName=${lastName})}" th:title="#{previous}"
4545
class="fa fa-step-backward"></a>
4646
<span th:unless="${currentPage > 1}" th:title="#{previous}" class="fa fa-step-backward"></span>
4747
</span>
4848
<span>
49-
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${currentPage + 1}__'}" th:title="#{next}"
49+
<a th:if="${currentPage < totalPages}" th:href="@{/owners(page=${currentPage + 1},firstName=${firstName},lastName=${lastName})}" th:title="#{next}"
5050
class="fa fa-step-forward"></a>
5151
<span th:unless="${currentPage < totalPages}" th:title="#{next}" class="fa fa-step-forward"></span>
5252
</span>
5353
<span>
54-
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${totalPages}__'}" th:title="#{last}"
54+
<a th:if="${currentPage < totalPages}" th:href="@{/owners(page=${totalPages},firstName=${firstName},lastName=${lastName})}" th:title="#{last}"
5555
class="fa fa-fast-forward"></a>
5656
<span th:unless="${currentPage < totalPages}" th:title="#{last}" class="fa fa-fast-forward"></span>
5757
</span>

src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ private Owner george() {
9191
void setup() {
9292

9393
Owner george = george();
94-
given(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class)))
94+
given(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq(""), eq("Franklin"),
95+
any(Pageable.class)))
9596
.willReturn(new PageImpl<>(List.of(george)));
9697

9798
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(george));
@@ -142,29 +143,67 @@ void testInitFindForm() throws Exception {
142143
@Test
143144
void testProcessFindFormSuccess() throws Exception {
144145
Page<Owner> tasks = new PageImpl<>(List.of(george(), new Owner()));
145-
when(this.owners.findByLastNameStartingWith(anyString(), any(Pageable.class))).thenReturn(tasks);
146+
when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(anyString(), anyString(),
147+
any(Pageable.class)))
148+
.thenReturn(tasks);
146149
mockMvc.perform(get("/owners?page=1")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList"));
147150
}
148151

149152
@Test
150153
void testProcessFindFormByLastName() throws Exception {
151154
Page<Owner> tasks = new PageImpl<>(List.of(george()));
152-
when(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))).thenReturn(tasks);
155+
when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq(""), eq("Franklin"),
156+
any(Pageable.class)))
157+
.thenReturn(tasks);
153158
mockMvc.perform(get("/owners?page=1").param("lastName", "Franklin"))
154159
.andExpect(status().is3xxRedirection())
155160
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
156161
}
157162

163+
@Test
164+
void testProcessFindFormByFirstName() throws Exception {
165+
Page<Owner> tasks = new PageImpl<>(List.of(george()));
166+
when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq("George"), eq(""), any(Pageable.class)))
167+
.thenReturn(tasks);
168+
mockMvc.perform(get("/owners?page=1").param("firstName", "George"))
169+
.andExpect(status().is3xxRedirection())
170+
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
171+
}
172+
173+
@Test
174+
void testProcessFindFormByFirstNameAndLastName() throws Exception {
175+
Page<Owner> tasks = new PageImpl<>(List.of(george()));
176+
when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq("George"), eq("Franklin"),
177+
any(Pageable.class)))
178+
.thenReturn(tasks);
179+
mockMvc.perform(get("/owners?page=1").param("firstName", "George").param("lastName", "Franklin"))
180+
.andExpect(status().is3xxRedirection())
181+
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
182+
}
183+
158184
@Test
159185
void testProcessFindFormNoOwnersFound() throws Exception {
160186
Page<Owner> tasks = new PageImpl<>(List.of());
161-
when(this.owners.findByLastNameStartingWith(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks);
187+
when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq(""), eq("Unknown Surname"),
188+
any(Pageable.class)))
189+
.thenReturn(tasks);
162190
mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname"))
163191
.andExpect(status().isOk())
164192
.andExpect(model().attributeHasFieldErrors("owner", "lastName"))
165193
.andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound"))
166194
.andExpect(view().name("owners/findOwners"));
195+
}
167196

197+
@Test
198+
void testProcessFindFormByFirstNameNoOwnersFound() throws Exception {
199+
Page<Owner> tasks = new PageImpl<>(List.of());
200+
when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq("Unknown"), eq(""), any(Pageable.class)))
201+
.thenReturn(tasks);
202+
mockMvc.perform(get("/owners?page=1").param("firstName", "Unknown"))
203+
.andExpect(status().isOk())
204+
.andExpect(model().attributeHasFieldErrors("owner", "lastName"))
205+
.andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound"))
206+
.andExpect(view().name("owners/findOwners"));
168207
}
169208

170209
@Test

0 commit comments

Comments
 (0)