Skip to content

Commit ea456f0

Browse files
azayatiexo-swf
authored andcommitted
feat: Digest email lines for task notifications - EXO-89486_EXO-89487 - eXIP7.3.0.22 (#642)
1 parent 50dc6d5 commit ea456f0

4 files changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* This file is part of the Meeds project (https://meeds.io/).
3+
*
4+
* Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18+
*/
19+
package io.meeds.task.digest;
20+
21+
import org.apache.commons.lang3.StringUtils;
22+
23+
import org.exoplatform.commons.utils.CommonsUtils;
24+
import org.exoplatform.container.ExoContainerContext;
25+
import org.exoplatform.container.xml.InitParams;
26+
import org.exoplatform.social.core.identity.model.Identity;
27+
import org.exoplatform.social.core.manager.IdentityManager;
28+
import org.exoplatform.task.dto.TaskDto;
29+
import org.exoplatform.task.integration.notification.NotificationUtils;
30+
import org.exoplatform.task.exception.EntityNotFoundException;
31+
import org.exoplatform.task.service.TaskService;
32+
33+
import io.meeds.commons.digest.model.DigestItem;
34+
import io.meeds.commons.digest.model.DigestLine;
35+
import io.meeds.commons.digest.plugin.DigestLineContext;
36+
import io.meeds.commons.digest.plugin.DigestLinePlugin;
37+
import io.meeds.task.plugin.TaskPermanentLinkPlugin;
38+
39+
/**
40+
* The digest email lines of the task notifications: assigned, added as
41+
* coworker, mentioned. The task and its project are read fresh from the
42+
* stored task id; a deleted task gives no line.
43+
*/
44+
public class TaskDigestLinePlugin extends DigestLinePlugin {
45+
46+
public static final String TASK_ASSIGN_PLUGIN = "TaskAssignPlugin";
47+
48+
public static final String TASK_COWORKER_PLUGIN = "TaskCoworkerPlugin";
49+
50+
public static final String TASK_MENTIONED_PLUGIN = "TaskMentionedPlugin";
51+
52+
/** The stored parameters: the very keys the notification plugins write */
53+
static final String TASK_ID_PARAM = NotificationUtils.TASK_ID;
54+
55+
static final String CREATOR_PARAM = NotificationUtils.CREATOR.getKey();
56+
57+
static final String TASK_URL_PARAM = NotificationUtils.TASK_URL;
58+
59+
private static final String LINE_KEY_PREFIX = "digest.line.";
60+
61+
private TaskService taskService;
62+
63+
private IdentityManager identityManager;
64+
65+
public TaskDigestLinePlugin(InitParams params) {
66+
super(params);
67+
}
68+
69+
TaskDigestLinePlugin(InitParams params, TaskService taskService, IdentityManager identityManager) {
70+
super(params);
71+
this.taskService = taskService;
72+
this.identityManager = identityManager;
73+
}
74+
75+
@Override
76+
public DigestLine buildLine(DigestItem item, DigestLineContext context) {
77+
TaskDto task = findTask(item.getParam(TASK_ID_PARAM));
78+
if (task == null) {
79+
return null;
80+
}
81+
String key = LINE_KEY_PREFIX + item.getPluginId();
82+
String project = task.getStatus() == null || task.getStatus().getProject() == null ? ""
83+
: task.getStatus().getProject().getName();
84+
String url = url(item, task);
85+
return switch (item.getPluginId()) {
86+
case TASK_ASSIGN_PLUGIN -> DigestLine.of(key, task.getTitle(), project).withUrl(url);
87+
case TASK_COWORKER_PLUGIN, TASK_MENTIONED_PLUGIN ->
88+
DigestLine.of(key, fullName(item.getParam(CREATOR_PARAM)), task.getTitle(), project).withUrl(url);
89+
default -> null;
90+
};
91+
}
92+
93+
private TaskDto findTask(String taskId) {
94+
if (StringUtils.isBlank(taskId)) {
95+
return null;
96+
}
97+
try {
98+
return getTaskService().getTask(Long.parseLong(taskId));
99+
} catch (EntityNotFoundException | NumberFormatException e) {
100+
return null;
101+
}
102+
}
103+
104+
/**
105+
* The link the instant email used when it was stored, otherwise the task
106+
* detail page of the platform
107+
*/
108+
protected String url(DigestItem item, TaskDto task) {
109+
String stored = item.getParam(TASK_URL_PARAM);
110+
if (StringUtils.startsWith(stored, "http")) {
111+
return stored;
112+
}
113+
return CommonsUtils.getCurrentDomain()
114+
+ String.format(TaskPermanentLinkPlugin.URL_FORMAT, CommonsUtils.getCurrentPortalOwner(), task.getId());
115+
}
116+
117+
private String fullName(String username) {
118+
if (StringUtils.isBlank(username)) {
119+
return "";
120+
}
121+
Identity identity = getIdentityManager().getOrCreateUserIdentity(username);
122+
String fullName = identity == null || identity.getProfile() == null ? null : identity.getProfile().getFullName();
123+
return StringUtils.isBlank(fullName) ? username : fullName;
124+
}
125+
126+
private TaskService getTaskService() {
127+
if (taskService == null) {
128+
taskService = ExoContainerContext.getService(TaskService.class);
129+
}
130+
return taskService;
131+
}
132+
133+
private IdentityManager getIdentityManager() {
134+
if (identityManager == null) {
135+
identityManager = ExoContainerContext.getService(IdentityManager.class);
136+
}
137+
return identityManager;
138+
}
139+
140+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* This file is part of the Meeds project (https://meeds.io/).
3+
*
4+
* Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18+
*/
19+
package io.meeds.task.digest;
20+
21+
import static org.junit.Assert.assertEquals;
22+
import static org.junit.Assert.assertNotNull;
23+
import static org.junit.Assert.assertNull;
24+
import static org.mockito.Mockito.lenient;
25+
import static org.mockito.Mockito.when;
26+
27+
import java.time.Instant;
28+
import java.time.ZoneId;
29+
import java.util.ArrayList;
30+
import java.util.LinkedHashMap;
31+
import java.util.List;
32+
import java.util.Locale;
33+
import java.util.Map;
34+
35+
import org.junit.Before;
36+
import org.junit.Test;
37+
import org.junit.runner.RunWith;
38+
import org.mockito.Mock;
39+
import org.mockito.junit.MockitoJUnitRunner;
40+
41+
import org.exoplatform.container.xml.InitParams;
42+
import org.exoplatform.container.xml.ValuesParam;
43+
import org.exoplatform.social.core.identity.model.Identity;
44+
import org.exoplatform.social.core.identity.model.Profile;
45+
import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
46+
import org.exoplatform.social.core.manager.IdentityManager;
47+
import org.exoplatform.task.dto.ProjectDto;
48+
import org.exoplatform.task.dto.StatusDto;
49+
import org.exoplatform.task.dto.TaskDto;
50+
import org.exoplatform.task.exception.EntityNotFoundException;
51+
import org.exoplatform.task.service.TaskService;
52+
53+
import io.meeds.commons.digest.model.DigestItem;
54+
import io.meeds.commons.digest.model.DigestLine;
55+
import io.meeds.commons.digest.plugin.DigestLineContext;
56+
57+
@RunWith(MockitoJUnitRunner.class)
58+
public class TaskDigestLinePluginTest {
59+
60+
private static final DigestLineContext CONTEXT = new DigestLineContext("ayoub", Locale.ENGLISH, ZoneId.of("Europe/Paris"));
61+
62+
@Mock
63+
private TaskService taskService;
64+
65+
@Mock
66+
private IdentityManager identityManager;
67+
68+
private TaskDigestLinePlugin plugin;
69+
70+
@Before
71+
public void setUp() throws Exception {
72+
InitParams params = new InitParams();
73+
ValuesParam pluginIds = new ValuesParam();
74+
pluginIds.setName("pluginIds");
75+
pluginIds.setValues(new ArrayList<>(List.of(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN,
76+
TaskDigestLinePlugin.TASK_COWORKER_PLUGIN,
77+
TaskDigestLinePlugin.TASK_MENTIONED_PLUGIN)));
78+
params.addParameter(pluginIds);
79+
// The platform link needs the running portal: only the stored link is
80+
// exercised here
81+
plugin = new TaskDigestLinePlugin(params, taskService, identityManager);
82+
83+
ProjectDto project = new ProjectDto();
84+
project.setName("Website");
85+
StatusDto status = new StatusDto();
86+
status.setProject(project);
87+
TaskDto task = new TaskDto();
88+
task.setId(7);
89+
task.setTitle("Write the release notes");
90+
task.setStatus(status);
91+
lenient().when(taskService.getTask(7)).thenReturn(task);
92+
lenient().when(taskService.getTask(404)).thenThrow(new EntityNotFoundException(404, TaskDto.class));
93+
Identity john = new Identity(OrganizationIdentityProvider.NAME, "john");
94+
Profile profile = new Profile(john);
95+
profile.setProperty(Profile.FULL_NAME, "John Smith");
96+
john.setProfile(profile);
97+
lenient().when(identityManager.getOrCreateUserIdentity("john")).thenReturn(john);
98+
}
99+
100+
@Test
101+
public void testAssignedLineHasNoActor() {
102+
DigestLine line = plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, "taskId", "7", "creator", "john",
103+
"taskUrl", "https://platform/portal/dw/tasks/taskDetail/7"),
104+
CONTEXT);
105+
assertNotNull(line);
106+
assertEquals("digest.line.TaskAssignPlugin", line.getLabelKey());
107+
assertEquals(List.of("Write the release notes", "Website"), line.getArgs());
108+
assertEquals("https://platform/portal/dw/tasks/taskDetail/7", line.getUrl());
109+
}
110+
111+
@Test
112+
public void testCoworkerAndMentionLinesNameTheActor() {
113+
DigestLine coworker = plugin.buildLine(item(TaskDigestLinePlugin.TASK_COWORKER_PLUGIN, "taskId", "7", "creator", "john",
114+
"taskUrl", "https://platform/t/7"),
115+
CONTEXT);
116+
DigestLine mention = plugin.buildLine(item(TaskDigestLinePlugin.TASK_MENTIONED_PLUGIN, "taskId", "7", "creator", "john",
117+
"taskUrl", "https://platform/t/7"),
118+
CONTEXT);
119+
assertNotNull(coworker);
120+
assertNotNull(mention);
121+
assertEquals(List.of("John Smith", "Write the release notes", "Website"), coworker.getArgs());
122+
assertEquals(List.of("John Smith", "Write the release notes", "Website"), mention.getArgs());
123+
}
124+
125+
@Test
126+
public void testDeletedTaskGivesNoLine() {
127+
assertNull(plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, "taskId", "404"), CONTEXT));
128+
assertNull(plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, "taskId", "not a number"), CONTEXT));
129+
assertNull(plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN), CONTEXT));
130+
}
131+
132+
@Test
133+
public void testUnknownTypeGivesNoLine() {
134+
assertNull(plugin.buildLine(item("TaskCompletedPlugin", "taskId", "7", "taskUrl", "https://platform/t/7"), CONTEXT));
135+
}
136+
137+
private static DigestItem item(String pluginId, String... params) {
138+
Map<String, String> map = new LinkedHashMap<>();
139+
for (int i = 0; i + 1 < params.length; i += 2) {
140+
map.put(params[i], params[i + 1]);
141+
}
142+
return new DigestItem(1, "ayoub", pluginId, "tasks", Instant.now(), map);
143+
}
144+
145+
}

