diff --git a/conf/solr/8.8.1/schema.xml b/conf/solr/8.8.1/schema.xml index c6f6cd37cd6..3d46a8336be 100644 --- a/conf/solr/8.8.1/schema.xml +++ b/conf/solr/8.8.1/schema.xml @@ -162,6 +162,7 @@ + diff --git a/doc/release-notes/7967-curation-labels.md b/doc/release-notes/7967-curation-labels.md new file mode 100644 index 00000000000..2742519329c --- /dev/null +++ b/doc/release-notes/7967-curation-labels.md @@ -0,0 +1,35 @@ +### Curation Status Labels + +A new :AllowedCurationLabels setting allows a sysadmins to define one or more sets of labels that can be applied to a draft Dataset version via the user interface or API to indicate the status of the dataset with respect to a defined curation process. + +Labels are completely customizable (alphanumeric or spaces, up to 32 characters, e.g. "Author contacted", "Privacy Review", "Awaiting paper publication"). Superusers can select a specific set of labels, or disable this functionality per collection. Anyone who can publish a draft dataset (e.g. curators) can set/change/remove labels (from the set specified for the collection containing the dataset) via the user interface or via an API. The API also would allow external tools to search for, read and set labels on Datasets, providing an integration mechanism. Labels are visible on the Dataset page and in Dataverse collection listings/search results. Internally, the labels have no effect, and at publication, any existing label will be removed. A reporting API call allows admins to get a list of datasets and their curation statuses. + +The Solr schema must be updated as part of installing the release of Dataverse containing this feature for it to work. + +## Additional Release Steps + +1\. Replace Solr schema.xml to allow Curation Labels to be used. See specific instructions below for those installations with custom metadata blocks (1a) and those without (1b). + +1a\. + +For installations with Custom Metadata Blocks: + +-stop solr instance (usually service solr stop, depending on solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.7/installation/prerequisites.html#solr-init-script) + +- add the following line to your schema.xml: + + + +- restart solr instance (usually service solr start, depending on solr/OS) + +1b\. + +For installations without Custom Metadata Blocks: + +-stop solr instance (usually service solr stop, depending on solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.7/installation/prerequisites.html#solr-init-script) + +-replace schema.xml + +cp /tmp/dvinstall/schema.xml /usr/local/solr/solr-8.8.1/server/solr/collection1/conf + +-start solr instance (usually service solr start, depending on solr/OS) \ No newline at end of file diff --git a/doc/sphinx-guides/source/admin/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index a18204588c2..842dbc02b74 100644 --- a/doc/sphinx-guides/source/admin/dataverses-datasets.rst +++ b/doc/sphinx-guides/source/admin/dataverses-datasets.rst @@ -40,7 +40,7 @@ Recursively assigns the users and groups having a role(s),that are in the set co curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/$dataverse-alias/addRoleAssignmentsToChildren -Configure a Dataverse Collection to store all new files in a specific file store +Configure a Dataverse Collection to Store All New Files in a Specific File Store ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To direct new files (uploaded when datasets are created or edited) for all datasets in a given Dataverse collection, the store can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Only accessible to superusers. :: @@ -61,6 +61,36 @@ The available drivers can be listed with:: (Individual datasets can be configured to use specific file stores as well. See the "Datasets" section below.) +Configure a Dataverse Collection to Allow Use of a Given Curation Label Set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Datasets within a given Dataverse collection can be annotated with a Curation Label to indicate the status of the dataset with respect to a defined curation process. Labels are completely customizable (alphanumeric or spaces, up to 32 characters, e.g. "Author contacted", "Privacy Review", "Awaiting paper publication"). + +The label is applied to a draft Dataset version via the user interface or API and the available label sets are defined by :ref:`:AllowedCurationLabels <:AllowedCurationLabels>`. Internally, the labels have no effect, and at publication, any existing label will be removed. A reporting API call allows admins to get a list of datasets and their curation statuses. + +The label set used for a collection can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Only accessible to superusers. + +The curationLabelSet to use within a given collection can be set by specifying its name using:: + + curl -H "X-Dataverse-key: $API_TOKEN" -X PUT http://$SERVER/api/admin/dataverse/$dataverse-alias/curationLabelSet?name=$curationLabelSetName + +The reserved word "DISABLED" can be used to disable this feature within a given Dataverse collection. + +The name of the current curationLabelSet can be seen using:: + + curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/$dataverse-alias/curationLabelSet + +and can be reset to the default (inherited from the parent collection or DISABLED for the root collection) with:: + + curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/admin/dataverse/$dataverse-alias/curationLabelSet + +The available curation label sets can be listed with:: + + curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/curationLabelSets + +If the :AllowedCurationLabels setting has a value, one of the available choices will always be "DISABLED" which allows curation labels to be turned off for a given collection/dataset. + +Individual datasets can be configured to use specific curationLabelSets as well. See the "Datasets" section below. Datasets -------- @@ -134,7 +164,7 @@ Diagnose Constraint Violations Issues in Datasets To identify invalid data values in specific datasets (if, for example, an attempt to edit a dataset results in a ConstraintViolationException in the server log), or to check all the datasets in the Dataverse installation for constraint violations, see :ref:`Dataset Validation ` in the :doc:`/api/native-api` section of the User Guide. -Configure a Dataset to store all new files in a specific file store +Configure a Dataset to Store All New Files in a Specific File Store ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Configure a dataset to use a specific file store (this API can only be used by a superuser) :: @@ -153,4 +183,33 @@ The available drivers can be listed with:: curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/storageDrivers +Configure a Dataset to Allow Use of a Curation Label Set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A dataset can be annotated with a Curation Label to indicate the status of the dataset with respect to a defined curation process. Labels are completely customizable (alphanumeric or spaces, up to 32 characters, e.g. "Author contacted", "Privacy Review", "Awaiting paper publication"). + +The label is applied to a draft Dataset version via the user interface or API and the available label sets are defined by :ref:`:AllowedCurationLabels <:AllowedCurationLabels>`. Internally, the labels have no effect, and at publication, any existing label will be removed. A reporting API call allows admins to get a list of datasets and their curation statuses. + +The label set used for a dataset can be specified via the API as shown below. Only accessible to superusers. + +The curationLabelSet to use within a given dataset can be set by specifying its name using:: + + curl -H "X-Dataverse-key: $API_TOKEN" -X PUT http://$SERVER/api/datasets/$dataset-id/curationLabelSet?name=$curationLabelSetName + +The reserved word "DISABLED" can be used to disable this feature within a given Dataverse collection. + +The name of the current curationLabelSet can be seen using:: + + curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/datasets/$dataset-id/curationLabelSet + +and can be reset to the default (inherited from the parent collection) with (only a superuser can do this) :: + + curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/datasets/$dataset-id/curationLabelSet + +The available curationLabelSets can be listed with:: + + curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/curationLabelSets + +If the :AllowedCurationLabels setting has a value, one of the available choices will always be "DISABLED" which allows curation labels to be turned off for a given collection/dataset. +Collections can be configured to use specific curationLabelSets as well. See the "Dataverse Collections" section above. diff --git a/doc/sphinx-guides/source/api/curation-labels.rst b/doc/sphinx-guides/source/api/curation-labels.rst new file mode 100644 index 00000000000..36950a37eb3 --- /dev/null +++ b/doc/sphinx-guides/source/api/curation-labels.rst @@ -0,0 +1,95 @@ +Dataset Curation Label 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. + +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. + +Get a Draft Dataset's Curation Label +------------------------------------ + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export DATASET_ID='12345' + export DATASET_PID='doi:10.5072/FK2A1B2C3' + export SERVER_URL=https://demo.dataverse.org + + Example 1: Get the label using the DATASET ID + + curl -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/$DATASET_ID/curationStatus" + + Example 2: Get the label using the DATASET PID + + 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. + + +Set a Draft Dataset's Curation Label +------------------------------------ + +To add a curation label for a draft Dataset, specify the Dataset ID (DATASET_ID) or Persistent identifier (DATASET_PID) and the label desired. + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export DATASET_ID='12345' + export DATASET_PID='doi:10.5072/FK2A1B2C3' + export LABEL='Author contacted' + export SERVER_URL=https://demo.dataverse.org + + Example: Add the label using the DATASET ID + + curl -X PUT -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/$DATASET_ID/curationStatus?label=$LABEL" + + Example 2: Add a description using the DATASET PID + + curl -X PUT -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/:persistentId/curationStatus?label=$LABEL&persistentId=$DATASET_PID" + +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. + + +Delete a Draft Dataset's Curation Label +--------------------------------------- + +To delete the curation label on a draft Dataset, specify the Dataset ID (DATASET_ID) or Persistent identifier (DATASET_PID). + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export DATASET_PID='doi:10.5072/FK2A1B2C3' + export SERVER_URL=https://demo.dataverse.org + + Example: Delete the label using the DATASET PID + + curl -X DELETE -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/:persistentId/curationStatus?persistentId=$DATASET_PID" + +You should expect a 200 ("OK") response indicating the label has been removed. + + +Get the Set of Allowed Labels for a Dataset +------------------------------------------- + +To get the list of allowed curation labels allowed for a given Dataset + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export DATASET_ID='12345' + export DATASET_PID='doi:10.5072/FK2A1B2C3' + export SERVER_URL=https://demo.dataverse.org + + Example 1: Get the label using the DATASET ID + + curl -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/$DATASET_ID/allowedCurationLabels" + + Example 2: Get the label using the DATASET PID + + curl -H X-Dataverse-key:$API_TOKEN "$SERVER_URL/api/datasets/:persistentId/allowedCurationLabels?persistentId=$DATASET_PID" + +You should expect a 200 ("OK") response with a comma-separated list of allowed labels contained in a JSON 'data' object. diff --git a/doc/sphinx-guides/source/api/index.rst b/doc/sphinx-guides/source/api/index.rst index 45cdb1918fe..9fc58ef4e5a 100755 --- a/doc/sphinx-guides/source/api/index.rst +++ b/doc/sphinx-guides/source/api/index.rst @@ -20,5 +20,6 @@ API Guide sword client-libraries external-tools + curation-labels apps faq diff --git a/doc/sphinx-guides/source/developers/index.rst b/doc/sphinx-guides/source/developers/index.rst index 405e2e64a59..bf525422c84 100755 --- a/doc/sphinx-guides/source/developers/index.rst +++ b/doc/sphinx-guides/source/developers/index.rst @@ -39,3 +39,4 @@ Developer Guide dataset-migration-api workflows fontcustom + diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index bdc21ba29fc..50c856085a1 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2385,4 +2385,17 @@ Scripts that implement this association for specific service protocols are maint ``curl -X PUT --upload-file cvoc-conf.json http://localhost:8080/api/admin/settings/:CVocConf`` +.. _:AllowedCurationLabels: + +:AllowedCurationLabels +++++++++++++++++++++++ + +A JSON Object containing lists of allowed labels (up to 32 characters, spaces allowed) that can be set, via API or UI by users with the permission to publish a dataset. The set of labels allowed +for datasets can be selected by a superuser - via the Dataverse collection page (Edit/General Info) or set via API call. +The labels in a set should correspond to the states in an organization's curation process and are intended to help users/curators track the progress of a dataset through a defined curation process. +A dataset may only have one label at a time and if a label is set, it will be removed at publication time. +This functionality is disabled when this setting is empty/not set. +Each set of labels is identified by a curationLabelSet name and a JSON Array of the labels allowed in that set. + +``curl -X PUT -d '{"Standard Process":["Author contacted", "Privacy Review", "Awaiting paper publication", "Final Approval"], "Alternate Process":["State 1","State 2","State 3"]}' http://localhost:8080/api/admin/settings/:AllowedCurationLabels`` diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 128470ddbde..6523e564be1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -107,6 +107,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetResult; import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.ReturnDatasetToAuthorCommand; +import edu.harvard.iq.dataverse.engine.command.impl.SetCurationStatusCommand; import edu.harvard.iq.dataverse.engine.command.impl.SubmitDatasetForReviewCommand; import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; @@ -5546,4 +5547,25 @@ public List getVocabScripts() { public String getFieldLanguage(String languages) { return fieldService.getFieldLanguage(languages,session.getLocaleCode()); } + + public void setExternalStatus(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")); + } else { + JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("dataset.externalstatus.header"), BundleUtil.getStringFromBundle("dataset.externalstatus.info", Arrays.asList(status))); + } + + } catch (CommandException ex) { + String msg = BundleUtil.getStringFromBundle("dataset.externalstatus.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 getAllowedExternalStatuses() { + return settingsWrapper.getAllowedExternalStatuses(dataset); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 2cf1f0d094f..288575d5462 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -198,6 +198,11 @@ public List findAllUnindexed() { return em.createQuery("SELECT o.id FROM Dataset o WHERE o.indexTime IS null ORDER BY o.id DESC", Long.class).getResultList(); } + //Used in datasets listcurationstatus API + public List findAllUnpublished() { + return em.createQuery("SELECT object(o) FROM Dataset o, DvObject d WHERE d.id=o.id and d.publicationDate IS null ORDER BY o.id ASC", Dataset.class).getResultList(); + } + /** * For docs, see the equivalent method on the DataverseServiceBean. * @param numPartitions diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index 3a212a16f22..e278d6a558d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -171,6 +171,8 @@ public enum License { @OneToMany(mappedBy = "datasetVersion", cascade={CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST}) private List workflowComments; + @Column(nullable=true) + private String externalStatusLabel; public Long getId() { return this.id; @@ -1961,4 +1963,12 @@ public String getLocaleLastUpdateTime() { return DateUtil.formatDate(new Timestamp(lastUpdateTime.getTime())); } + public String getExternalStatusLabel() { + return externalStatusLabel; + } + + public void setExternalStatusLabel(String externalStatusLabel) { + this.externalStatusLabel = externalStatusLabel; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index c6731d26503..18886ce41a2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -1210,4 +1211,49 @@ public Set> getMetadataLanguages() { return settingsWrapper.getMetadataLanguages(this.dataverse).entrySet(); } + public Set> getCurationLabelSetOptions() { + HashMap setNames = new HashMap(); + Set allowedSetNames = systemConfig.getCurationLabels().keySet(); + if (allowedSetNames.size() > 0) { + // Add an entry for the default (inherited from an ancestor or the system + // default) + String inheritedLabelSet = getCurationLabelSetNameLabel(); + if (!StringUtils.isBlank(inheritedLabelSet)) { + setNames.put(inheritedLabelSet,SystemConfig.DEFAULTCURATIONLABELSET); + } + // Add an entry for disabled + setNames.put(BundleUtil.getStringFromBundle("dataverse.curationLabels.disabled"), SystemConfig.CURATIONLABELSDISABLED); + allowedSetNames.forEach(name -> { + setNames.put(name, name); + }); + } + return setNames.entrySet(); + } + + public String getCurationLabelSetNameLabel() { + Dataverse parent = dataverse.getOwner(); + String setName = null; + boolean fromAncestor = false; + if (parent != null) { + setName = parent.getEffectiveCurationLabelSetName(); + // recurse dataverse chain to root and if any have a curation label set name set, + // fromAncestor is true + while (parent != null) { + if (!parent.getCurationLabelSetName().equals(SystemConfig.DEFAULTCURATIONLABELSET)) { + fromAncestor = true; + break; + } + parent = parent.getOwner(); + } + } + if (setName != null) { + if (fromAncestor) { + setName = setName + " " + BundleUtil.getStringFromBundle("dataverse.inherited"); + } else { + setName = setName + " " + BundleUtil.getStringFromBundle("dataverse.default"); + } + } + return setName; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java index 87206020e36..8bde808252f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java @@ -1,8 +1,8 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.Locale; - import javax.persistence.MappedSuperclass; import org.apache.commons.lang3.StringUtils; @@ -95,4 +95,34 @@ public void setMetadataLanguage(String ml) { } } + + + /* Dataverse collections can be configured to allow use of Curation labels and have this inheritable value to decide which set of labels to use. + * This mechanism is similar to that for the storageDriver except that there is an addition option to disable use of labels. + */ + private String externalLabelSetName = null; + + public String getEffectiveCurationLabelSetName() { + String setName = externalLabelSetName; + if (StringUtils.isBlank(setName) || setName.equals(SystemConfig.DEFAULTCURATIONLABELSET)) { + if (this.getOwner() != null) { + setName = this.getOwner().getEffectiveCurationLabelSetName(); + } else { + setName = SystemConfig.CURATIONLABELSDISABLED; + } + } + return setName; + } + + public String getCurationLabelSetName() { + if (externalLabelSetName == null) { + return SystemConfig.DEFAULTCURATIONLABELSET; + } + return externalLabelSetName; + } + + public void setCurationLabelSetName(String setName) { + this.externalLabelSetName = setName; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 045ac1f934a..f3488d950db 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -238,6 +238,10 @@ public String init() { private boolean canViewUnpublishedDataset() { return permissionsWrapper.canViewUnpublishedDataset( dvRequestService.getDataverseRequest(), fileMetadata.getDatasetVersion().getDataset()); } + + public boolean canPublishDataset(){ + return permissionsWrapper.canIssuePublishDatasetCommand(fileMetadata.getDatasetVersion().getDataset()); + } public FileMetadata getFileMetadata() { diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index d3ecf1001f4..1067e341d31 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -528,6 +528,12 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio String[] paramArrayWorkflowFailure = {version.getDataset().getDisplayName(), getDatasetLink(version.getDataset()), comment}; messageText += MessageFormat.format(pattern, paramArrayWorkflowFailure); return messageText; + case STATUSUPDATED: + version = (DatasetVersion) targetObject; + pattern = BundleUtil.getStringFromBundle("notification.email.status.change"); + String[] paramArrayStatus = {version.getDataset().getDisplayName(), (version.getExternalStatusLabel()==null) ? "" : version.getExternalStatusLabel()}; + messageText += MessageFormat.format(pattern, paramArrayStatus); + return messageText; case CREATEACC: InternetAddress systemAddress = getSystemAddress(); String accountCreatedMessage = BundleUtil.getStringFromBundle("notification.email.welcome", Arrays.asList( @@ -628,6 +634,7 @@ private Object getObjectOfNotification (UserNotification userNotification){ case RETURNEDDS: case WORKFLOW_SUCCESS: case WORKFLOW_FAILURE: + case STATUSUPDATED: return versionService.find(userNotification.getObjectId()); case CREATEACC: return userNotification.getUser(); diff --git a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java index 752e3e879fe..73ffeb73ee6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java @@ -6,6 +6,7 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; import edu.harvard.iq.dataverse.settings.Setting; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; @@ -22,6 +23,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.logging.Logger; + import javax.ejb.EJB; import javax.faces.view.ViewScoped; import javax.inject.Named; @@ -34,6 +37,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.omnifaces.el.functions.Strings; /** * @@ -42,6 +46,8 @@ @ViewScoped @Named public class SettingsWrapper implements java.io.Serializable { + + private static final Logger logger = Logger.getLogger(SettingsWrapper.class.getCanonicalName()); @EJB SettingsServiceBean settingsService; @@ -373,5 +379,18 @@ public String getDefaultMetadataLanguage() { } } + List allowedExternalStatuses = null; + + public List getAllowedExternalStatuses(Dataset d) { + String setName = d.getEffectiveCurationLabelSetName(); + if(setName.equals(SystemConfig.CURATIONLABELSDISABLED)) { + return new ArrayList(); + } + String[] labelArray = systemConfig.getCurationLabels().get(setName); + if(labelArray==null) { + return new ArrayList(); + } + return Arrays.asList(labelArray); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index 1df51137969..335454053e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -72,6 +72,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.omnifaces.el.functions.Strings; import java.util.List; import edu.harvard.iq.dataverse.authorization.AuthTestDataServiceBean; @@ -96,6 +97,8 @@ import edu.harvard.iq.dataverse.util.ArchiverUtil; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; + import java.io.IOException; import java.io.OutputStream; @@ -1896,6 +1899,97 @@ public Response listStorageDrivers() throws WrappedResponse { return ok(bld); } + @GET + @Path("/dataverse/{alias}/curationLabelSet") + public Response getCurationLabelSet(@PathParam("alias") String alias) throws WrappedResponse { + Dataverse dataverse = dataverseSvc.findByAlias(alias); + if (dataverse == null) { + return error(Response.Status.NOT_FOUND, "Could not find dataverse based on alias supplied: " + alias + "."); + } + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + // Note that this returns what's set directly on this dataverse. If + // null/SystemConfig.DEFAULTCURATIONLABELSET, the user would have to recurse the + // chain of parents to find the effective curationLabelSet + return ok(dataverse.getCurationLabelSetName()); + } + + @PUT + @Path("/dataverse/{alias}/curationLabelSet") + public Response setCurationLabelSet(@PathParam("alias") String alias, @QueryParam("name") String name) throws WrappedResponse { + Dataverse dataverse = dataverseSvc.findByAlias(alias); + if (dataverse == null) { + return error(Response.Status.NOT_FOUND, "Could not find dataverse based on alias supplied: " + alias + "."); + } + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (SystemConfig.CURATIONLABELSDISABLED.equals(name) || SystemConfig.DEFAULTCURATIONLABELSET.equals(name)) { + dataverse.setCurationLabelSetName(name); + return ok("Curation Label Set Name set to: " + name); + } else { + for (String setName : systemConfig.getCurationLabels().keySet()) { + if (setName.equals(name)) { + dataverse.setCurationLabelSetName(name); + return ok("Curation Label Set Name set to: " + setName); + } + } + } + return error(Response.Status.BAD_REQUEST, + "No Curation Label Set found for : " + name); + } + + @DELETE + @Path("/dataverse/{alias}/curationLabelSet") + public Response resetCurationLabelSet(@PathParam("alias") String alias) throws WrappedResponse { + Dataverse dataverse = dataverseSvc.findByAlias(alias); + if (dataverse == null) { + return error(Response.Status.NOT_FOUND, "Could not find dataverse based on alias supplied: " + alias + "."); + } + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + dataverse.setCurationLabelSetName(SystemConfig.DEFAULTCURATIONLABELSET); + return ok("Curation Label Set reset to default: " + SystemConfig.DEFAULTCURATIONLABELSET); + } + + @GET + @Path("/dataverse/curationLabelSets") + public Response listCurationLabelSets() throws WrappedResponse { + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + JsonObjectBuilder bld = Json.createObjectBuilder(); + + systemConfig.getCurationLabels().entrySet().forEach(s -> { + JsonArrayBuilder labels = Json.createArrayBuilder(); + Arrays.asList(s.getValue()).forEach(l -> labels.add(l)); + bld.add(s.getKey(), labels); + }); + return ok(bld); + } + @POST @Path("/bannerMessage") public Response addBannerMessage(JsonObject jsonObject) throws WrappedResponse { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index f382320d8a2..1cca0157844 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -13,6 +13,7 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseRequestServiceBean; +import edu.harvard.iq.dataverse.DataverseRoleServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.DvObject; @@ -69,6 +70,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.RequestRsyncScriptCommand; import edu.harvard.iq.dataverse.engine.command.impl.ReturnDatasetToAuthorCommand; import edu.harvard.iq.dataverse.engine.command.impl.SetDatasetCitationDateCommand; +import edu.harvard.iq.dataverse.engine.command.impl.SetCurationStatusCommand; import edu.harvard.iq.dataverse.engine.command.impl.SubmitDatasetForReviewCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetTargetURLCommand; @@ -120,12 +122,14 @@ import java.io.StringReader; import java.sql.Timestamp; import java.text.MessageFormat; +import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -173,6 +177,7 @@ import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; +import org.omnifaces.el.functions.Strings; import com.amazonaws.services.s3.model.PartETag; @@ -181,7 +186,7 @@ public class Datasets extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Datasets.class.getCanonicalName()); - @Inject DataverseSession session; + @Inject DataverseSession session; @EJB DatasetServiceBean datasetService; @@ -237,6 +242,9 @@ public class Datasets extends AbstractApiBean { @Inject WorkflowServiceBean wfService; + + @Inject + DataverseRoleServiceBean dataverseRoleService; /** * Used to consolidate the way we parse and handle dataset versions. @@ -1752,6 +1760,62 @@ public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBo } } + @GET + @Path("{id}/curationStatus") + public Response getCurationStatus(@PathParam("id") String idSupplied) { + try { + Dataset ds = findDatasetOrDie(idSupplied); + DatasetVersion dsv = ds.getLatestVersion(); + if (dsv.isDraft() && permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), ds).has(Permission.PublishDataset)) { + return response(req -> ok(dsv.getExternalStatusLabel()==null ? "":dsv.getExternalStatusLabel())); + } else { + return error(Response.Status.FORBIDDEN, "You are not permitted to view the curation status of this dataset."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @PUT + @Path("{id}/curationStatus") + public Response setCurationStatus(@PathParam("id") String idSupplied, @QueryParam("label") String label) { + Dataset ds = null; + User u = null; + try { + ds = findDatasetOrDie(idSupplied); + u = findUserOrDie(); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + try { + execCommand(new SetCurationStatusCommand(createDataverseRequest(u), ds, label)); + return ok("Curation Status updated"); + } catch (WrappedResponse wr) { + // Just change to Bad Request and send + return Response.fromResponse(wr.getResponse()).status(Response.Status.BAD_REQUEST).build(); + } + } + + @DELETE + @Path("{id}/curationStatus") + public Response deleteCurationStatus(@PathParam("id") String idSupplied) { + Dataset ds = null; + User u = null; + try { + ds = findDatasetOrDie(idSupplied); + u = findUserOrDie(); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + try { + execCommand(new SetCurationStatusCommand(createDataverseRequest(u), ds, null)); + return ok("Curation Status deleted"); + } catch (WrappedResponse wr) { + //Just change to Bad Request and send + return Response.fromResponse(wr.getResponse()).status(Response.Status.BAD_REQUEST).build(); + } + } + @GET @Path("{id}/uploadsid") @Deprecated @@ -2564,6 +2628,127 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); } + @GET + @Path("{identifier}/curationLabelSet") + public Response getCurationLabelSet(@PathParam("identifier") String dvIdtf, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + Dataset dataset; + + try { + dataset = findDatasetOrDie(dvIdtf); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + + return response(req -> ok(dataset.getEffectiveCurationLabelSetName())); + } + + @PUT + @Path("{identifier}/curationLabelSet") + public Response setCurationLabelSet(@PathParam("identifier") String dvIdtf, + @QueryParam("name") String curationLabelSet, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + + // Superuser-only: + AuthenticatedUser user; + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.UNAUTHORIZED, "Authentication is required."); + } + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + + Dataset dataset; + + try { + dataset = findDatasetOrDie(dvIdtf); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + if (SystemConfig.CURATIONLABELSDISABLED.equals(curationLabelSet) || SystemConfig.DEFAULTCURATIONLABELSET.equals(curationLabelSet)) { + dataset.setCurationLabelSetName(curationLabelSet); + datasetService.merge(dataset); + return ok("Curation Label Set Name set to: " + curationLabelSet); + } else { + for (String setName : systemConfig.getCurationLabels().keySet()) { + if (setName.equals(curationLabelSet)) { + dataset.setCurationLabelSetName(curationLabelSet); + datasetService.merge(dataset); + return ok("Curation Label Set Name set to: " + setName); + } + } + } + return error(Response.Status.BAD_REQUEST, + "No Such Curation Label Set"); + } + + @DELETE + @Path("{identifier}/curationLabelSet") + public Response resetCurationLabelSet(@PathParam("identifier") String dvIdtf, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + + // Superuser-only: + AuthenticatedUser user; + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.BAD_REQUEST, "Authentication is required."); + } + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + + Dataset dataset; + + try { + dataset = findDatasetOrDie(dvIdtf); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + + dataset.setCurationLabelSetName(SystemConfig.DEFAULTCURATIONLABELSET); + datasetService.merge(dataset); + return ok("Curation Label Set reset to default: " + SystemConfig.DEFAULTCURATIONLABELSET); + } + + @GET + @Path("{identifier}/allowedCurationLabels") + public Response getAllowedCurationLabels(@PathParam("identifier") String dvIdtf, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + AuthenticatedUser user = null; + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + Dataset dataset; + + try { + dataset = findDatasetOrDie(dvIdtf); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + if (permissionSvc.requestOn(createDataverseRequest(user), dataset).has(Permission.PublishDataset)) { + String[] labelArray = systemConfig.getCurationLabels().get(dataset.getEffectiveCurationLabelSetName()); + return response(req -> ok(String.join(",", labelArray))); + } else { + return error(Response.Status.FORBIDDEN, "You are not permitted to view the allowed curation labels for this dataset."); + } + } + @GET @Path("{identifier}/timestamps") @Produces(MediaType.APPLICATION_JSON) @@ -2704,4 +2889,65 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, return addFileHelper.addFiles(jsonData, dataset, authUser); } + + /** + * API to find curation assignments and statuses + * + * @return + * @throws WrappedResponse + */ + @GET + @Path("/listCurationStates") + @Produces("text/csv") + public Response getCurationStates() throws WrappedResponse { + + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + List allRoles = dataverseRoleService.findAll(); + List curationRoles = new ArrayList(); + allRoles.forEach(r -> { + if (r.permissions().contains(Permission.PublishDataset)) + curationRoles.add(r); + }); + HashMap> assignees = new HashMap>(); + curationRoles.forEach(r -> { + assignees.put(r.getAlias(), null); + }); + + StringBuilder csvSB = new StringBuilder(String.join(",", + BundleUtil.getStringFromBundle("dataset"), + BundleUtil.getStringFromBundle("datasets.api.creationdate"), + BundleUtil.getStringFromBundle("datasets.api.modificationdate"), + BundleUtil.getStringFromBundle("datasets.api.curationstatus"), + String.join(",", assignees.keySet()))); + for (Dataset dataset : datasetSvc.findAllUnpublished()) { + List ras = permissionService.assignmentsOn(dataset); + curationRoles.forEach(r -> { + assignees.put(r.getAlias(), new HashSet()); + }); + for (RoleAssignment ra : ras) { + if (curationRoles.contains(ra.getRole())) { + assignees.get(ra.getRole().getAlias()).add(ra.getAssigneeIdentifier()); + } + } + String name = "\"" + dataset.getCurrentName().replace("\"", "\"\"") + "\""; + String status = dataset.getLatestVersion().getExternalStatusLabel(); + String url = systemConfig.getDataverseSiteUrl() + dataset.getTargetUrl() + dataset.getGlobalId().asString(); + String date = new SimpleDateFormat("yyyy-MM-dd").format(dataset.getCreateDate()); + String modDate = new SimpleDateFormat("yyyy-MM-dd").format(dataset.getModificationTime()); + String hyperlink = "\"=HYPERLINK(\"\"" + url + "\"\",\"\"" + name + "\"\")\""; + List sList = new ArrayList(); + assignees.entrySet().forEach(e -> sList.add(e.getValue().size() == 0 ? "" : String.join(";", e.getValue()))); + csvSB.append("\n").append(String.join(",", hyperlink, date, modDate, status == null ? "" : status, String.join(",", sList))); + } + csvSB.append("\n"); + return ok(csvSB.toString(), MediaType.valueOf(FileUtil.MIME_TYPE_CSV), "datasets.status.csv"); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index 846e091f9c7..ecaeef59a88 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -493,6 +493,7 @@ public void displayNotification() { case RETURNEDDS: case WORKFLOW_SUCCESS: case WORKFLOW_FAILURE: + case STATUSUPDATED: userNotification.setTheObject(datasetVersionService.find(userNotification.getObjectId())); break; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index f07156f81a1..01ac0cf5804 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -115,6 +115,9 @@ public Dataset execute(CommandContext ctxt) throws CommandException { theDataset.setPublicationDate(new Timestamp(new Date().getTime())); } + //Clear any external status + theDataset.getLatestVersion().setExternalStatusLabel(null); + // update metadata if (theDataset.getLatestVersion().getReleaseTime() == null) { // Allow migrated versions to keep original release dates diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCurationStatusCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCurationStatusCommand.java new file mode 100644 index 00000000000..4a7b8ee1d34 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/SetCurationStatusCommand.java @@ -0,0 +1,116 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetLock; +import edu.harvard.iq.dataverse.DatasetVersionUser; +import edu.harvard.iq.dataverse.UserNotification; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.batch.util.LoggingUtil; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; + +import java.io.IOException; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Future; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.solr.client.solrj.SolrServerException; + +import com.beust.jcommander.Strings; +import com.google.api.LabelDescriptor; + +@RequiredPermissions(Permission.PublishDataset) +public class SetCurationStatusCommand extends AbstractDatasetCommand { + + private static final Logger logger = Logger.getLogger(SetCurationStatusCommand.class.getName()); + + String label; + + public SetCurationStatusCommand(DataverseRequest aRequest, Dataset dataset, String label) { + super(aRequest, dataset); + this.label=label; + } + + @Override + public Dataset execute(CommandContext ctxt) throws CommandException { + + if (getDataset().getLatestVersion().isReleased()) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.status.failure.isReleased"), this); + } + if (label==null || label.isEmpty()) { + getDataset().getLatestVersion().setExternalStatusLabel(null); + } else { + String setName = getDataset().getEffectiveCurationLabelSetName(); + if(setName.equals(SystemConfig.CURATIONLABELSDISABLED)) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.status.failure.disabled"), this); + } + String[] labelArray = ctxt.systemConfig().getCurationLabels().get(setName); + boolean found = false; + for(String name: labelArray) { + if(name.equals(label)) { + found=true; + getDataset().getLatestVersion().setExternalStatusLabel(label); + break; + } + } + if(!found) { + logger.fine("Label not found: " + label + " in set " + setName); + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.status.failure.notallowed"), this); + } + } + Dataset updatedDataset = save(ctxt); + + return updatedDataset; + } + + public Dataset save(CommandContext ctxt) throws CommandException { + + getDataset().getEditVersion().setLastUpdateTime(getTimestamp()); + getDataset().setModificationTime(getTimestamp()); + + Dataset savedDataset = ctxt.em().merge(getDataset()); + ctxt.em().flush(); + + updateDatasetUser(ctxt); + + AuthenticatedUser requestor = getUser().isAuthenticated() ? (AuthenticatedUser) getUser() : null; + + List authUsers = ctxt.permissions().getUsersWithPermissionOn(Permission.PublishDataset, savedDataset); + for (AuthenticatedUser au : authUsers) { + ctxt.notifications().sendNotification(au, new Timestamp(new Date().getTime()), UserNotification.Type.STATUSUPDATED, savedDataset.getLatestVersion().getId(), "", requestor, false); + } + + // TODO: What should we do with the indexing result? Print it to the log? + return savedDataset; + } + + @Override + public boolean onSuccess(CommandContext ctxt, Object r) { + boolean retVal = true; + Dataset dataset = (Dataset) r; + + try { + Future indexString = ctxt.index().indexDataset(dataset, true); + } catch (IOException | SolrServerException e) { + String failureLogText = "Post submit for review indexing failed. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); + failureLogText += "\r\n" + e.getLocalizedMessage(); + LoggingUtil.writeOnSuccessFailureLog(this, failureLogText, dataset); + retVal = false; + } + return retVal; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index 7fbb953299e..b98e4de6d6c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -785,6 +785,9 @@ private String addOrUpdateDataset(IndexableDataset indexableDataset, Set d if (datasetVersion.isInReview()) { solrInputDocument.addField(SearchFields.PUBLICATION_STATUS, IN_REVIEW_STRING); } + if(datasetVersion.getExternalStatusLabel()!=null) { + solrInputDocument.addField(SearchFields.EXTERNAL_STATUS, datasetVersion.getExternalStatusLabel()); + } Map cvocMap = datasetFieldService.getCVocConf(false); for (DatasetField dsf : datasetVersion.getFlatDatasetFields()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java index a14cac88f1e..422e74f2207 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java @@ -220,6 +220,8 @@ public class SearchFields { * i.e. "Unpublished", "Draft" (multivalued) */ public static final String PUBLICATION_STATUS = "publicationStatus"; + + public static final String EXTERNAL_STATUS = "externalStatus"; /** * @todo reconcile different with Solr schema.xml where type is Long rather * than String. diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java index df17d4a5a86..ddda130739b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java @@ -1269,6 +1269,10 @@ public String dataFileSizeDisplay(DataFile datafile) { } + public boolean canPublishDataset(Long datasetId){ + return permissionsWrapper.canIssuePublishDatasetCommand(dvObjectService.findDvObject(datasetId)); + } + public void setDisplayCardValues() { Set harvestedDatasetIds = null; diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java index 66de72d798d..56e55b59053 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -427,6 +427,10 @@ public SolrQueryResponse search(DataverseRequest dataverseRequest, List matchedFields; + //External Status Label (enabled via AllowedCurationLabels setting) + private String externalStatus; + /** * @todo: remove name? */ @@ -1205,4 +1208,13 @@ public String getNameOfDataverse() { public void setNameOfDataverse(String id) { this.nameOfDataverse = id; } + + public String getExternalStatus() { + return externalStatus; + } + + public void setExternalStatus(String externalStatus) { + this.externalStatus = externalStatus; + + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index f90ed7ac128..7959e6b69b2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -466,7 +466,21 @@ Whether Harvesting (OAI) service is enabled * when a Dataset is created. Messages go to those who have the * ability/permission necessary to publish the dataset */ - SendNotificationOnDatasetCreation + SendNotificationOnDatasetCreation, + /** + * A JSON Object containing named comma separated sets(s) of allowed labels (up + * to 32 characters, spaces allowed) that can be set on draft datasets, via API + * or UI by users with the permission to publish a dataset. (Set names are + * string keys, labels are a JSON array of strings). These should correspond to + * the states in an organizations curation process(es) and are intended to help + * users/curators track the progress of a dataset through an externally defined + * curation process. Only one set of labels are allowed per dataset (defined via + * API by a superuser per collection (UI or API) or per dataset (API only)). A + * dataset may only have one label at a time and if a label is set, it will be + * removed at publication time. This functionality is disabled when this setting + * is empty/not set. + */ + AllowedCurationLabels ; @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java index dec0180ebc3..55c6f4d83d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java @@ -61,6 +61,8 @@ public static String getSubjectTextBasedOnNotification(UserNotification userNoti return BundleUtil.getStringFromBundle("notification.email.workflow.success.subject", rootDvNameAsList); case WORKFLOW_FAILURE: return BundleUtil.getStringFromBundle("notification.email.workflow.failure.subject", rootDvNameAsList); + case STATUSUPDATED: + return BundleUtil.getStringFromBundle("notification.email.status.change.subject", rootDvNameAsList); case CREATEACC: return BundleUtil.getStringFromBundle("notification.email.create.account.subject", rootDvNameAsList); case CHECKSUMFAIL: diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 04025c74d7f..77d11460913 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -9,6 +9,8 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; +import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; + import static edu.harvard.iq.dataverse.datasetutility.FileSizeChecker.bytesToHumanReadable; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.validation.PasswordValidatorUtil; @@ -21,6 +23,7 @@ import java.time.Year; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -29,6 +32,9 @@ import java.util.Properties; import java.util.ResourceBundle; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import javax.ejb.EJB; import javax.ejb.Stateless; import javax.inject.Named; @@ -117,6 +123,9 @@ public class SystemConfig { private static final long DEFAULT_GUESTBOOK_RESPONSES_DISPLAY_LIMIT = 5000L; + public final static String DEFAULTCURATIONLABELSET = "DEFAULT"; + public final static String CURATIONLABELSDISABLED = "DISABLED"; + public String getVersion() { return getVersion(false); } @@ -1111,4 +1120,41 @@ public long getFileValidationSizeLimit() { // -1 means no limit is set; return -1; } + public Map getCurationLabels() { + Map labelMap = new HashMap(); + + try { + JsonReader jsonReader = Json.createReader(new StringReader(settingsService.getValueForKey(SettingsServiceBean.Key.AllowedCurationLabels, ""))); + + Pattern pattern = Pattern.compile("(^[\\w ]+$)"); // alphanumeric, underscore and whitespace allowed + + JsonObject labelSets = jsonReader.readObject(); + for (String key : labelSets.keySet()) { + JsonArray labels = (JsonArray) labelSets.getJsonArray(key); + String[] labelArray = new String[labels.size()]; + + boolean allLabelsOK = true; + Iterator iter = labels.iterator(); + int i=0; + while(iter.hasNext()) { + String label = ((JsonString)iter.next()).getString(); + Matcher matcher = pattern.matcher(label); + if (!matcher.matches()) { + logger.warning("Label rejected: " + label + ", Label set " + key + " ignored."); + allLabelsOK = false; + break; + } + labelArray[i] = label; + i++; + } + if (allLabelsOK) { + labelMap.put(key, labelArray); + } + } + } catch (Exception e) { + logger.warning("Unable to parse " + SettingsServiceBean.Key.AllowedCurationLabels.name() + ": " + e.getLocalizedMessage()); + e.printStackTrace(); + } + return labelMap; + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index a4b53600052..5dff90a209b 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -40,6 +40,7 @@ institution=Institution position=Position affiliation=Affiliation storage=Storage +curationLabels=Curation Labels metadataLanguage=Dataset Metadata Language createDataverse=Create Dataverse remove=Remove @@ -205,6 +206,7 @@ notification.wasPublished={0} was published in {1}. notification.publishFailedPidReg={0} in {1} could not be published due to a failure to register, or update the Global Identifier for the dataset or one of the files in it. Contact support if this continues to happen. notification.workflowFailed=An external workflow run on {0} in {1} has failed. Check your email and/or view the Dataset page which may have additional details. Contact support if this continues to happen. notification.workflowSucceeded=An external workflow run on {0} in {1} has succeeded. Check your email and/or view the Dataset page which may have additional details. +notification.statusUpdated=The status of dataset {0} has been updated to {1}. notification.ingestCompleted=Dataset {1} ingest has successfully finished. notification.ingestCompletedWithErrors=Dataset {1} ingest has finished with errors. @@ -684,6 +686,8 @@ notification.email.workflow.success.subject={0}: Your dataset has been processed notification.email.workflow.success=A workflow running on {0} (view at {1}) succeeded: {2} notification.email.workflow.failure.subject={0}: Failed to process your dataset notification.email.workflow.failure=A workflow running on {0} (view at {1}) failed: {2} +notification.email.status.change.subject={0}: Dataset Status Change +notification.email.status.change=The Status of the dataset ({0}) has changed to {1} notification.email.workflow.nullMessage=No additional message sent from the workflow. notification.email.create.account.subject={0}: Your account has been created notification.email.assign.role.subject={0}: You have been assigned a role @@ -739,6 +743,8 @@ dataverse.identifier.title=Short name used for the URL of this dataverse. dataverse.affiliation.title=The organization with which this dataverse is affiliated. dataverse.storage.title=A storage service to be used for datasets in this dataverse. dataverse.metadatalanguage.title=Metadata entered for datasets in this dataverse will be assumed to be in the selected language. +dataverse.curationLabels.title=A set of curation status labels that are used to indicate the curation status of draft datasets. +dataverse.curationLabels.disabled=Disabled dataverse.category=Category dataverse.category.title=The type that most closely reflects this dataverse. dataverse.type.selectTab.top=Select one... @@ -1284,6 +1290,8 @@ dataset.shareBtn=Share dataset.publishBtn=Publish Dataset dataset.editBtn=Edit Dataset +dataset.changestatus=Change Curation Status +dataset.removestatus=Remove Current Status dataset.editBtn.itemLabel.upload=Files (Upload) dataset.editBtn.itemLabel.metadata=Metadata dataset.editBtn.itemLabel.terms=Terms @@ -1335,6 +1343,9 @@ dataset.submit.failure=Dataset Submission Failed - {0} dataset.submit.failure.null=Can't submit for review. Dataset is null. dataset.submit.failure.isReleased=Latest version of dataset is already released. Only draft versions can be submitted for review. dataset.submit.failure.inReview=You cannot submit this dataset for review because it is already in review. +dataset.status.failure.notallowed=Status update failed - label not allowed +dataset.status.failure.disabled=Status labeling disabled for this dataset +dataset.status.failure.isReleased=Latest version of dataset is already released. Status can only be set on draft versions dataset.rejectMessage=Return this dataset to contributor for modification. dataset.rejectMessage.label=Return to Author Reason dataset.rejectWatermark=Please enter a reason for returning this dataset to its author(s). @@ -1522,6 +1533,10 @@ dataset.privateurl.full=This Private URL provides full read access to the datase dataset.privateurl.anonymized=This Private URL provides access to the anonymized dataset dataset.privateurl.disabledSuccess=You have successfully disabled the Private URL for this unpublished dataset. dataset.privateurl.noPermToCreate=To create a Private URL you must have the following permissions: {0}. +dataset.externalstatus.header=Curation Status Changed +dataset.externalstatus.removed=Curation Status Removed +dataset.externalstatus.info=Curation Status is now "{0}" +dataset.externalstatus.cantchange=Unable to change Curation Status. Please contact the administrator. file.display.label=Change View file.display.table=Table file.display.tree=Tree @@ -2392,6 +2407,9 @@ datasets.api.privateurl.error.datasetnotfound=Could not find dataset. datasets.api.privateurl.error.alreadyexists=Private URL already exists for this dataset. datasets.api.privateurl.error.notdraft=Can't create Private URL because the latest version of this dataset is not a draft. datasets.api.privateurl.anonymized.error.released=Can't create a URL for anonymized access because this dataset has been published. +datasets.api.creationdate=Date Created +datasets.api.modificationdate=Last Modified Date +datasets.api.curationstatus=Curation Status #Dataverses.java diff --git a/src/main/resources/db/migration/V5.6.0.1__7967_external-curation-labels.sql b/src/main/resources/db/migration/V5.6.0.1__7967_external-curation-labels.sql new file mode 100644 index 00000000000..0f2c799482a --- /dev/null +++ b/src/main/resources/db/migration/V5.6.0.1__7967_external-curation-labels.sql @@ -0,0 +1,4 @@ +ALTER TABLE datasetversion ADD COLUMN IF NOT EXISTS externalstatuslabel varchar(32); +ALTER TABLE dataverse ADD COLUMN IF NOT EXISTS externallabelsetname varchar(32); +ALTER TABLE dataset ADD COLUMN IF NOT EXISTS externallabelsetname varchar(32); + diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 475702bb096..dffe8256eb8 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -114,6 +114,7 @@ + @@ -284,17 +285,17 @@
- + - + - #{bundle['dataset.publishBtn']} + #{showPublishLink ? bundle['dataset.publishBtn'] : (DatasetPage.dataset.latestVersion.inReview ? bundle['dataset.disabledSubmittedBtn'] : bundle['dataset.submitBtn'])}
diff --git a/src/main/webapp/dataverse.xhtml b/src/main/webapp/dataverse.xhtml index c14e872278d..cc1a1e8ed59 100644 --- a/src/main/webapp/dataverse.xhtml +++ b/src/main/webapp/dataverse.xhtml @@ -160,6 +160,19 @@ +
+ + #{bundle.curationLabels} + + +
+ + + + +
+
diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index 0bff64289bb..a5a38579c44 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -348,6 +348,16 @@ + + + + + #{item.theObject.getDataset().getDisplayName()} + + + + + diff --git a/src/main/webapp/file.xhtml b/src/main/webapp/file.xhtml index 7086217606a..8669f683c78 100644 --- a/src/main/webapp/file.xhtml +++ b/src/main/webapp/file.xhtml @@ -68,6 +68,7 @@ + diff --git a/src/main/webapp/search-include-fragment.xhtml b/src/main/webapp/search-include-fragment.xhtml index 1d3aeefbf4b..1c04c238c78 100644 --- a/src/main/webapp/search-include-fragment.xhtml +++ b/src/main/webapp/search-include-fragment.xhtml @@ -545,6 +545,7 @@ +
diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index c78cb4ceb85..b86b10af2a3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7,6 +7,7 @@ import java.util.logging.Logger; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; import org.skyscreamer.jsonassert.JSONAssert; import org.junit.Ignore; import com.jayway.restassured.path.json.JsonPath; @@ -22,6 +23,8 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.DataverseServiceBean; + import static edu.harvard.iq.dataverse.api.UtilIT.API_TOKEN_HTTP_HEADER; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; @@ -2299,7 +2302,7 @@ public void testSemanticMetadataAPIs() { assertEquals(200, deleteUserResponse.getStatusCode()); } - + @Test public void testReCreateDataset() { @@ -2368,6 +2371,59 @@ public void testReCreateDataset() { assertEquals(200, deleteUserResponse.getStatusCode()); } + @Test + public void testCurationLabelAPIs() { + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + SystemConfig systemConfig = Mockito.mock(SystemConfig.class); + Map labelSets = new HashMap(); + labelSets.put("StandardProcess", new String[] { "Author contacted", "Privacy Review", "Awaiting paper publication", "Final Approval"}); + labelSets.put("AlternateProcess", new String[] {"State 1","State 2","State 3"}); + Mockito.when(systemConfig.getCurationLabels()).thenReturn(labelSets); + + //Set curation label set on dataverse + //Valid option, bad user + Response setDataverseCurationLabelSetResponse = UtilIT.setDataverseCurationLabelSet(dataverseAlias, apiToken, "AlternateProcess"); + setDataverseCurationLabelSetResponse.then().assertThat().statusCode(FORBIDDEN.getStatusCode()); + + Response makeSuperUser = UtilIT.makeSuperUser(username); + assertEquals(200, makeSuperUser.getStatusCode()); + + //Non-existent option + setDataverseCurationLabelSetResponse = UtilIT.setDataverseCurationLabelSet(dataverseAlias, apiToken, "OddProcess"); + setDataverseCurationLabelSetResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + //Valid option, superuser + setDataverseCurationLabelSetResponse = UtilIT.setDataverseCurationLabelSet(dataverseAlias, apiToken, "AlternateProcess"); + setDataverseCurationLabelSetResponse.then().assertThat().statusCode(OK.getStatusCode()); + + + // Create a dataset using native api + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); + // Get the curation label set in use + Response response = UtilIT.getDatasetCurationLabelSet(datasetId, apiToken); + response.then().assertThat().statusCode(OK.getStatusCode()); + //Verify that the set name is what was set on the dataverse + String labelSetName = getData(response.getBody().asString()); + assertEquals("AlternateProcess", labelSetName); + + // Now set a label + //Option from the wrong set + response = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "Author contacted"); + response.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + // Valid option + response = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "State 1"); + response.then().assertThat().statusCode(OK.getStatusCode()); + } + private String getData(String body) { try (StringReader rdr = new StringReader(body)) { return Json.createReader(rdr).readObject().getJsonObject("data").toString(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 4070f153a8e..aab7ea76bfb 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -2672,6 +2672,27 @@ public static Response recreateDatasetJsonLD(String apiToken, String dataverseAl .post("/api/dataverses/" + dataverseAlias +"/datasets"); return response; } + + static Response setDataverseCurationLabelSet(String alias, String apiToken, String labelSetName) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .put("/api/admin/dataverse/" + alias + "/curationLabelSet?name=" + labelSetName); + return response; + } + + static Response getDatasetCurationLabelSet(Integer datasetId, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/" + datasetId + "/curationLabelSet"); + return response; + } + + static Response setDatasetCurationLabel(Integer datasetId, String apiToken, String label) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .put("/api/datasets/" + datasetId + "/curationLabelSet?label=" + label); + return response; + } private static DatasetField constructPrimitive(String fieldName, String value) { DatasetField field = new DatasetField();