diff --git a/doc/release-notes/8013-history-of-access-request-available-via-api.md b/doc/release-notes/8013-history-of-access-request-available-via-api.md new file mode 100644 index 00000000000..14ebe52f9f7 --- /dev/null +++ b/doc/release-notes/8013-history-of-access-request-available-via-api.md @@ -0,0 +1,8 @@ +### Feature: Extend List File Access Requests API ### + +Added ability to get access request history via the `/datafile/{id}/listRequests` API. The API returns a list of users/groups where the request for access is waiting for an accept or reject. Already accepted or rejected requests are not returned. + +By adding the flag 'includeHistory=true' all of the requests will be returned. Pagination is also implemented in this feature. Adding a start page parameter and max list size (`&start=0` and `&per_page=20`) can limit the amount of data being returned. + +See https://guides.dataverse.org/en/latest/api/dataaccess.html#list-file-access-requests + diff --git a/doc/sphinx-guides/source/api/dataaccess.rst b/doc/sphinx-guides/source/api/dataaccess.rst index 0782665776d..e7922546faf 100755 --- a/doc/sphinx-guides/source/api/dataaccess.rst +++ b/doc/sphinx-guides/source/api/dataaccess.rst @@ -406,6 +406,21 @@ A curl example using an ``id``:: curl -H "X-Dataverse-key:$API_TOKEN" -X GET http://$SERVER/api/access/datafile/{id}/listRequests +Query parameters have been added to retrieve the historical list of "created", "granted", and "rejected" requests: + +* `includeHistory` When `true` this will force the return of all requests and not just the "created" ones. +* `start` For pagination, use this to request a specific page. +* `per_page` For pagination, use this to limit the number of items in each paged list. + +.. note:: Pagination is only available when `includeHistory` is `true` + +If requesting a page beyond the last page this API will return a 404 "There are no access requests for this file:..." +If requesting a page before page 1 or requesting the number of items to be 0 or less this API will ignore these parameters and return the entire list. + +A curl example using an ``id``:: + + curl -H "X-Dataverse-key:$API_TOKEN" -X GET http://$SERVER/api/access/datafile/{id}/listRequests?includeHistory=true&start=1&per_page=20 + User Has Requested Access to a File: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 45604a5472b..5fd9d9484b4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -226,11 +226,31 @@ public String toString() { inverseJoinColumns = @JoinColumn(name = "authenticated_user_id")) private List fileAccessRequesters; - - public List getFileAccessRequests(){ - return fileAccessRequests; + public List getFileAccessRequests() { + return getFileAccessRequests(0, 0); } - + + /** + * Get Requests with pagination option + * @param numResultsPerPageRequested + * @param paginationStart starts at 1 + * @return + */ + public List getFileAccessRequests(int numResultsPerPageRequested, int paginationStart) { + if (numResultsPerPageRequested < 1 || paginationStart < 1) { + return fileAccessRequests; + } else { + int startIndex = (paginationStart - 1) * numResultsPerPageRequested; + int endIndex = startIndex + numResultsPerPageRequested; + if (startIndex >= fileAccessRequests.size()) { + return List.of(); + } else if (endIndex > fileAccessRequests.size()) { + endIndex = fileAccessRequests.size(); + } + return fileAccessRequests.subList(startIndex, endIndex); + } + } + public List getFileAccessRequests(FileAccessRequest.RequestState state){ return fileAccessRequests.stream().filter(far -> far.getState() == state).collect(Collectors.toList()); } @@ -828,6 +848,15 @@ public void addFileAccessRequest(FileAccessRequest request) { this.fileAccessRequests.add(request); } + public List getAccessRequestsForAssignee(RoleAssignee roleAssignee) { + if (this.fileAccessRequests == null) { + return null; + } + + return this.fileAccessRequests.stream() + .filter(fileAccessRequest -> fileAccessRequest.getRequester().equals(roleAssignee)).toList(); + } + public FileAccessRequest getAccessRequestForAssignee(RoleAssignee roleAssignee) { if (this.fileAccessRequests == null) { return null; diff --git a/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java b/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java index 43463e0cb91..c918c539c40 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileAccessRequest.java @@ -4,6 +4,7 @@ import java.util.Date; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -143,6 +144,20 @@ public String getStateLabel() { } return null; } + + // For use by UI to allow for internationalization + public String getStateLabelNationalized() { + if(isStateCreated()) { + return BundleUtil.getStringFromBundle("permission.fileAccess.created"); + } + if(isStateGranted()) { + return BundleUtil.getStringFromBundle("permission.fileAccess.granted"); + } + if(isStateRejected()) { + return BundleUtil.getStringFromBundle("permission.fileAccess.rejected"); + } + return null; + } public void setStateCreated() { this.requestState = RequestState.CREATED; @@ -197,4 +212,4 @@ public boolean equals(Object object) { } -} \ No newline at end of file +} diff --git a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java index c44529d9299..ee687305584 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ManageFilePermissionsPage.java @@ -116,6 +116,15 @@ public boolean isShowDeleted() { public void setShowDeleted(boolean showDeleted) { this.showDeleted = showDeleted; } + private boolean showHistory = false; + + public boolean isShowHistory() { + return showHistory; + } + + public void setShowHistory(boolean showHistory) { + this.showHistory = showHistory; + } public Dataset getDataset() { return dataset; @@ -143,6 +152,13 @@ public void showDeletedCheckboxChange() { } } + private boolean backingShowHistory = false; + public void showHistoryCheckboxChange() { + if (backingShowHistory != showHistory) { + initMaps(); + backingShowHistory = showHistory; + } + } public String init() { if (dataset.getId() != null) { @@ -199,7 +215,7 @@ private void initMaps() { fileMap.put(file, raList); // populate the file access requests map - for (FileAccessRequest fileAccessRequest : file.getFileAccessRequests(FileAccessRequest.RequestState.CREATED)) { + for (FileAccessRequest fileAccessRequest : !showHistory ? file.getFileAccessRequests(FileAccessRequest.RequestState.CREATED) : file.getFileAccessRequests()) { List fileAccessRequestList = fileAccessRequestMap.get(fileAccessRequest.getRequester()); if (fileAccessRequestList == null) { fileAccessRequestList = new ArrayList<>(); @@ -250,6 +266,21 @@ public String formatAccessRequestTimestamp(List fileAccessReq return Util.getDateTimeFormat().format(date); } + public String getAccessRequestStates(List fileAccessRequests) { + String result = ""; + if (fileAccessRequests != null) { + Map items = fileAccessRequests.stream() + .sorted(Comparator.comparing(FileAccessRequest::getState)) + .collect(Collectors.groupingBy( + FileAccessRequest::getStateLabelNationalized, + Collectors.counting())); + + result = items.entrySet().stream().map(entry -> entry.getKey() + ":" + entry.getValue()) + .collect(Collectors.joining(", ", "[ ", " ]")); + } + return result; + } + private void addFileToRoleAssignee(RoleAssignment assignment, boolean fileDeleted) { RoleAssignee ra = roleAssigneeService.getRoleAssignee(assignment.getAssigneeIdentifier()); List assignments = roleAssigneeMap.get(ra); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index 89a4cd743d7..f25be1c890e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -36,6 +36,7 @@ import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; +import edu.harvard.iq.dataverse.mydata.Pager; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; @@ -1467,8 +1468,11 @@ public Response requestFileAccess(@Context ContainerRequestContext crc, @PathPar @GET @AuthRequired @Path("/datafile/{id}/listRequests") - public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, @Context HttpHeaders headers) { - + public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, + @QueryParam("includeHistory") boolean includeHistory, + @QueryParam("per_page") final int numResultsPerPageRequested, + @QueryParam("start") final int paginationStart, + @Context HttpHeaders headers) { DataverseRequest dataverseRequest; DataFile dataFile; @@ -1489,7 +1493,8 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa return error(FORBIDDEN, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions")); } - List requests = dataFile.getFileAccessRequests(FileAccessRequest.RequestState.CREATED); + List requests = !includeHistory ? dataFile.getFileAccessRequests(FileAccessRequest.RequestState.CREATED) : + dataFile.getFileAccessRequests(numResultsPerPageRequested, paginationStart); if (requests == null || requests.isEmpty()) { List args = Arrays.asList(dataFile.getDisplayName()); @@ -1499,7 +1504,21 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa JsonArrayBuilder userArray = Json.createArrayBuilder(); for (FileAccessRequest fileAccessRequest : requests) { - userArray.add(json(fileAccessRequest.getRequester())); + userArray.add(json(fileAccessRequest)); + } + + // Check for pagination request + if (includeHistory && numResultsPerPageRequested > 0 && paginationStart > 0) { + JsonObjectBuilder builder = Json.createObjectBuilder() + .add("status", ApiConstants.STATUS_OK) + .add("data", userArray); + + Pager pager = new Pager(dataFile.getFileAccessRequests().size(), numResultsPerPageRequested, paginationStart); + builder.add("pagination", pager.asJsonObjectBuilder()); + + return Response.ok( builder.build() ) + .type(MediaType.APPLICATION_JSON) + .build(); } return ok(userArray); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 27b7a122c93..494e6038339 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -147,6 +147,11 @@ public static JsonObjectBuilder json(AuthenticatedUser authenticatedUser) { .add("authenticationProviderId", authenticatedUser.getAuthenticatedUserLookup().getAuthenticationProviderId()); return builder; } + public static JsonObjectBuilder json(FileAccessRequest fileAccessRequest) { + JsonObjectBuilder builder = json(fileAccessRequest.getRequester()) + .add("requestState", fileAccessRequest.getStateLabel()); + return builder; + } public static JsonArrayBuilder jsonRoleAssignments(List roleAssignments) { JsonArrayBuilder bld = Json.createArrayBuilder(); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index a9330998015..ede59e789c2 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1262,6 +1262,7 @@ dataverse.permissionsFiles.files.invalidMsg=There are no restricted files in thi dataverse.permissionsFiles.files.requested=Requested Files dataverse.permissionsFiles.files.selected=Selecting {0} of {1} {2} dataverse.permissionsFiles.files.includeDeleted=Include Deleted Files +dataverse.permissionsFiles.files.showHistory=Show Historical Requests dataverse.permissionsFiles.files.draftUnpublished=Draft/Unpublished dataverse.permissionsFiles.viewRemoveDialog.header=File Access dataverse.permissionsFiles.viewRemoveDialog.removeBtn=Remove Access @@ -2763,6 +2764,9 @@ permission.roleNotAbleToBeRemoved=The role assignment was not able to be removed permission.fileAccessGranted=File Access request by {0} was granted. permission.fileAccessRejected=File Access request by {0} was rejected. permission.roleNotAbleToBeAssigned=The role was not able to be assigned. +permission.fileAccess.created=Requested +permission.fileAccess.granted=Granted +permission.fileAccess.rejected=Rejected #ManageGroupsPage.java dataverse.manageGroups.create.success=Successfully created group {0}. Refresh to update your page. diff --git a/src/main/webapp/permissions-manage-files.xhtml b/src/main/webapp/permissions-manage-files.xhtml index 2eb9e2a6f5f..045376c14c4 100644 --- a/src/main/webapp/permissions-manage-files.xhtml +++ b/src/main/webapp/permissions-manage-files.xhtml @@ -52,6 +52,9 @@ + + +
@@ -88,8 +91,8 @@ rendered="#{manageFilePermissionsPage.formatAccessRequestDate(access.value) == null}" /> -
- + #{bundle['dataverse.permissionsFiles.assignDialog.grantBtn']} - #{bundle['dataverse.permissionsFiles.assignDialog.rejectBtn']} +
@@ -324,7 +328,7 @@
-

#{bundle['dataverse.permissionsFiles.assignDialog.description']}

+

#{manageFilePermissionsPage.showHistory ? '' : bundle['dataverse.permissionsFiles.assignDialog.description']}