Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a4c4f4
adding type and dvObject to dataverse featured items
stevenwinship May 16, 2025
8c471f5
fix checkstyle
stevenwinship May 16, 2025
f6d6af5
fix
stevenwinship May 16, 2025
0f89799
fix unit test
stevenwinship May 16, 2025
d81129d
add back a test
stevenwinship May 19, 2025
35c6935
Merge branch 'develop' into 11414-add-dvobject-type-to-featured-items
stevenwinship May 20, 2025
7eb2f4c
Merge branch 'develop' into 11414-add-dvobject-type-to-featured-items
stevenwinship May 23, 2025
4134f22
comment fixes
stevenwinship May 27, 2025
de4ada4
adding user permission check
stevenwinship May 28, 2025
de2c6ce
Update src/main/java/propertyFiles/Bundle.properties
stevenwinship May 29, 2025
ebf37b8
Update src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java
stevenwinship May 29, 2025
a3ef7c1
Update src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java
stevenwinship May 29, 2025
f8d515e
Update src/main/java/edu/harvard/iq/dataverse/dataverse/featured/Data…
stevenwinship May 29, 2025
95c0cd9
Update src/main/java/edu/harvard/iq/dataverse/dataverse/featured/Data…
stevenwinship May 29, 2025
19f8997
Update doc/release-notes/11414-add-dvobject-type-to-featured-items.md
stevenwinship May 29, 2025
88c5162
Update src/test/java/edu/harvard/iq/dataverse/engine/command/impl/Cre…
stevenwinship May 29, 2025
f640917
fixed mismatch error and added test
stevenwinship May 29, 2025
def04e9
add happy test
stevenwinship May 29, 2025
88a7dab
refactor
stevenwinship May 30, 2025
0b20ecd
Merge branch 'develop' into 11414-add-dvobject-type-to-featured-items
stevenwinship May 30, 2025
5b01135
Update src/main/java/propertyFiles/Bundle.properties
stevenwinship May 30, 2025
2b98b14
allow content to be null if not type custom
stevenwinship May 30, 2025
ba2ab06
remove filter code and delete item if restricted or deaccessioned
stevenwinship May 30, 2025
efefc4b
fix unit test
stevenwinship Jun 2, 2025
16e6582
fix unit test
stevenwinship Jun 2, 2025
c372496
fix test with content null
stevenwinship Jun 2, 2025
d6bc940
rename findByDvoOrDie to include the type check
stevenwinship Jun 3, 2025
0b14812
rename findByDvoOrDie to include the type check
stevenwinship Jun 3, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Feature: Added dvObject and type fields to Featured Items

Dataverse Featured Items can now be linked to Dataverses, Datasets, or Datafiles.

Pre-existing featured items as well as new items without dvObjects will be defaulted to type=custom.

Featured Items with dvObjects will be filtered out of lists if the dvObject should not be viewed (i.e. datafiles that are restricted or datasets that are deaccessioned)
14 changes: 12 additions & 2 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1218,9 +1218,13 @@ The ``file`` parameter must be specified for each image we want to attach to fea

The ``id`` parameter must be ``0`` for new items or set to the item's identifier for updates. The ``fileName`` parameter should be empty to exclude an image or match the name of a file sent in a ``file`` parameter to set a new image. ``keepFile`` must always be set to ``false``, unless it's an update to a featured item where we want to preserve the existing image, if one exists.

The ``type`` and ``dvObject`` parameters are optional. These allow you to link the featured item to a Dataverse, Dataset, or Datafile.
The ``dvObject`` can be passed as the id or the persistent identifier and the ``type`` must be passed as either "dataverse", "dataset", or "datafile", depending on the type of object.
If no ``dvObject`` is passed the ``type`` will default to "custom" designating no linked object.

Note that any existing featured item not included in the call with its associated identifier and corresponding properties will be removed from the collection.

The following example creates two featured items, with an image assigned to the second one:
The following example creates two featured items, with an image and a dataset assigned to the second one:

.. code-block:: bash

Expand All @@ -1234,6 +1238,8 @@ The following example creates two featured items, with an image assigned to the
export SECOND_ITEM_IMAGE_FILENAME='image.png'
export SECOND_ITEM_CONTENT='Content 2'
export SECOND_ITEM_DISPLAY_ORDER=2
export SECOND_ITEM_TYPE="dataset"
export SECOND_ITEM_DVOBJECT="doi:ZZ7/MOSEISLEYDB94"

