Skip to content

Commit 7363117

Browse files
committed
support read/unread status for notifications #11650
- displayAsRead boolean added to "get all" - new GET unreadCount API endpoint - new PUT markAsRead API endpoint
1 parent bfe59f0 commit 7363117

File tree

6 files changed

+150
-5
lines changed

6 files changed

+150
-5
lines changed

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5889,6 +5889,8 @@ Notifications
58895889
58905890
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.
58915891
5892+
.. _get-all-notifications:
5893+
58925894
Get All Notifications by User
58935895
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
58945896
@@ -5898,6 +5900,52 @@ Each user can get a dump of their notifications by passing in their API token:
58985900
58995901
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all"
59005902
5903+
The expected OK (200) response looks something like this:
5904+
5905+
.. code-block:: text
5906+
5907+
{
5908+
"status": "OK",
5909+
"data": {
5910+
"notifications": [
5911+
{
5912+
"id": 38,
5913+
"type": "CREATEACC",
5914+
"displayAsRead": true,
5915+
"subjectText": "Root: Your account has been created",
5916+
"messageText": "Hello, \nWelcome to...",
5917+
"sentTimestamp": "2025-07-21T19:15:37Z"
5918+
}
5919+
...
5920+
5921+
Get Unread Count
5922+
~~~~~~~~~~~~~~~~
5923+
5924+
You can get a count of your unread messages as shown below.
5925+
5926+
.. code-block:: bash
5927+
5928+
curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/notifications/unreadCount"
5929+
5930+
Mark Notification As Read
5931+
~~~~~~~~~~~~~~~~~~~~~~~~~
5932+
5933+
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 message as read over and over.
5934+
5935+
.. code-block:: bash
5936+
5937+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
5938+
export SERVER_URL=https://demo.dataverse.org
5939+
export NOTIFICATION_ID=555
5940+
5941+
curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/notifications/$NOTIFICATION_ID/markAsRead"
5942+
5943+
The fully expanded example above (without environment variables) looks like this:
5944+
5945+
.. code-block:: bash
5946+
5947+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/notifications/555/markAsRead"
5948+
59015949
Delete Notification by User
59025950
~~~~~~~~~~~~~~~~~~~~~~~~~~~
59035951

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)