mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2025-07-21 15:25:49 +00:00
Implement search for pets by name
This commit is contained in:
parent
516722647a
commit
353b5033b1
9 changed files with 333 additions and 7 deletions
|
@ -18,6 +18,7 @@ package org.springframework.samples.petclinic.owner;
|
|||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
@ -40,13 +41,13 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
|||
*/
|
||||
@Controller
|
||||
@RequestMapping("/owners/{ownerId}")
|
||||
class PetController {
|
||||
class OwnerPetController {
|
||||
|
||||
private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";
|
||||
|
||||
private final OwnerRepository owners;
|
||||
|
||||
public PetController(OwnerRepository owners) {
|
||||
public OwnerPetController(OwnerRepository owners) {
|
||||
this.owners = owners;
|
||||
}
|
||||
|
||||
|
@ -135,10 +136,10 @@ class PetController {
|
|||
|
||||
String petName = pet.getName();
|
||||
|
||||
// checking if the pet name already exist for the owner
|
||||
// checking if the pet name already exists for the owner
|
||||
if (StringUtils.hasText(petName)) {
|
||||
Pet existingPet = owner.getPet(petName.toLowerCase(), false);
|
||||
if (existingPet != null && existingPet.getId() != pet.getId()) {
|
||||
if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) {
|
||||
result.rejectValue("name", "duplicate", "already exists");
|
||||
}
|
||||
}
|
|
@ -79,4 +79,11 @@ public interface OwnerRepository extends Repository<Owner, Integer> {
|
|||
@Transactional(readOnly = true)
|
||||
Page<Owner> findAll(Pageable pageable);
|
||||
|
||||
/**
|
||||
* Returns the {@link Owner} of a pet
|
||||
* @param id the pet id to search for
|
||||
* @return the {@link Owner} if found
|
||||
*/
|
||||
Owner findByPets_Id(Integer id);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* @author Thibault Helsmoortel
|
||||
*/
|
||||
public interface PetRepository extends Repository<Pet, Integer> {
|
||||
|
||||
@Query("SELECT DISTINCT pet FROM Pet pet WHERE lower(pet.name) LIKE :name% ")
|
||||
@Transactional(readOnly = true)
|
||||
Page<Pet> findByName(String name, Pageable pageable);
|
||||
|
||||
Pet findById(Integer id);
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package org.springframework.samples.petclinic.pet;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.samples.petclinic.owner.Owner;
|
||||
import org.springframework.samples.petclinic.owner.OwnerRepository;
|
||||
import org.springframework.samples.petclinic.owner.Pet;
|
||||
import org.springframework.samples.petclinic.owner.PetRepository;
|
||||
import org.springframework.samples.petclinic.owner.PetValidator;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* @author Thibault Helsmoortel
|
||||
*/
|
||||
@Controller
|
||||
public class PetController {
|
||||
|
||||
private final PetRepository pets;
|
||||
|
||||
private final OwnerRepository owners;
|
||||
|
||||
public PetController(PetRepository pets, OwnerRepository owners) {
|
||||
this.pets = pets;
|
||||
this.owners = owners;
|
||||
}
|
||||
|
||||
@InitBinder("pet")
|
||||
public void initPetBinder(WebDataBinder dataBinder) {
|
||||
dataBinder.setValidator(new PetValidator());
|
||||
}
|
||||
|
||||
@ModelAttribute("pet")
|
||||
public Pet findPet(@PathVariable(name = "petId", required = false) Integer petId) {
|
||||
if (petId == null) {
|
||||
return new Pet();
|
||||
}
|
||||
|
||||
Pet pet = pets.findById(petId);
|
||||
if (pet == null) {
|
||||
throw new IllegalArgumentException("Pet ID not found: " + petId);
|
||||
}
|
||||
|
||||
return pet;
|
||||
}
|
||||
|
||||
@GetMapping("/pets/find")
|
||||
public String initFindForm() {
|
||||
return "pets/findPets";
|
||||
}
|
||||
|
||||
@GetMapping("/pets")
|
||||
public String processFindForm(@RequestParam(defaultValue = "1") int page, Pet pet, BindingResult result,
|
||||
Model model) {
|
||||
// allow parameterless GET request for /pets to return all records
|
||||
String petName = "";
|
||||
if (pet.getName() != null) {
|
||||
petName = pet.getName().toLowerCase();
|
||||
}
|
||||
|
||||
// find pets by name
|
||||
Page<Pet> petsResults = findPaginatedForPetName(page, petName);
|
||||
if (petsResults.isEmpty()) {
|
||||
// no pets found
|
||||
result.rejectValue("name", "notFound", "not found");
|
||||
|
||||
return "pets/findPets";
|
||||
}
|
||||
|
||||
if (petsResults.getTotalElements() == 1) {
|
||||
// 1 pet found
|
||||
pet = petsResults.iterator().next();
|
||||
|
||||
return "redirect:/pets/" + pet.getId();
|
||||
}
|
||||
|
||||
// multiple pets found
|
||||
return addPaginationModel(page, model, petsResults);
|
||||
}
|
||||
|
||||
private Page<Pet> findPaginatedForPetName(int page, String name) {
|
||||
int pageSize = 5;
|
||||
Pageable pageable = PageRequest.of(page - 1, pageSize);
|
||||
|
||||
return pets.findByName(name, pageable);
|
||||
}
|
||||
|
||||
private String addPaginationModel(int page, Model model, Page<Pet> paginated) {
|
||||
List<Pet> listPets = paginated.getContent();
|
||||
model.addAttribute("currentPage", page);
|
||||
model.addAttribute("totalPages", paginated.getTotalPages());
|
||||
model.addAttribute("totalItems", paginated.getTotalElements());
|
||||
model.addAttribute("listPets", listPets);
|
||||
model.addAttribute("listPetAndOwners",
|
||||
listPets.stream().map(pet -> new PetAndOwner(pet, owners.findByPets_Id(pet.getId()))).toList());
|
||||
|
||||
return "pets/petsList";
|
||||
}
|
||||
|
||||
record PetAndOwner(Pet pet, Owner owner) {
|
||||
}
|
||||
|
||||
@GetMapping("/pets/{petId}")
|
||||
public ModelAndView showPet(@PathVariable("petId") int petId) {
|
||||
ModelAndView mav = new ModelAndView("pets/petDetails");
|
||||
Pet pet = this.pets.findById(petId);
|
||||
Owner owner = owners.findByPets_Id(petId);
|
||||
mav.addObject("pet", pet);
|
||||
mav.addObject("owner", owner);
|
||||
|
||||
return mav;
|
||||
}
|
||||
|
||||
}
|
|
@ -55,6 +55,11 @@
|
|||
<span>Find owners</span>
|
||||
</li>
|
||||
|
||||
<li th:replace="~{::menuItem ('/pets/find','pets','find pets','search','Find pets')}">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
<span>Find pets</span>
|
||||
</li>
|
||||
|
||||
<li th:replace="~{::menuItem ('/vets.html','vets','veterinarians','th-list','Veterinarians')}">
|
||||
<span class="fa fa-th-list" aria-hidden="true"></span>
|
||||
<span>Veterinarians</span>
|
||||
|
|
34
src/main/resources/templates/pets/findPets.html
Normal file
34
src/main/resources/templates/pets/findPets.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<html xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{fragments/layout :: layout (~{::body},'pets')}">
|
||||
|
||||
<body>
|
||||
|
||||
<h2>Find pets</h2>
|
||||
|
||||
<form th:object="${pet}" th:action="@{/pets}" method="get"
|
||||
class="form-horizontal" id="search-pet-form">
|
||||
<div class="form-group">
|
||||
<div class="control-group" id="nameGroup">
|
||||
<label class="col-sm-2 control-label">Name </label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" th:field="*{name}" size="30"
|
||||
maxlength="80" /> <span class="help-inline"><div
|
||||
th:if="${#fields.hasAnyErrors()}">
|
||||
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">Error</p>
|
||||
</div></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Find
|
||||
pet</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary" th:href="@{/pets/new}">Add pet</a>
|
||||
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
77
src/main/resources/templates/pets/petDetails.html
Normal file
77
src/main/resources/templates/pets/petDetails.html
Normal file
|
@ -0,0 +1,77 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{fragments/layout :: layout (~{::body},'pets')}">
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<h2>Pet Information</h2>
|
||||
|
||||
<div th:if="${message}" class="alert alert-success" id="success-message">
|
||||
<span th:text="${message}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger" id="error-message">
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table table-striped" th:object="${pet}">
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Name</dt>
|
||||
<dd th:text="${pet.name}"></dd>
|
||||
<dt>Birth Date</dt>
|
||||
<dd
|
||||
th:text="${#temporals.format(pet.birthDate, 'yyyy-MM-dd')}"></dd>
|
||||
<dt>Type</dt>
|
||||
<dd th:text="${pet.type}"></dd>
|
||||
<dt>Owner</dt>
|
||||
<dd><a th:text="${owner.firstName + ' ' + owner.lastName}" th:href="@{../owners/__${owner.id}__}"></a></dd>
|
||||
</dl>
|
||||
</td>
|
||||
<table class="table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Visit Date</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr th:each="visit : ${pet.visits}">
|
||||
<td th:text="${#temporals.format(visit.date, 'yyyy-MM-dd')}"></td>
|
||||
<td th:text="${visit?.description}"></td>
|
||||
</tr>
|
||||
<tr th:object="${owner}">
|
||||
<a style="all: unset" th:href="@{../owners/__${owner.id}__/pets/__${pet.id}__/edit}">
|
||||
<button style="margin-bottom: 1em"
|
||||
class="btn btn-primary" type="button">
|
||||
Edit Pet
|
||||
</button>
|
||||
</a>
|
||||
<td><a th:href="@{../owners/__${owner.id}__/pets/__${pet.id}__/visits/new}">Add Visit</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<script>
|
||||
// Function to hide the success and error messages after 3 seconds
|
||||
function hideMessages() {
|
||||
setTimeout(function () {
|
||||
document.getElementById("success-message").style.display = "none";
|
||||
document.getElementById("error-message").style.display = "none";
|
||||
}, 3000); // 3000 milliseconds (3 seconds)
|
||||
}
|
||||
|
||||
// Call the function to hide messages
|
||||
hideMessages();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
58
src/main/resources/templates/pets/petsList.html
Normal file
58
src/main/resources/templates/pets/petsList.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'pets')}">
|
||||
|
||||
<body>
|
||||
|
||||
<h2>Pets</h2>
|
||||
|
||||
<table id="pets" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 150px;">Name</th>
|
||||
<th style="width: 150px;">Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="pet : ${listPets}">
|
||||
<td>
|
||||
<a th:href="@{/pets/__${pet.id}__}" th:text="${pet.name}"/></a>
|
||||
</td>
|
||||
<td th:each="petAndOwner : ${listPetAndOwners}" th:if="${petAndOwner.pet().id == pet.id}">
|
||||
<a th:href="@{/pets/__${petAndOwner.owner().id}__}" th:text="${petAndOwner.owner().firstName + ' ' + petAndOwner.owner().lastName}"/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div th:if="${totalPages > 1}">
|
||||
<span>Pages:</span>
|
||||
<span>[</span>
|
||||
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
|
||||
<a th:if="${currentPage != i}" th:href="@{'/pets?page=' + ${i}}">[[${i}]]</a>
|
||||
<span th:unless="${currentPage != i}">[[${i}]]</span>
|
||||
</span>
|
||||
<span>] </span>
|
||||
<span>
|
||||
<a th:if="${currentPage > 1}" th:href="@{'/pets?page=1'}" title="First"
|
||||
class="fa fa-fast-backward"></a>
|
||||
<span th:unless="${currentPage > 1}" title="First" class="fa fa-fast-backward"></span>
|
||||
</span>
|
||||
<span>
|
||||
<a th:if="${currentPage > 1}" th:href="@{'/pets?page=__${currentPage - 1}__'}" title="Previous"
|
||||
class="fa fa-step-backward"></a>
|
||||
<span th:unless="${currentPage > 1}" title="Previous" class="fa fa-step-backward"></span>
|
||||
</span>
|
||||
<span>
|
||||
<a th:if="${currentPage < totalPages}" th:href="@{'/pets?page=__${currentPage + 1}__'}" title="Next"
|
||||
class="fa fa-step-forward"></a>
|
||||
<span th:unless="${currentPage < totalPages}" title="Next" class="fa fa-step-forward"></span>
|
||||
</span>
|
||||
<span>
|
||||
<a th:if="${currentPage < totalPages}" th:href="@{'/pets?page=__${totalPages}__'}" title="Last"
|
||||
class="fa fa-fast-forward"></a>
|
||||
<span th:unless="${currentPage < totalPages}" title="Last" class="fa fa-step-forward"></span>
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -36,15 +36,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
|
||||
|
||||
/**
|
||||
* Test class for the {@link PetController}
|
||||
* Test class for the {@link OwnerPetController}
|
||||
*
|
||||
* @author Colin But
|
||||
*/
|
||||
@WebMvcTest(value = PetController.class,
|
||||
@WebMvcTest(value = OwnerPetController.class,
|
||||
includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
|
||||
@DisabledInNativeImage
|
||||
@DisabledInAotMode
|
||||
class PetControllerTests {
|
||||
class OwnerPetControllerTests {
|
||||
|
||||
private static final int TEST_OWNER_ID = 1;
|
||||
|
Loading…
Reference in a new issue