Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
0882b3f
Initial refactor
qqmyers Feb 14, 2025
b050305
change to have status as param
qqmyers Feb 14, 2025
7faac7d
use getLocaleCurationStatusLabel
qqmyers Feb 14, 2025
b511fba
Temp fix to match current api call returning string
qqmyers Feb 14, 2025
1353167
update to use new CurationStatus class
qqmyers Feb 14, 2025
8b2b30e
restore label-based util call, update other uses
qqmyers Feb 14, 2025
e297262
typo
qqmyers Feb 14, 2025
985590c
make authuser and time nullable for legacy data
qqmyers Feb 14, 2025
d0e868f
fix import javax->jakarta
qqmyers Feb 14, 2025
06cdc93
typo
qqmyers Feb 14, 2025
82eb6cc
fix script
qqmyers Feb 14, 2025
c708579
bug fixes
qqmyers Feb 14, 2025
863a27c
more bugs
qqmyers Feb 14, 2025
fb31335
update listCurationStates report with new columns
qqmyers Feb 14, 2025
bb2b5b5
minor fixes
qqmyers Feb 14, 2025
995b0eb
add missing columns
qqmyers Feb 14, 2025
5ae00ea
add includeHistory param to report
qqmyers Feb 14, 2025
ffca519
add history and use JSON for getCurationStatus
qqmyers Feb 14, 2025
864a8e4
dco updates
qqmyers Feb 14, 2025
6907de8
add facet
qqmyers Feb 14, 2025
d810a47
update tests
qqmyers Feb 14, 2025
5391ea3
add bundle entry
qqmyers Feb 14, 2025
b253d1f
typo
qqmyers Feb 14, 2025
5126827
test fix
qqmyers Feb 14, 2025
1504893
add curation createTime to solr index
qqmyers Feb 15, 2025
abb059f
update schema
qqmyers Feb 15, 2025
2fc5764
missing field
qqmyers Feb 15, 2025
8f8f09d
change date using getTime
qqmyers Feb 15, 2025
8aeb0ef
more external->curation refactoring
qqmyers Feb 15, 2025
cabf0b6
use pdate
qqmyers Feb 15, 2025
d43a665
release note, change log
qqmyers Feb 15, 2025
c10a766
add option to let creators see curation status
qqmyers Feb 15, 2025
7720886
refactor
qqmyers Feb 15, 2025
789e94a
docs
qqmyers Feb 15, 2025
743d799
support flag in API
qqmyers Feb 15, 2025
5abf460
update api doc
qqmyers Feb 15, 2025
7b6bede
sync test name
qqmyers Feb 15, 2025
bb668b6
update release note
qqmyers Feb 15, 2025
d521d6a
typo
qqmyers Feb 15, 2025
1046a01
remove unused method
qqmyers Feb 15, 2025
b2e6b45
fix #10835
qqmyers Feb 15, 2025
af202df
Improve mail message per #10668
qqmyers Feb 15, 2025
d8f18e4
send notices based on feature flag
qqmyers Feb 15, 2025
8e3f86a
add history menu item
qqmyers Feb 15, 2025
7fb5aa1
bugs, display improvements
qqmyers Feb 16, 2025
85afd88
change logic to show with submit for review
qqmyers Feb 16, 2025
6859bf9
start separate menu
qqmyers Feb 16, 2025
02d4a5b
typo, fix label, styling
qqmyers Feb 17, 2025
243d46c
fix merge
qqmyers Feb 17, 2025
4f13222
Merge remote-tracking branch 'IQSS/develop' into IQSS/9247-CurationSt…
qqmyers Feb 18, 2025
9cab16a
test fix - use getDataAsJsonObject from #11271
qqmyers Feb 20, 2025
7f0b959
typo
qqmyers Feb 20, 2025
c0c7775
handle nulls
qqmyers Feb 20, 2025
d2c79a8
fix test
qqmyers Feb 20, 2025
5e430ce
fix getData methods
qqmyers Feb 20, 2025
a722287
use delete to remove label
qqmyers Feb 21, 2025
f552153
Merge remote-tracking branch 'IQSS/develop' into IQSS/9247-CurationSt…
qqmyers Feb 21, 2025
5e9f609
cleanup, fix merge issue
qqmyers Feb 21, 2025
2899614
Add an index
qqmyers Feb 25, 2025
fa2324b
Merge remote-tracking branch 'IQSS/develop' into
qqmyers Mar 18, 2025
8cfdc22
remove redundant h:form
qqmyers Mar 18, 2025
371b88f
Merge remote-tracking branch 'IQSS/develop' into IQSS/9247-CurationSt…
qqmyers Mar 19, 2025
ea1188b
Merge remote-tracking branch 'IQSS/develop' into
qqmyers May 30, 2025
6ebf7ca
nulls last
qqmyers May 30, 2025
91192a3
Merge remote-tracking branch 'IQSS/develop' into IQSS/9247-CurationSt…
qqmyers Jun 20, 2025
d87a649
changes per review
qqmyers Jun 20, 2025
28b8791
Merge remote-tracking branch 'IQSS/develop' into
qqmyers Jun 24, 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
3 changes: 2 additions & 1 deletion conf/solr/schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@
<field name="dvSubject" type="string" stored="true" indexed="true" multiValued="true"/>

