Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6bc880d
#11912 WIP
sekmiller Jan 29, 2026
c2124bc
Merge branch 'develop' into 11912-edit-template-api
sekmiller Jan 29, 2026
9cd0905
#11912 add commands, etc.
sekmiller Feb 2, 2026
a314c7b
#11912 fix endpoints
sekmiller Feb 3, 2026
f7c0424
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 4, 2026
cefb654
#11912 update instructions, etc.
sekmiller Feb 5, 2026
a905b7c
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 5, 2026
84c50cc
#11912 update tests
sekmiller Feb 5, 2026
2fb6083
#11912 update name
sekmiller Feb 5, 2026
ede1c84
#11912 more tests
sekmiller Feb 6, 2026
9bdcae5
#11912 fix command/tests
sekmiller Feb 9, 2026
1afe95b
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 9, 2026
d8b484f
#11912 update terms test
sekmiller Feb 9, 2026
f40d44d
#11912 simplify endpoint
sekmiller Feb 11, 2026
2840982
#11912 add command for update access
sekmiller Feb 12, 2026
23f08a9
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 12, 2026
8ce04ec
#11912 fix update and tests
sekmiller Feb 12, 2026
38fba3c
#11912 add unit tests
sekmiller Feb 12, 2026
9776524
#11912 release notes and cleanup
sekmiller Feb 12, 2026
0b7f7e5
#11912 typo
sekmiller Feb 12, 2026
61d0594
#11912 sample json for update template
sekmiller Feb 12, 2026
92463aa
Update native-api.rst
sekmiller Feb 12, 2026
2c2e6a8
#11912 return test clean up
sekmiller Feb 12, 2026
deb8f28
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 13, 2026
c8fb718
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 13, 2026
68e82e6
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 18, 2026
8f93821
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 20, 2026
49f60c8
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 24, 2026
3052ce5
#11912 remove license with custom terms
sekmiller Feb 24, 2026
0eba7d0
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 25, 2026
4916813
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 26, 2026
414f0ca
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 26, 2026
6fbc176
Merge branch 'develop' into 11912-edit-template-api
sekmiller Feb 27, 2026
d3f5e0c
#11912 CR suggestions
sekmiller Mar 2, 2026
9893ca1
#11912 remove extraneous comments
sekmiller Mar 2, 2026
27ce2fb
#11912 allow to omit fields
sekmiller Mar 4, 2026
cbec678
Merge branch 'develop' into 11912-edit-template-api
sekmiller Mar 4, 2026
d098c2e
#11912 allow partial template updates
sekmiller Mar 4, 2026
7f6c9fa
Merge branch 'develop' into 11912-edit-template-api
sekmiller Mar 9, 2026
055ae30
Merge branch 'develop' into 11912-edit-template-api
sekmiller Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions doc/release-notes/11912-edit-template-apis
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## New Endpoint: PUT `/dataverses/{templateId}/metadata`

A new endpoint has been implemented to edit the metadata and field instructions for a given template.

### Functionality
- Updates the metadata and field instructions for a template based on a json file provided.
- You must have edit dataverse permission in the collection in order to use this endpoint.

## New Endpoint: PUT `/dataverses/{templateId}/licenseTerms`

A new endpoint has been implemented to edit the license or custom terms of use for a given template.

### Functionality
- Updates the license or custom terms of use for a template based on a json file provided.
- You must have edit dataverse permission in the collection in order to use this endpoint.

## New Endpoint: PUT `/dataverses/{templateId}/access`

A new endpoint has been implemented to edit the terms of access for a given template.

