Skip to content

Commit 1e3afbe

Browse files
committed
Merge branch 'develop' into 11447-mydata-retrieve-empty-result-set
2 parents 79ed683 + 98f448e commit 1e3afbe

29 files changed

Lines changed: 308 additions & 62 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Feature: Extend List File Access Requests API ###
2+
3+
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.
4+
5+
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.
6+
7+
See https://guides.dataverse.org/en/latest/api/dataaccess.html#list-file-access-requests
8+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ v6.11
1212

1313
- The GET /api/mydata/retrieve, if the search returns no data, now includes the "data" block with 0 results. The message that was returned in "error_message" will be returned in "message" and the "success" will be `true`. All other errors will continue to reply with "success":false and the error message in "error_message".
1414

15-
- The Croissant :ref:`metadata export format <metadata-export-formats>` has been updated from version 1.0 to 1.1, which is reflected in the ``conformsTo`` property. ``@vocab`` and ``sc`` properties now use "http" as `recommended <https://github.com/mlcommons/croissant/pull/929#pullrequestreview-3079137662>`_. The unused ``wd`` property has been dropped.
15+
- The Croissant :ref:`metadata export format <metadata-export-formats>` has been updated from version 1.0 to 1.1, which is reflected in the ``conformsTo`` property. The unused ``wd`` property has been dropped.
1616

1717
v6.10
1818
-----

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,22 @@ This method returns a list of Authenticated Users who have requested access to t
440440

441441
A curl example using an ``id``::
442442

443-
curl -H "X-Dataverse-key:$API_TOKEN" -X GET http://$SERVER/api/access/datafile/{id}/listRequests
443+
curl -H "X-Dataverse-key:$API_TOKEN" -X GET $SERVER/api/access/datafile/{id}/listRequests
444+
445+
Query parameters have been added to retrieve the historical list of "created", "granted", and "rejected" requests:
446+
447+
* `includeHistory` When `true` this will force the return of all requests and not just the "created" ones.
448+
* `start` For pagination, use this to request a specific page.
449+
* `per_page` For pagination, use this to limit the number of items in each paged list.
450+
451+
.. note:: Pagination is only available when `includeHistory` is `true`
452+
453+
If requesting a page beyond the last page this API will return a 404 "There are no access requests for this file:..."
454+
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.
455+
456+
A curl example using an ``id``::
457+
458+
curl -H "X-Dataverse-key:$API_TOKEN" -X GET "$SERVER/api/access/datafile/{id}/listRequests?includeHistory=true&start=1&per_page=20"
444459

445460
User Has Requested Access to a File:
446461
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -451,7 +466,7 @@ This method returns true or false depending on whether or not the calling user h
451466

452467
A curl example using an ``id``::
453468

454-
curl -H "X-Dataverse-key:$API_TOKEN" -X GET "http://$SERVER/api/access/datafile/{id}/userFileAccessRequested"
469+
curl -H "X-Dataverse-key:$API_TOKEN" -X GET "$SERVER/api/access/datafile/{id}/userFileAccessRequested"
455470

456471

457472
Get User Permissions on a File:

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,31 @@ public String toUri() {
247247
inverseJoinColumns = @JoinColumn(name = "authenticated_user_id"))
248248
private List<AuthenticatedUser> fileAccessRequesters;
249249

