Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7ebb100
placeholder
jp-tosca Nov 6, 2025
4c9044a
Endpoints work but need polish.
jp-tosca Nov 10, 2025
9460189
Merge remote-tracking branch 'origin/develop' into 11914-set-template…
jp-tosca Nov 12, 2025
097e972
pr notes
jp-tosca Nov 12, 2025
5d7379e
Merge remote-tracking branch 'origin/develop' into 11914-set-template…
jp-tosca Nov 18, 2025
d5aab83
Initial test
jp-tosca Nov 18, 2025
31a2078
Changes to show the ID of the created template
jp-tosca Nov 20, 2025
76ff3e0
Changes to the set default endpoint
jp-tosca Nov 20, 2025
8e72fea
Flush entity manager after setting default template in CreateTemplate…
jp-tosca Nov 20, 2025
17bb055
Update success messages for removing default dataset template
jp-tosca Nov 20, 2025
04fcb16
Implement RemoveDefaultDatasetCommand and update endpoint to use it f…
jp-tosca Nov 20, 2025
72237fb
Add tests for setting and removing default template in DataversesIT
jp-tosca Nov 21, 2025
a86a9ab
Fix permission description in API documentation and method signatures…
jp-tosca Nov 24, 2025
924da37
Merge branch 'develop' into 11914-set-template-default
jp-tosca Nov 24, 2025
fd645d8
Merge branch 'develop' into 11914-set-template-default
sekmiller Jan 8, 2026
1599767
#11914 fix merge conflicts
sekmiller Jan 8, 2026
e57568e
#11914 return default true for test
sekmiller Jan 8, 2026
1649109
#11914 fix find template and tests
sekmiller Jan 9, 2026
93fcb43
#11914 update command name for clarity
sekmiller Jan 9, 2026
bf10460
Merge branch 'develop' into 11914-set-template-default
sekmiller Jan 12, 2026
671901b
Merge branch 'develop' into 11914-set-template-default
sekmiller Jan 14, 2026
dc1c200
Update src/main/java/edu/harvard/iq/dataverse/engine/command/impl/Set…
sekmiller Jan 14, 2026
eef6ce0
Update src/main/java/edu/harvard/iq/dataverse/engine/command/impl/Rem…
sekmiller Jan 14, 2026
2fd494f
#11914 change path
sekmiller Jan 14, 2026
23253fb
Update native-api.rst
sekmiller Jan 14, 2026
f668cfa
Update native-api.rst
sekmiller Jan 14, 2026
53447e0
Merge branch 'develop' into 11914-set-template-default
sekmiller Jan 14, 2026
3e04cee
fix docs
stevenwinship Jan 14, 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
15 changes: 15 additions & 0 deletions doc/release-notes/11914-set-template-default-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## New Endpoint: POST `/dataverses/{id}/template/default/{templateId}`
Comment thread
stevenwinship marked this conversation as resolved.

A new endpoint has been implemented to set the default template to a given dataverse collection.

### Functionality
- Sets the default template of the given dataverse collection.
- You must have edit dataverse permission in the collection in order to use this endpoint.

## New Endpoint: DELETE `/dataverses/{id}/template/default`
Comment thread
jp-tosca marked this conversation as resolved.
Comment thread
stevenwinship marked this conversation as resolved.
Comment thread
stevenwinship marked this conversation as resolved.

A new endpoint has been implemented to remove the default template to a given dataverse collection.

### Functionality
- Removes the default template of the given dataverse collection.
- You must have edit dataverse permission in the collection in order to use this endpoint.
19 changes: 19 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,25 @@ protected Dataverse findDataverseOrDie( String dvIdtf ) throws WrappedResponse {
}
return dv;
}

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

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

templates.addAll(dataverse.getTemplates());
templates.addAll(dataverse.getParentTemplates());

Template template = templates.stream()
.filter(t -> Objects.equals(t.getId(), templateId))
.findFirst()
.orElse(null);

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