‎webapps/src/main/resources/locale/notification/TaskNotification_en.properties‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,7 @@ Notification.digest.more.TaskMentionedPlugin=You've been mentioned in $COUNT tas
137137

138138
# Digest mail notifications: label of the category this addon owns
139139
digest.category.tasks=Tasks
140+
# Digest mail notifications: one line per notification type
141+
digest.line.TaskAssignPlugin=You were assigned to "{0}" ({1})
142+
digest.line.TaskCoworkerPlugin={0} added you as coworker on "{1}" ({2})
143+
digest.line.TaskMentionedPlugin={0} mentioned you in "{1}" ({2})

‎webapps/src/main/webapp/WEB-INF/conf/task-addon/notification-configuration.xml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,5 +408,20 @@
408408
</values-param>
409409
</init-params>
410410
</component-plugin>
411+
<!-- The email lines of these types, built at send time from the stored ids -->
412+
<component-plugin>
413+
<name>task.digest.lines</name>
414+
<set-method>addLineProvider</set-method>
415+
<type>io.meeds.task.digest.TaskDigestLinePlugin</type>
416+
<description>Builds the digest email lines of the task notifications</description>
417+
<init-params>
418+
<values-param>
419+
<name>pluginIds</name>
420+
<value>TaskAssignPlugin</value>
421+
<value>TaskCoworkerPlugin</value>
422+
<value>TaskMentionedPlugin</value>
423+
</values-param>
424+
</init-params>
425+
</component-plugin>
411426
</external-component-plugins>
412427
</configuration>

0 commit comments

Comments
 (0)