### Functionality
- Updates the terms of access for a template based on a json file provided.
- You must have edit dataverse permission in the collection in order to use this endpoint.
13 changes: 13 additions & 0 deletions doc/sphinx-guides/source/_static/api/template-update-access.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"customTermsOfAccess": {
"fileAccessRequest": false,
"termsOfAccess": "Here are the terms...",
"dataAccessPlace": "dataAccessPlace",
"originalArchive": "originalArchive",
"availabilityStatus": "availabilityStatus",
"contactForAccess": "contactForAccess",
"sizeOfCollection": "sizeOfCollection",
"studyCompletion": "studyCompletion",
"confidentialityDeclaration": "confidentialityDeclaration"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "name": "CC BY 4.0" }
30 changes: 30 additions & 0 deletions doc/sphinx-guides/source/_static/api/template-update-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "Dataverse template - edited",
"fields": [
{
"typeName": "author",
"value": [
{
"authorName": {
"typeName": "authorName",
"value": "Brady, Tom"
},
"authorAffiliation": {
"typeName": "authorIdentifierScheme",
"value": "ORCID"
}
}
]
}
],
"instructions": [
{
"instructionField": "author",
"instructionText": "The author data, edited"
},
{
"instructionField": "subtitle",
"instructionText": "Instructions for subtitle"
}
]
}
12 changes: 12 additions & 0 deletions doc/sphinx-guides/source/_static/api/template-update-terms.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"customTerms": {
"termsOfUse": "testTermsOfUse",
"confidentialityDeclaration": "testConfidentialityDeclaration",
"specialPermissions": "testSpecialPermissions",
"restrictions": "testRestrictions",
"citationRequirements": "testCitationRequirements",
"depositorRequirements": "testDepositorRequirements",
"conditions": "testConditions",
"disclaimer": "testDisclaimer"
}
}
63 changes: 63 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,69 @@ The fully expanded example above (without environment variables) looks like this

curl -H "X-Dataverse-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/dataverses/1/templates" --upload-file dataverse-template.json

Update the Metadata and Instructions of a Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Updates the metadata and instructions of a template with a given ``id``.

To update the template, you must send a JSON file. Your JSON file might look like :download:`template-update-metadata.json <../_static/api/template-update-metadata.json>` which you would send to the Dataverse installation like this:

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=1

curl -H "X-Dataverse-key: $API_TOKEN" -X PUT "$SERVER_URL/api/dataverses/{ID}/metadata" --upload-file template-update-metadata.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" -X PUT "https://demo.dataverse.org/api/dataverses/1/metadata" --upload-file template-update-metadata.json

Update the License or Terms Of Use of a Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Updates the license or custom terms of use of a template with a given ``id``.

To update the template, you must send a JSON file containing either the name of an active license or custom terms of use. Your JSON file might look like :download:`template-update-license.json <../_static/api/template-update-license.json>` or :download:`template-update-terms.json <../_static/api/template-update-terms.json>` which you would send to the Dataverse installation like this:

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=1

curl -H "X-Dataverse-key: $API_TOKEN" -X PUT "$SERVER_URL/api/dataverses/{ID}/licenseTerms" --upload-file template-update-license.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" -X PUT "https://demo.dataverse.org/api/dataverses/1/licenseTerms" --upload-file template-update-license.json

Update the Terms Of Access of a Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Updates the terms of access of a template with a given ``id``.

To update the template, you must send a JSON file containing either the name of an active license or custom terms of use. Your JSON file might look like :download:`template-update-access.json <../_static/api/template-update-access.json>` which you would send to the Dataverse installation like this:

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=1

curl -H "X-Dataverse-key: $API_TOKEN" -X PUT "$SERVER_URL/api/dataverses/{ID}/access" --upload-file template-update-access.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" -X PUT "https://demo.dataverse.org/api/dataverses/1/access" --upload-file template-update-access.json

Set a Default Template for a Collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
15 changes: 14 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ String getWrappedMessageWhenJson() {

@EJB
GuestbookResponseServiceBean gbRespSvc;

@EJB
TemplateServiceBean templateSvc;

@Inject
FailedPIDResolutionLoggingServiceBean fprLogService;
Expand Down Expand Up @@ -370,8 +373,18 @@ protected Dataverse findDataverseOrDie( String dvIdtf ) throws WrappedResponse {
}
return dv;
}

protected Template findTemplateOrDie(Long templateId) throws WrappedResponse {

Template template = templateSvc.find(templateId);
if (template == null) {
throw new WrappedResponse(
error(Response.Status.NOT_FOUND, "Can't find template with identifier='" + templateId + "'"));
}
return template;
}

