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..7a67b84f5d0 --- /dev/null +++ b/doc/release-notes/11747-review-dataset-type.md @@ -0,0 +1,24 @@ +## Highlights + +### 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/_static/api/dataset-create-review.json b/doc/sphinx-guides/source/_static/api/dataset-create-review.json new file mode 100644 index 00000000000..e7a507ad39c --- /dev/null +++ b/doc/sphinx-guides/source/_static/api/dataset-create-review.json @@ -0,0 +1,99 @@ +{ + "datasetType": "review", + "datasetVersion": { + "license": { + "name": "CC0 1.0", + "uri": "http://creativecommons.org/publicdomain/zero/1.0" + }, + "metadataBlocks": { + "citation": { + "fields": [ + { + "typeName": "title", + "value": "Review of Percent of Children That Have Asthma", + "typeClass": "primitive", + "multiple": false + }, + { + "value": [ + { + "authorName": { + "value": "Wazowski, Mike", + "typeClass": "primitive", + "multiple": false, + "typeName": "authorName" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "author" + }, + { + "value": [ + { + "datasetContactEmail": { + "value": "mwazowski@mailinator.com", + "typeClass": "primitive", + "multiple": false, + "typeName": "datasetContactEmail" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "datasetContact" + }, + { + "value": [ + { + "dsDescriptionValue": { + "value": "This is a review of a dataset.", + "typeClass": "primitive", + "multiple": false, + "typeName": "dsDescriptionValue" + } + } + ], + "typeClass": "compound", + "multiple": true, + "typeName": "dsDescription" + }, + { + "value": [ + "Medicine, Health and Life Sciences" + ], + "typeClass": "controlledVocabulary", + "multiple": true, + "typeName": "subject" + }, + { + "value": { + "itemReviewedUrl": { + "value": "https://datacommons.org/tools/statvar#sv=Percent_Person_Children_WithAsthma", + "typeClass": "primitive", + "multiple": false, + "typeName": "itemReviewedUrl" + }, + "itemReviewedType": { + "value": "Dataset", + "typeClass": "controlledVocabulary", + "multiple": false, + "typeName": "itemReviewedType" + }, + "itemReviewedCitation": { + "value": "\"Statistical Variable Explorer - Data Commons.\" Datacommons.org, 2026, datacommons.org/tools/statvar#sv=Percent_Person_Children_WithAsthma. Accessed 9 Mar. 2026.", + "typeClass": "primitive", + "multiple": false, + "typeName": "itemReviewedCitation" + } + }, + "typeClass": "compound", + "multiple": false, + "typeName": "itemReviewed" + } + ] + } + } + } +} diff --git a/doc/sphinx-guides/source/admin/dataverses-datasets.rst b/doc/sphinx-guides/source/admin/dataverses-datasets.rst index c916b79aaa8..9696c758b04 100644 --- a/doc/sphinx-guides/source/admin/dataverses-datasets.rst +++ b/doc/sphinx-guides/source/admin/dataverses-datasets.rst @@ -109,6 +109,64 @@ 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 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`. Copy and paste from below or download :download:`review.json <../../../../scripts/api/data/datasetTypes/review.json>` and pass it to the API. + +.. literalinclude:: ../../../../scripts/api/data/datasetTypes/review.json + :language: json + +Create a Collection for Reviews and Configure Permissions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Follow the normal steps: + +- :ref:`create-dataverse`. +- :ref:`dataverse-permissions`. + +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, if 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 3a0f7215da9..b6f2d2e1cbd 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1015,8 +1015,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. @@ -1070,8 +1070,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. @@ -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"). 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`. @@ -3966,11 +3967,13 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -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: @@ -3994,6 +3997,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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -4246,30 +4250,46 @@ 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. +The default dataset type is "dataset" and ships with Dataverse. + +Only superusers can add additional dataset types. Once added, they can only be used if a collection has been configured to allow them (see ``allowedDatasetTypes`` under :ref:`collection-attributes-api`). + +Here's an example of all available fields when creating a dataset type: + +.. literalinclude:: ../../../../scripts/api/data/datasetTypes/datasetTypeAllFields.json + :language: json -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. +Here's a description of each field: + +- ``name`` (required): Machine-readable name. Cannot be only digits. +- ``displayName`` (required): Human-readable name. +- ``description``: A description. +- ``linkedMetadataBlocks``: Linking a dataset type with one or more metadata blocks results in additional fields from those blocks appearing in the output from the :ref:`list-metadata-blocks-for-a-collection` API endpoint. Use the machine-readable names of the blocks. See :ref:`api-link-dataset-type` for details. +- ``availableLicenses``: Limits the dataset type to certain licenses. For example, a "software" dataset type could be limited to "MIT" and "Apache-2.0". See :ref:`dataset-types-set-available-licenses` for details. + +Download the :download:`datasetTypeAllFields.json <../../../../scripts/api/data/datasetTypes/datasetTypeAllFields.json>` file show above, edit it to suit your needs, and use it in the following command. .. code-block:: bash export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx export SERVER_URL=https://demo.dataverse.org - export JSON='{"name":"software","linkedMetadataBlocks":["codeMeta20"],"availableLicenses":["MIT", "Apache-2.0"]}' - curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-Type: application/json" "$SERVER_URL/api/datasets/datasetTypes" -X POST -d $JSON + curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-Type: application/json" "$SERVER_URL/api/datasets/datasetTypes" -X POST --upload-file datasetTypeAllFields.json The fully expanded example above (without environment variables) looks like this: .. code-block:: bash - curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -H "Content-Type: application/json" "https://demo.dataverse.org/api/datasets/datasetTypes" -X POST -d '{"name":"software","linkedMetadataBlocks":["codeMeta20"],"availableLicenses":["MIT", "Apache-2.0"]}' + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -H "Content-Type: application/json" "https://demo.dataverse.org/api/datasets/datasetTypes" -X POST --upload-file datasetTypeAllFields.json + +Note that 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. .. _api-delete-dataset-type: 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 @@ -4315,6 +4335,8 @@ To update the blocks that are linked, send an array with those blocks. To remove all links to blocks, send an empty array. +.. _dataset-types-set-available-licenses: + Set Available Licenses for a Dataset Type ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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 3c5111082ea..f3cd9c6a533 100755 --- a/doc/sphinx-guides/source/user/dataset-management.rst +++ b/doc/sphinx-guides/source/user/dataset-management.rst @@ -847,21 +847,116 @@ 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. +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 research data related 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 appropriate type ("Dataset", "Software", "Workflow") will be sent to DataCite when the dataset is published for those three types. +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 also `upstream schema `_). 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) and "Review" as the ``resourceType`` value. See the table under :ref:`dataset-types-datacite` for details and comparison. Please note that we are not using the "PeerReview" for ``resourceTypeGeneral`` because it is (in our view) specific to scholarly communications and may carry related connotations. + +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) + + - Here is an example JSON file for reference: :download:`dataset-create-review.json <../_static/api/dataset-create-review.json>`. + +- :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 Type + - Value sent to DataCite + * - dataset + - + * - software + - + * - workflow + - + * - review + - Review -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. +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/scripts/api/data/datasetTypes/datasetTypeAllFields.json b/scripts/api/data/datasetTypes/datasetTypeAllFields.json new file mode 100644 index 00000000000..571decfa2b0 --- /dev/null +++ b/scripts/api/data/datasetTypes/datasetTypeAllFields.json @@ -0,0 +1,13 @@ +{ + "name": "foobar", + "displayName": "Foobar", + "description": "The best dataset type!", + "linkedMetadataBlocks": [ + "block1", + "block2" + ], + "availableLicenses": [ + "license1", + "license2" + ] +} diff --git a/scripts/api/data/datasetTypes/review.json b/scripts/api/data/datasetTypes/review.json new file mode 100644 index 00000000000..79c3dd91fcd --- /dev/null +++ b/scripts/api/data/datasetTypes/review.json @@ -0,0 +1,8 @@ +{ + "name": "review", + "displayName": "Review", + "description": "A review of a dataset compiled by the expert community.", + "linkedMetadataBlocks": [ + "review" + ] +} diff --git a/scripts/api/data/metadatablocks/review.tsv b/scripts/api/data/metadatablocks/review.tsv new file mode 100644 index 00000000000..89215153109 --- /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 TRUE TRUE review + itemReviewedUrl URL The URL of the item being reviewed url 2 FALSE FALSE FALSE FALSE TRUE TRUE itemReviewed review + itemReviewedType Type The type of the item being reviewed text 3 FALSE TRUE FALSE FALSE TRUE TRUE itemReviewed review + itemReviewedCitation Citation The full bibliographic citation of the item being reviewed textbox 4 FALSE FALSE FALSE FALSE TRUE TRUE 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/main/java/edu/harvard/iq/dataverse/DataCitation.java b/src/main/java/edu/harvard/iq/dataverse/DataCitation.java index 30d9928f59a..57734911470 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(); - if (type.equals(DatasetType.DATASET_TYPE_SOFTWARE)) { + // TODO consider making this a switch + if (type.getName().equals(DatasetType.DATASET_TYPE_SOFTWARE)) { itemBuilder.type(CSLType.SOFTWARE); + } else if (type.getName().equals(DatasetType.DATASET_TYPE_REVIEW)) { + itemBuilder.type(CSLType.REVIEW); } else { itemBuilder.type(CSLType.DATASET); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index c85ebb90a43..da7328e3b66 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -116,6 +116,7 @@ import java.util.logging.Level; 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.impl.AbstractSubmitToArchiveCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetLinkingDataverseCommand; @@ -4103,8 +4104,15 @@ public String save() { return null; } } - populateDatasetUpdateFailureMessage(); - return returnToDraftVersion(); + if (ex instanceof InvalidFieldsCommandException) { + InvalidFieldsCommandException ifce = (InvalidFieldsCommandException) ex; + String error = ifce.getFieldErrors().get("datasetType"); + JsfHelper.addErrorMessage(error); + return null; + } else { + populateDatasetUpdateFailureMessage(); + return returnToDraftVersion(); + } } // Have we just deleted some draft datafiles (successfully)? diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index 98c3e965dad..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; @@ -166,6 +167,13 @@ public String getIndexableCategoryName() { } private String affiliation; + + /** + * If null, only the default dataset type (dataset) is allowed. + * See AbstractCreateDatasetCommand. + */ + @ManyToMany(cascade = {CascadeType.MERGE}) + private List allowedDatasetTypes = new ArrayList<>(); ///private String storageDriver=null; @@ -779,6 +787,14 @@ public void setAffiliation(String affiliation) { this.affiliation = affiliation; } + public List getAllowedDatasetTypes() { + return allowedDatasetTypes; + } + + public void setAllowedDatasetTypes(List allowedDatasetTypes) { + this.allowedDatasetTypes = allowedDatasetTypes; + } + public boolean isMetadataBlockRoot() { return metadataBlockRoot; } 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 b6688a8143b..5e6630776d3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -35,6 +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.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.ingest.IngestUtil; import edu.harvard.iq.dataverse.makedatacount.*; @@ -100,13 +101,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; @@ -5737,17 +5737,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 { @@ -5760,7 +5762,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); } @@ -5785,6 +5787,8 @@ 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<>(); @@ -5793,6 +5797,8 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json try { 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()) { @@ -5830,6 +5836,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."); @@ -5838,12 +5847,17 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json try { DatasetType datasetType = new DatasetType(); datasetType.setName(nameIn); + datasetType.setDisplayName(displayNameIn); + datasetType.setDescription(descriptionIn); datasetType.setMetadataBlocks(metadataBlocksToSave); datasetType.setLicenses(licensesToSave); 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()); } @@ -5871,7 +5885,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/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java index c55324f66e3..e07e80f7dd3 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,8 @@ 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; @@ -19,6 +21,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,19 +41,37 @@ 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"; + public static final String DATASET_TYPE_REVIEW = "review"; public static final String DEFAULT_DATASET_TYPE = DATASET_TYPE_DATASET; @Id @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, 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. */ @@ -80,6 +103,30 @@ 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; + } + + 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; } @@ -96,7 +143,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()); @@ -105,11 +152,96 @@ public JsonObjectBuilder toJson() { 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); } + 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.fine(name + ".displayName missing from " + propertiesFile + " (or file does not exist). Returning English version."); + 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.fine(name + ".description missing from " + propertiesFile + " (or file does not exist). Returning English version."); + return description; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DatasetType other = (DatasetType) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + } 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..aae8e886abb 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; @@ -120,14 +125,41 @@ public Dataset execute(CommandContext ctxt) throws CommandException { pidProvider.generatePid(theDataset); } + DatasetType datasetTypeToPersist = theDataset.getDatasetType(); DatasetType defaultDatasetType = ctxt.datasetTypes().getByName(DatasetType.DEFAULT_DATASET_TYPE); - 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); + // The datasetType is only sent via API. JSF doesn't send a type. + DatasetType dsTypeFromApi = theDataset.getDatasetType(); + logger.fine("dataset type sent via API: " + dsTypeFromApi); + if (dsTypeFromApi != null) { + datasetTypeToPersist = dsTypeFromApi; } else { - theDataset.setDatasetType(defaultDatasetType); + // If the API didn't set the dataset type or if the dataset + // is being created in JSF, use the default datasetType. + datasetTypeToPersist = defaultDatasetType; + } + theDataset.setDatasetType(datasetTypeToPersist); + + // Check if the datasetType is allowed by the collection + 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 + allowedDatasetTypesFinal.add(defaultDatasetType); + } else { + allowedDatasetTypesFinal.addAll(allowedByCollection); + } + // Throw error if type isn't allowed + if (!allowedDatasetTypesFinal.contains(datasetTypeToPersist)) { + List typeNames = allowedDatasetTypesFinal.stream() + .map(DatasetType::getName) + .toList(); + Map fieldErrors = new HashMap<>(); + fieldErrors.put("datasetType", "The parent collection does not allow the datasetType " + + datasetTypeToPersist.getName() + ". Allowed types: " + String.join(", ", typeNames)); + throw new InvalidFieldsCommandException("The dataset could not be created due to the datasetType.", + this, fieldErrors); } // Attempt the registration if importing dataset through the API, or the app (but not harvest) 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..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 @@ -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,39 @@ 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 allowedDatasetTypes = new ArrayList<>(); + 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); + } else { + allowedDatasetTypes.add(datasetType); + } + } + + 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(allowedDatasetTypes); + } + } 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..d7531f60278 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/i18n/I18nUtil.java @@ -0,0 +1,46 @@ +package edu.harvard.iq.dataverse.i18n; + +import java.util.List; +import java.util.Locale; + +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" + * @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/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java b/src/main/java/edu/harvard/iq/dataverse/pidproviders/doi/XmlMetadataTemplate.java index 0499f8e9cde..1d14b89e11a 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,12 +836,18 @@ 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"; + // We are not using the “PeerReview” for resourceTypeGeneral because it is + // specific to scholarly communications and may carry related connotations. + // We've asked DataCite to support "Review" so we don't have to use "Other". + // See also https://github.com/datacite/datacite-suggestions/discussions/214 + case DatasetType.DATASET_TYPE_REVIEW -> "Other"; default -> "Dataset"; }; } @@ -864,6 +870,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/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/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 11a3e7b53d8..d1c1f9b8294 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,18 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean re if (childCount != null) { bld.add("childCount", childCount); } + 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/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 7f4518e65bd..f21ad2ecfed 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/main/java/propertyFiles/datasetTypes.properties b/src/main/java/propertyFiles/datasetTypes.properties new file mode 100644 index 00000000000..5e88b894f4a --- /dev/null +++ b/src/main/java/propertyFiles/datasetTypes.properties @@ -0,0 +1,9 @@ +# 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 +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 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 diff --git a/src/main/resources/db/migration/V6.9.0.1.sql b/src/main/resources/db/migration/V6.9.0.1.sql new file mode 100644 index 00000000000..bfff4224976 --- /dev/null +++ b/src/main/resources/db/migration/V6.9.0.1.sql @@ -0,0 +1,8 @@ +-- Add displayname column to datasettype table +ALTER TABLE datasettype ADD COLUMN IF NOT EXISTS displayname VARCHAR(255) NOT NULL DEFAULT ''; +-- Populate displayname with name but capitalize it (name=dataset becomes displayname=Dataset) +UPDATE datasettype SET displayname = CONCAT(UPPER(SUBSTRING(name, 1, 1)), SUBSTRING(name, 2)); +-- 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. A dataset can comprise a single file or multiple files.' WHERE name = 'dataset'; 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 @@ + > 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/ReviewsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java new file mode 100644 index 00000000000..2fc74c4b3f1 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/ReviewsIT.java @@ -0,0 +1,347 @@ +package edu.harvard.iq.dataverse.api; + +import edu.harvard.iq.dataverse.dataset.DatasetType; +import edu.harvard.iq.dataverse.util.json.JsonUtil; +import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; +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 static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * This test class has not been added to the API test suite at + * tests/integration-tests.txt because it relies on the review.tsv which is not + * loaded out of the box. When we start loading review.tsv for new installations + * of Dataverse (and stop putting it under "experiemental" on the list of + * metadata blocks in the guides), we'll add this test class to the API test + * suite. + * + * To run these tests, manually load review.tsv (or temporarily set + * loadReviewTsv to true below) and update Solr. Be advised that there are + * other places in the API test suite that make assertions on the number of + * metadata blocks. Again, some day we might ship Dataverse with review.tsv + * already loaded. + */ +public class ReviewsIT { + + private static String apiTokenSuperuser; + + @BeforeAll + public static void setUpClass() { + RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat().statusCode(OK.getStatusCode()); + String usernameSuperuser = UtilIT.getUsernameFromResponse(createUser); + apiTokenSuperuser = UtilIT.getApiTokenFromResponse(createUser); + UtilIT.setSuperuserStatus(usernameSuperuser, true).then().assertThat().statusCode(OK.getStatusCode()); + + byte[] reviewTsv = null; + try { + reviewTsv = Files.readAllBytes(Paths.get("scripts/api/data/metadatablocks/review.tsv")); + } catch (IOException e) { + } + + // See warnings above. If you enable this, don't forget to update Solr. + boolean loadReviewTsv = false; + if (loadReviewTsv) { + Response response = UtilIT.loadMetadataBlock(apiTokenSuperuser, reviewTsv); + response.prettyPrint(); + assertEquals(200, response.getStatusCode()); + response.then().assertThat().statusCode(OK.getStatusCode()); + } + + 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, apiTokenSuperuser); + + String reviewDescription = null; + try { + reviewDescription = JsonUtil.getJsonObjectFromFile("scripts/api/data/datasetTypes/review.json").getString("description"); + } catch (IOException e) { + } + ensureDatasetTypeIsPresent(DatasetType.DATASET_TYPE_REVIEW, "Review", reviewDescription, apiTokenSuperuser); + } + + private static void ensureDatasetTypeIsPresent(String name, String displayName, String description, + String apiToken) { + Response getDatasetType = UtilIT.getDatasetType(name); + getDatasetType.prettyPrint(); + 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."); + } + String jsonIn = NullSafeJsonBuilder.jsonObjectBuilder() + .add("name", name) + .add("displayName", displayName) + .add("description", description) + .add("linkedMetadataBlocks", Json.createArrayBuilder() + .add("review") + ) + .build().toString(); + // System.out.println(JsonUtil.prettyPrint(jsonIn)); + Response typeAdded = UtilIT.addDatasetType(jsonIn, apiToken); + typeAdded.prettyPrint(); + typeAdded.then().assertThat().statusCode(OK.getStatusCode()); + } + + @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 setAllowedDatasetTypes = UtilIT.setCollectionAttribute(dataverseAlias, "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("A review of a dataset compiled by the expert community.")); + + String itemReviewedTitle = "Percent of Children That Have Asthma"; + String itemReviewedUrl = "https://datacommons.org/tools/statvar#sv=Percent_Person_Children_WithAsthma"; + // This citation came from https://www.mybib.com + String itemReviewedCitation = "\"Statistical Variable Explorer - Data Commons.\" Datacommons.org, 2026, datacommons.org/tools/statvar#sv=Percent_Person_Children_WithAsthma. Accessed 9 Mar. 2026."; + 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("itemReviewedCitation", + Json.createObjectBuilder() + .add("value", itemReviewedCitation) + .add("typeClass", "primitive") + .add("multiple", false) + .add("typeName", "itemReviewedCitation"))) + .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"); + + } + + /** + * In this test, we check if temReviewedUrl and itemReviewedType are required. (They are subfields of itemReviewed.) In review.tsv they are set to required. + */ + @Test + public void testCreateReviewRequiredFields() { + + 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 setAllowedDatasetTypes = UtilIT.setCollectionAttribute(dataverseAlias, "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("A review of a dataset compiled by the expert community.")); + + 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")))))); + + Response createReview = UtilIT.createDataset(dataverseAlias, jsonForCreatingReview, apiToken); + createReview.prettyPrint(); + // FIXME: The review was created but it shouldn't have been because + // required fields were not supplied. In review.tsv various fields + // are required. See https://github.com/IQSS/dataverse/issues/12196 + createReview.then().assertThat().statusCode(CREATED.getStatusCode()); + Integer reviewId = UtilIT.getDatasetIdFromResponse(createReview); + String reviewPid = JsonPath.from(createReview.getBody().asString()).getString("data.persistentId"); + + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java index ca9e19e1bbf..04b552ded5f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java @@ -119,6 +119,7 @@ public void testSearchPermisions() { .body("data.total_count", CoreMatchers.is(1)) .body("data.count_in_response", CoreMatchers.is(1)) .body("data.items[0].name", CoreMatchers.is("Darwin's Finches")) + // Note that "Unpublished" and "Draft" are in English. That's how they are indexed. .body("data.items[0].publicationStatuses", CoreMatchers.hasItems("Unpublished", "Draft")) .statusCode(OK.getStatusCode()); 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 24ab8b56eff..2b2c6e0bc10 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; @@ -4257,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. @@ -4741,14 +4751,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..10eadf3082b --- /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