Merge branch 'main' into 'fix/wiring_print_button_with_functionality'
# Conflicts: fixed import merge conflict # client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java
This commit is contained in:
commit
6caf4eaa61
35 changed files with 1310 additions and 203 deletions
64
README.md
64
README.md
|
|
@ -1,18 +1,62 @@
|
|||
# CSEP Template Project
|
||||
# CSEP FoodPal Application (Team 76)
|
||||
|
||||
This repository contains the template for the CSE project. Please extend this README.md with sufficient instructions that will illustrate for your TA and the course staff how they can run your project.
|
||||
## Usage
|
||||
|
||||
To run the template project from the command line, you either need to have [Maven](https://maven.apache.org/install.html) installed on your local system (`mvn`) or you need to use the Maven wrapper (`mvnw`). You can then execute
|
||||
The project uses Java 25. Make sure you have the correct Java version installed.
|
||||
|
||||
mvn -pl server -am spring-boot:run
|
||||
### Client
|
||||
|
||||
to run the server and
|
||||
The client needs to be launched **after** a server is already running, see Usage.Server section:
|
||||
|
||||
mvn -pl client -am javafx:run
|
||||
```
|
||||
mvn -pl client -am javafx:run
|
||||
```
|
||||
|
||||
to run the client. Please note that the server needs to be running, before you can start the client.
|
||||
### Server
|
||||
|
||||
Get the template project running from the command line first to ensure you have the required tools on your sytem.
|
||||
By default, the server listens to the port `8080`.
|
||||
[TODO(1)]:: Configurable port.
|
||||
|
||||
Once it is working, you can try importing the project into your favorite IDE. Especially the client is a bit more tricky to set up there due to the dependency on a JavaFX SDK.
|
||||
To help you get started, you can find additional instructions in the corresponding README of the client project.
|
||||
```
|
||||
mvn -pl server -am spring-boot:run
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Recipe tracking in an intuitive GUI.
|
||||
- Ability to input arbitrary amounts of an ingredient in a recipe. The design is very human. See Manual.Ingredients for more usage details.
|
||||
- Native localization in more than 2, and less than 4 languages.
|
||||
- Configurable via JSON, See Manual.Configuration.
|
||||
|
||||
## Manual
|
||||
|
||||
### Configuration
|
||||
|
||||
The configuration is with JSON, read from `config.json` in the working directory. We illustrate an example configuration below.
|
||||
```json
|
||||
{
|
||||
"language": "en",
|
||||
"serverUrl": "http://localhost:8080",
|
||||
"favourites": [
|
||||
1,
|
||||
],
|
||||
"shoppingList": [
|
||||
"Ingredient A",
|
||||
],
|
||||
}
|
||||
```
|
||||
#### Options
|
||||
|
||||
- `language: string`
|
||||
- One of \[en, nl, pl\] (as of Jan 11 2026)
|
||||
- `serverUrl: string`
|
||||
- The host that the FoodPal server runs on, see configuration example.
|
||||
- `favourites: [number]`
|
||||
- The list of recipe IDs that the user has marked as favourite.
|
||||
- `shoppingList: [string]`
|
||||
- The list of ingredients that the user has in their shopping list.
|
||||
|
||||
### Ingredients
|
||||
|
||||
- To input a **formal** ingredient, you write the numeric amount in the first input box, then the Unit in the selection dropdown, and then write the name of the ingredient, e.g. salt, apples, etc. should it be not visible already on the platform.
|
||||
- To input an **informal** ingredient, describe the amount in the first input box, like "some of", or "a sprinkle of", then select "<NONE>" in the unit selection box, and write the name of your ingredient or pick from one of the availables from the dropdown.
|
||||
|
|
|
|||
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package client.Ingredient;
|
||||
|
||||
import commons.Ingredient;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.Button;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class IngredientController {
|
||||
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@FXML
|
||||
private Button deleteButton;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
||||
|
||||
@FXML
|
||||
private void handleDeleteIngredient(ActionEvent event) {
|
||||
// Get selected ingredient
|
||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
if (selectedIngredient == null) {
|
||||
showError("No ingredient selected", "Please select an ingredient to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the ingredient is used in any recipe
|
||||
checkIngredientUsage(selectedIngredient);
|
||||
}
|
||||
|
||||
private void checkIngredientUsage(Ingredient ingredient) {
|
||||
String url = "http://localhost:8080/api/ingredients/" + ingredient.getId() + "/usage";
|
||||
|
||||
ResponseEntity<IngredientUsageResponse> response = restTemplate.getForEntity(url, IngredientUsageResponse.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
showError("Ingredient not found", "The ingredient does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
IngredientUsageResponse usageResponse = response.getBody();
|
||||
long usedInRecipes = usageResponse != null ? usageResponse.getUsedInRecipes() : 0;
|
||||
|
||||
if (usedInRecipes > 0) {
|
||||
// If ingredient is in use, show warning to the user
|
||||
showWarningDialog(ingredient, usedInRecipes);
|
||||
} else {
|
||||
// delete if not in use
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
}
|
||||
|
||||
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
|
||||
Alert alert = new Alert(AlertType.WARNING);
|
||||
alert.setTitle("Warning");
|
||||
alert.setHeaderText("Ingredient in Use");
|
||||
alert.setContentText("The ingredient '" + ingredient.getName() + "' is used in " + usedInRecipes + " recipe(s). Are you sure you want to delete it?");
|
||||
|
||||
ButtonType deleteButton = new ButtonType("Delete Anyway");
|
||||
ButtonType cancelButton = new ButtonType("Cancel");
|
||||
|
||||
alert.getButtonTypes().setAll(deleteButton, cancelButton);
|
||||
|
||||
alert.showAndWait().ifPresent(response -> {
|
||||
if (response == deleteButton) {
|
||||
// delete if the user confirms
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteIngredient(Ingredient ingredient) {
|
||||
String url = "http://localhost:8080/api/ingredients/" + ingredient.getId();
|
||||
|
||||
restTemplate.delete(url);
|
||||
showConfirmation("Deletion Successful", "The ingredient '" + ingredient.getName() + "' has been successfully deleted.");
|
||||
|
||||
// refresh
|
||||
refreshIngredientList();
|
||||
}
|
||||
|
||||
private void showError(String title, String message) {
|
||||
Alert alert = new Alert(AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void showConfirmation(String title, String message) {
|
||||
Alert alert = new Alert(AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void refreshIngredientList() {
|
||||
// Refresh
|
||||
ingredientListView.getItems().clear();
|
||||
}
|
||||
|
||||
// Inner class for usage response
|
||||
public static class IngredientUsageResponse {
|
||||
private long ingredientId;
|
||||
private long usedInRecipes;
|
||||
|
||||
public long getIngredientId() {
|
||||
return ingredientId;
|
||||
}
|
||||
|
||||
public void setIngredientId(long ingredientId) {
|
||||
this.ingredientId = ingredientId;
|
||||
}
|
||||
|
||||
public long getUsedInRecipes() {
|
||||
return usedInRecipes;
|
||||
}
|
||||
|
||||
public void setUsedInRecipes(long usedInRecipes) {
|
||||
this.usedInRecipes = usedInRecipes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,4 +26,8 @@ public class UI extends Application {
|
|||
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
|
||||
mainCtrl.setup(primaryStage, foodpal);
|
||||
}
|
||||
|
||||
public static MyFXML getFXML() {
|
||||
return FXML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import java.util.logging.Logger;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import client.exception.InvalidModificationException;
|
||||
import client.scenes.recipe.IngredientsPopupCtrl;
|
||||
import client.scenes.recipe.RecipeDetailCtrl;
|
||||
|
||||
import client.utils.Config;
|
||||
|
|
@ -20,6 +21,7 @@ import client.utils.LocaleManager;
|
|||
import client.utils.ServerUtils;
|
||||
import client.utils.WebSocketDataService;
|
||||
import client.utils.WebSocketUtils;
|
||||
import commons.Ingredient;
|
||||
import commons.Recipe;
|
||||
|
||||
import commons.ws.Topics;
|
||||
|
|
@ -36,17 +38,20 @@ import javafx.scene.control.Button;
|
|||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
|
||||
public class FoodpalApplicationCtrl implements LocaleAware {
|
||||
private final ServerUtils server;
|
||||
private final WebSocketUtils webSocketUtils;
|
||||
private final LocaleManager localeManager;
|
||||
private final WebSocketDataService<Long, Recipe> dataService;
|
||||
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
|
||||
|
||||
@FXML
|
||||
private RecipeDetailCtrl recipeDetailController;
|
||||
|
||||
|
|
@ -60,6 +65,9 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
@FXML
|
||||
public ListView<Recipe> recipeList;
|
||||
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@FXML
|
||||
private Button addRecipeButton;
|
||||
|
||||
|
|
@ -93,6 +101,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
|
||||
this.configService = configService;
|
||||
this.dataService = recipeDataService;
|
||||
|
||||
setupDataService();
|
||||
logger.info("WebSocket processor initialized.");
|
||||
initializeWebSocket();
|
||||
|
|
@ -268,7 +277,10 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
public void refresh() {
|
||||
List<Recipe> recipes;
|
||||
try {
|
||||
recipes = server.getRecipesFiltered(searchBarController.getFilter());
|
||||
recipes = server.getRecipesFiltered(
|
||||
searchBarController.getFilter(),
|
||||
this.configService.getConfig().getRecipeLanguages()
|
||||
);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
recipes = Collections.emptyList();
|
||||
String msg = "Failed to load recipes: " + e.getMessage();
|
||||
|
|
@ -400,6 +412,139 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
this.recipeDetailController.refreshFavouriteButton();
|
||||
this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty());
|
||||
}
|
||||
|
||||
//Delete Ingredient button click
|
||||
@FXML
|
||||
private void handleDeleteIngredient() {
|
||||
// Get selected ingredient
|
||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
|
||||
if (selectedIngredient == null) {
|
||||
// Show an error message if no ingredient is selected
|
||||
showError("No ingredient selected", "Please select an ingredient to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the ingredient is used in any recipe
|
||||
checkIngredientUsage(selectedIngredient);
|
||||
}
|
||||
|
||||
// Check if ingredient is used in any recipe before deleting
|
||||
private void checkIngredientUsage(Ingredient ingredient) {
|
||||
try {
|
||||
long usageCount = server.getIngredientUsage(ingredient.getId()); // Check ingredient usage via ServerUtils
|
||||
|
||||
if (usageCount > 0) {
|
||||
// If ingredient is used, show a warning dialog
|
||||
showWarningDialog(ingredient, usageCount);
|
||||
} else {
|
||||
// If not used, delete
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Error", "Failed to check ingredient usage: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteIngredient(Ingredient ingredient) {
|
||||
try {
|
||||
server.deleteIngredient(ingredient.getId()); // Call ServerUtils to delete the ingredient
|
||||
showConfirmation("Success", "Ingredient '" + ingredient.getName() + "' has been deleted.");
|
||||
refreshIngredientList(); // refresh the ingredient list
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Error", "Failed to delete ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
}
|
||||
|
||||
private void showError(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void showConfirmation(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void refreshIngredientList() {
|
||||
// Refresh
|
||||
ingredientListView.getItems().clear();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@FXML
|
||||
private void handleAddIngredient() {
|
||||
//ask the user for the ingredient name
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setTitle("Add Ingredient");
|
||||
dialog.setHeaderText("Enter the ingredient name:");
|
||||
dialog.setContentText("Ingredient:");
|
||||
|
||||
// Wait for the user to enter a value
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
|
||||
result.ifPresent(name -> {
|
||||
// Create a new Ingredient object
|
||||
Ingredient newIngredient = new Ingredient();
|
||||
newIngredient.setName(name);
|
||||
|
||||
// Add the new ingredient to the ListView
|
||||
ingredientListView.getItems().add(newIngredient);
|
||||
});
|
||||
}
|
||||
|
||||
// display ingredient name
|
||||
@FXML
|
||||
private void initializeIngredients() {
|
||||
ingredientListView.setCellFactory(list -> new ListCell<Ingredient>() {
|
||||
@Override
|
||||
protected void updateItem(Ingredient item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.getName()); // Display the ingredient name in the ListView
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void openIngredientsPopup() {
|
||||
try {
|
||||
var pair = client.UI.getFXML().load(
|
||||
IngredientsPopupCtrl.class,
|
||||
"client", "scenes", "recipe", "IngredientsPopup.fxml"
|
||||
);
|
||||
|
||||
var root = pair.getValue();
|
||||
|
||||
var stage = new javafx.stage.Stage();
|
||||
stage.setTitle("Ingredients");
|
||||
stage.initModality(javafx.stage.Modality.APPLICATION_MODAL);
|
||||
stage.setScene(new javafx.scene.Scene(root));
|
||||
stage.showAndWait();
|
||||
} catch (Exception e) {
|
||||
var alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Failed to open ingredients popup");
|
||||
alert.setContentText(e.getMessage());
|
||||
alert.showAndWait();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -410,3 +555,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
87
client/src/main/java/client/scenes/LanguageFilterCtrl.java
Normal file
87
client/src/main/java/client/scenes/LanguageFilterCtrl.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
import com.google.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckMenuItem;
|
||||
import javafx.scene.control.MenuButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LanguageFilterCtrl implements LocaleAware {
|
||||
private final LocaleManager manager;
|
||||
private final ConfigService configService;
|
||||
private final FoodpalApplicationCtrl appCtrl;
|
||||
|
||||
@Inject
|
||||
public LanguageFilterCtrl(LocaleManager manager, ConfigService configService, FoodpalApplicationCtrl appCtrl) {
|
||||
this.manager = manager;
|
||||
this.configService = configService;
|
||||
this.appCtrl = appCtrl;
|
||||
}
|
||||
|
||||
@FXML
|
||||
MenuButton langFilterMenu;
|
||||
|
||||
List<String> selectedLanguages = new ArrayList<>();
|
||||
|
||||
private String getSelectedLanguagesDisplay() {
|
||||
String joined = String.join(", ", selectedLanguages);
|
||||
if (joined.isEmpty()) {
|
||||
return "none";
|
||||
} else {
|
||||
return joined;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMenuButtonDisplay() {
|
||||
langFilterMenu.setText(getLocaleString("menu.label.selected-langs") + ": " + this.getSelectedLanguagesDisplay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateText() {
|
||||
this.updateMenuButtonDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocaleManager getLocaleManager() {
|
||||
return this.manager;
|
||||
}
|
||||
|
||||
public void initializeComponents() {
|
||||
var items = this.langFilterMenu.getItems();
|
||||
|
||||
final List<String> languages = List.of("en", "nl", "pl");
|
||||
this.selectedLanguages = this.configService.getConfig().getRecipeLanguages();
|
||||
this.updateMenuButtonDisplay();
|
||||
|
||||
items.clear();
|
||||
|
||||
languages.forEach(lang -> {
|
||||
CheckMenuItem it = new CheckMenuItem(lang);
|
||||
|
||||
if (selectedLanguages.contains(it.getText())) {
|
||||
it.setSelected(true);
|
||||
}
|
||||
|
||||
it.selectedProperty().addListener((observable, _, value) -> {
|
||||
if (value) {
|
||||
selectedLanguages.add(it.getText());
|
||||
} else {
|
||||
selectedLanguages.remove(it.getText());
|
||||
}
|
||||
|
||||
configService.save();
|
||||
selectedLanguages.sort(String::compareTo);
|
||||
appCtrl.refresh();
|
||||
|
||||
this.updateMenuButtonDisplay();
|
||||
});
|
||||
|
||||
items.add(it);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
import client.utils.ServerUtils;
|
||||
|
|
@ -28,6 +29,7 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
|
||||
private final LocaleManager localeManager;
|
||||
private final ServerUtils serverUtils;
|
||||
private final ConfigService configService;
|
||||
|
||||
private Consumer<List<Recipe>> onSearchCallback;
|
||||
|
||||
|
|
@ -41,9 +43,10 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
private Task<List<Recipe>> currentSearchTask = null;
|
||||
|
||||
@Inject
|
||||
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils) {
|
||||
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils, ConfigService configService) {
|
||||
this.localeManager = localeManager;
|
||||
this.serverUtils = serverUtils;
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
|
@ -96,7 +99,7 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
currentSearchTask = new Task<>() {
|
||||
@Override
|
||||
protected List<Recipe> call() throws IOException, InterruptedException {
|
||||
return serverUtils.getRecipesFiltered(filter);
|
||||
return serverUtils.getRecipesFiltered(filter, configService.getConfig().getRecipeLanguages());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient
|
|||
String name = nameInput.getText();
|
||||
if (unit == null || !unit.isFormal()) {
|
||||
String desc = amountInput.getText();
|
||||
if (desc.isEmpty() || name.isEmpty()) {
|
||||
// TODO printError() integration
|
||||
return; // The user is forced to kindly try again until something valid comes up.
|
||||
}
|
||||
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
|
||||
commitEdit(new VagueIngredient(newIngredient, desc));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import client.utils.ServerUtils;
|
||||
import commons.Ingredient;
|
||||
import jakarta.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
//TODO and check for capital letter milk and MILK are seen as different
|
||||
|
||||
|
||||
public class IngredientsPopupCtrl {
|
||||
|
||||
private final ServerUtils server;
|
||||
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@Inject
|
||||
public IngredientsPopupCtrl(ServerUtils server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
ingredientListView.setCellFactory(list -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Ingredient item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
@FXML
|
||||
private void addIngredient() {
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setTitle("Add Ingredient");
|
||||
dialog.setHeaderText("Create a new ingredient");
|
||||
dialog.setContentText("Name:");
|
||||
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
if (result.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = result.get().trim();
|
||||
if (name.isEmpty()) {
|
||||
showError("Ingredient name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
server.createIngredient(name); // calls POST /api/ingredients
|
||||
refresh(); // reload list from server
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to create ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FXML
|
||||
private void refresh() {
|
||||
try {
|
||||
List<Ingredient> ingredients = server.getIngredients();
|
||||
ingredientListView.getItems().setAll(ingredients);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to load ingredients: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void deleteSelected() {
|
||||
Ingredient selected = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
if (selected == null) {
|
||||
showError("No ingredient selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long usageCount = server.getIngredientUsage(selected.getId());
|
||||
if (usageCount > 0) {
|
||||
boolean proceed = confirmDeleteUsed(selected.getName(), usageCount);
|
||||
if (!proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
server.deleteIngredient(selected.getId());
|
||||
refresh();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to delete ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void close() {
|
||||
Stage stage = (Stage) ingredientListView.getScene().getWindow();
|
||||
stage.close();
|
||||
}
|
||||
|
||||
private boolean confirmDeleteUsed(String name, long usedInRecipes) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Warning");
|
||||
alert.setHeaderText("Ingredient in use");
|
||||
alert.setContentText("Ingredient '" + name + "' is used in " + usedInRecipes
|
||||
+ " recipe(s). Delete anyway?");
|
||||
|
||||
var delete = new javafx.scene.control.ButtonType("Delete Anyway");
|
||||
var cancel = new javafx.scene.control.ButtonType("Cancel");
|
||||
alert.getButtonTypes().setAll(delete, cancel);
|
||||
|
||||
return alert.showAndWait().orElse(cancel) == delete;
|
||||
}
|
||||
|
||||
private void showError(String msg) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(msg);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import javafx.scene.control.Label;
|
|||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
|
@ -82,6 +83,9 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
@FXML
|
||||
private Button favouriteButton;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> langSelector;
|
||||
|
||||
private ListView<Recipe> getParentRecipeList() {
|
||||
return this.appCtrl.recipeList;
|
||||
}
|
||||
|
|
@ -143,6 +147,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
this.showName(recipe.getName());
|
||||
this.ingredientListController.refetchFromRecipe(recipe);
|
||||
this.stepListController.refetchFromRecipe(recipe);
|
||||
this.langSelector.setValue(recipe.getLocale());
|
||||
this.refreshFavouriteButton();
|
||||
}
|
||||
|
||||
|
|
@ -331,6 +336,20 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
editableTitleArea.getChildren().add(nameLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the recipe's language.
|
||||
*/
|
||||
@FXML
|
||||
void changeLanguage() {
|
||||
recipe.setLocale(this.langSelector.getValue());
|
||||
|
||||
try {
|
||||
server.updateRecipe(this.recipe);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new UpdateException("Error occurred when updating recipe locale!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateText() {
|
||||
editRecipeTitleButton.setText(getLocaleString("menu.button.edit"));
|
||||
|
|
@ -346,5 +365,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
@Override
|
||||
public void initializeComponents() {
|
||||
initStepsIngredientsList();
|
||||
|
||||
langSelector.getItems().addAll("en", "nl", "pl");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
public class Config {
|
||||
|
||||
|
||||
private String language = "en";
|
||||
private List<String> recipeLanguages = new ArrayList<>();
|
||||
private String serverUrl = "http://localhost:8080";
|
||||
|
||||
private List<Long> favourites = new ArrayList<>();
|
||||
private List<String> shoppingList = new ArrayList<>();
|
||||
|
||||
public Config() {
|
||||
this.language = "en";
|
||||
this.serverUrl = "http://localhost:8080";
|
||||
this.favourites = new ArrayList<>();
|
||||
this.shoppingList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
|
|
@ -66,5 +69,28 @@ public class Config {
|
|||
public void removeFavourite(long recipeId) {
|
||||
getFavourites().remove(recipeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of languages that should filter the displayed recipes.
|
||||
*
|
||||
* @return The desired languages the user would like to see.
|
||||
*/
|
||||
public List<String> getRecipeLanguages() {
|
||||
return this.recipeLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a language to the list of filtering languages.
|
||||
*/
|
||||
public void addRecipeLanguage(String lang) {
|
||||
this.recipeLanguages.add(lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a language from the list of filtering languages.
|
||||
*/
|
||||
public void removeRecipeLanguage(String lang) {
|
||||
this.recipeLanguages.remove(lang);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public class DefaultValueFactory {
|
|||
return new Recipe(
|
||||
null,
|
||||
"Untitled recipe",
|
||||
"en",
|
||||
List.of(),
|
||||
List.of());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package client.utils;
|
|||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.inject.Inject;
|
||||
import commons.Ingredient;
|
||||
import commons.Recipe;
|
||||
import commons.RecipeIngredient;
|
||||
import jakarta.ws.rs.ProcessingException;
|
||||
|
|
@ -39,9 +40,16 @@ public class ServerUtils {
|
|||
* Gets all the recipes from the backend.
|
||||
* @return a JSON string with all the recipes
|
||||
*/
|
||||
public List<Recipe> getRecipes() throws IOException, InterruptedException {
|
||||
public List<Recipe> getRecipes(List<String> locales) throws IOException, InterruptedException {
|
||||
|
||||
String uri =
|
||||
SERVER +
|
||||
"/recipes" +
|
||||
"?locales=" +
|
||||
String.join(",", locales);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/recipes"))
|
||||
.uri(URI.create(uri))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
|
@ -56,9 +64,9 @@ public class ServerUtils {
|
|||
return list; // JSON string-> List<Recipe> (Jackson)
|
||||
}
|
||||
|
||||
public List<Recipe> getRecipesFiltered(String filter) throws IOException, InterruptedException {
|
||||
public List<Recipe> getRecipesFiltered(String filter, List<String> locales) throws IOException, InterruptedException {
|
||||
// TODO: implement filtering on server side
|
||||
return this.getRecipes();
|
||||
return this.getRecipes(locales);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -87,7 +95,7 @@ public class ServerUtils {
|
|||
*/
|
||||
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
|
||||
//Make sure the name of the newRecipe is unique
|
||||
List<Recipe> allRecipes = getRecipes();
|
||||
List<Recipe> allRecipes = getRecipes(List.of());
|
||||
newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong
|
||||
// now that each recipeIngredient has its own ID in the database,
|
||||
// we set that to null too to force a new persist value on the server
|
||||
|
|
@ -207,4 +215,84 @@ public class ServerUtils {
|
|||
|
||||
updateRecipe(recipe);
|
||||
}
|
||||
|
||||
|
||||
// how many ingredients are getting used in recipes
|
||||
|
||||
public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients/" + ingredientId + "/usage"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to get usage for ingredient with id: " + ingredientId
|
||||
+ " body: " + response.body());
|
||||
}
|
||||
|
||||
record IngredientUsageResponse(long ingredientId, long usedInRecipes) {}
|
||||
IngredientUsageResponse usage =
|
||||
objectMapper.readValue(response.body(), IngredientUsageResponse.class);
|
||||
|
||||
return usage.usedInRecipes();
|
||||
}
|
||||
|
||||
|
||||
public void deleteIngredient(long ingredientId) throws IOException, InterruptedException {
|
||||
// Send delete request to remove the ingredient
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients/" + ingredientId))
|
||||
.DELETE()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to delete ingredient with id: " + ingredientId + " body: " + response.body());
|
||||
}
|
||||
|
||||
logger.info("Successfully deleted ingredient with id: " + ingredientId);
|
||||
}
|
||||
|
||||
|
||||
//retrieves the list of ingredients saved to backend
|
||||
|
||||
public List<Ingredient> getIngredients() throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to fetch ingredients. Server responds with: " + response.body());
|
||||
}
|
||||
|
||||
return objectMapper.readValue(response.body(), new com.fasterxml.jackson.core.type.TypeReference<List<Ingredient>>() {});
|
||||
}
|
||||
|
||||
//creates new ingredients in the ingredient list
|
||||
|
||||
public Ingredient createIngredient(String name) throws IOException, InterruptedException {
|
||||
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
|
||||
String json = objectMapper.writeValueAsString(ingredient);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to create ingredient. Server responds with: " + response.body());
|
||||
}
|
||||
|
||||
return objectMapper.readValue(response.body(), Ingredient.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,18 +44,25 @@
|
|||
<Label fx:id="recipesLabel" text="Recipes">
|
||||
<font>
|
||||
<Font name="System Bold" size="15.0" />
|
||||
</font></Label>
|
||||
</font>
|
||||
</Label>
|
||||
|
||||
<fx:include source="SearchBar.fxml" fx:id="searchBar" />
|
||||
<fx:include source="LanguageFilter.fxml" fx:id="langFilter" />
|
||||
|
||||
<ListView fx:id="recipeList" />
|
||||
|
||||
<HBox spacing="10">
|
||||
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
|
||||
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
||||
<Button fx:id= "cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
|
||||
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView"/>
|
||||
<Button fx:id="cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
|
||||
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
|
||||
</HBox>
|
||||
|
||||
<Button fx:id="manageIngredientsButton"
|
||||
onAction="#openIngredientsPopup"
|
||||
text="Ingredients..." />
|
||||
|
||||
</VBox>
|
||||
</left>
|
||||
|
||||
|
|
|
|||
12
client/src/main/resources/client/scenes/LanguageFilter.fxml
Normal file
12
client/src/main/resources/client/scenes/LanguageFilter.fxml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.MenuButton?>
|
||||
|
||||
<MenuButton
|
||||
mnemonicParsing="false"
|
||||
text="Languages"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
xmlns="http://javafx.com/javafx/25"
|
||||
fx:controller="client.scenes.LanguageFilterCtrl"
|
||||
fx:id="langFilterMenu"
|
||||
/>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="client.scenes.recipe.IngredientsPopupCtrl"
|
||||
spacing="10" prefWidth="420" prefHeight="520">
|
||||
|
||||
<padding>
|
||||
<Insets top="12" right="12" bottom="12" left="12"/>
|
||||
</padding>
|
||||
|
||||
<Label text="Ingredients" style="-fx-font-size: 18px; -fx-font-weight: bold;"/>
|
||||
|
||||
<ListView fx:id="ingredientListView" VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<ButtonBar>
|
||||
<buttons>
|
||||
<Button text="Add" onAction="#addIngredient"/>
|
||||
<Button text="Refresh" onAction="#refresh"/>
|
||||
<Button text="Delete" onAction="#deleteSelected"/>
|
||||
<Button text="Close" onAction="#close"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
|
|
@ -27,6 +27,8 @@
|
|||
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
|
||||
</HBox>
|
||||
|
||||
<ComboBox fx:id="langSelector" onAction="#changeLanguage" />
|
||||
|
||||
<!-- Ingredients -->
|
||||
<fx:include source="IngredientList.fxml" fx:id="ingredientList"
|
||||
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ menu.button.edit=Edit
|
|||
menu.button.clone=Clone
|
||||
menu.button.print=Print recipe
|
||||
|
||||
menu.label.selected-langs=Languages
|
||||
|
||||
lang.en.display=English
|
||||
lang.nl.display=Dutch
|
||||
lang.pl.display=Polish
|
||||
|
|
@ -26,6 +26,9 @@ menu.button.clone=Clone
|
|||
menu.button.print=Print recipe
|
||||
|
||||
menu.search=Search...
|
||||
|
||||
menu.label.selected-langs=Languages
|
||||
|
||||
lang.en.display=English
|
||||
lang.nl.display=Nederlands
|
||||
lang.pl.display=Polski
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ menu.button.edit=Bewerken
|
|||
menu.button.clone=Dupliceren
|
||||
menu.button.print=Recept afdrukken
|
||||
|
||||
menu.label.selected-langs=Talen
|
||||
|
||||
menu.search=Zoeken...
|
||||
lang.en.display=English
|
||||
lang.nl.display=Nederlands
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ menu.button.clone=Duplikuj
|
|||
menu.button.print=Drukuj przepis
|
||||
|
||||
menu.search=Szukaj...
|
||||
|
||||
menu.label.selected-langs=J?zyki
|
||||
|
||||
lang.en.display=English
|
||||
lang.nl.display=Nederlands
|
||||
lang.pl.display=Polski
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class ServerUtilsTest {
|
|||
void tearDown() throws IOException, InterruptedException {
|
||||
// Not applicable in pipeline testing
|
||||
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
|
||||
dv.getRecipes().stream().map(Recipe::getId).forEach(id -> {
|
||||
dv.getRecipes(List.of()).stream().map(Recipe::getId).forEach(id -> {
|
||||
try {
|
||||
dv.deleteRecipe(id);
|
||||
} catch (Exception ex) {
|
||||
|
|
@ -77,7 +77,7 @@ class ServerUtilsTest {
|
|||
|
||||
@Test
|
||||
void getAllRecipesTest() throws IOException, InterruptedException {
|
||||
List<Recipe> recipes = dv.getRecipes();
|
||||
List<Recipe> recipes = dv.getRecipes(List.of());
|
||||
|
||||
assertNotNull(recipes, "The list should not be null");
|
||||
assertTrue(recipes.size() >= 0, "The list should be 0 (when no recipes), or more");
|
||||
|
|
|
|||
|
|
@ -1,96 +1,120 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.Config;
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.ServerUtils;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConfigServiceTest {
|
||||
static ServerUtils dv = new ServerUtils();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private static final long TEST_ID_A = 23412L;
|
||||
private static final long TEST_ID_B = 25412L;
|
||||
private Path configPath;
|
||||
private ConfigService configService;
|
||||
|
||||
@BeforeEach
|
||||
public void setup(){
|
||||
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
|
||||
void setUp() throws IOException {
|
||||
configPath = tempDir.resolve("configServiceTest.json");
|
||||
configService = new ConfigService(configPath);
|
||||
}
|
||||
@Test
|
||||
public void constructorTest(){
|
||||
assertEquals(configPath, configService.getConfigPath());
|
||||
assertNotNull(configService.getMapper());
|
||||
assertNotNull(configService.getConfig());
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Tests if the config file loads properly with a prewritten json file
|
||||
*/
|
||||
@Test
|
||||
public void configServiceFileLoadTest(@TempDir Path tempDir) throws IOException {
|
||||
Path configPath = tempDir.resolve("config.json");
|
||||
public void constructorFileDNETest(){
|
||||
Path nonExistentPath = tempDir.resolve("DNE.json");
|
||||
ConfigService newService = new ConfigService(nonExistentPath);
|
||||
|
||||
assertNotNull(newService.getConfig());
|
||||
assertEquals("en", newService.getConfig().getLanguage());
|
||||
assertEquals("http://localhost:8080", newService.getConfig().getServerUrl());
|
||||
assertTrue(newService.getConfig().getFavourites().isEmpty());
|
||||
assertTrue(newService.getConfig().getShoppingList().isEmpty());
|
||||
}
|
||||
@Test
|
||||
public void validJsonLoadTest() throws IOException {
|
||||
String json = """
|
||||
{
|
||||
"language": "de",
|
||||
"serverUrl": "http://exmple12.com",
|
||||
"favourites": [23412, 25412],
|
||||
"serverUrl": "http://example.com:8080",
|
||||
"favourites": [123, 456],
|
||||
"shoppingList": ["milk", "butter"]
|
||||
}
|
||||
""";
|
||||
Files.writeString(configPath, json); //writes into path
|
||||
ConfigService configService = new ConfigService(configPath);//initiates configservice
|
||||
|
||||
Config config = configService.getConfig(); //checks
|
||||
Files.writeString(configPath, json);
|
||||
ConfigService loadedService = new ConfigService(configPath);
|
||||
Config loadedConfig = loadedService.getConfig();
|
||||
|
||||
assertEquals("de", config.getLanguage());
|
||||
assertEquals("http://exmple12.com", config.getServerUrl());
|
||||
|
||||
List<Long> x = new ArrayList<>();
|
||||
|
||||
x.add(TEST_ID_A);
|
||||
x.add(TEST_ID_B);
|
||||
|
||||
List<String> y = new ArrayList<>();
|
||||
y.add("milk");
|
||||
y.add("butter");
|
||||
|
||||
assertEquals(x , config.getFavourites());
|
||||
assertEquals(y , config.getShoppingList());
|
||||
assertEquals("de", loadedConfig.getLanguage());
|
||||
assertEquals("http://example.com:8080", loadedConfig.getServerUrl());
|
||||
assertEquals(2, loadedConfig.getFavourites().size());
|
||||
assertTrue(loadedConfig.getFavourites().contains(123L));
|
||||
assertTrue(loadedConfig.getFavourites().contains(456L));
|
||||
assertEquals(2, loadedConfig.getShoppingList().size());
|
||||
assertTrue(loadedConfig.getShoppingList().contains("milk"));
|
||||
assertTrue(loadedConfig.getShoppingList().contains("butter"));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Tests if the save method saves changes to the config file.
|
||||
*/
|
||||
@Test
|
||||
public void configSaveTest(@TempDir Path tempDir) throws IOException {
|
||||
Path configPath = tempDir.resolve("config.json");
|
||||
ConfigService configService = new ConfigService(configPath);
|
||||
public void invalidJsonLoadTest() throws IOException {
|
||||
Files.writeString(configPath, "{ invalid json text");
|
||||
|
||||
Config config = configService.getConfig();
|
||||
assertThrows(RuntimeException.class, () -> {
|
||||
ConfigService corruptedService = new ConfigService(configPath);
|
||||
corruptedService.getConfig();
|
||||
});
|
||||
}
|
||||
@Test
|
||||
public void safeTest(){
|
||||
Config realConfig = new Config();
|
||||
|
||||
realConfig.setLanguage("de");
|
||||
realConfig.setServerUrl("http://example.com:8080");
|
||||
realConfig.addFavourite(1L);
|
||||
realConfig.addFavourite(2L);
|
||||
realConfig.setShoppingList(List.of("milk", "bread"));
|
||||
|
||||
config.setLanguage("fr");
|
||||
config.setServerUrl("www.domain1.com");
|
||||
|
||||
configService.setConfig(realConfig);
|
||||
configService.save();
|
||||
|
||||
String jsonTest = Files.readString(configPath);
|
||||
|
||||
assertTrue(jsonTest.contains("\"language\":\"fr\""));
|
||||
assertTrue(jsonTest.contains("\"serverUrl\":\"www.domain1.com\""));
|
||||
|
||||
ConfigService loadedService = new ConfigService(configPath);
|
||||
Config loadedConfig = loadedService.getConfig();
|
||||
|
||||
assertEquals("de", loadedConfig.getLanguage());
|
||||
assertEquals("http://example.com:8080", loadedConfig.getServerUrl());
|
||||
assertEquals(2, loadedConfig.getFavourites().size());
|
||||
assertTrue(loadedConfig.getFavourites().contains(1L));
|
||||
assertTrue(loadedConfig.getFavourites().contains(2L));
|
||||
assertEquals(2, loadedConfig.getShoppingList().size());
|
||||
assertTrue(loadedConfig.getShoppingList().contains("milk"));
|
||||
assertTrue(loadedConfig.getShoppingList().contains("bread"));
|
||||
}
|
||||
@Test
|
||||
public void multipleTimesSaveTest(){
|
||||
Config realConfig = new Config();
|
||||
|
||||
configService.setConfig(realConfig);
|
||||
configService.save();
|
||||
|
||||
realConfig.setLanguage("de");
|
||||
configService.save();
|
||||
realConfig.setServerUrl("http://example.com:8080");
|
||||
configService.save();
|
||||
|
||||
ConfigService loadedService = new ConfigService(configPath);
|
||||
Config loadedConfig = loadedService.getConfig();
|
||||
|
||||
assertEquals("de", loadedConfig.getLanguage());
|
||||
assertEquals("http://example.com:8080", loadedConfig.getServerUrl());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.Config;
|
||||
import client.utils.ServerUtils;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
|
@ -11,42 +9,64 @@ import java.util.ArrayList;
|
|||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConfigTest {
|
||||
static ServerUtils dv = new ServerUtils();
|
||||
private Config config;
|
||||
|
||||
private static final long FAV_LIST_ID_1 = 1234L;
|
||||
private static final long FAV_LIST_ID_2 = 1235L;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup(){
|
||||
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
|
||||
void setUp(){
|
||||
config = new Config();
|
||||
config.setLanguage("nl");
|
||||
config.setServerUrl("http://localhost:8081");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void configDefaultValueTest(){
|
||||
|
||||
Config config = new Config();
|
||||
assertEquals("en", config.getLanguage());
|
||||
assertEquals("http://localhost:8080", config.getServerUrl());
|
||||
assertEquals("nl", config.getLanguage());
|
||||
assertEquals("http://localhost:8081", config.getServerUrl());
|
||||
assertTrue(config.getFavourites().isEmpty());
|
||||
assertTrue(config.getShoppingList().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configGetterTest(){
|
||||
Config config = new Config();
|
||||
config.setLanguage("nl");
|
||||
config.setServerUrl("http://localhost:8081");
|
||||
ArrayList<String> x = new ArrayList<>();
|
||||
x.add("Lava Cake");
|
||||
x.add("Brownie");
|
||||
|
||||
ArrayList<Long> y = new ArrayList<>();
|
||||
y.add(FAV_LIST_ID_1);
|
||||
y.add(FAV_LIST_ID_2);
|
||||
config.setFavourites(y);
|
||||
|
||||
assertEquals(config.getFavourites(), y);
|
||||
config.setShoppingList(x);
|
||||
assertEquals(config.getShoppingList(), x);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFavTest(){
|
||||
assertFalse(config.isFavourite(FAV_LIST_ID_1)); //not yet fav
|
||||
|
||||
ArrayList<Long> y = new ArrayList<>();
|
||||
y.add(FAV_LIST_ID_1);
|
||||
config.setFavourites(y);
|
||||
|
||||
assertTrue(config.isFavourite(FAV_LIST_ID_1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeFavTest(){
|
||||
ArrayList<Long> y = new ArrayList<>();
|
||||
y.add(FAV_LIST_ID_1);
|
||||
config.setFavourites(y);
|
||||
|
||||
assertTrue(config.isFavourite(FAV_LIST_ID_1));
|
||||
|
||||
config.removeFavourite(FAV_LIST_ID_1);
|
||||
assertFalse(config.isFavourite(FAV_LIST_ID_1));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,9 @@ package client.scenes;
|
|||
|
||||
import client.utils.DefaultValueFactory;
|
||||
import client.utils.PrintExportService;
|
||||
import client.utils.ServerUtils;
|
||||
import commons.Recipe;
|
||||
import commons.RecipeIngredient;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
|
|
@ -19,22 +17,20 @@ import java.util.List;
|
|||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class PrintExportTest {
|
||||
static ServerUtils dv = new ServerUtils();
|
||||
@BeforeEach
|
||||
public void setup(){
|
||||
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
|
||||
}
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
public void buildRecipeTextTest(){
|
||||
List<RecipeIngredient> ingredients = new ArrayList<>();
|
||||
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Banana"));
|
||||
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Bread"));
|
||||
|
||||
final long testRecipeId = 1234L;
|
||||
List<String> preparationSteps = new ArrayList<>();
|
||||
preparationSteps.add("Mix Ingredients");
|
||||
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("""
|
||||
Title: Banana Bread
|
||||
|
|
@ -44,11 +40,8 @@ public class PrintExportTest {
|
|||
1: Mix Ingredients
|
||||
2: Heat in Oven
|
||||
""", PrintExportService.buildRecipeText(recipe1));
|
||||
|
||||
}
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
@Test
|
||||
public void validateFolderWithValidFolderTest(){
|
||||
assertDoesNotThrow(() -> PrintExportService.validateFolder(tempDir));
|
||||
|
|
@ -168,4 +161,27 @@ public class PrintExportTest {
|
|||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void succesExportTest() throws IOException {
|
||||
String data = "recipe data";
|
||||
String fileName = "succes.txt";
|
||||
Path filePath = tempDir.resolve(fileName);
|
||||
|
||||
PrintExportService.exportToFile(data,tempDir,fileName);
|
||||
|
||||
assertTrue(Files.exists(filePath));
|
||||
assertEquals(data, Files.readString(filePath));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failExportTest(){
|
||||
String data = "recipe data";
|
||||
String fileName = "succes.txt";
|
||||
Path filePath = tempDir.resolve("fail/failDir");
|
||||
|
||||
IllegalArgumentException i = assertThrows(IllegalArgumentException.class,
|
||||
()->PrintExportService.exportToFile(data,filePath,fileName));
|
||||
assertEquals("Folder does not exist", i.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,12 @@ public class Ingredient {
|
|||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
public long id;
|
||||
public Long id;
|
||||
|
||||
// FIXME Dec 22 2025::temporarily made this not a unique constraint because of weird JPA behaviour
|
||||
@Column(name = "name", nullable = false, unique = false)
|
||||
public String name;
|
||||
|
||||
|
||||
@Column(name = "protein", nullable = false)
|
||||
public double proteinPer100g;
|
||||
|
||||
|
|
@ -62,13 +61,15 @@ public class Ingredient {
|
|||
this.carbsPer100g = carbsPer100g;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public double kcalPer100g() {
|
||||
return proteinPer100g * KCAL_PER_GRAM_PROTEIN
|
||||
+ carbsPer100g * KCAL_PER_GRAM_CARBS
|
||||
+ fatPer100g * KCAL_PER_GRAM_FAT;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ public class Ingredient {
|
|||
return carbsPer100g;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +93,10 @@ public class Ingredient {
|
|||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ public class Recipe {
|
|||
@Column(name = "name", nullable = false, unique = true)
|
||||
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:
|
||||
// recipe_ingredients(recipe_id -> recipes(id), ingredient).
|
||||
// Example recipe_ingredients table:
|
||||
|
|
@ -95,11 +99,15 @@ public class Recipe {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
// TODO: Replace String with Embeddable Ingredient Class for ingredients
|
||||
public Recipe(Long id, String name, List<RecipeIngredient> ingredients, List<String> preparationSteps) {
|
||||
public Recipe(Long id,
|
||||
String name,
|
||||
String locale,
|
||||
List<RecipeIngredient> ingredients,
|
||||
List<String> preparationSteps) {
|
||||
// Not used by JPA/Spring
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.locale = locale;
|
||||
this.ingredients = ingredients;
|
||||
this.preparationSteps = preparationSteps;
|
||||
}
|
||||
|
|
@ -120,6 +128,14 @@ public class Recipe {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public void setLocale(String locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
// TODO: Replace String with Embeddable Ingredient Class
|
||||
public List<RecipeIngredient> getIngredients() {
|
||||
// Disallow modifying the returned list.
|
||||
|
|
@ -159,7 +175,7 @@ public class Recipe {
|
|||
@Override
|
||||
public String toString() {
|
||||
return "Recipe " + id +
|
||||
" - " + name +
|
||||
" - " + name + " (" + locale + ")" +
|
||||
": " + ingredients.size() + " ingredients / " +
|
||||
preparationSteps.size() + " steps";
|
||||
}
|
||||
|
|
@ -170,6 +186,7 @@ public class Recipe {
|
|||
return "Recipe{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", locale='" + locale + "'" +
|
||||
", ingredients=" + ingredients +
|
||||
", preparationSteps=" + preparationSteps +
|
||||
'}';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package commons;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class IngredientTest {
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ class IngredientTest {
|
|||
assertEquals(ZERO, sugar.proteinPer100g);
|
||||
assertEquals(ZERO, sugar.fatPer100g);
|
||||
assertEquals(HUNDRED, sugar.carbsPer100g);
|
||||
assertEquals(0L, sugar.id);
|
||||
assertNull(sugar.id);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
146
docs/agenda/agenda-07.md
Normal file
146
docs/agenda/agenda-07.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Week 7 meeting agenda
|
||||
|
||||
| Key | Value |
|
||||
| ------------ |----------------------------------------------------------------------------------------------------------|
|
||||
| Date | Jan 9th 2026 |
|
||||
| Time | 16:45 |
|
||||
| Location | DW PC Hall 2 |
|
||||
| Chair | Mei Chang van der Werff |
|
||||
| Minute Taker | Aysegul Aydinlik |
|
||||
| Attendees | Natalia Cholewa,Oskar Rasienski, Rithvik Sriram, Aysegul Aydinlik, Steven Liu, Mei Chang van der Werff |
|
||||
## Table of contents
|
||||
### Opening
|
||||
|
||||
1. (1 min) Introduction by chair
|
||||
> Meeting starts at 16:48
|
||||
|
||||
2. (1 min) Any additions to the agenda?
|
||||
> None
|
||||
|
||||
3. (1-2 min) TA announcements?
|
||||
> - Technology feedback has been graded
|
||||
> - Implemented features to be graded this weekend
|
||||
> - For the final evaluation we need to have the basic requirements otherwise it won't get checked at all
|
||||
> - We can and should update read.me
|
||||
> - We get to see our LOC next week
|
||||
|
||||
### Feedback/assignments
|
||||
4. (3 min) Go over TA feedback given (technology)
|
||||
- Do we have any questions about the feedback?
|
||||
> **Technology feedback**
|
||||
> - Server and business logic and controller logic should be seperated
|
||||
> - Avoid hardcoding; move logic into server utilities where appropriate
|
||||
5. (3 min) Go over the implemented feature assignment due today
|
||||
> - 4.1 Printing not finished
|
||||
> - Meaningfull additions:
|
||||
> - Extra language support
|
||||
> - Text-to-voice functionality
|
||||
|
||||
### Current progress
|
||||
6. How far are we in terms of completion?
|
||||
* (5 min) Progress check in terms of feature/issue completion
|
||||
- What have we completed/going to complete this week?
|
||||
> - 4.1 not finished
|
||||
> - 4.2 done
|
||||
> - 4.3 requires significant work
|
||||
- Who implemented what, this week?
|
||||
> - Natalia: refactored server-side, recipe language handling
|
||||
> - Steven: refactoring, fixing application compatibility with intelliJ
|
||||
> - Aysegul: Ingredient list view + ingredient usage
|
||||
> - Oskar: Refactoring
|
||||
> - Rithvik: Fixed recipe detail UI sizing, implemented server changed, printing
|
||||
> - Mei Chang: Client-side testing, hardcoded value fixes, refactoring
|
||||
* (3 min) Small Showcase of the product
|
||||
> - small showcase of the favourite button
|
||||
|
||||
### Planning
|
||||
7. (15 min) Planning week 8
|
||||
- What needs to be done? (provisionary tasks below)
|
||||
- Handle Backend-Server code delegation
|
||||
- How do we do it?
|
||||
- Who does what?
|
||||
|
||||
|
||||
* (2 min) Designate teams.
|
||||
* (1 min) Who is the next chair and minute taker?
|
||||
> **Planning week 8**
|
||||
> - resolve unfinished issues from previous weeks
|
||||
> - improve server utilities and dependency injection
|
||||
> - No shopping list implementation planned
|
||||
> - Nutrition logic should be implemented
|
||||
>
|
||||
> **SERVER**
|
||||
>
|
||||
> Mei Chang and Aysegul
|
||||
>
|
||||
> **BACKEND**
|
||||
>
|
||||
> Oskar, Natalia, Rithvik, Steven
|
||||
>
|
||||
> **Task Distribution**
|
||||
>
|
||||
> Aysegul: Server Testing + Additional server tasks
|
||||
>
|
||||
> Mei Chang: Nutrition logic & serving logic + server testing
|
||||
>
|
||||
> Oskar: Figuring out Mockito
|
||||
>
|
||||
> Rithvik: Printing functionality
|
||||
>
|
||||
> Steven: UI nutrition + dropdowns ingredients
|
||||
>
|
||||
> Natalia: Refactoring
|
||||
|
||||
Next chair and minute taker
|
||||
> Aysegul will be the next Chair
|
||||
>
|
||||
> Natalia will be the next Minute taker
|
||||
|
||||
### Closing
|
||||
8. (2 min) Questions?
|
||||
> -Emphasis on separation, refactoring, and maintainability
|
||||
9. (2 min) Summarize everything
|
||||
> - Focus next week: finishing 4.1 and 4.3
|
||||
>
|
||||
> - Server refactoring and nutrition logic are priorities
|
||||
>
|
||||
> - 2 members must still fulfill server LOC requirements
|
||||
>
|
||||
> - README must clearly reflect extensions and improvementsa
|
||||
|
||||
10. (1 min) TA remarks
|
||||
|
||||
> TA will check implemented Features on sunday
|
||||
>
|
||||
> meeting ends at 17:32
|
||||
>
|
||||
> actual meeting time: 44 minutes
|
||||
|
||||
Estimated meeting time: ~40 minutes
|
||||
|
||||
Provisionary tasks (updated)
|
||||
4.1
|
||||
- Connect the printing functionality to the button, rn the button does nothing
|
||||
|
||||
4.2
|
||||
-removing the refresh button and method, since it's automatic? (optional)
|
||||
- fix: not being able to make a empty title for recipe name
|
||||
|
||||
4.3
|
||||
- Implement fats/protein/carbs/kcal
|
||||
- Being able to select ingredients from a dropdown menu
|
||||
- Actually receive the delete recipe warning
|
||||
- Serving implementation
|
||||
- Informal unit fix (Steven)
|
||||
|
||||
4.4
|
||||
- Make the search bar functionality
|
||||
- Get the favourite warning message
|
||||
|
||||
|
||||
4.5 (old)
|
||||
Assuming that we're going to start with the shopping cart
|
||||
- Create shopping list
|
||||
- reset shopping list (backend)
|
||||
|
||||
- Issues/task that weren't finished this week
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# Week 7 meeting agenda
|
||||
|
||||
| Key | Value |
|
||||
| ------------ |-----------------------------------------------------------------------------------------|
|
||||
| Date | Jan 9th 2026 |
|
||||
| Time | 14:45 |
|
||||
| Location | DW PC Hall 2 |
|
||||
| Chair | Mei Chang van der Werff |
|
||||
| Minute Taker | Aysegul Aydinlik |
|
||||
| Attendees | Natalia Cholewa, Rithvik Sriram, Aysegul Aydinlik, Steven Liu, Mei Chang van der Werff |
|
||||
## Table of contents
|
||||
### Opening
|
||||
|
||||
1. (1 min) Introduction by chair
|
||||
|
||||
2. (1 min) Any additions to the agenda?
|
||||
3. (1-2 min) TA announcements?
|
||||
|
||||
### Feedback/assignments
|
||||
4. (3 min) Go over TA feedback given (technology)
|
||||
- Do we have any questions about the feedback?
|
||||
5. (3 min) Go over the implemented feature assignment due today
|
||||
|
||||
### Current progress
|
||||
6. How far are we in terms of completion?
|
||||
* (5 min) Progress check in terms of feature/issue completion
|
||||
- What have we completed/going to complete this week?
|
||||
- Who implemented what, this week?
|
||||
* (3 min) Small Showcase of the product
|
||||
|
||||
### Planning
|
||||
7. (15 min) Planning week 8
|
||||
- What needs to be done? (provisionary tasks below)
|
||||
- Handle Backend-Server code delegation
|
||||
- How do we do it?
|
||||
- Who does what?
|
||||
|
||||
|
||||
* (2 min) Designate teams.
|
||||
* (1 min) Who is the next chair and minute taker?
|
||||
|
||||
### Closing
|
||||
8. (2 min) Questions?
|
||||
9. (2 min) Summarize everything
|
||||
|
||||
10. (1 min) TA remarks
|
||||
Estimated meeting time: ~40 minutes
|
||||
|
||||
Provisionary tasks (updated)
|
||||
4.1
|
||||
- Connect the printing functionality to the button, rn the button does nothing
|
||||
|
||||
4.2
|
||||
-removing the refresh button and method, since it's automatic? (optional)
|
||||
- fix: not being able to make a empty title for recipe name
|
||||
|
||||
4.3
|
||||
- Implement fats/protein/carbs/kcal
|
||||
- Being able to select ingredients from a dropdown menu
|
||||
- Actually receive the delete recipe warning
|
||||
- Serving implementation
|
||||
- Informal unit fix (Steven)
|
||||
|
||||
4.4
|
||||
- Make the search bar functionality
|
||||
- Get the favourite warning message
|
||||
|
||||
|
||||
4.5 (old)
|
||||
Assuming that we're going to start with the shopping cart
|
||||
- Create shopping list
|
||||
- reset shopping list (backend)
|
||||
|
||||
- Issues/task that weren't finished this week
|
||||
73
docs/feedback/week-07.md
Normal file
73
docs/feedback/week-07.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
|
||||
# Meeting feedback
|
||||
|
||||
**Quick Summary Scale**
|
||||
|
||||
Insufficient/Sufficient/Good/Excellent
|
||||
|
||||
|
||||
#### Agenda
|
||||
|
||||
|
||||
Feedback: **Excellent**
|
||||
|
||||
- The agenda was uploaded on time
|
||||
- The agenda is formatted according to the template.
|
||||
- The individual points are clear.
|
||||
- I appreciate that you used separate sections for your ideas (e.g. assignments, current progress and planning).
|
||||
|
||||
|
||||
#### Performance of the Previous Minute Taker
|
||||
|
||||
Feedback: **Excellent**
|
||||
|
||||
- The notes have been merged into the agenda file.
|
||||
- Notes are clear and grouped based on the agenda point. The formatting makes the notes clearer.
|
||||
- The agreements are clear and realistic.
|
||||
- I like that you included what everyone completed up to that point.
|
||||
- Everyone was assigned to a task. I understand you do a proper task distribution separate from the TA meeting.
|
||||
- I also appreciated that you added all questions that arose during the meeting.
|
||||
|
||||
#### Chair performance
|
||||
|
||||
Feedback: **Excellent**
|
||||
|
||||
- I liked how you started the meeting and checked in with everyone.
|
||||
- You followed all points from the agenda and everyone followed along.
|
||||
- You initiated all points and ensured everyone was heard, including members who joined online.
|
||||
- I appreciated that you asked everyone for feedback or other remarks.
|
||||
- You also pointed out what the team should focus more in the following week (e.g fixing bugs)
|
||||
- Time estimates seemed accurate.
|
||||
|
||||
|
||||
#### Attitude & Relation
|
||||
|
||||
Feedback: **Excellent**
|
||||
|
||||
|
||||
- It was a bit difficult to engage everyone that was also online, but you did a great job overall.
|
||||
- Everyone took ownership of the meeting.
|
||||
- The atmosphere was constructive and positive.
|
||||
- All ideas were listened to and considered by all team members.
|
||||
- Everyone was active in the meeting and sharing ideas.
|
||||
|
||||
|
||||
#### Potentially Shippable Product
|
||||
|
||||
Feedback: **Excellent**
|
||||
|
||||
|
||||
- The team presented the current state of the application.
|
||||
- The basic requirements are completed and most extensions show good progress. Make sure to not miss any requirements for the extensions you already started.
|
||||
- Progress has been made compared to the last meeting.
|
||||
- If you maintain the same workflow, I believe you can complete all extensions.
|
||||
|
||||
|
||||
#### Work Contribution/Distribution in the Team
|
||||
|
||||
Feedback: **Excellent**
|
||||
|
||||
- Everyone explained what they completed since the last meeting.
|
||||
- It is good that you are aware of the current issues and you are working on them.
|
||||
- Everyone reached their goal. Well done!
|
||||
- Everyone contributes to the project.
|
||||
|
|
@ -6,6 +6,7 @@ import commons.ws.Topics;
|
|||
import commons.ws.messages.CreateIngredientMessage;
|
||||
import commons.ws.messages.DeleteIngredientMessage;
|
||||
import commons.ws.messages.UpdateIngredientMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
|
|
@ -115,6 +116,8 @@ public class IngredientController {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update an existing ingredient by its ID.
|
||||
* Maps to <code>PATCH /api/ingredients/{id}</code>
|
||||
|
|
@ -166,18 +169,26 @@ public class IngredientController {
|
|||
*/
|
||||
@PostMapping("/ingredients")
|
||||
public ResponseEntity<Ingredient> createIngredient(@RequestBody Ingredient ingredient) {
|
||||
if (ingredient.name == null || ingredient.name.isEmpty()) {
|
||||
if (ingredient == null
|
||||
|| ingredient.name == null
|
||||
|| ingredient.name.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
return ingredientService.create(ingredient)
|
||||
.map(saved -> {
|
||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new CreateIngredientMessage(saved));
|
||||
messagingTemplate.convertAndSend(
|
||||
Topics.INGREDIENTS,
|
||||
new CreateIngredientMessage(saved)
|
||||
);
|
||||
return ResponseEntity.ok(saved);
|
||||
})
|
||||
.orElseGet(() -> ResponseEntity.badRequest().build());
|
||||
.orElseGet(() -> ResponseEntity.status
|
||||
(HttpStatus.CONFLICT).build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an ingredient by its ID.
|
||||
* Maps to <code>DELETE /api/ingredients/{id}</code>
|
||||
|
|
@ -194,13 +205,24 @@ public class IngredientController {
|
|||
*/
|
||||
@DeleteMapping("/ingredients/{id}")
|
||||
public ResponseEntity<Boolean> deleteIngredient(@PathVariable Long id) {
|
||||
//check if the ingredient is used in any recipe
|
||||
long usageCount = ingredientService.countUsage(id);
|
||||
|
||||
if (usageCount > 0) {
|
||||
// If used in recipes, return a warning response (HTTP 400)
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(false); // Ingredient in use, don't delete
|
||||
}
|
||||
|
||||
// delete if the ingredient is not in use
|
||||
if (!ingredientService.delete(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new DeleteIngredientMessage(id));
|
||||
return ResponseEntity.ok(true);
|
||||
return ResponseEntity.ok(true); // deleted~
|
||||
}
|
||||
|
||||
|
||||
public record IngredientUsageResponse(Long ingredientId, long usedInRecipes){}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import server.database.RecipeRepository;
|
||||
import server.service.RecipeService;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -29,9 +30,11 @@ import java.util.logging.Logger;
|
|||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class RecipeController {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(RecipeController.class.getName());
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final RecipeService recipeService;
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
private RecipeService recipeService;
|
||||
private RecipeRepository recipeRepository;
|
||||
|
||||
public RecipeController(RecipeService recipeService, SimpMessagingTemplate messagingTemplate) {
|
||||
this.recipeService = recipeService;
|
||||
|
|
@ -56,19 +59,35 @@ public class RecipeController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Mapping for <code>GET /recipes(?limit=)</code>
|
||||
* Mapping for <code>GET /recipes(?limit=)(&locales=)</code>
|
||||
* <p>
|
||||
* If the limit parameter is unspecified, return all recipes in the repository.
|
||||
* @param limit Integer limit of items you want to get
|
||||
* @return The list of 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.");
|
||||
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.
|
||||
limit.map(recipeService::findAll).orElseGet(recipeService::findAll)
|
||||
);
|
||||
() -> limit.map(recipeService::findAll).orElseGet(recipeService::findAll));
|
||||
|
||||
|
||||
return ResponseEntity.ok(recipes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -128,4 +147,42 @@ public class RecipeController {
|
|||
messagingTemplate.convertAndSend(Topics.RECIPES, new DeleteRecipeMessage(id)); // Send to WS.
|
||||
return ResponseEntity.ok(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a search based on a case-insensitive partial match on
|
||||
* Recipe name and limits the result to a set amount of results.
|
||||
* @param search - name of the recipe to be searched for.
|
||||
* @param limit - limit of the results queried for.
|
||||
* @param lang - stores the info of the language of the user to provide server support/
|
||||
* @return - returns a ResponseEntity with a List of Recipes and an HTTP 200 ok status.
|
||||
*/
|
||||
public ResponseEntity<List<Recipe>> getRecipes(
|
||||
@RequestParam Optional<String> search,
|
||||
@RequestParam Optional<Integer> limit,
|
||||
@RequestParam Optional<String> lang){
|
||||
|
||||
List<Recipe> recipes = recipeRepository.findAll();
|
||||
|
||||
List<Recipe> finalRecipes = recipes;
|
||||
recipes = search
|
||||
.filter(s -> !s.trim().isEmpty()) // filters recipes if the string is not empty by doing a lowercase search
|
||||
.map(s -> {
|
||||
String lowercaseSearch = s.toLowerCase();
|
||||
return finalRecipes.stream()
|
||||
.filter(recipe ->
|
||||
recipe.getName().toLowerCase().contains(lowercaseSearch)
|
||||
)
|
||||
.toList();
|
||||
})
|
||||
.orElse(recipes);
|
||||
recipes = limit // filters based on limit if provided
|
||||
.filter(l -> l < finalRecipes.size())
|
||||
.map(l -> finalRecipes.stream().limit(l).toList())
|
||||
.orElse(recipes);
|
||||
return ResponseEntity.ok(recipes);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||
|
||||
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> {
|
||||
boolean existsByName(String name);
|
||||
|
||||
List<Recipe> findAllByLocaleIsIn(Collection<String> locales);
|
||||
Page<Recipe> findAllByLocaleIsIn(Collection<String> locales, Pageable pageable);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package server.service;
|
||||
|
||||
import commons.Ingredient;
|
||||
import commons.Recipe;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -7,6 +8,7 @@ import server.database.IngredientRepository;
|
|||
import server.database.RecipeIngredientRepository;
|
||||
import server.database.RecipeRepository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
|
@ -36,6 +38,14 @@ public class RecipeService {
|
|||
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.
|
||||
* @param recipe Recipe to be saved in the db.
|
||||
|
|
@ -79,8 +89,11 @@ public class RecipeService {
|
|||
recipeIngredient.setIngredient(
|
||||
ingredientRepository
|
||||
.findByName(recipeIngredient.getIngredient().name)
|
||||
.orElseGet(() -> ingredientRepository.save(recipeIngredient.getIngredient())
|
||||
))
|
||||
.orElseGet(() -> {
|
||||
Ingredient i = recipeIngredient.getIngredient();
|
||||
i.setId(null);
|
||||
return ingredientRepository.save(i);
|
||||
}))
|
||||
);
|
||||
recipeIngredientRepository.saveAll(recipe.getIngredients());
|
||||
return recipeRepository.save(recipe);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ public class RecipeControllerTest {
|
|||
public void setup(TestInfo info) {
|
||||
recipes = LongStream
|
||||
.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();
|
||||
controller = new RecipeController(
|
||||
recipeService,
|
||||
|
|
@ -107,7 +107,7 @@ public class RecipeControllerTest {
|
|||
@Tag("test-from-init-data")
|
||||
public void getManyRecipes() {
|
||||
// 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
|
||||
|
|
@ -115,7 +115,29 @@ public class RecipeControllerTest {
|
|||
public void getSomeRecipes() {
|
||||
final int LIMIT = 5;
|
||||
// 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue