From f846d7e18c9f91d5a608b70578beb3b5def0af30 Mon Sep 17 00:00:00 2001 From: azayati Date: Thu, 3 Sep 2026 23:18:05 +0200 Subject: [PATCH 1/2] feat: Digest email lines for the task notifications - EXO-89486 - eXIP7.3.0.22 One Kernel line plugin declared next to the digest category: the email line of each digest notification type is built at send time from the stored ids, a vanished object gives no line. Co-Authored-By: Claude Fable 5.1 --- .../task/digest/TaskDigestLinePlugin.java | 139 +++++++++++++++++ .../task/digest/TaskDigestLinePluginTest.java | 145 ++++++++++++++++++ .../TaskNotification_en.properties | 4 + .../task-addon/notification-configuration.xml | 15 ++ 4 files changed, 303 insertions(+) create mode 100644 services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java create mode 100644 services/src/test/java/io/meeds/task/digest/TaskDigestLinePluginTest.java diff --git a/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java b/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java new file mode 100644 index 000000000..6fce1bb21 --- /dev/null +++ b/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java @@ -0,0 +1,139 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.task.digest; + +import org.apache.commons.lang3.StringUtils; + +import org.exoplatform.commons.utils.CommonsUtils; +import org.exoplatform.container.ExoContainerContext; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.service.LinkProvider; +import org.exoplatform.task.dto.TaskDto; +import org.exoplatform.task.exception.EntityNotFoundException; +import org.exoplatform.task.service.TaskService; + +import io.meeds.commons.digest.model.DigestItem; +import io.meeds.commons.digest.model.DigestLine; +import io.meeds.commons.digest.plugin.DigestLineContext; +import io.meeds.commons.digest.plugin.DigestLinePlugin; + +/** + * The digest email lines of the task notifications: assigned, added as + * coworker, mentioned. The task and its project are read fresh from the + * stored task id; a deleted task gives no line. + */ +public class TaskDigestLinePlugin extends DigestLinePlugin { + + public static final String TASK_ASSIGN_PLUGIN = "TaskAssignPlugin"; + + public static final String TASK_COWORKER_PLUGIN = "TaskCoworkerPlugin"; + + public static final String TASK_MENTIONED_PLUGIN = "TaskMentionedPlugin"; + + /** The stored parameters, the same names as in the notifications */ + static final String TASK_ID_PARAM = "taskId"; + + static final String CREATOR_PARAM = "creator"; + + static final String TASK_URL_PARAM = "taskUrl"; + + private static final String LINE_KEY_PREFIX = "digest.line."; + + private TaskService taskService; + + private IdentityManager identityManager; + + public TaskDigestLinePlugin(InitParams params) { + super(params); + } + + TaskDigestLinePlugin(InitParams params, TaskService taskService, IdentityManager identityManager) { + super(params); + this.taskService = taskService; + this.identityManager = identityManager; + } + + @Override + public DigestLine buildLine(DigestItem item, DigestLineContext context) { + TaskDto task = findTask(item.getParam(TASK_ID_PARAM)); + if (task == null) { + return null; + } + String key = LINE_KEY_PREFIX + item.getPluginId(); + String project = task.getStatus() == null || task.getStatus().getProject() == null ? "" + : task.getStatus().getProject().getName(); + String url = url(item, task); + return switch (item.getPluginId()) { + case TASK_ASSIGN_PLUGIN -> DigestLine.of(key, task.getTitle(), project).withUrl(url); + case TASK_COWORKER_PLUGIN, TASK_MENTIONED_PLUGIN -> + DigestLine.of(key, fullName(item.getParam(CREATOR_PARAM)), task.getTitle(), project).withUrl(url); + default -> null; + }; + } + + private TaskDto findTask(String taskId) { + if (StringUtils.isBlank(taskId)) { + return null; + } + try { + return getTaskService().getTask(Long.parseLong(taskId)); + } catch (EntityNotFoundException | NumberFormatException e) { + return null; + } + } + + /** + * The link the instant email used when it was stored, otherwise the task + * detail page of the platform + */ + protected String url(DigestItem item, TaskDto task) { + String stored = item.getParam(TASK_URL_PARAM); + if (StringUtils.startsWith(stored, "http")) { + return stored; + } + return CommonsUtils.getCurrentDomain() + "/" + LinkProvider.getPortalName(null) + "/" + CommonsUtils.getCurrentPortalOwner() + + "/tasks/taskDetail/" + task.getId(); + } + + private String fullName(String username) { + if (StringUtils.isBlank(username)) { + return ""; + } + Identity identity = getIdentityManager().getOrCreateUserIdentity(username); + String fullName = identity == null || identity.getProfile() == null ? null : identity.getProfile().getFullName(); + return StringUtils.isBlank(fullName) ? username : fullName; + } + + private TaskService getTaskService() { + if (taskService == null) { + taskService = ExoContainerContext.getService(TaskService.class); + } + return taskService; + } + + private IdentityManager getIdentityManager() { + if (identityManager == null) { + identityManager = ExoContainerContext.getService(IdentityManager.class); + } + return identityManager; + } + +} diff --git a/services/src/test/java/io/meeds/task/digest/TaskDigestLinePluginTest.java b/services/src/test/java/io/meeds/task/digest/TaskDigestLinePluginTest.java new file mode 100644 index 000000000..7a291a430 --- /dev/null +++ b/services/src/test/java/io/meeds/task/digest/TaskDigestLinePluginTest.java @@ -0,0 +1,145 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.task.digest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.container.xml.ValuesParam; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.model.Profile; +import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.task.dto.ProjectDto; +import org.exoplatform.task.dto.StatusDto; +import org.exoplatform.task.dto.TaskDto; +import org.exoplatform.task.exception.EntityNotFoundException; +import org.exoplatform.task.service.TaskService; + +import io.meeds.commons.digest.model.DigestItem; +import io.meeds.commons.digest.model.DigestLine; +import io.meeds.commons.digest.plugin.DigestLineContext; + +@RunWith(MockitoJUnitRunner.class) +public class TaskDigestLinePluginTest { + + private static final DigestLineContext CONTEXT = new DigestLineContext("ayoub", Locale.ENGLISH, ZoneId.of("Europe/Paris")); + + @Mock + private TaskService taskService; + + @Mock + private IdentityManager identityManager; + + private TaskDigestLinePlugin plugin; + + @Before + public void setUp() throws Exception { + InitParams params = new InitParams(); + ValuesParam pluginIds = new ValuesParam(); + pluginIds.setName("pluginIds"); + pluginIds.setValues(new ArrayList<>(List.of(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, + TaskDigestLinePlugin.TASK_COWORKER_PLUGIN, + TaskDigestLinePlugin.TASK_MENTIONED_PLUGIN))); + params.addParameter(pluginIds); + // The platform link needs the running portal: only the stored link is + // exercised here + plugin = new TaskDigestLinePlugin(params, taskService, identityManager); + + ProjectDto project = new ProjectDto(); + project.setName("Website"); + StatusDto status = new StatusDto(); + status.setProject(project); + TaskDto task = new TaskDto(); + task.setId(7); + task.setTitle("Write the release notes"); + task.setStatus(status); + lenient().when(taskService.getTask(7)).thenReturn(task); + lenient().when(taskService.getTask(404)).thenThrow(new EntityNotFoundException(404, TaskDto.class)); + Identity john = new Identity(OrganizationIdentityProvider.NAME, "john"); + Profile profile = new Profile(john); + profile.setProperty(Profile.FULL_NAME, "John Smith"); + john.setProfile(profile); + lenient().when(identityManager.getOrCreateUserIdentity("john")).thenReturn(john); + } + + @Test + public void testAssignedLineHasNoActor() { + DigestLine line = plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, "taskId", "7", "creator", "john", + "taskUrl", "https://platform/portal/dw/tasks/taskDetail/7"), + CONTEXT); + assertNotNull(line); + assertEquals("digest.line.TaskAssignPlugin", line.getLabelKey()); + assertEquals(List.of("Write the release notes", "Website"), line.getArgs()); + assertEquals("https://platform/portal/dw/tasks/taskDetail/7", line.getUrl()); + } + + @Test + public void testCoworkerAndMentionLinesNameTheActor() { + DigestLine coworker = plugin.buildLine(item(TaskDigestLinePlugin.TASK_COWORKER_PLUGIN, "taskId", "7", "creator", "john", + "taskUrl", "https://platform/t/7"), + CONTEXT); + DigestLine mention = plugin.buildLine(item(TaskDigestLinePlugin.TASK_MENTIONED_PLUGIN, "taskId", "7", "creator", "john", + "taskUrl", "https://platform/t/7"), + CONTEXT); + assertNotNull(coworker); + assertNotNull(mention); + assertEquals(List.of("John Smith", "Write the release notes", "Website"), coworker.getArgs()); + assertEquals(List.of("John Smith", "Write the release notes", "Website"), mention.getArgs()); + } + + @Test + public void testDeletedTaskGivesNoLine() { + assertNull(plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, "taskId", "404"), CONTEXT)); + assertNull(plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN, "taskId", "not a number"), CONTEXT)); + assertNull(plugin.buildLine(item(TaskDigestLinePlugin.TASK_ASSIGN_PLUGIN), CONTEXT)); + } + + @Test + public void testUnknownTypeGivesNoLine() { + assertNull(plugin.buildLine(item("TaskCompletedPlugin", "taskId", "7", "taskUrl", "https://platform/t/7"), CONTEXT)); + } + + private static DigestItem item(String pluginId, String... params) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i + 1 < params.length; i += 2) { + map.put(params[i], params[i + 1]); + } + return new DigestItem(1, "ayoub", pluginId, "tasks", Instant.now(), map); + } + +} diff --git a/webapps/src/main/resources/locale/notification/TaskNotification_en.properties b/webapps/src/main/resources/locale/notification/TaskNotification_en.properties index 07646ae06..90b6ca32f 100644 --- a/webapps/src/main/resources/locale/notification/TaskNotification_en.properties +++ b/webapps/src/main/resources/locale/notification/TaskNotification_en.properties @@ -137,3 +137,7 @@ Notification.digest.more.TaskMentionedPlugin=You've been mentioned in $COUNT tas # Digest mail notifications: label of the category this addon owns digest.category.tasks=Tasks +# Digest mail notifications: one line per notification type +digest.line.TaskAssignPlugin=You were assigned to "{0}" ({1}) +digest.line.TaskCoworkerPlugin={0} added you as coworker on "{1}" ({2}) +digest.line.TaskMentionedPlugin={0} mentioned you in "{1}" ({2}) diff --git a/webapps/src/main/webapp/WEB-INF/conf/task-addon/notification-configuration.xml b/webapps/src/main/webapp/WEB-INF/conf/task-addon/notification-configuration.xml index 511608374..ee05a82a9 100644 --- a/webapps/src/main/webapp/WEB-INF/conf/task-addon/notification-configuration.xml +++ b/webapps/src/main/webapp/WEB-INF/conf/task-addon/notification-configuration.xml @@ -408,5 +408,20 @@ + + + task.digest.lines + addLineProvider + io.meeds.task.digest.TaskDigestLinePlugin + Builds the digest email lines of the task notifications + + + pluginIds + TaskAssignPlugin + TaskCoworkerPlugin + TaskMentionedPlugin + + + From 0dfeefa45fb154633d6701ece993a9c56c453008 Mon Sep 17 00:00:00 2001 From: azayati Date: Fri, 4 Sep 2026 09:09:02 +0200 Subject: [PATCH 2/2] fix: Digest lines review round 1 - EXO-89486 - eXIP7.3.0.22 Co-Authored-By: Claude Fable 5.1 --- .../meeds/task/digest/TaskDigestLinePlugin.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java b/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java index 6fce1bb21..726a3dc4d 100644 --- a/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java +++ b/services/src/main/java/io/meeds/task/digest/TaskDigestLinePlugin.java @@ -25,8 +25,8 @@ import org.exoplatform.container.xml.InitParams; import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.manager.IdentityManager; -import org.exoplatform.social.core.service.LinkProvider; import org.exoplatform.task.dto.TaskDto; +import org.exoplatform.task.integration.notification.NotificationUtils; import org.exoplatform.task.exception.EntityNotFoundException; import org.exoplatform.task.service.TaskService; @@ -34,6 +34,7 @@ import io.meeds.commons.digest.model.DigestLine; import io.meeds.commons.digest.plugin.DigestLineContext; import io.meeds.commons.digest.plugin.DigestLinePlugin; +import io.meeds.task.plugin.TaskPermanentLinkPlugin; /** * The digest email lines of the task notifications: assigned, added as @@ -48,12 +49,12 @@ public class TaskDigestLinePlugin extends DigestLinePlugin { public static final String TASK_MENTIONED_PLUGIN = "TaskMentionedPlugin"; - /** The stored parameters, the same names as in the notifications */ - static final String TASK_ID_PARAM = "taskId"; + /** The stored parameters: the very keys the notification plugins write */ + static final String TASK_ID_PARAM = NotificationUtils.TASK_ID; - static final String CREATOR_PARAM = "creator"; + static final String CREATOR_PARAM = NotificationUtils.CREATOR.getKey(); - static final String TASK_URL_PARAM = "taskUrl"; + static final String TASK_URL_PARAM = NotificationUtils.TASK_URL; private static final String LINE_KEY_PREFIX = "digest.line."; @@ -109,8 +110,8 @@ protected String url(DigestItem item, TaskDto task) { if (StringUtils.startsWith(stored, "http")) { return stored; } - return CommonsUtils.getCurrentDomain() + "/" + LinkProvider.getPortalName(null) + "/" + CommonsUtils.getCurrentPortalOwner() - + "/tasks/taskDetail/" + task.getId(); + return CommonsUtils.getCurrentDomain() + + String.format(TaskPermanentLinkPlugin.URL_FORMAT, CommonsUtils.getCurrentPortalOwner(), task.getId()); } private String fullName(String username) {