From e2e19d8b95ffcbdf4b2e7d5ef66b3b9f35253478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Sun, 24 Nov 2024 21:54:00 +0300 Subject: [PATCH] feat(backend): users can follow/unfollow tags --- .../Constants/EndpointConstants.java | 1 + .../Controllers/TagController.java | 44 +++++++++++++++++ .../Entities/Tag.java | 11 +++++ .../Entities/User.java | 10 ++++ .../Exceptions/ExceptionResponseHandler.java | 42 ++++++++++++++++ .../Services/TagService.java | 49 +++++++++++++++++++ .../Services/TagServiceTests.java | 4 +- 7 files changed, 159 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Constants/EndpointConstants.java b/backend/src/main/java/com/group1/programminglanguagesforum/Constants/EndpointConstants.java index 6d9be26e..dda5644d 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Constants/EndpointConstants.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Constants/EndpointConstants.java @@ -49,6 +49,7 @@ public static class SparqlEndpoints { public static class TagEndpoints { public static final String BASE_PATH = "/tags"; public static final String TAG_ID = BASE_PATH + "/{id}"; + public static final String TAG_FOLLOW = BASE_PATH + "/{id}/follow"; public static final String SEARCH = "/search" + BASE_PATH; } diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/TagController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/TagController.java index 5a76ecde..aa0d72a7 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/TagController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/TagController.java @@ -5,9 +5,16 @@ import com.group1.programminglanguagesforum.DTOs.Responses.ErrorResponse; import com.group1.programminglanguagesforum.DTOs.Responses.GenericApiResponse; import com.group1.programminglanguagesforum.DTOs.Responses.GetTagDetailsResponseDto; +import com.group1.programminglanguagesforum.DTOs.Responses.TagDto; import com.group1.programminglanguagesforum.DTOs.Responses.TagSearchResponseDto; +import com.group1.programminglanguagesforum.Entities.User; +import com.group1.programminglanguagesforum.Exceptions.ExceptionResponseHandler; +import com.group1.programminglanguagesforum.Exceptions.UnauthorizedAccessException; import com.group1.programminglanguagesforum.Services.TagService; +import com.group1.programminglanguagesforum.Services.UserContextService; import com.group1.programminglanguagesforum.Util.ApiResponseBuilder; + +import jakarta.persistence.EntityExistsException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -20,11 +27,15 @@ import java.util.Arrays; import java.util.NoSuchElementException; + @RestController @RequiredArgsConstructor @RequestMapping("/api/v1") public class TagController extends BaseController { + private final TagService tagService; + private final UserContextService userContextService; + @GetMapping(value = EndpointConstants.TagEndpoints.SEARCH) public ResponseEntity> tagSearch( @RequestParam String q, @@ -107,4 +118,37 @@ public ResponseEntity> createTag(@R } } + + @PostMapping(value = EndpointConstants.TagEndpoints.TAG_FOLLOW) + public ResponseEntity> postMethodName(@PathVariable(value = "id") Long tagId) { + try { + User user = userContextService.getCurrentUser(); + TagDto tagDto = tagService.followTag(user, tagId); + GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse(TagDto.class, "Tag followed successfully", 200, tagDto); + return buildResponse(response, HttpStatus.OK); + + } catch (UnauthorizedAccessException e) { + return ExceptionResponseHandler.UnauthorizedAccessException(e); + } catch (NoSuchElementException e) { + return ExceptionResponseHandler.NoSuchElementException(e); + } catch (EntityExistsException e) { + return ExceptionResponseHandler.EntityExistsException(e); + } + } + + @DeleteMapping(value = EndpointConstants.TagEndpoints.TAG_FOLLOW) + public ResponseEntity> deleteMethodName(@PathVariable(value = "id") Long tagId) { + try { + User user = userContextService.getCurrentUser(); + TagDto tagDto = tagService.unfollowTag(user, tagId); + GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse(TagDto.class, "Tag unfollowed successfully", 200, tagDto); + return buildResponse(response, HttpStatus.OK); + + } catch (UnauthorizedAccessException e) { + return ExceptionResponseHandler.UnauthorizedAccessException(e); + } catch (NoSuchElementException e) { + return ExceptionResponseHandler.NoSuchElementException(e); + } + } + } diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Entities/Tag.java b/backend/src/main/java/com/group1/programminglanguagesforum/Entities/Tag.java index 67be4adf..68fc36f7 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Entities/Tag.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Entities/Tag.java @@ -1,5 +1,8 @@ package com.group1.programminglanguagesforum.Entities; +import java.util.HashSet; +import java.util.Set; + import jakarta.persistence.*; import lombok.*; @@ -18,6 +21,14 @@ public class Tag { private String wikidataId; private String tagName; private String tagDescription; + + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinTable(name = "USER_TAGS", + joinColumns = @JoinColumn(name = "tag_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + private Set followers = new HashSet<>(); + public Tag(String wikidataId, String tagName, String tagDescription) { this.wikidataId = wikidataId; this.tagName = tagName; diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Entities/User.java b/backend/src/main/java/com/group1/programminglanguagesforum/Entities/User.java index 83549e25..418b8fe6 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Entities/User.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Entities/User.java @@ -57,6 +57,16 @@ public class User implements UserDetails { private int followingCount = 0; @Builder.Default private Long reputationPoints = 0L; + + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable( + name = "USER_TAGS", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + @Builder.Default + private Set followedTags = new HashSet<>(); + @OneToMany(mappedBy = "askedBy", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private List questions = new ArrayList<>(); diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Exceptions/ExceptionResponseHandler.java b/backend/src/main/java/com/group1/programminglanguagesforum/Exceptions/ExceptionResponseHandler.java index 09f06933..bf941861 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Exceptions/ExceptionResponseHandler.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Exceptions/ExceptionResponseHandler.java @@ -4,6 +4,9 @@ import com.group1.programminglanguagesforum.DTOs.Responses.GenericApiResponse; import com.group1.programminglanguagesforum.DTOs.Responses.UserProfileResponseDto; import com.group1.programminglanguagesforum.Util.ApiResponseBuilder; + +import jakarta.persistence.EntityExistsException; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -88,4 +91,43 @@ public static ResponseEntity> NoSuchElementException(N ); } + + public static ResponseEntity> IllegalArgumentException(IllegalArgumentException e) { + ErrorResponse errorResponse = ErrorResponse.builder() + .errorMessage(e.getMessage()) + .stackTrace(Arrays.toString(e.getStackTrace())) + .build(); + + GenericApiResponse response = ApiResponseBuilder.buildErrorResponse( + UserProfileResponseDto.class, + e.getMessage(), + HttpStatus.BAD_REQUEST.value(), + errorResponse + ); + + return new ResponseEntity<>( + response, + HttpStatus.valueOf(response.getStatus()) + ); + } + + public static ResponseEntity> EntityExistsException(EntityExistsException e) { + ErrorResponse errorResponse = ErrorResponse.builder() + .errorMessage(e.getMessage()) + .stackTrace(Arrays.toString(e.getStackTrace())) + .build(); + + GenericApiResponse response = ApiResponseBuilder.buildErrorResponse( + UserProfileResponseDto.class, + e.getMessage(), + HttpStatus.CONFLICT.value(), + errorResponse + ); + + return new ResponseEntity<>( + response, + HttpStatus.valueOf(response.getStatus()) + ); + } + } diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java index 65a71823..45857cfc 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java @@ -5,6 +5,9 @@ import com.group1.programminglanguagesforum.Entities.*; import com.group1.programminglanguagesforum.Repositories.QuestionRepository; import com.group1.programminglanguagesforum.Repositories.TagRepository; +import com.group1.programminglanguagesforum.Repositories.UserRepository; + +import jakarta.persistence.EntityExistsException; import lombok.RequiredArgsConstructor; import org.modelmapper.ModelMapper; import org.springframework.data.domain.Page; @@ -21,6 +24,7 @@ public class TagService { private final TagRepository tagRepository; private final ModelMapper modelMapper; private final QuestionRepository questionRepository; + private final UserRepository userRepository; public List findAllByIdIn(List tagIds) { return tagRepository.findAllByIdIn(tagIds); @@ -103,4 +107,49 @@ public Page searchTags(String q, Pageable pageable) { .tagType(getTagType(tag).toString()) .build()); } + + public TagDto followTag(User user, Long tagId) { + + Optional tag = tagRepository.findById(tagId); + if (tag.isEmpty()) { + throw new NoSuchElementException("Tag not found"); + } + + Tag tagEntity = tag.get(); + + if (user.getFollowedTags().stream().anyMatch(t -> t.getId().equals(tagId))) { + throw new EntityExistsException("User already follows this tag"); + } + + user.getFollowedTags().add(tagEntity); + userRepository.save(user); + + return TagDto.builder() + .id(tagEntity.getId()) + .name(tagEntity.getTagName()) + .build(); + } + + public TagDto unfollowTag(User user, Long tagId) { + + Optional tag = tagRepository.findById(tagId); + if (tag.isEmpty()) { + throw new NoSuchElementException("Tag not found"); + } + + Tag tagEntity = tag.get(); + + if (!user.getFollowedTags().stream().anyMatch(t -> t.getId().equals(tagId))) { + throw new NoSuchElementException("User does not follow this tag"); + } + + user.getFollowedTags().removeIf(t -> t.getId().equals(tagId)); + userRepository.save(user); + + return TagDto.builder() + .id(tagEntity.getId()) + .name(tagEntity.getTagName()) + .build(); + } + } diff --git a/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java b/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java index afe16bfc..d1bef799 100644 --- a/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java +++ b/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java @@ -67,7 +67,7 @@ void testCreateTag() { .build(); // Mock the Tag returned by the repository save method - Tag savedTag = new Tag(1L, null, "New Tag", "Tag description"); + Tag savedTag = new Tag(1L, null, "New Tag", "Tag description", null); // Mock tagRepository behavior when(tagRepository.save(any(Tag.class))).thenReturn(savedTag); @@ -90,7 +90,7 @@ void testCreateTag() { void testGetTagDetails_Success() { Long tagId = 1L; - Tag mockTag = new Tag(1L, null, "Tag1", "Description1"); + Tag mockTag = new Tag(1L, null, "Tag1", "Description1", null); List mockQuestions = Arrays.asList( new Question(1L, "Question1", "Body1", DifficultyLevel.EASY, 0L, 0L, null, null, null, null, null,null), new Question(2L, "Question2", "Body2", DifficultyLevel.MEDIUM, 0L, 0L, null, null, null, null, null,null));