diff --git a/conf/solr/schema.xml b/conf/solr/schema.xml index 50835957b04..5517175d443 100644 --- a/conf/solr/schema.xml +++ b/conf/solr/schema.xml @@ -157,7 +157,8 @@ - + + diff --git a/doc/release-notes/9247-CurationStatusUpdates.md b/doc/release-notes/9247-CurationStatusUpdates.md new file mode 100644 index 00000000000..0af705bb7f3 --- /dev/null +++ b/doc/release-notes/9247-CurationStatusUpdates.md @@ -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. \ No newline at end of file diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 9bcf0cbfaa6..47901aec637 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -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 ---- diff --git a/doc/sphinx-guides/source/api/curation-labels.rst b/doc/sphinx-guides/source/api/curation-labels.rst index 0675eeec398..50ac05b670f 100644 --- a/doc/sphinx-guides/source/api/curation-labels.rst +++ b/doc/sphinx-guides/source/api/curation-labels.rst @@ -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 @@ -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 ------------------------------------ @@ -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 --------------------------------------- @@ -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. @@ -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 '' 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. + diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 6a3c64291af..0925cdb5c90 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -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 diff --git a/src/main/java/edu/harvard/iq/dataverse/CurationStatus.java b/src/main/java/edu/harvard/iq/dataverse/CurationStatus.java new file mode 100644 index 00000000000..b5a113a03d6 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/CurationStatus.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 9f0441062c2..e44bbfe103c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -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; @@ -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! @@ -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 getAllowedExternalStatuses() { - return settingsWrapper.getAllowedExternalStatuses(dataset); + public List getAllowedCurationStatuses() { + return settingsWrapper.getAllowedCurationStatuses(dataset); } public Embargo getSelectionEmbargo() { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index 7ffb797652d..abc38db5c47 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -216,9 +216,10 @@ public enum VersionState { @OneToMany(mappedBy = "datasetVersion", cascade={CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST}) private List workflowComments; - @Column(nullable=true) - private String externalStatusLabel; - + @OneToMany(mappedBy = "datasetVersion", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("createTime DESC NULLS LAST") + private List curationStatuses = new ArrayList<>(); + @Transient private DatasetVersionDifference dvd; @@ -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 getCurationStatuses() { + return curationStatuses; + } + + protected void setCurationStatuses(List 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() { diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 4eb9828253d..4d2949c83c6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -1241,7 +1241,7 @@ public Set> 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); }); } diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 6ad934b0640..72dde0d7d0e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -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; diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index b10ccf1ae82..569bb82ff80 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -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) ? "" : 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; diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java index 5ce9edb3a9e..2c6f8ff2fb1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionsWrapper.java @@ -251,7 +251,7 @@ public boolean canIssueDeleteDatasetCommand(DvObject dvo){ return canIssueCommand(dvo, DeleteDatasetCommand.class); } - // PLUBLISH DATASET + // PUBLISH DATASET public boolean canIssuePublishDatasetCommand(DvObject dvo){ return canIssueCommand(dvo, PublishDatasetCommand.class); } diff --git a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java index 4d04d9d26a5..3ff27699379 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java @@ -836,7 +836,7 @@ private Boolean getUploadMethodAvailable(String method){ List allowedExternalStatuses = null; - public List getAllowedExternalStatuses(Dataset d) { + public List getAllowedCurationStatuses(Dataset d) { String setName = d.getEffectiveCurationLabelSetName(); if(setName.equals(SystemConfig.CURATIONLABELSDISABLED)) { return new ArrayList(); 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 fd9e614eda4..3f099ef0612 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -67,6 +67,7 @@ import jakarta.ws.rs.core.*; import jakarta.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; @@ -2560,13 +2561,38 @@ public Response returnToAuthor(@Context ContainerRequestContext crc, @PathParam( @GET @AuthRequired @Path("{id}/curationStatus") - public Response getCurationStatus(@Context ContainerRequestContext crc, @PathParam("id") String idSupplied) { + @Produces(MediaType.APPLICATION_JSON) + public Response getCurationStatus(@Context ContainerRequestContext crc, + @PathParam("id") String idSupplied, + @QueryParam("includeHistory") boolean includeHistory) { try { Dataset ds = findDatasetOrDie(idSupplied); DatasetVersion dsv = ds.getLatestVersion(); User user = getRequestUser(crc); - if (dsv.isDraft() && permissionSvc.requestOn(createDataverseRequest(user), ds).has(Permission.PublishDataset)) { - return response(req -> ok(dsv.getExternalStatusLabel()==null ? "":dsv.getExternalStatusLabel()), user); + + boolean canSeeStatus = false; + // Check if curation labels should be shown to all users + boolean showCurationLabelsToAll = JvmSettings.UI_SHOW_CURATION_STATUS_TO_ALL.lookupOptional(Boolean.class).orElse(false); + // If so, see if this user + if (showCurationLabelsToAll) { + // See if user can view the draft version + canSeeStatus = permissionSvc.requestOn(createDataverseRequest(user), ds).has(Permission.ViewUnpublishedDataset); + } else { + // Check if the user can publish the dataset + canSeeStatus = permissionSvc.requestOn(createDataverseRequest(user), ds).has(Permission.PublishDataset); + } + + if (dsv.isDraft() && (canSeeStatus)) { + List statuses = includeHistory ? dsv.getCurationStatuses() : Collections.singletonList(dsv.getCurrentCurationStatus()); + if (includeHistory) { + JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + for (CurationStatus status : statuses) { + arrayBuilder.add(curationStatusToJson(status)); + } + return ok(arrayBuilder); + } else { + return ok(curationStatusToJson(statuses.get(0))); + } } else { return error(Response.Status.FORBIDDEN, "You are not permitted to view the curation status of this dataset."); } @@ -2575,6 +2601,17 @@ public Response getCurationStatus(@Context ContainerRequestContext crc, @PathPar } } + private JsonObject curationStatusToJson(CurationStatus status) { + if (status == null) { + return Json.createObjectBuilder().build(); + } + return NullSafeJsonBuilder.jsonObjectBuilder() + .add("label", status.getLabel()) + .add("createTime", status.getCreateTime().toString()) + .add("assigner", status.getAuthenticatedUser().getIdentifier()) + .build(); + } + @PUT @AuthRequired @Path("{id}/curationStatus") @@ -4861,16 +4898,29 @@ public Response updateMultipleFileMetadata(@Context ContainerRequestContext crc, } } /** - * API to find curation assignments and statuses + * API to retrieve curation assignments and statuses for datasets. * - * @return - * @throws WrappedResponse + * @param crc The ContainerRequestContext for authentication. + * @param includeHistory A boolean flag to determine whether to include the full history of curation statuses. + * If true, all historical statuses will be included. If false (default), only the latest status for each dataset is returned. + * @return A Response object containing a CSV formatted string with curation assignments and statuses. + * The CSV includes the following columns in order: + * 1. Dataset Title (as a hyperlink to the dataset page) + * 2. Creation Date of the draft dataset version + * 3. Latest Modification Date of the draft dataset version + * 4. Assigned curation status or '' if no curation status is assigned but was previously, null if no curation state has every been set. + * 5. Time when the curation status was applied to the draft dataset version + * 6. The user who assigned this curation status + * 7. Users (comma separated list) 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. + * @throws WrappedResponse If there's an error in authentication or data retrieval. */ @GET @AuthRequired @Path("/listCurationStates") @Produces("text/csv") - public Response getCurationStates(@Context ContainerRequestContext crc) throws WrappedResponse { + public Response getCurationStates(@Context ContainerRequestContext crc, + @QueryParam("includeHistory") @DefaultValue("false") boolean includeHistory) throws WrappedResponse { try { AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); @@ -4897,6 +4947,8 @@ public Response getCurationStates(@Context ContainerRequestContext crc) throws W BundleUtil.getStringFromBundle("datasets.api.creationdate"), BundleUtil.getStringFromBundle("datasets.api.modificationdate"), BundleUtil.getStringFromBundle("datasets.api.curationstatus"), + BundleUtil.getStringFromBundle("datasets.api.curationstatuscreatetime"), + BundleUtil.getStringFromBundle("datasets.api.curationstatussetter"), String.join(",", assignees.keySet()))); for (Dataset dataset : datasetSvc.findAllWithDraftVersion()) { List ras = permissionService.assignmentsOn(dataset); @@ -4910,14 +4962,33 @@ public Response getCurationStates(@Context ContainerRequestContext crc) throws W } DatasetVersion dsv = dataset.getLatestVersion(); String name = "\"" + dataset.getCurrentName().replace("\"", "\"\"") + "\""; - String status = dsv.getExternalStatusLabel(); - String url = systemConfig.getDataverseSiteUrl() + dataset.getTargetUrl() + dataset.getGlobalId().asString(); - String date = new SimpleDateFormat("yyyy-MM-dd").format(dsv.getCreateTime()); - String modDate = new SimpleDateFormat("yyyy-MM-dd").format(dsv.getLastUpdateTime()); - 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))); + + List statuses = includeHistory ? dsv.getCurationStatuses() : Collections.singletonList(dsv.getCurrentCurationStatus()); + + for (CurationStatus status : statuses) { + String label = BundleUtil.getStringFromBundle("dataset.curationstatus.none"); + String statusCreator = BundleUtil.getStringFromBundle("dataset.curationstatus.none"); + String createTime = BundleUtil.getStringFromBundle("dataset.curationstatus.none"); + + if (status != null) { + if (Strings.isNotBlank(status.getLabel())) { + label = status.getLabel(); + } + if (status.getAuthenticatedUser() != null) { + statusCreator = status.getAuthenticatedUser().getUserIdentifier(); + } + if (status.getCreateTime() != null) { + createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(status.getCreateTime()); + } + } + String url = systemConfig.getDataverseSiteUrl() + dataset.getTargetUrl() + dataset.getGlobalId().asString(); + String date = new SimpleDateFormat("yyyy-MM-dd").format(dsv.getCreateTime()); + String modDate = new SimpleDateFormat("yyyy-MM-dd").format(dsv.getLastUpdateTime()); + 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) ? "" : label, statusCreator, createTime, 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/dataset/DatasetUtil.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java index 012ba464ecc..46f458c5403 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java @@ -38,6 +38,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.EnumUtils; +import org.apache.logging.log4j.util.Strings; public class DatasetUtil { @@ -716,17 +717,25 @@ public static String getLocalizedLicenseDetails(License license,String keyPart) return localizedLicenseValue; } - public static String getLocaleExternalStatus(String status) { - String localizedName = "" ; - try { - localizedName = BundleUtil.getStringFromPropertyFile(status.toLowerCase().replace(" ", "_"), "CurationLabels"); + public static String getLocaleCurationStatusLabel(CurationStatus status) { + String label = (status != null && Strings.isNotBlank(status.getLabel())) ? status.getLabel() : null; + return getLocaleCurationStatusLabelFromString(label); + } + + public static String getLocaleCurationStatusLabelFromString(String label) { + + if (label == null) { + return null; } - catch (Exception e) { - localizedName = status; + String localizedName = ""; + try { + localizedName = BundleUtil.getStringFromPropertyFile(label.toLowerCase().replace(" ", "_"), "CurationLabels"); + } catch (Exception e) { + localizedName = label; } if (localizedName == null) { - localizedName = status ; + localizedName = label; } return localizedName; } 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 9bfdd958189..4ba8d39a949 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 @@ -1,11 +1,14 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.ControlledVocabularyValue; +import edu.harvard.iq.dataverse.CurationStatus; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetField; import edu.harvard.iq.dataverse.DatasetFieldConstant; import edu.harvard.iq.dataverse.DatasetLock; +import edu.harvard.iq.dataverse.DatasetVersion; + import static edu.harvard.iq.dataverse.DatasetVersion.VersionState.*; import edu.harvard.iq.dataverse.DatasetVersionUser; import edu.harvard.iq.dataverse.Dataverse; @@ -26,6 +29,8 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.workflow.WorkflowContext.TriggerType; + +import java.awt.datatransfer.StringSelection; import java.io.IOException; import java.sql.Timestamp; import java.util.Date; @@ -39,6 +44,8 @@ import edu.harvard.iq.dataverse.util.FileUtil; import java.util.ArrayList; import java.util.concurrent.Future; + +import org.apache.logging.log4j.util.Strings; import org.apache.solr.client.solrj.SolrServerException; @@ -151,20 +158,24 @@ public Dataset execute(CommandContext ctxt) throws CommandException { theDataset.setEmbargoCitationDate(latestEmbargoDate); } - //Clear any external status - theDataset.getLatestVersion().setExternalStatusLabel(null); + DatasetVersion version = theDataset.getLatestVersion(); + // Clear any external status + CurationStatus status = version.getCurrentCurationStatus(); + if (status != null && Strings.isNotBlank(status.getLabel())) { + version.addCurationStatus(new CurationStatus(null, version, getRequest().getAuthenticatedUser())); + } // update metadata - if (theDataset.getLatestVersion().getReleaseTime() == null) { + if (version.getReleaseTime() == null) { // Allow migrated versions to keep original release dates - theDataset.getLatestVersion().setReleaseTime(getTimestamp()); + version.setReleaseTime(getTimestamp()); } - theDataset.getLatestVersion().setLastUpdateTime(getTimestamp()); + version.setLastUpdateTime(getTimestamp()); theDataset.setModificationTime(getTimestamp()); theDataset.setFileAccessRequest(theDataset.getLatestVersion().getTermsOfUseAndAccess().isFileAccessRequest()); //Use dataset pub date (which may not be the current date for migrated datasets) - updateFiles(new Timestamp(theDataset.getLatestVersion().getReleaseTime().getTime()), ctxt); + updateFiles(new Timestamp(version.getReleaseTime().getTime()), ctxt); // // TODO: Not sure if this .merge() is necessary here - ? 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 index 557f9dff622..53901ef5128 100644 --- 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 @@ -1,7 +1,9 @@ package edu.harvard.iq.dataverse.engine.command.impl; +import edu.harvard.iq.dataverse.CurationStatus; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLock; +import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetVersionUser; import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.authorization.Permission; @@ -13,6 +15,7 @@ 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.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -27,6 +30,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.logging.log4j.util.Strings; import org.apache.solr.client.solrj.SolrServerException; import com.google.api.LabelDescriptor; @@ -35,43 +39,57 @@ 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; + this.label = label; } @Override public Dataset execute(CommandContext ctxt) throws CommandException { + DatasetVersion version = getDataset().getLatestVersion(); + if (version.isReleased()) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.curationstatus.failure.isReleased"), this); + } + CurationStatus currentStatus = version.getCurrentCurationStatus(); - if (getDataset().getLatestVersion().isReleased()) { - throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.status.failure.isReleased"), this); + CurationStatus status = null; + if (((currentStatus == null || Strings.isBlank(currentStatus.getLabel())) && Strings.isNotBlank(label)) || + (currentStatus != null && !currentStatus.getLabel().equals(label))) { + status = new CurationStatus(label, version, getRequest().getAuthenticatedUser()); } - 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); + + String setName = getDataset().getEffectiveCurationLabelSetName(); + if (setName.equals(SystemConfig.CURATIONLABELSDISABLED)) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.curationstatus.failure.disabled"), this); + } + if (status != null) { boolean found = false; - for(String name: labelArray) { - if(name.equals(label)) { - found=true; - getDataset().getLatestVersion().setExternalStatusLabel(label); - break; + if (status.getLabel() != null) { + String[] labelArray = ctxt.systemConfig().getCurationLabels().get(setName); + for (String name : labelArray) { + if (name.equals(label)) { + found = true; + version.addCurationStatus(status); + break; + } } + } else { + // + found = true; + version.addCurationStatus(status); } - if(!found) { + if (!found) { logger.fine("Label not found: " + label + " in set " + setName); - throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.status.failure.notallowed"), this); + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.curationstatus.failure.notallowed"), this); } + } else { + logger.fine("Attempt to reset with the same label : " + label); + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.curationstatus.failure.noChange"), this); } Dataset updatedDataset = save(ctxt); - return updatedDataset; } @@ -86,16 +104,23 @@ public Dataset save(CommandContext ctxt) throws CommandException { updateDatasetUser(ctxt); AuthenticatedUser requestor = getUser().isAuthenticated() ? (AuthenticatedUser) getUser() : null; - - List authUsers = ctxt.permissions().getUsersWithPermissionOn(Permission.PublishDataset, savedDataset); + + boolean showToAll = JvmSettings.UI_SHOW_CURATION_STATUS_TO_ALL.lookupOptional(Boolean.class).orElse(false); + + List authUsers; + if (showToAll) { + authUsers = ctxt.permissions().getUsersWithPermissionOn(Permission.ViewUnpublishedDataset, savedDataset); + } else { + 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? + + // 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; 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 d53f4ddd872..22fdb841b28 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.search; import edu.harvard.iq.dataverse.ControlledVocabularyValue; +import edu.harvard.iq.dataverse.CurationStatus; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.DataFileTag; @@ -54,9 +55,11 @@ import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.LocalDate; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; + import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -93,6 +96,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery.SortClause; import org.apache.solr.client.solrj.SolrServerException; @@ -230,6 +234,7 @@ public Future indexDataverse(Dataverse dataverse, boolean processPaths) solrInputDocument.addField(SearchFields.PUBLICATION_STATUS, UNPUBLISHED_STRING); solrInputDocument.addField(SearchFields.RELEASE_OR_CREATE_DATE, dataverse.getCreateDate()); } + /* We don't really have harvested dataverses yet; (I have in fact just removed the isHarvested() method from the Dataverse object) -- L.A. if (dataverse.isHarvested()) { @@ -1050,9 +1055,20 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set langs = settingsService.getConfiguredLanguages(); Map cvocMap = datasetFieldService.getCVocConf(true); 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 109c17d6ef9..67ada72da9a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java @@ -233,7 +233,8 @@ public class SearchFields { */ public static final String PUBLICATION_STATUS = "publicationStatus"; - public static final String EXTERNAL_STATUS = "externalStatus"; + public static final String CURATION_STATUS = "curationStatus"; + public static final String CURATION_STATUS_CREATE_TIME = "curationStatusCreateTime"; /** * @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 9328dd03ca2..a34bf6fd4d7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java @@ -23,6 +23,7 @@ import edu.harvard.iq.dataverse.WidgetWrapper; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import java.time.LocalDate; @@ -1255,7 +1256,7 @@ public List getFriendlyNamesFromFilterQuery(String filterQuery) { if (datasetfieldFriendyName != null) { friendlyNames.add(datasetfieldFriendyName); } else { - // Get non dataset field friendly name from "staticSearchFields" ressource bundle file + // Get non dataset field friendly name from "staticSearchFields" resource bundle file String nonDatasetSolrField = staticSolrFieldFriendlyNamesBySolrField.get(key); if (nonDatasetSolrField != null) { friendlyNames.add(nonDatasetSolrField); @@ -1552,6 +1553,15 @@ public boolean isValid(SolrSearchResult result) { }); } + public boolean canSeeCurationStatus(Long datasetId) { + boolean creatorsCanSeeStatus = JvmSettings.UI_SHOW_CURATION_STATUS_TO_ALL.lookupOptional(Boolean.class).orElse(false); + if (creatorsCanSeeStatus) { + return permissionsWrapper.canViewUnpublishedDataset(getDataverseRequest(),(Dataset) dvObjectService.findDvObject(datasetId)); + } else { + return canPublishDataset(datasetId); + } + } + public enum SortOrder { asc, desc 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 08ddbab202a..d777007d29f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -240,6 +240,7 @@ public SolrQueryResponse search( */ solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); solrQuery.addFacetField(SearchFields.DATASET_LICENSE); + solrQuery.addFacetField(SearchFields.CURATION_STATUS); /** * @todo when a new method on datasetFieldService is available * (retrieveFacetsByDataverse?) only show the facets that the @@ -555,9 +556,9 @@ public SolrQueryResponse search( // this method also sets booleans for individual statuses solrSearchResult.setPublicationStatuses(states); } - String externalStatus = (String) solrDocument.getFieldValue(SearchFields.EXTERNAL_STATUS); + String externalStatus = (String) solrDocument.getFieldValue(SearchFields.CURATION_STATUS); if (externalStatus != null) { - solrSearchResult.setExternalStatus(externalStatus); + solrSearchResult.setCurationStatus(externalStatus); } // logger.info(id + ": " + description); solrSearchResult.setId(id); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java b/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java index c34743532bd..e5e89e042a0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchResult.java @@ -277,7 +277,7 @@ public void setDeaccessionedState(boolean deaccessionedState) { private List matchedFields; // External Status Label (enabled via AllowedCurationLabels setting) - private String externalStatus; + private String curationStatus; /** * @todo: remove name? @@ -1350,12 +1350,12 @@ public void setNameOfDataverse(String id) { this.nameOfDataverse = id; } - public String getExternalStatus() { - return externalStatus; + public String getCurationStatus() { + return curationStatus; } - public void setExternalStatus(String externalStatus) { - this.externalStatus = externalStatus; + public void setCurationStatus(String curationStatus) { + this.curationStatus = curationStatus; } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index 2725c7afb6b..1559fcab833 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -253,6 +253,7 @@ public enum JvmSettings { UI_ALLOW_REVIEW_INCOMPLETE(SCOPE_UI, "allow-review-for-incomplete"), UI_SHOW_VALIDITY_FILTER(SCOPE_UI, "show-validity-filter"), UI_SHOW_VALIDITY_LABEL_WHEN_PUBLISHED(SCOPE_UI, "show-validity-label-when-published"), + UI_SHOW_CURATION_STATUS_TO_ALL(SCOPE_UI, "show-curation-status-to-all"), // NetCDF SETTINGS SCOPE_NETCDF(PREFIX, "netcdf"), diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 9b87f112bb1..05421179d7e 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -827,7 +827,7 @@ notification.email.workflow.success=A workflow running on {0} (view at {1} ) suc notification.email.workflow.failure.subject={0}: Failed to process your dataset "{1}" notification.email.workflow.failure=A workflow running on {0} (view at {1} ) failed: {2} notification.email.status.change.subject={0}: Dataset "{1}" Status Change -notification.email.status.change=The Status of the dataset ({0}) has changed to {1} +notification.email.status.change=The curation status of the dataset named {0} (view at {1} ) in collection {2} ( view at {3} ) has been updated to "{4}". notification.email.pid.reconciled.subject={0}: Dataset "{1}" persistent identifier change notification.email.pid.reconciled=The persistent identifier of dataset {0} has been updated to `{1}`. notification.email.workflow.nullMessage=No additional message sent from the workflow. @@ -1569,9 +1569,15 @@ 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.curationstatus.failure.notallowed=Status update failed - label not allowed +dataset.curationstatus.failure.noChange=Status update failed - the dataset already has this status +dataset.curationstatus.failure.disabled=Status labeling disabled for this dataset +dataset.curationstatus.failure.isReleased=Latest version of dataset is already released. Status can only be set on draft versions +dataset.curationstatus.header=Curation Status Changed +dataset.curationstatus.removed=Curation Status Removed +dataset.curationstatus.info=Curation Status is now "{0}" +dataset.curationstatus.none= +dataset.curationstatus.cantchange=Unable to change Curation Status. Please contact the administrator. dataset.rejectMessage=Return this dataset to contributor for modification. dataset.rejectMessageReason=The reason for return entered below will be sent by email to the author. dataset.rejectMessage.label=Return to Author Reason @@ -1819,10 +1825,6 @@ dataset.privateurl.full=This Preview URL provides full read access to the datase dataset.privateurl.anonymized=This Preview URL provides access to the anonymized dataset dataset.privateurl.disabledSuccess=You have successfully disabled the Preview URL for this unpublished dataset. dataset.privateurl.noPermToCreate=To create a Preview 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 @@ -2674,6 +2676,12 @@ datasetversion.update.archive.failure=Dataset Version Update succeeded, but the datasetversion.update.success=The published version of your Dataset has been updated. datasetversion.update.archive.success=The published version of your Dataset, and its archival copy, have been updated. dataset.license.custom.blankterms=When selecting Custom Dataset Terms, you must provide some Terms of Use. +dataset.curationStatusMenu=Curation Status +dataset.viewCurationStatusHistory=View Curation Status History +dataset.curationStatusHistory=Curation Status History +dataset.curationStatus=Status +dataset.curationDate=Date +dataset.curationAssigner=Assigner #ThemeWidgetFragment.java theme.validateTagline=Tagline must be at most 140 characters. @@ -2820,6 +2828,8 @@ datasets.api.privateurl.anonymized.error.released=Can't create a URL for anonymi datasets.api.creationdate=Date Created datasets.api.modificationdate=Last Modified Date datasets.api.curationstatus=Curation Status +datasets.api.curationstatuscreatetime=Status Set Time +datasets.api.curationstatussetter=Status Set By datasets.api.version.files.invalid.order.criteria=Invalid order criteria: {0} datasets.api.version.files.invalid.access.status=Invalid access status: {0} datasets.api.deaccessionDataset.invalid.version.identifier.error=Only {0} or a specific version can be deaccessioned diff --git a/src/main/java/propertyFiles/staticSearchFields.properties b/src/main/java/propertyFiles/staticSearchFields.properties index 9a208e841d6..5e6f1144ac1 100644 --- a/src/main/java/propertyFiles/staticSearchFields.properties +++ b/src/main/java/propertyFiles/staticSearchFields.properties @@ -11,3 +11,4 @@ staticSearchFields.fileAccess=Access staticSearchFields.publicationStatus=Publication Status staticSearchFields.subject_ss=Subject staticSearchFields.datasetType=Dataset Type +staticSearchFields.curationStatus=Curation Status diff --git a/src/main/resources/db/migration/V6.6.0.3.sql b/src/main/resources/db/migration/V6.6.0.3.sql new file mode 100644 index 00000000000..24d360aa467 --- /dev/null +++ b/src/main/resources/db/migration/V6.6.0.3.sql @@ -0,0 +1,32 @@ +-- Create the curationstatus table if it doesn't exist +CREATE TABLE IF NOT EXISTS curationstatus ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + label VARCHAR(255), + datasetversion_id BIGINT NOT NULL, + authenticateduser_id BIGINT NULL, + createtime TIMESTAMP, + CONSTRAINT fk_curationstatus_datasetversion FOREIGN KEY (datasetversion_id) REFERENCES datasetversion(id), + CONSTRAINT fk_curationstatus_authenticateduser FOREIGN KEY (authenticateduser_id) REFERENCES authenticateduser(id) +); + +CREATE INDEX IF NOT EXISTS index_curationstatus_datasetversion ON curationstatus (datasetversion_id); + +-- Migrate existing data from datasetversion.externalstatuslabel to curationstatus if it hasn't been done already +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'datasetversion' AND column_name = 'externalstatuslabel') THEN + INSERT INTO curationstatus (label, datasetversion_id, authenticateduser_id, createtime) + SELECT DISTINCT ON (externalstatuslabel, id) externalstatuslabel, id, NULL, NULL + FROM datasetversion + WHERE externalstatuslabel IS NOT NULL AND externalstatuslabel != '' + AND NOT EXISTS ( + SELECT 1 FROM curationstatus + WHERE curationstatus.label = datasetversion.externalstatuslabel + AND curationstatus.datasetversion_id = datasetversion.id + ); + + -- Drop the externalstatuslabel column from datasetversion + ALTER TABLE datasetversion DROP COLUMN IF EXISTS externalstatuslabel; + END IF; +END +$$; diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index f32e7933430..5c9585ef450 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -149,7 +149,7 @@ - + @@ -336,17 +336,17 @@
- + - + - #{showPublishLink ? bundle['dataset.publishBtn'] : (DatasetPage.dataset.latestVersion.inReview ? bundle['dataset.disabledSubmittedBtn'] : bundle['dataset.submitBtn'])} + #{showPublishLink ? bundle['dataset.publishBtn'] : (DatasetPage.dataset.latestVersion.inReview ? bundle['dataset.disabledSubmittedBtn'] : bundle['dataset.submitBtn'])}
@@ -515,7 +522,45 @@ - + +
+
+ + +
+
+
+ + + + + + + + + + + + + + +

diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index dd9fe6e2460..6c7826783d7 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -450,7 +450,7 @@ #{item.theObject.getDataset().getDisplayName()} - + diff --git a/src/main/webapp/file.xhtml b/src/main/webapp/file.xhtml index 633b71702f6..8a5c2709e5d 100644 --- a/src/main/webapp/file.xhtml +++ b/src/main/webapp/file.xhtml @@ -71,7 +71,8 @@ - + + @@ -79,7 +80,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 b6484c16628..a48334c27f9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3846,7 +3846,7 @@ public void testReCreateDataset() { } @Test - public void testCurationLabelAPIs() { + public void testCurationStatusAPIs() { Response createUser = UtilIT.createRandomUser(); createUser.prettyPrint(); String username = UtilIT.getUsernameFromResponse(createUser); @@ -3888,16 +3888,60 @@ public void testCurationLabelAPIs() { String labelSetName = getData(response.getBody().asString()); // full should be {"message":"AlternateProcess"} assertTrue(labelSetName.contains("AlternateProcess")); - - // Now set a label - //Option from the wrong set - Response response2 = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "Author contacted"); - response2.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); - // Valid option - Response response3 = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "State 1"); - response3.then().assertThat().statusCode(OK.getStatusCode()); + + // Set curation statuses and verify history + Response setStatus1 = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "State 1"); + setStatus1.then().assertThat().statusCode(OK.getStatusCode()); + + Response setStatus2 = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "State 2"); + setStatus2.then().assertThat().statusCode(OK.getStatusCode()); + + Response setStatus3 = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "State 3"); + setStatus3.then().assertThat().statusCode(OK.getStatusCode()); + + // Get curation status with history + Response getStatusWithHistory = UtilIT.getDatasetCurationStatus(datasetId, apiToken, true); + getStatusWithHistory.then().assertThat().statusCode(OK.getStatusCode()); + JsonArray history = getDataAsJsonArray(getStatusWithHistory.body().asString()); + + // Verify history + assertEquals(3, history.size()); + assertEquals("State 3", history.getJsonObject(0).getString("label")); + assertEquals("State 2", history.getJsonObject(1).getString("label")); + assertEquals("State 1", history.getJsonObject(2).getString("label")); + + // Reset status to null + Response resetStatus = UtilIT.deleteDatasetCurationLabel(datasetId, apiToken); + resetStatus.then().assertThat().statusCode(OK.getStatusCode()); + + // Verify null status + Response getStatusAfterReset = UtilIT.getDatasetCurationStatus(datasetId, apiToken, false); + getStatusAfterReset.then().assertThat().statusCode(OK.getStatusCode()); + + JsonObject statusAfterReset = Json.createReader(new StringReader(getStatusAfterReset.body().asString())).readObject(); + assertFalse(statusAfterReset.containsKey("label")); + + // Attempt to set invalid status + Response setInvalidStatus = UtilIT.setDatasetCurationLabel(datasetId, apiToken, "Invalid Status"); + setInvalidStatus.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + + // Clean up + Response deleteDatasetResponse = UtilIT.deleteDatasetViaNativeApi(datasetId, apiToken); + assertEquals(200, deleteDatasetResponse.getStatusCode()); + + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + assertEquals(200, deleteUserResponse.getStatusCode()); } + private JsonArray getDataAsJsonArray(String body) { + try (StringReader rdr = new StringReader(body)) { + return Json.createReader(rdr).readObject().getJsonArray("data"); + } + } + private JsonObject getDataAsJsonObject(String body) { try (StringReader rdr = new StringReader(body)) { return Json.createReader(rdr).readObject().getJsonObject("data"); 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 9a1deca887e..f0d1d6fba4a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3913,6 +3913,20 @@ static Response setDatasetCurationLabel(Integer datasetId, String apiToken, Stri return response; } + static Response deleteDatasetCurationLabel(Integer datasetId, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .delete("/api/datasets/" + datasetId + "/curationStatus"); + return response; + } + + static Response getDatasetCurationStatus(Integer datasetId, String apiToken, boolean includeHistory) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/" + datasetId + "/curationStatus?includeHistory=" + includeHistory); + return response; + } + static Response getDatasetVersionArchivalStatus(Integer datasetId, String version, String apiToken) { Response response = given() .header(API_TOKEN_HTTP_HEADER, apiToken)