Skip to content

Commit

Permalink
20025 add partner locations to email template
Browse files Browse the repository at this point in the history
  • Loading branch information
palina-krukovich committed Oct 30, 2024
1 parent c5d662e commit 30ab900
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 89 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ dependencies {

//email
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

implementation 'junit:junit:4.13.2'
implementation group: 'org.mapstruct', name: 'mapstruct', version: '1.5.2.Final'
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/kontur/disasterninja/dto/Partner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.kontur.disasterninja.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

@Data
@AllArgsConstructor
public class Partner {
private String name;
private int totalLocations;
private Set<String> locations;
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package io.kontur.disasterninja.notifications.email;

import io.kontur.disasterninja.dto.EmailDto;
import io.kontur.disasterninja.dto.Partner;
import io.kontur.disasterninja.dto.eventapi.EventApiEventDto;
import io.kontur.disasterninja.dto.eventapi.FeedEpisode;
import io.kontur.disasterninja.notifications.MessageFormatter;
import io.micrometer.core.instrument.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;

import static io.kontur.disasterninja.util.FormatUtil.formatNumber;
import static java.time.ZoneOffset.UTC;
import static org.apache.commons.lang3.text.WordUtils.capitalizeFully;

@Component
Expand All @@ -26,61 +28,53 @@ public class EmailMessageFormatter extends MessageFormatter {

private final static Logger LOG = LoggerFactory.getLogger(EmailMessageFormatter.class);

private final static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MMMM d, yyyy");
private final static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MMMM d, yyyy HH:mm 'UTC'");

private final TemplateEngine templateEngine;

@Value("${notifications.konturUrlPattern}")
private String konturUrlPattern;

@Value("${notifications.feed}")
private String feed;

public EmailDto format(EventApiEventDto event, Map<String, Object> urbanPopulationProperties,
Map<String, Double> analytics) throws IOException {
String textTemplate = loadTemplate("notification/gg-email-template.txt");
String htmlTemplate = loadTemplate("notification/gg-email-template.html");
public EmailMessageFormatter(TemplateEngine templateEngine) {
this.templateEngine = templateEngine;
}

public EmailDto format(EventApiEventDto event, Map<String, Object> urbanPopulationProperties,
Map<String, Double> analytics, List<Partner> partners) throws IOException {
FeedEpisode lastEpisode = getLatestEpisode(event);

return new EmailDto(
createSubject(event, lastEpisode),
replacePlaceholders(textTemplate, event, lastEpisode, urbanPopulationProperties),
replacePlaceholders(htmlTemplate, event, lastEpisode, urbanPopulationProperties));
}

private String loadTemplate(String templateName) throws IOException {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(templateName)) {
if (inputStream == null) {
throw new IllegalArgumentException("Failed to find notification template: " + templateName);
}
return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
}
generateTemplate("gg-email-template.txt", event, lastEpisode, urbanPopulationProperties, partners),
generateTemplate("gg-email-template.html", event, lastEpisode, urbanPopulationProperties, partners));
}

private String createSubject(EventApiEventDto event, FeedEpisode lastEpisode) {
StringBuilder sb = new StringBuilder();
sb.append(getMessageColorCode(event, lastEpisode, true));
sb.append(getEventStatus(event));
sb.append(event.getName());
return sb.toString();
}

private String replacePlaceholders(String template, EventApiEventDto event, FeedEpisode lastEpisode, Map<String, Object> urbanPopulationProperties) {

return template
.replace("${name}", event.getName())
.replace("${link}", String.format(konturUrlPattern, event.getEventId(), feed))
.replace("${description}", lastEpisode.getDescription())
.replace("${urbanPopulation}", formatNumber(urbanPopulationProperties.get("population")))
.replace("${urbanArea}", formatNumber(urbanPopulationProperties.get("areaKm2")))
.replace("${population}", formatNumber(lastEpisode.getEpisodeDetails().get("population")))
.replace("${populatedArea}", formatNumber(lastEpisode.getEpisodeDetails().get("populatedAreaKm2")))
.replace("${industrialArea}", formatNumber(lastEpisode.getEpisodeDetails().get("industrialAreaKm2")))
.replace("${forestArea}", formatNumber(lastEpisode.getEpisodeDetails().get("forestAreaKm2")))
.replace("${location}", event.getLocation())
.replace("${type}", capitalizeFully(event.getType()))
.replace("${severity}", capitalizeFully(event.getSeverity().name()))
.replace("${startedAt}", event.getStartedAt().format(dateFormatter))
.replace("${updatedAt}", event.getUpdatedAt().format(dateFormatter));
public String generateTemplate(String template, EventApiEventDto event, FeedEpisode lastEpisode, Map<String, Object> urbanPopulationProperties, List<Partner> partners) {
Context context = new Context();
context.setVariable("link", String.format(konturUrlPattern, event.getEventId(), feed));
context.setVariable("description", lastEpisode.getDescription());
context.setVariable("urbanPopulation", formatNumber(urbanPopulationProperties.get("population")));
context.setVariable("urbanArea", formatNumber(urbanPopulationProperties.get("areaKm2")));
context.setVariable("population", formatNumber(lastEpisode.getEpisodeDetails().get("population")));
context.setVariable("populatedArea", formatNumber(lastEpisode.getEpisodeDetails().get("populatedAreaKm2")));
context.setVariable("industrialArea", formatNumber(lastEpisode.getEpisodeDetails().get("industrialAreaKm2")));
context.setVariable("forestArea", formatNumber(lastEpisode.getEpisodeDetails().get("forestAreaKm2")));
context.setVariable("type", capitalizeFully(event.getType()));
context.setVariable("severity", capitalizeFully(event.getSeverity().name()));
context.setVariable("location", event.getLocation());
context.setVariable("startedAt", event.getStartedAt().withOffsetSameInstant(UTC).format(dateFormatter));
context.setVariable("updatedAt", event.getUpdatedAt().withOffsetSameInstant(UTC).format(dateFormatter));
context.setVariable("partners", partners);

return templateEngine.process(template, context);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.kontur.disasterninja.notifications.email;

import io.kontur.disasterninja.dto.EmailDto;
import io.kontur.disasterninja.dto.Partner;
import io.kontur.disasterninja.dto.eventapi.EventApiEventDto;
import io.kontur.disasterninja.notifications.NotificationService;
import io.kontur.disasterninja.service.GeometryTransformer;
Expand All @@ -12,11 +13,14 @@
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.wololo.geojson.Feature;
import org.wololo.geojson.FeatureCollection;
import org.wololo.geojson.Geometry;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Component
@ConditionalOnProperty(value = "notifications.enabled")
Expand Down Expand Up @@ -50,7 +54,8 @@ public void process(EventApiEventDto event, Map<String, Object> urbanPopulationP
LOG.info("Found new event, sending email notification. Event ID = '{}', name = '{}'", event.getEventId(), event.getName());

try {
EmailDto emailDto = emailMessageFormatter.format(event, urbanPopulationProperties, analytics);
List<Partner> partners = getPartners(getRelevantLocations(event.getGeometries()));
EmailDto emailDto = emailMessageFormatter.format(event, urbanPopulationProperties, analytics, partners);
emailSender.send(emailDto);
LOG.info("Successfully sent email notification. Event ID = '{}', name = '{}'", event.getEventId(), event.getName());
} catch (Exception e) {
Expand All @@ -60,8 +65,27 @@ public void process(EventApiEventDto event, Map<String, Object> urbanPopulationP

@Override
public boolean isApplicable(EventApiEventDto event) {
Geometry geometry = geometryTransformer.makeValid(geometryTransformer.getGeometryFromGeoJson(event.getGeometries()));
List<Feature> features = layersApiService.getFeatures(geometry, relevantLocationsLayer, UUID.fromString(relevantLocationsLayerAppId));
return !CollectionUtils.isEmpty(features);
return event.getEventDetails() != null
&& event.getEpisodes() != null
&& event.getEpisodes().stream().noneMatch(episode -> episode.getEpisodeDetails() == null)
&& !CollectionUtils.isEmpty(getRelevantLocations(event.getGeometries()));
}

private List<Partner> getPartners(List<Feature> partnerLocations) {
Map<String, List<String>> partnerLocationMap = partnerLocations.stream()
.collect(Collectors.groupingBy(
feature -> (String) feature.getProperties().get("partner"),
Collectors.mapping(
feature -> (String) feature.getProperties().get("location"),
Collectors.toList())));

return partnerLocationMap.entrySet().stream()
.map(entry -> new Partner(entry.getKey(), entry.getValue().size(), new HashSet<>(entry.getValue())))
.collect(Collectors.toList());
}

private List<Feature> getRelevantLocations(FeatureCollection fc) {
Geometry geometry = geometryTransformer.makeValid(geometryTransformer.getGeometryFromGeoJson(fc));
return layersApiService.getFeatures(geometry, relevantLocationsLayer, UUID.fromString(relevantLocationsLayerAppId));
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ spring:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
default-property-inclusion: non_null
thymeleaf:
prefix: classpath:/notification/
cache: false
encoding: UTF-8

graphql:
apollo:
Expand Down
134 changes: 102 additions & 32 deletions src/main/resources/notification/gg-email-template.html
Original file line number Diff line number Diff line change
@@ -1,44 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${name}</title>
<title th:text="${name}">Event Notification</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 10px auto;
}
h3 {
font-size: 18px;
margin-bottom: 8px;
}
p, li {
font-size: 14px;
line-height: 1.5;
}
.button-link {
display: block;
padding: 8px;
width: 180px;
background-color: #4A90E2;
color: #ffffff !important;
text-decoration: none;
font-size: 14px;
text-align: center;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.button-link:hover {
background-color: #3B73B5;
color: #ffffff !important;
}
</style>
</head>
<body>
<div class="section">
<h3>🌍 Event Overview:</h3>
<p>View the event on <a href="${link}" target="_blank">globalgiving.kontur.io</a>.</p>
<p>${description}</p>
</div>
<div class="container">
<div class="section">
<h3>DISASTER ALERT</h3>
<p th:text="${description}"></p>
<a th:href="${link}" target="_blank" class="button-link">View Full Event Details</a>
</div>

<div class="section">
<h3>👥 Impact Summary:</h3>
<ul>
<li><strong>Urban core:</strong> ${urbanPopulation} people on ${urbanArea} km².</li>
<li><strong>Total population:</strong> ${population} people on ${populatedArea} km².</li>
<li><strong>Industrial area:</strong> ${industrialArea} km².</li>
<li><strong>Forest area:</strong> ${forestArea} km².</li>
<li><strong>Location:</strong> ${location}</li>
</ul>
</div>
<div class="section" th:if="${urbanPopulation != null && urbanArea != null || population != null && populatedArea != null || industrialArea != null || forestArea != null}">
<h3>👥 Impact Summary</h3>
<ul>
<li th:if="${urbanPopulation != null && urbanArea != null}">
<strong>Urban core:</strong> <span th:text="${urbanPopulation}"></span> people on <span th:text="${urbanArea}"></span> km²
</li>
<li th:if="${population != null && populatedArea != null}">
<strong>Total population:</strong> <span th:text="${population}"></span> people on <span th:text="${populatedArea}"></span> km²
</li>
<li th:if="${industrialArea != null}">
<strong>Industrial area:</strong> <span th:text="${industrialArea}"></span> km²
</li>
<li th:if="${forestArea != null}">
<strong>Forest area:</strong> <span th:text="${forestArea}"></span> km²
</li>
</ul>
</div>

<div class="section">
<h3>🗓️ Event Status:</h3>
<ul>
<li><strong>Type:</strong> ${type}</li>
<li><strong>Severity:</strong> ${severity}</li>
<li><strong>Start Date:</strong> ${startedAt}</li>
<li><strong>Latest Update:</strong> ${updatedAt}</li>
</ul>
</div>
<div class="section" th:if="${type != null || severity != null || startedAt != null || updatedAt != null || location != null}">
<h3>🗓️ Event Status</h3>
<ul>
<li th:if="${type != null}">
<strong>Type:</strong> <span th:text="${type}"></span>
</li>
<li th:if="${severity != null}">
<strong>Severity:</strong> <span th:text="${severity}"></span>
</li>
<li th:if="${location != null}">
<strong>Location:</strong> <span th:text="${location}"></span>
</li>
<li th:if="${startedAt != null}">
<strong>Start Date:</strong> <span th:text="${startedAt}"></span>
</li>
<li th:if="${updatedAt != null}">
<strong>Latest Update:</strong> <span th:text="${updatedAt}"></span>
</li>
</ul>
</div>

<div class="section">
<h3>✉️ Need Help?</h3>
<p>For any questions or issues, feel free to contact us at
<a href="mailto:[email protected]">[email protected]</a>.
</p>
</div>
<div class="section" th:if="${partners != null && #lists.size(partners) > 0}">
<h3>🏢 Affected Partner Locations</h3>
<ul>
<li th:each="partner : ${partners}">
Partner <strong><span th:text="${partner.name}"></span></strong> has <span th:text="${partner.totalLocations}"></span> affected location<span th:if="${partner.totalLocations > 1}">s</span>:
<ul>
<li th:each="partnerLocation, iterStat : ${partner.locations}">
<span th:if="${iterStat.index < 3}" th:text="${partnerLocation}"></span>
<span th:if="${iterStat.index == 3}">See full list of locations on <a th:href="${link}" target="_blank">globalgiving.kontur.io</a></span>
</li>
</ul>
</li>
</ul>
</div>

<hr>

<div class="section">
<p>For any questions or issues, feel free to contact us at
<a href="mailto:[email protected]">[email protected]</a>.
</p>
</div>
</div>
</body>
</html>
Loading

0 comments on commit 30ab900

Please sign in to comment.