Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions doc/release-notes/12524-sort-guestbook-responses.md
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 11 additions & 2 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -110,24 +112,53 @@ public List<Long> 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<GuestbookResponse> findAllByGuestbookId(Long guestbookId) {
return findAllByGuestbookId(guestbookId, null, null);
private Order getOrderBy(CriteriaBuilder cb, Path<Object> pathObj, boolean isDescending) {
return isDescending ? cb.desc(pathObj) : cb.asc(pathObj);
}
public List<GuestbookResponse> findAllByGuestbookId(Long guestbookId, Integer offset, Integer limit) {
public List<GuestbookResponse> findAllByGuestbookId(Long guestbookId, String sortField, String sortOrder, Integer offset, Integer limit) {
if (guestbookId != null) {
TypedQuery<GuestbookResponse> 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<GuestbookResponse> cq = cb.createQuery(GuestbookResponse.class);
Root<GuestbookResponse> 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<GuestbookResponse, DataFile> 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
Expand Down
13 changes: 6 additions & 7 deletions src/main/java/edu/harvard/iq/dataverse/api/Guestbooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -195,7 +194,7 @@ public Response getResponses(@Context ContainerRequestContext crc,
guestbook.setUsageCount(totalUsageCount);
guestbook.setResponseCount(totalResponseCount);

List<GuestbookResponse> responses = guestbookResponseService.findAllByGuestbookId(guestbook.getId(), offset, limit);
List<GuestbookResponse> responses = guestbookResponseService.findAllByGuestbookId(guestbook.getId(), sortField, sortOrder, offset, limit);

JsonObjectBuilder guestbookResponseObject = jsonObjectBuilder();
guestbookResponseObject.add("guestbook", JsonPrinter.json(guestbook));
Expand Down
30 changes: 29 additions & 1 deletion src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -4117,14 +4117,21 @@ 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
int pagedTotalCount = 0;
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();
Expand All @@ -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<String,String> 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();
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
}

Expand Down
Loading