Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0eeac97
#11918 prelim check in
sekmiller Oct 27, 2025
f74a9cd
#11918 fix perms and test
sekmiller Oct 28, 2025
e21f007
#11918 add unit tests for command
sekmiller Oct 29, 2025
8888d00
#11918 fix create test for flush/new template
sekmiller Oct 30, 2025
4775c27
#11918 add get template by id
sekmiller Oct 31, 2025
2d24a5a
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 5, 2025
a2714fd
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 5, 2025
8069e6c
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 5, 2025
0b2cc27
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 6, 2025
09d7775
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 7, 2025
7585924
#11918 unused code
sekmiller Nov 7, 2025
09e4a28
#11918 add release notes
sekmiller Nov 10, 2025
e1d4f67
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 10, 2025
5a06245
#11918 update release note
sekmiller Nov 10, 2025
9a5454e
Update native-api.rst
sekmiller Nov 10, 2025
0dcc385
Merge branch '11918-template-apis' of https://github.com/IQSS/dataver…
sekmiller Nov 10, 2025
a62265d
#11918 code format
sekmiller Nov 12, 2025
fc64bdf
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 12, 2025
261d7ae
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 13, 2025
7fce175
#11918 CR fixes
sekmiller Nov 13, 2025
656b0b6
Update native-api.rst
sekmiller Nov 13, 2025
9ea3f16
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 18, 2025
09c7974
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 20, 2025
967cf3c
Merge branch 'develop' into 11918-template-apis
sekmiller Nov 24, 2025
acf58a2
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 1, 2025
d73c3be
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 3, 2025
2d8054f
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 4, 2025
d080fd8
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 9, 2025
afa57c3
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 9, 2025
39ba71c
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 10, 2025
c2e75a6
Merge branch 'develop' into 11918-template-apis
sekmiller Dec 12, 2025
7b9a106
Update CreateTemplateCommandTest.java
sekmiller Dec 12, 2025
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
17 changes: 17 additions & 0 deletions doc/release-notes/11918-template-apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## New Endpoint: GET `/dataverses/{id}/template`

A new endpoint has been implemented to manage templates belonging to a given dataverse collection.

### Functionality
- Returns the template of the given {id} in json format.
- You must have add dataset permission in the collection in order to use this endpoint.

## New Endpoint: DELETE `/dataverses/{id}/template`

A new endpoint has been implemented to manage templates belonging to a given dataverse collection.

### Functionality
- Deletes the template of the given {id}.
- You must have Edit Dataverse permission in order to use this endpoint.


42 changes: 42 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,48 @@ The fully expanded example above (without environment variables) looks like this

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X GET "https://demo.dataverse.org/api/dataverses/1/templates"

List Single Template by its Identifier
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Gets the json representation of a template by its ``id``:

.. 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 GET "$SERVER_URL/api/dataverses/{ID}/template"

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 GET "https://demo.dataverse.org/api/dataverses/1/template"

You must have Create Dataset permission within the given dataverse collection to invoke this api.

Delete a Template by its Identifier
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Deletes a template by its ``id``:

.. 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 DELETE "$SERVER_URL/api/dataverses/{ID}/template"

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/1/template"

You must have Edit Dataverse permission within the given dataverse collection to invoke this api.

Create a Template for a Collection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
46 changes: 45 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.collect.Lists;
import com.google.api.client.util.ArrayMap;
import edu.harvard.iq.dataverse.*;
import static edu.harvard.iq.dataverse.api.AbstractApiBean.error;
import edu.harvard.iq.dataverse.api.auth.AuthRequired;
import edu.harvard.iq.dataverse.api.datadeposit.SwordServiceBean;
import edu.harvard.iq.dataverse.api.dto.*;
Expand All @@ -23,6 +24,7 @@
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean;
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.pidproviders.PidProvider;
import edu.harvard.iq.dataverse.pidproviders.PidUtil;
Expand Down Expand Up @@ -118,6 +120,9 @@ public class Dataverses extends AbstractApiBean {

@EJB
PermissionServiceBean permissionService;

@EJB
TemplateServiceBean templateService;

@EJB
DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean;
Expand Down Expand Up @@ -1989,6 +1994,21 @@ public Response getTemplates(@Context ContainerRequestContext crc, @PathParam("i
return e.getResponse();
}
}

@GET
@AuthRequired
@Path("/template/{id}")
public Response getTemplate(@Context ContainerRequestContext crc, @PathParam("id") Long templateId) {
try {
Template template = templateService.find(templateId);
if (template == null){
return error(Response.Status.NOT_FOUND, "Template with id " + templateId + " - not found.");
}
return ok(jsonTemplate(execCommand(new GetTemplateCommand(createDataverseRequest(getRequestUser(crc)), template))));
} catch (WrappedResponse e) {
return e.getResponse();
}
}

@POST
@AuthRequired
Expand All @@ -2002,7 +2022,10 @@ public Response createTemplate(@Context ContainerRequestContext crc, String body
} catch (JsonParseException ex) {
return error(Status.BAD_REQUEST, MessageFormat.format(BundleUtil.getStringFromBundle("dataverse.createTemplate.error.jsonParseMetadataFields"), ex.getMessage()));
}
return ok(jsonTemplate(execCommand(new CreateTemplateCommand(newTemplateDTO.toTemplate(), createDataverseRequest(getRequestUser(crc)), dataverse, true))));
Template created = execCommand(new CreateTemplateCommand(newTemplateDTO.toTemplate(), createDataverseRequest(getRequestUser(crc)), dataverse, true));

return created("/dataverses/template/" + created.getId(), jsonTemplate(created));

} catch (WrappedResponse e) {
return e.getResponse();
}
Expand Down Expand Up @@ -2036,6 +2059,27 @@ public Response setMetadataLanguage(@Context ContainerRequestContext crc, @PathP
}, getRequestUser(crc));
}