<field name="publicationStatus" type="string" stored="true" indexed="true" multiValued="true"/>
<field name="externalStatus" type="string" stored="true" indexed="true" multiValued="false"/>
<field name="curationStatus" type="string" stored="true" indexed="true" multiValued="false"/>
<field name="curationStatusCreateTime" type="pdate" indexed="true" stored="true"/>
<field name="embargoEndDate" type="plong" stored="true" indexed="true" multiValued="false"/>
<field name="retentionEndDate" type="plong" stored="true" indexed="true" multiValued="false"/>

Expand Down
14 changes: 14 additions & 0 deletions doc/release-notes/9247-CurationStatusUpdates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
The External/Curation Status Label mechanism has been enhanced:

- adding tracking of who creates the status label and when,
- keeping a history of past statuses
- updating the CSV report to include the creation time and assigner of a status
- updating the getCurationStatus api call to return a JSON object for the status with label, assigner, and create time
- adding an includeHistory query param for these API calls to allow seeing prior statuses
- adding a facet to allow filtering by curation status (for users able to set them)
- adding the creation time to solr as a pdate to support search by time period, e.g. current status set prior to a give date
- standardizing the language around 'curation status' vs 'external status'
- adding a 'curation-status' class to displayed labels to allow styling
- adding a dataverse.ui.show-curation-status-to-all feature flag that allows users who can see a draft but not publish it to also view the curation status

Due to changes in the solr schema, updating the solr schema and reindexing is required. Background reindexing should be OK.
3 changes: 2 additions & 1 deletion doc/sphinx-guides/source/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ This API changelog is experimental and we would love feedback on its usefulness.

v6.7
----

- An undocumented :doc:`search` parameter called "show_my_data" has been removed. It was never exercised by tests and is believed to be unused. API users should use the :ref:`api-mydata` API instead.
- /api/datasets/{id}/curationStatus API now includes a JSON object with curation label, createtime, and assigner rather than a string 'label' and it supports a new boolean includeHistory parameter (default false) that returns a JSON array of statuses
- /api/datasets/{id}/listCurationStates includes new columns "Status Set Time" and "Status Set By" columns listing the time the current status was applied and by whom. It also supports the boolean includeHistory parameter.

v6.6
----
Expand Down
33 changes: 27 additions & 6 deletions doc/sphinx-guides/source/api/curation-labels.rst
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
Dataset Curation Label API
==========================
Dataset Curation Status API
===========================

When the :ref:`:AllowedCurationLabels <:AllowedCurationLabels>` setting has been used to define Curation Labels, this API can be used to set these labels on draft datasets.
Superusers can define which set of labels are allowed for a given datasets in a collection/an individual dataset using the api described in the :doc:`/admin/dataverses-datasets` section.
The API here can be used by curators/those who have permission to publish the dataset to get/set/change/delete the label currently assigned to a draft dataset.
If the :ref:`dataverse.ui.show-curation-status-to-all` flag is enabled, users who can see the draft dataset version can use the get API call.

This functionality is intended as a mechanism to integrate the Dataverse software with an external curation process/application: it is a way to make the state of a draft dataset,
as defined in the external process, visible within Dataverse. These labels have no other effect in Dataverse and are only visible to curators/those with permission to publish the dataset.
Any curation label assigned to a draft dataset will be removed upon publication.