250-
251-
public List<FileAccessRequest> getFileAccessRequests(){
252-
return fileAccessRequests;
250+
public List<FileAccessRequest> getFileAccessRequests() {
251+
return getFileAccessRequests(0, 0);
253252
}
254-
253+
254+
/**
255+
* Get Requests with pagination option
256+
* @param numResultsPerPageRequested
257+
* @param paginationStart starts at 1
258+
* @return
259+
*/
260+
public List<FileAccessRequest> getFileAccessRequests(int numResultsPerPageRequested, int paginationStart) {
261+
if (numResultsPerPageRequested < 1 || paginationStart < 1) {
262+
return fileAccessRequests;
263+
} else {
264+
int startIndex = (paginationStart - 1) * numResultsPerPageRequested;
265+
int endIndex = startIndex + numResultsPerPageRequested;
266+
if (startIndex >= fileAccessRequests.size()) {
267+
return List.of();
268+
} else if (endIndex > fileAccessRequests.size()) {
269+
endIndex = fileAccessRequests.size();
270+
}
271+
return fileAccessRequests.subList(startIndex, endIndex);
272+
}
273+
}
274+
255275
public List<FileAccessRequest> getFileAccessRequests(FileAccessRequest.RequestState state){
256276
return fileAccessRequests.stream().filter(far -> far.getState() == state).collect(Collectors.toList());
257277
}
@@ -849,6 +869,15 @@ public void addFileAccessRequest(FileAccessRequest request) {
849869
this.fileAccessRequests.add(request);
850870
}
851871

872+
public List<FileAccessRequest> getAccessRequestsForAssignee(RoleAssignee roleAssignee) {
873+
if (this.fileAccessRequests == null) {
874+
return null;
875+
}
876+
877+
return this.fileAccessRequests.stream()
878+
.filter(fileAccessRequest -> fileAccessRequest.getRequester().equals(roleAssignee)).toList();
879+
}
880+
852881
public FileAccessRequest getAccessRequestForAssignee(RoleAssignee roleAssignee) {
853882
if (this.fileAccessRequests == null) {
854883
return null;

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,25 @@ public boolean isValid(DatasetField value, ConstraintValidatorContext context) {
5959
}
6060

6161
// if value is not primitive or not empty
62-
if (!dsfType.isPrimitive() || !StringUtils.isBlank(value.getValue())) {
62+
// For controlled vocabulary fields, check that actual CV values are selected,
63+
// not just that datasetFieldValues contains something (which might be an invalid N/A placeholder)
64+
// See https://github.com/IQSS/dataverse/issues/11900
65+
if (!dsfType.isPrimitive()) {
6366
return true;
6467
}
68+
69+
if (dsfType.isControlledVocabulary()) {
70+
// For CV fields, check if there are actual controlled vocabulary values selected
71+
if (value.getControlledVocabularyValues() != null && !value.getControlledVocabularyValues().isEmpty()) {
72+
return true;
73+
}
74+
// If no CV values, fall through to required field check below
75+
} else {
76+
// For non-CV primitive fields, check if value is not blank
77+
if (!StringUtils.isBlank(value.getValue())) {
78+
return true;
79+
}
80+
}
6581

6682
if (value.isRequired()) {
6783
String errorMessage = null;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4039,8 +4039,8 @@ public String save() {
40394039
dataset.setOwner(ownerId != null ? dataverseService.find(ownerId) : null);
40404040
}
40414041
// Validate
4042-
Set<ConstraintViolation> constraintViolations = workingVersion.validate();
4043-
if (!constraintViolations.isEmpty()) {
4042+
workingVersion.validate(); // add validation messages to dataset fields
4043+
if (!workingVersion.isValid()) {
40444044
FacesContext.getCurrentInstance().validationFailed();
40454045
return "";
40464046
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.Date;
55

66
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
7+
import edu.harvard.iq.dataverse.util.BundleUtil;
78
import jakarta.persistence.Column;
89
import jakarta.persistence.Entity;
910
import jakarta.persistence.EnumType;
@@ -143,6 +144,20 @@ public String getStateLabel() {
143144
}
144145
return null;
145146
}
147+
148+
// For use by UI to allow for internationalization
149+
public String getStateLabelNationalized() {
150+
if(isStateCreated()) {
151+
return BundleUtil.getStringFromBundle("permission.fileAccess.created");
152+
}
153+
if(isStateGranted()) {
154+
return BundleUtil.getStringFromBundle("permission.fileAccess.granted");
155+
}
156+
if(isStateRejected()) {
157+
return BundleUtil.getStringFromBundle("permission.fileAccess.rejected");
158+
}
159+
return null;
160+
}
146161

147162
public void setStateCreated() {
148163
this.requestState = RequestState.CREATED;
@@ -197,4 +212,4 @@ public boolean equals(Object object) {
197212
}
198213

199214

200-
}
215+
}

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ public boolean isShowDeleted() {
116116
public void setShowDeleted(boolean showDeleted) {
117117
this.showDeleted = showDeleted;
118118
}
119+
private boolean showHistory = false;
120+
121+
public boolean isShowHistory() {
122+
return showHistory;
123+
}
124+
125+
public void setShowHistory(boolean showHistory) {
126+
this.showHistory = showHistory;
127+
}
119128

120129
public Dataset getDataset() {
121130
return dataset;
@@ -143,6 +152,13 @@ public void showDeletedCheckboxChange() {
143152
}
144153

145154
}
155+
private boolean backingShowHistory = false;
156+
public void showHistoryCheckboxChange() {
157+
if (backingShowHistory != showHistory) {
158+
initMaps();
159+
backingShowHistory = showHistory;
160+
}
161+
}
146162

147163
public String init() {
148164
if (dataset.getId() != null) {
@@ -199,7 +215,7 @@ private void initMaps() {
199215
fileMap.put(file, raList);
200216

201217
// populate the file access requests map
202-
for (FileAccessRequest fileAccessRequest : file.getFileAccessRequests(FileAccessRequest.RequestState.CREATED)) {
218+
for (FileAccessRequest fileAccessRequest : !showHistory ? file.getFileAccessRequests(FileAccessRequest.RequestState.CREATED) : file.getFileAccessRequests()) {
203219
List<FileAccessRequest> fileAccessRequestList = fileAccessRequestMap.get(fileAccessRequest.getRequester());
204220
if (fileAccessRequestList == null) {
205221
fileAccessRequestList = new ArrayList<>();
@@ -250,6 +266,21 @@ public String formatAccessRequestTimestamp(List<FileAccessRequest> fileAccessReq
250266
return Util.getDateTimeFormat().format(date);
251267
}
252268

269+
public String getAccessRequestStates(List<FileAccessRequest> fileAccessRequests) {
270+
String result = "";
271+
if (fileAccessRequests != null) {
272+
Map<String, Long> items = fileAccessRequests.stream()
273+
.sorted(Comparator.comparing(FileAccessRequest::getState))
274+
.collect(Collectors.groupingBy(
275+
FileAccessRequest::getStateLabelNationalized,
276+
Collectors.counting()));
277+
278+
result = items.entrySet().stream().map(entry -> entry.getKey() + ":" + entry.getValue())
279+
.collect(Collectors.joining(", ", "[ ", " ]"));
280+
}
281+
return result;
282+
}
283+
253284
private void addFileToRoleAssignee(RoleAssignment assignment, boolean fileDeleted) {
254285
RoleAssignee ra = roleAssigneeService.getRoleAssignee(assignment.getAssigneeIdentifier());
255286
List<RoleAssignmentRow> assignments = roleAssigneeMap.get(ra);

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import edu.harvard.iq.dataverse.export.DDIExportServiceBean;
2929
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean;
3030
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry;
31+
import edu.harvard.iq.dataverse.mydata.Pager;
3132
import edu.harvard.iq.dataverse.settings.JvmSettings;
3233
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
3334
import edu.harvard.iq.dataverse.util.*;
@@ -1695,8 +1696,11 @@ public Response requestFileAccess(@Context ContainerRequestContext crc
16951696
@GET
16961697
@AuthRequired
16971698
@Path("/datafile/{id}/listRequests")
1698-
public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, @Context HttpHeaders headers) {
1699-
1699+
public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId,
1700+
@QueryParam("includeHistory") boolean includeHistory,
1701+
@QueryParam("per_page") final int numResultsPerPageRequested,
1702+
@QueryParam("start") final int paginationStart,
1703+
@Context HttpHeaders headers) {
17001704
DataverseRequest dataverseRequest;
17011705

17021706
DataFile dataFile;
@@ -1717,7 +1721,8 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa
17171721
return error(FORBIDDEN, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions"));
17181722
}
17191723

1720-
List<FileAccessRequest> requests = dataFile.getFileAccessRequests(FileAccessRequest.RequestState.CREATED);
1724+
List<FileAccessRequest> requests = !includeHistory ? dataFile.getFileAccessRequests(FileAccessRequest.RequestState.CREATED) :
1725+
dataFile.getFileAccessRequests(numResultsPerPageRequested, paginationStart);
17211726

17221727
if (requests == null || requests.isEmpty()) {
17231728
List<String> args = Arrays.asList(dataFile.getDisplayName());
@@ -1727,7 +1732,21 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa
17271732
JsonArrayBuilder userArray = Json.createArrayBuilder();
17281733

17291734
for (FileAccessRequest fileAccessRequest : requests) {
1730-
userArray.add(json(fileAccessRequest.getRequester()));
1735+
userArray.add(json(fileAccessRequest));
1736+
}
1737+
1738+
// Check for pagination request
1739+
if (includeHistory && numResultsPerPageRequested > 0 && paginationStart > 0) {
1740+
JsonObjectBuilder builder = Json.createObjectBuilder()
1741+
.add("status", ApiConstants.STATUS_OK)
1742+
.add("data", userArray);
1743+
1744+
Pager pager = new Pager(dataFile.getFileAccessRequests().size(), numResultsPerPageRequested, paginationStart);
1745+
builder.add("pagination", pager.asJsonObjectBuilder());
1746+
1747+
return Response.ok( builder.build() )
1748+
.type(MediaType.APPLICATION_JSON)
1749+
.build();
17311750
}
17321751

17331752
return ok(userArray);

src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractDatasetCommand.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,14 @@ protected void validateOrDie(DatasetVersion dsv, Boolean lenient) throws Command
110110
Set<ConstraintViolation> constraintViolations = dsv.validate();
111111
if (!constraintViolations.isEmpty()) {
112112
if (lenient) {
113-
// populate invalid fields with N/A
113+
// populate invalid primitive fields with N/A
114+
// Note: controlled vocabulary fields should NOT get N/A values in datasetfieldvalue,
115+
// as this creates an inconsistent state where the CV field appears valid but is empty.
116+
// See https://github.com/IQSS/dataverse/issues/11900
114117
constraintViolations.stream()
115118
.filter(cv -> cv.getRootBean() instanceof DatasetField)
116119
.map(cv -> ((DatasetField) cv.getRootBean()))
120+
.filter(f -> !f.getDatasetFieldType().isControlledVocabulary())
117121
.forEach(f -> f.setSingleValue(DatasetField.NA_VALUE));
118122

119123
} else {

0 commit comments

Comments
 (0)