diff --git a/doc/release-notes/12524-sort-guestbook-responses.md b/doc/release-notes/12524-sort-guestbook-responses.md new file mode 100644 index 00000000000..6c2a402436d --- /dev/null +++ b/doc/release-notes/12524-sort-guestbook-responses.md @@ -0,0 +1,2 @@ +## Feature ## +This feature adds the ability to sort the Guestbook Responses in the API /api/guestbooks/{id}/responses. Responses can be sorted by Event Type(type), File Name(file), User Name(user), and Response Date(date) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 582891e0861..e68471b1a0e 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1443,6 +1443,15 @@ Retrieve Guestbook Responses for a Guestbook For more about guestbooks, see :ref:`dataset-guestbooks` in the User Guide. In order to retrieve the Guestbook Responses for a Guestbook within a Dataverse collection, you must know the ID if the Guestbook. This API also supports pagination by passing a page limit and an optional offset (starting point). The resulting Json will include 'Next' and 'Prev' urls for navigation as well as the total number of responses. +The Responses can be sorted by specifying one of the following in query parameter 'sort' and 'order': + +* ``type``: Event Type +* ``file``: File Name +* ``user``: User Name +* ``date``: Response Date (Default) + +To sort in reverse order you can add ``&order=desc``. + The resulting Json will be more detailed than that of the :ref:`download-guestbook-api` CSV response file by including Guestbook metadata as well as Guestbook Response metadata. .. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of ``export`` below. @@ -1454,14 +1463,14 @@ The resulting Json will be more detailed than that of the :ref:`download-guestbo export ID=1 curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/guestbooks/$ID/responses" - curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/guestbooks/$ID/responses?limit10&offset=0" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/guestbooks/$ID/responses?limit10&offset=0&sort=file&order=asc" The fully expanded example above (without environment variables) looks like this: .. code-block:: bash curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/guestbooks/1/responses" - curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/guestbooks/1/responses?limit10&offset=0" + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/guestbooks/1/responses?limit10&offset=0&sort=type&order=desc" .. _collection-attributes-api: diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java index 8ddda6a2a7e..9c764acd065 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java @@ -8,6 +8,7 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.externaltools.ExternalTool; +import edu.harvard.iq.dataverse.search.SortBy; import edu.harvard.iq.dataverse.util.StringUtil; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; @@ -19,6 +20,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.*; import org.apache.commons.text.StringEscapeUtils; import java.io.IOException; @@ -110,24 +112,53 @@ public List findAllIds(Long dataverseId) { return em.createQuery("select o.id from GuestbookResponse o, Dataset d where o.dataset.id = d.id and d.owner.id = " + dataverseId + " order by o.responseTime desc", Long.class).getResultList(); } - public List findAllByGuestbookId(Long guestbookId) { - return findAllByGuestbookId(guestbookId, null, null); + private Order getOrderBy(CriteriaBuilder cb, Path pathObj, boolean isDescending) { + return isDescending ? cb.desc(pathObj) : cb.asc(pathObj); } - public List findAllByGuestbookId(Long guestbookId, Integer offset, Integer limit) { + public List findAllByGuestbookId(Long guestbookId, String sortField, String sortOrder, Integer offset, Integer limit) { if (guestbookId != null) { - TypedQuery query = em.createQuery("select o from GuestbookResponse as o where o.guestbook.id = " + guestbookId + " order by o.responseTime desc", GuestbookResponse.class); - if (offset != null) { - query.setFirstResult(offset); + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(GuestbookResponse.class); + Root guestbookResponseRoot = cq.from(GuestbookResponse.class); + + boolean isDescending = sortOrder != null && sortOrder.equalsIgnoreCase(SortBy.DESCENDING); + Order order; + String orderField = (sortField == null) ? "" : sortField.toLowerCase(); + switch(orderField) { + case "date": + order = getOrderBy(cb, guestbookResponseRoot.get("responseTime"), isDescending); + break; + case "type": + order = getOrderBy(cb, guestbookResponseRoot.get("eventType"), isDescending); + break; + case "file": + Join dataFileJoin = guestbookResponseRoot.join("dataFile", JoinType.INNER); + order = getOrderBy(cb, dataFileJoin.get("fileMetadatas").get("label"), isDescending); + break; + case "user": + order = getOrderBy(cb, guestbookResponseRoot.get("name"), isDescending); + break; + default: + order = null; } - if (limit != null) { - query.setMaxResults(limit); + + cq.where(cb.equal(guestbookResponseRoot.get("guestbook").get("id"), guestbookId)); + if (order != null) { + cq.orderBy(order); } + cq.distinct(true); + + int firstResult = offset == null ? 0 : offset; + int pageSize = limit == null ? Integer.MAX_VALUE : limit; - return query.getResultList(); + return em.createQuery(cq) + .setFirstResult(firstResult) + .setMaxResults(pageSize) + .getResultList(); } return null; } - + /* This method is used for streaming downloads of guestbook responses, in CSV format, both for individual guestbooks, and for entire dataverses diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Guestbooks.java b/src/main/java/edu/harvard/iq/dataverse/api/Guestbooks.java index 1867b0b515b..f2f648974e4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Guestbooks.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Guestbooks.java @@ -174,12 +174,11 @@ public Response updateGuestbook(@Context ContainerRequestContext crc, @Operation(summary = "Lists guestbook responses", description = "Returns guestbook metadata and response records, with pagination links when a limit is supplied.") public Response getResponses(@Context ContainerRequestContext crc, - @Parameter(description = "Numeric id of the guestbook whose responses are listed.", required = true) - @PathParam("id") Long id, - @Parameter(description = "Maximum number of response records to return.") - @QueryParam("limit") Integer limit, - @Parameter(description = "Response record offset.") - @QueryParam("offset") Integer offset) { + @Parameter(description = "Numeric id of the guestbook whose responses are listed.", required = true) @PathParam("id") Long id, + @Parameter(description = "Sort Field. One of: 'Dataset'; 'Date'; 'Type'; 'File'; 'User'") @QueryParam("sort") String sortField, + @Parameter(description = "Sort order. ('asc' or 'desc')") @QueryParam("order") String sortOrder, + @Parameter(description = "Maximum number of response records to return.") @QueryParam("limit") Integer limit, + @Parameter(description = "Response record offset.") @QueryParam("offset") Integer offset) { return response( req -> { Guestbook guestbook = guestbookService.find(id); @@ -195,7 +194,7 @@ public Response getResponses(@Context ContainerRequestContext crc, guestbook.setUsageCount(totalUsageCount); guestbook.setResponseCount(totalResponseCount); - List responses = guestbookResponseService.findAllByGuestbookId(guestbook.getId(), offset, limit); + List responses = guestbookResponseService.findAllByGuestbookId(guestbook.getId(), sortField, sortOrder, offset, limit); JsonObjectBuilder guestbookResponseObject = jsonObjectBuilder(); guestbookResponseObject.add("guestbook", JsonPrinter.json(guestbook)); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 7bc06f7c92c..47f3d704c71 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -4117,6 +4117,13 @@ public void testDownloadFileWithGuestbookResponse() throws IOException, JsonPars assertTrue(totalCount > 0); assertNotNull(jsonPath.getString("data.responses[0].name")); + // Test Get All Responses Sorted + testSortByField(guestbook.getId(), "file", "asc", ownerApiToken); + testSortByField(guestbook.getId(), "user", "asc", ownerApiToken); + testSortByField(guestbook.getId(), "user", "desc", ownerApiToken); + testSortByField(guestbook.getId(), "date", "asc", ownerApiToken); + testSortByField(guestbook.getId(), "date", "desc", ownerApiToken); + // Test Get Responses with pagination int pages = 4; // total should be 17. set to 4 pages int limit = (totalCount / pages) + 1; // should be 5 per page. we should see 5, 5, 5, 2 @@ -4124,7 +4131,7 @@ public void testDownloadFileWithGuestbookResponse() throws IOException, JsonPars int totalCountFromJson = 0; for (int i = 0; i < pages; i++) { int offset = limit * i; - guestbookListResponses = UtilIT.getGuestbooksResponses(guestbook.getId(), offset, limit, ownerApiToken); + guestbookListResponses = UtilIT.getGuestbooksResponses(guestbook.getId(), "date", null, offset, limit, ownerApiToken); guestbookListResponses.prettyPrint(); jsonPath = JsonPath.from(guestbookListResponses.body().asString()); pagedTotalCount += jsonPath.getList("data.responses").size(); @@ -4136,6 +4143,27 @@ public void testDownloadFileWithGuestbookResponse() throws IOException, JsonPars assertEquals(pagedTotalCount, totalCountFromJson); } + private void testSortByField(Long id, String sortField, String order, String token) { + Response guestbookListResponses = UtilIT.getGuestbooksResponses(id, sortField, order, 0, Integer.MAX_VALUE, token); + Map fieldMap = Map.of("type", "type", "user", "name", "file", "fileName", "date", "date"); + String fieldName = fieldMap.get(sortField); + boolean isDescending = order != null && order.equalsIgnoreCase("desc"); + guestbookListResponses.prettyPrint(); + guestbookListResponses.then().assertThat() + .statusCode(OK.getStatusCode()); + JsonPath jsonPath = JsonPath.from(guestbookListResponses.body().asString()); + int totalCount = jsonPath.getList("data.responses").size(); + assertTrue(totalCount > 0); + String lastFieldValue = jsonPath.getString("data.responses[0]." + fieldName).toLowerCase(); // The sort seems to be case-insensitive + for (int i = 1; i < totalCount; i++) { + String fieldValue = jsonPath.getString("data.responses[" + i + "]." + fieldName).toLowerCase(); + assertTrue(isDescending ? fieldValue.compareTo(lastFieldValue) <= 0 : fieldValue.compareTo(lastFieldValue) >= 0); + lastFieldValue = fieldValue; + } + int totalCountFromJson = jsonPath.getInt("data.pagination.totalResponses"); + assertEquals(totalCountFromJson, totalCount); + } + @Test public void testGetFileCitationFormatted() { Response createUser = UtilIT.createRandomUser(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index b6f8d66c0af..b1b3173faa5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -641,6 +641,9 @@ static Response getGuestbooks(String dataverseAlias, String apiToken, boolean in } static Response getGuestbooksResponses(Long guestbookId, Integer offset, Integer limit, String apiToken) { + return getGuestbooksResponses(guestbookId, null, null, offset, limit, apiToken); + } + static Response getGuestbooksResponses(Long guestbookId, String sortField, String sortOrder, Integer offset, Integer limit, String apiToken) { RequestSpecification requestSpec = given(); if (apiToken != null) { requestSpec.header(API_TOKEN_HTTP_HEADER, apiToken); @@ -651,6 +654,12 @@ static Response getGuestbooksResponses(Long guestbookId, Integer offset, Integer if (limit != null) { requestSpec.queryParam("limit", limit); } + if (sortField != null) { + requestSpec.queryParam("sort", sortField); + } + if (sortOrder != null) { + requestSpec.queryParam("order", sortOrder); + } return requestSpec.get("/api/guestbooks/" + guestbookId + "/responses"); }