Dataverse tracks the Curation Label as well as when it was assigned and by whom. It also keeps track of the history of prior assignments.

Get a Draft Dataset's Curation Label
------------------------------------
Get a Draft Dataset's Curation Status
-------------------------------------

.. code-block:: bash

Expand All @@ -27,8 +30,13 @@ Get a Draft Dataset's Curation Label

curl -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/:persistentId/curationStatus?persistentId=$DATASET_PID"

You should expect a 200 ("OK") response and the draft dataset's curation status label contained in a JSON 'data' object.
You should expect a 200 ("OK") response and the draft dataset's curation status as a JSON object contained in a JSON 'data' object. The status will include a 'label','createTime', and the 'assigner'.

If the optional includeHistory query parameter is set to true, the responses 'data' entry will be a JSON array of curation status objects

curl -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/:persistentId/curationStatus?persistentId=$DATASET_PID&includeHistory=true"

For draft datasets that were created prior to v6.7, it is possible that curation status objects will have no createTime or assigner.

Set a Draft Dataset's Curation Label
------------------------------------
Expand All @@ -53,6 +61,8 @@ To add a curation label for a draft Dataset, specify the Dataset ID (DATASET_ID)

You should expect a 200 ("OK") response indicating that the label has been set. 403/Forbidden and 400/Bad Request responses are also possible, i.e. if you don't have permission to make this change or are trying to add a label that isn't in the allowed set or to add a label to a dataset with no draft version.

Note that Dataverse will add the current time as the createTime and the user as the 'assigner' of the label.


Delete a Draft Dataset's Curation Label
---------------------------------------
Expand Down Expand Up @@ -98,7 +108,7 @@ You should expect a 200 ("OK") response with a comma-separated list of allowed l
Get a Report on the Curation Status of All Datasets
---------------------------------------------------

To get a CSV file listing the curation label assigned to each Dataset with a draft version, along with the creation and last modification dates, and list of those with permissions to publish the version.
To get a CSV file listing the curation statuses assigned to each Dataset with a draft version, along with the creation and last modification dates, and list of those with permissions to publish the version.

This API call is restricted to superusers.

Expand All @@ -112,3 +122,14 @@ This API call is restricted to superusers.
curl -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/listCurationStates"

You should expect a 200 ("OK") response with a CSV formatted response.

The CSV response includes the following columns in order:
#. Dataset Title (as a hyperlink to the dataset page)
#. Creation Date of the draft dataset version
#. Latest Modification Date of the draft dataset version
#. Assigned curation status or '<none>' if no curation status is assigned but was previously, null if no curation state has every been set.
#. Time when the curation status was applied to the draft dataset version
#. The user who assigned this curation status
#. (and beyond): Users (comma separated list) with the Roles (column headings) that can publish datasets and therefore see/set curation status
When includeHistory is true, multiple rows may be present for each dataset, showing the full history of curation statuses.

15 changes: 15 additions & 0 deletions doc/sphinx-guides/source/installation/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3270,6 +3270,21 @@ Defaults to ``true``.
Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable
``DATAVERSE_API_SHOW_LABEL_FOR_INCOMPLETE_WHEN_PUBLISHED``. Will accept ``[tT][rR][uU][eE]|1|[oO][nN]`` as "true" expressions.

.. _dataverse.ui.show-curation-status-to-all:

dataverse.ui.show-curation-status-to-all
++++++++++++++++++++++++++++++++++++++++

By default the curation status assigned to a draft dataset versioncan only be seen by those who can publish it. When this flag is true, anyone who can see the draft dataset can see the assigned status.
These users will also get notifications/emails about changes to the status.
See :ref:`:AllowedCurationLabels <:AllowedCurationLabels>` and the :doc:`/admin/dataverses-datasets` section for more information about curation status.

Defaults to ``false``.

Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable
``DATAVERSE_API_SHOW_CURATION_STATUS_TO_ALL``. Will accept ``[tT][rR][uU][eE]|1|[oO][nN]`` as "true" expressions.


.. _dataverse.signposting.level1-author-limit:

dataverse.signposting.level1-author-limit
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/CurationStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package edu.harvard.iq.dataverse;

import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import jakarta.persistence.*;

import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "curationstatus", indexes = {
@Index(name = "index_curationstatus_datasetversion", columnList = "datasetversion_id")
})
public class CurationStatus implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = true)
private String label;

