Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3836ea4
feat: add isDefault field to JSON output
g-saracca Jul 31, 2025
42693d2
test: assert isDefault property
g-saracca Jul 31, 2025
c01e803
Merge branch 'develop' of github.com:IQSS/dataverse into 11703-return…
GPortas Aug 28, 2025
6d22479
Fixed: null check in jsonTermsOfUseAndAccess for license
GPortas Aug 28, 2025
45dfbc7
Added: unit tests for JsonPrinter.jsonTermsOfUseAndAccess
GPortas Aug 28, 2025
d744bf7
Added: unit tests for JsonPrinter.jsonTemplate
GPortas Aug 28, 2025
e346080
Fixed: marking default template in ListDataverseTemplatesCommand
GPortas Sep 1, 2025
135d703
Merge branch 'develop' of github.com:IQSS/dataverse into 11703-return…
GPortas Sep 1, 2025
f5bbe94
Added: extended ListDataverseTemplatesCommand unit tests
GPortas Sep 1, 2025
3f9b7f6
Changed: minor code format
GPortas Sep 1, 2025
166f5fa
Fixed: isTemplateRoot being checked in ListDataverseTemplatesCommand
GPortas Sep 1, 2025
8041907
Added: release notes for #11703
GPortas Sep 1, 2025
eeef609
Added: release note tweak
GPortas Sep 1, 2025
e96f1bd
Added: fileAccessRequest field of terms of use and access to JsonPrinter
GPortas Sep 2, 2025
8adc6e1
Added: contactForAccess field to terms of use and access API payload
GPortas Sep 2, 2025
e5c170b
Fixed: InApp notifications API json printer missing null check for re…
GPortas Sep 3, 2025
79b21a4
Changed: release notes updated
GPortas Sep 3, 2025
2b050dd
Merge branch 'develop' of github.com:IQSS/dataverse into 11703-return…
GPortas Sep 3, 2025
541fd21
Fixed: setting default template in CreateTemplateCommand
GPortas Sep 3, 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
13 changes: 13 additions & 0 deletions doc/release-notes/11703-notifications-and-templates-api-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Release Notes

## Templates API fixes
- Added the missing `isDefault` field to the **Get Dataset Templates** API response.
- Added the missing `fileAccessRequest` field of terms of use and access to the **Get Dataset Templates** API response.
- Added the missing `contactForAccess` field of terms of use and access to the **Get Dataset Templates** API response.
- Resolved an API **500 error** that occurred when working with templates containing custom license terms.
- Updated API behavior so templates are **no longer returned from parent dataverses** when the "Include Templates from Root" option is unchecked in the UI.

## Notifications API fixes
- Fixed API errors caused by NullPointerException when retrieving notifications without a requestor.

See #11704
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public Template toTemplate() {
template.updateInstructions();
template.setCreateTime(new Timestamp(new Date().getTime()));
template.setUsageCount(0L);
template.setIsDefaultForDataverse(isDefault);

return template;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ public Template execute(CommandContext ctxt) throws CommandException {
DatasetFieldUtil.tidyUpFields(template.getDatasetFields(), false);
}

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

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

return template;
}

