From bf0f63e0519f3be298086880f8ad07cbcd6aafb6 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 15 Aug 2025 13:06:09 -0400 Subject: [PATCH 01/29] add "review" dataset type #11747 --- .../11747-review-dataset-type.md | 3 + doc/sphinx-guides/source/api/native-api.rst | 30 +- .../source/user/dataset-management.rst | 4 +- .../harvard/iq/dataverse/DataCitation.java | 3 + .../iq/dataverse/dataset/DatasetType.java | 1 + .../pidproviders/doi/XmlMetadataTemplate.java | 2 + src/main/java/propertyFiles/Bundle.properties | 1 + .../iq/dataverse/api/DatasetTypesIT.java | 290 ++++++++++++++++++ 8 files changed, 327 insertions(+), 7 deletions(-) create mode 100644 doc/release-notes/11747-review-dataset-type.md diff --git a/doc/release-notes/11747-review-dataset-type.md b/doc/release-notes/11747-review-dataset-type.md new file mode 100644 index 00000000000..aa05d48dc66 --- /dev/null +++ b/doc/release-notes/11747-review-dataset-type.md @@ -0,0 +1,3 @@ +### New Dataset Type: Review + +A new, experimental dataset type called "review" has been added. When this type is published, it will be sent to DataCite as "Other" for resourceTypeGeneral. See #11747. diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index ada3ae5a8af..747df73a712 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -970,8 +970,8 @@ You should expect an HTTP 200 ("OK") response and JSON indicating the database I .. _api-create-dataset-with-type: -Create a Dataset with a Dataset Type (Software, etc.) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Create a Dataset with a Dataset Type (Software, Workflow, Review, etc.) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, datasets are given the type "dataset" but if your installation had added additional types (see :ref:`api-add-dataset-type`), you can specify the type. @@ -1025,8 +1025,8 @@ Before calling the API, make sure the data files referenced by the ``POST``\ ed .. _import-dataset-with-type: -Import a Dataset with a Dataset Type (Software, etc.) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Import a Dataset with a Dataset Type (Software, Workflow, Review, etc.) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, datasets are given the type "dataset" but if your installation had added additional types (see :ref:`api-add-dataset-type`), you can specify the type. @@ -3925,7 +3925,27 @@ The fully expanded example above (without environment variables) looks like this Add Dataset Type ^^^^^^^^^^^^^^^^ -Note: Before you add any types of your own, there should be a single type called "dataset". If you add "software" or "workflow", these types will be sent to DataCite (if you use DataCite). Otherwise, the only functionality you gain currently from adding types is an entry in the "Dataset Type" facet but be advised that if you add a type other than "software" or "workflow", you will need to add your new type to your Bundle.properties file for it to appear in Title Case rather than lower case in the "Dataset Type" facet. +Note: Before you add any types of your own, there should be a single type called "dataset". + +Adding certain dataset types will result in a value other than "Dataset" being sent to DataCite (if you use DataCite) as shown in the table below. + +.. list-table:: Values sent to DataCite for resourceTypeGeneral by Dataset Type + :header-rows: 1 + :stub-columns: 1 + :align: left + + * - Dataset Type + - Value sent to DataCite + * - dataset + - Dataset + * - software + - Software + * - workflow + - Workflow + * - review + - Other + +Other than sending a different resourceTypeGeneral to DataCite, the only functionality you gain currently from adding types is an entry in the "Dataset Type" facet but be advised that if you add a type other than "software", "workflow", or "review", you will need to add your new type to your Bundle.properties file for it to appear in Title Case rather than lower case in the "Dataset Type" facet. With all that said, we'll add a "software" type in the example below. This API endpoint is superuser only. The "name" of a type cannot be only digits. Note that this endpoint also allows you to add metadata blocks and available licenses for your new dataset type by adding "linkedMetadataBlocks" and/or "availableLicenses" arrays to your JSON. diff --git a/doc/sphinx-guides/source/user/dataset-management.rst b/doc/sphinx-guides/source/user/dataset-management.rst index d73459969ce..8b443520cc7 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -824,11 +824,11 @@ Dataset Types .. note:: Development of the dataset types feature is ongoing. Please see https://github.com/IQSS/dataverse-pm/issues/307 for details. -Out of the box, all datasets have a dataset type of "dataset". Superusers can add additional types such as "software" or "workflow" using the :ref:`api-add-dataset-type` API endpoint. +Out of the box, all datasets have a dataset type of "dataset". Superusers can add additional types such as "software", "workflow", or "review" using the :ref:`api-add-dataset-type` API endpoint. Once more than one type appears in search results, a facet called "Dataset Type" will appear allowing you to filter down to a certain type. -If your installation is configured to use DataCite as a persistent ID (PID) provider, the appropriate type ("Dataset", "Software", "Workflow") will be sent to DataCite when the dataset is published for those three types. +If your installation is configured to use DataCite as a persistent ID (PID) provider, the appropriate type ("Dataset", "Software", "Workflow", "Review") will be sent to DataCite when the dataset is published for those types. Currently, specifying a type for a dataset can only be done via API and only when the dataset is created. The type can't currently be changed afterward. For details, see the following sections of the API guide: diff --git a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java index 30d9928f59a..1f6380f020f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java @@ -740,8 +740,11 @@ public Map getDataCiteMetadata() { public JsonObject getCSLJsonFormat() { CSLItemDataBuilder itemBuilder = new CSLItemDataBuilder(); + // TODO consider making this a switch if (type.equals(DatasetType.DATASET_TYPE_SOFTWARE)) { itemBuilder.type(CSLType.SOFTWARE); + } else if (type.equals(DatasetType.DATASET_TYPE_REVIEW)) { + itemBuilder.type(CSLType.REVIEW); } else { itemBuilder.type(CSLType.DATASET); } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index c55324f66e3..e437af472e2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -39,6 +39,7 @@ public class DatasetType implements Serializable { public static final String DATASET_TYPE_DATASET = "dataset"; public static final String DATASET_TYPE_SOFTWARE = "software"; public static final String DATASET_TYPE_WORKFLOW = "workflow"; + public static final String DATASET_TYPE_REVIEW = "review"; public static final String DEFAULT_DATASET_TYPE = DATASET_TYPE_DATASET; @Id 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 9ebb346baf8..efc81d4179f 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 @@ -842,6 +842,8 @@ private void writeResourceType(XMLStreamWriter xmlw, DvObject dvObject) throws X case DatasetType.DATASET_TYPE_DATASET -> "Dataset"; case DatasetType.DATASET_TYPE_SOFTWARE -> "Software"; case DatasetType.DATASET_TYPE_WORKFLOW -> "Workflow"; + // "Other" for now but we might ask DataCite to support https://schema.org/CriticReview + case DatasetType.DATASET_TYPE_REVIEW -> "Other"; default -> "Dataset"; }; } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 1016bb96788..0bbf1758dba 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -13,6 +13,7 @@ passwd=Password dataset=Dataset software=Software workflow=Workflow +review=Review # END dataset types datasets=Datasets newDataset=New Dataset diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 205725822ff..924950f5e8c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -1,6 +1,8 @@ package edu.harvard.iq.dataverse.api; +import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_LATEST_PUBLISHED; import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.util.StringUtil; import io.restassured.RestAssured; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; @@ -37,6 +39,7 @@ public static void setUpClass() { UtilIT.setSuperuserStatus(username, true).then().assertThat().statusCode(OK.getStatusCode()); ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_SOFTWARE, apiToken); + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, apiToken); ensureDatasetTypeIsPresent(INSTRUMENT, apiToken); } @@ -713,4 +716,291 @@ public void testCreateDatasetWithCustomType() { } + /** + * In this test, there are two users: one who publishes a dataset and + * another who publishes a review of that dataset. + */ + @Test + public void testCreateReview() { + Response createDatasetDepositor = UtilIT.createRandomUser(); + createDatasetDepositor.then().assertThat().statusCode(OK.getStatusCode()); + String apiTokenDepositor = UtilIT.getApiTokenFromResponse(createDatasetDepositor); + + Response createCollectionOfData = UtilIT.createRandomDataverse(apiTokenDepositor); + createCollectionOfData.then().assertThat().statusCode(CREATED.getStatusCode()); + String collectionOfDataAlias = UtilIT.getAliasFromResponse(createCollectionOfData); + + Response createDataset = UtilIT.createRandomDatasetViaNativeApi(collectionOfDataAlias, apiTokenDepositor); + createDataset.then().assertThat().statusCode(CREATED.getStatusCode()); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDataset); + String datasetPid = JsonPath.from(createDataset.getBody().asString()).getString("data.persistentId"); + + UtilIT.publishDataverseViaNativeApi(collectionOfDataAlias, apiTokenDepositor).then().assertThat().statusCode(OK.getStatusCode()); + UtilIT.publishDatasetViaNativeApi(datasetPid, "major", apiTokenDepositor); + + Response createReviewer = UtilIT.createRandomUser(); + createReviewer.then().assertThat().statusCode(OK.getStatusCode()); + String apiTokenReviewer = UtilIT.getApiTokenFromResponse(createReviewer); + + // We assume the reviewer wants their own collection for reviews. + Response createCollectionOfReviews = UtilIT.createRandomDataverse(apiTokenReviewer); + createCollectionOfReviews.then().assertThat().statusCode(CREATED.getStatusCode()); + String collectionOfReviewsAlias = UtilIT.getAliasFromResponse(createCollectionOfReviews); + + Response datasetMetadataResponse = UtilIT.nativeGet(datasetId, apiTokenReviewer); + datasetMetadataResponse.then().assertThat().statusCode(OK.getStatusCode()); + datasetMetadataResponse.prettyPrint(); + JsonPath datasetMetadata = JsonPath.from(datasetMetadataResponse.body().asString()); + String datasetTitle = datasetMetadata.getString("data.latestVersion.metadataBlocks.citation.fields[0].value"); + String datasetPidUrl = datasetMetadata.getString("data.persistentUrl"); + String datasetPidProtocol = datasetMetadata.getString("data.protocol"); + String datasetPidAuthority = datasetMetadata.getString("data.authority"); + String datasetPidSeparator = datasetMetadata.getString("data.separator"); + String datasetPidIdentifier = datasetMetadata.getString("data.identifier"); + String datasetPidWithoutProtocol = datasetPidAuthority + datasetPidSeparator + datasetPidIdentifier; + + Response getCitation = UtilIT.getDatasetVersionCitation(datasetId, DS_VERSION_LATEST_PUBLISHED, false, apiTokenReviewer); + getCitation.prettyPrint(); + getCitation.then().assertThat().statusCode(OK.getStatusCode()); + String datasetCitationHtml = JsonPath.from(getCitation.getBody().asString()).getString("data.message"); + String datasetCitationText = StringUtil.html2text(datasetCitationHtml); + + JsonObjectBuilder jsonForCreatingReview = Json.createObjectBuilder() + /** + * See above where this type is added to the installation and + * therefore available for use. + */ + .add("datasetType", DatasetType.DATASET_TYPE_REVIEW) + .add("datasetVersion", Json.createObjectBuilder() + .add("license", Json.createObjectBuilder() + .add("name", "CC0 1.0") + .add("uri", "http://creativecommons.org/publicdomain/zero/1.0") + ) + .add("metadataBlocks", Json.createObjectBuilder() + .add("citation", Json.createObjectBuilder() + .add("fields", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("typeName", "title") + .add("value", "Review of " + datasetTitle) + .add("typeClass", "primitive") + .add("multiple", false) + ) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("authorName", + Json.createObjectBuilder() + .add("value", "Simpson, Homer") + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "authorName")) + ) + ) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "author") + ) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("datasetContactEmail", + Json.createObjectBuilder() + .add("value", "hsimpson@mailinator.com") + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "datasetContactEmail")) + ) + ) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "datasetContact") + ) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("dsDescriptionValue", + Json.createObjectBuilder() + .add("value", "This is a review of a dataset.") + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "dsDescriptionValue")) + ) + ) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "dsDescription") + ) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add("Other") + ) + .add("typeClass", "controlledVocabulary") + .add("multiple", true) + .add("typeName", "subject") + ) + /** + * Related Dataset. + * + * The tooltip for Related + * Dataset says "Information, + * such as a persistent ID or + * citation, about a related + * dataset, such as previous + * research on the Dataset's + * subject". + * + * For now we'll add multiple + * forms so we can discuss and + * decide which one to use. + * + * Also, we are well aware that + * there is a custom metadata + * block called + * "relatedDatasetsV2" at + * https://github.com/vera/related-datasets-cvoc + * that we hope to play with + * soon. + */ + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(datasetPid) + .add(datasetPidUrl) + .add(datasetCitationText) + .add(datasetCitationHtml) + ) + .add("typeClass", "primitive") + .add("multiple", true) + .add("typeName", "relatedDatasets") + ) + /** + * Related Publication. + * + * Related Dataset is more + * appropriate than Related + * Publication but we're adding + * Related Publication (twice, + * with plain text and HTML + * citations) because unlike + * Related Dataset, Related + * Publication lets us send + * publicationRelationType, + * which gets sent to DataCite + * as "References" or "Cites" or + * whatever. + * + * (Ideally, we'd send "Reviews" + * but we get this error: + * + * "Error parsing Json: Invalid + * controlled vocabulary in + * compound field Value + * 'Reviews' does not exist in + * type + * 'publicationRelationType';") + */ + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + // Plain text citation version + .add(Json.createObjectBuilder() + .add("publicationRelationType", + Json.createObjectBuilder() + .add("value", "References") + // .add("value", "Reviews") + .add("typeClass", "controlledVocabulary") + .add("multiple", false) + .add("typeName", "publicationRelationType") + ) + .add("publicationCitation", + Json.createObjectBuilder() + .add("value", datasetCitationHtml) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "publicationCitation") + ) + .add("publicationIDType", + Json.createObjectBuilder() + .add("value", datasetPidProtocol) + .add("typeClass", "controlledVocabulary") + .add("multiple", false) + .add("typeName", "publicationIDType") + ) + .add("publicationIDNumber", + Json.createObjectBuilder() + .add("value", datasetPidWithoutProtocol) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "publicationIDNumber") + ) + .add("publicationURL", + Json.createObjectBuilder() + .add("value", datasetPidUrl) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "publicationURL") + ) + ) + // HTML citation version + .add(Json.createObjectBuilder() + .add("publicationRelationType", + Json.createObjectBuilder() + .add("value", "References") + // .add("value", "Reviews") + .add("typeClass", "controlledVocabulary") + .add("multiple", false) + .add("typeName", "publicationRelationType") + ) + .add("publicationCitation", + Json.createObjectBuilder() + .add("value", datasetCitationText) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "publicationCitation") + ) + .add("publicationIDType", + Json.createObjectBuilder() + .add("value", datasetPidProtocol) + .add("typeClass", "controlledVocabulary") + .add("multiple", false) + .add("typeName", "publicationIDType") + ) + .add("publicationIDNumber", + Json.createObjectBuilder() + .add("value", datasetPidWithoutProtocol) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "publicationIDNumber") + ) + .add("publicationURL", + Json.createObjectBuilder() + .add("value", datasetPidUrl) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "publicationURL") + ) + ) + ) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "publication") + ) + ) + ) + )); + + Response createReview = UtilIT.createDataset(collectionOfReviewsAlias, jsonForCreatingReview, apiTokenReviewer); + createReview.prettyPrint(); + createReview.then().assertThat().statusCode(CREATED.getStatusCode()); + Integer reviewId = UtilIT.getDatasetIdFromResponse(createReview); + String reviewPid = JsonPath.from(createReview.getBody().asString()).getString("data.persistentId"); + + Response getReviewMetadata = UtilIT.nativeGet(reviewId, apiTokenReviewer); + getReviewMetadata.prettyPrint(); + getReviewMetadata.then().assertThat().statusCode(OK.getStatusCode()); + String datasetType = JsonPath.from(getReviewMetadata.getBody().asString()).getString("data.datasetType"); + assertEquals("review", datasetType); + + UtilIT.publishDataverseViaNativeApi(collectionOfReviewsAlias, apiTokenReviewer).then().assertThat().statusCode(OK.getStatusCode()); + UtilIT.publishDatasetViaNativeApi(reviewPid, "major", apiTokenReviewer).then().assertThat().statusCode(OK.getStatusCode()); + } + } From 0a48cc8017bdf7b6b4967cfe42cd67ffbb26b219 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 26 Aug 2025 10:12:42 -0400 Subject: [PATCH 02/29] only populate Related Dataset and only HTML version #11747 Also, rework Javadoc comment. --- .../iq/dataverse/api/DatasetTypesIT.java | 154 ++---------------- 1 file changed, 17 insertions(+), 137 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 924950f5e8c..42926aad758 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -765,6 +765,23 @@ public void testCreateReview() { String datasetCitationHtml = JsonPath.from(getCitation.getBody().asString()).getString("data.message"); String datasetCitationText = StringUtil.html2text(datasetCitationHtml); + /** + * We are added the HTML version of a Related Dataset. We like the HTML + * version because both JSF and the SPA render the DOI link as a + * clickable link. + * + * The tooltip for Related Dataset says "Information, such as a + * persistent ID or citation, about a related dataset, such as previous + * research on the Dataset's subject". + * + * We are aware that there is a custom metadata block called + * "relatedDatasetsV2" at https://github.com/vera/related-datasets-cvoc + * that we have been playing with. We especially like that relationships + * can be expressed between the current object (a review) and the + * related dataset. This is simlar to how "Related Publication" works. + * See also discussion at + * https://dataverse.zulipchat.com/#narrow/channel/379673-dev/topic/Improved.20.22Related.20datasets.22/near/534969036 + */ JsonObjectBuilder jsonForCreatingReview = Json.createObjectBuilder() /** * See above where this type is added to the installation and @@ -838,151 +855,14 @@ public void testCreateReview() { .add("multiple", true) .add("typeName", "subject") ) - /** - * Related Dataset. - * - * The tooltip for Related - * Dataset says "Information, - * such as a persistent ID or - * citation, about a related - * dataset, such as previous - * research on the Dataset's - * subject". - * - * For now we'll add multiple - * forms so we can discuss and - * decide which one to use. - * - * Also, we are well aware that - * there is a custom metadata - * block called - * "relatedDatasetsV2" at - * https://github.com/vera/related-datasets-cvoc - * that we hope to play with - * soon. - */ .add(Json.createObjectBuilder() .add("value", Json.createArrayBuilder() - .add(datasetPid) - .add(datasetPidUrl) - .add(datasetCitationText) .add(datasetCitationHtml) ) .add("typeClass", "primitive") .add("multiple", true) .add("typeName", "relatedDatasets") ) - /** - * Related Publication. - * - * Related Dataset is more - * appropriate than Related - * Publication but we're adding - * Related Publication (twice, - * with plain text and HTML - * citations) because unlike - * Related Dataset, Related - * Publication lets us send - * publicationRelationType, - * which gets sent to DataCite - * as "References" or "Cites" or - * whatever. - * - * (Ideally, we'd send "Reviews" - * but we get this error: - * - * "Error parsing Json: Invalid - * controlled vocabulary in - * compound field Value - * 'Reviews' does not exist in - * type - * 'publicationRelationType';") - */ - .add(Json.createObjectBuilder() - .add("value", Json.createArrayBuilder() - // Plain text citation version - .add(Json.createObjectBuilder() - .add("publicationRelationType", - Json.createObjectBuilder() - .add("value", "References") - // .add("value", "Reviews") - .add("typeClass", "controlledVocabulary") - .add("multiple", false) - .add("typeName", "publicationRelationType") - ) - .add("publicationCitation", - Json.createObjectBuilder() - .add("value", datasetCitationHtml) - .add("typeClass", "primitive") - .add("multiple", false) - .add("typeName", "publicationCitation") - ) - .add("publicationIDType", - Json.createObjectBuilder() - .add("value", datasetPidProtocol) - .add("typeClass", "controlledVocabulary") - .add("multiple", false) - .add("typeName", "publicationIDType") - ) - .add("publicationIDNumber", - Json.createObjectBuilder() - .add("value", datasetPidWithoutProtocol) - .add("typeClass", "primitive") - .add("multiple", false) - .add("typeName", "publicationIDNumber") - ) - .add("publicationURL", - Json.createObjectBuilder() - .add("value", datasetPidUrl) - .add("typeClass", "primitive") - .add("multiple", false) - .add("typeName", "publicationURL") - ) - ) - // HTML citation version - .add(Json.createObjectBuilder() - .add("publicationRelationType", - Json.createObjectBuilder() - .add("value", "References") - // .add("value", "Reviews") - .add("typeClass", "controlledVocabulary") - .add("multiple", false) - .add("typeName", "publicationRelationType") - ) - .add("publicationCitation", - Json.createObjectBuilder() - .add("value", datasetCitationText) - .add("typeClass", "primitive") - .add("multiple", false) - .add("typeName", "publicationCitation") - ) - .add("publicationIDType", - Json.createObjectBuilder() - .add("value", datasetPidProtocol) - .add("typeClass", "controlledVocabulary") - .add("multiple", false) - .add("typeName", "publicationIDType") - ) - .add("publicationIDNumber", - Json.createObjectBuilder() - .add("value", datasetPidWithoutProtocol) - .add("typeClass", "primitive") - .add("multiple", false) - .add("typeName", "publicationIDNumber") - ) - .add("publicationURL", - Json.createObjectBuilder() - .add("value", datasetPidUrl) - .add("typeClass", "primitive") - .add("multiple", false) - .add("typeName", "publicationURL") - ) - ) - ) - .add("typeClass", "compound") - .add("multiple", true) - .add("typeName", "publication") - ) ) ) )); From d2879078257926632db025d6e9e10642b751617a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 26 Aug 2025 10:24:28 -0400 Subject: [PATCH 03/29] add blocks for Trusted Data and Repo Characteristics #11747 --- doc/sphinx-guides/source/user/appendix.rst | 2 + .../repositorycharacteristics.tsv | 8 +++ .../trusteddatadimensionsintensities.tsv | 61 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 scripts/api/data/metadatablocks/repositorycharacteristics.tsv create mode 100644 scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv diff --git a/doc/sphinx-guides/source/user/appendix.rst b/doc/sphinx-guides/source/user/appendix.rst index d1c46a93fdf..99a01fa41b0 100755 --- a/doc/sphinx-guides/source/user/appendix.rst +++ b/doc/sphinx-guides/source/user/appendix.rst @@ -43,6 +43,8 @@ Unlike supported metadata, experimental metadata is not enabled by default in a - Computational Workflow Metadata (`see .tsv `__): adapted from `Bioschemas Computational Workflow Profile, version 1.0 `__ and `Codemeta `__. - Archival Metadata (`see .tsv `__): Enables repositories to register metadata relating to the potential archiving of the dataset at a depositor archive, whether that be your own institutional archive or an external archive, i.e. a historical archive. - Local Contexts Metadata (`see .tsv `__): Supports integration with the `Local Contexts `__ platform, enabling the use of Traditional Knowledge and Biocultural Labels, and Notices. For more information on setup and configuration, see :doc:`../installation/localcontexts`. +- Trusted Data Dimensions and Intensities (`see .tsv `__): Enables repositories to indicate dimensions of trust. +- Repository Characteristics (`see .tsv `__): Details related to the security, sustainability, and certifications of the repository. Please note: these custom metadata schemas are not included in the Solr schema for indexing by default, you will need to add them as necessary for your custom metadata blocks. See "Update the Solr Schema" in :doc:`../admin/metadatacustomization`. diff --git a/scripts/api/data/metadatablocks/repositorycharacteristics.tsv b/scripts/api/data/metadatablocks/repositorycharacteristics.tsv new file mode 100644 index 00000000000..3951ebb146f --- /dev/null +++ b/scripts/api/data/metadatablocks/repositorycharacteristics.tsv @@ -0,0 +1,8 @@ +#metadataBlock name dataverseAlias displayName blockURI + repositorycharacteristics Repository Characteristics +#datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI + repositoryOrHosting Repository or Hosting Where and how the data are stored (e.g. institutional repository, cloud) and the oversight and management policies that ensure ethical and transparent handling of the data textbox 0 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics + securityAndTechnicalSafeguards Security and Technical Safeguards The mechanisms that the repository or hosting platform uses to prevent tampering with or unauthorized access to the data textbox 1 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics + sustainabilityAndLongevity Sustainability and Longevity The strategies that the repository or hosting platform uses to support the long-term preservation and maintenance of the data textbox 2 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics + standardsCertification Standards Certification The certification of the repository or hosting platform (e.g. CoreTrustSeal) textbox 3 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics +#controlledVocabulary DatasetField Value identifier displayOrder \ No newline at end of file diff --git a/scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv b/scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv new file mode 100644 index 00000000000..44af02ee2be --- /dev/null +++ b/scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv @@ -0,0 +1,61 @@ +#metadataBlock name dataverseAlias displayName blockURI + trusteddatadimensionsintensities Trusted Data Dimensions and Intensities +#datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI + reviewTarget Review Target The type of research object reviewed text 0 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities + authorAndProvenance Author and Provenance The level of trust in the data creators and in other provenance information text 1 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities + integrityAndUsability Integrity and Usability The level of trust in the accuracy, completeness, and ease of use of the data text 2 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities + fitnessForScopeAndContextualRelevance Fitness for Scope and Contextual Relevance The level of trust in the suitability of the data for specific contexts, questions, or policy applications text 3 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities + licensingAndLegalClarity Licensing and Legal Clarity The level of trust in the explicitness of the data’s usage rights and their compliance with relevant laws and regulations text 4 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities + transparencyOfMethodsAndDocumentation Transparency of Methods and Documentation The level of trust in the clarity of the descriptions of data collection and processing methods text 5 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities + biasEquityAndRepresentativeness Bias, Equity, and Representativeness The level of trust in the inclusivity and fairness of the coverage of the data text 6 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities +#controlledVocabulary DatasetField Value identifier displayOrder + reviewTarget Audiovisual 0 + reviewTarget Award 1 + reviewTarget Book 2 + reviewTarget Book Chapter 3 + reviewTarget Collection 4 + reviewTarget Computational Notebook 5 + reviewTarget Conference Paper 6 + reviewTarget Conference Proceeding 7 + reviewTarget DataPaper 8 + reviewTarget Dataset 9 + reviewTarget Dissertation 10 + reviewTarget Event 11 + reviewTarget Image 12 + reviewTarget Interactive Resource 13 + reviewTarget Instrument 14 + reviewTarget Journal 15 + reviewTarget Journal Article 16 + reviewTarget Model 17 + reviewTarget Output Management Plan 18 + reviewTarget Peer Review 19 + reviewTarget Physical Object 20 + reviewTarget Preprint 21 + reviewTarget Project 22 + reviewTarget Report 23 + reviewTarget Service 24 + reviewTarget Software 25 + reviewTarget Sound 26 + reviewTarget Standard 27 + reviewTarget Study Registration 28 + reviewTarget Text 29 + reviewTarget Workflow 30 + reviewTarget Other 31 + authorAndProvenance Low 0 + authorAndProvenance Medium 1 + authorAndProvenance High 2 + integrityAndUsability Low 0 + integrityAndUsability Medium 1 + integrityAndUsability High 2 + fitnessForScopeAndContextualRelevance Low 0 + fitnessForScopeAndContextualRelevance Medium 1 + fitnessForScopeAndContextualRelevance High 2 + licensingAndLegalClarity Low 0 + licensingAndLegalClarity Medium 1 + licensingAndLegalClarity High 2 + transparencyOfMethodsAndDocumentation Low 0 + transparencyOfMethodsAndDocumentation Medium 1 + transparencyOfMethodsAndDocumentation High 2 + biasEquityAndRepresentativeness Low 0 + biasEquityAndRepresentativeness Medium 1 + biasEquityAndRepresentativeness High 2 \ No newline at end of file From c938a7a51d1de91f4b3acd2feb95865d763b59aa Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Tue, 2 Sep 2025 09:21:50 -0400 Subject: [PATCH 04/29] Adding properties files for two metadata blocks Adding properties files for the repositorycharacteristics.tsv and trusteddatadimensionsintensities.tsv metadata blocks --- .../repositorycharacteristics.properties | 15 ++++ ...rusteddatadimensionsintensities.properties | 74 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/main/java/propertyFiles/repositorycharacteristics.properties create mode 100644 src/main/java/propertyFiles/trusteddatadimensionsintensities.properties diff --git a/src/main/java/propertyFiles/repositorycharacteristics.properties b/src/main/java/propertyFiles/repositorycharacteristics.properties new file mode 100644 index 00000000000..6d07ae3cdac --- /dev/null +++ b/src/main/java/propertyFiles/repositorycharacteristics.properties @@ -0,0 +1,15 @@ +metadatablock.name=repositorycharacteristics +metadatablock.displayName=Repository Characteristics +metadatablock.displayFacet= +datasetfieldtype.repositoryOrHosting.title=Repository or Hosting +datasetfieldtype.securityAndTechnicalSafeguards.title=Security and Technical Safeguards +datasetfieldtype.sustainabilityAndLongevity.title=Sustainability and Longevity +datasetfieldtype.standardsCertification.title=Standards Certification +datasetfieldtype.repositoryOrHosting.description=Where and how the data are stored (e.g. institutional repository, cloud) and the oversight and management policies that ensure ethical and transparent handling of the data +datasetfieldtype.securityAndTechnicalSafeguards.description=The mechanisms that the repository or hosting platform uses to prevent tampering with or unauthorized access to the data +datasetfieldtype.sustainabilityAndLongevity.description=The strategies that the repository or hosting platform uses to support the long-term preservation and maintenance of the data +datasetfieldtype.standardsCertification.description=The certification of the repository or hosting platform (e.g. CoreTrustSeal) +datasetfieldtype.repositoryOrHosting.watermark= +datasetfieldtype.securityAndTechnicalSafeguards.watermark= +datasetfieldtype.sustainabilityAndLongevity.watermark= +datasetfieldtype.standardsCertification.watermark= \ No newline at end of file diff --git a/src/main/java/propertyFiles/trusteddatadimensionsintensities.properties b/src/main/java/propertyFiles/trusteddatadimensionsintensities.properties new file mode 100644 index 00000000000..a06cea18234 --- /dev/null +++ b/src/main/java/propertyFiles/trusteddatadimensionsintensities.properties @@ -0,0 +1,74 @@ +metadatablock.name=trusteddatadimensionsintensities +metadatablock.displayName=Trusted Data Dimensions and Intensities +metadatablock.displayFacet= +datasetfieldtype.reviewTarget.title=Review Target +datasetfieldtype.authorAndProvenance.title=Author and Provenance +datasetfieldtype.integrityAndUsability.title=Integrity and Usability +datasetfieldtype.fitnessForScopeAndContextualRelevance.title=Fitness for Scope and Contextual Relevance +datasetfieldtype.licensingAndLegalClarity.title=Licensing and Legal Clarity +datasetfieldtype.transparencyOfMethodsAndDocumentation.title=Transparency of Methods and Documentation +datasetfieldtype.biasEquityAndRepresentativeness.title=Bias, Equity, and Representativeness +datasetfieldtype.reviewTarget.description=The type of research object reviewed +datasetfieldtype.authorAndProvenance.description=The level of trust in the data creators and in other provenance information +datasetfieldtype.integrityAndUsability.description=The level of trust in the accuracy, completeness, and ease of use of the data +datasetfieldtype.fitnessForScopeAndContextualRelevance.description=The level of trust in the suitability of the data for specific contexts, questions, or policy applications +datasetfieldtype.licensingAndLegalClarity.description=The level of trust in the explicitness of the data’s usage rights and their compliance with relevant laws and regulations +datasetfieldtype.transparencyOfMethodsAndDocumentation.description=The level of trust in the clarity of the descriptions of data collection and processing methods +datasetfieldtype.biasEquityAndRepresentativeness.description=The level of trust in the inclusivity and fairness of the coverage of the data +datasetfieldtype.reviewTarget.watermark= +datasetfieldtype.authorAndProvenance.watermark= +datasetfieldtype.integrityAndUsability.watermark= +datasetfieldtype.fitnessForScopeAndContextualRelevance.watermark= +datasetfieldtype.licensingAndLegalClarity.watermark= +datasetfieldtype.transparencyOfMethodsAndDocumentation.watermark= +datasetfieldtype.biasEquityAndRepresentativeness.watermark= +controlledvocabulary.reviewTarget.audiovisual=Audiovisual +controlledvocabulary.reviewTarget.award=Award +controlledvocabulary.reviewTarget.book=Book +controlledvocabulary.reviewTarget.book_chapter=Book Chapter +controlledvocabulary.reviewTarget.collection=Collection +controlledvocabulary.reviewTarget.computational_notebook=Computational Notebook +controlledvocabulary.reviewTarget.conference_paper=Conference Paper +controlledvocabulary.reviewTarget.conference_proceeding=Conference Proceeding +controlledvocabulary.reviewTarget.datapaper=DataPaper +controlledvocabulary.reviewTarget.dataset=Dataset +controlledvocabulary.reviewTarget.dissertation=Dissertation +controlledvocabulary.reviewTarget.event=Event +controlledvocabulary.reviewTarget.image=Image +controlledvocabulary.reviewTarget.interactive_resource=Interactive Resource +controlledvocabulary.reviewTarget.instrument=Instrument +controlledvocabulary.reviewTarget.journal=Journal +controlledvocabulary.reviewTarget.journal_article=Journal Article +controlledvocabulary.reviewTarget.model=Model +controlledvocabulary.reviewTarget.output_management_plan=Output Management Plan +controlledvocabulary.reviewTarget.peer_review=Peer Review +controlledvocabulary.reviewTarget.physical_object=Physical Object +controlledvocabulary.reviewTarget.preprint=Preprint +controlledvocabulary.reviewTarget.project=Project +controlledvocabulary.reviewTarget.report=Report +controlledvocabulary.reviewTarget.service=Service +controlledvocabulary.reviewTarget.software=Software +controlledvocabulary.reviewTarget.sound=Sound +controlledvocabulary.reviewTarget.standard=Standard +controlledvocabulary.reviewTarget.study_registration=Study Registration +controlledvocabulary.reviewTarget.text=Text +controlledvocabulary.reviewTarget.workflow=Workflow +controlledvocabulary.reviewTarget.other=Other +controlledvocabulary.authorAndProvenance.low=Low +controlledvocabulary.authorAndProvenance.medium=Medium +controlledvocabulary.authorAndProvenance.high=High +controlledvocabulary.integrityAndUsability.low=Low +controlledvocabulary.integrityAndUsability.medium=Medium +controlledvocabulary.integrityAndUsability.high=High +controlledvocabulary.fitnessForScopeAndContextualRelevance.low=Low +controlledvocabulary.fitnessForScopeAndContextualRelevance.medium=Medium +controlledvocabulary.fitnessForScopeAndContextualRelevance.high=High +controlledvocabulary.licensingAndLegalClarity.low=Low +controlledvocabulary.licensingAndLegalClarity.medium=Medium +controlledvocabulary.licensingAndLegalClarity.high=High +controlledvocabulary.transparencyOfMethodsAndDocumentation.low=Low +controlledvocabulary.transparencyOfMethodsAndDocumentation.medium=Medium +controlledvocabulary.transparencyOfMethodsAndDocumentation.high=High +controlledvocabulary.biasEquityAndRepresentativeness.low=Low +controlledvocabulary.biasEquityAndRepresentativeness.medium=Medium +controlledvocabulary.biasEquityAndRepresentativeness.high=High \ No newline at end of file From 1c7379390ffd5fd203dffd19fcc834bc879278d3 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 8 Oct 2025 12:55:40 -0400 Subject: [PATCH 05/29] add displayName for datasetType #11747 #11887 --- .../iq/dataverse/dataset/DatasetType.java | 18 ++++++++++++++++++ src/main/resources/db/migration/V6.8.0.1.sql | 6 ++++++ 2 files changed, 24 insertions(+) create mode 100644 src/main/resources/db/migration/V6.8.0.1.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index e437af472e2..36066afd577 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -46,10 +46,19 @@ public class DatasetType implements Serializable { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + /** + * Machine readable name to use via API. + */ // Any constraints? @Pattern regexp? @Column(nullable = false) private String name; + /** + * Human readable name to show in the UI. + */ + @Column(nullable = false) + private String displayName; + /** * The metadata blocks this dataset type is linked to. */ @@ -81,6 +90,14 @@ public void setName(String name) { this.name = name; } + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + public List getMetadataBlocks() { return metadataBlocks; } @@ -109,6 +126,7 @@ public JsonObjectBuilder toJson() { return Json.createObjectBuilder() .add("id", getId()) .add("name", getName()) + .add("displayName", getDisplayName()) .add("linkedMetadataBlocks", linkedMetadataBlocks) .add("availableLicenses", availableLicenses); } diff --git a/src/main/resources/db/migration/V6.8.0.1.sql b/src/main/resources/db/migration/V6.8.0.1.sql new file mode 100644 index 00000000000..352ce14f1fb --- /dev/null +++ b/src/main/resources/db/migration/V6.8.0.1.sql @@ -0,0 +1,6 @@ +-- Add displayname column to datasettype table +ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS displayname VARCHAR(255); +-- Set displayname for dataset +UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; +-- Make displayname required +ALTER TABLE datasettype ALTER COLUMN displayname SET NOT NULL; \ No newline at end of file From 9138524e40e62e9eb5b60e7866cac3d059e8cff0 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 8 Oct 2025 15:02:29 -0400 Subject: [PATCH 06/29] have displayname default to empty string #11747 This is so an earlier script (V6.3.0.3.sql) continues to work. It inserts a row without a displayName. --- .../java/edu/harvard/iq/dataverse/dataset/DatasetType.java | 2 +- src/main/resources/db/migration/V6.8.0.1.sql | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index 36066afd577..528132665fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -56,7 +56,7 @@ public class DatasetType implements Serializable { /** * Human readable name to show in the UI. */ - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "VARCHAR(255) DEFAULT ''") private String displayName; /** diff --git a/src/main/resources/db/migration/V6.8.0.1.sql b/src/main/resources/db/migration/V6.8.0.1.sql index 352ce14f1fb..158278843ab 100644 --- a/src/main/resources/db/migration/V6.8.0.1.sql +++ b/src/main/resources/db/migration/V6.8.0.1.sql @@ -1,6 +1,4 @@ -- Add displayname column to datasettype table -ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS displayname VARCHAR(255); +ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS displayname VARCHAR(255) NOT NULL DEFAULT ''; -- Set displayname for dataset -UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; --- Make displayname required -ALTER TABLE datasettype ALTER COLUMN displayname SET NOT NULL; \ No newline at end of file +UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; \ No newline at end of file From e6447b77ecc908fed1f3dd1e7a852f4bdce84b72 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 8 Oct 2025 16:29:30 -0400 Subject: [PATCH 07/29] when creating dataset types, allow displayName #11747 --- .../harvard/iq/dataverse/api/Datasets.java | 6 +++++ .../iq/dataverse/api/DatasetTypesIT.java | 25 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) 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 3fd490c38bc..1cbce4e1806 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -5732,6 +5732,7 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json } String nameIn = null; + String displayNameIn = null; JsonArrayBuilder datasetTypesAfter = Json.createArrayBuilder(); List metadataBlocksToSave = new ArrayList<>(); @@ -5740,6 +5741,7 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json try { JsonObject datasetTypeObj = JsonUtil.getJsonObject(jsonIn); nameIn = datasetTypeObj.getString("name"); + displayNameIn = datasetTypeObj.getString("displayName", null); JsonArray arr = datasetTypeObj.getJsonArray("linkedMetadataBlocks"); if (arr != null && !arr.isEmpty()) { @@ -5777,6 +5779,9 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json if (nameIn == null) { return error(BAD_REQUEST, "A name for the dataset type is required"); } + if (displayNameIn == null) { + return error(BAD_REQUEST, "A displayName for the dataset type is required"); + } if (StringUtils.isNumeric(nameIn)) { // getDatasetTypes supports id or name so we don't want a names that looks like an id return error(BAD_REQUEST, "The name of the type cannot be only digits."); @@ -5785,6 +5790,7 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json try { DatasetType datasetType = new DatasetType(); datasetType.setName(nameIn); + datasetType.setDisplayName(displayNameIn); datasetType.setMetadataBlocks(metadataBlocksToSave); datasetType.setLicenses(licensesToSave); DatasetType saved = datasetTypeSvc.save(datasetType); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 42926aad758..1eee1c7811f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -58,12 +58,20 @@ private static void ensureDatasetTypeIsPresent(String datasetType, String apiTok return; } System.out.println("The " + datasetType + "type wasn't found. Create it."); - String jsonIn = Json.createObjectBuilder().add("name", datasetType).build().toString(); + String displayName = capitalize(datasetType); + String jsonIn = Json.createObjectBuilder() + .add("name", datasetType) + .add("displayName", displayName) + .build().toString(); Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); typeAdded.prettyPrint(); typeAdded.then().assertThat().statusCode(OK.getStatusCode()); } + private static String capitalize(String stringIn) { + return stringIn.substring(0, 1).toUpperCase() + stringIn.substring(1); + } + @Test public void testCreateSoftwareDatasetNative() { Response createUser = UtilIT.createRandomUser(); @@ -262,7 +270,11 @@ public void testAddAndDeleteDatasetType() { //Avoid all-numeric names (which are not allowed) String randomName = "A" + UUID.randomUUID().toString().substring(0, 8); - String jsonIn = Json.createObjectBuilder().add("name", randomName).build().toString(); + String displayName = capitalize(randomName); + String jsonIn = Json.createObjectBuilder() + .add("name", randomName) + .add("displayName", displayName) + .build().toString(); System.out.println("adding type with name " + randomName); Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); @@ -295,6 +307,7 @@ public void testAddDatasetTypeWithMDBLicense(){ JsonObjectBuilder job = Json.createObjectBuilder(); job.add("name", "testDatasetType"); + job.add("displayName", "testDatasetType"); job.add("linkedMetadataBlocks", Json.createArrayBuilder().add("geospatial")); job.add("availableLicenses", Json.createArrayBuilder().add("CC0 1.0")); @@ -352,6 +365,7 @@ public void testUpdateDatasetTypeWithLicense(){ JsonObjectBuilder job = Json.createObjectBuilder(); job.add("name", "testDatasetType"); + job.add("displayName", "testDatasetType"); Response typeAdded = UtilIT.addDatasetType(job.build(), apiToken); typeAdded.prettyPrint(); @@ -420,7 +434,11 @@ public void testUpdateDatasetTypeLinksWithMetadataBlocks() { //Avoid all-numeric names (which are not allowed) String randomName = "zzz" + UUID.randomUUID().toString().substring(0, 8); - String jsonIn = Json.createObjectBuilder().add("name", randomName).build().toString(); + String displayName = capitalize(randomName); + String jsonIn = Json.createObjectBuilder() + .add("name", randomName) + .add("displayName", displayName) + .build().toString(); System.out.println("adding type with name " + randomName); Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); @@ -655,6 +673,7 @@ public void testCreateDatasetWithCustomType() { JsonObjectBuilder job = Json.createObjectBuilder(); job.add("name", "testDatasetType"); + job.add("displayName", "testDatasetType"); job.add("linkedMetadataBlocks", Json.createArrayBuilder().add("geospatial")); job.add("availableLicenses", Json.createArrayBuilder().add("CC0 1.0")); From 99fcede6aa3a31d96e3a3933b8b2c0ffff227d88 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 23 Oct 2025 16:49:51 -0400 Subject: [PATCH 08/29] allow i18n of displayName for dataset types #11747 --- .../harvard/iq/dataverse/api/Datasets.java | 19 ++++-- .../iq/dataverse/dataset/DatasetType.java | 39 ++++++++++- .../harvard/iq/dataverse/i18n/i18nUtil.java | 25 +++++++ .../propertyFiles/datasetTypes.properties | 8 +++ .../iq/dataverse/api/DatasetTypesIT.java | 65 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 25 +++++-- .../iq/dataverse/i18n/i18nUtilTest.java | 44 +++++++++++++ 7 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java create mode 100644 src/main/java/propertyFiles/datasetTypes.properties create mode 100644 src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java 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 1cbce4e1806..5494738d257 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -34,6 +34,7 @@ import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler; import edu.harvard.iq.dataverse.globus.GlobusServiceBean; import edu.harvard.iq.dataverse.globus.GlobusUtil; +import edu.harvard.iq.dataverse.i18n.i18nUtil; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.ingest.IngestUtil; import edu.harvard.iq.dataverse.makedatacount.*; @@ -99,13 +100,12 @@ import java.util.stream.Collectors; import static edu.harvard.iq.dataverse.api.ApiConstants.*; -import edu.harvard.iq.dataverse.dataset.DatasetType; -import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.license.License; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; +import static jakarta.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; @@ -5684,17 +5684,19 @@ public Response resetPidGenerator(@Context ContainerRequestContext crc, @PathPar @GET @Path("datasetTypes") - public Response getDatasetTypes() { + public Response getDatasetTypes(@HeaderParam(ACCEPT_LANGUAGE) String acceptLanguage) { + Locale locale = i18nUtil.parseAcceptLanguageHeader(acceptLanguage); JsonArrayBuilder jab = Json.createArrayBuilder(); for (DatasetType datasetType : datasetTypeSvc.listAll()) { - jab.add(datasetType.toJson()); + jab.add(datasetType.toJson(locale)); } return ok(jab); } @GET @Path("datasetTypes/{idOrName}") - public Response getDatasetTypes(@PathParam("idOrName") String idOrName) { + public Response getDatasetTypes(@PathParam("idOrName") String idOrName, @HeaderParam(ACCEPT_LANGUAGE) String acceptLanguage) { + Locale locale = i18nUtil.parseAcceptLanguageHeader(acceptLanguage); DatasetType datasetType = null; if (StringUtils.isNumeric(idOrName)) { try { @@ -5707,7 +5709,7 @@ public Response getDatasetTypes(@PathParam("idOrName") String idOrName) { datasetType = datasetTypeSvc.getByName(idOrName); } if (datasetType != null) { - return ok(datasetType.toJson()); + return ok(datasetType.toJson(locale)); } else { return error(NOT_FOUND, "Could not find a dataset type with name " + idOrName); } @@ -5796,7 +5798,10 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json DatasetType saved = datasetTypeSvc.save(datasetType); Long typeId = saved.getId(); String name = saved.getName(); - return ok(saved.toJson()); + // Locale is null because when creating the dataset type we are relying entirely + // on the database. The new dataset type has not yet been localized in a + // properties file. + return ok(saved.toJson(null)); } catch (WrappedResponse ex) { return error(BAD_REQUEST, ex.getMessage()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index 528132665fc..14d97c0824b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.license.License; +import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObjectBuilder; @@ -19,6 +20,9 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.logging.Logger; @NamedQueries({ @NamedQuery(name = "DatasetType.findAll", @@ -36,6 +40,8 @@ public class DatasetType implements Serializable { + private static final Logger logger = Logger.getLogger(DatasetType.class.getCanonicalName()); + public static final String DATASET_TYPE_DATASET = "dataset"; public static final String DATASET_TYPE_SOFTWARE = "software"; public static final String DATASET_TYPE_WORKFLOW = "workflow"; @@ -90,6 +96,10 @@ public void setName(String name) { this.name = name; } + /** + * In most cases, you should call the getDisplayName(locale) version. This is + * here in case you really want the value from the database. + */ public String getDisplayName() { return displayName; } @@ -114,7 +124,7 @@ public void setLicenses(List licenses) { this.licenses = licenses; } - public JsonObjectBuilder toJson() { + public JsonObjectBuilder toJson(Locale locale) { JsonArrayBuilder linkedMetadataBlocks = Json.createArrayBuilder(); for (MetadataBlock metadataBlock : this.getMetadataBlocks()) { linkedMetadataBlocks.add(metadataBlock.getName()); @@ -126,9 +136,34 @@ public JsonObjectBuilder toJson() { return Json.createObjectBuilder() .add("id", getId()) .add("name", getName()) - .add("displayName", getDisplayName()) + .add("displayName", getDisplayName(locale)) .add("linkedMetadataBlocks", linkedMetadataBlocks) .add("availableLicenses", availableLicenses); } + public String getDisplayName(Locale locale) { + logger.fine("Getting display name for dataset type " + name + " and locale " + locale); + if (locale == null) { + logger.fine("Locale is null, returning default display name: " + displayName); + return displayName; + } + if (locale.getLanguage().isBlank()) { + logger.fine("Locale couldn't be parsed, returning default display name: " + displayName); + return displayName; + } + if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { + // This is here to prevent looking up datasetTypes_en.properties, which doesn't exist. + // The English strings are in datasetTypes.properties (no _en). + logger.fine("Locale is English, returning default display name: " + displayName); + return displayName; + } + String propertiesFile = "datasetTypes_" + locale.toLanguageTag() + ".properties"; + try { + logger.fine("Looking up " + name + ".displayName in " + propertiesFile); + return BundleUtil.getStringFromPropertyFile(name + ".displayName", "datasetTypes", locale); + } catch (MissingResourceException e) { + logger.warning(name + ".displayName missing from " + propertiesFile + " (or file does not exist). Returning English version."); + return displayName; + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java b/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java new file mode 100644 index 00000000000..11f855256e1 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java @@ -0,0 +1,25 @@ +package edu.harvard.iq.dataverse.i18n; + +import java.util.List; +import java.util.Locale; + +public class i18nUtil { + + /** + * @param acceptLanguageHeader The Accept-Language header value such as + * "Accept-Language: en-US,en;q=0.5" + * @return The first Locale or null. + */ + public static Locale parseAcceptLanguageHeader(String acceptLanguageHeader) { + if (acceptLanguageHeader == null || acceptLanguageHeader.isEmpty()) { + return null; + } + List list = Locale.LanguageRange.parse(acceptLanguageHeader); + if (list.isEmpty()) { + return null; + } + Locale.LanguageRange languageRange = list.get(0); + return Locale.forLanguageTag(languageRange.getRange()); + } + +} diff --git a/src/main/java/propertyFiles/datasetTypes.properties b/src/main/java/propertyFiles/datasetTypes.properties new file mode 100644 index 00000000000..8b650b63f11 --- /dev/null +++ b/src/main/java/propertyFiles/datasetTypes.properties @@ -0,0 +1,8 @@ +# This file contains the strings for dataset types that can be +# translated into other languages (French, Spanish, etc.). +# Only the default dataset type (dataset) is included, as an example. +# If you add additional dataset types (e.g. software), you don't +# need to add them to this file if you are only running in English. +# However, if you are running in additional languages, you should +# add the additional dataset types to this file. +dataset.displayName=Dataset \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 1eee1c7811f..3e9066b0c50 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -3,6 +3,7 @@ import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_LATEST_PUBLISHED; import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.util.StringUtil; +import static io.restassured.path.json.JsonPath.with; import io.restassured.RestAssured; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; @@ -12,6 +13,8 @@ import static jakarta.ws.rs.core.Response.Status.CREATED; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.OK; +import java.util.List; +import java.util.Map; import java.util.UUID; import org.hamcrest.CoreMatchers; import static org.hamcrest.CoreMatchers.containsString; @@ -902,4 +905,66 @@ public void testCreateReview() { UtilIT.publishDatasetViaNativeApi(reviewPid, "major", apiTokenReviewer).then().assertThat().statusCode(OK.getStatusCode()); } + @Test + public void testInternationalization() { + Response getDatasetType = UtilIT.getDatasetType("software"); + getDatasetType.prettyPrint(); + getDatasetType.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.name", is("software")) + .body("data.displayName", is("Software")); + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language + getDatasetType = UtilIT.getDatasetType("software", "en-US,en;q=0.5"); + getDatasetType.prettyPrint(); + getDatasetType.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.name", is("software")) + .body("data.displayName", is("Software")); + + getDatasetType = UtilIT.getDatasetType("software", "en-US"); + getDatasetType.prettyPrint(); + getDatasetType.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.name", is("software")) + .body("data.displayName", is("Software")); + + getDatasetType = UtilIT.getDatasetType("software", ""); + getDatasetType.prettyPrint(); + getDatasetType.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.name", is("software")) + .body("data.displayName", is("Software")); + + boolean i18nIsConfigured = false; + if (!i18nIsConfigured) { + System.out.println("i18n is not configured; skipping test of non-English languages"); + return; + } + + getDatasetType = UtilIT.getDatasetType("software", "fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4"); + getDatasetType.prettyPrint(); + getDatasetType.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.name", is("software")) + .body("data.displayName", is("Logiciel")); + + getDatasetType = UtilIT.getDatasetTypes("fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4"); + getDatasetType.prettyPrint(); + getDatasetType.then().assertThat() + .statusCode(OK.getStatusCode()); + + // Messy but the only way we've figured out ¯\_(ツ)_/¯ + List> dataset = with(getDatasetType.body().asString()).param("dataset", "dataset") + .getList("data.findAll { data -> data.name == dataset }"); + Map firstDataset = dataset.get(0); + assertEquals("Ensemble de données", firstDataset.get("displayName")); + + List> instrument = with(getDatasetType.body().asString()).param("instrument", "instrument") + .getList("data.findAll { data -> data.name == instrument }"); + Map firstInstrument = instrument.get(0); + // Instrument isn't translated in the French properties file; should fall back to English + assertEquals("Instrument", firstInstrument.get("displayName")); + } + } 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 e80fabc0137..7ba5205833d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -16,6 +16,7 @@ import jakarta.json.JsonObject; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; +import static jakarta.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE; import static jakarta.ws.rs.core.Response.Status.CREATED; import java.nio.charset.StandardCharsets; @@ -4622,14 +4623,28 @@ static Response listDataverseInputLevels(String dataverseAlias, String apiToken) } public static Response getDatasetTypes() { - Response response = given() - .get("/api/datasets/datasetTypes"); - return response; + return getDatasetTypes(null); + } + + public static Response getDatasetTypes(String acceptLanguage) { + RequestSpecification requestSpecification = given(); + if (acceptLanguage != null) { + https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language + requestSpecification.header(ACCEPT_LANGUAGE, acceptLanguage); + } + return requestSpecification.get("/api/datasets/datasetTypes"); } static Response getDatasetType(String idOrName) { - return given() - .get("/api/datasets/datasetTypes/" + idOrName); + return getDatasetType(idOrName, null); + } + + static Response getDatasetType(String idOrName, String acceptLanguage) { + RequestSpecification requestSpecification = given(); + if (acceptLanguage != null) { + requestSpecification.header(ACCEPT_LANGUAGE, acceptLanguage); + } + return requestSpecification.get("/api/datasets/datasetTypes/" + idOrName); } static Response addDatasetType(String jsonIn, String apiToken) { diff --git a/src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java new file mode 100644 index 00000000000..99e20e52433 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java @@ -0,0 +1,44 @@ +package edu.harvard.iq.dataverse.i18n; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import java.util.Locale; + +public class i18nUtilTest { + + @Test + void testParseAcceptLanguageHeader_singleLanguage() { + Locale locale = i18nUtil.parseAcceptLanguageHeader("en-US"); + assertEquals(Locale.forLanguageTag("en-US"), locale); + } + + @Test + void testParseAcceptLanguageHeader_singleLanguageWithQ() { + Locale locale = i18nUtil.parseAcceptLanguageHeader("en-US,en;q=0.5"); + assertEquals(Locale.forLanguageTag("en-US"), locale); + } + + @Test + void testParseAcceptLanguageHeader_multipleLanguages() { + Locale locale = i18nUtil.parseAcceptLanguageHeader("fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4"); + assertEquals(Locale.forLanguageTag("fr-CA"), locale); + } + + @Test + void testParseAcceptLanguageHeader_emptyHeader() { + Locale locale = i18nUtil.parseAcceptLanguageHeader(""); + assertNull(locale); + } + + @Test + void testParseAcceptLanguageHeader_nullHeader() { + Locale locale = i18nUtil.parseAcceptLanguageHeader(null); + assertNull(locale); + } + + @Test + void testParseAcceptLanguageHeader_invalidHeader() { + Locale locale = i18nUtil.parseAcceptLanguageHeader("invalid-header"); + assertEquals(Locale.forLanguageTag("invalid-header"), locale); + } +} \ No newline at end of file From a7608e9abf35213e4d6d32563d2002193e9987da Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Jan 2026 14:07:10 -0500 Subject: [PATCH 09/29] rename sql script --- src/main/resources/db/migration/{V6.8.0.1.sql => V6.9.0.1.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.8.0.1.sql => V6.9.0.1.sql} (100%) diff --git a/src/main/resources/db/migration/V6.8.0.1.sql b/src/main/resources/db/migration/V6.9.0.1.sql similarity index 100% rename from src/main/resources/db/migration/V6.8.0.1.sql rename to src/main/resources/db/migration/V6.9.0.1.sql From 016929ca99622cfc0250f011534707cbbaadfc2d Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Jan 2026 14:19:13 -0500 Subject: [PATCH 10/29] remove metadata blocks use for demo These will be provided by each installation rather than shipping with Dataverse. --- .../repositorycharacteristics.tsv | 8 -- .../trusteddatadimensionsintensities.tsv | 61 --------------- .../repositorycharacteristics.properties | 15 ---- ...rusteddatadimensionsintensities.properties | 74 ------------------- 4 files changed, 158 deletions(-) delete mode 100644 scripts/api/data/metadatablocks/repositorycharacteristics.tsv delete mode 100644 scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv delete mode 100644 src/main/java/propertyFiles/repositorycharacteristics.properties delete mode 100644 src/main/java/propertyFiles/trusteddatadimensionsintensities.properties diff --git a/scripts/api/data/metadatablocks/repositorycharacteristics.tsv b/scripts/api/data/metadatablocks/repositorycharacteristics.tsv deleted file mode 100644 index 3951ebb146f..00000000000 --- a/scripts/api/data/metadatablocks/repositorycharacteristics.tsv +++ /dev/null @@ -1,8 +0,0 @@ -#metadataBlock name dataverseAlias displayName blockURI - repositorycharacteristics Repository Characteristics -#datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI - repositoryOrHosting Repository or Hosting Where and how the data are stored (e.g. institutional repository, cloud) and the oversight and management policies that ensure ethical and transparent handling of the data textbox 0 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics - securityAndTechnicalSafeguards Security and Technical Safeguards The mechanisms that the repository or hosting platform uses to prevent tampering with or unauthorized access to the data textbox 1 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics - sustainabilityAndLongevity Sustainability and Longevity The strategies that the repository or hosting platform uses to support the long-term preservation and maintenance of the data textbox 2 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics - standardsCertification Standards Certification The certification of the repository or hosting platform (e.g. CoreTrustSeal) textbox 3 FALSE FALSE FALSE FALSE FALSE FALSE repositorycharacteristics -#controlledVocabulary DatasetField Value identifier displayOrder \ No newline at end of file diff --git a/scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv b/scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv deleted file mode 100644 index 44af02ee2be..00000000000 --- a/scripts/api/data/metadatablocks/trusteddatadimensionsintensities.tsv +++ /dev/null @@ -1,61 +0,0 @@ -#metadataBlock name dataverseAlias displayName blockURI - trusteddatadimensionsintensities Trusted Data Dimensions and Intensities -#datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI - reviewTarget Review Target The type of research object reviewed text 0 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities - authorAndProvenance Author and Provenance The level of trust in the data creators and in other provenance information text 1 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities - integrityAndUsability Integrity and Usability The level of trust in the accuracy, completeness, and ease of use of the data text 2 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities - fitnessForScopeAndContextualRelevance Fitness for Scope and Contextual Relevance The level of trust in the suitability of the data for specific contexts, questions, or policy applications text 3 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities - licensingAndLegalClarity Licensing and Legal Clarity The level of trust in the explicitness of the data’s usage rights and their compliance with relevant laws and regulations text 4 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities - transparencyOfMethodsAndDocumentation Transparency of Methods and Documentation The level of trust in the clarity of the descriptions of data collection and processing methods text 5 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities - biasEquityAndRepresentativeness Bias, Equity, and Representativeness The level of trust in the inclusivity and fairness of the coverage of the data text 6 TRUE TRUE FALSE TRUE FALSE FALSE trusteddatadimensionsintensities -#controlledVocabulary DatasetField Value identifier displayOrder - reviewTarget Audiovisual 0 - reviewTarget Award 1 - reviewTarget Book 2 - reviewTarget Book Chapter 3 - reviewTarget Collection 4 - reviewTarget Computational Notebook 5 - reviewTarget Conference Paper 6 - reviewTarget Conference Proceeding 7 - reviewTarget DataPaper 8 - reviewTarget Dataset 9 - reviewTarget Dissertation 10 - reviewTarget Event 11 - reviewTarget Image 12 - reviewTarget Interactive Resource 13 - reviewTarget Instrument 14 - reviewTarget Journal 15 - reviewTarget Journal Article 16 - reviewTarget Model 17 - reviewTarget Output Management Plan 18 - reviewTarget Peer Review 19 - reviewTarget Physical Object 20 - reviewTarget Preprint 21 - reviewTarget Project 22 - reviewTarget Report 23 - reviewTarget Service 24 - reviewTarget Software 25 - reviewTarget Sound 26 - reviewTarget Standard 27 - reviewTarget Study Registration 28 - reviewTarget Text 29 - reviewTarget Workflow 30 - reviewTarget Other 31 - authorAndProvenance Low 0 - authorAndProvenance Medium 1 - authorAndProvenance High 2 - integrityAndUsability Low 0 - integrityAndUsability Medium 1 - integrityAndUsability High 2 - fitnessForScopeAndContextualRelevance Low 0 - fitnessForScopeAndContextualRelevance Medium 1 - fitnessForScopeAndContextualRelevance High 2 - licensingAndLegalClarity Low 0 - licensingAndLegalClarity Medium 1 - licensingAndLegalClarity High 2 - transparencyOfMethodsAndDocumentation Low 0 - transparencyOfMethodsAndDocumentation Medium 1 - transparencyOfMethodsAndDocumentation High 2 - biasEquityAndRepresentativeness Low 0 - biasEquityAndRepresentativeness Medium 1 - biasEquityAndRepresentativeness High 2 \ No newline at end of file diff --git a/src/main/java/propertyFiles/repositorycharacteristics.properties b/src/main/java/propertyFiles/repositorycharacteristics.properties deleted file mode 100644 index 6d07ae3cdac..00000000000 --- a/src/main/java/propertyFiles/repositorycharacteristics.properties +++ /dev/null @@ -1,15 +0,0 @@ -metadatablock.name=repositorycharacteristics -metadatablock.displayName=Repository Characteristics -metadatablock.displayFacet= -datasetfieldtype.repositoryOrHosting.title=Repository or Hosting -datasetfieldtype.securityAndTechnicalSafeguards.title=Security and Technical Safeguards -datasetfieldtype.sustainabilityAndLongevity.title=Sustainability and Longevity -datasetfieldtype.standardsCertification.title=Standards Certification -datasetfieldtype.repositoryOrHosting.description=Where and how the data are stored (e.g. institutional repository, cloud) and the oversight and management policies that ensure ethical and transparent handling of the data -datasetfieldtype.securityAndTechnicalSafeguards.description=The mechanisms that the repository or hosting platform uses to prevent tampering with or unauthorized access to the data -datasetfieldtype.sustainabilityAndLongevity.description=The strategies that the repository or hosting platform uses to support the long-term preservation and maintenance of the data -datasetfieldtype.standardsCertification.description=The certification of the repository or hosting platform (e.g. CoreTrustSeal) -datasetfieldtype.repositoryOrHosting.watermark= -datasetfieldtype.securityAndTechnicalSafeguards.watermark= -datasetfieldtype.sustainabilityAndLongevity.watermark= -datasetfieldtype.standardsCertification.watermark= \ No newline at end of file diff --git a/src/main/java/propertyFiles/trusteddatadimensionsintensities.properties b/src/main/java/propertyFiles/trusteddatadimensionsintensities.properties deleted file mode 100644 index a06cea18234..00000000000 --- a/src/main/java/propertyFiles/trusteddatadimensionsintensities.properties +++ /dev/null @@ -1,74 +0,0 @@ -metadatablock.name=trusteddatadimensionsintensities -metadatablock.displayName=Trusted Data Dimensions and Intensities -metadatablock.displayFacet= -datasetfieldtype.reviewTarget.title=Review Target -datasetfieldtype.authorAndProvenance.title=Author and Provenance -datasetfieldtype.integrityAndUsability.title=Integrity and Usability -datasetfieldtype.fitnessForScopeAndContextualRelevance.title=Fitness for Scope and Contextual Relevance -datasetfieldtype.licensingAndLegalClarity.title=Licensing and Legal Clarity -datasetfieldtype.transparencyOfMethodsAndDocumentation.title=Transparency of Methods and Documentation -datasetfieldtype.biasEquityAndRepresentativeness.title=Bias, Equity, and Representativeness -datasetfieldtype.reviewTarget.description=The type of research object reviewed -datasetfieldtype.authorAndProvenance.description=The level of trust in the data creators and in other provenance information -datasetfieldtype.integrityAndUsability.description=The level of trust in the accuracy, completeness, and ease of use of the data -datasetfieldtype.fitnessForScopeAndContextualRelevance.description=The level of trust in the suitability of the data for specific contexts, questions, or policy applications -datasetfieldtype.licensingAndLegalClarity.description=The level of trust in the explicitness of the data’s usage rights and their compliance with relevant laws and regulations -datasetfieldtype.transparencyOfMethodsAndDocumentation.description=The level of trust in the clarity of the descriptions of data collection and processing methods -datasetfieldtype.biasEquityAndRepresentativeness.description=The level of trust in the inclusivity and fairness of the coverage of the data -datasetfieldtype.reviewTarget.watermark= -datasetfieldtype.authorAndProvenance.watermark= -datasetfieldtype.integrityAndUsability.watermark= -datasetfieldtype.fitnessForScopeAndContextualRelevance.watermark= -datasetfieldtype.licensingAndLegalClarity.watermark= -datasetfieldtype.transparencyOfMethodsAndDocumentation.watermark= -datasetfieldtype.biasEquityAndRepresentativeness.watermark= -controlledvocabulary.reviewTarget.audiovisual=Audiovisual -controlledvocabulary.reviewTarget.award=Award -controlledvocabulary.reviewTarget.book=Book -controlledvocabulary.reviewTarget.book_chapter=Book Chapter -controlledvocabulary.reviewTarget.collection=Collection -controlledvocabulary.reviewTarget.computational_notebook=Computational Notebook -controlledvocabulary.reviewTarget.conference_paper=Conference Paper -controlledvocabulary.reviewTarget.conference_proceeding=Conference Proceeding -controlledvocabulary.reviewTarget.datapaper=DataPaper -controlledvocabulary.reviewTarget.dataset=Dataset -controlledvocabulary.reviewTarget.dissertation=Dissertation -controlledvocabulary.reviewTarget.event=Event -controlledvocabulary.reviewTarget.image=Image -controlledvocabulary.reviewTarget.interactive_resource=Interactive Resource -controlledvocabulary.reviewTarget.instrument=Instrument -controlledvocabulary.reviewTarget.journal=Journal -controlledvocabulary.reviewTarget.journal_article=Journal Article -controlledvocabulary.reviewTarget.model=Model -controlledvocabulary.reviewTarget.output_management_plan=Output Management Plan -controlledvocabulary.reviewTarget.peer_review=Peer Review -controlledvocabulary.reviewTarget.physical_object=Physical Object -controlledvocabulary.reviewTarget.preprint=Preprint -controlledvocabulary.reviewTarget.project=Project -controlledvocabulary.reviewTarget.report=Report -controlledvocabulary.reviewTarget.service=Service -controlledvocabulary.reviewTarget.software=Software -controlledvocabulary.reviewTarget.sound=Sound -controlledvocabulary.reviewTarget.standard=Standard -controlledvocabulary.reviewTarget.study_registration=Study Registration -controlledvocabulary.reviewTarget.text=Text -controlledvocabulary.reviewTarget.workflow=Workflow -controlledvocabulary.reviewTarget.other=Other -controlledvocabulary.authorAndProvenance.low=Low -controlledvocabulary.authorAndProvenance.medium=Medium -controlledvocabulary.authorAndProvenance.high=High -controlledvocabulary.integrityAndUsability.low=Low -controlledvocabulary.integrityAndUsability.medium=Medium -controlledvocabulary.integrityAndUsability.high=High -controlledvocabulary.fitnessForScopeAndContextualRelevance.low=Low -controlledvocabulary.fitnessForScopeAndContextualRelevance.medium=Medium -controlledvocabulary.fitnessForScopeAndContextualRelevance.high=High -controlledvocabulary.licensingAndLegalClarity.low=Low -controlledvocabulary.licensingAndLegalClarity.medium=Medium -controlledvocabulary.licensingAndLegalClarity.high=High -controlledvocabulary.transparencyOfMethodsAndDocumentation.low=Low -controlledvocabulary.transparencyOfMethodsAndDocumentation.medium=Medium -controlledvocabulary.transparencyOfMethodsAndDocumentation.high=High -controlledvocabulary.biasEquityAndRepresentativeness.low=Low -controlledvocabulary.biasEquityAndRepresentativeness.medium=Medium -controlledvocabulary.biasEquityAndRepresentativeness.high=High \ No newline at end of file From c60359ab5ee89ad82bdc044af5026896075aa08b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 9 Jan 2026 12:00:32 -0500 Subject: [PATCH 11/29] add review metadata block and tests #12015 --- scripts/api/data/metadatablocks/review.tsv | 40 ++++ .../harvard/iq/dataverse/api/ReviewsIT.java | 178 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 scripts/api/data/metadatablocks/review.tsv create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java diff --git a/scripts/api/data/metadatablocks/review.tsv b/scripts/api/data/metadatablocks/review.tsv new file mode 100644 index 00000000000..54e0760b325 --- /dev/null +++ b/scripts/api/data/metadatablocks/review.tsv @@ -0,0 +1,40 @@ +#metadataBlock name dataverseAlias displayName + review Review Metadata +#datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI + itemReviewed Item Reviewed The item being reviewed none 1 FALSE FALSE FALSE FALSE FALSE FALSE review + itemReviewedUrl URL The URL of the item being reviewed url 2 FALSE FALSE FALSE FALSE FALSE FALSE itemReviewed review + itemReviewedType Type The type of the item being reviewed text 3 FALSE TRUE FALSE FALSE FALSE FALSE itemReviewed review + itemReviewedCitation Citation The full bibliographic citation of the item being reviewed textbox 4 FALSE FALSE FALSE FALSE FALSE FALSE itemReviewed review +#controlledVocabulary DatasetField Value identifier displayOrder + itemReviewedType Audiovisual 0 + itemReviewedType Award 1 + itemReviewedType Book 2 + itemReviewedType Book Chapter 3 + itemReviewedType Collection 4 + itemReviewedType Computational Notebook 5 + itemReviewedType Conference Paper 6 + itemReviewedType Conference Proceeding 7 + itemReviewedType DataPaper 8 + itemReviewedType Dataset 9 + itemReviewedType Dissertation 10 + itemReviewedType Event 11 + itemReviewedType Image 12 + itemReviewedType Interactive Resource 13 + itemReviewedType Instrument 14 + itemReviewedType Journal 15 + itemReviewedType Journal Article 16 + itemReviewedType Model 17 + itemReviewedType Output Management Plan 18 + itemReviewedType Peer Review 19 + itemReviewedType Physical Object 20 + itemReviewedType Preprint 21 + itemReviewedType Project 22 + itemReviewedType Report 23 + itemReviewedType Service 24 + itemReviewedType Software 25 + itemReviewedType Sound 26 + itemReviewedType Standard 27 + itemReviewedType Study Registration 28 + itemReviewedType Text 29 + itemReviewedType Workflow 30 + itemReviewedType Other 31 \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java new file mode 100644 index 00000000000..9e5f6c013d4 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java @@ -0,0 +1,178 @@ +package edu.harvard.iq.dataverse.api; + +import edu.harvard.iq.dataverse.dataset.DatasetType; +import io.restassured.RestAssured; +import io.restassured.path.json.JsonPath; +import io.restassured.response.Response; +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; +import static jakarta.ws.rs.core.Response.Status.CREATED; +import static jakarta.ws.rs.core.Response.Status.OK; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ReviewsIT { + + @BeforeAll + public static void setUpClass() { + RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat().statusCode(OK.getStatusCode()); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + UtilIT.setSuperuserStatus(username, true).then().assertThat().statusCode(OK.getStatusCode()); + + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, apiToken); + } + + private static void ensureDatasetTypeIsPresent(String datasetType, String apiToken) { + Response getDatasetType = UtilIT.getDatasetType(datasetType); + getDatasetType.prettyPrint(); + String typeFound = JsonPath.from(getDatasetType.getBody().asString()).getString("data.name"); + System.out.println("type found: " + typeFound); + if (datasetType.equals(typeFound)) { + return; + } + System.out.println("The " + datasetType + "type wasn't found. Create it."); + String displayName = capitalize(datasetType); + String jsonIn = Json.createObjectBuilder() + .add("name", datasetType) + .add("displayName", displayName) + .build().toString(); + Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); + typeAdded.prettyPrint(); + typeAdded.then().assertThat().statusCode(OK.getStatusCode()); + } + + private static String capitalize(String stringIn) { + return stringIn.substring(0, 1).toUpperCase() + stringIn.substring(1); + } + + @Test + public void testCreateReview() { + + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + createUser.then().assertThat() + .statusCode(OK.getStatusCode()); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + createDataverseResponse.then().assertThat() + .statusCode(CREATED.getStatusCode()); + + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response setMetadataBlocks = UtilIT.setMetadataBlocks(dataverseAlias, + Json.createArrayBuilder().add("citation").add("review"), apiToken); + setMetadataBlocks.prettyPrint(); + setMetadataBlocks.then().assertThat().statusCode(OK.getStatusCode()); + + String[] testInputLevelNames = { "itemReviewedUrl", "itemReviewedType" }; + boolean[] testRequiredInputLevels = { true, true }; + boolean[] testIncludedInputLevels = { true, true }; + Response updateDataverseInputLevelsResponse = UtilIT.updateDataverseInputLevels(dataverseAlias, + testInputLevelNames, testRequiredInputLevels, testIncludedInputLevels, apiToken); + updateDataverseInputLevelsResponse.prettyPrint(); + updateDataverseInputLevelsResponse.then().assertThat().statusCode(OK.getStatusCode()); + + String itemReviewedTitle = "Percent of Children That Have Asthma"; + String itemReviewedUrl = "https://datacommons.org/tools/statvar#sv=Percent_Person_Children_WithAsthma"; + String reviewTitle = "Review of " + itemReviewedTitle; + String authorName = "Wazowski, Mike"; + String authorEmail = "mwazowski@mailinator.com"; + JsonObjectBuilder jsonForCreatingReview = Json.createObjectBuilder() + /** + * See above where this type is added to the installation and + * therefore available for use. + */ + .add("datasetType", DatasetType.DATASET_TYPE_REVIEW) + .add("datasetVersion", Json.createObjectBuilder() + .add("license", Json.createObjectBuilder() + .add("name", "CC0 1.0") + .add("uri", "http://creativecommons.org/publicdomain/zero/1.0")) + .add("metadataBlocks", Json.createObjectBuilder() + .add("citation", Json.createObjectBuilder() + .add("fields", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("typeName", "title") + .add("value", reviewTitle) + .add("typeClass", "primitive") + .add("multiple", false)) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("authorName", + Json.createObjectBuilder() + .add("value", authorName) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", + "authorName")))) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "author")) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("datasetContactEmail", + Json.createObjectBuilder() + .add("value", authorEmail) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", + "datasetContactEmail")))) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "datasetContact")) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add(Json.createObjectBuilder() + .add("dsDescriptionValue", + Json.createObjectBuilder() + .add("value", + "This is a review of a dataset.") + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", + "dsDescriptionValue")))) + .add("typeClass", "compound") + .add("multiple", true) + .add("typeName", "dsDescription")) + .add(Json.createObjectBuilder() + .add("value", Json.createArrayBuilder() + .add("Medicine, Health and Life Sciences")) + .add("typeClass", "controlledVocabulary") + .add("multiple", true) + .add("typeName", "subject")) + .add(Json.createObjectBuilder() + .add("value", Json.createObjectBuilder() + .add("itemReviewedUrl", + Json.createObjectBuilder() + .add("value", itemReviewedUrl) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "itemReviewedUrl")) + .add("itemReviewedType", + Json.createObjectBuilder() + .add("value", "Dataset") + .add("typeClass", + "controlledVocabulary") + .add("multiple", false) + .add("typeName", "itemReviewedType"))) + .add("typeClass", "compound") + .add("multiple", false) + .add("typeName", "itemReviewed")))))); + + Response createReview = UtilIT.createDataset(dataverseAlias, jsonForCreatingReview, apiToken); + createReview.prettyPrint(); + createReview.then().assertThat().statusCode(CREATED.getStatusCode()); + Integer reviewId = UtilIT.getDatasetIdFromResponse(createReview); + String reviewPid = JsonPath.from(createReview.getBody().asString()).getString("data.persistentId"); + + } + +} From b49b5229227291b5a184a078255b7ce6e4886676 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 9 Jan 2026 15:16:52 -0500 Subject: [PATCH 12/29] add properties file for review metadata block #12015 --- src/main/java/propertyFiles/review.properties | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/propertyFiles/review.properties diff --git a/src/main/java/propertyFiles/review.properties b/src/main/java/propertyFiles/review.properties new file mode 100644 index 00000000000..6799d5000d6 --- /dev/null +++ b/src/main/java/propertyFiles/review.properties @@ -0,0 +1,47 @@ +metadatablock.name=review +metadatablock.displayName=Review Metadata +metadatablock.displayFacet=Review +datasetfieldtype.itemReviewed.title=Item Reviewed +datasetfieldtype.itemReviewedUrl.title=URL +datasetfieldtype.itemReviewedType.title=Type +datasetfieldtype.itemReviewedCitation.title=Citation +datasetfieldtype.itemReviewed.description=The item being reviewed +datasetfieldtype.itemReviewedUrl.description=The URL of the item being reviewed +datasetfieldtype.itemReviewedType.description=The type of the item being reviewed +datasetfieldtype.itemReviewedCitation.description=The full bibliographic citation of the item being reviewed +datasetfieldtype.itemReviewed.watermark= +datasetfieldtype.itemReviewedUrl.watermark= +datasetfieldtype.itemReviewedType.watermark= +datasetfieldtype.itemReviewedCitation.watermark= +controlledvocabulary.itemReviewedType.audiovisual=Audiovisual +controlledvocabulary.itemReviewedType.award=Award +controlledvocabulary.itemReviewedType.book=Book +controlledvocabulary.itemReviewedType.book_chapter=Book Chapter +controlledvocabulary.itemReviewedType.collection=Collection +controlledvocabulary.itemReviewedType.computational_notebook=Computational Notebook +controlledvocabulary.itemReviewedType.conference_paper=Conference Paper +controlledvocabulary.itemReviewedType.conference_proceeding=Conference Proceeding +controlledvocabulary.itemReviewedType.datapaper=DataPaper +controlledvocabulary.itemReviewedType.dataset=Dataset +controlledvocabulary.itemReviewedType.dissertation=Dissertation +controlledvocabulary.itemReviewedType.event=Event +controlledvocabulary.itemReviewedType.image=Image +controlledvocabulary.itemReviewedType.interactive_resource=Interactive Resource +controlledvocabulary.itemReviewedType.instrument=Instrument +controlledvocabulary.itemReviewedType.journal=Journal +controlledvocabulary.itemReviewedType.journal_article=Journal Article +controlledvocabulary.itemReviewedType.model=Model +controlledvocabulary.itemReviewedType.output_management_plan=Output Management Plan +controlledvocabulary.itemReviewedType.peer_review=Peer Review +controlledvocabulary.itemReviewedType.physical_object=Physical Object +controlledvocabulary.itemReviewedType.preprint=Preprint +controlledvocabulary.itemReviewedType.project=Project +controlledvocabulary.itemReviewedType.report=Report +controlledvocabulary.itemReviewedType.service=Service +controlledvocabulary.itemReviewedType.software=Software +controlledvocabulary.itemReviewedType.sound=Sound +controlledvocabulary.itemReviewedType.standard=Standard +controlledvocabulary.itemReviewedType.study_registration=Study Registration +controlledvocabulary.itemReviewedType.text=Text +controlledvocabulary.itemReviewedType.workflow=Workflow +controlledvocabulary.itemReviewedType.other=Other From 3de3913b14be7a88f0ca8e31526e5fcdec8ccb08 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 12 Jan 2026 10:25:53 -0500 Subject: [PATCH 13/29] add description for dataset types #11887 #11747 --- .../harvard/iq/dataverse/api/Datasets.java | 3 ++ .../iq/dataverse/dataset/DatasetType.java | 49 ++++++++++++++++++- src/main/resources/db/migration/V6.9.0.1.sql | 6 ++- .../harvard/iq/dataverse/api/ReviewsIT.java | 36 ++++++++------ 4 files changed, 78 insertions(+), 16 deletions(-) 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 cac02308c2e..ce2aa643e70 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -5771,6 +5771,7 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json String nameIn = null; String displayNameIn = null; + String descriptionIn = null; JsonArrayBuilder datasetTypesAfter = Json.createArrayBuilder(); List metadataBlocksToSave = new ArrayList<>(); @@ -5780,6 +5781,7 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json JsonObject datasetTypeObj = JsonUtil.getJsonObject(jsonIn); nameIn = datasetTypeObj.getString("name"); displayNameIn = datasetTypeObj.getString("displayName", null); + descriptionIn = datasetTypeObj.getString("description", null); JsonArray arr = datasetTypeObj.getJsonArray("linkedMetadataBlocks"); if (arr != null && !arr.isEmpty()) { @@ -5829,6 +5831,7 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json DatasetType datasetType = new DatasetType(); datasetType.setName(nameIn); datasetType.setDisplayName(displayNameIn); + datasetType.setDescription(descriptionIn); datasetType.setMetadataBlocks(metadataBlocksToSave); datasetType.setLicenses(licensesToSave); DatasetType saved = datasetTypeSvc.save(datasetType); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index 14d97c0824b..df525601c12 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.MetadataBlock; import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObjectBuilder; @@ -65,6 +66,12 @@ public class DatasetType implements Serializable { @Column(nullable = false, columnDefinition = "VARCHAR(255) DEFAULT ''") private String displayName; + /** + * Human readable description to show in the UI. + */ + @Column(nullable = true, columnDefinition = "VARCHAR(255) DEFAULT ''") + private String description; + /** * The metadata blocks this dataset type is linked to. */ @@ -108,6 +115,18 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } + /** + * In most cases, you should call the getDescription(locale) version. This is + * here in case you really want the value from the database. + */ + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + public List getMetadataBlocks() { return metadataBlocks; } @@ -133,10 +152,11 @@ public JsonObjectBuilder toJson(Locale locale) { for (License license : this.getLicenses()) { availableLicenses.add(license.getName()); } - return Json.createObjectBuilder() + return NullSafeJsonBuilder.jsonObjectBuilder() .add("id", getId()) .add("name", getName()) .add("displayName", getDisplayName(locale)) + .add("description", getDescription(locale)) .add("linkedMetadataBlocks", linkedMetadataBlocks) .add("availableLicenses", availableLicenses); } @@ -166,4 +186,31 @@ public String getDisplayName(Locale locale) { return displayName; } } + + public String getDescription(Locale locale) { + logger.fine("Getting description for dataset type " + name + " and locale " + locale); + if (locale == null) { + logger.fine("Locale is null, returning default description: " + description); + return description; + } + if (locale.getLanguage().isBlank()) { + logger.fine("Locale couldn't be parsed, returning default description: " + description); + return description; + } + if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { + // This is here to prevent looking up datasetTypes_en.properties, which doesn't exist. + // The English strings are in datasetTypes.properties (no _en). + logger.fine("Locale is English, returning default description: " + description); + return description; + } + String propertiesFile = "datasetTypes_" + locale.toLanguageTag() + ".properties"; + try { + logger.fine("Looking up " + name + ".description in " + propertiesFile); + return BundleUtil.getStringFromPropertyFile(name + ".description", "datasetTypes", locale); + } catch (MissingResourceException e) { + logger.warning(name + ".description missing from " + propertiesFile + " (or file does not exist). Returning English version."); + return description; + } + } + } diff --git a/src/main/resources/db/migration/V6.9.0.1.sql b/src/main/resources/db/migration/V6.9.0.1.sql index 158278843ab..2cd51dd0c1e 100644 --- a/src/main/resources/db/migration/V6.9.0.1.sql +++ b/src/main/resources/db/migration/V6.9.0.1.sql @@ -1,4 +1,8 @@ -- Add displayname column to datasettype table ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS displayname VARCHAR(255) NOT NULL DEFAULT ''; -- Set displayname for dataset -UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; \ No newline at end of file +UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; +-- Add description column to datasettype table +ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS description VARCHAR(255); +-- Set description for dataset +UPDATE datasettype SET description = 'A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java index 9e5f6c013d4..ba09de2155c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import io.restassured.RestAssured; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; @@ -23,32 +24,39 @@ public static void setUpClass() { String apiToken = UtilIT.getApiTokenFromResponse(createUser); UtilIT.setSuperuserStatus(username, true).then().assertThat().statusCode(OK.getStatusCode()); - ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, apiToken); + String datasetDescription = "A traditional dataset."; + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_DATASET, "Dataset", datasetDescription, apiToken); + + String reviewDescription = "A review of a dataset compiled by community experts."; + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, "Review", reviewDescription, apiToken); } - private static void ensureDatasetTypeIsPresent(String datasetType, String apiToken) { - Response getDatasetType = UtilIT.getDatasetType(datasetType); + private static void ensureDatasetTypeIsPresent(String name, String displayName, String description, + String apiToken) { + Response getDatasetType = UtilIT.getDatasetType(name); getDatasetType.prettyPrint(); - String typeFound = JsonPath.from(getDatasetType.getBody().asString()).getString("data.name"); - System.out.println("type found: " + typeFound); - if (datasetType.equals(typeFound)) { + String nameFound = JsonPath.from(getDatasetType.getBody().asString()).getString("data.name"); + String displayNameFound = JsonPath.from(getDatasetType.getBody().asString()).getString("data.displayName"); + String descriptionFound = JsonPath.from(getDatasetType.getBody().asString()).getString("data.description"); + System.out.println("Found: name=" + nameFound + ". Display name=" + displayNameFound + ". Description=" + + descriptionFound); + if (name.equals(nameFound)) { + System.out.println(name + "=" + nameFound + ". Exists. No need to create. Returning."); return; + } else { + System.out.println(name + " wasn't found. Create it."); } - System.out.println("The " + datasetType + "type wasn't found. Create it."); - String displayName = capitalize(datasetType); - String jsonIn = Json.createObjectBuilder() - .add("name", datasetType) + String jsonIn = NullSafeJsonBuilder.jsonObjectBuilder() + .add("name", name) .add("displayName", displayName) + .add("description", description) .build().toString(); + // System.out.println(JsonUtil.prettyPrint(jsonIn)); Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); typeAdded.prettyPrint(); typeAdded.then().assertThat().statusCode(OK.getStatusCode()); } - private static String capitalize(String stringIn) { - return stringIn.substring(0, 1).toUpperCase() + stringIn.substring(1); - } - @Test public void testCreateReview() { From d54ae98447b29226d6bd9a7139580a9f1dd14642 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 12 Jan 2026 10:51:10 -0500 Subject: [PATCH 14/29] add description of dataset to properties file #11887 #11747 --- src/main/java/propertyFiles/datasetTypes.properties | 3 ++- src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/propertyFiles/datasetTypes.properties b/src/main/java/propertyFiles/datasetTypes.properties index 8b650b63f11..35794a7a17c 100644 --- a/src/main/java/propertyFiles/datasetTypes.properties +++ b/src/main/java/propertyFiles/datasetTypes.properties @@ -5,4 +5,5 @@ # need to add them to this file if you are only running in English. # However, if you are running in additional languages, you should # add the additional dataset types to this file. -dataset.displayName=Dataset \ No newline at end of file +dataset.displayName=Dataset +dataset.description=A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files. \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java index ba09de2155c..225b53de393 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java @@ -24,7 +24,7 @@ public static void setUpClass() { String apiToken = UtilIT.getApiTokenFromResponse(createUser); UtilIT.setSuperuserStatus(username, true).then().assertThat().statusCode(OK.getStatusCode()); - String datasetDescription = "A traditional dataset."; + String datasetDescription = "A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files."; ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_DATASET, "Dataset", datasetDescription, apiToken); String reviewDescription = "A review of a dataset compiled by community experts."; From ef981f364bc3b8e4e6315d1b70218cb67cc338d6 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 5 Feb 2026 15:04:16 -0500 Subject: [PATCH 15/29] control over which dataset types are available, per collection #12115 --- .../edu/harvard/iq/dataverse/Dataverse.java | 14 ++++ .../impl/AbstractCreateDatasetCommand.java | 37 +++++++++- .../impl/UpdateDataverseAttributeCommand.java | 42 +++++++++++ .../iq/dataverse/util/json/JsonPrinter.java | 4 ++ src/main/resources/db/migration/V6.9.0.1.sql | 4 +- .../iq/dataverse/api/DatasetTypesIT.java | 71 +++++++++++++++++-- 6 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index 98f52b705a8..9af89774435 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -166,6 +166,12 @@ public String getIndexableCategoryName() { } private String affiliation; + + /** + * If null, only the default dataset type (dataset) is allowed. + * See AbstractCreateDatasetCommand. + */ + private String allowedDatasetTypes; ///private String storageDriver=null; @@ -770,6 +776,14 @@ public void setAffiliation(String affiliation) { this.affiliation = affiliation; } + public String getAllowedDatasetTypes() { + return allowedDatasetTypes; + } + + public void setAllowedDatasetTypes(String allowedDatasetTypes) { + this.allowedDatasetTypes = allowedDatasetTypes; + } + public boolean isMetadataBlockRoot() { return metadataBlockRoot; } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java index b36a638956f..3e522a549e5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java @@ -11,9 +11,14 @@ 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.InvalidFieldsCommandException; import edu.harvard.iq.dataverse.pidproviders.PidProvider; import static edu.harvard.iq.dataverse.util.StringUtil.isEmpty; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.logging.Logger; import org.apache.solr.client.solrj.SolrServerException; @@ -124,8 +129,36 @@ public Dataset execute(CommandContext ctxt) throws CommandException { DatasetType existingDatasetType = theDataset.getDatasetType(); logger.fine("existing dataset type: " + existingDatasetType); if (existingDatasetType != null) { - // A dataset type can be specified via API, for example. - theDataset.setDatasetType(existingDatasetType); + List allowedDatasetTypes = new ArrayList<>(); + String allowedDatasetTypesString = theDataset.getOwner().getAllowedDatasetTypes(); + if (allowedDatasetTypesString == null) { + // If allowedDatasetTypes is unspecified, assume + // only the default type (dataset) is allowed + allowedDatasetTypes.add(defaultDatasetType); + } else { + // Turn comma-separated String into actual values + String[] allowedDatasetTypeNames = allowedDatasetTypesString.split(","); + for (String datasetTypeName : allowedDatasetTypeNames) { + DatasetType datasetType = ctxt.datasetTypes().getByName(datasetTypeName.trim()); + if (datasetType != null) { + allowedDatasetTypes.add(datasetType); + } else { + logger.warning("Could not find datasetType based on " + datasetTypeName); + } + } + } + if (allowedDatasetTypes.contains(existingDatasetType)) { + theDataset.setDatasetType(existingDatasetType); + } else { + List typeNames = allowedDatasetTypes.stream() + .map(DatasetType::getName) + .toList(); + Map fieldErrors = new HashMap<>(); + fieldErrors.put("datasetType", "The parent collection does not allow the datasetType " + + existingDatasetType.getName() + ". Allowed types: " + String.join(", ", typeNames)); + throw new InvalidFieldsCommandException("The dataset could not be created due to the datasetType.", + this, fieldErrors); + } } else { theDataset.setDatasetType(defaultDatasetType); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java index ab12d8eea26..aa9f72dad0b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java @@ -2,16 +2,22 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.engine.command.exception.InvalidFieldsCommandException; import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Command to update an existing Dataverse attribute. @@ -25,6 +31,7 @@ public class UpdateDataverseAttributeCommand extends AbstractCommand private static final String ATTRIBUTE_AFFILIATION = "affiliation"; private static final String ATTRIBUTE_FILE_PIDS_ENABLED = "filePIDsEnabled"; private static final String ATTRIBUTE_REQUIRE_FILES_TO_PUBLISH_DATASET = "requireFilesToPublishDataset"; + private static final String ATTRIBUTE_ALLOWED_DATASET_TYPES = "allowedDatasetTypes"; private final Dataverse dataverse; private final String attributeName; private final Object attributeValue; @@ -49,6 +56,9 @@ public Dataverse execute(CommandContext ctxt) throws CommandException { case ATTRIBUTE_FILE_PIDS_ENABLED: setBooleanAttribute(ctxt, true); break; + case ATTRIBUTE_ALLOWED_DATASET_TYPES: + setAllowedDatasetTypes(ctxt, attributeValue); + break; default: throw new IllegalCommandException("'" + attributeName + "' is not a supported attribute", this); } @@ -116,4 +126,36 @@ private void setBooleanAttribute(CommandContext ctxt, boolean adminOnly) throws throw new IllegalCommandException("Unsupported boolean attribute: " + attributeName, this); } } + + private void setAllowedDatasetTypes(CommandContext ctxt, Object allowedDatasetTypesIn) throws CommandException { + if (!getRequest().getUser().isSuperuser()) { + throw new PermissionException("You must be a superuser to change this setting", + this, null, dataverse); + } + if (!(allowedDatasetTypesIn instanceof String stringValue)) { + throw new IllegalCommandException("'" + ATTRIBUTE_ALLOWED_DATASET_TYPES + "' requires a string value", + this); + } + + List invalidDatasetTypes = new ArrayList<>(); + + String[] allowedDatasetTypeNames = stringValue.split(","); + for (String datasetTypeName : allowedDatasetTypeNames) { + DatasetType datasetType = ctxt.datasetTypes().getByName(datasetTypeName.trim()); + if (datasetType == null) { + invalidDatasetTypes.add(datasetTypeName); + } + } + + if (!invalidDatasetTypes.isEmpty()) { + Map fieldErrors = new HashMap<>(); + fieldErrors.put(ATTRIBUTE_ALLOWED_DATASET_TYPES, "The following dataset types do not exist: " + + String.join(", ", invalidDatasetTypes)); + throw new InvalidFieldsCommandException("The collection could not be updated because " + + ATTRIBUTE_ALLOWED_DATASET_TYPES + " is invalid.", this, fieldErrors); + } + + dataverse.setAllowedDatasetTypes(stringValue); + } + } 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 27b7a122c93..350445f138e 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 @@ -358,6 +358,10 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean re if (childCount != null) { bld.add("childCount", childCount); } + String allowedDatasetTypes = dv.getAllowedDatasetTypes(); + if (allowedDatasetTypes != null) { + bld.add("allowedDatasetTypes", allowedDatasetTypes); + } addDatasetFileCountLimit(dv, bld); return bld; } diff --git a/src/main/resources/db/migration/V6.9.0.1.sql b/src/main/resources/db/migration/V6.9.0.1.sql index 2cd51dd0c1e..9862916d576 100644 --- a/src/main/resources/db/migration/V6.9.0.1.sql +++ b/src/main/resources/db/migration/V6.9.0.1.sql @@ -5,4 +5,6 @@ UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; -- Add description column to datasettype table ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS description VARCHAR(255); -- Set description for dataset -UPDATE datasettype SET description = 'A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; \ No newline at end of file +UPDATE datasettype SET description = 'A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; +-- at collection level, control which dataset types are allowed +ALTER TABLE dataverse ADD COLUMN IF NOT EXISTS alloweddatasettypes VARCHAR(255); \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 3e9066b0c50..69867dbf979 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -30,6 +30,7 @@ public class DatasetTypesIT { final static String INSTRUMENT = "instrument"; + private static String apiTokenSuperuser; @BeforeAll public static void setUpClass() { @@ -37,13 +38,13 @@ public static void setUpClass() { Response createUser = UtilIT.createRandomUser(); createUser.then().assertThat().statusCode(OK.getStatusCode()); - String username = UtilIT.getUsernameFromResponse(createUser); - String apiToken = UtilIT.getApiTokenFromResponse(createUser); - UtilIT.setSuperuserStatus(username, true).then().assertThat().statusCode(OK.getStatusCode()); + String usernameSuperuser = UtilIT.getUsernameFromResponse(createUser); + apiTokenSuperuser = UtilIT.getApiTokenFromResponse(createUser); + UtilIT.setSuperuserStatus(usernameSuperuser, true).then().assertThat().statusCode(OK.getStatusCode()); - ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_SOFTWARE, apiToken); - ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, apiToken); - ensureDatasetTypeIsPresent(INSTRUMENT, apiToken); + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_SOFTWARE, apiTokenSuperuser); + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, apiTokenSuperuser); + ensureDatasetTypeIsPresent(INSTRUMENT, apiTokenSuperuser); } @AfterAll @@ -75,6 +76,15 @@ private static String capitalize(String stringIn) { return stringIn.substring(0, 1).toUpperCase() + stringIn.substring(1); } + private void setAllowedDatasetTypes(String dataverseAlias, String allowedDatasetTypes) { + Response setAllowedDatasetTypes = UtilIT.setCollectionAttribute(dataverseAlias, "allowedDatasetTypes", allowedDatasetTypes, + apiTokenSuperuser); + setAllowedDatasetTypes.prettyPrint(); + setAllowedDatasetTypes.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.allowedDatasetTypes", is(allowedDatasetTypes)); + } + @Test public void testCreateSoftwareDatasetNative() { Response createUser = UtilIT.createRandomUser(); @@ -87,6 +97,8 @@ public void testCreateSoftwareDatasetNative() { String dataverseAlias = UtilIT.getAliasFromResponse(createDataverse); Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverse); + setAllowedDatasetTypes(dataverseAlias, "dataset,software"); + String jsonIn = UtilIT.getDatasetJson("doc/sphinx-guides/source/_static/api/dataset-create-software.json"); Response createSoftware = UtilIT.createDataset(dataverseAlias, jsonIn, apiToken); @@ -157,6 +169,7 @@ public void testCreateDatasetSemantic() { createDataverse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverse); Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverse); + setAllowedDatasetTypes(dataverseAlias, "software"); String jsonIn = UtilIT.getDatasetJson("doc/sphinx-guides/source/_static/api/dataset-create-software.jsonld"); @@ -191,6 +204,7 @@ public void testImportJson() { createDataverse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverse); Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverse); + setAllowedDatasetTypes(dataverseAlias, "software"); String jsonIn = UtilIT.getDatasetJson("doc/sphinx-guides/source/_static/api/dataset-create-software.json"); @@ -688,6 +702,8 @@ public void testCreateDatasetWithCustomType() { getTypes = UtilIT.getDatasetTypes(); getTypes.prettyPrint(); + setAllowedDatasetTypes(dataverseAlias, "testDatasetType"); + String pathToJsonFile = "scripts/api/data/dataset-create-new-with-type.json"; Response createDatasetResponse = UtilIT.createDatasetViaNativeApi(dataverseAlias, pathToJsonFile, apiToken); @@ -738,6 +754,47 @@ public void testCreateDatasetWithCustomType() { } + @Test + public void testDatasetTypeNotAllowed() { + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat().statusCode(OK.getStatusCode()); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverse = UtilIT.createRandomDataverse(apiToken); + createDataverse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverse); + Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverse); + + String jsonIn = UtilIT.getDatasetJson("doc/sphinx-guides/source/_static/api/dataset-create-software.json"); + + Response createSoftware = UtilIT.createDataset(dataverseAlias, jsonIn, apiToken); + createSoftware.prettyPrint(); + + createSoftware.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + } + + @Test + public void testUpdateCollectionWithInvalidDatasetType() { + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat().statusCode(OK.getStatusCode()); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverse = UtilIT.createRandomDataverse(apiToken); + createDataverse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverse); + Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverse); + + String nonExistentTypes = "foo,bar,baz"; + + Response setAllowedDatasetTypesFail = UtilIT.setCollectionAttribute(dataverseAlias, "allowedDatasetTypes", + nonExistentTypes, apiTokenSuperuser); + setAllowedDatasetTypesFail.prettyPrint(); + setAllowedDatasetTypesFail.then().assertThat() + .statusCode(BAD_REQUEST.getStatusCode()); + } + /** * In this test, there are two users: one who publishes a dataset and * another who publishes a review of that dataset. @@ -889,6 +946,8 @@ public void testCreateReview() { ) )); + setAllowedDatasetTypes(collectionOfReviewsAlias, "review"); + Response createReview = UtilIT.createDataset(collectionOfReviewsAlias, jsonForCreatingReview, apiTokenReviewer); createReview.prettyPrint(); createReview.then().assertThat().statusCode(CREATED.getStatusCode()); From 6c9df720660024f1739510452003650b163c7d8d Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 5 Feb 2026 15:35:55 -0500 Subject: [PATCH 16/29] explain that deleting dataset type by name is not supported #11833 Return 400 instead of 500 if you try. --- .../harvard/iq/dataverse/api/Datasets.java | 2 +- .../iq/dataverse/api/DatasetTypesIT.java | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) 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 8347cfebace..c0446de1861 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -5867,7 +5867,7 @@ public Response deleteDatasetType(@Context ContainerRequestContext crc, @PathPar try { idToDelete = Long.parseLong(doomed); } catch (NumberFormatException e) { - throw new IllegalArgumentException("ID must be a number"); + return error(BAD_REQUEST,"ID must be a number"); } DatasetType datasetTypeToDelete = datasetTypeSvc.getById(idToDelete); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 69867dbf979..b17e61f17ab 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -299,15 +299,24 @@ public void testAddAndDeleteDatasetType() { typeAdded.then().assertThat().statusCode(OK.getStatusCode()); - Long doomed = JsonPath.from(typeAdded.getBody().asString()).getLong("data.id"); + Long doomedId = JsonPath.from(typeAdded.getBody().asString()).getLong("data.id"); + // Deleting by name is not supported + String doomedName = JsonPath.from(typeAdded.getBody().asString()).getString("data.name"); - System.out.println("doomed: " + doomed); - Response getTypeById = UtilIT.getDatasetType(doomed.toString()); + System.out.println("doomed: " + doomedId); + Response getTypeById = UtilIT.getDatasetType(doomedId.toString()); getTypeById.prettyPrint(); getTypeById.then().assertThat().statusCode(OK.getStatusCode()); - System.out.println("deleting type with id " + doomed); - Response typeDeleted = UtilIT.deleteDatasetTypes(doomed, apiToken); + System.out.println("try to delete type by name " + doomedName + " should fail"); + Response deleteByNameFail = RestAssured.given().header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken) + .delete("/api/datasets/datasetTypes/" + doomedName); + deleteByNameFail.prettyPrint(); + deleteByNameFail.then().assertThat() + .statusCode(BAD_REQUEST.getStatusCode()); + + System.out.println("deleting type with id " + doomedId); + Response typeDeleted = UtilIT.deleteDatasetTypes(doomedId, apiToken); typeDeleted.prettyPrint(); typeDeleted.then().assertThat().statusCode(OK.getStatusCode()); From 467a05dfd2d29d04da702b5d4605c053612cc92a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 6 Feb 2026 12:22:06 -0500 Subject: [PATCH 17/29] use machine-readable names under the "Dataset Type" facet #11758 We do this so that the facet works for the SPA. See this issue: https://github.com/IQSS/dataverse-frontend/issues/809 As noted in the xhtml, we are aware that when you click the machine-readable name, such as "dataset" (lower case), the "friendly" name, such as "Dataset" appears above the search results. This is suboptimal but we are trying to touch JSF as little as possible. In the SPA, the value will be consistent as lower case "dataset", for example at left and at top. --- .../search/SolrSearchServiceBean.java | 29 ++++++++++++++++--- src/main/webapp/search-include-fragment.xhtml | 1 + .../iq/dataverse/api/DatasetTypesIT.java | 4 +-- .../harvard/iq/dataverse/api/SearchIT.java | 1 + 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchServiceBean.java index 530d3f9ef7e..b265ad967d4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SolrSearchServiceBean.java @@ -759,12 +759,33 @@ public SolrQueryResponse search( } catch (Exception e) { localefriendlyName = facetFieldCount.getName(); } + } else if (facetField.getName().equals(SearchFields.DATASET_TYPE)) { + /** + * For dataset types we use the machine readable name (e.g. "dataset" or + * "software") rather than the display name (e.g. "Dataset" or "Software") + * because otherwise the facet doesn't work in the SPA when you click it. The + * SPA operates on the "labels" array (see below) and the keys of the objects in + * this array are passed back into the Search API when clicked (e.g. + * "fq=datasetType:dataset"). + * + * "datasetType": { + * "friendly": "Dataset Type", + * "labels": [ + * {"dataset":8}, + * {"software":1} + * ] + * } + * See also https://github.com/IQSS/dataverse-frontend/issues/809 + * and https://github.com/IQSS/dataverse/issues/11758 . + * + * We recognize that this will be a problem for internationalizing the SPA but + * the SPA will likely have similar problems with facets like publicationStatus + * where the labels are in English (e.g. {"Draft":42}). The Search API much use + * the English string when faceting (e.g. "fq=publicationStatus:Draft"). + */ + localefriendlyName = facetFieldCount.getName(); } else { try { - // This is where facets are capitalized. - // This will be a problem for the API clients because they get back a string like this from the Search API... - // {"datasetType":{"friendly":"Dataset Type","labels":[{"Dataset":1},{"Software":1}]} - // ... but they will need to use the lower case version (e.g. "software") to narrow results. localefriendlyName = BundleUtil.getStringFromPropertyFile(facetFieldCount.getName(), "Bundle"); } catch (Exception e) { localefriendlyName = facetFieldCount.getName(); diff --git a/src/main/webapp/search-include-fragment.xhtml b/src/main/webapp/search-include-fragment.xhtml index 34ac72b3571..71c3775b833 100644 --- a/src/main/webapp/search-include-fragment.xhtml +++ b/src/main/webapp/search-include-fragment.xhtml @@ -381,6 +381,7 @@ + Date: Fri, 6 Feb 2026 15:17:53 -0500 Subject: [PATCH 18/29] address code review, link to table #11747 --- doc/sphinx-guides/source/user/dataset-management.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/user/dataset-management.rst b/doc/sphinx-guides/source/user/dataset-management.rst index 40dcedcea9c..3c351d71222 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -851,7 +851,7 @@ Out of the box, all datasets have a dataset type of "dataset". Superusers can ad Once more than one type appears in search results, a facet called "Dataset Type" will appear allowing you to filter down to a certain type. -If your installation is configured to use DataCite as a persistent ID (PID) provider, the appropriate type ("Dataset", "Software", "Workflow", "Review") will be sent to DataCite when the dataset is published for those types. +If your installation is configured to use DataCite as a persistent ID (PID) provider, the dataset type will be sent to DataCite as ``resourceTypeGeneral``. See the table under :ref:`api-add-dataset-type` in the API Guide for details. Currently, specifying a type for a dataset can only be done via API and only when the dataset is created. The type can't currently be changed afterward. For details, see the following sections of the API guide: From 6994e22cfa40288f4c3ad2750ca4c871904b1faa Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 6 Feb 2026 15:54:04 -0500 Subject: [PATCH 19/29] for CSL citation, type can be dataset, software, or review #11747 Also, fix typo in API Guide. The format is CSL, not CSLJson. --- doc/sphinx-guides/source/api/native-api.rst | 4 ++-- .../edu/harvard/iq/dataverse/DataCitation.java | 4 ++-- .../iq/dataverse/api/DatasetTypesIT.java | 18 ++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 9 +++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 0a167c85e6e..55bc6655bd8 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3961,8 +3961,8 @@ Usage example: Get Citation In Other Formats ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Dataverse can also generate dataset citations in "EndNote", "RIS", "BibTeX", and "CSLJson" formats. -Unlike the call above, which wraps the result in JSON, this API call sends the raw format with the appropriate content-type (EndNote is XML, RIS and BibTeX are plain text, and CSLJson is JSON). ("Internal" is also a valid value, returning the same content as the above call as HTML). +Dataverse can also generate dataset citations in "EndNote", "RIS", "BibTeX", and "CSL" formats. +Unlike the call above, which wraps the result in JSON, this API call sends the raw format with the appropriate content-type (EndNote is XML, RIS and BibTeX are plain text, and CSL is JSON). ("Internal" is also a valid value, returning the same content as the above call as HTML). This API call adds a format parameter in the API call which can be any of the values listed above. Usage example: diff --git a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java index 1f6380f020f..57734911470 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataCitation.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java @@ -741,9 +741,9 @@ public Map getDataCiteMetadata() { public JsonObject getCSLJsonFormat() { CSLItemDataBuilder itemBuilder = new CSLItemDataBuilder(); // TODO consider making this a switch - if (type.equals(DatasetType.DATASET_TYPE_SOFTWARE)) { + if (type.getName().equals(DatasetType.DATASET_TYPE_SOFTWARE)) { itemBuilder.type(CSLType.SOFTWARE); - } else if (type.equals(DatasetType.DATASET_TYPE_REVIEW)) { + } else if (type.getName().equals(DatasetType.DATASET_TYPE_REVIEW)) { itemBuilder.type(CSLType.REVIEW); } else { itemBuilder.type(CSLType.DATASET); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 7861cf7666b..8f78a0a5b99 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -148,6 +148,12 @@ public void testCreateSoftwareDatasetNative() { .body("data.facets[0].datasetType.labels[1].software", CoreMatchers.is(1)) .statusCode(OK.getStatusCode()); + Response getCitationCsl = UtilIT.getDatasetVersionCitationFormat(datasetId, DS_VERSION_LATEST_PUBLISHED, false, "CSL", apiToken); + getCitationCsl.prettyPrint(); + getCitationCsl.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("type", equalTo("software")); + // Response searchAsGuest = UtilIT.search(SearchFields.DATASET_TYPE + ":software", null); // searchAsGuest.prettyPrint(); // searchAsGuest.then().assertThat() @@ -853,6 +859,12 @@ public void testCreateReview() { String datasetCitationHtml = JsonPath.from(getCitation.getBody().asString()).getString("data.message"); String datasetCitationText = StringUtil.html2text(datasetCitationHtml); + Response getCitationCsl = UtilIT.getDatasetVersionCitationFormat(datasetId, DS_VERSION_LATEST_PUBLISHED, false, "CSL", apiTokenReviewer); + getCitationCsl.prettyPrint(); + getCitationCsl.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("type", equalTo("dataset")); + /** * We are added the HTML version of a Related Dataset. We like the HTML * version because both JSF and the SPA render the DOI link as a @@ -971,6 +983,12 @@ public void testCreateReview() { UtilIT.publishDataverseViaNativeApi(collectionOfReviewsAlias, apiTokenReviewer).then().assertThat().statusCode(OK.getStatusCode()); UtilIT.publishDatasetViaNativeApi(reviewPid, "major", apiTokenReviewer).then().assertThat().statusCode(OK.getStatusCode()); + + Response getCitationCslReview = UtilIT.getDatasetVersionCitationFormat(reviewId, DS_VERSION_LATEST_PUBLISHED, false, "CSL", apiTokenReviewer); + getCitationCslReview.prettyPrint(); + getCitationCslReview.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("type", equalTo("review")); } @Test 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 d7b13c281f2..4a130b681db 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4258,6 +4258,15 @@ static Response getDatasetVersionCitation(Integer datasetId, String version, boo return response; } + static Response getDatasetVersionCitationFormat(Integer datasetId, String version, boolean includeDeaccessioned, String format, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .contentType("application/json") + .queryParam("includeDeaccessioned", includeDeaccessioned) + .get("/api/datasets/" + datasetId + "/versions/" + version + "/citation/" + format); + return response; + } + static Response setDatasetCitationDateField(String datasetIdOrPersistentId, String dateField, String apiToken) { String idInPath = datasetIdOrPersistentId; // Assume it's a number. String optionalQueryParam = ""; // If idOrPersistentId is a number we'll just put it in the path. From 3895cd657254ce98358297e68543a25f8be42d55 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 9 Feb 2026 11:26:41 -0500 Subject: [PATCH 20/29] remove refs to blocks deleted in 016929ca9 #11747 --- doc/sphinx-guides/source/user/appendix.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/sphinx-guides/source/user/appendix.rst b/doc/sphinx-guides/source/user/appendix.rst index 99a01fa41b0..d1c46a93fdf 100755 --- a/doc/sphinx-guides/source/user/appendix.rst +++ b/doc/sphinx-guides/source/user/appendix.rst @@ -43,8 +43,6 @@ Unlike supported metadata, experimental metadata is not enabled by default in a - Computational Workflow Metadata (`see .tsv `__): adapted from `Bioschemas Computational Workflow Profile, version 1.0 `__ and `Codemeta `__. - Archival Metadata (`see .tsv `__): Enables repositories to register metadata relating to the potential archiving of the dataset at a depositor archive, whether that be your own institutional archive or an external archive, i.e. a historical archive. - Local Contexts Metadata (`see .tsv `__): Supports integration with the `Local Contexts `__ platform, enabling the use of Traditional Knowledge and Biocultural Labels, and Notices. For more information on setup and configuration, see :doc:`../installation/localcontexts`. -- Trusted Data Dimensions and Intensities (`see .tsv `__): Enables repositories to indicate dimensions of trust. -- Repository Characteristics (`see .tsv `__): Details related to the security, sustainability, and certifications of the repository. Please note: these custom metadata schemas are not included in the Solr schema for indexing by default, you will need to add them as necessary for your custom metadata blocks. See "Update the Solr Schema" in :doc:`../admin/metadatacustomization`. From 20f9bb620e14ab364bc3d554b60cf475468ebb3b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 9 Feb 2026 17:21:46 -0500 Subject: [PATCH 21/29] add docs for review datasets, update docs for dataset types #11747 --- .../11747-review-dataset-type.md | 25 ++++- .../source/admin/dataverses-datasets.rst | 71 ++++++++++++ .../source/admin/metadatacustomization.rst | 2 + doc/sphinx-guides/source/api/changelog.rst | 1 + doc/sphinx-guides/source/api/native-api.rst | 26 ++--- doc/sphinx-guides/source/user/appendix.rst | 5 + .../source/user/dataset-management.rst | 102 ++++++++++++++++-- 7 files changed, 205 insertions(+), 27 deletions(-) diff --git a/doc/release-notes/11747-review-dataset-type.md b/doc/release-notes/11747-review-dataset-type.md index aa05d48dc66..7a67b84f5d0 100644 --- a/doc/release-notes/11747-review-dataset-type.md +++ b/doc/release-notes/11747-review-dataset-type.md @@ -1,3 +1,24 @@ -### New Dataset Type: Review +## Highlights -A new, experimental dataset type called "review" has been added. When this type is published, it will be sent to DataCite as "Other" for resourceTypeGeneral. See #11747. +### Review Datasets + +Dataverse now supports review datasets, a type of dataset that can be used to review resources such as other datasets in the Dataverse installation itself or various resources in external data repositories. APIs and a new "review" metadata block (with an "Item Reviewed" field) are in place but the UI for this feature will only available in a future version of the new React-based [Dataverse Frontend](https://github.com/IQSS/dataverse-frontend). See also the [guides](https://dataverse-guide--11753.org.readthedocs.build/en/11753/api/native-api.html#add-dataset-type), #11747, #12015, #11887, #12115, and #11753. + +## Other Features Added + +- Citation Style Language (CSL) output now includes "type:software" or "type:review" when those dataset types are used. See the [guides](https://dataverse-guide--11753.org.readthedocs.build/en/11753/api/native-api.html#get-citation-in-other-formats) and #11753. + +## Updated APIs + +- The Change Collection Attributes API now supports `allowedDatasetTypes`. See the [guides](https://dataverse-guide--11753.org.readthedocs.build/en/11753/api/native-api.html#change-collection-attributes), #12115, and #11753. + +## Bugs Fixed + +- 500 error when deleting dataset type by name. See #11833 and #11753. +- Dataset Type facet works in JSF but not the SPA. See #11758 and #11753. + +## Backward Incompatible Changes + +### Dataset Types Must Be Allowed, Per-Collection, Before Use + +In previous releases of Dataverse, as soon as additional dataset types were added (such as "software", "workflow", etc.), they could be used by all users when creating datasets (via API only). As of this release, on a per-collection basis, superusers must allow these dataset types to be used. See #12115 and #11753. diff --git a/doc/sphinx-guides/source/admin/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index c916b79aaa8..7b34453719f 100644 --- a/doc/sphinx-guides/source/admin/dataverses-datasets.rst +++ b/doc/sphinx-guides/source/admin/dataverses-datasets.rst @@ -109,6 +109,77 @@ If the :AllowedCurationLabels setting has a value, one of the available choices Individual datasets can be configured to use specific curationLabelSets as well. See the "Datasets" section below. +.. _review-datasets-setup: + +Configure a Collection for Review Datasets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ref:`review-datasets-user` are a specialized type of dataset that can be used to review resources (such as datasets) in the Dataverse installation itself or resources in external data repositories. + +Review datasets require some setup, as described below. + +Load the Review Metadata Block +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, download the Review metadata block tsv file from :ref:`experimental-metadata`. + +Then, load the block and update Solr. See the following sections of :doc:`metadatacustomization` for details: + +- :ref:`load-tsv` +- :ref:`update-solr-schema` + +Create a Collection for Reviews and Configure Permissions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Follow the normal steps: + +- :ref:`create-dataverse`. +- :ref:`dataverse-permissions`. + +Mark Fields in the "Review" Metadata Block as Required for the Collection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Follow the normal steps to edit the collection (:ref:`general-information`), then enable the Review block for that collection and make the following fields required for the collection you created: + +- URL (``itemReviewedUrl``) +- Type (``itemReviewedType``) + +There is a third field that you can optionally make required: + +- Citation (``itemReviewedCitation``) + +Create and Enable Custom "Rubric" Metadata Blocks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Review metadata block gives you a few basic fields common to all reviews such as the URL of the item being reviewed. + +You probably will want to create your own metadata blocks specific to the resources you are reviewing, your own "rubric". See :doc:`metadatacustomization` for details on creating and enabling custom metadata blocks. + +Instead of creating a new custom metadata block from scratch (if you simply want to evaluate the feature, for example), you can use the metadata blocks at https://github.com/IQSS/dataverse.harvard.edu + +After loading the block, don't forget to update the Solr schema! + +Create a Review Dataset Type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Review datasets are built on the :ref:`dataset-types` feature. Dataset types can only be created via API so follow the steps under :ref:`api-add-dataset-type`. The JSON you send will look something like this: + +``{"name":"review","displayName":"Review"}`` + +Do not send "linkedMetadataBlocks" or "availableLicenses" in the JSON when creating the dataset type. + +Allow the Review Dataset Type for the Collection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Non-dataset types, such as the "review" type, are only available when a collection admin has enabled them, via API. + +Using the API :ref:`collection-attributes-api`, change the ``allowedDatasetTypes`` attribute so that it includes "review". If you only want to allow reviews, you can pass just ``review``. If you want to allow multiple dataset types, you can pass a comma-separated list, such as ``review,dataset``. + +Invite Users to Create Review Datasets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At this point, users should be able to create review datasets via API, you gave them permission on the collection. You can point them to :ref:`creating-a-review-dataset` for details. + Datasets -------- diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 3cdee6d779a..e7875c247b2 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -444,6 +444,8 @@ Please note that metadata fields share a common namespace so they must be unique We'll use this command again below to update the Solr schema to accomodate metadata fields we've added. +.. _load-tsv: + Loading TSV files into a Dataverse Installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 4c7a5914b1e..1b49a0982f4 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -10,6 +10,7 @@ This API changelog is experimental and we would love feedback on its usefulness. v6.9 ---- +- When creating datasets that contain a datasetType, that datasetType must be allowed at the collection level. This can be accomplished by passing ``allowedDatasetTypes`` to the :ref:`collection-attributes-api` API. - The POST /api/admin/makeDataCount/{id}/updateCitationsForDataset processing is now asynchronous and the response no longer includes the number of citations. The response can be OK if the request is queued or 503 if the queue is full (default queue size is 1000). - The way to set per-format size limits for tabular ingest has changed. JSON input is now used. See :ref:`:TabularIngestSizeLimit`. - In the past, the settings API would accept any key and value. This is no longer the case because validation has been added. See :ref:`settings_put_single`, for example. diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 55bc6655bd8..4c99cafba5d 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1185,6 +1185,7 @@ The following attributes are supported: * ``affiliation`` Affiliation * ``filePIDsEnabled`` ("true" or "false") Restricted to use by superusers and only when the :ref:`:AllowEnablingFilePIDsPerCollection <:AllowEnablingFilePIDsPerCollection>` setting is true. Enables or disables registration of file-level PIDs in datasets within the collection (overriding the instance-wide setting). * ``requireFilesToPublishDataset`` ("true" or "false") Restricted to use by superusers. Defines if Dataset needs files in order to be published. If not set the determination will be made through inheritance by checking the owners of this collection. Publishing by a superusers will not be blocked. +* ``allowedDatasetTypes`` Restricted to use by superusers. By default "dataset" is implied. Pass a comma-separated list of dataset types (e.g. "dataset,software"). See also :ref:`dataset-types`. See also :ref:`update-dataverse-api`. @@ -3958,6 +3959,8 @@ Usage example: curl -H "Accept:application/json" "$SERVER_URL/api/datasets/:persistentId/versions/$VERSION/citation?persistentId=$PERSISTENT_IDENTIFIER&includeDeaccessioned=true" +.. _get-citation-in-other-formats: + Get Citation In Other Formats ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -3986,6 +3989,7 @@ Usage example: curl "$SERVER_URL/api/datasets/:persistentId/versions/$VERSION/citation/$FORMAT?persistentId=$PERSISTENT_IDENTIFIER&includeDeaccessioned=true" +The type under CSL can vary based on the dataset type, with "dataset", "software", and "review" as supported values. See also :ref:`dataset-types`. Get Citation by Preview URL Token ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -4239,25 +4243,9 @@ Add Dataset Type Note: Before you add any types of your own, there should be a single type called "dataset". -Adding certain dataset types will result in a value other than "Dataset" being sent to DataCite (if you use DataCite) as shown in the table below. - -.. list-table:: Values sent to DataCite for resourceTypeGeneral by Dataset Type - :header-rows: 1 - :stub-columns: 1 - :align: left - - * - Dataset Type - - Value sent to DataCite - * - dataset - - Dataset - * - software - - Software - * - workflow - - Workflow - * - review - - Other - -Other than sending a different resourceTypeGeneral to DataCite, the only functionality you gain currently from adding types is an entry in the "Dataset Type" facet but be advised that if you add a type other than "software", "workflow", or "review", you will need to add your new type to your Bundle.properties file for it to appear in Title Case rather than lower case in the "Dataset Type" facet. +Adding certain dataset types will result in a value other than "Dataset" being sent to DataCite (if you use DataCite), see :ref:`dataset-types-datacite` for details. + +Be advised that if you add a type other than "software", "workflow", or "review", you will need to add your new type to your Bundle.properties file for it to appear in Title Case rather than lower case in the "Dataset Type" facet. With all that said, we'll add a "software" type in the example below. This API endpoint is superuser only. The "name" of a type cannot be only digits. Note that this endpoint also allows you to add metadata blocks and available licenses for your new dataset type by adding "linkedMetadataBlocks" and/or "availableLicenses" arrays to your JSON. diff --git a/doc/sphinx-guides/source/user/appendix.rst b/doc/sphinx-guides/source/user/appendix.rst index d1c46a93fdf..e2c78e1e99c 100755 --- a/doc/sphinx-guides/source/user/appendix.rst +++ b/doc/sphinx-guides/source/user/appendix.rst @@ -17,6 +17,8 @@ The Dataverse Project is committed to using standard-compliant metadata to ensur metadata can be mapped easily to standard metadata schemas and be exported into JSON format (XML for tabular file metadata) for preservation and interoperability. +.. _supported-metadata: + Supported Metadata ~~~~~~~~~~~~~~~~~~ @@ -34,6 +36,8 @@ Detailed below are what metadata schemas we support for Citation and Domain Spec - Journal Metadata (`see .tsv `__): based on the `Journal Archiving and Interchange Tag Set, version 1.2 `__. - 3D Objects Metadata (`see .tsv `__). +.. _experimental-metadata: + Experimental Metadata ~~~~~~~~~~~~~~~~~~~~~ @@ -43,6 +47,7 @@ Unlike supported metadata, experimental metadata is not enabled by default in a - Computational Workflow Metadata (`see .tsv `__): adapted from `Bioschemas Computational Workflow Profile, version 1.0 `__ and `Codemeta `__. - Archival Metadata (`see .tsv `__): Enables repositories to register metadata relating to the potential archiving of the dataset at a depositor archive, whether that be your own institutional archive or an external archive, i.e. a historical archive. - Local Contexts Metadata (`see .tsv `__): Supports integration with the `Local Contexts `__ platform, enabling the use of Traditional Knowledge and Biocultural Labels, and Notices. For more information on setup and configuration, see :doc:`../installation/localcontexts`. +- Review Metadata (`see .tsv `__): For :ref:`review-datasets-user`. Please note: these custom metadata schemas are not included in the Solr schema for indexing by default, you will need to add them as necessary for your custom metadata blocks. See "Update the Solr Schema" in :doc:`../admin/metadatacustomization`. diff --git a/doc/sphinx-guides/source/user/dataset-management.rst b/doc/sphinx-guides/source/user/dataset-management.rst index 3c351d71222..61104bf5151 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -847,21 +847,111 @@ Dataset Types .. note:: Development of the dataset types feature is ongoing. Please see https://github.com/IQSS/dataverse-pm/issues/307 for details. -Out of the box, all datasets have a dataset type of "dataset". Superusers can add additional types such as "software", "workflow", or "review" using the :ref:`api-add-dataset-type` API endpoint. +The vision for dataset types is to have variations on datasets. The best documented use case is :ref:`review-datasets-user`, as explained below, but other types of datasets are possible such as software datasets (see :ref:`api-add-dataset-type` for an example) or workflow datasets. -Once more than one type appears in search results, a facet called "Dataset Type" will appear allowing you to filter down to a certain type. +Out of the box, all datasets have a dataset type of "dataset", which is the traditional dataset in Dataverse. Superusers can add additional types using the :ref:`api-add-dataset-type` API endpoint. These additional dataset types cannot be used until a superuser has allowed them on a per-collection basis using the :ref:`collection-attributes-api` API endpoint (by passing ``allowedDatasetTypes``). -If your installation is configured to use DataCite as a persistent ID (PID) provider, the dataset type will be sent to DataCite as ``resourceTypeGeneral``. See the table under :ref:`api-add-dataset-type` in the API Guide for details. +Dataset types can be listed, added, or deleted via API. See :ref:`api-dataset-types` in the API Guide for more. -Currently, specifying a type for a dataset can only be done via API and only when the dataset is created. The type can't currently be changed afterward. For details, see the following sections of the API guide: +Currently, specifying a type for a dataset can only be done via API and only when the dataset is created. (The type can't be changed afterward.) For details, see the following sections of the API guide: - :ref:`api-create-dataset-with-type` (Native API) - :ref:`api-semantic-create-dataset-with-type` (Semantic API) - :ref:`import-dataset-with-type` -Dataset types can be listed, added, or deleted via API. See :ref:`api-dataset-types` in the API Guide for more. +Once more than one type appears in dataset search results, a facet called "Dataset Type" will appear allowing you to filter down to a certain type. + +Dataset types can be linked with metadata blocks to make fields from those blocks available when datasets of that type are created or edited (via API). See :ref:`api-link-dataset-type` and :ref:`list-metadata-blocks-for-a-collection` for details. + +Dataset types can change the "type" in Citation Style Language (CSL) output. See :ref:`get-citation-in-other-formats` for details. + +If your installation is configured to use DataCite as a persistent ID (PID) provider, the dataset type may be sent to DataCite as ``resourceTypeGeneral``. See the table under :ref:`dataset-types-datacite` for details. + +.. _review-datasets-user: + +Review Datasets +--------------- + +.. _review-datasets-overview: + +Review Dataset Overview +~~~~~~~~~~~~~~~~~~~~~~~ + +Review datasets are a specialized type of dataset that can be used to review resources (such as datasets) in the Dataverse installation itself or resources in external data repositories. + +This feature is only available via API and only if it has been configured by a superuser for your collection. See :ref:`review-datasets-setup` for details. + +In the recommended setup, a collection is created that is managed by a research community, typically approved at the installation level. + +In a typical use case, the reviews will be generated by these research communities based on the aggregation of scores for a particular domain by community-identified experts. These scores are stored in a custom metadata block, a rubric. An additional metadata block is required to hold information about the review itself, such a pointer to the resource being reviewed. + +We recommend implementing a policy where there is only one review of a given resource per collection. + +Almost all functionality is the same between regular datasets and review datasets. Review datasets build upon existing dataset functionality such as custom metadata blocks, versioning, publishing workflows, permissions, and file handling. + +Review datasets build on the :ref:`dataset-types` feature, allowing users to choose a dataset type of "review", which leads to a couple differences from regular datasets. + +First, when multiple dataset types exist, a "Dataset Type" search facet appears that allows users to narrow results to the various kinds of dataset types that have been added, such as dataset, review, software, workflow, etc. (Under the "Collections, Datasets, Files" area, review datasets are considered datasets.) + +Second, when review datasets are published, different ``resourceType`` metadata is sent to DataCite. Review datasets send "Other" for the field resourceTypeGeneral ("Work Type" in the UI at https://commons.datacite.org). See the table under :ref:`dataset-types-datacite` for details. + +The following table summaries how regular datasets compare to review datasets. + +.. list-table:: Differences between regular and review datasets + :header-rows: 1 + :stub-columns: 1 + :align: left + + * - + - Regular Dataset + - Review Dataset + * - Collections/Datasets/Files search facet + - dataset + - dataset + * - Dataset Type search facet + - dataset + - review + * - DataCite + - See table under :ref:`api-add-dataset-type` + - See table under :ref:`api-add-dataset-type` + +.. _creating-a-review-dataset: + +Creating a Review Dataset +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can only create a review dataset if setup has already been done by a superuser. See :ref:`review-datasets-setup` for details. + +Review Datasets can only be created via API. You have the following options: + +- :ref:`api-create-dataset-with-type` (Native API) +- :ref:`api-semantic-create-dataset-with-type` (Semantic API) +- :ref:`import-dataset-with-type` + +When creating a review dataset you will likely need to fill in required fields like ``itemReviewedUrl`` as well as fields from one or more "rubric" metadata blocks, as described above under :ref:`review-datasets-overview`. + +.. _dataset-types-datacite: + +Dataset Types and DataCite +-------------------------- + +Adding certain dataset types will result in a value other than "Dataset" being sent to DataCite (if you use DataCite) as shown in the table below. + +.. list-table:: Values sent to DataCite for resourceTypeGeneral by Dataset Type + :header-rows: 1 + :stub-columns: 1 + :align: left -Dataset types can be linked with metadata blocks to make fields from those blocks available when datasets of that type are created or edited. See :ref:`api-link-dataset-type` and :ref:`list-metadata-blocks-for-a-collection` for details. + * - Dataset Type + - Value sent to DataCite + * - dataset + - Dataset + * - software + - Software + * - workflow + - Workflow + * - review + - Other .. |image1| image:: ./img/DatasetDiagram.png :class: img-responsive From 8091c45da64c6b47e249d0883cff92962c6621f3 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 10 Feb 2026 11:59:08 -0500 Subject: [PATCH 22/29] assert resourceTypeGeneral=Other for review datasets #11747 --- .../java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 8f78a0a5b99..8a871c97832 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -989,6 +989,12 @@ public void testCreateReview() { getCitationCslReview.then().assertThat() .statusCode(OK.getStatusCode()) .body("type", equalTo("review")); + + Response exportDatacite = UtilIT.exportDataset(reviewPid, "Datacite", false, "1.0", apiTokenReviewer); + exportDatacite.prettyPrint(); + exportDatacite.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("resource.resourceType.@resourceTypeGeneral", CoreMatchers.equalTo("Other")); } @Test From e65d76dc614c39d95f1760f94673d25b081f7366 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 10 Feb 2026 12:29:39 -0500 Subject: [PATCH 23/29] send Review to DataCite #11747 Also, add note about how "Data Type" (kindOfData) metadata field is used in resourceType for DataCite exports. See also https://github.com/datacite/datacite-suggestions/discussions/214 --- doc/sphinx-guides/source/user/dataset-management.rst | 10 ++++++---- .../pidproviders/doi/XmlMetadataTemplate.java | 7 +++++-- .../edu/harvard/iq/dataverse/api/DatasetTypesIT.java | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/sphinx-guides/source/user/dataset-management.rst b/doc/sphinx-guides/source/user/dataset-management.rst index 61104bf5151..7d54aa1df54 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -945,13 +945,15 @@ Adding certain dataset types will result in a value other than "Dataset" being s * - Dataset Type - Value sent to DataCite * - dataset - - Dataset + - * - software - - Software + - * - workflow - - Workflow + - * - review - - Other + - Review + +Note that the value for resourceType (which is either empty or "Review", as shown above) can be overridden by values in the "Data Type" (``kindOfData``) metadata field. .. |image1| image:: ./img/DatasetDiagram.png :class: img-responsive 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 efc81d4179f..461b583ffe9 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 @@ -836,13 +836,14 @@ private void writeResourceType(XMLStreamWriter xmlw, DvObject dvObject) throws X List kindOfDataValues = new ArrayList(); Map attributes = new HashMap(); String resourceType = "Dataset"; + String datasetTypeName = null; if (dvObject instanceof Dataset dataset) { - String datasetTypeName = dataset.getDatasetType().getName(); + datasetTypeName = dataset.getDatasetType().getName(); resourceType = switch (datasetTypeName) { case DatasetType.DATASET_TYPE_DATASET -> "Dataset"; case DatasetType.DATASET_TYPE_SOFTWARE -> "Software"; case DatasetType.DATASET_TYPE_WORKFLOW -> "Workflow"; - // "Other" for now but we might ask DataCite to support https://schema.org/CriticReview + // "Other" for now but we might ask DataCite to support "Review" case DatasetType.DATASET_TYPE_REVIEW -> "Other"; default -> "Dataset"; }; @@ -866,6 +867,8 @@ private void writeResourceType(XMLStreamWriter xmlw, DvObject dvObject) throws X if (!kindOfDataValues.isEmpty()) { XmlWriterUtil.writeFullElementWithAttributes(xmlw, "resourceType", attributes, String.join(";", kindOfDataValues)); + } else if (DatasetType.DATASET_TYPE_REVIEW.equals(datasetTypeName)) { + XmlWriterUtil.writeFullElementWithAttributes(xmlw, "resourceType", attributes, "Review"); } else { // Write an attribute only element if there are no kindOfData values. xmlw.writeStartElement("resourceType"); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 8a871c97832..cdb4d6fb36f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -994,6 +994,7 @@ public void testCreateReview() { exportDatacite.prettyPrint(); exportDatacite.then().assertThat() .statusCode(OK.getStatusCode()) + .body("resource.resourceType", CoreMatchers.equalTo("Review")) .body("resource.resourceType.@resourceTypeGeneral", CoreMatchers.equalTo("Other")); } From d2f12dfd9eec3d782e68493b6f9293fecaf60b9e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 12 Feb 2026 11:35:50 -0500 Subject: [PATCH 24/29] switch allowedDatasetTypes in entity from String to a ManyToMany List #12115 --- doc/sphinx-guides/source/api/native-api.rst | 4 +-- .../edu/harvard/iq/dataverse/Dataverse.java | 8 +++-- .../impl/AbstractCreateDatasetCommand.java | 26 ++++++---------- .../impl/UpdateDataverseAttributeCommand.java | 5 ++- .../iq/dataverse/util/json/JsonPrinter.java | 14 +++++++-- src/main/resources/db/migration/V6.9.0.1.sql | 2 -- .../iq/dataverse/api/DatasetTypesIT.java | 31 +++++++++++++++++-- 7 files changed, 60 insertions(+), 30 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 4c99cafba5d..a1874aa1501 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1185,7 +1185,7 @@ The following attributes are supported: * ``affiliation`` Affiliation * ``filePIDsEnabled`` ("true" or "false") Restricted to use by superusers and only when the :ref:`:AllowEnablingFilePIDsPerCollection <:AllowEnablingFilePIDsPerCollection>` setting is true. Enables or disables registration of file-level PIDs in datasets within the collection (overriding the instance-wide setting). * ``requireFilesToPublishDataset`` ("true" or "false") Restricted to use by superusers. Defines if Dataset needs files in order to be published. If not set the determination will be made through inheritance by checking the owners of this collection. Publishing by a superusers will not be blocked. -* ``allowedDatasetTypes`` Restricted to use by superusers. By default "dataset" is implied. Pass a comma-separated list of dataset types (e.g. "dataset,software"). See also :ref:`dataset-types`. +* ``allowedDatasetTypes`` Restricted to use by superusers. By default "dataset" is implied. Pass a comma-separated list of dataset types (e.g. "dataset,software"). You cannot unset this attribute so if you want to delete a dataset type, set ``allowedDatasetTypes`` to a dataset type you won't be deleting. See also :ref:`dataset-types`. See also :ref:`update-dataverse-api`. @@ -4268,7 +4268,7 @@ The fully expanded example above (without environment variables) looks like this Delete Dataset Type ^^^^^^^^^^^^^^^^^^^ -Superuser only. +Superuser only. Note that if a collection has the type listed as an allowed dataset type, you will be unable to delete the dataset type until you first use the :ref:`collection-attributes-api` to change ``allowedDatasetTypes`` to a dataset type (or dataset types) that you are not trying to delete. .. code-block:: bash diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index d6d9256d6c4..ea27bbd7f8e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearch; import edu.harvard.iq.dataverse.storageuse.StorageUse; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -171,7 +172,8 @@ public String getIndexableCategoryName() { * If null, only the default dataset type (dataset) is allowed. * See AbstractCreateDatasetCommand. */ - private String allowedDatasetTypes; + @ManyToMany(cascade = {CascadeType.MERGE}) + private List allowedDatasetTypes = new ArrayList<>(); ///private String storageDriver=null; @@ -785,11 +787,11 @@ public void setAffiliation(String affiliation) { this.affiliation = affiliation; } - public String getAllowedDatasetTypes() { + public List getAllowedDatasetTypes() { return allowedDatasetTypes; } - public void setAllowedDatasetTypes(String allowedDatasetTypes) { + public void setAllowedDatasetTypes(List allowedDatasetTypes) { this.allowedDatasetTypes = allowedDatasetTypes; } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java index 3e522a549e5..075df88d672 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java @@ -126,31 +126,25 @@ public Dataset execute(CommandContext ctxt) throws CommandException { } DatasetType defaultDatasetType = ctxt.datasetTypes().getByName(DatasetType.DEFAULT_DATASET_TYPE); + // Why is this called existingDatasetType? Would incomingDatasetType make more sense? DatasetType existingDatasetType = theDataset.getDatasetType(); logger.fine("existing dataset type: " + existingDatasetType); if (existingDatasetType != null) { - List allowedDatasetTypes = new ArrayList<>(); - String allowedDatasetTypesString = theDataset.getOwner().getAllowedDatasetTypes(); - if (allowedDatasetTypesString == null) { + List allowedByCollection = theDataset.getOwner().getAllowedDatasetTypes(); + // Final because we apply some logic first + List allowedDatasetTypesFinal = new ArrayList<>(); + if (allowedByCollection.isEmpty()) { // If allowedDatasetTypes is unspecified, assume // only the default type (dataset) is allowed - allowedDatasetTypes.add(defaultDatasetType); + allowedDatasetTypesFinal.add(defaultDatasetType); } else { - // Turn comma-separated String into actual values - String[] allowedDatasetTypeNames = allowedDatasetTypesString.split(","); - for (String datasetTypeName : allowedDatasetTypeNames) { - DatasetType datasetType = ctxt.datasetTypes().getByName(datasetTypeName.trim()); - if (datasetType != null) { - allowedDatasetTypes.add(datasetType); - } else { - logger.warning("Could not find datasetType based on " + datasetTypeName); - } - } + allowedDatasetTypesFinal.addAll(allowedByCollection); } - if (allowedDatasetTypes.contains(existingDatasetType)) { + // Set type if allowed. Otherwise, return error. + if (allowedDatasetTypesFinal.contains(existingDatasetType)) { theDataset.setDatasetType(existingDatasetType); } else { - List typeNames = allowedDatasetTypes.stream() + List typeNames = allowedDatasetTypesFinal.stream() .map(DatasetType::getName) .toList(); Map fieldErrors = new HashMap<>(); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java index aa9f72dad0b..6fdbfa59f69 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseAttributeCommand.java @@ -137,6 +137,7 @@ private void setAllowedDatasetTypes(CommandContext ctxt, Object allowedDatasetTy this); } + List allowedDatasetTypes = new ArrayList<>(); List invalidDatasetTypes = new ArrayList<>(); String[] allowedDatasetTypeNames = stringValue.split(","); @@ -144,6 +145,8 @@ private void setAllowedDatasetTypes(CommandContext ctxt, Object allowedDatasetTy DatasetType datasetType = ctxt.datasetTypes().getByName(datasetTypeName.trim()); if (datasetType == null) { invalidDatasetTypes.add(datasetTypeName); + } else { + allowedDatasetTypes.add(datasetType); } } @@ -155,7 +158,7 @@ private void setAllowedDatasetTypes(CommandContext ctxt, Object allowedDatasetTy + ATTRIBUTE_ALLOWED_DATASET_TYPES + " is invalid.", this, fieldErrors); } - dataverse.setAllowedDatasetTypes(stringValue); + dataverse.setAllowedDatasetTypes(allowedDatasetTypes); } } 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 350445f138e..6b54e363923 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 @@ -358,9 +358,17 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean re if (childCount != null) { bld.add("childCount", childCount); } - String allowedDatasetTypes = dv.getAllowedDatasetTypes(); - if (allowedDatasetTypes != null) { - bld.add("allowedDatasetTypes", allowedDatasetTypes); + List allowedDatasetTypes = dv.getAllowedDatasetTypes(); + if (allowedDatasetTypes != null && !allowedDatasetTypes.isEmpty()) { + JsonArrayBuilder jab = Json.createArrayBuilder(); + for (DatasetType datasetType : allowedDatasetTypes) { + NullSafeJsonBuilder json = NullSafeJsonBuilder.jsonObjectBuilder() + .add("name", datasetType.getName()) + .add("displayName", datasetType.getDisplayName()) + .add("description", datasetType.getDescription()); + jab.add(json); + } + bld.add("allowedDatasetTypes", jab); } addDatasetFileCountLimit(dv, bld); return bld; diff --git a/src/main/resources/db/migration/V6.9.0.1.sql b/src/main/resources/db/migration/V6.9.0.1.sql index 9862916d576..4d838a8381d 100644 --- a/src/main/resources/db/migration/V6.9.0.1.sql +++ b/src/main/resources/db/migration/V6.9.0.1.sql @@ -6,5 +6,3 @@ UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS description VARCHAR(255); -- Set description for dataset UPDATE datasettype SET description = 'A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; --- at collection level, control which dataset types are allowed -ALTER TABLE dataverse ADD COLUMN IF NOT EXISTS alloweddatasettypes VARCHAR(255); \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index cdb4d6fb36f..1a1a477352a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -66,6 +66,8 @@ private static void ensureDatasetTypeIsPresent(String datasetType, String apiTok String jsonIn = Json.createObjectBuilder() .add("name", datasetType) .add("displayName", displayName) + // Obviously, a better description should be passed in real life. + .add("description", "The " + displayName + " dataset type.") .build().toString(); Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); typeAdded.prettyPrint(); @@ -76,13 +78,19 @@ private static String capitalize(String stringIn) { return stringIn.substring(0, 1).toUpperCase() + stringIn.substring(1); } + /** + * @param allowedDatasetTypes comma separated (e.g. "dataset,software") + */ private void setAllowedDatasetTypes(String dataverseAlias, String allowedDatasetTypes) { + String[] allowedDatasetTypeNames = allowedDatasetTypes.split(","); Response setAllowedDatasetTypes = UtilIT.setCollectionAttribute(dataverseAlias, "allowedDatasetTypes", allowedDatasetTypes, apiTokenSuperuser); setAllowedDatasetTypes.prettyPrint(); setAllowedDatasetTypes.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.allowedDatasetTypes", is(allowedDatasetTypes)); + // Just test the first name. (We only have the name to test with, + // as an argument to this menthod.) They should be in order. + .body("data.allowedDatasetTypes[0].name", is(allowedDatasetTypeNames[0])); } @Test @@ -746,7 +754,12 @@ public void testCreateDatasetWithCustomType() { Response deleteDatasetResponse = UtilIT.deleteDatasetViaNativeApi(datasetId, apiToken); deleteDatasetResponse.prettyPrint(); assertEquals(200, deleteDatasetResponse.getStatusCode()); - + + // We are about to delete the dataset type "testDatasetType" but first we need to + // disassociate it from the collection. We do this by associating a dataset type + // that we aren't deleting ("dataset", the default dataset type). + setAllowedDatasetTypes(dataverseAlias, "dataset"); + Long doomed = JsonPath.from(typeAdded.getBody().asString()).getLong("data.id"); System.out.println("doomed: " + doomed); @@ -967,7 +980,19 @@ public void testCreateReview() { ) )); - setAllowedDatasetTypes(collectionOfReviewsAlias, "review"); + /** + * We could just call `setAllowedDatasetTypes(collectionOfReviewsAlias, + * "review")` like other places in the code, but here we are making assertions + * on the first (and only) object under "allowedDatasetTypes". + */ + Response setAllowedDatasetTypes = UtilIT.setCollectionAttribute(collectionOfReviewsAlias, "allowedDatasetTypes", + "review", apiTokenSuperuser); + setAllowedDatasetTypes.prettyPrint(); + setAllowedDatasetTypes.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.allowedDatasetTypes[0].name", is("review")) + .body("data.allowedDatasetTypes[0].displayName", is("Review")) + .body("data.allowedDatasetTypes[0].description", is("The Review dataset type.")); Response createReview = UtilIT.createDataset(collectionOfReviewsAlias, jsonForCreatingReview, apiTokenReviewer); createReview.prettyPrint(); From 060215f447a483f780933f4af4584db6e33ca33b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 12 Feb 2026 14:40:32 -0500 Subject: [PATCH 25/29] improve description of a dataset Co-authored-by: Julian Gautier --- src/main/java/propertyFiles/datasetTypes.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/propertyFiles/datasetTypes.properties b/src/main/java/propertyFiles/datasetTypes.properties index 35794a7a17c..5e88b894f4a 100644 --- a/src/main/java/propertyFiles/datasetTypes.properties +++ b/src/main/java/propertyFiles/datasetTypes.properties @@ -6,4 +6,4 @@ # However, if you are running in additional languages, you should # add the additional dataset types to this file. dataset.displayName=Dataset -dataset.description=A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files. \ No newline at end of file +dataset.description=A study, experiment, set of observations, or publication. A dataset can comprise a single file or multiple files. \ No newline at end of file From ca8a7c617d3e765729d2b27eb2c9f3f945d9bc4f Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 12 Feb 2026 14:48:09 -0500 Subject: [PATCH 26/29] shorten description of dataset and match properties file Co-authored-by: Julian Gautier --- src/main/resources/db/migration/V6.9.0.1.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/db/migration/V6.9.0.1.sql b/src/main/resources/db/migration/V6.9.0.1.sql index 4d838a8381d..09cba0f6b93 100644 --- a/src/main/resources/db/migration/V6.9.0.1.sql +++ b/src/main/resources/db/migration/V6.9.0.1.sql @@ -5,4 +5,4 @@ UPDATE datasettype SET displayname = 'Dataset' WHERE name = 'dataset'; -- Add description column to datasettype table ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS description VARCHAR(255); -- Set description for dataset -UPDATE datasettype SET description = 'A study, experiment, set of observations, or publication that is uploaded by a user. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; +UPDATE datasettype SET description = 'A study, experiment, set of observations, or publication. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; From 828094798e5e9836fc61b6338f57788a29836ba8 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 12 Feb 2026 15:41:41 -0500 Subject: [PATCH 27/29] cleanup: logging, javadoc, and comments #11747 --- .../iq/dataverse/dataset/DatasetType.java | 4 ++-- .../harvard/iq/dataverse/i18n/i18nUtil.java | 21 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index df525601c12..a3e1f18ba46 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java @@ -182,7 +182,7 @@ public String getDisplayName(Locale locale) { logger.fine("Looking up " + name + ".displayName in " + propertiesFile); return BundleUtil.getStringFromPropertyFile(name + ".displayName", "datasetTypes", locale); } catch (MissingResourceException e) { - logger.warning(name + ".displayName missing from " + propertiesFile + " (or file does not exist). Returning English version."); + logger.fine(name + ".displayName missing from " + propertiesFile + " (or file does not exist). Returning English version."); return displayName; } } @@ -208,7 +208,7 @@ public String getDescription(Locale locale) { logger.fine("Looking up " + name + ".description in " + propertiesFile); return BundleUtil.getStringFromPropertyFile(name + ".description", "datasetTypes", locale); } catch (MissingResourceException e) { - logger.warning(name + ".description missing from " + propertiesFile + " (or file does not exist). Returning English version."); + logger.fine(name + ".description missing from " + propertiesFile + " (or file does not exist). Returning English version."); return description; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java b/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java index 11f855256e1..f3b05a6773d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java @@ -5,6 +5,27 @@ public class i18nUtil { + /** + * A comment from poikilotherm from + * https://github.com/IQSS/dataverse/pull/11753#discussion_r2787986962 + * + * IMHO any parsing of the locale should be done using JAX-RS mechanisms to + * follow DRY principle. + * + * Solution A) Keep @HeaderParam, but make it a Locale, moving the parsing to a + * ParamConverter. This is still a lot of repeated boilerplate code. + * + * Solution B) Have a @Context HttpHeaders parameter give you access via + * headers.getAcceptableLanguages() or a @Context Request give you access via + * request.getLanguage() to the Locale without manual parsing. Still some + * boilerplate per method + * + * Solution C) Create a CDI @Producer method that is @RequestScoped, receiving + * the Locale as a class field @Inject Locale. This would be greatly enhanced by + * adding an annotation like @ClientLocale to be used as qualifier for both + * field and producer method. This is the least boilerplate code. + */ + /** * @param acceptLanguageHeader The Accept-Language header value such as * "Accept-Language: en-US,en;q=0.5" 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 4a130b681db..2b2c6e0bc10 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4757,7 +4757,7 @@ public static Response getDatasetTypes() { public static Response getDatasetTypes(String acceptLanguage) { RequestSpecification requestSpecification = given(); if (acceptLanguage != null) { - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language requestSpecification.header(ACCEPT_LANGUAGE, acceptLanguage); } return requestSpecification.get("/api/datasets/datasetTypes"); From f48b48e6cd08e1acc08717d72a19ee851a139ba4 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 12 Feb 2026 16:00:55 -0500 Subject: [PATCH 28/29] rename class: first character upper case --- .../edu/harvard/iq/dataverse/api/Datasets.java | 6 +++--- .../i18n/{i18nUtil.java => I18nUtil.java} | 2 +- .../i18n/{i18nUtilTest.java => I18NUtilTest.java} | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/main/java/edu/harvard/iq/dataverse/i18n/{i18nUtil.java => I18nUtil.java} (98%) rename src/test/java/edu/harvard/iq/dataverse/i18n/{i18nUtilTest.java => I18NUtilTest.java} (70%) 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 c0446de1861..d1bd96e97ae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -35,7 +35,7 @@ import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler; import edu.harvard.iq.dataverse.globus.GlobusServiceBean; import edu.harvard.iq.dataverse.globus.GlobusUtil; -import edu.harvard.iq.dataverse.i18n.i18nUtil; +import edu.harvard.iq.dataverse.i18n.I18nUtil; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.ingest.IngestUtil; import edu.harvard.iq.dataverse.makedatacount.*; @@ -5720,7 +5720,7 @@ public Response resetPidGenerator(@Context ContainerRequestContext crc, @PathPar @GET @Path("datasetTypes") public Response getDatasetTypes(@HeaderParam(ACCEPT_LANGUAGE) String acceptLanguage) { - Locale locale = i18nUtil.parseAcceptLanguageHeader(acceptLanguage); + Locale locale = I18nUtil.parseAcceptLanguageHeader(acceptLanguage); JsonArrayBuilder jab = Json.createArrayBuilder(); for (DatasetType datasetType : datasetTypeSvc.listAll()) { jab.add(datasetType.toJson(locale)); @@ -5731,7 +5731,7 @@ public Response getDatasetTypes(@HeaderParam(ACCEPT_LANGUAGE) String acceptLangu @GET @Path("datasetTypes/{idOrName}") public Response getDatasetTypes(@PathParam("idOrName") String idOrName, @HeaderParam(ACCEPT_LANGUAGE) String acceptLanguage) { - Locale locale = i18nUtil.parseAcceptLanguageHeader(acceptLanguage); + Locale locale = I18nUtil.parseAcceptLanguageHeader(acceptLanguage); DatasetType datasetType = null; if (StringUtils.isNumeric(idOrName)) { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java b/src/main/java/edu/harvard/iq/dataverse/i18n/I18nUtil.java similarity index 98% rename from src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java rename to src/main/java/edu/harvard/iq/dataverse/i18n/I18nUtil.java index f3b05a6773d..d7531f60278 100644 --- a/src/main/java/edu/harvard/iq/dataverse/i18n/i18nUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/i18n/I18nUtil.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Locale; -public class i18nUtil { +public class I18nUtil { /** * A comment from poikilotherm from diff --git a/src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/i18n/I18NUtilTest.java similarity index 70% rename from src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java rename to src/test/java/edu/harvard/iq/dataverse/i18n/I18NUtilTest.java index 99e20e52433..10eadf3082b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/i18n/i18nUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/i18n/I18NUtilTest.java @@ -4,41 +4,41 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.Locale; -public class i18nUtilTest { +public class I18NUtilTest { @Test void testParseAcceptLanguageHeader_singleLanguage() { - Locale locale = i18nUtil.parseAcceptLanguageHeader("en-US"); + Locale locale = I18nUtil.parseAcceptLanguageHeader("en-US"); assertEquals(Locale.forLanguageTag("en-US"), locale); } @Test void testParseAcceptLanguageHeader_singleLanguageWithQ() { - Locale locale = i18nUtil.parseAcceptLanguageHeader("en-US,en;q=0.5"); + Locale locale = I18nUtil.parseAcceptLanguageHeader("en-US,en;q=0.5"); assertEquals(Locale.forLanguageTag("en-US"), locale); } @Test void testParseAcceptLanguageHeader_multipleLanguages() { - Locale locale = i18nUtil.parseAcceptLanguageHeader("fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4"); + Locale locale = I18nUtil.parseAcceptLanguageHeader("fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4"); assertEquals(Locale.forLanguageTag("fr-CA"), locale); } @Test void testParseAcceptLanguageHeader_emptyHeader() { - Locale locale = i18nUtil.parseAcceptLanguageHeader(""); + Locale locale = I18nUtil.parseAcceptLanguageHeader(""); assertNull(locale); } @Test void testParseAcceptLanguageHeader_nullHeader() { - Locale locale = i18nUtil.parseAcceptLanguageHeader(null); + Locale locale = I18nUtil.parseAcceptLanguageHeader(null); assertNull(locale); } @Test void testParseAcceptLanguageHeader_invalidHeader() { - Locale locale = i18nUtil.parseAcceptLanguageHeader("invalid-header"); + Locale locale = I18nUtil.parseAcceptLanguageHeader("invalid-header"); assertEquals(Locale.forLanguageTag("invalid-header"), locale); } } \ No newline at end of file From 403a32d7317fa0ac8f18102336c723ca20a6d18c Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 12 Feb 2026 16:54:45 -0500 Subject: [PATCH 29/29] a description for "review" dataset type --- .../source/admin/dataverses-datasets.rst | 7 +++--- scripts/api/data/datasetTypes/review.json | 5 ++++ .../iq/dataverse/api/DatasetTypesIT.java | 24 +++++++++++++------ 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 scripts/api/data/datasetTypes/review.json diff --git a/doc/sphinx-guides/source/admin/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index 7b34453719f..a8fc8bc9deb 100644 --- a/doc/sphinx-guides/source/admin/dataverses-datasets.rst +++ b/doc/sphinx-guides/source/admin/dataverses-datasets.rst @@ -162,11 +162,12 @@ After loading the block, don't forget to update the Solr schema! Create a Review Dataset Type ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Review datasets are built on the :ref:`dataset-types` feature. Dataset types can only be created via API so follow the steps under :ref:`api-add-dataset-type`. The JSON you send will look something like this: +Review datasets are built on the :ref:`dataset-types` feature. Dataset types can only be created via API so follow the steps under :ref:`api-add-dataset-type`. Copy and paste from below or download :download:`review.json <../../../../scripts/api/data/datasetTypes/review.json>` and pass it to the API. -``{"name":"review","displayName":"Review"}`` +.. literalinclude:: ../../../../scripts/api/data/datasetTypes/review.json + :language: json -Do not send "linkedMetadataBlocks" or "availableLicenses" in the JSON when creating the dataset type. +We suggest using neither "linkedMetadataBlocks" nor "availableLicenses" in the JSON when creating the dataset type. Again, the JSON above is suggested. Allow the Review Dataset Type for the Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/scripts/api/data/datasetTypes/review.json b/scripts/api/data/datasetTypes/review.json new file mode 100644 index 00000000000..44e421ef31f --- /dev/null +++ b/scripts/api/data/datasetTypes/review.json @@ -0,0 +1,5 @@ +{ + "name": "review", + "displayName": "Review", + "description": "A review of a dataset compiled by community experts." +} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java index 1a1a477352a..26747ff506e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java @@ -3,6 +3,8 @@ import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_LATEST_PUBLISHED; import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.util.StringUtil; +import edu.harvard.iq.dataverse.util.json.JsonUtil; + import static io.restassured.path.json.JsonPath.with; import io.restassured.RestAssured; import io.restassured.path.json.JsonPath; @@ -13,6 +15,8 @@ import static jakarta.ws.rs.core.Response.Status.CREATED; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.OK; + +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.UUID; @@ -42,9 +46,16 @@ public static void setUpClass() { apiTokenSuperuser = UtilIT.getApiTokenFromResponse(createUser); UtilIT.setSuperuserStatus(usernameSuperuser, true).then().assertThat().statusCode(OK.getStatusCode()); - ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_SOFTWARE, apiTokenSuperuser); - ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, apiTokenSuperuser); - ensureDatasetTypeIsPresent(INSTRUMENT, apiTokenSuperuser); + // This description for software is shortened from https://datacite-metadata-schema.readthedocs.io/en/4.5/appendices/appendix-1/resourceTypeGeneral/#software + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_SOFTWARE, "A computer program in either source code (text) or compiled form.", apiTokenSuperuser); + String reviewDescription = null; + try { + reviewDescription = JsonUtil.getJsonObjectFromFile("scripts/api/data/datasetTypes/review.json").getString("description"); + } catch (IOException e) { + } + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, reviewDescription, apiTokenSuperuser); + // This description for instrument is from https://datacite-metadata-schema.readthedocs.io/en/4.5/appendices/appendix-1/resourceTypeGeneral/#instrument + ensureDatasetTypeIsPresent(INSTRUMENT, "A device, tool or apparatus used to obtain, measure and/or analyze data.", apiTokenSuperuser); } @AfterAll @@ -53,7 +64,7 @@ public static void afterClass() { UtilIT.setDisplayOnCreate("astroInstrument", false); } - private static void ensureDatasetTypeIsPresent(String datasetType, String apiToken) { + private static void ensureDatasetTypeIsPresent(String datasetType, String description, String apiToken) { Response getDatasetType = UtilIT.getDatasetType(datasetType); getDatasetType.prettyPrint(); String typeFound = JsonPath.from(getDatasetType.getBody().asString()).getString("data.name"); @@ -66,8 +77,7 @@ private static void ensureDatasetTypeIsPresent(String datasetType, String apiTok String jsonIn = Json.createObjectBuilder() .add("name", datasetType) .add("displayName", displayName) - // Obviously, a better description should be passed in real life. - .add("description", "The " + displayName + " dataset type.") + .add("description", description) .build().toString(); Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); typeAdded.prettyPrint(); @@ -992,7 +1002,7 @@ public void testCreateReview() { .statusCode(OK.getStatusCode()) .body("data.allowedDatasetTypes[0].name", is("review")) .body("data.allowedDatasetTypes[0].displayName", is("Review")) - .body("data.allowedDatasetTypes[0].description", is("The Review dataset type.")); + .body("data.allowedDatasetTypes[0].description", is("A review of a dataset compiled by community experts.")); Response createReview = UtilIT.createDataset(collectionOfReviewsAlias, jsonForCreatingReview, apiTokenReviewer); createReview.prettyPrint();