@ManyToOne
@JoinColumn(name = "datasetversion_id", nullable = false)
private DatasetVersion datasetVersion;

@ManyToOne
@JoinColumn(name = "authenticateduser_id", nullable = true)
private AuthenticatedUser authenticatedUser;

@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = true)
private Date createTime;

// Constructors, getters, and setters

public CurationStatus() {
}

public CurationStatus(String label, DatasetVersion datasetVersion, AuthenticatedUser authenticatedUser) {
this.label = label;
this.datasetVersion = datasetVersion;
this.authenticatedUser = authenticatedUser;
this.createTime = new Date();
}

// Getters and setters for all fields

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getLabel() {
return label;
}

public void setLabel(String label) {
this.label = label;
}

public DatasetVersion getDatasetVersion() {
return datasetVersion;
}

public void setDatasetVersion(DatasetVersion datasetVersion) {
this.datasetVersion = datasetVersion;
}

public AuthenticatedUser getAuthenticatedUser() {
return authenticatedUser;
}

public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) {
this.authenticatedUser = authenticatedUser;
}

public Date getCreateTime() {
return createTime;
}

public void setCreateTime(Date createTime) {
this.createTime = createTime;
}

public boolean isNoStatus() {
return label == null || label.trim().isEmpty();
}
}
28 changes: 19 additions & 9 deletions src/main/java/edu/harvard/iq/dataverse/DatasetPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
import jakarta.servlet.http.HttpServletResponse;

import org.apache.commons.text.StringEscapeUtils;
import org.apache.logging.log4j.util.Strings;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.apache.commons.io.IOUtils;
import org.primefaces.component.selectonemenu.SelectOneMenu;
Expand Down Expand Up @@ -1477,6 +1478,15 @@ public boolean canViewUnpublishedDataset() {
return permissionsWrapper.canViewUnpublishedDataset( dvRequestService.getDataverseRequest(), dataset);
}

public boolean canSeeCurationStatus() {
boolean creatorsCanSeeStatus = JvmSettings.UI_SHOW_CURATION_STATUS_TO_ALL.lookupOptional(Boolean.class).orElse(false);
if (creatorsCanSeeStatus) {
return canViewUnpublishedDataset();
} else {
return canPublishDataset();
}
}