private static void updateTermsOfUseAndAccess(CommandContext ctxt, Template template) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@ public ListDataverseTemplatesCommand(DataverseRequest request, Dataverse dataver

@Override
public List<Template> execute(CommandContext ctxt) throws CommandException {
List<Template> templates = new ArrayList<>();
List<Template> availableTemplates = new ArrayList<>(dataverse.getTemplates());

if (dataverse.getOwner() != null) {
templates.addAll(dataverse.getParentTemplates());
if (!dataverse.isTemplateRoot() && dataverse.getOwner() != null) {
availableTemplates.addAll(dataverse.getParentTemplates());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to make this recursive for multiple generations of owners? While dv.getOwner() != null...

}

templates.addAll(dataverse.getTemplates());
setDefaultTemplate(availableTemplates);

return templates;
return availableTemplates;
}

private void setDefaultTemplate(List<Template> availableTemplates) {
Optional.ofNullable(dataverse.getDefaultTemplate())
.map(Template::getId).flatMap(defaultTemplateId -> availableTemplates.stream()
.filter(template -> template.getId().equals(defaultTemplateId))
.findFirst())
.ifPresent(template -> template.setIsDefaultForDataverse(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,11 @@ private void addDatasetCreatedFields(final NullSafeJsonBuilder notificationJson,
}

private void addRequestorFields(final NullSafeJsonBuilder notificationJson, final AuthenticatedUser requestor) {
notificationJson.add(KEY_REQUESTOR_FIRST_NAME, requestor.getFirstName());
notificationJson.add(KEY_REQUESTOR_LAST_NAME, requestor.getLastName());
notificationJson.add(KEY_REQUESTOR_EMAIL, requestor.getEmail());
if (requestor != null) {
notificationJson.add(KEY_REQUESTOR_FIRST_NAME, requestor.getFirstName());
notificationJson.add(KEY_REQUESTOR_LAST_NAME, requestor.getLastName());
notificationJson.add(KEY_REQUESTOR_EMAIL, requestor.getEmail());
}
}

private void addDatasetFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,7 @@ public static JsonObjectBuilder jsonTemplate(Template template) {
return jsonObjectBuilder()
.add("id", template.getId())
.add("name", template.getName())
.add("isDefault", template.isIsDefaultForDataverse())
.add("usageCount", template.getUsageCount())
.add("createTime", template.getCreateTime().toString())
.add("createDate", template.getCreateDate())
Expand All @@ -1602,9 +1603,10 @@ public static JsonObjectBuilder jsonTemplate(Template template) {
}

public static JsonObjectBuilder jsonTermsOfUseAndAccess(TermsOfUseAndAccess termsOfUseAndAccess) {
License license = termsOfUseAndAccess.getLicense();
return jsonObjectBuilder()
.add("id", termsOfUseAndAccess.getId())
.add("license", json(termsOfUseAndAccess.getLicense()))
.add("license", license != null ? json(license) : null)
.add("termsOfUse", termsOfUseAndAccess.getTermsOfUse())
.add("termsOfAccess", termsOfUseAndAccess.getTermsOfAccess())
.add("confidentialityDeclaration", termsOfUseAndAccess.getConfidentialityDeclaration())
Expand All @@ -1618,7 +1620,9 @@ public static JsonObjectBuilder jsonTermsOfUseAndAccess(TermsOfUseAndAccess term
.add("originalArchive", termsOfUseAndAccess.getOriginalArchive())
.add("availabilityStatus", termsOfUseAndAccess.getAvailabilityStatus())
.add("sizeOfCollection", termsOfUseAndAccess.getSizeOfCollection())
.add("studyCompletion", termsOfUseAndAccess.getStudyCompletion());
.add("studyCompletion", termsOfUseAndAccess.getStudyCompletion())
.add("contactForAccess", termsOfUseAndAccess.getContactForAccess())
.add("fileAccessRequest", termsOfUseAndAccess.isFileAccessRequest());
}

public static JsonArrayBuilder jsonTemplateInstructions(Map<String, String> templateInstructions) {
Expand Down
2 changes: 2 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -2335,6 +2335,7 @@ public void testCreateAndGetTemplates() {
);
createTemplateResponse.then().assertThat().statusCode(OK.getStatusCode())
.body("data.name", equalTo("Dataverse template"))
.body("data.isDefault", equalTo(true))
.body("data.usageCount", equalTo(0))
.body("data.termsOfUseAndAccess.license.name", equalTo("CC0 1.0"))
.body("data.datasetFields.citation.fields.size()", equalTo(1))
Expand All @@ -2358,6 +2359,7 @@ public void testCreateAndGetTemplates() {
getTemplateResponse.then().assertThat().statusCode(OK.getStatusCode())
.body("data.size()", equalTo(1))
.body("data[0].name", equalTo("Dataverse template"))
.body("data[0].isDefault", equalTo(true))
.body("data[0].usageCount", equalTo(0))
.body("data[0].termsOfUseAndAccess.license.name", equalTo("CC0 1.0"))
.body("data[0].datasetFields.citation.fields.size()", equalTo(1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
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 edu.harvard.iq.dataverse.util.template.TemplateBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ListDataverseTemplatesCommandTest {
Expand All @@ -28,52 +32,138 @@ public void setUp() {

@Test
public void execute_shouldReturnTemplates_noParent() throws CommandException {
// Arrange
Dataverse dataverseMock = Mockito.mock(Dataverse.class);

Mockito.when(dataverseMock.getTemplates()).thenReturn(Collections.singletonList(testTemplate));

ListDataverseTemplatesCommand sut = new ListDataverseTemplatesCommand(dataverseRequestStub, dataverseMock);

// Act
List<Template> result = sut.execute(Mockito.mock(CommandContext.class));

// Assert
assertEquals(1, result.size());
assertEquals(testTemplate, result.get(0));
}

@Test
public void execute_shouldReturnTemplates_parentHasTemplates() throws CommandException {
// Arrange
Template parentTemplate = new Template();

Dataverse dataverseMock = Mockito.mock(Dataverse.class);
Dataverse parentDataverseMock = Mockito.mock(Dataverse.class);

Mockito.when(dataverseMock.getTemplates()).thenReturn(Collections.singletonList(testTemplate));
Mockito.when(dataverseMock.getParentTemplates()).thenReturn(Collections.singletonList(parentTemplate));
Mockito.when(dataverseMock.getOwner()).thenReturn(parentDataverseMock);

ListDataverseTemplatesCommand sut = new ListDataverseTemplatesCommand(dataverseRequestStub, dataverseMock);

// Act
List<Template> result = sut.execute(Mockito.mock(CommandContext.class));

// Assert
assertEquals(2, result.size());
assertTrue(result.contains(testTemplate));
assertTrue(result.contains(parentTemplate));
}

@Test
public void execute_shouldNotIncludeParentTemplates_whenDataverseIsRoot() throws CommandException {
// Arrange
String parentTemplateName = "parentTemplate";
Template parentTemplate = TemplateBuilder.aTemplate().withName(parentTemplateName).build();
String childTemplateName = "childTemplateName";
Template childTemplate = TemplateBuilder.aTemplate().withName(childTemplateName).build();
Dataverse dataverseMock = Mockito.mock(Dataverse.class);
Dataverse parentDataverseMock = Mockito.mock(Dataverse.class);
Mockito.when(dataverseMock.getTemplates()).thenReturn(Collections.singletonList(childTemplate));
Mockito.when(dataverseMock.getParentTemplates()).thenReturn(Collections.singletonList(parentTemplate));
Mockito.when(dataverseMock.getOwner()).thenReturn(parentDataverseMock);
Mockito.when(dataverseMock.isTemplateRoot()).thenReturn(true);

ListDataverseTemplatesCommand sut = new ListDataverseTemplatesCommand(dataverseRequestStub, dataverseMock);

// Act
List<Template> result = sut.execute(Mockito.mock(CommandContext.class));

// Assert
assertEquals(1, result.size());
assertTrue(result.get(0).getName().equals(childTemplateName));
}


@Test
public void execute_shouldReturnTemplates_parentHasNoTemplates() throws CommandException {
// Arrange
Dataverse dataverseMock = Mockito.mock(Dataverse.class);
Dataverse parentDataverseStub = Mockito.mock(Dataverse.class);

Mockito.when(dataverseMock.getTemplates()).thenReturn(Collections.singletonList(testTemplate));
Mockito.when(dataverseMock.getOwner()).thenReturn(parentDataverseStub);
Mockito.when(dataverseMock.getParentTemplates()).thenReturn(Collections.emptyList());

ListDataverseTemplatesCommand sut = new ListDataverseTemplatesCommand(dataverseRequestStub, dataverseMock);

// Act
List<Template> result = sut.execute(Mockito.mock(CommandContext.class));

// Assert
assertEquals(1, result.size());
assertTrue(result.contains(testTemplate));
}


@Test
public void execute_shouldSetDefaultTemplate_whenDefaultIsPresent() throws CommandException {
// Arrange
String defaultTplName = "defaultTplName";
String otherTplName = "otherTplName";

Template defaultTpl = TemplateBuilder.aTemplate().withName(defaultTplName).isDefault(true).build();
Template otherTpl = TemplateBuilder.aTemplate().withName(otherTplName).isDefault(false).build();

Dataverse dataverseMock = Mockito.mock(Dataverse.class);
Mockito.when(dataverseMock.getTemplates()).thenReturn(Arrays.asList(defaultTpl, otherTpl));
Mockito.when(dataverseMock.getDefaultTemplate()).thenReturn(defaultTpl);

ListDataverseTemplatesCommand sut = new ListDataverseTemplatesCommand(dataverseRequestStub, dataverseMock);

// Act
List<Template> result = sut.execute(Mockito.mock(CommandContext.class));

// Assert
assertEquals(2, result.size());

Template resultDefault = findTemplateByName(result, defaultTplName);
assertNotNull(resultDefault, "Default template should be in the list");
assertTrue(resultDefault.isIsDefaultForDataverse(), "The template with name " + defaultTplName + " should be marked as default");

Template resultOther = findTemplateByName(result, otherTplName);
assertNotNull(resultOther, "Other template should be in the list");
assertFalse(resultOther.isIsDefaultForDataverse(), "The template with name " + otherTplName + " should NOT be marked as default");
}

@Test
public void execute_shouldNotSetDefault_whenNoDefaultTemplateExists() throws CommandException {
// Arrange
testTemplate = TemplateBuilder.aTemplate().isDefault(false).build();
Dataverse dataverseMock = Mockito.mock(Dataverse.class);
Mockito.when(dataverseMock.getTemplates()).thenReturn(Collections.singletonList(testTemplate));
Mockito.when(dataverseMock.getDefaultTemplate()).thenReturn(null);

ListDataverseTemplatesCommand sut = new ListDataverseTemplatesCommand(dataverseRequestStub, dataverseMock);

// Act
List<Template> result = sut.execute(Mockito.mock(CommandContext.class));

// Assert
assertEquals(1, result.size());
assertFalse(result.get(0).isIsDefaultForDataverse(), "No template should be marked as default");
}

private Template findTemplateByName(List<Template> templates, String name) {
return templates.stream()
.filter(t -> name.equals(t.getName()))
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,26 @@ public void testAddFieldsByType_requestFileAccess() {
verify(notificationJson).add(KEY_DATAFILE_DISPLAY_NAME, "Test File");
}

@Test
public void testAddFieldsByType_requestFileAccess_nullRequestor() {
userNotification.setType(UserNotification.Type.REQUESTFILEACCESS);
userNotification.setObjectId(1L);
userNotification.setRequestor(null);

DataFile dataFile = mock(DataFile.class);
when(dataFile.getId()).thenReturn(1L);
when(dataFile.getDisplayName()).thenReturn("Test File");
when(dataFileService.find(1L)).thenReturn(dataFile);

sut.addFieldsByType(notificationJson, authenticatedUser, userNotification);

verify(notificationJson, never()).add(eq(KEY_REQUESTOR_FIRST_NAME), anyString());
verify(notificationJson, never()).add(eq(KEY_REQUESTOR_LAST_NAME), anyString());
verify(notificationJson, never()).add(eq(KEY_REQUESTOR_EMAIL), anyString());
verify(notificationJson).add(KEY_DATAFILE_ID, Long.valueOf("1"));
verify(notificationJson).add(KEY_DATAFILE_DISPLAY_NAME, "Test File");
}

@Test
public void testAddFieldsByType_grantFileAccess() {
userNotification.setType(UserNotification.Type.GRANTFILEACCESS);
Expand Down
Loading