@Path("{id}/template")
@AuthRequired
@DELETE
public Response deleteTemplate(@Context ContainerRequestContext crc, @PathParam("id") long id) {

Template doomed = templateService.find(id);
if (doomed == null) {
return error(Response.Status.NOT_FOUND, "Template with id " + id + " - not found.");
}

Dataverse dv = doomed.getDataverse();
List<Dataverse> dataverseWDefaultTemplate = templateService.findDataversesByDefaultTemplateId(doomed.getId());
try {
execCommand(new DeleteTemplateCommand(createDataverseRequest(getRequestUser(crc)), dv, doomed, dataverseWDefaultTemplate));
} catch (WrappedResponse wr) {
return handleWrappedResponse(wr);
}

return ok("Template " + doomed.getName() + " deleted.");
}

@GET
@AuthRequired
@Path("{identifier}/assignments/history")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,15 @@ public Template execute(CommandContext ctxt) throws CommandException {
}

Template createdTemplate = ctxt.templates().save(template);

createdTemplate.setIsDefaultForDataverse(template.isIsDefaultForDataverse());
if (initialize && createdTemplate.isIsDefaultForDataverse()) {
dataverse.setDefaultTemplate(createdTemplate);
ctxt.em().merge(dataverse);
}
}

//Flush so that api response can include the id
ctxt.em().flush();
return createdTemplate;

}

private static void updateTermsOfUseAndAccess(CommandContext ctxt, Template template) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

package edu.harvard.iq.dataverse.engine.command.impl;

import edu.harvard.iq.dataverse.Template;
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.exception.CommandException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
*
* @author stephenkraffmiller
*/
public class GetTemplateCommand extends AbstractCommand<Template> {

private final Template template;

public GetTemplateCommand (DataverseRequest aRequest, Template template){
super(aRequest, template.getDataverse());
this.template = template;
}



@Override
public Template execute(CommandContext ctxt) throws CommandException {
return template;
}

@Override
public Map<String, Set<Permission>> getRequiredPermissions() {
//at least need add dataset if not published also - view unpublished
Set<Permission> requiredPermissions = new HashSet<>();

requiredPermissions.add(Permission.AddDataset);
if (!template.getDataverse().isReleased()) {
requiredPermissions.add(Permission.ViewUnpublishedDataverse);
}

return Collections.singletonMap("", requiredPermissions);

}


}
66 changes: 64 additions & 2 deletions src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -2609,6 +2609,10 @@ public void testUpdateInputLevelDisplayOnCreateOverride() {

@Test
public void testCreateAndGetTemplates() throws JsonParseException {
/*
Also Delete...and get single template
*/

Response createUserResponse = UtilIT.createRandomUser();
String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse);
String username = UtilIT.getUsernameFromResponse(createUserResponse);
Expand All @@ -2623,9 +2627,13 @@ public void testCreateAndGetTemplates() throws JsonParseException {
*/

Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken);
createDataverseResponse.prettyPrint();
createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode());
String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse);

Integer dataverseId = UtilIT.getDataverseIdFromResponse(createDataverseResponse);

System.out.print("dataverseId: " + dataverseId);