protected DataverseLinkingDataverse findDataverseLinkingDataverseOrDie(String dataverseId, String linkedDataverseId) throws WrappedResponse {
DataverseLinkingDataverse dvld;
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,45 @@ public Response createTemplate(@Context ContainerRequestContext crc, String body
}
}

@POST
@AuthRequired
@Path("{identifier}/templates/default/{templateId}")
public Response setDefaultTemplate(@Context ContainerRequestContext crc,
@PathParam("identifier") String dvId,
@PathParam("templateId") Long templateId) {

try {

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

execCommand(command);

return ok(BundleUtil.getStringFromBundle("dataverse.setDefaultTemplate.success"));

} catch (WrappedResponse e) {
return e.getResponse();
}
}
Comment thread
jp-tosca marked this conversation as resolved.

@DELETE
@AuthRequired
@Path("{identifier}/templates/default")
public Response removeDefaultTemplate(@Context ContainerRequestContext crc,
@PathParam("identifier") String dvId) {
try {
Dataverse dataverse = findDataverseOrDie(dvId);
RemoveDefaultTemplateCommand command = new RemoveDefaultTemplateCommand(createDataverseRequest(getRequestUser(crc)), dataverse);
execCommand(command);
return ok(BundleUtil.getStringFromBundle("dataverse.removeDefaultTemplate.success"));
} catch (WrappedResponse e) {
return e.getResponse();
}
}
Comment thread
jp-tosca marked this conversation as resolved.