protected Template findTemplateOrDie(Long templateId, Dataverse dataverse) throws WrappedResponse {
protected Template findTemplateInDataverseOrParentsOrDie(Long templateId, Dataverse dataverse) throws WrappedResponse {

List<Template> templates = new ArrayList<>();

Expand Down
114 changes: 111 additions & 3 deletions src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
import edu.harvard.iq.dataverse.engine.command.impl.*;
import edu.harvard.iq.dataverse.license.License;
import edu.harvard.iq.dataverse.pidproviders.PidProvider;
import edu.harvard.iq.dataverse.pidproviders.PidUtil;
import edu.harvard.iq.dataverse.settings.JvmSettings;
Expand Down Expand Up @@ -72,6 +73,7 @@
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
import jakarta.ws.rs.core.StreamingOutput;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
Expand Down Expand Up @@ -2016,9 +2018,9 @@ public Response getTemplate(@Context ContainerRequestContext crc, @PathParam("id
public Response createTemplate(@Context ContainerRequestContext crc, String body, @PathParam("identifier") String dvIdtf) {
try {
Dataverse dataverse = findDataverseOrDie(dvIdtf);
NewTemplateDTO newTemplateDTO;
TemplateDTO newTemplateDTO;
try {
newTemplateDTO = NewTemplateDTO.fromRequestBody(body, jsonParser());
newTemplateDTO = TemplateDTO.fromRequestBody(body, jsonParser());
} catch (JsonParseException ex) {
return error(Status.BAD_REQUEST, MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.createTemplate.error.jsonParseMetadataFields"), ex.getMessage()));
}
Expand All @@ -2030,6 +2032,112 @@ public Response createTemplate(@Context ContainerRequestContext crc, String body
return e.getResponse();
}
}

@PUT
@AuthRequired
@Path("{templateId}/metadata")
public Response updateTemplateMetadata(@Context ContainerRequestContext crc, String body, @PathParam("templateId") Long templateId, @QueryParam("replace") boolean replaceData) {
try {
Template template = findTemplateOrDie(templateId);
Dataverse dataverse = template.getDataverse();

JsonObject json = JsonUtil.getJsonObject(body);

/*
You can also set a new name for your template in the json
*/

if (json.containsKey("name") && !json.getString("name").isBlank()) {
template.setName(json.getString("name"));
}


List<DatasetField> updatedFields = new ArrayList<>();
//if it doesn't contain fields, instructions or name it better have a single dataset field
//to be updated
if (json.getJsonArray("fields") == null) {
if (!json.containsKey("instructions") && !json.containsKey("name")){
updatedFields.add(jsonParser().parseField(json, Boolean.FALSE, replaceData));
}
} else {
updatedFields = jsonParser().parseMultipleFields(json, replaceData);
}

Map<String, String> instructionsMap = jsonParser().parseRequestBodyInstructionsMap(json);

Template updated = execCommand(new UpdateTemplateFieldsCommand(template, dataverse, updatedFields, instructionsMap, replaceData, createDataverseRequest(getRequestUser(crc))));

return created("/dataverses/template/" + updated.getId(), jsonTemplate(updated));
} catch (JsonParseException ex) {
logger.log(Level.SEVERE, "Semantic error parsing dataset update Json: " + ex.getMessage(), ex);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a nit: a json parsing error is syntactic not semantic - maybe just "Error parsing..."

return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("datasets.api.editMetadata.error.parseUpdate", List.of(ex.getMessage())));


} catch (WrappedResponse e) {
return e.getResponse();
}
}

@PUT
@AuthRequired
@Path("{templateId}/licenseTerms")
public Response updateTemplateLicenseTerms(@Context ContainerRequestContext crc, LicenseUpdateRequest requestBody, @PathParam("templateId") Long templateId, @QueryParam("replace") boolean replaceData) {
try {
Template template = findTemplateOrDie(templateId);
Dataverse dataverse = template.getDataverse();

if (requestBody.getName() != null && !requestBody.getName().isBlank()) {
String licenseName = requestBody.getName();
License license = licenseSvc.getByNameOrUri(licenseName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably out of scope - this method is supposed to allow using a name or URI (which is actually meant as an identifier) but using LicenseUpdateRequest limits this to a name. It's the same for the Datasets method so this PR isn't introducing anything new, but supporting URIs would be a useful addition at some point.

if (license == null) {
return notFound(BundleUtil.getStringFromBundle("datasets.api.updateLicense.licenseNotFound", List.of(licenseName)));
}

execCommand(new UpdateTemplateLicenseCommand(createDataverseRequest(getRequestUser(crc)), template, dataverse, license));
return ok(BundleUtil.getStringFromBundle("dataverses.api.update.template.license.success"));
} else if (requestBody.getCustomTerms() != null) {
CustomTermsDTO customTerms = requestBody.getCustomTerms();
execCommand(new UpdateTemplateLicenseCommand(createDataverseRequest(getRequestUser(crc)), template, dataverse, customTerms.toTermsOfUseAndAccess()));
return ok(BundleUtil.getStringFromBundle("dataverses.api.update.template.license.success"));
} else {
return badRequest(BundleUtil.getStringFromBundle("datasets.api.updateLicense.licenseNameIsEmpty"));
}

} catch (WrappedResponse e) {
return e.getResponse();
}
}

@PUT
@AuthRequired
@Path("{templateId}/access")
public Response updateTemplateTermsOfAccess(@Context ContainerRequestContext crc, String jsonBody, @PathParam("templateId") Long templateId) {
try {

boolean publicInstall = settingsSvc.isTrueForKey(SettingsServiceBean.Key.PublicInstall, false);

Template template = findTemplateOrDie(templateId);

JsonObject json = JsonUtil.getJsonObject(jsonBody);

TermsOfUseAndAccess toua = jsonParser().parseTermsOfAccess(json);

if (publicInstall && (toua.isFileAccessRequest() || !toua.getTermsOfAccess().isEmpty())){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBlank for consistency? is null possible?

return error(BAD_REQUEST, "Setting File Access Request or Terms of Access is not permitted on a public installation.");
}

execCommand(new UpdateTemplateTermsOfAccessCommand(createDataverseRequest(getRequestUser(crc)), template, template.getDataverse(), toua ));

return ok(BundleUtil.getStringFromBundle("dataverses.api.update.template.access.success"));

} catch (JsonParseException ex) {
logger.log(Level.SEVERE, "Semantic error parsing template terms update Json: " + ex.getMessage(), ex);
return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("datasets.api.editMetadata.error.parseUpdate", List.of(ex.getMessage())));
} catch (WrappedResponse ex) {
logger.log(Level.SEVERE, "Update terms of access error: " + ex.getMessage(), ex);
return ex.getResponse();
}
}

@POST
@AuthRequired
Expand All @@ -2041,7 +2149,7 @@ public Response setDefaultTemplate(@Context ContainerRequestContext crc,
try {

Dataverse dataverse = findDataverseOrDie(dvId);
Template template = findTemplateOrDie(templateId, dataverse);
Template template = findTemplateInDataverseOrParentsOrDie(templateId, dataverse);
DataverseRequest dvReq = createDataverseRequest(getRequestUser(crc));
SetDefaultTemplateCommand command = new SetDefaultTemplateCommand(template, dvReq, dataverse);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@
import java.sql.Timestamp;
import java.util.*;

public class NewTemplateDTO {
public class TemplateDTO {

private String name;
private List<DatasetField> datasetFields;
private Map<String, String> instructionsMap;
private boolean isDefault;

public static NewTemplateDTO fromRequestBody(String requestBody, JsonParser jsonParser) throws JsonParseException {
NewTemplateDTO newTemplateDTO = new NewTemplateDTO();
public static TemplateDTO fromRequestBody(String requestBody, JsonParser jsonParser) throws JsonParseException {
TemplateDTO templateDTO = new TemplateDTO();

JsonObject jsonObject = JsonUtil.getJsonObject(requestBody);

newTemplateDTO.name = jsonObject.getString("name");
newTemplateDTO.datasetFields = jsonParser.parseMultipleFields(jsonObject);
newTemplateDTO.instructionsMap = parseRequestBodyInstructionsMap(jsonObject);
newTemplateDTO.isDefault = jsonObject.getBoolean("isDefault", false);

return newTemplateDTO;
templateDTO.name = jsonObject.getString("name");
templateDTO.datasetFields = jsonParser.parseMultipleFields(jsonObject);
templateDTO.instructionsMap = jsonParser.parseRequestBodyInstructionsMap(jsonObject);
templateDTO.isDefault = jsonObject.getBoolean("isDefault", false);
return templateDTO;
}

public Template toTemplate() {
Expand Down Expand Up @@ -59,18 +59,4 @@ public boolean isDefault() {
return isDefault;
}

private static Map<String, String> parseRequestBodyInstructionsMap(JsonObject jsonObject) {
Map<String, String> instructionsMap = new HashMap<>();
JsonArray instructionsJsonArray = jsonObject.getJsonArray("instructions");
if (instructionsJsonArray == null) {
return null;
}
for (JsonObject instructionJsonObject : instructionsJsonArray.getValuesAs(JsonObject.class)) {
instructionsMap.put(
instructionJsonObject.getString("instructionField"),
instructionJsonObject.getString("instructionText")
);
}
return instructionsMap;
}
}
Loading
Loading