String newName = "New Test Dataverse Name";
String newAffiliation = "New Test Dataverse Affiliation";
String newDataverseType = Dataverse.DataverseType.TEACHING_COURSES.toString();
Expand All @@ -2644,6 +2652,10 @@ public void testCreateAndGetTemplates() throws JsonParseException {
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 = """
Expand Down Expand Up @@ -2681,8 +2693,11 @@ public void testCreateAndGetTemplates() throws JsonParseException {
jsonString,
apiToken
);

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

createTemplateResponse.then().assertThat().statusCode(OK.getStatusCode())
createTemplateResponse.then().assertThat().statusCode(CREATED.getStatusCode())
.body("data.name", equalTo("Dataverse template"))
.body("data.isDefault", equalTo(true))
.body("data.usageCount", equalTo(0))
Expand Down Expand Up @@ -2724,12 +2739,59 @@ public void testCreateAndGetTemplates() throws JsonParseException {

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


//set to super to update role
UtilIT.setSuperuserStatus(username, true);

Response grantRoleResponse = UtilIT.grantRoleOnDataverse(dataverseAlias, DataverseRole.DS_CONTRIBUTOR, "@" + secondUsername, apiToken);
grantRoleResponse.prettyPrint();
grantRoleResponse.then().assertThat().statusCode(OK.getStatusCode());

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

Response getTemplateByIdResponse = UtilIT.getTemplate(templateId.toString(), apiToken);
getTemplateByIdResponse.prettyPrint();
getTemplateByIdResponse.then().assertThat().statusCode(OK.getStatusCode());

//guest user shouldn't get it
getTemplateByIdResponse = UtilIT.getTemplate(templateId.toString());
getTemplateByIdResponse.prettyPrint();
getTemplateByIdResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());

Response deleteTemplateResponse = UtilIT.deleteTemplate(templateId.toString(), secondApiToken);
deleteTemplateResponse.prettyPrint();
deleteTemplateResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());

//set back to show super not needed for delete - just Edit Dataverse
UtilIT.setSuperuserStatus(username, false);

String badId = "8675309";

deleteTemplateResponse = UtilIT.deleteTemplate(badId, apiToken);
deleteTemplateResponse.prettyPrint();
deleteTemplateResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode());

deleteTemplateResponse = UtilIT.deleteTemplate(templateId.toString(), apiToken);
deleteTemplateResponse.prettyPrint();
deleteTemplateResponse.then().assertThat().statusCode(OK.getStatusCode());
// back to super for cleanup

UtilIT.setSuperuserStatus(username, true);

Response deleteDataverse1Response = UtilIT.deleteDataverse(dataverseAlias, apiToken);
deleteDataverse1Response.prettyPrint();
assertEquals(200, deleteDataverse1Response.getStatusCode());

Response deleteUserResponse = UtilIT.deleteUser(secondUsername);
deleteUserResponse.prettyPrint();
assertEquals(200, deleteUserResponse.getStatusCode());

deleteUserResponse = UtilIT.deleteUser(username);
deleteUserResponse.prettyPrint();
assertEquals(200, deleteUserResponse.getStatusCode());


}

@Test
Expand Down
26 changes: 26 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 @@ -337,6 +337,13 @@ static Integer getDataverseIdFromResponse(Response createDataverseResponse) {
logger.info("Id found in create dataverse response: " + dataverseId);
return dataverseId;
}

static Long getTemplateIdFromResponse(Response createTemplateResponse) {
JsonPath createdTemplate = JsonPath.from(createTemplateResponse.body().asString());
Long templateId = createdTemplate.getLong("data.id");
logger.info("Id found in create template response: " + templateId);
return templateId;
}

static Integer getDatasetIdFromResponse(Response createDatasetResponse) {
JsonPath createdDataset = JsonPath.from(createDatasetResponse.body().asString());
Expand Down Expand Up @@ -5128,6 +5135,12 @@ public static Response createTemplate(String dataverseAlias, String jsonString,
.body(jsonString)
.post("/api/dataverses/" + dataverseAlias + "/templates");
}

public static Response deleteTemplate(String id, String apiToken) {
return given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.delete("/api/dataverses/"+id+"/template");
}

public static Response getTemplates(String dataverseAlias, String apiToken) {
return given()
Expand All @@ -5136,6 +5149,19 @@ public static Response getTemplates(String dataverseAlias, String apiToken) {
.get("/api/dataverses/" + dataverseAlias + "/templates");
}

public static Response getTemplate(String templateId) {
return given()
.contentType(ContentType.JSON)
.get("/api/dataverses/template/" + templateId);
}

public static Response getTemplate(String templateId, String apiToken) {
return given()
.contentType(ContentType.JSON)
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("/api/dataverses/template/" + templateId);
}

/**
* Gets the tool URL for a dataset with optional parameters
* @param datasetId The ID of the dataset
Expand Down
Loading