@GET
@AuthRequired
@Path("{identifier}/allowedMetadataLanguages")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public Template execute(CommandContext ctxt) throws CommandException {
if (initialize && createdTemplate.isIsDefaultForDataverse()) {
dataverse.setDefaultTemplate(createdTemplate);
ctxt.em().merge(dataverse);
ctxt.em().flush();
}

//Flush so that api response can include the id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package edu.harvard.iq.dataverse.engine.command.impl;

import edu.harvard.iq.dataverse.*;
import edu.harvard.iq.dataverse.authorization.Permission;

import edu.harvard.iq.dataverse.engine.command.AbstractCommand;
import edu.harvard.iq.dataverse.engine.command.CommandContext;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.RequiredPermissions;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;

/**
* @author J.P. Tosca
* Removes the default template {@link Template} for a {@link Dataverse}.
*/
@RequiredPermissions(Permission.EditDataverse)
public class RemoveDefaultTemplateCommand extends AbstractCommand<Dataverse>{

private final Dataverse dataverse;

public RemoveDefaultTemplateCommand(DataverseRequest request, Dataverse dataverse) {
super(request, dataverse);
this.dataverse = dataverse;
}

@Override
public Dataverse execute(CommandContext ctxt) throws CommandException {
dataverse.setDefaultTemplate(null);
return dataverse;
Comment thread
sekmiller marked this conversation as resolved.
Outdated
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package edu.harvard.iq.dataverse.engine.command.impl;

import edu.harvard.iq.dataverse.*;
import edu.harvard.iq.dataverse.authorization.Permission;

import edu.harvard.iq.dataverse.engine.command.AbstractCommand;
import edu.harvard.iq.dataverse.engine.command.CommandContext;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.RequiredPermissions;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
import edu.harvard.iq.dataverse.Template;

/**
* @author J.P. Tosca
* Sets a default template {@link Template} for a {@link Dataverse}.
*/
@RequiredPermissions(Permission.EditDataverse)
public class SetDefaultTemplateCommand extends AbstractCommand<Template> {

private final Template template;
private final Dataverse dataverse;

public SetDefaultTemplateCommand(Template template, DataverseRequest request, Dataverse dataverse) {
super(request, dataverse);
this.template = template;
this.dataverse = dataverse;
}

@Override
public Template execute(CommandContext ctxt) throws CommandException {
dataverse.setDefaultTemplate(template);
Comment thread
sekmiller marked this conversation as resolved.
return template;
}

}
2 changes: 2 additions & 0 deletions src/main/java/propertyFiles/Bundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,8 @@ dataverse.update.featuredItems.error.invalidType=Unknown 'type' must be one of {
dataverse.update.featuredItems.error.typeAndDvObjectMismatch=The 'type' passed does not match the dvObject type.
dataverse.delete.featuredItems.success=All featured items of this Dataverse have been successfully deleted.
dataverse.createTemplate.error.jsonParseMetadataFields=Error parsing the POSTed template dataset fields: {0}
dataverse.setDefaultTemplate.success=The default dataset template has been successfully set for this dataverse.
dataverse.removeDefaultTemplate.success=The default dataset template has been successfully removed from this dataverse.
# rolesAndPermissionsFragment.xhtml

# advanced.xhtml
Expand Down
129 changes: 71 additions & 58 deletions src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -2617,9 +2617,9 @@ public void testCreateAndGetTemplates() throws JsonParseException {
String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse);
String username = UtilIT.getUsernameFromResponse(createUserResponse);

Response createSecondUserResponse = UtilIT.createRandomUser();
String secondApiToken = UtilIT.getApiTokenFromResponse(createSecondUserResponse);
String secondUsername = UtilIT.getUsernameFromResponse(createSecondUserResponse);
Response createSecondUserResponse = UtilIT.createRandomUser();
String secondApiToken = UtilIT.getApiTokenFromResponse(createSecondUserResponse);
String secondUsername = UtilIT.getUsernameFromResponse(createSecondUserResponse);


/*
Expand All @@ -2642,61 +2642,52 @@ public void testCreateAndGetTemplates() throws JsonParseException {
String[] newFacetIds = new String[]{"contributorName"};
String[] newMetadataBlockNames = new String[]{"citation", "geospatial", "biomedical"};

//Giving the new Dataverse updated metadatablocks so that it will not inherit templates
Response updateDataverseResponse = UtilIT.updateDataverse(
dataverseAlias, dataverseAlias, newName, newAffiliation, newDataverseType, newContactEmails, newInputLevelNames,
// Giving the new Dataverse updated metadatablocks so that it will not inherit
// templates
Response updateDataverseResponse = UtilIT.updateDataverse(
dataverseAlias, dataverseAlias, newName, newAffiliation, newDataverseType, newContactEmails,
newInputLevelNames,
null, newMetadataBlockNames, apiToken,
Boolean.FALSE, Boolean.FALSE, null
);

Boolean.FALSE, Boolean.FALSE, null);

updateDataverseResponse.then().assertThat()
.statusCode(OK.getStatusCode());

Response publishDataverse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken);
assertEquals(200, publishDataverse.getStatusCode());
assertTrue(publishDataverse.prettyPrint().contains("isReleased\": true"));

// Create a template

String jsonString = """
{
"name": "Dataverse template",
"isDefault": true,
"fields": [
{
"typeName": "author",
"value": [
{
"authorName": {
"typeName": "authorName",
"value": "Belicheck, Bill"
},
"authorAffiliation": {
"typeName": "authorIdentifierScheme",
"value": "ORCID"
}
}
]
}
],
"instructions": [
{
"instructionField": "author",
"instructionText": "The author data"
}
]
}
""";
{
"name": "Dataverse template",
"isDefault": true,
"fields": [
{
"typeName": "author",
"value": [
{
"authorName": {
"typeName": "authorName",
"value": "Belicheck, Bill"
},
"authorAffiliation": {
"typeName": "authorIdentifierScheme",
"value": "ORCID"
}
}
]
}
],
"instructions": [
{
"instructionField": "author",
"instructionText": "The author data"
}
]
}
""";

Response createTemplateResponse = UtilIT.createTemplate(
dataverseAlias,
jsonString,
apiToken
);
apiToken);

createTemplateResponse.prettyPrint();
Long templateId = UtilIT.getTemplateIdFromResponse(createTemplateResponse);

createTemplateResponse.then().assertThat().statusCode(CREATED.getStatusCode())
.body("data.name", equalTo("Dataverse template"))
.body("data.isDefault", equalTo(true))
Expand All @@ -2708,17 +2699,24 @@ public void testCreateAndGetTemplates() throws JsonParseException {
.body("data.instructions[0].instructionText", equalTo("The author data"))
.body("data.dataverseAlias", equalTo(dataverseAlias));

// Template creation should fail if the user lacks dataverse edit permissions
Long templateId = createTemplateResponse.body().jsonPath().getLong("data.id");

//Check for failure due unauthorized user.
Response setDefaultResp = UtilIT.setDefaultTemplate(dataverseAlias, templateId, secondApiToken);
setDefaultResp.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());

// Set default template
setDefaultResp = UtilIT.setDefaultTemplate(dataverseAlias, templateId, apiToken);
setDefaultResp.then().assertThat().statusCode(OK.getStatusCode());

// Template creation should fail if the user lacks dataverse edit permissions
createTemplateResponse = UtilIT.createTemplate(
dataverseAlias,
jsonString,
secondApiToken
);
secondApiToken);
createTemplateResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());

// Get templates

// Get templates and check this one is default now
Response getTemplateResponse = UtilIT.getTemplates(dataverseAlias, apiToken);
getTemplateResponse.then().assertThat().statusCode(OK.getStatusCode())
.body("data.size()", equalTo(1))
Expand All @@ -2731,13 +2729,28 @@ public void testCreateAndGetTemplates() throws JsonParseException {
.body("data[0].instructions[0].instructionField", equalTo("author"))
.body("data[0].instructions[0].instructionText", equalTo("The author data"))
.body("data[0].dataverseAlias", equalTo(dataverseAlias));


// Remove default template
System.out.print("***************: " + dataverseAlias );

Response removeDefaultResp = UtilIT.removeDefaultTemplate(dataverseAlias, apiToken);
removeDefaultResp.prettyPrint();
removeDefaultResp.then().assertThat().statusCode(OK.getStatusCode());

// Templates retrieval should fail if a secondary user lacks dataset creation permissions
//check that template is no longer default.
getTemplateResponse = UtilIT.getTemplates(dataverseAlias, apiToken);
getTemplateResponse.then().assertThat().statusCode(OK.getStatusCode())
.body("data.size()", equalTo(1))
.body("data[0].isDefault", equalTo(false));

getTemplateResponse = UtilIT.getTemplates(dataverseAlias, secondApiToken);
getTemplateResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());

// Templates retrieval should succeed if the secondary user has dataset creation permissions
// Templates retrieval should fail if a secondary user lacks dataset creation
// permissions

getTemplateResponse = UtilIT.getTemplates(dataverseAlias, secondApiToken);
getTemplateResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());



//set to super to update role
Expand Down Expand Up @@ -2791,7 +2804,7 @@ public void testCreateAndGetTemplates() throws JsonParseException {
deleteUserResponse.prettyPrint();
assertEquals(200, deleteUserResponse.getStatusCode());


}

@Test
Expand Down
16 changes: 16 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -5157,6 +5157,22 @@ public static Response getTemplates(String dataverseAlias, String apiToken) {
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("/api/dataverses/" + dataverseAlias + "/templates");
}

public static Response setDefaultTemplate(String dataverseAlias, Long templateId, String apiToken) {
return given()
.contentType(ContentType.JSON)
.header(API_TOKEN_HTTP_HEADER, apiToken)
.post("/api/dataverses/" + dataverseAlias + "/templates/default/" + templateId);
}

public static Response removeDefaultTemplate(String dataverseAlias, String apiToken) {
return given()
.contentType(ContentType.JSON)
.header(API_TOKEN_HTTP_HEADER, apiToken)
.delete("/api/dataverses/" + dataverseAlias + "/templates/default");
}



public static Response getTemplate(String templateId) {
return given()
Expand Down