Skip to content

Commit

Permalink
Merge branch 'staging' into OCD-4725
Browse files Browse the repository at this point in the history
  • Loading branch information
tmy1313 committed Nov 13, 2024
2 parents 9ecd661 + 80d2dd5 commit a8100a6
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 41 deletions.
15 changes: 15 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Release Notes

## Version 47.3.0
_12 November 2024_

### Features
* Endpoint updates for HTI-1 – (a)(9) to (b)(11) report
* Add hasUsers parameter to developer search endpoint
* Add users without contact info to bottom of preview message
* Give new Cognito users appropriate environment group access
* Add warning about similarly named products on developer

### Flagged Features
* Update functionality for SSO flag

---

## Version 47.2.0
_28 October 2024_

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -223,6 +224,10 @@ public SearchDevelopersController(DeveloperSearchService developerSearchService,
+ "specified or may have met any one or more of the activeListingsOptions",
allowEmptyValue = true, in = ParameterIn.QUERY, name = "activeListingsOptionsOperator")
@RequestParam(value = "activeListingsOptionsOperator", required = false, defaultValue = "OR") String activeListingsOptionsOperator,
@Parameter(description = "Either true or false. Defaults to null."
+ "Indicates whether to search for developers that do or do not have users.",
allowEmptyValue = true, in = ParameterIn.QUERY, name = "hasUsers")
@RequestParam(value = "hasUsers", required = false, defaultValue = "") String hasUsers,
@Parameter(description = "Zero-based page number used in concert with pageSize. Defaults to 0.",
allowEmptyValue = true, in = ParameterIn.QUERY, name = "pageNumber")
@RequestParam(value = "pageNumber", required = false, defaultValue = "0") Integer pageNumber,
Expand Down Expand Up @@ -250,6 +255,7 @@ public SearchDevelopersController(DeveloperSearchService developerSearchService,
.decertificationDateEnd(decertificationDateEnd)
.activeListingsOptionsStrings(convertToSetWithDelimeter(activeListingsOptionsDelimited, ","))
.activeListingsOptionsOperatorString(activeListingsOptionsOperator)
.hasUsers(!StringUtils.isEmpty(hasUsers) ? BooleanUtils.toBooleanObject(hasUsers) : null)
.pageSize(pageSize)
.pageNumber(pageNumber)
.orderByString(orderBy)
Expand Down Expand Up @@ -311,6 +317,10 @@ public void downloadV3(
+ "specified or may have met any one or more of the activeListingsOptions",
allowEmptyValue = true, in = ParameterIn.QUERY, name = "activeListingsOptionsOperator")
@RequestParam(value = "activeListingsOptionsOperator", required = false, defaultValue = "OR") String activeListingsOptionsOperator,
@Parameter(description = "Either true or false. Defaults to null."
+ "Indicates whether to search for developers that do or do not have users.",
allowEmptyValue = true, in = ParameterIn.QUERY, name = "hasUsers")
@RequestParam(value = "hasUsers", required = false, defaultValue = "") String hasUsers,
@Parameter(description = "What to order by. Options are one of the following: DEVELOPER_NAME, DEVELOPER_CODE, "
+ "DECERTIFICATION_DATE, or STATUS. Defaults to DEVELOPER_NAME.",
allowEmptyValue = true, in = ParameterIn.QUERY, name = "orderBy")
Expand All @@ -332,6 +342,7 @@ public void downloadV3(
.decertificationDateEnd(decertificationDateEnd)
.activeListingsOptionsStrings(convertToSetWithDelimeter(activeListingsOptionsDelimited, ","))
.activeListingsOptionsOperatorString(activeListingsOptionsOperator)
.hasUsers(!StringUtils.isEmpty(hasUsers) ? BooleanUtils.toBooleanObject(hasUsers) : null)
.pageSize(DeveloperSearchRequest.MAX_PAGE_SIZE)
.pageNumber(0)
.orderByString(orderBy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
cellpadding="0" border="0"
align="center"
style="max-width: 800px; width: 100%;"
bgcolor="#FFFFFF">
bgcolor="${paragraph-background-color}">
<tr>
<td align="center" valign="top">
<table width="800"
cellspacing="0"
cellpadding="0"
border="0"
align="center"
style="max-width: 800px; width: 100%; background-color: #ffffff;">
style="max-width: 800px; width: 100%; background-color: ${paragraph-background-color};">
<tr>
<td valign="top"
style="padding: 16px; font-size: 18px; color: #000; font-family: 'Lato', sans-serif;">
Expand Down
11 changes: 10 additions & 1 deletion chpl/chpl-resources/src/main/resources/environment.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ chplUrlBegin=https://chpl.healthit.gov
developerUrlPart=/#/organizations/developers/%s
jndiName=java:comp/env/jdbc/openchpl
persistenceUnitName=openchpl
api.version=47.2.0
api.version=47.3.0
api.description=Created by CHPL Development Team. Please submit any questions using the Health IT \
Feedback Form and select the "Certified Health IT Products List (CHPL)" category. <br/>\
See more at <a href="%s" target="_blank">%s</a>
Expand Down Expand Up @@ -55,10 +55,19 @@ datadog.syntheticsTest.location=aws:us-east-1
#########################################

###### AZURE SETTINGS ######

#ASTP Azure settings
azure.user=SECRET
azure.clientId=SECRET
azure.clientSecret=SECRET
azure.tenantId=SECRET

#ONC Azure settings - going away at some point
azure.user.onc=SECRET
azure.clientId.onc=SECRET
azure.clientSecret.onc=SECRET
azure.tenantId.onc=SECRET

emailRetryIntervalInMinutes=30
# A value of -1 will continue to try sending email indefinitely
emailRetryAttempts=48
Expand Down
1 change: 1 addition & 0 deletions chpl/chpl-resources/src/main/resources/errors.properties
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ listing.developer.bannedOrSuspended.noCreate=The developer %s has a status of %s
listing.developer.userAndSystemMismatch=The user-entered developer %s of '%s' does not match the system value of '%s'.
listing.developer.notFound=No developer with the name '%s' was found in the system.
developer.messaging.missingSubjectOrBody=The message subject and body are both required.
developer.messaging.missingActiveUsers=%s Developer%s ha%s no active users and will not receive this message:

#system-specific developer errors
system.developer.pendingACBNameNullOrEmpty=Error retrieving the pending certified product ONC-ACB name. Please upload a fully valid listing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ private FeatureList() {
public static final String CMS_A9_GRACE_PERIOD_END = "cms-a9-grace-period-end";
public static final String DEMOGRAPHIC_CHANGE_REQUEST = "demographic-change-request";
public static final String SSO = "sso";
public static final String ONC_TO_ASTP_EMAIL = "onc-to-astp-email";
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package gov.healthit.chpl;

import org.ff4j.FF4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;

import com.azure.identity.ClientSecretCredential;
Expand All @@ -19,21 +22,58 @@ public class GraphConfiguration {
@Autowired
private Environment env;

@Autowired
private FF4j ff4j;

//When the feature ONC_TO_ASTP_EMAIL is removed the @Scope annotation is no longer needed.
//It is here so that each time the GraphServiceClient bean is requested the flag will be checked
//to get the correct email configuration. When that no longer needs to be able to be switched between
//ONC and ASTP then we can remove the Prototype Scope.
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean
public GraphServiceClient getGraphServiceClient() {
ClientSecretCredential clientSecretCredential = null;
GraphServiceClient graphServiceClient = null;

LOGGER.debug("Creating a new ClientSecretCredentialBuilder");
LOGGER.info("Creating a new ClientSecretCredentialBuilder");

clientSecretCredential = new ClientSecretCredentialBuilder()
.clientId(env.getProperty("azure.clientId"))
.tenantId(env.getProperty("azure.tenantId"))
.clientSecret(env.getProperty("azure.clientSecret"))
.clientId(getClientId())
.tenantId(getTenantId())
.clientSecret(getClientSecret())
.build();

graphServiceClient = new GraphServiceClient(clientSecretCredential, GRAPH_DEFAULT_SCOPE);

return graphServiceClient;
}

private String getClientId() {
if (ff4j.check(FeatureList.ONC_TO_ASTP_EMAIL)) {
LOGGER.info("Getting ASTP client ID");
return env.getProperty("azure.clientId");
} else {
LOGGER.info("Getting ONC client ID");
return env.getProperty("azure.clientId.onc");
}
}

private String getTenantId() {
if (ff4j.check(FeatureList.ONC_TO_ASTP_EMAIL)) {
LOGGER.info("Getting ASTP tenant ID");
return env.getProperty("azure.tenantId");
} else {
LOGGER.info("Getting ONC tenant ID");
return env.getProperty("azure.tenantId.onc");
}
}

private String getClientSecret() {
if (ff4j.check(FeatureList.ONC_TO_ASTP_EMAIL)) {
LOGGER.info("Getting ASTP client secret");
return env.getProperty("azure.clientSecret");
} else {
LOGGER.info("Getting ONC client secret");
return env.getProperty("azure.clientSecret.onc");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public class DeveloperSearchRequest implements Serializable {
@JsonDeserialize(using = StringToSearchSetOperator.class)
private SearchSetOperator attestationsOptionsOperator;

@Builder.Default
private Boolean hasUsers = null;

@JsonDeserialize(using = CommaDelimitedStringToSetOfLongs.class)
@Builder.Default
private Set<Long> developerIds = new HashSet<Long>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import gov.healthit.chpl.domain.auth.User;
import gov.healthit.chpl.exception.ValidationException;
import gov.healthit.chpl.manager.DeveloperManager;
import gov.healthit.chpl.permissions.ResourcePermissionsFactory;
import gov.healthit.chpl.search.domain.SearchSetOperator;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
Expand All @@ -33,12 +35,15 @@ public class DeveloperSearchService {
private SearchRequestNormalizer searchRequestNormalizer;
private DeveloperManager developerManager;
private DateTimeFormatter dateFormatter;
private ResourcePermissionsFactory resourcePermissionsFactory;

@Autowired
public DeveloperSearchService(@Qualifier("developerSearchRequestValidator") SearchRequestValidator searchRequestValidator,
DeveloperManager developerManager) {
DeveloperManager developerManager,
ResourcePermissionsFactory resourcePermissionsFactory) {
this.searchRequestValidator = searchRequestValidator;
this.developerManager = developerManager;
this.resourcePermissionsFactory = resourcePermissionsFactory;
this.searchRequestNormalizer = new SearchRequestNormalizer();
dateFormatter = DateTimeFormatter.ofPattern(DeveloperSearchRequest.DATE_SEARCH_FORMAT);
}
Expand All @@ -49,6 +54,13 @@ public DeveloperSearchResponse findDevelopers(DeveloperSearchRequest searchReque

List<DeveloperSearchResult> developers = developerManager.getDeveloperSearchResults();
LOGGER.debug("Total developers: " + developers.size());
List<User> allEnabledDeveloperUsers = new ArrayList<User>();
if (searchRequest.getHasUsers() != null
&& (resourcePermissionsFactory.get().isUserRoleOnc() || resourcePermissionsFactory.get().isUserRoleAdmin())) {
allEnabledDeveloperUsers.addAll(resourcePermissionsFactory.get().getAllDeveloperUsers().stream()
.filter(user -> user.getAccountEnabled())
.toList());
}
List<DeveloperSearchResult> matchedDevelopers = developers.stream()
.filter(dev -> matchesSearchTerm(dev, searchRequest.getSearchTerm()))
.filter(dev -> matchesDeveloperName(dev, searchRequest.getDeveloperName()))
Expand All @@ -59,6 +71,7 @@ public DeveloperSearchResponse findDevelopers(DeveloperSearchRequest searchReque
.filter(dev -> matchesDecertificationDateRange(dev, searchRequest.getDecertificationDateStart(), searchRequest.getDecertificationDateEnd()))
.filter(dev -> matchesAttestationsFilter(dev, searchRequest))
.filter(dev -> matchesActiveListingsFilter(dev, searchRequest))
.filter(dev -> matchesHasUsersFilter(dev, searchRequest, allEnabledDeveloperUsers))
.filter(dev -> matchesDeveloperId(dev, searchRequest.getDeveloperIds()))
.collect(Collectors.toList());
LOGGER.debug("Total matched developers: " + matchedDevelopers.size());
Expand Down Expand Up @@ -226,6 +239,25 @@ private boolean matchesActiveListingsFilter(DeveloperSearchResult developer, Dev
return true;
}

private boolean matchesHasUsersFilter(DeveloperSearchResult developer, DeveloperSearchRequest searchRequest,
List<User> allEnabledDeveloperUsers) {
if (CollectionUtils.isEmpty(allEnabledDeveloperUsers)) {
return true;
}

if (searchRequest.getHasUsers()) {
return allEnabledDeveloperUsers.stream()
.flatMap(user -> user.getOrganizations().stream())
.filter(org -> org.getId().equals(developer.getId()))
.findAny().isPresent();
} else {
return allEnabledDeveloperUsers.stream()
.flatMap(user -> user.getOrganizations().stream())
.filter(org -> org.getId().equals(developer.getId()))
.findAny().isEmpty();
}
}

private boolean matchesAttestationsFilter(DeveloperSearchResult developer, DeveloperSearchRequest searchRequest) {
Boolean matchesHasSubmittedAttestationsFilter = null;
if (searchRequest.getAttestationsOptions().contains(AttestationsSearchOptions.HAS_SUBMITTED)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ public File getAsCsv(DeveloperSearchRequest searchRequest, Logger logger) throws
csvPrinter.printRecord(getHeadingRecord());
csvPrinter.flush();
if (!CollectionUtils.isEmpty(allSearchResults)) {
final List<User> allDeveloperUsers = new ArrayList<User>();
List<User> allDeveloperUsers = new ArrayList<User>();
if (isAuthorizedToSeeUserData()) {
allDeveloperUsers.addAll(resourcePermissionsFactory.get().getAllDeveloperUsers());
allDeveloperUsers.addAll(resourcePermissionsFactory.get().getAllDeveloperUsers().stream()
.filter(user -> user.getAccountEnabled())
.toList());
}
allSearchResults.stream()
.forEach(rethrowConsumer(searchResult -> csvPrinter.printRecord(getDeveloperRecord(searchResult, allDeveloperUsers))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ public class ChplHtmlEmailBuilder {
private static final String TITLE_TAG = "${title}";
private static final String PARAGRAPH_HEADING_TAG = "${paragraph-heading}";
private static final String PARAGRAPH_TEXT_TAG = "${paragraph-text}";
private static final String PARAGRAPH_BG_COLOR_TAG = "${paragraph-background-color}";
private static final String TABLE_HEADER_TAG = "${table-header}";
private static final String TABLE_DATA_TAG = "${table-data}";
private static final String TABLE_CAPTION_TAG = "${table-caption}";
private static final String BUTTON_BAR_TAG = "${buttons}";
private static final String FEEDBACK_URL_TAG = "${feedback-url}";
private static final String EMPTY_TABLE_DEFAULT_TEXT = "No Applicable Data";
private static final String DEFAULT_PARAGRAPH_HEADING_LEVEL = "h2";
private static final String DEFAULT_PARAGRAPH_BG_COLOR = "#FFFFFF";
private static final String TABLE_CAPTION_HTML = "<caption>" + TABLE_CAPTION_TAG + "</caption>";

private String htmlSkeleton;
Expand Down Expand Up @@ -78,15 +79,15 @@ public ChplHtmlEmailBuilder heading(String title) {
}

public ChplHtmlEmailBuilder paragraph(String heading, String text) {
return paragraph(heading, text, "h2");
return paragraph(heading, text, DEFAULT_PARAGRAPH_HEADING_LEVEL);
}

public ChplHtmlEmailBuilder paragraph(String heading, String text, String headingLevel) {
if (StringUtils.isAllBlank(heading, text)) {
return this;
}

String modifiedHtmlParagraph = getParagraphHtml(heading, text, headingLevel);
String modifiedHtmlParagraph = getParagraphHtml(heading, text, headingLevel, DEFAULT_PARAGRAPH_BG_COLOR);
addItemToEmailContents(modifiedHtmlParagraph);
return this;
}
Expand Down Expand Up @@ -217,6 +218,10 @@ public String getTableHtml(List<String> tableHeadings, List<List<String>> tableD
}

public String getParagraphHtml(String heading, String text, String headingLevel) {
return getParagraphHtml(heading, text, headingLevel, null);
}

public String getParagraphHtml(String heading, String text, String headingLevel, String bgColor) {
if (StringUtils.isEmpty(headingLevel)) {
headingLevel = DEFAULT_PARAGRAPH_HEADING_LEVEL;
}
Expand All @@ -233,6 +238,11 @@ public String getParagraphHtml(String heading, String text, String headingLevel)
} else {
customHtmlParagraph = customHtmlParagraph.replace(PARAGRAPH_TEXT_TAG, "");
}
if (!StringUtils.isEmpty(bgColor)) {
customHtmlParagraph = customHtmlParagraph.replace(PARAGRAPH_BG_COLOR_TAG, bgColor);
} else {
customHtmlParagraph = customHtmlParagraph.replace(PARAGRAPH_BG_COLOR_TAG, DEFAULT_PARAGRAPH_BG_COLOR);
}
return customHtmlParagraph;
}
}
Loading

0 comments on commit a8100a6

Please sign in to comment.