diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml
index 6d33851e..7fbcc1f8 100644
--- a/.github/workflows/backend_test.yml
+++ b/.github/workflows/backend_test.yml
@@ -3,30 +3,23 @@ name: backend-test
on:
push:
paths:
- - 'backend/**'
+ - "backend/**"
- compose.yml
- dev.yml
- - '.github/workflows/backend_test.yml'
-
+ - ".github/workflows/backend_test.yml"
pull_request:
branches:
- main
- staging
- - develop
-
jobs:
test:
runs-on: ubuntu-latest
env:
- working-directory:
- backend
+ working-directory: backend
steps:
- name: Checkout
uses: actions/checkout@v4
-
+
- name: Test with Maven
run: docker compose -f dev.yml run --rm backend mvn test
-
-
-
diff --git a/.github/workflows/frontend_test.yml b/.github/workflows/frontend_test.yml
index b57d72a0..79b24a57 100644
--- a/.github/workflows/frontend_test.yml
+++ b/.github/workflows/frontend_test.yml
@@ -1,6 +1,14 @@
name: Front-End CI
-on: [push, pull_request]
+on:
+ push:
+ paths:
+ - "frontend/**"
+ - ".github/workflows/frontend_test.yml"
+ pull_request:
+ branches:
+ - main
+ - staging
jobs:
build:
@@ -15,8 +23,8 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v2
with:
- node-version: '20'
- cache: 'yarn'
+ node-version: "20"
+ cache: "yarn"
- name: Enable Corepack
run: corepack enable
diff --git a/backend/pom.xml b/backend/pom.xml
index 470caae3..81791029 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -21,6 +21,18 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.mockito
+ mockito-core
+ 5.2.0
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.2.0
+ test
+
org.apache.jena
apache-jena-libs
diff --git a/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java b/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java
index 653b2358..7ee00e81 100644
--- a/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java
+++ b/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java
@@ -45,6 +45,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http)
.hasRole("ADMIN") // Require ADMIN role for "/api/v1/resources"
.requestMatchers(HttpMethod.POST,"/**")
.authenticated()
+ .requestMatchers("/feed?type=following").authenticated()
.requestMatchers(HttpMethod.DELETE,"/**").authenticated()) // Require authentication for all other requests
.sessionManagement(
manager ->
diff --git a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java
index 0ffe909b..777b70ad 100644
--- a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java
+++ b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java
@@ -6,6 +6,7 @@
import com.group1.cuisines.dao.response.AuthenticationTokenResponse;
import com.group1.cuisines.services.AuthenticationService;
import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -23,13 +24,27 @@ public class AuthenticationController {
public ResponseEntity> signup(
@RequestBody SignUpRequest request
) {
- return ResponseEntity.ok(authenticationService.signup(request)); // Return response
+ ApiResponse response = authenticationService.signup(request);
+
+ if (response.getStatus() == 409) {
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
+ } else if (response.getStatus() == 400) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
+ } else {
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ }
}
@PostMapping("/login") // Sign in endpoint
public ResponseEntity> signin(
@RequestBody SignInRequest request
) {
- return ResponseEntity.ok(authenticationService.signin(request)); // Return response
+ ApiResponse response = authenticationService.signin(request);
+
+ if (response.getStatus() == 401) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
+ } else {
+ return ResponseEntity.ok(response);
+ }
}
}
diff --git a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java
index 996df87f..eadf6d76 100644
--- a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java
+++ b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java
@@ -1,11 +1,16 @@
package com.group1.cuisines.controllers;
import com.group1.cuisines.dao.response.ApiResponse;
+import com.group1.cuisines.dao.response.ErrorResponse;
+import com.group1.cuisines.dao.response.SuccessResponse;
+import com.group1.cuisines.dto.CuisineDetailsDto;
import com.group1.cuisines.entities.Cuisine;
import com.group1.cuisines.repositories.CuisineRepository;
import com.group1.cuisines.services.CuisineService;
+import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -21,14 +26,12 @@ public class CuisineController {
private final CuisineRepository cuisineRepository;
@GetMapping("/{cuisineId}")
- public ResponseEntity> getCuisineDetails(
- @PathVariable String cuisineId,
- @RequestParam(defaultValue = "false") boolean includeDishes) {
-
- Cuisine cuisine = cuisineService.getCuisineById(cuisineId, includeDishes);
- if (cuisine == null) {
- return ResponseEntity.notFound().build();
+ public ResponseEntity> getCuisineById(@PathVariable String cuisineId, @RequestParam(required = false) Boolean includeDishes) {
+ try {
+ CuisineDetailsDto cuisineDetails = cuisineService.getCuisineById(cuisineId, Boolean.TRUE.equals(includeDishes));
+ return ResponseEntity.ok(new SuccessResponse<>(200,cuisineDetails, "Cuisine details fetched successfully"));
+ } catch (EntityNotFoundException e) {
+ return ResponseEntity.ok(new ErrorResponse(204,"Cuisine not found"));
}
- return ResponseEntity.ok(cuisine);
}
}
diff --git a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java
new file mode 100644
index 00000000..d4fdc09b
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java
@@ -0,0 +1,45 @@
+package com.group1.cuisines.controllers;
+
+import com.group1.cuisines.dao.response.SuccessResponse;
+import com.group1.cuisines.dto.RecipeDetailsDto;
+import com.group1.cuisines.services.RecipeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v1")
+public class FeedController {
+
+ @Autowired
+ private RecipeService recipeService;
+
+ @GetMapping("/feed")
+ public ResponseEntity> getFeed(@RequestParam String type, Authentication authentication) {
+ if (!"explore".equals(type) && !"following".equals(type)) {
+ return ResponseEntity.badRequest().body("Invalid type parameter.");
+ }
+
+ if ("following".equals(type)) {
+ if (authentication == null || !authentication.isAuthenticated()) {
+ // Return an empty set and a message for unauthenticated users
+ return ResponseEntity.ok(new SuccessResponse<>(400,Collections.emptyList(), "No content available. Please log in and follow other users !."));
+ }
+ // Fetch following users' recipes for authenticated users
+ String username = authentication.getName();
+ List recipes = recipeService.getRecipesByType(type, username);
+ return ResponseEntity.ok(new SuccessResponse<>(200,recipes, "Recipes fetched successfully from followed users."));
+ }
+
+ // For 'explore', accessible to everyone
+ List recipes = recipeService.getRecipesByType(type, null);
+ return ResponseEntity.ok(new SuccessResponse<>(200,recipes, "Recipes fetched successfully."));
+ }
+}
diff --git a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java
index 71804121..814ec4d0 100644
--- a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java
+++ b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java
@@ -1,8 +1,6 @@
package com.group1.cuisines.controllers;
-import com.group1.cuisines.dto.CommentsDto;
-import com.group1.cuisines.dto.NewRecipeDto;
-import com.group1.cuisines.dto.RatingDto;
-import com.group1.cuisines.dto.RecipeDetailDto;
+import com.group1.cuisines.dao.response.SuccessResponse;
+import com.group1.cuisines.dto.*;
import com.group1.cuisines.entities.Comment;
import com.group1.cuisines.entities.User;
import com.group1.cuisines.services.RecipeService;
@@ -23,6 +21,28 @@ public class RecipeController {
@Autowired
private RecipeService recipeService;
+
+ @GetMapping("/recipes/{recipeId}")
+ public ResponseEntity> getRecipeById(@PathVariable Integer recipeId) {
+ RecipeDetailsDto recipeDetails = recipeService.getRecipeById(recipeId);
+ if (recipeDetails != null) {
+ return ResponseEntity.ok(new SuccessResponse<>(200,recipeDetails, "Recipe fetched successfully"));
+ } else {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Recipe not found");
+ }
+ }
+
+
+ @GetMapping("/recipes")
+ public ResponseEntity> getRecipes(@RequestParam(required = false) String sort,
+ @RequestParam(required = false) String dishId,
+ @RequestParam(required = false) String cuisineId) {
+ List recipes = recipeService.findRecipes(sort, dishId, cuisineId);
+ return ResponseEntity.ok(recipes);
+ }
+
+
+
@PostMapping("/recipes")
public ResponseEntity> createRecipe(@RequestBody NewRecipeDto newRecipe) throws Exception{
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
diff --git a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java
index 05799f48..1d53a486 100644
--- a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java
+++ b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java
@@ -1,6 +1,10 @@
package com.group1.cuisines.controllers;
import com.group1.cuisines.dao.response.ApiResponse;
+import com.group1.cuisines.dao.response.ErrorResponse;
+import com.group1.cuisines.dao.response.SuccessResponse;
+import com.group1.cuisines.dto.DishResponseDto;
+import com.group1.cuisines.dto.UserDto;
import com.group1.cuisines.entities.Dish;
import com.group1.cuisines.entities.User;
import com.group1.cuisines.services.SearchService;
@@ -8,6 +12,7 @@
import com.group1.cuisines.services.WikidataService;
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@@ -28,15 +33,24 @@ public ResponseEntity> searchUsers(@RequestParam(required = false) String q) {
List users = userService.searchUsers(q);
if (users.isEmpty()) {
// Return a custom message with a "No Content" status when no users are found
- return ResponseEntity.status(HttpStatus.NO_CONTENT).body("No users found");
+ return ResponseEntity.ok(new ErrorResponse(204,"No users found"));
}
- return ResponseEntity.ok(users); // Return the list of users when found
+ return ResponseEntity.ok(new SuccessResponse<>(200, users.stream().map(user -> new UserDto(
+ user.getId(),
+ user.getUsername(),
+ user.getFirstName(),
+ user.getLastName(),
+ user.getFollowerCount(),
+ user.getFollowingCount(),
+ user.getRecipeCount()
+ )).collect(Collectors.toList()), "Users fetched successfully"));
+
}
@GetMapping("/dishes")
- public ApiResponse> searchDishes(@RequestParam(required = false) String q,
- @RequestParam(required = false) String cuisine,
- @RequestParam(required = false) String foodType) {
+ public ApiResponse> searchDishes(@RequestParam(required = false) String q,
+ @RequestParam(required = false) String cuisine,
+ @RequestParam(required = false) String foodType) {
return new ApiResponse<>(
200,
"Search completed",
diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java
index c5a9d711..a7b6a138 100644
--- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java
+++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java
@@ -1,12 +1,17 @@
package com.group1.cuisines.controllers;
+import com.group1.cuisines.dao.response.ErrorResponse;
+import com.group1.cuisines.dao.response.SuccessResponse;
import com.group1.cuisines.dto.UserDto;
+import com.group1.cuisines.dto.UserProfileDto;
import com.group1.cuisines.entities.User;
import com.group1.cuisines.repositories.UserRepository;
import com.group1.cuisines.services.UserService;
+import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -25,6 +30,18 @@ public class UserController {
private final UserService userService;
private final UserRepository userRepository;
+
+ @GetMapping("/{userId}")
+ public ResponseEntity> getUserById(@PathVariable Integer userId, Authentication authentication) {
+ String currentUsername = authentication != null ? authentication.getName() : null;
+ try {
+ UserProfileDto userProfile = userService.getUserProfileById(userId, currentUsername);
+ return ResponseEntity.ok(new SuccessResponse<>(200,userProfile, "User profile fetched successfully"));
+ } catch (EntityNotFoundException e) {
+ return ResponseEntity.ok(new ErrorResponse(204,"User not found"));
+ }
+ }
+
@GetMapping("/me")
public ResponseEntity> getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
if (userDetails instanceof com.group1.cuisines.entities.User) {
@@ -41,38 +58,47 @@ public ResponseEntity> getUserDetails(@AuthenticationPrincipal UserDetails use
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated");
}
- @PostMapping("/{userId}/follow")
- public ResponseEntity> followUser(@PathVariable Integer userId) {
+ @PostMapping("/follow")
+ public ResponseEntity> followUser(@RequestBody Map requestBody) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication.getPrincipal()=="anonymousUser"){
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required.");
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(401,"Authentication required") );
+
}
String username = authentication.getName();
Integer followerId = userRepository.findUserIdByUsername(username);
+ Integer userId = requestBody.get("userId");
if (followerId == null || userId == null) {
- return ResponseEntity.badRequest().body("Invalid user data");
+
+ return ResponseEntity.ok(new ErrorResponse(204,"Invalid user data"));
}
boolean result = userService.followUser(userId, followerId);
if (!result) {
- return ResponseEntity.status(HttpStatus.ALREADY_REPORTED).body("Already following");
+
+ return ResponseEntity.ok(new ErrorResponse(209,"Already following"));
}
- return ResponseEntity.ok().body("Followed successfully");
+
+ return ResponseEntity.ok(new SuccessResponse<>(200,null,"Followed successfully"));
}
@GetMapping("/{userId}/following")
public ResponseEntity> getUserFollowing(@PathVariable Integer userId) {
// Validate the provided user ID
if (userId == null) {
- return ResponseEntity.badRequest().body("Invalid user ID provided");
+
+ return ResponseEntity.ok(new ErrorResponse(204,"Invalid user ID provided") );
+ }
+ if(userRepository.findById(userId).isEmpty()){
+ return ResponseEntity.ok(new ErrorResponse(204,"User not found") );
}
Set following = userService.getUserFollowing(userId);
if (following.isEmpty()) {
- return ResponseEntity.ok().body("User is not following anyone");
+ return ResponseEntity.ok(new ErrorResponse(204,"User is not following anyone"));
} else {
Set followingDto = following.stream()
.map(user -> UserDto.builder()
@@ -85,18 +111,22 @@ public ResponseEntity> getUserFollowing(@PathVariable Integer userId) {
.recipeCount(user.getRecipeCount())
.build())
.collect(Collectors.toSet());
- return ResponseEntity.ok().body(followingDto);
+ return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User following fetched successfully"));
}
}
@GetMapping("/{userId}/followers")
public ResponseEntity> getUserFollowers(@PathVariable Integer userId) {
// Validate the provided user ID
if (userId == null) {
- return ResponseEntity.badRequest().body("Invalid user ID provided");
+
+ return ResponseEntity.ok(new ErrorResponse(204,"Invalid user ID provided") );
+ }
+ if(userRepository.findById(userId).isEmpty()){
+ return ResponseEntity.ok(new ErrorResponse(204,"User not found") );
}
Set followers = userService.getUserFollower(userId);
if (followers.isEmpty()) {
- return ResponseEntity.ok().body("User is not followed by anyone");
+ return ResponseEntity.ok(new ErrorResponse(204,"User is not followed by anyone"));
} else {
Set followingDto = followers.stream()
.map(user -> UserDto.builder()
@@ -109,29 +139,31 @@ public ResponseEntity> getUserFollowers(@PathVariable Integer userId) {
.recipeCount(user.getRecipeCount())
.build())
.collect(Collectors.toSet());
- return ResponseEntity.ok().body(followingDto);
+ return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User followers fetched successfully"));
}
}
- @PostMapping("/{userId}/unfollow")
- public ResponseEntity> unfollowUser(@PathVariable Integer userId) {
+ @PostMapping("/unfollow")
+ public ResponseEntity> unfollowUser(@RequestBody Map requestBody) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication.getPrincipal()=="anonymousUser"){
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required.");
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(401,"Authentication required") );
}
String username = authentication.getName();
Integer followerId = userRepository.findUserIdByUsername(username);
+ Integer userId = requestBody.get("userId");
if (followerId == null || userId == null) {
- return ResponseEntity.badRequest().body("Invalid user data.");
+ return ResponseEntity.ok(new ErrorResponse(204,"Invalid user data"));
}
boolean result = userService.unfollowUser(userId, followerId);
if (!result) {
- return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Follow relationship does not exist.");
+
+ return ResponseEntity.ok(new ErrorResponse(209,"Follow relationship does not exist"));
}
- return ResponseEntity.ok().body("Unfollowed successfully.");
+ return ResponseEntity.ok(new SuccessResponse<>(200,null,"Unfollowed successfully"));
}
}
diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java
new file mode 100644
index 00000000..df3e3fca
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java
@@ -0,0 +1,12 @@
+package com.group1.cuisines.dao.response;
+import lombok.*;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class ErrorResponse {
+ private int status;
+ private String message;
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java
new file mode 100644
index 00000000..b4ab374e
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java
@@ -0,0 +1,10 @@
+package com.group1.cuisines.dao.response;
+import lombok.*;
+@Data
+@AllArgsConstructor
+public class SuccessResponse {
+ private int status;
+ private T data;
+ private String message;
+
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java
new file mode 100644
index 00000000..e22aeb60
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java
@@ -0,0 +1,17 @@
+package com.group1.cuisines.dto;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuthorDto {
+ private Integer id;
+ private String name;
+ private String username;
+ private Integer followersCount;
+ private Integer followingCount;
+ private Integer recipesCount;
+
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java b/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java
new file mode 100644
index 00000000..2f920dae
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java
@@ -0,0 +1,21 @@
+package com.group1.cuisines.dto;
+import lombok.*;
+import java.util.List;
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class BookmarkDto {
+
+ private Integer id;
+ private String name;
+ private String instructions;
+ private List ingredients;
+ private int servingSize;
+ private Integer cookTime;
+ //private List images;
+ private CuisineDto cuisine;
+ private DishDto dish;
+ private Double avgRating;
+
+ private AuthorDto author;
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java b/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java
new file mode 100644
index 00000000..e06a70d0
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java
@@ -0,0 +1,15 @@
+package com.group1.cuisines.dto;
+import lombok.*;
+
+import java.util.List;
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CuisineDetailsDto {
+ private String id;
+ private String name;
+ // private String description;
+ //private String image;
+ // private Boolean isSelfFollowing;
+ private List dishes;
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java b/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java
new file mode 100644
index 00000000..efdd9272
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java
@@ -0,0 +1,13 @@
+package com.group1.cuisines.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CuisineDto {
+ private String id;
+ private String name;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/group1/cuisines/dto/DishDto.java b/backend/src/main/java/com/group1/cuisines/dto/DishDto.java
new file mode 100644
index 00000000..93879540
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/DishDto.java
@@ -0,0 +1,10 @@
+package com.group1.cuisines.dto;
+import lombok.*;
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DishDto {
+ private String id;
+ private String name;
+ private String image;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java b/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java
new file mode 100644
index 00000000..6eb4915e
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java
@@ -0,0 +1,19 @@
+package com.group1.cuisines.dto;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DishResponseDto {
+ private String id;
+ private String name;
+ private String image;
+ private String description;
+ private String countries;
+ private String ingredients;
+ private String foodTypes;
+ private String cuisines;
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java b/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java
index 1bc7faec..cc4c853a 100644
--- a/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java
+++ b/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java
@@ -14,5 +14,5 @@ public class NewRecipeDto {
private int cookingTime;
private int servingSize;
private String dishId;
- private List ingredients; // Nested DTOs for ingredients
+ private List ingredients;
}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java b/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java
new file mode 100644
index 00000000..5f5ec185
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java
@@ -0,0 +1,29 @@
+package com.group1.cuisines.dto;
+
+import java.util.List;
+
+import com.group1.cuisines.entities.Ingredient;
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RecipeDetailsDto {
+ private Integer id;
+ private String name;
+ // private String description;
+ private String instructions;
+ private List ingredients;
+ //private List images;
+ private Integer cookTime;
+ private Integer servingSize;
+ // private List allergens;
+ private CuisineDto cuisine;
+ private DishDto dish;
+ private Double avgRating;
+
+ private AuthorDto author;
+
+
+
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java b/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java
new file mode 100644
index 00000000..bca92e79
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java
@@ -0,0 +1,23 @@
+package com.group1.cuisines.dto;
+
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RecipeDto {
+ private Integer id;
+ private String title;
+ private String instructions;
+ private int preparationTime;
+ private int cookingTime;
+ private int servingSize;
+ private double averageRating;
+ private String dishName;
+
+
+
+
+}
diff --git a/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java b/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java
new file mode 100644
index 00000000..447263f8
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java
@@ -0,0 +1,24 @@
+package com.group1.cuisines.dto;
+import lombok.*;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+
+
+public class UserProfileDto {
+ private Integer id;
+ private String username;
+ private String name;
+ private String bio;
+ private Integer followersCount;
+ private Integer followingCount;
+ //private String gender;
+ // private String profilePicture;
+ // private List diets;
+ private Integer recipeCount;
+ private List bookmarks;
+ private List recipes;
+}
diff --git a/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java b/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java
index 020aa4c3..6adc193a 100644
--- a/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java
+++ b/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java
@@ -9,7 +9,9 @@
import java.net.URL;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
@Data
@@ -32,8 +34,20 @@ public class Cuisine {
inverseJoinColumns = @JoinColumn(name = "dish_id")
)
- private List dishes = new ArrayList<>();
+ private Set dishes = new HashSet<>();
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Cuisine cuisine = (Cuisine) o;
+ return id != null && id.equals(cuisine.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31;
+ }
@Override
public String toString() {
return "Cuisine{" +
diff --git a/backend/src/main/java/com/group1/cuisines/entities/Dish.java b/backend/src/main/java/com/group1/cuisines/entities/Dish.java
index 71acad01..6e1790c9 100644
--- a/backend/src/main/java/com/group1/cuisines/entities/Dish.java
+++ b/backend/src/main/java/com/group1/cuisines/entities/Dish.java
@@ -34,6 +34,19 @@ public class Dish {
@ManyToMany(mappedBy = "dishes")
private List cuisines = new ArrayList<>();
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Dish dish = (Dish) o;
+ return id != null && id.equals(dish.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31;
+ }
+
@Override
public String toString() {
return "Dish{" +
diff --git a/backend/src/main/java/com/group1/cuisines/entities/Recipe.java b/backend/src/main/java/com/group1/cuisines/entities/Recipe.java
index 10e33734..eadeb35c 100644
--- a/backend/src/main/java/com/group1/cuisines/entities/Recipe.java
+++ b/backend/src/main/java/com/group1/cuisines/entities/Recipe.java
@@ -4,6 +4,7 @@
import lombok.*;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
@Data
@@ -31,6 +32,11 @@ public class Recipe {
@Builder.Default
private List ratings = new ArrayList<>();
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "created_at", nullable = false, updatable = false)
+ @org.hibernate.annotations.CreationTimestamp
+ private Date createdAt;
+
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
diff --git a/backend/src/main/java/com/group1/cuisines/entities/User.java b/backend/src/main/java/com/group1/cuisines/entities/User.java
index 5e7d5923..16a2e4b7 100644
--- a/backend/src/main/java/com/group1/cuisines/entities/User.java
+++ b/backend/src/main/java/com/group1/cuisines/entities/User.java
@@ -1,10 +1,7 @@
package com.group1.cuisines.entities;
import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@@ -18,6 +15,8 @@
@NoArgsConstructor
@AllArgsConstructor
@Entity
+@Getter
+@Setter
@Table(name = "users")
public class User implements UserDetails {
@@ -53,6 +52,13 @@ public class User implements UserDetails {
@Builder.Default
private int recipeCount = 0;
+ @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
+ private Set recipes = new HashSet<>();
+ @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
+ private Set bookmarks = new HashSet<>();
+
+
+
diff --git a/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java b/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java
new file mode 100644
index 00000000..bff8d299
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java
@@ -0,0 +1,26 @@
+package com.group1.cuisines.exceptions;
+
+import com.group1.cuisines.dao.response.ApiResponse;
+import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity> handleException(Exception e) {
+ ApiResponse response = new ApiResponse<>(500, "An unexpected error occurred: " + e.getMessage(), null);
+ return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(ResourceNotFoundException.class)
+ public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) {
+ ApiResponse response = new ApiResponse<>(400, e.getMessage(), null);
+ return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
+ }
+
+
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java b/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java
new file mode 100644
index 00000000..d1ab87bf
--- /dev/null
+++ b/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java
@@ -0,0 +1,7 @@
+package com.group1.cuisines.exceptions;
+
+public class ResourceNotFoundException extends RuntimeException {
+ public ResourceNotFoundException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java b/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java
index a1b94f43..7eedc5ac 100644
--- a/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java
+++ b/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java
@@ -1,24 +1,28 @@
package com.group1.cuisines.repositories;
import com.group1.cuisines.entities.Dish;
+import java.util.List;
+import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
-import java.util.List;
-import java.util.Optional;
-
@Repository
-public interface DishRepository extends JpaRepository {
- @Query("SELECT d FROM Dish d WHERE LOWER(d.name) LIKE LOWER(CONCAT('%', :name, '%'))")
+public interface DishRepository extends JpaRepository {
+ @Query(
+ "SELECT d FROM Dish d WHERE LOWER(d.name) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(d.countries) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(d.ingredients) LIKE LOWER(CONCAT('%', :name, '%'))"
+ )
List findByNameContainingIgnoreCase(String name);
- @Query("SELECT d FROM Dish d JOIN d.cuisines c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :cuisineName, '%'))")
+
+ @Query(
+ "SELECT d FROM Dish d JOIN d.cuisines c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :cuisineName, '%'))"
+ )
List findByCuisinesName(@Param("cuisineName") String cuisineName);
+
List findByFoodTypesContainingIgnoreCase(String foodType);
Optional findById(String id);
@Query("SELECT d FROM Dish d JOIN d.cuisines c WHERE c.id = :cuisineId")
List findByCuisineId(@Param("cuisineId") String cuisineId);
-
}
diff --git a/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java b/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java
index 9d027dd9..c34d8c78 100644
--- a/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java
+++ b/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java
@@ -3,10 +3,34 @@
import com.group1.cuisines.entities.Recipe;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
+import java.util.List;
+
@Repository
-public interface RecipeRepository extends JpaRepository {
+public interface RecipeRepository extends JpaRepository {
+
+
+
+
+ @Query("SELECT r FROM Recipe r WHERE " +
+ "(:dishId IS NULL OR r.dish.id = :dishId) AND " +
+ "(:cuisineId IS NULL OR EXISTS (SELECT d FROM Dish d JOIN d.cuisines c WHERE d = r.dish AND c.id = :cuisineId)) " +
+ "ORDER BY CASE WHEN :sort = 'recent' THEN r.createdAt ELSE r.averageRating END DESC")
+ List findByDishIdAndCuisineIdWithSort(
+ @Param("dishId") String dishId,
+ @Param("cuisineId") String cuisineId,
+ @Param("sort") String sort);
+
+ // Fetch recipes by dishId and cuisineId
+ @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId ORDER BY r.createdAt DESC")
+ List findByDishIdAndCuisineIdOrderByCreatedAtDesc(String dishId, String cuisineId);
+
+ @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId ORDER BY r.averageRating DESC")
+ List findByDishIdAndCuisineIdOrderByAverageRatingDesc(String dishId, String cuisineId);
+ @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId")
+ List findByDishIdAndCuisineId(String dishId, String cuisineId);
@Query("SELECT AVG(r.ratingValue) FROM Rating r WHERE r.recipe.id = :recipeId")
Double findAverageByRecipeId(Integer recipeId);
}
diff --git a/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java b/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java
index 06bb7823..1221f008 100644
--- a/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java
+++ b/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java
@@ -52,7 +52,7 @@ public ApiResponse signup(
String token = jwtService.generateToken(user);
return new ApiResponse<>(
- 200,
+ 201,
"User registered successfully.",
AuthenticationTokenResponse.builder().token(token).build()
);
diff --git a/backend/src/main/java/com/group1/cuisines/services/CuisineService.java b/backend/src/main/java/com/group1/cuisines/services/CuisineService.java
index e65a84ca..e0211558 100644
--- a/backend/src/main/java/com/group1/cuisines/services/CuisineService.java
+++ b/backend/src/main/java/com/group1/cuisines/services/CuisineService.java
@@ -1,24 +1,43 @@
package com.group1.cuisines.services;
+import com.group1.cuisines.dto.CuisineDetailsDto;
+import com.group1.cuisines.dto.DishDto;
import com.group1.cuisines.entities.Cuisine;
+import com.group1.cuisines.entities.Dish;
import com.group1.cuisines.repositories.CuisineRepository;
+import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
@Service
public class CuisineService {
@Autowired
private CuisineRepository cuisineRepository;
- public Cuisine getCuisineById(String cuisineId, boolean includeDishes) {
- return cuisineRepository.findById(cuisineId)
- .map(cuisine -> {
- if (!includeDishes) {
- cuisine.setDishes(null); // Clear the dishes if not required
- }
- return cuisine;
- })
- .orElse(null);
+ public CuisineDetailsDto getCuisineById(String cuisineId, boolean includeDishes) {
+ Cuisine cuisine = cuisineRepository.findById(cuisineId)
+ .orElseThrow(() -> new EntityNotFoundException("Cuisine not found"));
+
+ CuisineDetailsDto detailsDto = new CuisineDetailsDto(
+ cuisine.getId(),
+ cuisine.getName(),
+ // cuisine.getDescription(),
+
+
+ includeDishes ? convertDishes(cuisine.getDishes()) : new ArrayList<>()
+ );
+ return detailsDto;
+ }
+
+ private List convertDishes(Set dishes) {
+ return dishes.stream()
+ .map(dish -> new DishDto(dish.getId(), dish.getName(),dish.getImage()))
+ .collect(Collectors.toList());
}
}
diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java
index cc811ffe..51efe39f 100644
--- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java
+++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java
@@ -1,19 +1,19 @@
package com.group1.cuisines.services;
-import com.group1.cuisines.dto.CommentsDto;
-import com.group1.cuisines.dto.IngredientsDto;
-import com.group1.cuisines.dto.NewRecipeDto;
-import com.group1.cuisines.dto.RecipeDetailDto;
+import com.group1.cuisines.dto.*;
import com.group1.cuisines.entities.*;
import com.group1.cuisines.repositories.*;
+import jakarta.annotation.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
@Service
@@ -35,6 +35,25 @@ public class RecipeService {
@Autowired
private CommentRepository commentRepository;
+
+
+
+ public List findRecipes(String sort, String dishId, String cuisineId) {
+ List recipes = recipeRepository.findByDishIdAndCuisineIdWithSort(dishId, cuisineId, sort);
+
+ return recipes.stream()
+ .map(recipe -> new RecipeDto(
+ recipe.getId(),
+ recipe.getTitle(),
+ recipe.getInstructions(),
+ recipe.getPreparationTime(),
+ recipe.getCookingTime(),
+ recipe.getServingSize(),
+ recipe.getAverageRating(),
+ recipe.getTitle()))
+ .collect(Collectors.toList());
+ }
+
@Transactional
public RecipeDetailDto createRecipe(NewRecipeDto newRecipe, String username) throws Exception {
Optional user = userRepository.findByUsername(username);
@@ -150,6 +169,7 @@ public List getWhoBookmarked(Integer recipeId) {
return bookmarkRepository.findByRecipeId(recipeId).stream().map(Bookmark::getUser).toList();
}
+
public List getCommentsByRecipeId(Integer recipeId) {
return commentRepository.findByRecipeId(recipeId).stream()
.map(comment -> CommentsDto.builder()
@@ -162,4 +182,89 @@ public List getCommentsByRecipeId(Integer recipeId) {
.build())
.collect(Collectors.toList());
}
+
+ public RecipeDetailsDto getRecipeById(Integer recipeId) {
+ Optional recipe = recipeRepository.findById(recipeId);
+
+
+ if (recipe.isPresent()) {
+ CuisineDto cuisineDto = new CuisineDto();
+ Recipe r = recipe.get();
+ if (r.getDish() != null && !r.getDish().getCuisines().isEmpty()) {
+
+ cuisineDto.setId(r.getDish().getCuisines().get(0).getId());
+ cuisineDto.setName(r.getDish().getCuisines().get(0).getName());
+
+ }
+ else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){
+ cuisineDto.setId("No cuisine Id from wikidata");
+ cuisineDto.setName("No cuisine name from wikidata");
+ }
+ // Conversion from Recipe entity to RecipeDetailsDto
+ return new RecipeDetailsDto(
+ r.getId(),
+ r.getTitle(),
+
+ r.getInstructions(),
+ r.getIngredients().stream().map(ingredient -> new IngredientsDto( ingredient.getName())).collect(Collectors.toList()),
+
+ r.getCookingTime(),
+ r.getServingSize(),
+ cuisineDto,
+
+ new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()),
+ r.getAverageRating(),
+ new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowing().size(),r.getUser().getFollowers().size(), r.getUser().getRecipeCount())
+
+ );
+ }
+ return null;
+ }
+
+ public List getRecipesByType(String type, @Nullable String username) {
+ if ("explore".equals(type)) {
+ return recipeRepository.findAll().stream()
+ .map(this::convertToRecipeDto)
+ .collect(Collectors.toList());
+ } else if ("following".equals(type) && username != null) {
+ User user = userRepository.findByUsername(username)
+ .orElseThrow(() -> new RuntimeException("User not found"));
+ return user.getFollowing().stream()
+ .flatMap(followingUser -> followingUser.getRecipes().stream())
+ .map(this::convertToRecipeDto)
+ .collect(Collectors.toList());
+ }
+ return new ArrayList<>(); // Return an empty list if username is null or other conditions are not met
+ }
+
+ private RecipeDetailsDto convertToRecipeDto(Recipe r) {
+ CuisineDto cuisineDto = new CuisineDto();
+ if (r.getDish() != null && !r.getDish().getCuisines().isEmpty()) {
+
+ cuisineDto.setId(r.getDish().getCuisines().get(0).getId());
+ cuisineDto.setName(r.getDish().getCuisines().get(0).getName());
+
+ }
+ else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){
+ cuisineDto.setId("No cuisine Id from wikidata");
+ cuisineDto.setName("No cuisine name from wikidata");
+ }
+ // Conversion logic here
+ return new RecipeDetailsDto(
+ r.getId(),
+ r.getTitle(),
+
+ r.getInstructions(),
+ r.getIngredients().stream().map(ingredient -> new IngredientsDto( ingredient.getName())).collect(Collectors.toList()),
+
+ r.getCookingTime(),
+ r.getServingSize(),
+ cuisineDto,
+
+ new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()),
+ r.getAverageRating(),
+ new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(),r.getUser().getFollowing().size(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount())
+
+ );
+ }
}
diff --git a/backend/src/main/java/com/group1/cuisines/services/SearchService.java b/backend/src/main/java/com/group1/cuisines/services/SearchService.java
index ee33a83e..abbe6422 100644
--- a/backend/src/main/java/com/group1/cuisines/services/SearchService.java
+++ b/backend/src/main/java/com/group1/cuisines/services/SearchService.java
@@ -1,6 +1,9 @@
package com.group1.cuisines.services;
+import com.group1.cuisines.dto.DishDto;
+import com.group1.cuisines.dto.DishResponseDto;
import com.group1.cuisines.entities.Dish;
+import com.group1.cuisines.exceptions.ResourceNotFoundException;
import com.group1.cuisines.repositories.DishRepository;
import com.group1.cuisines.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
@@ -16,17 +19,23 @@
public class SearchService {
private final DishRepository dishRepository;
- public List searchDishes(String query, String cuisine, String foodType) {
+ public List searchDishes(String query, String cuisine, String foodType) {
List dishes = dishRepository.findAll();
// Filter by dish name
if (query != null && !query.isEmpty()) {
dishes = dishRepository.findByNameContainingIgnoreCase(query);
+ if (dishes.isEmpty()) {
+ throw new ResourceNotFoundException("No dishes found with the given name query.");
+ }
}
// Filter by cuisine name
if (cuisine != null && !cuisine.isEmpty()) {
List dishesByCuisine = dishRepository.findByCuisinesName(cuisine);
+ if (dishesByCuisine.isEmpty()) {
+ throw new ResourceNotFoundException("No dishes found with the given cuisine.");
+ }
dishes = dishes.stream()
.filter(dishesByCuisine::contains)
.collect(Collectors.toList());
@@ -37,9 +46,25 @@ public List searchDishes(String query, String cuisine, String foodType) {
dishes = dishes.stream()
.filter(d -> d.getFoodTypes() != null && d.getFoodTypes().contains(foodType))
.collect(Collectors.toList());
+
+ if (dishes.isEmpty()) {
+ throw new ResourceNotFoundException("No dishes found with the given food type.");
+ }
}
- return dishes;
+ // Map to DishResponseDto
+ return dishes.stream()
+ .map(d -> new DishResponseDto(
+ d.getId(),
+ d.getName(),
+ d.getImage(),
+ d.getDescription(),
+ d.getCountries(),
+ d.getIngredients(),
+ d.getFoodTypes(),
+ d.getCuisines().isEmpty() ? null : d.getCuisines().get(0).getName()
+ ))
+ .collect(Collectors.toList());
}
diff --git a/backend/src/main/java/com/group1/cuisines/services/UserService.java b/backend/src/main/java/com/group1/cuisines/services/UserService.java
index 0332882e..7dacf859 100644
--- a/backend/src/main/java/com/group1/cuisines/services/UserService.java
+++ b/backend/src/main/java/com/group1/cuisines/services/UserService.java
@@ -1,7 +1,11 @@
package com.group1.cuisines.services;
+import com.group1.cuisines.dto.*;
+import com.group1.cuisines.entities.Bookmark;
+import com.group1.cuisines.entities.Recipe;
import com.group1.cuisines.entities.User;
import com.group1.cuisines.repositories.UserRepository;
+import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -14,6 +18,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@@ -108,4 +113,94 @@ public Set getUserFollower(Integer userId) {
}
return Collections.emptySet();
}
+
+ public UserProfileDto getUserProfileById(Integer userId, String currentUsername) {
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new EntityNotFoundException("User not found"));
+
+ boolean isSelf = user.getUsername().equals(currentUsername);
+
+ UserProfileDto profile = new UserProfileDto();
+ profile.setId(user.getId());
+ profile.setUsername(user.getUsername());
+ profile.setName(user.getFirstName() + " " + user.getLastName());
+ profile.setBio(user.getBio());
+ profile.setFollowersCount(user.getFollowers().size());
+ profile.setFollowingCount(user.getFollowing().size());
+ profile.setRecipeCount(user.getRecipes().size());
+ profile.setRecipes(user.getRecipes().stream()
+ .map(this::convertToRecipeDetailsDto)
+ .collect(Collectors.toList()));
+
+ if (isSelf) {
+ profile.setBookmarks(user.getBookmarks().stream()
+ .map(this::convertToBookmarkDto)
+ .collect(Collectors.toList()));
+ } else {
+ profile.setBookmarks(Collections.emptyList());
+ }
+
+ return profile;
+ }
+
+ private RecipeDetailsDto convertToRecipeDetailsDto(Recipe recipe) {
+ CuisineDto cuisineDto = new CuisineDto();
+ if (recipe.getDish() != null && !recipe.getDish().getCuisines().isEmpty()) {
+
+ cuisineDto.setId(recipe.getDish().getCuisines().get(0).getId());
+ cuisineDto.setName(recipe.getDish().getCuisines().get(0).getName());
+
+ }
+ else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){
+ cuisineDto.setId("No cuisine Id from wikidata");
+ cuisineDto.setName("No cuisine name from wikidata");
+ }
+ return new RecipeDetailsDto(
+ recipe.getId(),
+ recipe.getTitle(),
+ recipe.getInstructions(),
+ recipe.getIngredients().stream()
+ .map(ingredient -> new IngredientsDto(ingredient.getName()))
+ .collect(Collectors.toList()),
+ recipe.getCookingTime(),
+ recipe.getServingSize(),
+ cuisineDto,
+ new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()),
+ recipe.getAverageRating(),
+ new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(),
+ recipe.getUser().getFollowingCount(), recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount())
+ );
+ }
+
+ private BookmarkDto convertToBookmarkDto(Bookmark bookmark) {
+
+ Recipe recipe = bookmark.getRecipe();
+ CuisineDto cuisineDto = new CuisineDto();
+ if (recipe.getDish() != null && !recipe.getDish().getCuisines().isEmpty()) {
+
+ cuisineDto.setId(recipe.getDish().getCuisines().get(0).getId());
+ cuisineDto.setName(recipe.getDish().getCuisines().get(0).getName());
+
+ }
+ else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){
+ cuisineDto.setId("No cuisine Id from wikidata");
+ cuisineDto.setName("No cuisine name from wikidata");
+ }
+ return new BookmarkDto(
+ recipe.getId(),
+ recipe.getTitle(),
+ recipe.getInstructions(),
+ recipe.getIngredients().stream()
+ .map(ingredient -> new IngredientsDto(ingredient.getName()))
+ .collect(Collectors.toList()),
+ recipe.getServingSize(),
+ recipe.getCookingTime(),
+ //recipe.getImages(),
+ cuisineDto,
+ new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()),
+ recipe.getAverageRating(),
+ new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(),
+ recipe.getUser().getFollowingCount(), recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount())
+ );
+ }
}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 14a044e0..14898104 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -5,11 +5,15 @@ jwt.signing.key=413F4428472B4B6250655368566D5970337336763979244226452948404D6351
spring.application.name=cuisines
spring.datasource.url=jdbc:mysql://localhost:3306/cuisines-test?createDatabaseIfNotExist=true
spring.datasource.username=root
-spring.datasource.password=adminadmin
+spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
logging.level.com.group1.cuisines=DEBUG
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.properties.hibernate.use_sql_comments=true
+logging.level.org.hibernate.type.descriptor.sql=trace
+
spring.banner.location=./banner.txt
diff --git a/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java b/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java
index d3d6218b..4aa77e4c 100644
--- a/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java
+++ b/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java
@@ -14,6 +14,7 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public class AuthenticationControllerTest {
@@ -31,123 +32,65 @@ public void setUp() {
@Test
public void shouldReturnSuccessOnValidSignin() {
- SignInRequest signInRequest = new SignInRequest(
- "testUser",
- "testPassword"
- );
- ApiResponse apiResponse =
- new ApiResponse<>(
- 200,
- "Success",
- new AuthenticationTokenResponse("token")
- );
- when(authenticationService.signin(signInRequest)).thenReturn(
- apiResponse
- );
- ResponseEntity<
- ApiResponse
- > responseEntity = authenticationController.signin(signInRequest);
- assertEquals(
- 200,
- responseEntity.getBody().getStatus(),
- "Status code does not match expected value on successful signin"
- );
- assertEquals(
- apiResponse,
- responseEntity.getBody(),
- "Response body does not match expected value on successful signin"
- );
+ SignInRequest signInRequest = new SignInRequest("testUser", "testPassword");
+ ApiResponse apiResponse = new ApiResponse<>(200, "Success", new AuthenticationTokenResponse("token"));
+ when(authenticationService.signin(signInRequest)).thenReturn(apiResponse);
+
+ ResponseEntity> responseEntity = authenticationController.signin(signInRequest);
+
+ assertEquals(HttpStatus.OK.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on successful signin");
+ assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on successful signin");
}
@Test
public void shouldReturnFailureOnInvalidSignin() {
- SignInRequest signInRequest = new SignInRequest(
- "testUser",
- "wrongPassword"
- );
- ApiResponse apiResponse =
- new ApiResponse<>(401, "Failure", null);
- when(authenticationService.signin(signInRequest)).thenReturn(
- apiResponse
- );
- ResponseEntity<
- ApiResponse
- > responseEntity = authenticationController.signin(signInRequest);
- assertEquals(
- 401,
- responseEntity.getBody().getStatus(),
- "Status code does not match expected value on failed signin"
- );
- assertEquals(
- apiResponse,
- responseEntity.getBody(),
- "Response body does not match expected value on failed signin"
- );
+ SignInRequest signInRequest = new SignInRequest("testUser", "wrongPassword");
+ ApiResponse apiResponse = new ApiResponse<>(401, "Invalid email/username or password.", null);
+ when(authenticationService.signin(signInRequest)).thenReturn(apiResponse);
+
+ ResponseEntity> responseEntity = authenticationController.signin(signInRequest);
+
+ assertEquals(HttpStatus.UNAUTHORIZED.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on failed signin");
+ assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on failed signin");
}
@Test
public void shouldReturnSuccessOnValidSignup() {
SignUpRequest signUpRequest = SignUpRequest.builder()
- .email("newUser@gmail.com")
- .username("newUser")
- .country("USA")
- .bio("Bio of the new user")
- .password("newPassword")
- .firstName("New")
- .lastName("User")
- .build();
- ApiResponse apiResponse =
- new ApiResponse<>(
- 200,
- "Success",
- new AuthenticationTokenResponse("token")
- );
- when(authenticationService.signup(signUpRequest)).thenReturn(
- apiResponse
- );
- ResponseEntity<
- ApiResponse
- > responseEntity = authenticationController.signup(signUpRequest);
- assertEquals(
- 200,
- responseEntity.getBody().getStatus(),
- "Status code does not match expected value on successful signup"
- );
- assertEquals(
- apiResponse,
- responseEntity.getBody(),
- "Response body does not match expected value on successful signup"
- );
+ .email("newUser@gmail.com")
+ .username("newUser")
+ .country("USA")
+ .bio("Bio of the new user")
+ .password("newPassword")
+ .firstName("New")
+ .lastName("User")
+ .build();
+ ApiResponse apiResponse = new ApiResponse<>(201, "User registered successfully.", new AuthenticationTokenResponse("token"));
+ when(authenticationService.signup(signUpRequest)).thenReturn(apiResponse);
+
+ ResponseEntity> responseEntity = authenticationController.signup(signUpRequest);
+
+ assertEquals(HttpStatus.CREATED.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on successful signup");
+ assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on successful signup");
}
@Test
public void shouldReturnFailureOnInvalidSignup() {
SignUpRequest signUpRequest = SignUpRequest.builder()
- .email("newUser@gmail.com")
- .username("newUser")
- .country("USA")
- .bio("Bio of the new user")
- .password("newPassword")
- .firstName("New")
- .lastName("User")
- .build();
- ApiResponse apiResponse =
- new ApiResponse<>(409, "Failure", null);
- when(authenticationService.signup(signUpRequest)).thenReturn(
- apiResponse
- );
- ResponseEntity<
- ApiResponse
- > responseEntity = authenticationController.signup(signUpRequest);
- assertEquals(
- 409,
- responseEntity.getBody().getStatus(),
- "Status code does not match expected value on failed signup"
- );
- assertEquals(
- apiResponse,
- responseEntity.getBody(),
- "Response body does not match expected value on failed signup"
- );
+ .email("newUser@gmail.com")
+ .username("newUser")
+ .country("USA")
+ .bio("Bio of the new user")
+ .password("newPassword")
+ .firstName("New")
+ .lastName("User")
+ .build();
+ ApiResponse apiResponse = new ApiResponse<>(409, "Email or username already exists.", null);
+ when(authenticationService.signup(signUpRequest)).thenReturn(apiResponse);
+
+ ResponseEntity> responseEntity = authenticationController.signup(signUpRequest);
+
+ assertEquals(HttpStatus.CONFLICT.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on failed signup");
+ assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on failed signup");
}
}