diff --git a/conf/solr/schema.xml b/conf/solr/schema.xml index 048874816f7..50835957b04 100644 --- a/conf/solr/schema.xml +++ b/conf/solr/schema.xml @@ -206,7 +206,8 @@ - + + diff --git a/doc/release-notes/8431-versionNotes.md b/doc/release-notes/8431-versionNotes.md new file mode 100644 index 00000000000..b9804833548 --- /dev/null +++ b/doc/release-notes/8431-versionNotes.md @@ -0,0 +1,12 @@ +Dataverse now supports the option of adding a versionNote before/during publication of a dataset that can be used to indicate why a version was created and/or how it differs from the prior version. Whether this feature is enabled is controlled by a flag. Version notes are shown in the user interface (dataset page version table), indexed, available via the API, and have been added to the JSON, DDI, DataCite, and OAI-ORE exports. + +With the addition of this feature, work has been done to clean-up and rename fields that have been used for specifying the reason for deaccessioning a dataset and providing an optional link to a non-Dataverse location where the dataset still can be found. The former was listed in some JSON-based API calls and exports as "versionNote" and is now "deaccessionNote", while the latter was referred to as "archiveNote" and is now "deacccessionLink". These result in incompatibilities in the UI related to deaccessioned datasets. + +Further, some database consolidation has been done to combine the deaccessionlink and archivenote fields which appear to have both been used for the same purpose (the deaccessionlink db field is older and was not displayed in the current UI. Going forward, only the deaccessionlink column exists. + +New Feature Flags: + +VERSION_NOTE - false by default. + +Update of the solr schema (using the standard instructions) is needed. + diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 15dabecda35..48a8dbce32c 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -17,6 +17,11 @@ v6.6 v6.5 ---- +- The JSON representation for a datasetVersion sent or received in API calls has changed such that + - "versionNote" -> "deaccessionNote" + - "archiveNote" --> "deaccessionLink" + These may be non-null for deaccessioned versions and an optional new "versionNote" field indicating the reason a version was created may be present on any datasetversion. + - **/api/datasets/{identifier}/links**: The response from :ref:`list-collections-linked-from-dataset` has been improved to provide a more structured (but backward-incompatible) JSON response. v6.4 diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index e0e948d9a19..b0b37b68058 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3582,6 +3582,68 @@ The fully expanded example above (without environment variables) looks like this .. code-block:: bash curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/datasets/datasetTypes/3" + + .. _api-dataset-version-note: + +Dataset Version Notes +~~~~~~~~~~~~~~~~~~~~~ + +Intended as :ref:`provenance` information about why the version was created/how it differs from the prior version + +Depositors who can edit the dataset and curators can add a version note for the draft version. Superusers can add/delete version notes for any version. + +Version notes can be retrieved via the following, with authorization required to see a note on the :draft version + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=3 + export VERSION=:draft + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/datasets/$ID/versions/$VERSION/versionNote" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/datasets/3/versions/:draft/versionNote" + +Notes can be set with: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=3 + export VERSION=:draft + export NOTE=Files updated to correct typos + + curl -H "X-Dataverse-key:$API_TOKEN" -X PUT -d "$NOTE" "$SERVER_URL/api/datasets/$ID/versions/$VERSION/versionNote" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -d "Files updated to correct typos" "https://demo.dataverse.org/api/datasets/3/versions/:draft/versionNote" + +And deleted via: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=3 + export VERSION=2.0 + + curl -H "X-Dataverse-key:$API_TOKEN" -X DELETE "$SERVER_URL/api/datasets/$ID/versions/$VERSION/versionNote" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/datasets/3/versions/2.0/versionNote" + .. _api-link-dataset-type: diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 6c683a82ba0..9db1ae6d80d 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3451,7 +3451,7 @@ This setting is required in conjunction with the ``globus-use-experimental-async Feature Flags ------------- -Certain features might be deactivated because they are experimental and/or opt-in previews. If you want to enable these, +Certain features might be deactivated because they are experimental and/or opt-in capabilities. If you want to enable these, please find all known feature flags below. Any of these flags can be activated using a boolean value (case-insensitive, one of "true", "1", "YES", "Y", "ON") for the setting. @@ -3496,6 +3496,9 @@ please find all known feature flags below. Any of these flags can be activated u * - index-harvested-metadata-source - Index the nickname or the source name (See the optional ``sourceName`` field in :ref:`create-a-harvesting-client`) of the harvesting client as the "metadata source" of harvested datasets and files. If enabled, the Metadata Source facet will show separate groupings of the content harvested from different sources (by harvesting client nickname or source name) instead of the default behavior where there is one "Harvested" grouping for all harvested content. - ``Off`` + * - enable-version-note + - Turns on the ability to add/view/edit/delete per-dataset-version notes intended to provide :ref:`provenance` information about why the dataset/version was created. + - ``Off`` **Note:** Feature flags can be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_FEATURE_XXX`` (e.g. ``DATAVERSE_FEATURE_API_SESSION_AUTH=1``). These environment variables can be set in your shell before starting Payara. If you are using :doc:`Docker for development `, you can set them in the `docker compose `_ file. diff --git a/doc/sphinx-guides/source/user/dataset-management.rst b/doc/sphinx-guides/source/user/dataset-management.rst index 33cd011101e..ce72bb0df5a 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -576,7 +576,26 @@ When you access a dataset's file-level permissions page, you will see two sectio Data Provenance =============== -Data Provenance is a record of where your data came from and how it reached its current form. It describes the origin of a data file, any transformations that have been made to that file, and any persons or organizations associated with that file. A data file's provenance can aid in reproducibility and compliance with legal regulations. The Dataverse Software can help you keep track of your data's provenance. Currently, the Dataverse Software only makes provenance information available to those who have edit permissions on your dataset, but in the future we plan to expand this feature to make provenance information available to the public. +Dataset-Level +------------- +When configured, the Dataverse software can allow data depositors, curators, and administrators +to provide information about why a new version of a dataset was created and/or how its contents +differ from a prior version. These users can add an optional "Version Note" to a draft dataset +version in the dataset page/versions tab or during publication. This information is publicly +available via the user interface (dataset page/versions tab), API, and in metadata exports +(including the DataCite, JSON, DDI, and OAI_ORE exports). + +File-Level +---------- + +Data Provenance is a record of where your data came from and how it reached its current form. +It describes the origin of a data file, any transformations that have been made to that file, +and any persons or organizations associated with that file. A data file's provenance can aid in +reproducibility and compliance with legal regulations. When configured to support provenance, +the Dataverse Software can help you keep track of your data's provenance. Currently, the Dataverse +Software only makes provenance information available to those who have edit permissions on your +dataset, but in the future we plan to expand this feature to make provenance information available +to the public. .. COMMENTED OUT UNTIL PROV FILE DOWNLOAD IS ADDED: , and make it available to those who need it. diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionNoteValidator.java b/src/main/java/edu/harvard/iq/dataverse/DatasetDeaccessionNoteValidator.java similarity index 76% rename from src/main/java/edu/harvard/iq/dataverse/DatasetVersionNoteValidator.java rename to src/main/java/edu/harvard/iq/dataverse/DatasetDeaccessionNoteValidator.java index a5ea487a68f..7c6263fe9b9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionNoteValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetDeaccessionNoteValidator.java @@ -13,28 +13,28 @@ * * @author skraffmi */ -public class DatasetVersionNoteValidator implements ConstraintValidator { +public class DatasetDeaccessionNoteValidator implements ConstraintValidator { private String versionState; - private String versionNote; + private String deaccessionNote; @Override - public void initialize(ValidateVersionNote constraintAnnotation) { + public void initialize(ValidateDeaccessionNote constraintAnnotation) { versionState = constraintAnnotation.versionState(); - versionNote = constraintAnnotation.versionNote(); + deaccessionNote = constraintAnnotation.deaccessionNote(); } @Override public boolean isValid(DatasetVersion value, ConstraintValidatorContext context) { - if (versionState.equals(DatasetVersion.VersionState.DEACCESSIONED) && versionNote.isEmpty()){ + if (versionState.equals(DatasetVersion.VersionState.DEACCESSIONED) && deaccessionNote.isEmpty()){ if (context != null) { context.buildConstraintViolationWithTemplate(value + " " + BundleUtil.getStringFromBundle("file.deaccessionDialog.dialog.textForReason.error")).addConstraintViolation(); } return false; } - if (versionState.equals(DatasetVersion.VersionState.DEACCESSIONED) && versionNote.length() > DatasetVersion.VERSION_NOTE_MAX_LENGTH){ + if (versionState.equals(DatasetVersion.VersionState.DEACCESSIONED) && deaccessionNote.length() > DatasetVersion.VERSION_NOTE_MAX_LENGTH){ if (context != null) { context.buildConstraintViolationWithTemplate(value + " " + BundleUtil.getStringFromBundle("file.deaccessionDialog.dialog.limitChar.error")).addConstraintViolation(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 57afdec7752..014b0f2a26d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -184,7 +184,7 @@ public class DatasetPage implements java.io.Serializable { public enum EditMode { - CREATE, INFO, FILE, METADATA, LICENSE + CREATE, INFO, FILE, METADATA, LICENSE, VERSIONNOTE }; public enum DisplayMode { @@ -2119,6 +2119,7 @@ private String init(boolean initFull) { if (workingVersion.isDraft() && canUpdateDataset()) { readOnly = false; } + publishDialogVersionNote = workingVersion.getVersionNote(); // This will default to all the files in the version, if the search term // parameter hasn't been specified yet: fileMetadatasSearch = selectFileMetadatasForDisplay(); @@ -2775,6 +2776,7 @@ public String releaseDataset() { if(!dataset.getOwner().isReleased()){ releaseParentDV(); } + workingVersion.setVersionNote(publishDialogVersionNote); if(publishDatasetPopup()|| publishBothPopup() || !dataset.getLatestVersion().isMinorUpdate()){ return releaseDataset(false); } @@ -2841,35 +2843,35 @@ private DatasetVersion setDatasetVersionDeaccessionReasonAndURL(DatasetVersion d String deacessionReasonDetail = getDeaccessionReasonText() != null ? ( getDeaccessionReasonText()).trim() : ""; switch (deaccessionReasonCode) { case 1: - dvIn.setVersionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.identifiable") ); + dvIn.setDeaccessionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.identifiable") ); break; case 2: - dvIn.setVersionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.beRetracted") ); + dvIn.setDeaccessionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.beRetracted") ); break; case 3: - dvIn.setVersionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.beTransferred") ); + dvIn.setDeaccessionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.beTransferred") ); break; case 4: - dvIn.setVersionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.IRB")); + dvIn.setDeaccessionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.IRB")); break; case 5: - dvIn.setVersionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.legalIssue")); + dvIn.setDeaccessionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.legalIssue")); break; case 6: - dvIn.setVersionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.notValid")); + dvIn.setDeaccessionNote(BundleUtil.getStringFromBundle("file.deaccessionDialog.reason.selectItem.notValid")); break; case 7: break; } if (!deacessionReasonDetail.isEmpty()){ - if (!StringUtil.isEmpty(dvIn.getVersionNote())){ - dvIn.setVersionNote(dvIn.getVersionNote() + " " + deacessionReasonDetail); + if (!StringUtil.isEmpty(dvIn.getDeaccessionNote())){ + dvIn.setDeaccessionNote(dvIn.getDeaccessionNote() + " " + deacessionReasonDetail); } else { - dvIn.setVersionNote(deacessionReasonDetail); + dvIn.setDeaccessionNote(deacessionReasonDetail); } } - dvIn.setArchiveNote(getDeaccessionForwardURLFor()); + dvIn.setDeaccessionLink(getDeaccessionForwardURLFor()); return dvIn; } @@ -3935,7 +3937,7 @@ public void validateForwardURL(FacesContext context, UIComponent toValidate, Obj return; } - if (value.toString().length() <= DatasetVersion.ARCHIVE_NOTE_MAX_LENGTH) { + if (value.toString().length() <= DatasetVersion.DEACCESSION_NOTE_MAX_LENGTH) { ((UIInput) toValidate).setValid(true); } else { ((UIInput) toValidate).setValid(false); @@ -4106,8 +4108,9 @@ public String save() { } if (editMode.equals(EditMode.FILE)) { JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.filesSuccess")); + } if (editMode.equals(EditMode.VERSIONNOTE)) { + JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.versionNoteSuccess")); } - } else { // must have been a bulk file update or delete: if (bulkFileDeleteInProgress) { @@ -6780,5 +6783,21 @@ public String getSignpostingLinkHeader() { public boolean isDOI() { return AbstractDOIProvider.DOI_PROTOCOL.equals(dataset.getGlobalId().getProtocol()); } + + public void saveVersionNote() { + this.editMode=EditMode.VERSIONNOTE; + publishDialogVersionNote = workingVersion.getVersionNote(); + save(); + } + String publishDialogVersionNote = null; + + // Make separate property for versionNote - can't have two p:dialogs changing the same property + public String getPublishDialogVersionNote() { + return publishDialogVersionNote; + } + + public void setPublishDialogVersionNote(String note) { + publishDialogVersionNote =note; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index a7bbc7c3ad4..bfb2850944d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -8,7 +8,6 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.license.License; -import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -17,6 +16,9 @@ import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -80,7 +82,7 @@ @Entity @Table(indexes = {@Index(columnList="dataset_id")}, uniqueConstraints = @UniqueConstraint(columnNames = {"dataset_id,versionnumber,minorversionnumber"})) -@ValidateVersionNote(versionNote = "versionNote", versionState = "versionState") +@ValidateDeaccessionNote(deaccessionNote = "deaccessionNote", versionState = "versionState") public class DatasetVersion implements Serializable { private static final Logger logger = Logger.getLogger(DatasetVersion.class.getCanonicalName()); @@ -114,7 +116,8 @@ public enum VersionState { DRAFT, RELEASED, ARCHIVED, DEACCESSIONED } - public static final int ARCHIVE_NOTE_MAX_LENGTH = 1000; + public static final int DEACCESSION_NOTE_MAX_LENGTH = 1000; + public static final int DEACCESSION_LINK_MAX_LENGTH = 1260; //Long enough to cover the case where a legacy deaccessionLink(256 char) and archiveNote (1000) are combined (with a space) public static final int VERSION_NOTE_MAX_LENGTH = 1000; //Archival copies: Status message required components @@ -137,10 +140,16 @@ public enum VersionState { private Long versionNumber; private Long minorVersionNumber; + //This is used for the deaccession reason + @Size(min=0, max=DEACCESSION_NOTE_MAX_LENGTH) + @Column(length = DEACCESSION_NOTE_MAX_LENGTH) + private String deaccessionNote; + + //This is a plain text, optional reason for the version's creation @Size(min=0, max=VERSION_NOTE_MAX_LENGTH) @Column(length = VERSION_NOTE_MAX_LENGTH) private String versionNote; - + /* * @todo versionState should never be null so when we are ready, uncomment * the `nullable = false` below. @@ -177,12 +186,6 @@ public enum VersionState { @Temporal(value = TemporalType.TIMESTAMP) private Date archiveTime; - @Size(min=0, max=ARCHIVE_NOTE_MAX_LENGTH) - @Column(length = ARCHIVE_NOTE_MAX_LENGTH) - //@ValidateURL() - this validation rule was making a bunch of older legacy datasets invalid; - // removed pending further investigation (v4.13) - private String archiveNote; - // Originally a simple string indicating the location of the archival copy. As // of v5.12, repurposed to provide a more general json archival status (failure, // pending, success) and message (serialized as a string). The archival copy @@ -191,7 +194,9 @@ public enum VersionState { @Column(nullable=true, columnDefinition = "TEXT") private String archivalCopyLocation; - + //This is used for the deaccession reason + @Size(min=0, max=DEACCESSION_LINK_MAX_LENGTH) + @Column(length = DEACCESSION_LINK_MAX_LENGTH) private String deaccessionLink; @Transient @@ -361,19 +366,6 @@ public void setArchiveTime(Date archiveTime) { this.archiveTime = archiveTime; } - public String getArchiveNote() { - return archiveNote; - } - - public void setArchiveNote(String note) { - // @todo should this be using bean validation for trsting note length? - if (note != null && note.length() > ARCHIVE_NOTE_MAX_LENGTH) { - throw new IllegalArgumentException("Error setting archiveNote: String length is greater than maximum (" + ARCHIVE_NOTE_MAX_LENGTH + ")." - + " StudyVersion id=" + id + ", archiveNote=" + note); - } - this.archiveNote = note; - } - public String getArchivalCopyLocation() { return archivalCopyLocation; } @@ -417,11 +409,21 @@ public String getDeaccessionLink() { } public void setDeaccessionLink(String deaccessionLink) { + if (deaccessionLink != null && deaccessionLink.length() > DEACCESSION_LINK_MAX_LENGTH) { + throw new IllegalArgumentException("Error setting deaccessionLink: String length is greater than maximum (" + DEACCESSION_LINK_MAX_LENGTH + ")." + + " StudyVersion id=" + id + ", deaccessionLink=" + deaccessionLink); + } this.deaccessionLink = deaccessionLink; } - public GlobalId getDeaccessionLinkAsGlobalId() { - return PidUtil.parseAsGlobalID(deaccessionLink); + public String getDeaccessionLinkAsURLString() { + String dLink = null; + try { + dLink = new URI(deaccessionLink).toURL().toExternalForm(); + } catch (URISyntaxException | MalformedURLException e) { + logger.fine("Invalid deaccessionLink - not a URL: " + deaccessionLink); + } + return dLink; } public Date getCreateTime() { @@ -490,8 +492,8 @@ public void setContributorNames(String contributorNames) { } - public String getVersionNote() { - return versionNote; + public String getDeaccessionNote() { + return deaccessionNote; } public DatasetVersionDifference getDefaultVersionDifference() { @@ -541,12 +543,12 @@ public VersionState getPriorVersionState() { return null; } - public void setVersionNote(String note) { - if (note != null && note.length() > VERSION_NOTE_MAX_LENGTH) { - throw new IllegalArgumentException("Error setting versionNote: String length is greater than maximum (" + VERSION_NOTE_MAX_LENGTH + ")." - + " StudyVersion id=" + id + ", versionNote=" + note); + public void setDeaccessionNote(String note) { + if (note != null && note.length() > DEACCESSION_NOTE_MAX_LENGTH) { + throw new IllegalArgumentException("Error setting deaccessionNote: String length is greater than maximum (" + DEACCESSION_NOTE_MAX_LENGTH + ")." + + " StudyVersion id=" + id + ", deaccessionNote=" + note); } - this.versionNote = note; + this.deaccessionNote = note; } public Long getVersionNumber() { @@ -2158,4 +2160,17 @@ public void setExternalStatusLabel(String externalStatusLabel) { this.externalStatusLabel = externalStatusLabel; } + public String getVersionNote() { + return versionNote; + } + + public void setVersionNote(String note) { + if (note != null && note.length() > VERSION_NOTE_MAX_LENGTH) { + throw new IllegalArgumentException("Error setting versionNote: String length is greater than maximum (" + VERSION_NOTE_MAX_LENGTH + ")." + + " StudyVersion id=" + id + ", versionNote=" + note); + } + + this.versionNote = note; + } } + diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java index 762319884b9..7e9b778c6f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java @@ -1060,42 +1060,6 @@ public boolean doesChecksumExistInDatasetVersion(DatasetVersion datasetVersion, } - public List> getBasicDatasetVersionInfo(Dataset dataset){ - - if (dataset == null){ - throw new NullPointerException("dataset cannot be null"); - } - - String query = "SELECT id, dataset_id, releasetime, versionnumber," - + " minorversionnumber, versionstate, versionnote" - + " FROM datasetversion" - + " WHERE dataset_id = " + dataset.getId() - + " ORDER BY versionnumber DESC," - + " minorversionnumber DESC," - + " versionstate;"; - msg("query: " + query); - Query nativeQuery = em.createNativeQuery(query); - List datasetVersionInfoList = nativeQuery.getResultList(); - - List> hashList = new ArrayList<>(); - - HashMap mMap = new HashMap<>(); - for (Object[] dvInfo : datasetVersionInfoList) { - mMap = new HashMap<>(); - mMap.put("datasetVersionId", dvInfo[0]); - mMap.put("datasetId", dvInfo[1]); - mMap.put("releaseTime", dvInfo[2]); - mMap.put("versionnumber", dvInfo[3]); - mMap.put("minorversionnumber", dvInfo[4]); - mMap.put("versionstate", dvInfo[5]); - mMap.put("versionnote", dvInfo[6]); - hashList.add(mMap); - } - return hashList; - } // end getBasicDatasetVersionInfo - - - public HashMap getFileMetadataHistory(DataFile df){ if (df == null){ diff --git a/src/main/java/edu/harvard/iq/dataverse/ValidateVersionNote.java b/src/main/java/edu/harvard/iq/dataverse/ValidateDeaccessionNote.java similarity index 78% rename from src/main/java/edu/harvard/iq/dataverse/ValidateVersionNote.java rename to src/main/java/edu/harvard/iq/dataverse/ValidateDeaccessionNote.java index c8d64d4a642..d6bf2b857d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ValidateVersionNote.java +++ b/src/main/java/edu/harvard/iq/dataverse/ValidateDeaccessionNote.java @@ -22,17 +22,17 @@ @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) -@Constraint(validatedBy = {DatasetVersionNoteValidator.class}) +@Constraint(validatedBy = {DatasetDeaccessionNoteValidator.class}) @Documented -public @interface ValidateVersionNote { +public @interface ValidateDeaccessionNote { - String message() default "Failed Validation for DatasetVersionNote"; + String message() default "Failed Validation for DatasetsDeaccessionNote"; Class[] groups() default {}; Class[] payload() default {}; - String versionNote(); + String deaccessionNote(); String versionState(); 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 f85aa228f0b..9d7c3dd2225 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -5063,11 +5063,11 @@ public Response deaccessionDataset(@Context ContainerRequestContext crc, @PathPa DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); try { JsonObject jsonObject = JsonUtil.getJsonObject(jsonBody); - datasetVersion.setVersionNote(jsonObject.getString("deaccessionReason")); + datasetVersion.setDeaccessionNote(jsonObject.getString("deaccessionReason")); String deaccessionForwardURL = jsonObject.getString("deaccessionForwardURL", null); if (deaccessionForwardURL != null) { try { - datasetVersion.setArchiveNote(deaccessionForwardURL); + datasetVersion.setDeaccessionLink(deaccessionForwardURL); } catch (IllegalArgumentException iae) { return badRequest(BundleUtil.getStringFromBundle("datasets.api.deaccessionDataset.invalid.forward.url", List.of(iae.getMessage()))); } @@ -5528,4 +5528,75 @@ public Response deleteDatasetFiles(@Context ContainerRequestContext crc, @PathPa }, getRequestUser(crc)); } + +@GET + @AuthRequired + @Path("{id}/versions/{versionId}/versionNote") + public Response getVersionCreationNote(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + + return response(req -> { + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); + String note = datasetVersion.getVersionNote(); + if(note == null) { + return ok(Json.createObjectBuilder()); + } + return ok(note); + }, getRequestUser(crc)); + } + + @PUT + @AuthRequired + @Path("{id}/versions/{versionId}/versionNote") + public Response addVersionNote(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @PathParam("versionId") String versionId, String note, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + if (!FeatureFlags.VERSION_NOTE.enabled()) { + return notFound(BundleUtil.getStringFromBundle("datasets.api.addVersionNote.notEnabled")); + } + if (!DS_VERSION_DRAFT.equals(versionId)) { + try { + AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); + + if (!user.isSuperuser()) { + return forbidden(BundleUtil.getStringFromBundle("datasets.api.addVersionNote.forbidden")); + } + return response(req -> { + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); + datasetVersion.setVersionNote(note); + execCommand(new UpdatePublishedDatasetVersionCommand(req, datasetVersion)); + return ok("Note added to version " + datasetVersion.getFriendlyVersionNumber()); + }, getRequestUser(crc)); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } + return response(req -> { + DatasetVersion datasetVersion = findDatasetOrDie(datasetId).getOrCreateEditVersion(); + datasetVersion.setVersionNote(note); + execCommand(new UpdateDatasetVersionCommand(datasetVersion.getDataset(), req)); + + return ok("Note added"); + }, getRequestUser(crc)); + } + + @DELETE + @AuthRequired + @Path("{id}/versions/{versionId}/versionNote") + public Response deleteVersionNote(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + if(!FeatureFlags.VERSION_NOTE.enabled()) { + return notFound(BundleUtil.getStringFromBundle("datasets.api.addVersionNote.notEnabled")); + } + if (!DS_VERSION_DRAFT.equals(versionId)) { + AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc); + if (!user.isSuperuser()) { + return forbidden(BundleUtil.getStringFromBundle("datasets.api.addVersionNote.forbidden")); + } + } + return response(req -> { + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); + datasetVersion.setVersionNote(null); + execCommand(new UpdateDatasetVersionCommand(datasetVersion.getDataset(), req)); + + return ok("Note deleted"); + }, getRequestUser(crc)); + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/dto/DatasetVersionDTO.java b/src/main/java/edu/harvard/iq/dataverse/api/dto/DatasetVersionDTO.java index 37fe197280b..1bc080b8858 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/dto/DatasetVersionDTO.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/dto/DatasetVersionDTO.java @@ -10,7 +10,6 @@ * @author ellenk */ public class DatasetVersionDTO { - String archiveNote; String deacessionLink; // FIXME: Change to versionNumberMajor and versionNumberMinor? Some partial renaming of "minor" was done. Long versionNumber; @@ -47,6 +46,8 @@ public class DatasetVersionDTO { List fileMetadatas; List files; + String versionNote; + public boolean isInReview() { return inReview; } @@ -215,14 +216,6 @@ public void setFiles(List files) { this.files = files; } - public String getArchiveNote() { - return archiveNote; - } - - public void setArchiveNote(String archiveNote) { - this.archiveNote = archiveNote; - } - public String getDeacessionLink() { return deacessionLink; } @@ -328,17 +321,17 @@ public List getDatasetFields() { return null; } + public String getVersionNote() { + return versionNote; + } + + public void setVersionNote(String versionNote) { + this.versionNote = versionNote; + } + @Override public String toString() { - return "DatasetVersionDTO{" + "archiveNote=" + archiveNote + ", deacessionLink=" + deacessionLink + ", versionNumber=" + versionNumber + ", minorVersionNumber=" + versionMinorNumber + ", id=" + id + ", versionState=" + versionState + ", releaseDate=" + releaseDate + ", lastUpdateTime=" + lastUpdateTime + ", createTime=" + createTime + ", archiveTime=" + archiveTime + ", UNF=" + UNF + ", metadataBlocks=" + metadataBlocks + ", fileMetadatas=" + fileMetadatas + '}'; + return "DatasetVersionDTO{deacessionLink=" + deacessionLink + ", versionNumber=" + versionNumber + ", minorVersionNumber=" + versionMinorNumber + ", id=" + id + ", versionState=" + versionState + ", releaseDate=" + releaseDate + ", lastUpdateTime=" + lastUpdateTime + ", createTime=" + createTime + ", archiveTime=" + archiveTime + ", UNF=" + UNF + ", metadataBlocks=" + metadataBlocks + ", fileMetadatas=" + fileMetadatas + '}'; } - - - - - - - - - + } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java index e378e2e2ef7..3629432b7e4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CuratePublishedDatasetVersionCommand.java @@ -72,6 +72,10 @@ public Dataset execute(CommandContext ctxt) throws CommandException { TermsOfUseAndAccess newTerms = newVersion.getTermsOfUseAndAccess(); newTerms.setDatasetVersion(updateVersion); updateVersion.setTermsOfUseAndAccess(newTerms); + + //Creation Note + updateVersion.setVersionNote(newVersion.getVersionNote()); + // Clear unnecessary terms relationships .... newVersion.setTermsOfUseAndAccess(null); oldTerms.setDatasetVersion(null); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java new file mode 100644 index 00000000000..f8f5d05d972 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdatePublishedDatasetVersionCommand.java @@ -0,0 +1,63 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.authorization.Permission; +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; + +/** + * Updates the specified dataset version, which must already be published/not + * draft. The command is only usable by superusers. The initial use is to allow + * adding version notes to existing versions but it is otherwise generic + * assuming there may be other cases where updating some aspect of an existing + * version is needed. Note this is similar to the + * CuratePublishedDatasetVersionCommand, but in that case changes in an existing + * draft version are pushed into the latest published version as a form of + * republishing (and the draft version ceases to exist). This command assumes + * changes have been made to the existing dataset version of interest, which may + * not be the latest published one, and it does not make any changes to a + * dataset's draft version if that exists. + */ +@RequiredPermissions(Permission.EditDataset) +public class UpdatePublishedDatasetVersionCommand extends AbstractCommand { + + private final DatasetVersion datasetVersion; + + public UpdatePublishedDatasetVersionCommand(DataverseRequest aRequest, DatasetVersion datasetVersion) { + super(aRequest, datasetVersion.getDataset()); + this.datasetVersion = datasetVersion; + } + + @Override + public DatasetVersion execute(CommandContext ctxt) throws CommandException { + // Check if the user is a superuser + if (!getUser().isSuperuser()) { + throw new IllegalCommandException("Only superusers can update published dataset versions", this); + } + + // Ensure the version is published + if (!datasetVersion.isReleased()) { + throw new IllegalCommandException("This command can only be used on published dataset versions", this); + } + + // Save the changes + DatasetVersion savedVersion = ctxt.em().merge(datasetVersion); + + return savedVersion; + } + + @Override + public boolean onSuccess(CommandContext ctxt, Object r) { + DatasetVersion version = (DatasetVersion) r; + // Only need to reindex if this version is the latest published version for the + // dataset + if (version.equals(version.getDataset().getLatestVersionForCopy())) { + ctxt.index().asyncIndexDataset(version.getDataset(), true); + } + return true; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java b/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java index a1f480af197..1a02089aef9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/ddi/DdiExportUtil.java @@ -417,6 +417,12 @@ private static void writeVersionStatement(XMLStreamWriter xmlw, DatasetVersionDT XmlWriterUtil.writeAttribute(xmlw,"type", datasetVersionDTO.getVersionState().toString()); xmlw.writeCharacters(datasetVersionDTO.getVersionNumber().toString()); xmlw.writeEndElement(); // version + if (!StringUtils.isBlank(datasetVersionDTO.getVersionNote())) { + xmlw.writeStartElement("notes"); + xmlw.writeCharacters(datasetVersionDTO.getVersionNote()); + xmlw.writeEndElement(); // notes + } + xmlw.writeEndElement(); // verStmt } diff --git a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java index e9046a505c7..baf8302437d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java +++ b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java @@ -1379,7 +1379,13 @@ private void writeDescriptions(XMLStreamWriter xmlw, DvObject dvObject, boolean } } - + String versionNote = dv.getVersionNote(); + if(!StringUtils.isBlank(versionNote)) { + attributes.clear(); + attributes.put("descriptionType", "TechnicalInfo"); + descriptionsWritten = XmlWriterUtil.writeOpenTagIfNeeded(xmlw, "descriptions", descriptionsWritten); + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "description", attributes, versionNote); + } } if (descriptionsWritten) { 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 5fbcd6ea520..a8e6c0661d7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -1355,11 +1355,15 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set findPermissionsInSolrOnly() throws SearchException { String dtype = dvObjectService.getDtype(id); if (dtype == null) { permissionInSolrOnly.add(docId); - }else if (dtype.equals(DType.Dataset.getDType())) { + } else if (dtype.equals(DType.Dataset.getDType())) { List states = datasetService.getVersionStates(id); if (states != null) { String latestState = states.get(states.size() - 1); 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 712f90186f5..109c17d6ef9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchFields.java @@ -258,6 +258,7 @@ public class SearchFields { public static final String DATASET_CITATION = "citation"; public static final String DATASET_CITATION_HTML = "citationHtml"; public static final String DATASET_DEACCESSION_REASON = "deaccessionReason"; + public static final String DATASET_VERSION_NOTE = "versionNote"; /** * In contrast to PUBLICATION_YEAR, this field applies only to datasets for more targeted results for just datasets. The format is YYYY (i.e. diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 04ae0018323..4326dea6e1c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -131,6 +131,15 @@ public enum FeatureFlags { * @since Dataverse 6.4 */ GLOBUS_USE_EXPERIMENTAL_ASYNC_FRAMEWORK("globus-use-experimental-async-framework"), + /** + * This flag adds a note field to input/display a reason explaining why a version was created. + * + * @apiNote Raise flag by setting + * "dataverse.feature.enable-version-creation-note" + * @since Dataverse 6.5 + */ + VERSION_NOTE("enable-version-note"), + ; final String flag; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java index 39573416db9..4cbc2aa7b9a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java @@ -49,7 +49,8 @@ public class OREMap { public static final String NAME = "OREMap"; //NOTE: Update this value whenever the output of this class is changed - private static final String DATAVERSE_ORE_FORMAT_VERSION = "Dataverse OREMap Format v1.0.0"; + private static final String DATAVERSE_ORE_FORMAT_VERSION = "Dataverse OREMap Format v1.0.1"; + //v1.0.1 - added versionNote private static final String DATAVERSE_SOFTWARE_NAME = "Dataverse"; private static final String DATAVERSE_SOFTWARE_URL = "https://github.com/iqss/dataverse"; @@ -122,13 +123,15 @@ public JsonObjectBuilder getOREMapBuilder(boolean aggregationOnly) { .add(JsonLDTerm.schemaOrg("name").getLabel(), version.getTitle()) .add(JsonLDTerm.schemaOrg("dateModified").getLabel(), version.getLastUpdateTime().toString()); addIfNotNull(aggBuilder, JsonLDTerm.schemaOrg("datePublished"), dataset.getPublicationDateFormattedYYYYMMDD()); + addIfNotNull(aggBuilder, JsonLDTerm.DVCore("versionNote"), version.getVersionNote()); + //Add version state info - DRAFT, RELEASED, DEACCESSIONED, ARCHIVED with extra info for DEACCESIONED VersionState vs = version.getVersionState(); if(vs.equals(VersionState.DEACCESSIONED)) { JsonObjectBuilder deaccBuilder = Json.createObjectBuilder(); deaccBuilder.add(JsonLDTerm.schemaOrg("name").getLabel(), vs.name()); - deaccBuilder.add(JsonLDTerm.DVCore("reason").getLabel(), version.getVersionNote()); - addIfNotNull(deaccBuilder, JsonLDTerm.DVCore("forwardUrl"), version.getArchiveNote()); + deaccBuilder.add(JsonLDTerm.DVCore("reason").getLabel(), version.getDeaccessionNote()); + addIfNotNull(deaccBuilder, JsonLDTerm.DVCore("forwardUrl"), version.getDeaccessionLink()); aggBuilder.add(JsonLDTerm.schemaOrg("creativeWorkStatus").getLabel(), deaccBuilder); } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index c51958577e5..8fd311f31ea 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -410,12 +410,15 @@ public Dataset parseDataset(JsonObject obj) throws JsonParseException { public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) throws JsonParseException { try { - String archiveNote = obj.getString("archiveNote", null); - if (archiveNote != null) { - dsv.setArchiveNote(archiveNote); - } - dsv.setDeaccessionLink(obj.getString("deaccessionLink", null)); + String deaccessionNote = obj.getString("deaccessionNote", null); + // ToDo - the treatment of null inputs is inconsistent across different fields (either the original value is kept or set to null). + // This is moot for most uses of this method, which start from an empty datasetversion, but use through https://github.com/IQSS/dataverse/blob/3e5a516670c42e019338063516a9d93a61833027/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/ContainerManagerImpl.java#L112 + // starts from an existing version where this inconsistency could be/is a problem. + if (deaccessionNote != null) { + dsv.setDeaccessionNote(deaccessionNote); + } + dsv.setVersionNote(obj.getString("versionNote", null)); int versionNumberInt = obj.getInt("versionNumber", -1); Long versionNumber = null; if (versionNumberInt !=-1) { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index fde1f8568e4..fdebbffc584 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -450,8 +450,7 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List anonymized .add("versionMinorNumber", dsv.getMinorVersionNumber()) .add("versionState", dsv.getVersionState().name()) .add("latestVersionPublishingState", dataset.getLatestVersion().getVersionState().name()) - .add("versionNote", dsv.getVersionNote()) - .add("archiveNote", dsv.getArchiveNote()) + .add("deaccessionNote", dsv.getDeaccessionNote()) .add("deaccessionLink", dsv.getDeaccessionLink()) .add("distributionDate", dsv.getDistributionDate()) .add("productionDate", dsv.getProductionDate()) @@ -461,7 +460,8 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List anonymized .add("createTime", format(dsv.getCreateTime())) .add("alternativePersistentId", dataset.getAlternativePersistentIdentifier()) .add("publicationDate", dataset.getPublicationDateFormattedYYYYMMDD()) - .add("citationDate", dataset.getCitationDateFormattedYYYYMMDD()); + .add("citationDate", dataset.getCitationDateFormattedYYYYMMDD()) + .add("versionNote", dsv.getVersionNote()); License license = DatasetUtil.getLicense(dsv); if (license != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java b/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java index 8fde76d84e1..55f69ee1471 100644 --- a/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/validation/URLValidator.java @@ -7,6 +7,7 @@ * * @author skraffmi */ +//Not currently used except in tests public class URLValidator implements ConstraintValidator { private String[] allowedSchemes; diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/ValidateURL.java b/src/main/java/edu/harvard/iq/dataverse/validation/ValidateURL.java index 3834b119598..7c368ea3934 100644 --- a/src/main/java/edu/harvard/iq/dataverse/validation/ValidateURL.java +++ b/src/main/java/edu/harvard/iq/dataverse/validation/ValidateURL.java @@ -13,6 +13,7 @@ @Retention(RUNTIME) @Constraint(validatedBy = {URLValidator.class}) @Documented +//Not currently used except in tests public @interface ValidateURL { String message() default "'${validatedValue}' {url.invalid}"; String[] schemes() default {"http", "https", "ftp"}; diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index bd05dfb0c52..06c935fd236 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1711,6 +1711,7 @@ dataset.message.createFailure=The dataset could not be created. dataset.message.termsFailure=The dataset terms could not be updated. dataset.message.label.fileAccess=Publicly-accessible storage dataset.message.publicInstall=Files in this dataset may be readable outside Dataverse, restricted and embargoed access are disabled +dataset.message.versionNoteSuccess=Version note successfully updated. dataset.message.parallelUpdateError=Changes cannot be saved. This dataset has been edited since this page was opened. To continue, copy your changes, refresh the page to see the recent updates, and re-enter any changes you want to save. dataset.message.parallelPublishError=Publishing is blocked. This dataset has been edited since this page was opened. To publish it, refresh the page to see the recent updates, and publish again. dataset.metadata.publicationDate=Publication Date @@ -2082,6 +2083,7 @@ file.dataFilesTab.button.direct=Direct file.dataFilesTab.versions=Versions file.dataFilesTab.versions.headers.dataset=Dataset Version file.dataFilesTab.versions.headers.summary=Summary +file.dataFilesTab.versions.headers.versionNote=Version Note file.dataFilesTab.versions.headers.contributors=Contributors file.dataFilesTab.versions.headers.contributors.withheld=Contributor name(s) withheld file.dataFilesTab.versions.headers.published=Published on @@ -2106,6 +2108,7 @@ file.dataFilesTab.versions.description.firstPublished=This is the first publishe file.dataFilesTab.versions.description.deaccessionedReason=Deaccessioned Reason: file.dataFilesTab.versions.description.beAccessedAt=The dataset can now be accessed at: file.dataFilesTab.versions.viewDetails.btn=View Details +file.dataFilesTab.versions.versionNote.btn=Edit Note file.dataFilesTab.versions.widget.viewMoreInfo=To view more information about the versions of this dataset, and to edit it if this is your dataset, please visit the full version of this dataset at the {2}. file.dataFilesTab.versions.preloadmessage=(Loading versions...) file.previewTab.externalTools.header=Available Previews @@ -2204,6 +2207,11 @@ file.auxfiles.types.NcML=XML from NetCDF/HDF5 (NcML) # Add more types here file.auxfiles.unspecifiedTypes=Other Auxiliary Files +dataset.version.versionNote.addEdit=Version Note +dataset.version.versionNote.title=The reason this version was created +dataset.versionNote.header=Add/Edit a Version Note +dataset.versionNote.tip=Enter the reason this version was created. To learn more about Version Notes, visit the Version Notes section of the User Guide. + # dataset-widgets.xhtml dataset.widgets.title=Dataset Thumbnail + Widgets dataset.widgets.notPublished.why.header=Why Use Widgets? diff --git a/src/main/resources/db/migration/V6.5.0.9.sql b/src/main/resources/db/migration/V6.5.0.9.sql new file mode 100644 index 00000000000..b653c10b638 --- /dev/null +++ b/src/main/resources/db/migration/V6.5.0.9.sql @@ -0,0 +1,21 @@ +-- Add deaccessionnote column +-- + +ALTER TABLE datasetversion ADD COLUMN IF NOT EXISTS deaccessionnote VARCHAR(1000); + +-- Move/merge archivenote contents and remove archivenote column (on existing DBs that have this column) +DO $$ +BEGIN +IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'datasetversion' AND COLUMN_NAME = 'archivenote') THEN +UPDATE datasetversion set deaccessionlink = CONCAT_WS(' ', deaccessionlink, archivenote); +ALTER TABLE datasetversion DROP COLUMN archivenote; + +-- Update deaccessionnote for existing datasetversions +-- Only do this once - if archivenote hasn't been deleted is a convenient trigger + +UPDATE datasetversion set deaccessionnote = versionnote; +UPDATE datasetversion set versionnote = null; + +END IF; +END +$$ \ No newline at end of file diff --git a/src/main/webapp/dataset-versions.xhtml b/src/main/webapp/dataset-versions.xhtml index 05574612e52..1bbeff0fb86 100644 --- a/src/main/webapp/dataset-versions.xhtml +++ b/src/main/webapp/dataset-versions.xhtml @@ -120,7 +120,11 @@ #{bundle['file.dataFilesTab.versions.description.firstPublished']} - #{bundle['file.dataFilesTab.versions.description.deaccessionedReason']} #{versionTab.versionNote} #{bundle['file.dataFilesTab.versions.description.beAccessedAt']} #{versionTab.archiveNote} + #{bundle['file.dataFilesTab.versions.description.deaccessionedReason']} #{versionTab.deaccessionNote} + #{bundle['file.dataFilesTab.versions.description.beAccessedAt']} + #{versionTab.deaccessionLink} + + + + + + +
+
+ +
diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 51a5e265538..560315378e7 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -103,6 +103,9 @@ + + + @@ -627,9 +630,12 @@
#{bundle['dataset.deaccession.reason']}
-

#{DatasetPage.workingVersion.versionNote}

- -

#{bundle['dataset.beAccessedAt']} #{DatasetPage.workingVersion.archiveNote}

+

#{DatasetPage.workingVersion.deaccessionNote}

+ +

#{bundle['dataset.beAccessedAt']} + #{DatasetPage.workingVersion.deaccessionLink} + +

@@ -983,6 +989,7 @@ + @@ -1964,6 +1971,16 @@ + +
+ + +
+

#{bundle['dataset.publish.terms.help.tip']}

@@ -2021,7 +2038,6 @@
-

#{bundle['dataset.rejectMessage']} #{disableReasonField ? '':bundle['dataset.rejectMessageReason']} @@ -2051,6 +2067,33 @@ + + +

+ + + + +

+
+ + +
+
+ + +
+ +
diff --git a/src/main/webapp/file-versions.xhtml b/src/main/webapp/file-versions.xhtml index f7f259ce2e0..fd457e137c2 100644 --- a/src/main/webapp/file-versions.xhtml +++ b/src/main/webapp/file-versions.xhtml @@ -126,7 +126,7 @@
+ value="#{bundle['file.dataFilesTab.versions.description.deaccessionedReason']} #{versionTab.datasetVersion.deaccessionNote}" escape="false"/>
diff --git a/src/main/webapp/file.xhtml b/src/main/webapp/file.xhtml index 835764d9cf5..925daa3feb4 100644 --- a/src/main/webapp/file.xhtml +++ b/src/main/webapp/file.xhtml @@ -326,9 +326,13 @@
#{bundle['dataset.deaccession.reason']}
-

#{FilePage.fileMetadata.datasetVersion.versionNote}

- -

#{bundle['dataset.beAccessedAt']} #{FilePage.fileMetadata.datasetVersion.archiveNote}

+

#{FilePage.fileMetadata.datasetVersion.deaccessionNote}

+ +

#{bundle['dataset.beAccessedAt']} + #{FilePage.fileMetadata.datasetVersion.deaccessionLink} + + +

diff --git a/src/main/webapp/resources/css/structure.css b/src/main/webapp/resources/css/structure.css index 61749faf7c9..4dba29e0a6e 100644 --- a/src/main/webapp/resources/css/structure.css +++ b/src/main/webapp/resources/css/structure.css @@ -608,7 +608,7 @@ div[id$='roleDisplay'] span.label, div[id$='roleDetails'] span.label {display:in /* RESPONSIVE ORDER HACK BOOTSTRAP 3 https://stackoverflow.com/a/24834574 */ #dataset-colorder-block {height: 50px;} @media(min-width:992px){ - #dataset-summary-metadata, #deaccession-reason-block {margin-top: -50px;} + #dataset-colorder-block + div {margin-top: -50px;} } .bg-citation {background:#ECF6FB; border: 0;} @@ -918,7 +918,16 @@ div.dvnDifferanceTable > div.ui-datatable-tablewrapper > table > tbody > tr, div div.dvnDifferanceTable > div.ui-datatable-tablewrapper > table > tbody > tr, div.dvnDifferanceTable > div.ui-datatable-tablewrapper > table > tbody > tr > td:last-child {border-right-width:0;} div.dvnDifferanceTable div.ui-datatable-tablewrapper table td.versionValue {width:30%;} div.dvnDifferanceTable div.ui-datatable-tablewrapper table td.versionDetails {width:35%;} -div.dvnDifferanceTable .diffDetailBlock {display:block;} +div.dvnDifferanceTable .diffDetailBlock { + display:block; + word-break:break-all; +} + +div.dvnDifferanceTable .versionValue { + word-break:break-all; +} +div[id$="versionsTable"] tbody {word-break:break-word;} + /* DATATABLE + DROPDOWN BUTTON + OVERFLOW VISIBLE */ thead.ui-datatable-scrollable-theadclone {display:none} 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 5d35db9af62..51b5d997992 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -53,7 +53,7 @@ import java.util.*; import java.util.logging.Logger; -import static edu.harvard.iq.dataverse.DatasetVersion.ARCHIVE_NOTE_MAX_LENGTH; +import static edu.harvard.iq.dataverse.DatasetVersion.DEACCESSION_LINK_MAX_LENGTH; import static edu.harvard.iq.dataverse.api.ApiConstants.*; import static edu.harvard.iq.dataverse.api.UtilIT.API_TOKEN_HTTP_HEADER; import static edu.harvard.iq.dataverse.api.UtilIT.equalToCI; @@ -5053,8 +5053,8 @@ public void deaccessionDataset() { Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); - // Test that a bad request error is received when the forward URL exceeds ARCHIVE_NOTE_MAX_LENGTH - String testInvalidDeaccessionForwardURL = RandomStringUtils.randomAlphabetic(ARCHIVE_NOTE_MAX_LENGTH + 1); + // Test that a bad request error is received when the forward URL exceeds DEACCESSION_LINK_MAX_LENGTH + String testInvalidDeaccessionForwardURL = RandomStringUtils.randomAlphabetic(DEACCESSION_LINK_MAX_LENGTH + 1); deaccessionDatasetResponse = UtilIT.deaccessionDataset(datasetId, DS_VERSION_LATEST_PUBLISHED, testDeaccessionReason, testInvalidDeaccessionForwardURL, apiToken); deaccessionDatasetResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode())