Implement search for pets by name

This commit is contained in:
Thibstars 2024-03-23 15:00:59 +01:00
parent 516722647a
commit 353b5033b1
9 changed files with 333 additions and 7 deletions

View file

@ -18,6 +18,7 @@ package org.springframework.samples.petclinic.owner;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Objects;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -40,13 +41,13 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/ */
@Controller @Controller
@RequestMapping("/owners/{ownerId}") @RequestMapping("/owners/{ownerId}")
class PetController { class OwnerPetController {
private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm"; private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";
private final OwnerRepository owners; private final OwnerRepository owners;
public PetController(OwnerRepository owners) { public OwnerPetController(OwnerRepository owners) {
this.owners = owners; this.owners = owners;
} }
@ -135,10 +136,10 @@ class PetController {
String petName = pet.getName(); 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)) { if (StringUtils.hasText(petName)) {
Pet existingPet = owner.getPet(petName.toLowerCase(), false); 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"); result.rejectValue("name", "duplicate", "already exists");
} }
} }

View file

@ -79,4 +79,11 @@ public interface OwnerRepository extends Repository<Owner, Integer> {
@Transactional(readOnly = true) @Transactional(readOnly = true)
Page<Owner> findAll(Pageable pageable); 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);
} }

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -55,6 +55,11 @@
<span>Find owners</span> <span>Find owners</span>
</li> </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')}"> <li th:replace="~{::menuItem ('/vets.html','vets','veterinarians','th-list','Veterinarians')}">
<span class="fa fa-th-list" aria-hidden="true"></span> <span class="fa fa-th-list" aria-hidden="true"></span>
<span>Veterinarians</span> <span>Veterinarians</span>

View 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>

View 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>

View 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>]&nbsp;</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>

View file

@ -36,15 +36,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 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 * @author Colin But
*/ */
@WebMvcTest(value = PetController.class, @WebMvcTest(value = OwnerPetController.class,
includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE)) includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
@DisabledInNativeImage @DisabledInNativeImage
@DisabledInAotMode @DisabledInAotMode
class PetControllerTests { class OwnerPetControllerTests {
private static final int TEST_OWNER_ID = 1; private static final int TEST_OWNER_ID = 1;