Skip to content

Commit

Permalink
Recipes CRUD
Browse files Browse the repository at this point in the history
  • Loading branch information
kassner committed Sep 19, 2023
1 parent 3bad49b commit d325304
Show file tree
Hide file tree
Showing 22 changed files with 621 additions and 21 deletions.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import org.gradle.api.internal.tasks.compile.CompileJavaBuildOperationType
import java.nio.file.Files
import kotlin.io.path.Path
import org.siouan.frontendgradleplugin.infrastructure.gradle.InstallFrontendTask
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
java
Expand All @@ -24,6 +22,8 @@ repositories {
dependencies {
implementation("org.postgresql:postgresql")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.flywaydb:flyway-core")
implementation("org.springframework.session:spring-session-core")
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"private": true,
"packageManager": "[email protected]",
"scripts": {
"build": "webpack --mode production"
"build": "webpack --mode production",
"watch": "webpack --mode production --watch --progress"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/se/kassner/whattocook/AppController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package se.kassner.whattocook;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AppController
{
@GetMapping("/")
public String home()
{
return "home";
}
}
5 changes: 4 additions & 1 deletion src/main/java/se/kassner/whattocook/Controller.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ public ResponseEntity<String> createSession()
public ResponseEntity<Recipe> getRecipe()
{
Session session = sessionService.get();

if (session == null) {
return ResponseEntity.notFound().build();
}

Recipe recipe = sessionService.getRecipe(session);
if (recipe == null) {
return ResponseEntity.notFound().build();
}

return ResponseEntity.ok(recipe);
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/se/kassner/whattocook/Ingredient.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public Ingredient(long id, @NonNull String name)
this.name = name;
}

public Ingredient(@NonNull String name)
{
this.name = name;
}

public long getId()
{
return id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
@Repository
public interface IngredientRepository extends JpaRepository<Ingredient, Long>
{
public Ingredient getOneByName(String name);
}
16 changes: 16 additions & 0 deletions src/main/java/se/kassner/whattocook/Recipe.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class Recipe
joinColumns = @JoinColumn(name = "recipe_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "ingredient_id", referencedColumnName = "id")
)
@OrderBy("name ASC")
private Set<Ingredient> ingredients;

public Recipe()
Expand Down Expand Up @@ -61,4 +62,19 @@ public Set<Ingredient> getIngredients()
{
return ingredients;
}

public void setName(@NonNull String name)
{
this.name = name;
}

public void setInstructions(String instructions)
{
this.instructions = instructions;
}

public void setIngredients(Set<Ingredient> ingredients)
{
this.ingredients = ingredients;
}
}
3 changes: 3 additions & 0 deletions src/main/java/se/kassner/whattocook/RecipeRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ public interface RecipeRepository extends JpaRepository<Recipe, Long>
nativeQuery = true
)
public Recipe findOneRandomWithoutIngredients(@Param("ingredient_ids") List<Long> excludedIngredientIds);

@Query("SELECT r FROM Recipe r ORDER BY r.name ASC")
public List<Recipe> findAllForListing();
}
180 changes: 180 additions & 0 deletions src/main/java/se/kassner/whattocook/manage/RecipeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package se.kassner.whattocook.manage;

import jakarta.persistence.EntityNotFoundException;
import org.hibernate.exception.ConstraintViolationException;
import jakarta.validation.Valid;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import se.kassner.whattocook.Ingredient;
import se.kassner.whattocook.IngredientRepository;
import se.kassner.whattocook.Recipe;
import se.kassner.whattocook.RecipeRepository;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Controller
@RequestMapping("/manage/recipe")
public class RecipeController
{
private final RecipeRepository recipeRepository;

private final IngredientRepository ingredientRepository;

public RecipeController(RecipeRepository recipeRepository, IngredientRepository ingredientRepository)
{
this.recipeRepository = recipeRepository;
this.ingredientRepository = ingredientRepository;
}

@GetMapping
public String get(Model model)
{
List<Recipe> recipes = recipeRepository.findAllForListing();
model.addAttribute("recipes", recipes);

return "recipe/index";
}

@GetMapping("/add")
public String add(RecipeForm recipeForm, Model model)
{
model.addAttribute("form_title", "Add recipe");
model.addAttribute("form_url", "/manage/recipe/add");

return "recipe/form";
}

@PostMapping("/add")
public String addPost(@Valid RecipeForm recipeForm, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
{
model.addAttribute("form_title", "Add recipe");
model.addAttribute("form_url", "/manage/recipe/add");

if (bindingResult.hasErrors()) {
return "recipe/form";
}

Recipe recipe = new Recipe();
recipe.setName(recipeForm.getName());
recipe.setInstructions(recipeForm.getInstructions());
recipe.setIngredients(convertIngredients(recipeForm.getIngredients()));

return saveRecipe(recipe, model, redirectAttributes);
}

@GetMapping("/{id}/edit")
public String edit(@PathVariable("id") Long recipeId, Model model, RedirectAttributes redirectAttributes)
{
Recipe recipe;
try {
recipe = recipeRepository.getReferenceById(recipeId);
} catch (EntityNotFoundException e) {
redirectAttributes.addFlashAttribute("error", e.getMessage());
return "redirect:/manage/recipe";
}

RecipeForm recipeForm = new RecipeForm(recipe);
model.addAttribute("form_title", String.format("Edit recipe \"%s\"", recipe.getName()));
model.addAttribute("form_url", String.format("/manage/recipe/%d/edit", recipe.getId()));
model.addAttribute("recipeForm", recipeForm);

return "recipe/form";
}

@PostMapping(value = "/{id}/edit")
public String edit(
@PathVariable("id") Long recipeId,
@Valid RecipeForm recipeForm,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes
)
{
Recipe recipe;
try {
recipe = recipeRepository.getReferenceById(recipeId);
} catch (EntityNotFoundException e) {
redirectAttributes.addFlashAttribute("error", e.getMessage());
return "redirect:/manage/recipe";
}

model.addAttribute("form_title", String.format("Edit recipe \"%s\"", recipe.getName()));
model.addAttribute("form_url", String.format("/manage/recipe/%d/edit", recipe.getId()));
model.addAttribute("recipeForm", recipeForm);

if (bindingResult.hasErrors()) {
return "recipe/form";
}

recipe.setName(recipeForm.getName());
recipe.setInstructions(recipeForm.getInstructions());
recipe.setIngredients(convertIngredients(recipeForm.getIngredients()));

return saveRecipe(recipe, model, redirectAttributes);
}

private Set<Ingredient> convertIngredients(String[] ingredientNames)
{
Set<Ingredient> recipeIngredients = new HashSet<>();

for (String ingredientName : ingredientNames) {
if (ingredientName.isEmpty()) {
continue;
}

Ingredient ingredient = ingredientRepository.getOneByName(ingredientName);

if (ingredient == null) {
ingredient = new Ingredient(ingredientName);
ingredientRepository.save(ingredient);
}

recipeIngredients.add(ingredient);
}

return recipeIngredients;
}

@PostMapping("/{id}/delete")
public String delete(@PathVariable Long id, RedirectAttributes redirectAttributes)
{
// @TODO do not allow delete recipes assigned to an ongoing session

try {
Recipe recipe = recipeRepository.getReferenceById(id);
recipeRepository.delete(recipe);
redirectAttributes.addFlashAttribute("success", String.format("Recipe %s deleted", recipe.getName()));
} catch (Exception e) {
redirectAttributes.addFlashAttribute("error", e.getMessage());
}

return "redirect:/manage/recipe";
}

private String saveRecipe(Recipe recipe, Model model, RedirectAttributes redirectAttributes)
{
try {
recipeRepository.saveAndFlush(recipe);
redirectAttributes.addFlashAttribute("success", String.format("Recipe \"%s\" modified", recipe.getName()));
} catch (DataIntegrityViolationException e) {
ConstraintViolationException cause = (ConstraintViolationException) e.getCause();
if (cause.getConstraintName().equals("recipe_name_unique")) {
model.addAttribute("error", "There is a recipe with the chosen name is already registered");
} else {
model.addAttribute("error", e.getMessage());
}

return "recipe/form";
} catch (Exception e) {
redirectAttributes.addFlashAttribute("error", e.getMessage());
}

return "redirect:/manage/recipe";
}
}
91 changes: 91 additions & 0 deletions src/main/java/se/kassner/whattocook/manage/RecipeForm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package se.kassner.whattocook.manage;

import jakarta.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.Length;
import se.kassner.whattocook.Ingredient;
import se.kassner.whattocook.Recipe;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class RecipeForm
{
private Long id;

@NotBlank
@Length(min = 2, max = 255)
private String name;

private String instructions;

private String[] ingredients = new String[]{""};

public RecipeForm()
{
}

public RecipeForm(Long id, String name, String instructions, String[] ingredients)
{
this.id = id;
this.name = name;
this.instructions = instructions;
this.ingredients = ingredients;

if (this.ingredients.length == 0) {
this.ingredients = new String[]{""};
}
}

public RecipeForm(Recipe recipe)
{
this.id = recipe.getId();
this.name = recipe.getName();
this.instructions = recipe.getInstructions();
this.ingredients = recipe.getIngredients().stream().map(Ingredient::getName).toArray(String[]::new);

if (this.ingredients.length == 0) {
this.ingredients = new String[]{""};
}
}

public Long getId()
{
return id;
}

public String getName()
{
return name;
}

public String getInstructions()
{
return instructions;
}

public String[] getIngredients()
{
return ingredients;
}

public void setId(Long id)
{
this.id = id;
}

public void setName(String name)
{
this.name = name;
}

public void setInstructions(String instructions)
{
this.instructions = instructions;
}

public void setIngredients(String[] ingredients)
{
this.ingredients = ingredients;
}
}
5 changes: 5 additions & 0 deletions src/main/js/app/main.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

html, body {
width: 100%;
height: 100%;
}
Loading

0 comments on commit d325304

Please sign in to comment.