curl -H "X-Dataverse-key:$API_TOKEN" \
-X PUT \
Expand All @@ -1243,6 +1249,8 @@ The following example creates two featured items, with an image assigned to the
-F "fileName=" -F "fileName=$SECOND_ITEM_IMAGE_FILENAME" \
-F "keepFile=false" -F "keepFile=false" \
-F "file=@$SECOND_ITEM_IMAGE_FILENAME" \
-F "type=" -F "type=@$SECOND_ITEM_TYPE" \
-F "dvObject=" -F "dvObject=@$SECOND_ITEM_DVOBJECT" \
"$SERVER_URL/api/dataverses/$ID/featuredItems"


Expand All @@ -1258,9 +1266,11 @@ The fully expanded example above (without environment variables) looks like this
-F "fileName=" -F "fileName=image.png" \
-F "keepFile=false" -F "keepFile=false" \
-F "[email protected]" \
-F "type=" -F "type=dataset" \
-F "dvObject=" -F "dvObject=doi:ZZ7/MOSEISLEYDB94" \
"https://demo.dataverse.org/api/dataverses/root/featuredItems"

The following example creates one featured item and updates a second one, keeping the existing image it may have had:
The following example creates one featured item and updates a second one, keeping the existing image it may have had but removes the dataset link and defaults the type to "custom":

.. code-block:: bash

Expand Down
11 changes: 0 additions & 11 deletions src/main/java/edu/harvard/iq/dataverse/Dataverse.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,17 +353,6 @@ public void setMetadataBlockFacets(List<DataverseMetadataBlockFacet> metadataBlo
this.metadataBlockFacets = metadataBlockFacets;
}

@OneToMany(mappedBy = "dataverse")
private List<DataverseFeaturedItem> dataverseFeaturedItems = new ArrayList<>();

public List<DataverseFeaturedItem> getDataverseFeaturedItems() {
return this.dataverseFeaturedItems;
}

public void setDataverseFeaturedItems(List<DataverseFeaturedItem> dataverseFeaturedItems) {
this.dataverseFeaturedItems = dataverseFeaturedItems;
}

