Skip to content

Commit d27d4c7

Browse files
authored
Merge pull request #11664 from IQSS/11650-unread
support read/unread status for notifications
2 parents 07a9647 + 5d753d1 commit d27d4c7

File tree

7 files changed

+161
-5
lines changed

7 files changed

+161
-5
lines changed

doc/release-notes/11650-unread.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## API Updates
2+
3+
### Support read/unread status for notifications
4+
5+
The API for managing notifications has been extended.
6+
7+
- displayAsRead boolean added to "get all"
8+
- new GET unreadCount API endpoint
9+
- new PUT markAsRead API endpoint
10+
11+
See also [the guides](https://dataverse-guide--11664.org.readthedocs.build/en/11664/api/native-api.html#notifications), #11650, and #11664.

doc/sphinx-guides/source/api/native-api.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5932,6 +5932,8 @@ Notifications
59325932
59335933
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.
59345934
5935+
.. _get-all-notifications:
5936+
59355937
Get All Notifications by User
59365938
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
59375939
@@ -5941,6 +5943,52 @@ Each user can get a dump of their notifications by passing in their API token:
59415943
59425944
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all"
59435945
5946+
The expected OK (200) response looks something like this:
5947+
5948+
.. code-block:: text
5949+
5950+
{
5951+
"status": "OK",
5952+
"data": {
5953+
"notifications": [
5954+
{
5955+
"id": 38,
5956+
"type": "CREATEACC",
5957+
"displayAsRead": true,
5958+
"subjectText": "Root: Your account has been created",
5959+
"messageText": "Hello, \nWelcome to...",
5960+
"sentTimestamp": "2025-07-21T19:15:37Z"
5961+
}
5962+
...
5963+
5964+
Get Unread Count
5965+
~~~~~~~~~~~~~~~~
5966+
5967+
You can get a count of your unread notifications as shown below.
5968+
5969+
.. code-block:: bash
5970+
5971+
curl -H "X-Dataverse-key:$API_TOKEN" -X GET "$SERVER_URL/api/notifications/unreadCount"
5972+
5973+
Mark Notification As Read
5974+
~~~~~~~~~~~~~~~~~~~~~~~~~
5975+
5976+
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.
5977+
5978+
.. code-block:: bash
5979+
5980+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
5981+
export SERVER_URL=https://demo.dataverse.org
5982+
export NOTIFICATION_ID=555
5983+
5984+
curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/notifications/$NOTIFICATION_ID/markAsRead"
5985+
5986+
The fully expanded example above (without environment variables) looks like this:
5987+
5988+
.. code-block:: bash
5989+
5990+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/notifications/555/markAsRead"
5991+
59445992
Delete Notification by User
59455993
~~~~~~~~~~~~~~~~~~~~~~~~~~~
59465994

src/main/java/edu/harvard/iq/dataverse/UserNotificationServiceBean.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ public UserNotification find(Object pk) {
8787
public UserNotification save(UserNotification userNotification) {
8888
return em.merge(userNotification);
8989
}
90-
90+
91+
public UserNotification markAsRead(UserNotification userNotification) {
92+
userNotification.setReadNotification(true);
93+
return em.merge(userNotification);
94+
}
95+
9196
public void delete(UserNotification userNotification) {
9297
em.remove(em.merge(userNotification));
9398
}

src/main/java/edu/harvard/iq/dataverse/api/Notifications.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public Response getAllNotificationsForUser(@Context ContainerRequestContext crc)
5454
Type type = notification.getType();
5555
notificationObjectBuilder.add("id", notification.getId());
5656
notificationObjectBuilder.add("type", type.toString());
57+
notificationObjectBuilder.add("displayAsRead", notification.isReadNotification());
5758
/* FIXME - Re-add reasons for return if/when they are added to the notifications page.
5859
if (Type.RETURNEDDS.equals(type) || Type.SUBMITTEDDS.equals(type)) {
5960
JsonArrayBuilder reasons = getReasonsForReturn(notification);
@@ -77,11 +78,48 @@ public Response getAllNotificationsForUser(@Context ContainerRequestContext crc)
7778
return ok(result);
7879
}
7980

81+
@GET
82+
@AuthRequired
83+
@Path("/unreadCount")
84+
public Response getUnreadNotificationsCountForUser(@Context ContainerRequestContext crc) {
85+
try {
86+
AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc);
87+
long unreadCount = userNotificationSvc.getUnreadNotificationCountByUser(au.getId());
88+
return ok(Json.createObjectBuilder()
89+
.add("unreadCount", unreadCount));
90+
} catch (WrappedResponse wr) {
91+
return wr.getResponse();
92+
}
93+
}
94+
8095
private JsonArrayBuilder getReasonsForReturn(UserNotification notification) {
8196
Long objectId = notification.getObjectId();
8297
return WorkflowUtil.getAllWorkflowComments(datasetVersionSvc.find(objectId));
8398
}
8499

100+
@PUT
101+
@AuthRequired
102+
@Path("/{id}/markAsRead")
103+
public Response markNotificationAsReadForUser(@Context ContainerRequestContext crc, @PathParam("id") long id) {
104+
try {
105+
AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc);
106+
Long userId = au.getId();
107+
Optional<UserNotification> notification = userNotificationSvc.findByUser(userId).stream().filter(x -> x.getId().equals(id)).findFirst();
108+
if (notification.isPresent()) {
109+
UserNotification saved = userNotificationSvc.markAsRead(notification.get());
110+
if (saved.isReadNotification()) {
111+
return ok("Notification " + id + " marked as read.");
112+
} else {
113+
return badRequest("Notification " + id + " could not be marked as read.");
114+
}
115+
} else {
116+
return notFound("Notification " + id + " not found.");
117+
}
118+
} catch (WrappedResponse wr) {
119+
return wr.getResponse();
120+
}
121+
}
122+
85123
@DELETE
86124
@AuthRequired
87125
@Path("/{id}")

src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ public void displayNotification() {
563563
userNotification.setDisplayAsRead(userNotification.isReadNotification());
564564
if (userNotification.isReadNotification() == false) {
565565
userNotification.setReadNotification(true);
566+
// consider switching to userNotificationService.markAsRead
566567
userNotificationService.save(userNotification);
567568
}
568569
}

src/test/java/edu/harvard/iq/dataverse/api/NotificationsIT.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.restassured.response.Response;
66
import java.util.logging.Logger;
77
import static jakarta.ws.rs.core.Response.Status.CREATED;
8+
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
89
import static jakarta.ws.rs.core.Response.Status.OK;
910
import static org.hamcrest.CoreMatchers.equalTo;
1011
import org.junit.jupiter.api.BeforeAll;
@@ -29,6 +30,12 @@ public void testNotifications() {
2930
String authorUsername = UtilIT.getUsernameFromResponse(createAuthor);
3031
String authorApiToken = UtilIT.getApiTokenFromResponse(createAuthor);
3132

33+
Response nopermsUser = UtilIT.createRandomUser();
34+
nopermsUser.prettyPrint();
35+
nopermsUser.then().assertThat()
36+
.statusCode(OK.getStatusCode());
37+
String nopermsApiToken = UtilIT.getApiTokenFromResponse(nopermsUser);
38+
3239
// Some API calls don't generate a notification: https://github.com/IQSS/dataverse/issues/1342
3340
Response createDataverseResponse = UtilIT.createRandomDataverse(authorApiToken);
3441
createDataverseResponse.prettyPrint();
@@ -41,22 +48,50 @@ public void testNotifications() {
4148
createDataset.prettyPrint();
4249
createDataset.then().assertThat()
4350
.statusCode(CREATED.getStatusCode());
51+
4452
Response getNotifications = UtilIT.getNotifications(authorApiToken);
4553
getNotifications.prettyPrint();
4654
getNotifications.then().assertThat()
4755
.body("data.notifications[0].type", equalTo("CREATEACC"))
56+
.body("data.notifications[0].displayAsRead", equalTo(false))
4857
.body("data.notifications[1]", equalTo(null))
4958
.statusCode(OK.getStatusCode());
5059

51-
long id = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[0].id");
60+
Response unreadCount = UtilIT.getUnreadNotificationsCount(authorApiToken);
61+
unreadCount.prettyPrint();
62+
unreadCount.then().assertThat()
63+
.statusCode(OK.getStatusCode())
64+
.body("data.unreadCount", equalTo(1));
5265

53-
Response deleteNotification = UtilIT.deleteNotification(id, authorApiToken);
54-
deleteNotification.prettyPrint();
55-
deleteNotification.then().assertThat().statusCode(OK.getStatusCode());
66+
long createAccountId = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[0].id");
67+
68+
Response markReadNoPerms = UtilIT.markNotificationAsRead(createAccountId, nopermsApiToken);
69+
markReadNoPerms.prettyPrint();
70+
markReadNoPerms.then().assertThat().statusCode(NOT_FOUND.getStatusCode());
71+
72+
Response markRead = UtilIT.markNotificationAsRead(createAccountId, authorApiToken);
73+
markRead.prettyPrint();
74+
markRead.then().assertThat().statusCode(OK.getStatusCode());
5675

5776
Response getNotifications2 = UtilIT.getNotifications(authorApiToken);
5877
getNotifications2.prettyPrint();
5978
getNotifications2.then().assertThat()
79+
.body("data.notifications[0].type", equalTo("CREATEACC"))
80+
.body("data.notifications[0].displayAsRead", equalTo(true))
81+
.body("data.notifications[1]", equalTo(null))
82+
.statusCode(OK.getStatusCode());
83+
84+
Response deleteNotificationNoPerms = UtilIT.deleteNotification(createAccountId, nopermsApiToken);
85+
deleteNotificationNoPerms.prettyPrint();
86+
deleteNotificationNoPerms.then().assertThat().statusCode(NOT_FOUND.getStatusCode());
87+
88+
Response deleteNotification = UtilIT.deleteNotification(createAccountId, authorApiToken);
89+
deleteNotification.prettyPrint();
90+
deleteNotification.then().assertThat().statusCode(OK.getStatusCode());
91+
92+
Response getNotifications3 = UtilIT.getNotifications(authorApiToken);
93+
getNotifications3.prettyPrint();
94+
getNotifications3.then().assertThat()
6095
.body("data.notifications[0]", equalTo(null))
6196
.statusCode(OK.getStatusCode());
6297

src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,24 @@ static Response getNotifications(String apiToken) {
17101710
return requestSpecification.get("/api/notifications/all");
17111711
}
17121712

1713+
static Response getUnreadNotificationsCount(String apiToken) {
1714+
RequestSpecification requestSpecification = given();
1715+
if (apiToken != null) {
1716+
requestSpecification = given()
1717+
.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
1718+
}
1719+
return requestSpecification.get("/api/notifications/unreadCount");
1720+
}
1721+
1722+
static Response markNotificationAsRead(long id, String apiToken) {
1723+
RequestSpecification requestSpecification = given();
1724+
if (apiToken != null) {
1725+
requestSpecification = given()
1726+
.header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken);
1727+
}
1728+
return requestSpecification.put("/api/notifications/" + id + "/markAsRead");
1729+
}
1730+
17131731
static Response deleteNotification(long id, String apiToken) {
17141732
RequestSpecification requestSpecification = given();
17151733
if (apiToken != null) {

0 commit comments

Comments
 (0)