-
Notifications
You must be signed in to change notification settings - Fork 6
Add bookmark functionality #777
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
Merged
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
96f64fc
Add bookmark endpoints
axlewin 5d303fd
Augment question results with bookmarked status
axlewin dbc2e45
Store more precise timestamp information for bookmarks
axlewin 3be8bbd
Add bookmarks table migration
axlewin 4ed4a46
Rename method
axlewin 01e6379
Move bookmark content augmentation out of DB layer
axlewin c87fb8b
Move bookmark content type check out of DB layer
axlewin fe4dc20
Avoid passing entire user object to bookmarks DB manager
axlewin 2563d3c
Move bookmark content type checking to facade
axlewin afbf7df
Add pg test data for user_bookmarks
axlewin 638ade5
Add user_bookmarks to DB creation script
axlewin afd9134
Update bookmarks PG test data to use IDs from ES test data
axlewin 7930013
Correct response code for successful requests
axlewin 495d61a
Add integration tests for bookmarks facade
axlewin 96a92c2
Improve responses for invalid bookmark requests
axlewin 76b8e5c
Update AbstractIsaacIntegrationTest with bookmark objects
axlewin 2f6c3dd
Extend bookmarks integration tests
axlewin 49c760a
Merge branch 'main' into feature/bookmarks
axlewin 6215cd1
Clarify & improve various bookmarks functionality
axlewin d2b888a
Fix test cleanup
axlewin c6df3aa
Improve logging & error responses
axlewin b474152
Remove redundant content type check
axlewin 5de26fb
Use "no content" HTTP response for successful bookmarks deletions
axlewin d72fabc
Update status code in test
axlewin b492bff
Move content ID to path param
axlewin e87e942
Replace more stack traces with log messages
axlewin d97b489
Augment bookmarks with content info more efficiently
axlewin 26eb621
Fix tests
axlewin 8705404
Make BookmarksFacade extend AbstractIsaacFacade
axlewin b166b3e
Move max number of bookmarks to constant
axlewin 04f288c
Store all bookmark properties in BookmarkDO
axlewin 19eb56f
Move bookmarks DB layer to DAO package
axlewin ea2e252
Avoid direct interaction with DB layer from bookmarks facade
axlewin 6ee4c85
Remove redundant content type check
axlewin df85625
Fix date conversion
axlewin e412da6
Add logged events for adding/removing bookmarks
axlewin c8ddd78
Skip content augmentation if bookmarks list is empty
axlewin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
195 changes: 195 additions & 0 deletions
195
src/main/java/uk/ac/cam/cl/dtg/isaac/api/BookmarksFacade.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| package uk.ac.cam.cl.dtg.isaac.api; | ||
|
|
||
| import com.google.common.collect.ImmutableMap; | ||
| import com.google.inject.Inject; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import uk.ac.cam.cl.dtg.isaac.api.managers.BookmarksManager; | ||
| import uk.ac.cam.cl.dtg.isaac.dos.BookmarkDO; | ||
| import uk.ac.cam.cl.dtg.isaac.dto.SegueErrorResponse; | ||
| import uk.ac.cam.cl.dtg.isaac.dto.content.ContentDTO; | ||
| import uk.ac.cam.cl.dtg.isaac.dto.content.ContentSummaryDTO; | ||
| import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; | ||
| import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager; | ||
| import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException; | ||
| import uk.ac.cam.cl.dtg.segue.dao.ILogManager; | ||
| import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; | ||
| import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; | ||
| import uk.ac.cam.cl.dtg.util.AbstractConfigLoader; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.ws.rs.DELETE; | ||
| import jakarta.ws.rs.GET; | ||
| import jakarta.ws.rs.POST; | ||
| import jakarta.ws.rs.Path; | ||
| import jakarta.ws.rs.PathParam; | ||
| import jakarta.ws.rs.Produces; | ||
| import jakarta.ws.rs.QueryParam; | ||
| import jakarta.ws.rs.core.Context; | ||
| import jakarta.ws.rs.core.MediaType; | ||
| import jakarta.ws.rs.core.Response; | ||
| import jakarta.ws.rs.core.Response.Status; | ||
| import java.util.Date; | ||
| import java.util.List; | ||
|
|
||
| import static uk.ac.cam.cl.dtg.isaac.api.Constants.*; | ||
|
|
||
| /** | ||
| * Bookmarks Facade | ||
| * | ||
| * Responsible for handling requests related to user bookmarks. | ||
| */ | ||
| @Path("/bookmarks") | ||
| @Tag(name = "BookmarksFacade", description = "/bookmarks") | ||
| public class BookmarksFacade extends AbstractIsaacFacade { | ||
| private static final Logger log = LoggerFactory.getLogger(BookmarksFacade.class); | ||
|
|
||
| private final UserAccountManager userManager; | ||
| private final GitContentManager contentManager; | ||
| private final BookmarksManager bookmarksManager; | ||
|
|
||
| @Inject | ||
| public BookmarksFacade(final AbstractConfigLoader propertiesLoader, final ILogManager logManager, | ||
| final UserAccountManager userManager, final GitContentManager contentManager, | ||
| final BookmarksManager bookmarksManager) { | ||
| super(propertiesLoader, logManager); | ||
|
|
||
| this.userManager = userManager; | ||
| this.contentManager = contentManager; | ||
| this.bookmarksManager = bookmarksManager; | ||
| } | ||
|
|
||
| /** | ||
| * Gets a list of content bookmarked by the current user. | ||
| * | ||
| * @param request | ||
| * - so we can find the current user. | ||
| * @param contentType | ||
| * - the type of content to filter bookmarks by, or null to return all bookmarks. | ||
| * @return the list of content that the user has bookmarked. | ||
| */ | ||
| @GET | ||
| @Path("/") | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| @Operation(summary = "Get bookmarks for the current user.") | ||
| public final Response getCurrentUserBookmarks(@Context final HttpServletRequest request, | ||
| @QueryParam("content_type") final String contentType) { | ||
| RegisteredUserDTO user; | ||
| try { | ||
| user = userManager.getCurrentRegisteredUser(request); | ||
| } catch (final NoUserLoggedInException e) { | ||
| return SegueErrorResponse.getNotLoggedInResponse(); | ||
| } | ||
|
|
||
| if (null != contentType && !contentType.isEmpty() | ||
| && !(contentType.equals("isaacQuestionPage") || contentType.equals("isaacConceptPage"))) { | ||
| log.warn("Invalid content type provided for bookmarks query: {}", contentType); | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "Only question and concept pages can be bookmarked!").toResponse(); | ||
| } | ||
|
|
||
| List<ContentSummaryDTO> bookmarks = bookmarksManager.getAugmentedBookmarksForUser(user.getId(), contentType); | ||
| return Response.ok(bookmarks).build(); | ||
| } | ||
|
|
||
| /** | ||
| * Adds a bookmark for the current user. | ||
| * | ||
| * @param request | ||
| * - so we can find the current user. | ||
| * @param contentId | ||
| * - the id of the content to bookmark. | ||
| */ | ||
| @POST | ||
| @Path("/{content_id}") | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| @Operation(summary = "Add a bookmark for the current user.") | ||
| public final Response addCurrentUserBookmark(@Context final HttpServletRequest request, | ||
| @PathParam("content_id") final String contentId) { | ||
| RegisteredUserDTO user; | ||
| try { | ||
| user = userManager.getCurrentRegisteredUser(request); | ||
| } catch (final NoUserLoggedInException e) { | ||
| return SegueErrorResponse.getNotLoggedInResponse(); | ||
| } | ||
|
|
||
| if (null == contentId || contentId.isEmpty()) { | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "Cannot create bookmark without content ID.").toResponse(); | ||
| } | ||
|
|
||
| List<BookmarkDO> currentBookmarks = bookmarksManager.getBookmarksForUser(user.getId(), null); | ||
|
|
||
| if (currentBookmarks.size() >= MAXIMUM_BOOKMARKS) { | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "You already have the maximum number of bookmarks!.").toResponse(); | ||
| } | ||
|
|
||
| if (currentBookmarks.stream().anyMatch(b -> b.contentId().equals(contentId))) { | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "You have already bookmarked this content.").toResponse(); | ||
| } | ||
|
|
||
| String contentType; | ||
| try { | ||
| ContentDTO content = this.contentManager.getContentById(contentId); | ||
| contentType = content.getType(); | ||
| if (null == contentType || !(contentType.equals("isaacQuestionPage") || contentType.equals("isaacConceptPage"))) { | ||
| log.warn("Invalid content type provided for bookmarks query: {}", contentType); | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "Only question and concept pages can be bookmarked!").toResponse(); | ||
| } | ||
|
|
||
| BookmarkDO bookmarkToAdd = new BookmarkDO(user.getId(), contentId, contentType, new Date()); | ||
| bookmarksManager.addBookmarkForUser(bookmarkToAdd); | ||
|
|
||
| } catch (final ContentManagerException | NullPointerException e) { | ||
| log.warn("Failed to create bookmark, could not find content with ID: {}", contentId); | ||
|
jsharkey13 marked this conversation as resolved.
Dismissed
|
||
| return new SegueErrorResponse(Status.NOT_FOUND, "Unable to find content to bookmark.").toResponse(); | ||
| } | ||
|
|
||
| this.getLogManager().logEvent(user, request, IsaacServerLogType.ADD_BOOKMARK, | ||
| ImmutableMap.of(BOOKMARK_USER_ID_LOG_FIELDNAME, user.getId(), | ||
| BOOKMARK_CONTENT_ID_LOG_FIELDNAME, contentId, | ||
| BOOKMARK_CONTENT_TYPE_LOG_FIELDNAME, contentType)); | ||
|
|
||
| return Response.ok().build(); | ||
| } | ||
|
|
||
| /** | ||
| * Removes a bookmark for the current user. | ||
| * | ||
| * @param request | ||
| * - so we can find the current user. | ||
| * @param contentId | ||
| * - the id of the content to be removed from the user's bookmarks. | ||
| */ | ||
| @DELETE | ||
| @Path("/{content_id}") | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| @Operation(summary = "Remove a bookmark for the current user.") | ||
| public final Response removeCurrentUserBookmark(@Context final HttpServletRequest request, | ||
| @PathParam("content_id") final String contentId) { | ||
| RegisteredUserDTO user; | ||
| try { | ||
| user = userManager.getCurrentRegisteredUser(request); | ||
| } catch (final NoUserLoggedInException e) { | ||
| return SegueErrorResponse.getNotLoggedInResponse(); | ||
| } | ||
|
|
||
| if (null == contentId || contentId.isEmpty()) { | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "Cannot delete bookmark without content ID.").toResponse(); | ||
| } | ||
|
|
||
| List<BookmarkDO> currentBookmarks = bookmarksManager.getBookmarksForUser(user.getId(), null); | ||
| if (currentBookmarks.stream().noneMatch(b -> b.contentId().equals(contentId))) { | ||
| return new SegueErrorResponse(Status.BAD_REQUEST, "You have not bookmarked this content.").toResponse(); | ||
| } | ||
|
|
||
| BookmarkDO bookmarkToRemove = new BookmarkDO(user.getId(), contentId, null, null); | ||
| bookmarksManager.removeBookmarkForUser(bookmarkToRemove); | ||
|
|
||
| this.getLogManager().logEvent(user, request, IsaacServerLogType.DELETE_BOOKMARK, | ||
| ImmutableMap.of(BOOKMARK_USER_ID_LOG_FIELDNAME, user.getId(), | ||
| BOOKMARK_CONTENT_ID_LOG_FIELDNAME, contentId)); | ||
|
|
||
| return Response.noContent().build(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.