public List<Guestbook> getParentGuestbooks() {
List<Guestbook> retList = new ArrayList<>();
Dataverse testDV = this;
Expand Down
18 changes: 13 additions & 5 deletions src/main/java/edu/harvard/iq/dataverse/DvObject.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package edu.harvard.iq.dataverse;

import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem;
import edu.harvard.iq.dataverse.pidproviders.PidUtil;
import edu.harvard.iq.dataverse.storageuse.StorageQuota;

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.logging.Logger;

import jakarta.persistence.*;
Expand Down Expand Up @@ -141,7 +139,17 @@ public String visit(DataFile df) {
private String storageIdentifier;

@Column(insertable = false, updatable = false) private String dtype;


@OneToMany(mappedBy="dvobject",fetch = FetchType.LAZY,cascade={CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
private List<DataverseFeaturedItem> dataverseFeaturedItems;

public List<DataverseFeaturedItem> getDataverseFeaturedItems() {
return this.dataverseFeaturedItems;
}
public void setDataverseFeaturedItems(List<DataverseFeaturedItem> dataverseFeaturedItems) {
this.dataverseFeaturedItems = dataverseFeaturedItems;
}

/*
* Add PID related fields
*/
Expand Down
67 changes: 56 additions & 11 deletions src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean;
import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleServiceBean;
import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem;
import edu.harvard.iq.dataverse.engine.command.Command;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.exception.*;
Expand Down Expand Up @@ -377,11 +378,21 @@ protected Dataset findDatasetOrDie(String id) throws WrappedResponse {
protected Dataset findDatasetOrDie(String id, boolean deep) throws WrappedResponse {
Long datasetId;
Dataset dataset;
if (id.equals(PERSISTENT_ID_KEY)) {
String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1));
if (persistentId == null) {
if (isNumeric(id)) {
try {
datasetId = Long.parseLong(id);
} catch (NumberFormatException nfe) {
throw new WrappedResponse(
badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1)))));
badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.bad.id", Collections.singletonList(id))));
}
} else {
String persistentId = id;
if (id.equals(PERSISTENT_ID_KEY)) {
persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1));
if (persistentId == null) {
throw new WrappedResponse(
badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1)))));
}
}
GlobalId globalId;
try {
Expand All @@ -398,13 +409,6 @@ protected Dataset findDatasetOrDie(String id, boolean deep) throws WrappedRespon
throw new WrappedResponse(
notFound(BundleUtil.getStringFromBundle("find.dataset.error.dataset_id_is_null", Collections.singletonList(PERSISTENT_ID_KEY.substring(1)))));
}
} else {
try {
datasetId = Long.parseLong(id);
} catch (NumberFormatException nfe) {
throw new WrappedResponse(
badRequest(BundleUtil.getStringFromBundle("find.dataset.error.dataset.not.found.bad.id", Collections.singletonList(id))));
}
}
if (deep) {
dataset = datasetSvc.findDeep(datasetId);
Expand Down Expand Up @@ -575,6 +579,47 @@ protected DvObject findDvo(@NotNull final String id) throws WrappedResponse {
return d;
}

/**
*
* @param dvIdtf
* @param type
* @return DvObject if type matches or throw exception
* @throws WrappedResponse
*/
@NotNull
protected DvObject findDvoByIdAndFeaturedItemTypeOrDie(@NotNull final String dvIdtf, String type) throws WrappedResponse {
try {
DataverseFeaturedItem.TYPES dvType = DataverseFeaturedItem.getDvType(type);
DvObject dvObject = isNumeric(dvIdtf) ? findDvo(Long.valueOf(dvIdtf)) : null;
if (dvObject == null) {
List<DataverseFeaturedItem.TYPES> types = new ArrayList<>();
types.addAll(List.of(DataverseFeaturedItem.TYPES.values()));
types.remove(dvType);
types.add(0, dvType); // put the requested type first for speed of lookup
for (DataverseFeaturedItem.TYPES t : types) {
try {
if (DataverseFeaturedItem.TYPES.DATAVERSE == t) {
dvObject = findDataverseOrDie(dvIdtf);
break;
} else if (DataverseFeaturedItem.TYPES.DATASET == t) {
dvObject = findDatasetOrDie(dvIdtf);
break;
} else if (DataverseFeaturedItem.TYPES.DATAFILE == t) {
dvObject = findDataFileOrDie(dvIdtf);
break;
}
} catch (WrappedResponse e) {
// ignore errors to allow other find*OrDie to be called
}
}
}
DataverseFeaturedItem.validateTypeAndDvObject(dvIdtf, dvObject, dvType);
return dvObject;
} catch (IllegalArgumentException e) {
throw new WrappedResponse(error(Response.Status.BAD_REQUEST, e.getMessage()));
}
}

protected <T> T failIfNull( T t, String errorMessage ) throws WrappedResponse {
if ( t != null ) return t;
throw new WrappedResponse( error( Response.Status.BAD_REQUEST,errorMessage) );
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.harvard.iq.dataverse.api;

import edu.harvard.iq.dataverse.DvObject;
import edu.harvard.iq.dataverse.api.auth.AuthRequired;
import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem;
Expand Down Expand Up @@ -51,6 +52,8 @@ public Response deleteFeaturedItem(@Context ContainerRequestContext crc, @PathPa
public Response updateFeaturedItem(@Context ContainerRequestContext crc,
@PathParam("id") Long id,
@FormDataParam("content") String content,
@FormDataParam("type") String type,
@FormDataParam("dvObject") String dvObjectIdtf,
@FormDataParam("displayOrder") int displayOrder,
@FormDataParam("keepFile") boolean keepFile,
@FormDataParam("file") InputStream imageFileInputStream,
Expand All @@ -60,7 +63,8 @@ public Response updateFeaturedItem(@Context ContainerRequestContext crc,
if (dataverseFeaturedItem == null) {
throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), id)));
}
UpdatedDataverseFeaturedItemDTO updatedDataverseFeaturedItemDTO = UpdatedDataverseFeaturedItemDTO.fromFormData(content, displayOrder, keepFile, imageFileInputStream, contentDispositionHeader);
DvObject dvObject = (dvObjectIdtf != null) ? findDvoByIdAndFeaturedItemTypeOrDie(dvObjectIdtf, type) : null;
UpdatedDataverseFeaturedItemDTO updatedDataverseFeaturedItemDTO = UpdatedDataverseFeaturedItemDTO.fromFormData(content, displayOrder, keepFile, imageFileInputStream, contentDispositionHeader, type, dvObject);
return ok(json(execCommand(new UpdateDataverseFeaturedItemCommand(createDataverseRequest(getRequestUser(crc)), dataverseFeaturedItem, updatedDataverseFeaturedItemDTO))));
} catch (WrappedResponse e) {
return e.getResponse();
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java
Original file line number Diff line number Diff line change
Expand Up @@ -1792,18 +1792,24 @@ public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext c
@Path("{identifier}/featuredItems")
public Response createFeaturedItem(@Context ContainerRequestContext crc,
@PathParam("identifier") String dvIdtf,
@FormDataParam("type") String type,
@FormDataParam("dvObject") String dvObjectIdtf,
@FormDataParam("content") String content,
@FormDataParam("displayOrder") int displayOrder,
@FormDataParam("file") InputStream imageFileInputStream,
@FormDataParam("file") FormDataContentDisposition contentDispositionHeader) {
Dataverse dataverse;
DvObject dvObject = null;
try {
dataverse = findDataverseOrDie(dvIdtf);
if (dvObjectIdtf != null) {
dvObject = findDvoByIdAndFeaturedItemTypeOrDie(dvObjectIdtf, type);
}
} catch (WrappedResponse wr) {
return wr.getResponse();
}
NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = NewDataverseFeaturedItemDTO.fromFormData(content, displayOrder, imageFileInputStream, contentDispositionHeader);
try {
NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = NewDataverseFeaturedItemDTO.fromFormData(content, displayOrder, imageFileInputStream, contentDispositionHeader, type, dvObject);
DataverseFeaturedItem dataverseFeaturedItem = execCommand(new CreateDataverseFeaturedItemCommand(
createDataverseRequest(getRequestUser(crc)),
dataverse,
Expand Down Expand Up @@ -1837,6 +1843,8 @@ public Response updateFeaturedItems(
@PathParam("dataverseId") String dvIdtf,
@FormDataParam("id") List<Long> ids,
@FormDataParam("content") List<String> contents,
@FormDataParam("type") List<String> types,
@FormDataParam("dvObject") List<String> dvObjectIdtf,
@FormDataParam("displayOrder") List<Integer> displayOrders,
@FormDataParam("keepFile") List<Boolean> keepFiles,
@FormDataParam("fileName") List<String> fileNames,
Expand All @@ -1848,7 +1856,15 @@ public Response updateFeaturedItems(
}

int size = ids.size();
if (contents.size() != size || displayOrders.size() != size || keepFiles.size() != size || fileNames.size() != size) {
if (types == null || types.isEmpty()) {
types = new ArrayList<>(Collections.nCopies(size, null));
}
if (dvObjectIdtf == null || dvObjectIdtf.isEmpty()) {
dvObjectIdtf = new ArrayList<>(Collections.nCopies(size, null));
}

if (contents.size() != size || displayOrders.size() != size || keepFiles.size() != size || fileNames.size() != size ||
types.size() != size || dvObjectIdtf.size() != size) {
throw new WrappedResponse(error(Response.Status.BAD_REQUEST,
BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.inputListsSizeMismatch")));
}
Expand All @@ -1873,17 +1889,20 @@ public Response updateFeaturedItems(
}
}

// ignore dvObject if the id is missing or an empty string
DvObject dvObject = dvObjectIdtf.get(i) != null && !dvObjectIdtf.get(i).isEmpty()
? findDvoByIdAndFeaturedItemTypeOrDie(dvObjectIdtf.get(i), types.get(i)) : null;
if (ids.get(i) == 0) {
newItems.add(NewDataverseFeaturedItemDTO.fromFormData(
contents.get(i), displayOrders.get(i), fileInputStream, contentDisposition));
contents.get(i), displayOrders.get(i), fileInputStream, contentDisposition, types.get(i), dvObject));
} else {
DataverseFeaturedItem existingItem = dataverseFeaturedItemServiceBean.findById(ids.get(i));
if (existingItem == null) {
throw new WrappedResponse(error(Response.Status.NOT_FOUND,
MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), ids.get(i))));
}
itemsToUpdate.put(existingItem, UpdatedDataverseFeaturedItemDTO.fromFormData(
contents.get(i), displayOrders.get(i), keepFiles.get(i), fileInputStream, contentDisposition));
contents.get(i), displayOrders.get(i), keepFiles.get(i), fileInputStream, contentDisposition, types.get(i), dvObject));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package edu.harvard.iq.dataverse.api.dto;

import edu.harvard.iq.dataverse.DataFile;
import edu.harvard.iq.dataverse.Dataset;
import edu.harvard.iq.dataverse.Dataverse;
import edu.harvard.iq.dataverse.DvObject;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem;

import java.io.InputStream;

public abstract class AbstractDataverseFeaturedItemDTO {
protected String content;
protected int displayOrder;
protected InputStream imageFileInputStream;
protected String imageFileName;
protected String type;
protected DvObject dvObject;

public void setContent(String content) {
this.content = content;
}

public String getContent() {
return content;
}

public void setDisplayOrder(int displayOrder) {
this.displayOrder = displayOrder;
}

public int getDisplayOrder() {
return displayOrder;
}

public void setImageFileInputStream(InputStream imageFileInputStream) {
this.imageFileInputStream = imageFileInputStream;
}

public InputStream getImageFileInputStream() {
return imageFileInputStream;
}

public void setImageFileName(String imageFileName) {
this.imageFileName = imageFileName;
}

public String getImageFileName() {
return imageFileName;
}

public void setType(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setDvObject(DvObject dvObject) {
this.dvObject = dvObject;
}
public DvObject getDvObject() {
return dvObject;
}
}
Loading