/*
* 4.2.1 optimization.
* HOWEVER, this doesn't appear to be saving us anything!
Expand Down Expand Up @@ -6278,28 +6288,28 @@ public String getFieldLanguage(String languages) {
return fieldService.getFieldLanguage(languages,session.getLocaleCode());
}

public void setExternalStatus(String status) {
public void setCurationStatus(String status) {
try {
dataset = commandEngine.submit(new SetCurationStatusCommand(dvRequestService.getDataverseRequest(), dataset, status));
workingVersion=dataset.getLatestVersion();
if (status == null || status.isEmpty()) {
JsfHelper.addInfoMessage(BundleUtil.getStringFromBundle("dataset.externalstatus.removed"));
if (Strings.isBlank(status)) {
JsfHelper.addInfoMessage(BundleUtil.getStringFromBundle("dataset.curationstatus.removed"));
} else {
JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("dataset.externalstatus.header"),
BundleUtil.getStringFromBundle("dataset.externalstatus.info",
Arrays.asList(DatasetUtil.getLocaleExternalStatus(status))
JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("dataset.curationstatus.header"),
BundleUtil.getStringFromBundle("dataset.curationstatus.info",
Arrays.asList(DatasetUtil.getLocaleCurationStatusLabelFromString(status))
));
}

} catch (CommandException ex) {
String msg = BundleUtil.getStringFromBundle("dataset.externalstatus.cantchange");
String msg = BundleUtil.getStringFromBundle("dataset.curationstatus.cantchange");
logger.warning("Unable to change external status to " + status + " for dataset id " + dataset.getId() + ". Message to user: " + msg + " Exception: " + ex);
JsfHelper.addErrorMessage(msg);
}
}

public List<String> getAllowedExternalStatuses() {
return settingsWrapper.getAllowedExternalStatuses(dataset);
public List<String> getAllowedCurationStatuses() {
return settingsWrapper.getAllowedCurationStatuses(dataset);
}

public Embargo getSelectionEmbargo() {
Expand Down
47 changes: 40 additions & 7 deletions src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,10 @@ public enum VersionState {
@OneToMany(mappedBy = "datasetVersion", cascade={CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST})
private List<WorkflowComment> workflowComments;

@Column(nullable=true)
private String externalStatusLabel;

@OneToMany(mappedBy = "datasetVersion", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("createTime DESC NULLS LAST")
private List<CurationStatus> curationStatuses = new ArrayList<>();

@Transient
private DatasetVersionDifference dvd;

Expand Down Expand Up @@ -2156,12 +2157,44 @@ public String getLocaleLastUpdateTime() {
return DateUtil.formatDate(new Timestamp(lastUpdateTime.getTime()));
}

public String getExternalStatusLabel() {
return externalStatusLabel;
// Add methods to manage curationLabels
public List<CurationStatus> getCurationStatuses() {
return curationStatuses;
}

protected void setCurationStatuses(List<CurationStatus> curationStatuses) {
this.curationStatuses = curationStatuses;
}

public CurationStatus getCurrentCurationStatus() {
return !getCurationStatuses().isEmpty() ? getCurationStatuses().get(0) : null;
}


public void addCurationStatus(CurationStatus status) {
status.setDatasetVersion(this);
curationStatuses.add(0, status); // Add the new status at the beginning of the list
}

public void setExternalStatusLabel(String externalStatusLabel) {
this.externalStatusLabel = externalStatusLabel;
public void removeCurationStatus(CurationStatus curationStatus) {
curationStatuses.remove(curationStatus);
curationStatus.setDatasetVersion(null);
}

public CurationStatus getCurationStatusAsOfDate(Date date) {
if (curationStatuses == null || curationStatuses.isEmpty()) {
return null;
}

// Find the first status whose createTime is before or equal to the given date
for (CurationStatus status : curationStatuses) {
if (status.getCreateTime().compareTo(date) <= 0) {
return status;
}
}

// If no status is found before the given date, return null
return null;
}

public String getVersionNote() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/harvard/iq/dataverse/DataversePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,7 @@ public Set<Entry<String, String>> getCurationLabelSetOptions() {
setNames.put(BundleUtil.getStringFromBundle("dataverse.curationLabels.disabled"), SystemConfig.CURATIONLABELSDISABLED);

allowedSetNames.forEach(name -> {
String localizedName = DatasetUtil.getLocaleExternalStatus(name) ;
String localizedName = DatasetUtil.getLocaleCurationStatusLabelFromString(name) ;
setNames.put(localizedName,name);
});
}
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/FilePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,15 @@ private boolean canViewUnpublishedDataset() {
public boolean canPublishDataset(){
return permissionsWrapper.canIssuePublishDatasetCommand(fileMetadata.getDatasetVersion().getDataset());
}


public boolean canSeeCurationStatus() {
boolean creatorsCanSeeStatus = JvmSettings.UI_SHOW_CURATION_STATUS_TO_ALL.lookupOptional(Boolean.class).orElse(false);
if (creatorsCanSeeStatus) {
return canViewUnpublishedDataset();
} else {
return canPublishDataset();
}
}

public FileMetadata getFileMetadata() {
return fileMetadata;
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,20 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio
case STATUSUPDATED:
version = (DatasetVersion) targetObject;
pattern = BundleUtil.getStringFromBundle("notification.email.status.change");
String[] paramArrayStatus = {version.getDataset().getDisplayName(), (version.getExternalStatusLabel()==null) ? "<none>" : DatasetUtil.getLocaleExternalStatus(version.getExternalStatusLabel())};
CurationStatus status = version.getCurationStatusAsOfDate(userNotification.getSendDateTimestamp());
String curationLabel = DatasetUtil.getLocaleCurationStatusLabel(status);
if(curationLabel == null) {
curationLabel = BundleUtil.getStringFromBundle("dataset.curationstatus.none");
}
String[] paramArrayStatus = {
version.getDataset().getDisplayName(),
getDatasetLink(version.getDataset()),
version.getDataset().getOwner().getDisplayName(),
getDataverseLink(version.getDataset().getOwner()),
curationLabel
};
messageText += MessageFormat.format(pattern, paramArrayStatus);

return messageText;
case PIDRECONCILED:
version = (DatasetVersion) targetObject;
Expand Down
Loading