Merge branch 'feature/server-recipe-i18n' into 'main'

Server-side recipe internationalization

See merge request cse1105/2025-2026/teams/csep-team-76!47
This commit is contained in:
Natalia Cholewa 2026-01-09 22:28:31 +01:00
commit 2224fe1f51
7 changed files with 87 additions and 13 deletions

View file

@ -26,6 +26,7 @@ public class DefaultValueFactory {
return new Recipe( return new Recipe(
null, null,
"Untitled recipe", "Untitled recipe",
"en",
List.of(), List.of(),
List.of()); List.of());
} }

View file

@ -34,7 +34,7 @@ public class PrintExportTest {
List<String> preparationSteps = new ArrayList<>(); List<String> preparationSteps = new ArrayList<>();
preparationSteps.add("Mix Ingredients"); preparationSteps.add("Mix Ingredients");
preparationSteps.add("Heat in Oven"); preparationSteps.add("Heat in Oven");
Recipe recipe1 = new Recipe(testRecipeId, "Banana Bread", ingredients, preparationSteps); Recipe recipe1 = new Recipe(testRecipeId, "Banana Bread", "en", ingredients, preparationSteps);
assertEquals(""" assertEquals("""
Title: Banana Bread Title: Banana Bread

View file

@ -49,6 +49,10 @@ public class Recipe {
@Column(name = "name", nullable = false, unique = true) @Column(name = "name", nullable = false, unique = true)
private String name; private String name;
// Locale in which the recipe was created.
@Column(name = "locale", nullable = false)
private String locale = "en";
// Creates another table named recipe_ingredients which stores: // Creates another table named recipe_ingredients which stores:
// recipe_ingredients(recipe_id -> recipes(id), ingredient). // recipe_ingredients(recipe_id -> recipes(id), ingredient).
// Example recipe_ingredients table: // Example recipe_ingredients table:
@ -95,11 +99,15 @@ public class Recipe {
this.name = name; this.name = name;
} }
// TODO: Replace String with Embeddable Ingredient Class for ingredients public Recipe(Long id,
public Recipe(Long id, String name, List<RecipeIngredient> ingredients, List<String> preparationSteps) { String name,
String locale,
List<RecipeIngredient> ingredients,
List<String> preparationSteps) {
// Not used by JPA/Spring // Not used by JPA/Spring
this.id = id; this.id = id;
this.name = name; this.name = name;
this.locale = locale;
this.ingredients = ingredients; this.ingredients = ingredients;
this.preparationSteps = preparationSteps; this.preparationSteps = preparationSteps;
} }
@ -120,6 +128,14 @@ public class Recipe {
this.name = name; this.name = name;
} }
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
// TODO: Replace String with Embeddable Ingredient Class // TODO: Replace String with Embeddable Ingredient Class
public List<RecipeIngredient> getIngredients() { public List<RecipeIngredient> getIngredients() {
// Disallow modifying the returned list. // Disallow modifying the returned list.
@ -159,7 +175,7 @@ public class Recipe {
@Override @Override
public String toString() { public String toString() {
return "Recipe " + id + return "Recipe " + id +
" - " + name + " - " + name + " (" + locale + ")" +
": " + ingredients.size() + " ingredients / " + ": " + ingredients.size() + " ingredients / " +
preparationSteps.size() + " steps"; preparationSteps.size() + " steps";
} }
@ -170,6 +186,7 @@ public class Recipe {
return "Recipe{" + return "Recipe{" +
"id=" + id + "id=" + id +
", name='" + name + '\'' + ", name='" + name + '\'' +
", locale='" + locale + "'" +
", ingredients=" + ingredients + ", ingredients=" + ingredients +
", preparationSteps=" + preparationSteps + ", preparationSteps=" + preparationSteps +
'}'; '}';

View file

@ -59,19 +59,35 @@ public class RecipeController {
} }
/** /**
* Mapping for <code>GET /recipes(?limit=)</code> * Mapping for <code>GET /recipes(?limit=)(&locales=)</code>
* <p> * <p>
* If the limit parameter is unspecified, return all recipes in the repository. * If the limit parameter is unspecified, return all recipes in the repository.
* @param limit Integer limit of items you want to get * @param limit Integer limit of items you want to get
* @return The list of recipes * @return The list of recipes
*/ */
@GetMapping("/recipes") @GetMapping("/recipes")
public ResponseEntity<List<Recipe>> getRecipes(@RequestParam Optional<Integer> limit) { public ResponseEntity<List<Recipe>> getRecipes(
@RequestParam Optional<List<String>> locales,
@RequestParam Optional<Integer> limit
) {
logger.info("GET /recipes called."); logger.info("GET /recipes called.");
return ResponseEntity.ok(
// TODO: maybe refactor this. this is horrid and evil and nightmare
var recipes = locales
.map(loc -> {
return limit.map(lim -> {
return recipeService.findAllWithLocales(loc, lim);
})
.orElseGet(() -> {
return recipeService.findAllWithLocales(loc);
});
})
.orElseGet(
// Choose the right overload. One has a limit, other doesn't. // Choose the right overload. One has a limit, other doesn't.
limit.map(recipeService::findAll).orElseGet(recipeService::findAll) () -> limit.map(recipeService::findAll).orElseGet(recipeService::findAll));
);
return ResponseEntity.ok(recipes);
} }
/** /**

View file

@ -19,6 +19,15 @@ import org.springframework.data.jpa.repository.JpaRepository;
import commons.Recipe; import commons.Recipe;
import java.util.Collection;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface RecipeRepository extends JpaRepository<Recipe, Long> { public interface RecipeRepository extends JpaRepository<Recipe, Long> {
boolean existsByName(String name); boolean existsByName(String name);
List<Recipe> findAllByLocaleIsIn(Collection<String> locales);
Page<Recipe> findAllByLocaleIsIn(Collection<String> locales, Pageable pageable);
} }

View file

@ -7,6 +7,7 @@ import server.database.IngredientRepository;
import server.database.RecipeIngredientRepository; import server.database.RecipeIngredientRepository;
import server.database.RecipeRepository; import server.database.RecipeRepository;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -36,6 +37,14 @@ public class RecipeService {
return recipeRepository.findAll(PageRequest.of(0, limit)).toList(); return recipeRepository.findAll(PageRequest.of(0, limit)).toList();
} }
public List<Recipe> findAllWithLocales(Collection<String> locales) {
return recipeRepository.findAllByLocaleIsIn(locales);
}
public List<Recipe> findAllWithLocales(Collection<String> locales, int limit) {
return recipeRepository.findAllByLocaleIsIn(locales, PageRequest.of(0, limit)).toList();
}
/** /**
* Creates a new recipe. Returns empty if the recipe with the same name already exists. * Creates a new recipe. Returns empty if the recipe with the same name already exists.
* @param recipe Recipe to be saved in the db. * @param recipe Recipe to be saved in the db.

View file

@ -64,7 +64,7 @@ public class RecipeControllerTest {
public void setup(TestInfo info) { public void setup(TestInfo info) {
recipes = LongStream recipes = LongStream
.range(0, NUM_RECIPES) .range(0, NUM_RECIPES)
.mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) .mapToObj(x -> new Recipe(null, "Recipe " + x, "en", List.of(), List.of()))
.toList(); .toList();
controller = new RecipeController( controller = new RecipeController(
recipeService, recipeService,
@ -107,7 +107,7 @@ public class RecipeControllerTest {
@Tag("test-from-init-data") @Tag("test-from-init-data")
public void getManyRecipes() { public void getManyRecipes() {
// The number of recipes returned is the same as the entire input list // The number of recipes returned is the same as the entire input list
assertEquals(recipes.size(), controller.getRecipes(Optional.empty()).getBody().size()); assertEquals(recipes.size(), controller.getRecipes(Optional.empty(), Optional.empty()).getBody().size());
} }
@Test @Test
@ -115,7 +115,29 @@ public class RecipeControllerTest {
public void getSomeRecipes() { public void getSomeRecipes() {
final int LIMIT = 5; final int LIMIT = 5;
// The number of recipes returned is the same as the entire input list // The number of recipes returned is the same as the entire input list
assertEquals(LIMIT, controller.getRecipes(Optional.of(LIMIT)).getBody().size()); assertEquals(LIMIT, controller.getRecipes(Optional.empty(), Optional.of(LIMIT)).getBody().size());
}
@Test
@Tag("test-from-init-data")
public void getManyRecipesWithLocale() {
// The number of recipes returned is the same as the entire input list
assertEquals(recipes.size(), controller.getRecipes(Optional.of(List.of("en", "nl")), Optional.empty()).getBody().size());
}
@Test
@Tag("test-from-init-data")
public void getNoRecipesWithLocale() {
// should have NO Dutch recipes (thank god)
assertEquals(0, controller.getRecipes(Optional.of(List.of("nl")), Optional.empty()).getBody().size());
}
@Test
@Tag("test-from-init-data")
public void getSomeRecipesWithLocale() {
final int LIMIT = 5;
// The number of recipes returned is the same as the entire input list
assertEquals(LIMIT, controller.getRecipes(Optional.of(List.of("en", "nl")), Optional.of(LIMIT)).getBody().size());
} }
@Test @Test