Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions doc/release-notes/11650-unread.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## API Updates

### Support read/unread status for notifications

The API for managing notifications has been extended.

- displayAsRead boolean added to "get all"
- new GET unreadCount API endpoint
- new PUT markAsRead API endpoint

See also [the guides](https://dataverse-guide--11664.org.readthedocs.build/en/11664/api/native-api.html#notifications), #11650, and #11664.
48 changes: 48 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5889,6 +5889,8 @@ Notifications

See :ref:`account-notifications` in the User Guide for an overview. For a list of all the notification types mentioned below (e.g. ASSIGNROLE), see :ref:`mute-notifications` in the Admin Guide.

.. _get-all-notifications:

Get All Notifications by User
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -5898,6 +5900,52 @@ Each user can get a dump of their notifications by passing in their API token:

curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all"

The expected OK (200) response looks something like this:

.. code-block:: text

{
"status": "OK",
"data": {
"notifications": [
{
"id": 38,
"type": "CREATEACC",
"displayAsRead": true,
"subjectText": "Root: Your account has been created",
"messageText": "Hello, \nWelcome to...",
"sentTimestamp": "2025-07-21T19:15:37Z"
}
...

Get Unread Count
~~~~~~~~~~~~~~~~

You can get a count of your unread notifications as shown below.

.. code-block:: bash

curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/notifications/unreadCount"
Comment thread
pdurbin marked this conversation as resolved.
Outdated

Mark Notification As Read
~~~~~~~~~~~~~~~~~~~~~~~~~

After finding the ID of a notification using :ref:`get-all-notifications`, you can pass it to the "markAsRead" API endpoint as shown below. Note that this endpoint is idempotent; you can mark an already-read notification as read over and over.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export NOTIFICATION_ID=555

curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/notifications/$NOTIFICATION_ID/markAsRead"

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/notifications/555/markAsRead"

Delete Notification by User
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ public UserNotification find(Object pk) {
public UserNotification save(UserNotification userNotification) {
return em.merge(userNotification);
}


public UserNotification markAsRead(UserNotification userNotification) {
userNotification.setReadNotification(true);
return em.merge(userNotification);
}

public void delete(UserNotification userNotification) {
em.remove(em.merge(userNotification));
}
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Notifications.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public Response getAllNotificationsForUser(@Context ContainerRequestContext crc)
Type type = notification.getType();
notificationObjectBuilder.add("id", notification.getId());
notificationObjectBuilder.add("type", type.toString());
notificationObjectBuilder.add("displayAsRead", notification.isReadNotification());
/* FIXME - Re-add reasons for return if/when they are added to the notifications page.
if (Type.RETURNEDDS.equals(type) || Type.SUBMITTEDDS.equals(type)) {
JsonArrayBuilder reasons = getReasonsForReturn(notification);
Expand All @@ -77,11 +78,48 @@ public Response getAllNotificationsForUser(@Context ContainerRequestContext crc)
return ok(result);
}

@GET
@AuthRequired
@Path("/unreadCount")
public Response getUnreadNotificationsCountForUser(@Context ContainerRequestContext crc) {
try {
AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc);
long unreadCount = userNotificationSvc.getUnreadNotificationCountByUser(au.getId());
return ok(Json.createObjectBuilder()
.add("unreadCount", unreadCount));
} catch (WrappedResponse wr) {
return wr.getResponse();
}
}

private JsonArrayBuilder getReasonsForReturn(UserNotification notification) {
Long objectId = notification.getObjectId();
return WorkflowUtil.getAllWorkflowComments(datasetVersionSvc.find(objectId));
}

@PUT
@AuthRequired
@Path("/{id}/markAsRead")
public Response markNotificationAsReadForUser(@Context ContainerRequestContext crc, @PathParam("id") long id) {
try {
AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc);
Long userId = au.getId();
Optional<UserNotification> notification = userNotificationSvc.findByUser(userId).stream().filter(x -> x.getId().equals(id)).findFirst();
if (notification.isPresent()) {
UserNotification saved = userNotificationSvc.markAsRead(notification.get());
if (saved.isReadNotification()) {
return ok("Notification " + id + " marked as read.");
} else {
return badRequest("Notification " + id + " could not be marked as read.");
}
} else {
return notFound("Notification " + id + " not found.");
}
} catch (WrappedResponse wr) {
return wr.getResponse();
}
}

@DELETE
@AuthRequired
@Path("/{id}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ public void displayNotification() {
userNotification.setDisplayAsRead(userNotification.isReadNotification());
if (userNotification.isReadNotification() == false) {
userNotification.setReadNotification(true);
// consider switching to userNotificationService.markAsRead
userNotificationService.save(userNotification);
}
}
Expand Down
43 changes: 39 additions & 4 deletions src/test/java/edu/harvard/iq/dataverse/api/NotificationsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.restassured.response.Response;
import java.util.logging.Logger;
import static jakarta.ws.rs.core.Response.Status.CREATED;
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
import static jakarta.ws.rs.core.Response.Status.OK;
import static org.hamcrest.CoreMatchers.equalTo;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -29,6 +30,12 @@ public void testNotifications() {
String authorUsername = UtilIT.getUsernameFromResponse(createAuthor);
String authorApiToken = UtilIT.getApiTokenFromResponse(createAuthor);

Response nopermsUser = UtilIT.createRandomUser();
nopermsUser.prettyPrint();
nopermsUser.then().assertThat()
.statusCode(OK.getStatusCode());
String nopermsApiToken = UtilIT.getApiTokenFromResponse(nopermsUser);

// Some API calls don't generate a notification: https://github.com/IQSS/dataverse/issues/1342
Response createDataverseResponse = UtilIT.createRandomDataverse(authorApiToken);
createDataverseResponse.prettyPrint();
Expand All @@ -41,22 +48,50 @@ public void testNotifications() {
createDataset.prettyPrint();
createDataset.then().assertThat()
.statusCode(CREATED.getStatusCode());

Response getNotifications = UtilIT.getNotifications(authorApiToken);
getNotifications.prettyPrint();
getNotifications.then().assertThat()
.body("data.notifications[0].type", equalTo("CREATEACC"))
.body("data.notifications[0].displayAsRead", equalTo(false))
.body("data.notifications[1]", equalTo(null))
.statusCode(OK.getStatusCode());

long id = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[0].id");
Response unreadCount = UtilIT.getUnreadNotificationsCount(authorApiToken);
unreadCount.prettyPrint();
unreadCount.then().assertThat()
.statusCode(OK.getStatusCode())
.body("data.unreadCount", equalTo(1));

Response deleteNotification = UtilIT.deleteNotification(id, authorApiToken);
deleteNotification.prettyPrint();
deleteNotification.then().assertThat().statusCode(OK.getStatusCode());
long createAccountId = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[0].id");

Response markReadNoPerms = UtilIT.markNotificationAsRead(createAccountId, nopermsApiToken);
markReadNoPerms.prettyPrint();
markReadNoPerms.then().assertThat().statusCode(NOT_FOUND.getStatusCode());

Response markRead = UtilIT.markNotificationAsRead(createAccountId, authorApiToken);
markRead.prettyPrint();
markRead.then().assertThat().statusCode(OK.getStatusCode());

Response getNotifications2 = UtilIT.getNotifications(authorApiToken);
getNotifications2.prettyPrint();
getNotifications2.then().assertThat()
.body("data.notifications[0].type", equalTo("CREATEACC"))
.body("data.notifications[0].displayAsRead", equalTo(true))
.body("data.notifications[1]", equalTo(null))
.statusCode(OK.getStatusCode());

Response deleteNotificationNoPerms = UtilIT.deleteNotification(createAccountId, nopermsApiToken);
deleteNotificationNoPerms.prettyPrint();
deleteNotificationNoPerms.then().assertThat().statusCode(NOT_FOUND.getStatusCode());

Response deleteNotification = UtilIT.deleteNotification(createAccountId, authorApiToken);
deleteNotification.prettyPrint();
deleteNotification.then().assertThat().statusCode(OK.getStatusCode());

Response getNotifications3 = UtilIT.getNotifications(authorApiToken);
getNotifications3.prettyPrint();
getNotifications3.then().assertThat()
.body("data.notifications[0]", equalTo(null))
.statusCode(OK.getStatusCode());

Expand Down
18 changes: 18 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 @@ -1710,6 +1710,24 @@ static Response getNotifications(String apiToken) {
return requestSpecification.get("/api/notifications/all");
}

static Response getUnreadNotificationsCount(String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
requestSpecification = given()
.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
}
return requestSpecification.get("/api/notifications/unreadCount");
}

static Response markNotificationAsRead(long id, String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
requestSpecification = given()
.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
}
return requestSpecification.put("/api/notifications/" + id + "/markAsRead");
}

static Response deleteNotification(long id, String apiToken) {
RequestSpecification requestSpecification = given();
if (apiToken != null) {
Expand Down