Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: modal to add favourite activity #1794

Merged
merged 31 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2b593ff
feat: refacto favorite actvity api to support consep
Dec 4, 2024
e95dd53
feat: add fav act page for consep
Dec 9, 2024
eddf779
Merge branch 'main' into feat/1580-list-favorite-activity-in-consep
Dec 9, 2024
f0ce209
feat: add migration for consep favorite activity
Dec 9, 2024
c7b62aa
feat: add all the activities in consep
Dec 10, 2024
03555f6
feat: add department key in favActivityTypes
Dec 10, 2024
5a2c6f0
feat: modal to add favourite activity
Dec 20, 2024
de82cd3
Merge branch 'main' into feat/1580-list-favorite-activity-in-consep
xiaopeng0202 Jan 3, 2025
97d998d
fix: address pr comment
Jan 7, 2025
304c4b0
fix: favourite act testing
Jan 7, 2025
62b74f5
Merge branch 'main' into feat/1580-list-favorite-activity-in-consep
xiaopeng0202 Jan 7, 2025
576c289
fix: fix style
Jan 7, 2025
b3fe626
fix: revert port
Jan 7, 2025
a0d945e
fix: merge conflit
Jan 7, 2025
25584d0
fix: testings
Jan 7, 2025
fc75a01
fix: testings
Jan 8, 2025
4190bd7
fix: testings
Jan 8, 2025
a9848a5
fix: testings
Jan 8, 2025
85875ed
fix: style
Jan 8, 2025
f1bc406
fix: style
Jan 8, 2025
b433bfb
fix: remove comment
Jan 8, 2025
52c99d2
Merge branch 'main' into feat/1580-list-favorite-activity-in-consep
xiaopeng0202 Jan 13, 2025
a731ee5
fix: update folder name
Jan 13, 2025
5b47f13
fix: eslint
Jan 13, 2025
482e9fd
fix: eslint
Jan 13, 2025
39870e0
fix: eslint
Jan 13, 2025
b49ba4d
Merge branch 'feat/1580-list-favorite-activity-in-consep' into feat/1…
Jan 14, 2025
25913ea
fix: address pr comments
Jan 14, 2025
9f5fc99
fix: merge conflit
Jan 14, 2025
157ad4a
fix: address more pr comments
Jan 21, 2025
3fc3c9e
fix: address merge conflits
Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
* @param activity The activity or page name
*/
@Schema(description = "An object representing the request body when creating a favourite activity")
public record FavouriteActivityCreateDto(@NotNull String activity) {}
public record FavouriteActivityCreateDto(
@NotNull String activity,
@Schema(description = "Indicates whether this activity is from Consep", defaultValue = "false")
Boolean isConsep) {}
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,29 @@ public class FavouriteActivityEndpoint {
* Creates to the logged user a {@link FavouriteActivityEntity} record based on the activity or
* page title.
*
* @param createDto a {@link FavouriteActivityCreateDto} with the activity title
* @param createDtos a {@link FavouriteActivityCreateDto} with the activity title
* @return a {@link FavouriteActivityEntity} created
*/
@PostMapping(consumes = "application/json", produces = "application/json")
@Operation(
summary = "Creates a Favourite Activity",
summary = "Creates a Favourite Activities in bulk",
description =
"""
Creates a Favourite Activity to the logged user based on the activity
title or page name.
Creates Favourite Activities for the logged user in bulk based on an array
of activity titles or page names, with optional isConsep flags.
""")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "The Favourite Activity entity was successfully created",
description = "The Favourite Activities were successfully created",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = FavouriteActivityEntity.class))),
@ApiResponse(
responseCode = "400",
description = "The activity doesn't exists or is already defined to that user",
description = "One or more activities failed validation or already exist",
content =
@Content(
mediaType = "application/json",
Expand All @@ -82,16 +82,17 @@ public class FavouriteActivityEndpoint {
content = @Content(schema = @Schema(implementation = Void.class)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public ResponseEntity<FavouriteActivityEntity> createUserActivity(
public ResponseEntity<List<FavouriteActivityEntity>> createUserActivities(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Body containing the activity name that will be created",
required = true,
content =
@Content(schema = @Schema(implementation = FavouriteActivityCreateDto.class)))
@Valid
@RequestBody
FavouriteActivityCreateDto createDto) {
FavouriteActivityEntity entity = favouriteActivityService.createUserActivity(createDto);
List<FavouriteActivityCreateDto> createDtos) {
List<FavouriteActivityEntity> entity =
favouriteActivityService.createUserActivities(createDtos);
return ResponseEntity.status(HttpStatus.CREATED).body(entity);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public class FavouriteActivityEntity {
@Column(name = "update_timestamp")
private LocalDateTime updateTimestamp;

@Column(name = "is_consep", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
@Schema(
description = "Defines if the favourite activity is a CONSEP activity",
example = "false")
private Boolean isConsep = false;

public FavouriteActivityEntity() {
this.highlighted = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface FavouriteActivityRepository extends CrudRepository<FavouriteAct

Optional<FavouriteActivityEntity> findByActivity(String activity);

boolean existsByUserIdAndActivity(String userId, String activity);

@Modifying
@Query("update FavouriteActivityEntity set highlighted = false where userId = ?1")
void removeAllHighlightedByUser(String userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ca.bc.gov.backendstartapi.repository.FavouriteActivityRepository;
import ca.bc.gov.backendstartapi.security.LoggedUserService;
import jakarta.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -36,33 +37,53 @@ public FavouriteActivityService(
this.favouriteActivityRepository = favouriteActivityRepository;
}

/**
* Validates the activity input.
*/
xiaopeng0202 marked this conversation as resolved.
Show resolved Hide resolved
private void validateActivityInput(FavouriteActivityCreateDto activityDto) {
if (Objects.isNull(activityDto.activity()) || activityDto.activity().isBlank()) {
throw new InvalidActivityException();
}
}

/**
* Builds a FavouriteActivityEntity.
*/
xiaopeng0202 marked this conversation as resolved.
Show resolved Hide resolved
private FavouriteActivityEntity buildFavouriteActivityEntity(
String userId, FavouriteActivityCreateDto dto) {
FavouriteActivityEntity entity = new FavouriteActivityEntity();
entity.setUserId(userId);
entity.setActivity(dto.activity());
entity.setIsConsep(Optional.ofNullable(dto.isConsep()).orElse(false));
return entity;
}

/**
* Create a user's activity in the database.
*
* @param activityDto a {@link FavouriteActivityCreateDto} containing the activity title
* @param activityDtos a {@link FavouriteActivityCreateDto} containing the activity title
* @return the {@link FavouriteActivityEntity} created
*/
public FavouriteActivityEntity createUserActivity(FavouriteActivityCreateDto activityDto) {
public List<FavouriteActivityEntity> createUserActivities(
List<FavouriteActivityCreateDto> activityDtos) {
String userId = loggedUserService.getLoggedUserId();
SparLog.info("Creating activity {} for user {}", activityDto.activity(), userId);

if (Objects.isNull(activityDto.activity()) || activityDto.activity().isBlank()) {
throw new InvalidActivityException();
}

List<FavouriteActivityEntity> userFavList = favouriteActivityRepository.findAllByUserId(userId);
if (userFavList.stream().anyMatch(ac -> ac.getActivity().equals(activityDto.activity()))) {
SparLog.info("Activity {} already exists for user {}!", activityDto.activity(), userId);
throw new FavoriteActivityExistsToUser();
SparLog.info("Creating activities for user {}", userId);

List<FavouriteActivityEntity> createdActivities = new ArrayList<>();

for (FavouriteActivityCreateDto dto : activityDtos) {
try {
validateActivityInput(dto);
if (favouriteActivityRepository.existsByUserIdAndActivity(userId, dto.activity())) {
continue;
}
FavouriteActivityEntity entity = buildFavouriteActivityEntity(userId, dto);
createdActivities.add(favouriteActivityRepository.save(entity));
} catch (InvalidActivityException | FavoriteActivityExistsToUser e) {
SparLog.error("Error creating activity: {}", e.getMessage());
}
}

FavouriteActivityEntity activityEntity = new FavouriteActivityEntity();
activityEntity.setUserId(userId);
activityEntity.setActivity(activityDto.activity());

FavouriteActivityEntity activityEntitySaved = favouriteActivityRepository.save(activityEntity);
SparLog.info("Activity {} created for user {}", activityDto.activity(), userId);
return activityEntitySaved;
return createdActivities;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE
spar.favourite_activity
ADD
COLUMN is_consep BOOLEAN DEFAULT FALSE NOT NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,27 @@ private FavouriteActivityEntity createEntity(String activity) {
@Test
@DisplayName("createFavoriteActivitySuccessTest")
void createFavoriteActivitySuccessTest() throws Exception {
String content = "[{\"activity\":\"CREATE_A_CLASS_SEEDLOT\"}]";
FavouriteActivityEntity activityEntity = createEntity("CREATE_A_CLASS_SEEDLOT");
when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(stringifyCreate("CREATE_A_CLASS_SEEDLOT")))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();
}

@Test
@DisplayName("createFavoriteActivityNotFoundTest")
void createFavoriteActivityNotFoundTest() throws Exception {
when(favouriteActivityService.createUserActivity(any()))
when(favouriteActivityService.createUserActivities(any()))
.thenThrow(new InvalidActivityException());

mockMvc
Expand All @@ -95,23 +96,23 @@ void createFavoriteActivityNotFoundTest() throws Exception {
@Test
@DisplayName("createFavoriteActivityDuplicatedTest")
void createFavoriteActivityDuplicatedTest() throws Exception {
String contentString = stringifyCreate("CREATE_A_CLASS_SEEDLOT");
String content = "[{\"activity\":\"CREATE_A_CLASS_SEEDLOT\"}]";
FavouriteActivityEntity activityEntity = createEntity("CREATE_A_CLASS_SEEDLOT");
when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(contentString))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();

when(favouriteActivityService.createUserActivity(any()))
when(favouriteActivityService.createUserActivities(any()))
.thenThrow(new FavoriteActivityExistsToUser());

mockMvc
Expand All @@ -120,7 +121,7 @@ void createFavoriteActivityDuplicatedTest() throws Exception {
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(contentString))
.content(content))
.andExpect(status().isBadRequest())
.andReturn();
}
Expand Down Expand Up @@ -153,20 +154,21 @@ void getAllUsersActivityTest() throws Exception {
@Test
@DisplayName("updateUserFavoriteActivity")
void updateUserFavoriteActivity() throws Exception {
String content = "[{\"activity\":\"EXISTING_SEEDLOTS\"}]";
FavouriteActivityEntity activityEntity = createEntity("EXISTING_SEEDLOTS");
activityEntity.setId(10000L);
when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(stringifyCreate("EXISTING_SEEDLOTS")))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();

activityEntity.setHighlighted(true);
Expand All @@ -191,21 +193,22 @@ void updateUserFavoriteActivity() throws Exception {
@Test
@DisplayName("deleteUserFavoriteActivity")
void deleteUserFavoriteActivity() throws Exception {
String content = "[{\"activity\":\"EXISTING_SEEDLOTS\"}]";
FavouriteActivityEntity activityEntity = createEntity("EXISTING_SEEDLOTS");
activityEntity.setId(10000L);

when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(stringifyCreate("EXISTING_SEEDLOTS")))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();

activityEntity.setHighlighted(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import ca.bc.gov.backendstartapi.dto.FavouriteActivityCreateDto;
import ca.bc.gov.backendstartapi.dto.FavouriteActivityUpdateDto;
import ca.bc.gov.backendstartapi.entity.FavouriteActivityEntity;
import ca.bc.gov.backendstartapi.exception.FavoriteActivityExistsToUser;
import ca.bc.gov.backendstartapi.exception.InvalidActivityException;
import ca.bc.gov.backendstartapi.repository.FavouriteActivityRepository;
import ca.bc.gov.backendstartapi.security.LoggedUserService;
Expand Down Expand Up @@ -49,50 +48,17 @@ void createUserActivityTest() {
entity.setHighlighted(Boolean.FALSE);
when(favouriteActivityRepository.save(any())).thenReturn(entity);

FavouriteActivityCreateDto createDto = new FavouriteActivityCreateDto("CREATE_A_CLASS_SEEDLOT");
FavouriteActivityEntity entitySaved = favouriteActivityService.createUserActivity(createDto);
FavouriteActivityCreateDto createDto =
new FavouriteActivityCreateDto("CREATE_A_CLASS_SEEDLOT", false);
List<FavouriteActivityEntity> entitiesSaved =
favouriteActivityService.createUserActivities(List.of(createDto));
FavouriteActivityEntity entitySaved = entitiesSaved.get(0);

Assertions.assertNotNull(entitySaved);
Assertions.assertEquals("CREATE_A_CLASS_SEEDLOT", entitySaved.getActivity());
Assertions.assertFalse(entitySaved.getHighlighted());
}

@Test
@DisplayName("createUserActivityExceptionTest")
void createUserActivityExceptionTest() {
xiaopeng0202 marked this conversation as resolved.
Show resolved Hide resolved
when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);

FavouriteActivityEntity entity = new FavouriteActivityEntity();
entity.setActivity("CREATE_A_CLASS_SEEDLOT");
entity.setHighlighted(Boolean.FALSE);
when(favouriteActivityRepository.save(any())).thenReturn(entity);

FavouriteActivityCreateDto createDto = new FavouriteActivityCreateDto(null);

Exception notFoundExc =
Assertions.assertThrows(
InvalidActivityException.class,
() -> favouriteActivityService.createUserActivity(createDto));

Assertions.assertEquals(
"404 NOT_FOUND \"Invalid or not found activity id!\"", notFoundExc.getMessage());

List<FavouriteActivityEntity> userFavList = List.of(entity);
when(favouriteActivityRepository.findAllByUserId(any())).thenReturn(userFavList);

FavouriteActivityCreateDto createAnotherDto =
new FavouriteActivityCreateDto("CREATE_A_CLASS_SEEDLOT");

Exception activityExists =
Assertions.assertThrows(
FavoriteActivityExistsToUser.class,
() -> favouriteActivityService.createUserActivity(createAnotherDto));

Assertions.assertEquals(
"400 BAD_REQUEST \"Activity already registered to this user!\"",
activityExists.getMessage());
}

@Test
@DisplayName("getAllUserFavoriteActivitiesTest")
void getAllUserFavoriteActivitiesTest() {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api-service/favouriteActivitiesAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export const getFavAct = () => {
});
};

export const postFavAct = (newAct: FavActivityPostType) => {
export const postFavAct = (newActs: FavActivityPostType[]) => {
const url = ApiConfig.favouriteActivities;
return api.post(url, newAct);
return api.post(url, newActs);
};

export const patchFavAct = (field: string, activity: FavActivityType) => {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/assets/img/fav-icon.svg
xiaopeng0202 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading