From 434054e829b9d1a3c6a46b06e7497d6de05dedd0 Mon Sep 17 00:00:00 2001 From: Titohma <102875311+Titohma@users.noreply.github.com> Date: Sun, 5 Mar 2023 23:01:06 +0100 Subject: [PATCH] fix(backlog#10): implement notification to call projects for news --- pom-cluecumber.xml | 2 +- pom.xml | 2 +- .../campaign/scheduler/CampaignScheduler.java | 7 +- .../core/news/controller/NewsController.java | 12 ++-- .../core/news/repository/NewsRepository.java | 20 ++---- .../core/news/service/NewsService.java | 5 ++ .../notification/model/NotificationName.java | 2 +- .../core/project/model/ProjectModel.java | 9 ++- .../project/repository/ProjectRepository.java | 3 + .../project/scheduler/ProjectScheduler.java | 64 +++++++++++++++++++ .../core/project/service/ProjectService.java | 7 +- .../slack/SlackNotificationScheduler.java | 41 +++++++++++- .../slack/repository/SlackUserRepository.java | 1 + .../slack/service/SlackUserService.java | 4 ++ src/main/resources/application.properties | 1 + .../db/changelog/db.changelog-0.16.sql | 7 ++ .../db/changelog/db.changelog-master.xml | 1 + src/main/resources/demo/dataset.sql | 2 +- .../slack/fr/PROJECT_CALL_FOR_NEWS.txt | 5 ++ 19 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 src/main/java/fr/lesprojetscagnottes/core/project/scheduler/ProjectScheduler.java create mode 100644 src/main/resources/db/changelog/db.changelog-0.16.sql create mode 100644 src/main/resources/templates/slack/fr/PROJECT_CALL_FOR_NEWS.txt diff --git a/pom-cluecumber.xml b/pom-cluecumber.xml index 1fdc0b0d..5361da1c 100644 --- a/pom-cluecumber.xml +++ b/pom-cluecumber.xml @@ -10,7 +10,7 @@ fr.les-projets-cagnottes core - 0.15.2 + 0.16.0 Les Projets Cagnottes - Core Les Projets Cagnottes - Main API Component diff --git a/pom.xml b/pom.xml index 98161df4..0b4a0d33 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ fr.les-projets-cagnottes core - 0.15.2 + 0.16.0 Les Projets Cagnottes - Core Les Projets Cagnottes - Main API Component diff --git a/src/main/java/fr/lesprojetscagnottes/core/campaign/scheduler/CampaignScheduler.java b/src/main/java/fr/lesprojetscagnottes/core/campaign/scheduler/CampaignScheduler.java index effaa7a8..d7c4b860 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/campaign/scheduler/CampaignScheduler.java +++ b/src/main/java/fr/lesprojetscagnottes/core/campaign/scheduler/CampaignScheduler.java @@ -9,7 +9,6 @@ import fr.lesprojetscagnottes.core.donation.task.DonationProcessingTask; import fr.lesprojetscagnottes.core.notification.model.NotificationName; import fr.lesprojetscagnottes.core.notification.service.NotificationService; -import fr.lesprojetscagnottes.core.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -30,8 +29,6 @@ public class CampaignScheduler { @Value("${fr.lesprojetscagnottes.web.url}") private String webUrl; - private final UserService userService; - private final DonationProcessingTask donationProcessingTask; private final CampaignRepository campaignRepository; @@ -41,12 +38,10 @@ public class CampaignScheduler { private final NotificationService notificationService; @Autowired - public CampaignScheduler(UserService userService, - DonationProcessingTask donationProcessingTask, + public CampaignScheduler(DonationProcessingTask donationProcessingTask, CampaignRepository campaignRepository, DonationRepository donationRepository, NotificationService notificationService) { - this.userService = userService; this.donationProcessingTask = donationProcessingTask; this.campaignRepository = campaignRepository; this.donationRepository = donationRepository; diff --git a/src/main/java/fr/lesprojetscagnottes/core/news/controller/NewsController.java b/src/main/java/fr/lesprojetscagnottes/core/news/controller/NewsController.java index e222ab79..2755f4c5 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/news/controller/NewsController.java +++ b/src/main/java/fr/lesprojetscagnottes/core/news/controller/NewsController.java @@ -1,11 +1,11 @@ package fr.lesprojetscagnottes.core.news.controller; -import com.google.gson.Gson; import fr.lesprojetscagnottes.core.common.exception.BadRequestException; import fr.lesprojetscagnottes.core.common.exception.ForbiddenException; import fr.lesprojetscagnottes.core.common.exception.NotFoundException; import fr.lesprojetscagnottes.core.news.entity.NewsEntity; import fr.lesprojetscagnottes.core.news.model.NewsModel; +import fr.lesprojetscagnottes.core.news.model.NewsType; import fr.lesprojetscagnottes.core.news.repository.NewsRepository; import fr.lesprojetscagnottes.core.organization.entity.OrganizationEntity; import fr.lesprojetscagnottes.core.organization.repository.OrganizationRepository; @@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.util.Date; import java.util.LinkedHashSet; import java.util.Set; @@ -38,9 +39,6 @@ @Tag(name = "News", description = "The News API") public class NewsController { - @Autowired - private Gson gson; - @Autowired private OrganizationRepository organizationRepository; @@ -158,6 +156,12 @@ public NewsModel create(Principal principal, @RequestBody NewsModel news) { throw new ForbiddenException(); } + // Update project if necessary + if(project != null && news.getType().equals(NewsType.ARTICLE)) { + project.setLastStatusUpdate(new Date()); + project = projectRepository.save(project); + } + // Save news NewsEntity newsToSave = new NewsEntity(); newsToSave.setType(news.getType()); diff --git a/src/main/java/fr/lesprojetscagnottes/core/news/repository/NewsRepository.java b/src/main/java/fr/lesprojetscagnottes/core/news/repository/NewsRepository.java index da64228a..b17b95ac 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/news/repository/NewsRepository.java +++ b/src/main/java/fr/lesprojetscagnottes/core/news/repository/NewsRepository.java @@ -4,26 +4,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -public interface NewsRepository extends JpaRepository { +import java.util.Date; - @Query(nativeQuery = true, - value = "select c.* from projects c " + - "inner join projects_organizations on c.id = projects_organizations.project_id " + - "inner join organizations o on projects_organizations.organization_id = o.id " + - "inner join organizations_users on organizations_users.organization_id = o.id " + - "inner join users u on u.id = organizations_users.user_id " + - "where u.id = ?1 and c.status IN (?2) --#pageable\n", - countQuery = "select count(*) from projects c " + - "inner join projects_organizations on c.id = projects_organizations.project_id " + - "inner join organizations o on projects_organizations.organization_id = o.id " + - "inner join organizations_users on organizations_users.organization_id = o.id " + - "inner join users u on u.id = organizations_users.user_id " + - "where u.id = ?1 c.status IN (?2)") - Page findAllByUser(Long userId, Pageable pageable); +public interface NewsRepository extends JpaRepository { Page findAllByOrganizationIdOrOrganizationIdIsNull(Long organizationId, Pageable pageable); Page findAllByProjectId(Long id, Pageable pageable); + + NewsEntity findFirstByProjectIdAndCreatedAtGreaterThanOrderByCreatedAtDesc(Long id, Date createdAt); } \ No newline at end of file diff --git a/src/main/java/fr/lesprojetscagnottes/core/news/service/NewsService.java b/src/main/java/fr/lesprojetscagnottes/core/news/service/NewsService.java index befa5359..874a9741 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/news/service/NewsService.java +++ b/src/main/java/fr/lesprojetscagnottes/core/news/service/NewsService.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import java.security.Principal; +import java.util.Date; @Slf4j @Service @@ -61,4 +62,8 @@ public DataPage listByProjects_Id(Principal principal, Long id, int o return models; } + public NewsEntity findFirstByProjectIdAndCreatedAtGreaterThanOrderByCreatedAtDesc(Long id, Date createdAt) { + return newsRepository.findFirstByProjectIdAndCreatedAtGreaterThanOrderByCreatedAtDesc(id, createdAt); + } + } diff --git a/src/main/java/fr/lesprojetscagnottes/core/notification/model/NotificationName.java b/src/main/java/fr/lesprojetscagnottes/core/notification/model/NotificationName.java index 54cd3a5d..1f64e410 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/notification/model/NotificationName.java +++ b/src/main/java/fr/lesprojetscagnottes/core/notification/model/NotificationName.java @@ -1,5 +1,5 @@ package fr.lesprojetscagnottes.core.notification.model; public enum NotificationName { - CAMPAIGN_STARTED, CAMPAIGN_REMINDER,PROJECT_PUBLISHED + CAMPAIGN_STARTED, CAMPAIGN_REMINDER,PROJECT_PUBLISHED,PROJECT_CALL_FOR_NEWS } diff --git a/src/main/java/fr/lesprojetscagnottes/core/project/model/ProjectModel.java b/src/main/java/fr/lesprojetscagnottes/core/project/model/ProjectModel.java index 7ced38d2..60f19ff2 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/project/model/ProjectModel.java +++ b/src/main/java/fr/lesprojetscagnottes/core/project/model/ProjectModel.java @@ -1,15 +1,16 @@ package fr.lesprojetscagnottes.core.project.model; +import fr.lesprojetscagnottes.core.common.GenericModel; import fr.lesprojetscagnottes.core.common.audit.AuditEntity; import fr.lesprojetscagnottes.core.common.strings.StringsCommon; -import fr.lesprojetscagnottes.core.common.GenericModel; import fr.lesprojetscagnottes.core.project.entity.ProjectEntity; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; -import jakarta.persistence.*; import javax.validation.constraints.NotNull; +import java.util.Date; import java.util.LinkedHashSet; import java.util.Set; @@ -27,6 +28,9 @@ public class ProjectModel extends AuditEntity { @Enumerated(EnumType.STRING) protected ProjectStatus status; + @Column(name = "last_status_update") + protected Date lastStatusUpdate; + @Column(name = "short_description") protected String shortDescription; @@ -63,6 +67,7 @@ public static ProjectModel fromEntity(ProjectEntity entity) { model.setId(entity.getId()); model.setTitle(entity.getTitle()); model.setStatus(entity.getStatus()); + model.setLastStatusUpdate(entity.getLastStatusUpdate()); model.setShortDescription(entity.getShortDescription()); model.setLongDescription(entity.getLongDescription()); model.setPeopleRequired(entity.getPeopleRequired()); diff --git a/src/main/java/fr/lesprojetscagnottes/core/project/repository/ProjectRepository.java b/src/main/java/fr/lesprojetscagnottes/core/project/repository/ProjectRepository.java index 42cd06e5..51037030 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/project/repository/ProjectRepository.java +++ b/src/main/java/fr/lesprojetscagnottes/core/project/repository/ProjectRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Date; import java.util.Set; public interface ProjectRepository extends JpaRepository { @@ -18,6 +19,8 @@ public interface ProjectRepository extends JpaRepository { Set findAllByPeopleGivingTime_Id(Long memberId); + Set findAllByStatusInAndLastStatusUpdateLessThan(Set status, Date lastStatusUpdate); + Page findAllByStatusIn(Set status, Pageable pageable); Page findAllByOrganizationIdAndStatusIn(Long id, Set status, Pageable pageable); diff --git a/src/main/java/fr/lesprojetscagnottes/core/project/scheduler/ProjectScheduler.java b/src/main/java/fr/lesprojetscagnottes/core/project/scheduler/ProjectScheduler.java new file mode 100644 index 00000000..fed6750b --- /dev/null +++ b/src/main/java/fr/lesprojetscagnottes/core/project/scheduler/ProjectScheduler.java @@ -0,0 +1,64 @@ +package fr.lesprojetscagnottes.core.project.scheduler; + +import fr.lesprojetscagnottes.core.common.date.DateUtils; +import fr.lesprojetscagnottes.core.news.entity.NewsEntity; +import fr.lesprojetscagnottes.core.news.service.NewsService; +import fr.lesprojetscagnottes.core.notification.model.NotificationName; +import fr.lesprojetscagnottes.core.notification.service.NotificationService; +import fr.lesprojetscagnottes.core.project.entity.ProjectEntity; +import fr.lesprojetscagnottes.core.project.model.ProjectStatus; +import fr.lesprojetscagnottes.core.project.repository.ProjectRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.*; + +@Component +@Slf4j +public class ProjectScheduler { + + @Value("${fr.lesprojetscagnottes.web.url}") + private String webUrl; + + private final NewsService newsService; + + private final NotificationService notificationService; + + private final ProjectRepository projectRepository; + + @Autowired + public ProjectScheduler(NewsService newsService, + NotificationService notificationService, + ProjectRepository projectRepository) { + this.newsService = newsService; + this.notificationService = notificationService; + this.projectRepository = projectRepository; + } + + @Scheduled(cron = "${fr.lesprojetscagnottes.core.schedule.newsproject}") + public void notifyCallForNews() { + log.info("Calling for news has started"); + Set status = new LinkedHashSet<>(); + status.add(ProjectStatus.IN_PROGRESS); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nowMinus2Months = now.minusMonths(2); + Date nowMinus2MonthsDate = DateUtils.asDate(nowMinus2Months); + Set projects = projectRepository.findAllByStatusInAndLastStatusUpdateLessThan(status, nowMinus2MonthsDate); + projects.forEach(project -> { + NewsEntity news = newsService.findFirstByProjectIdAndCreatedAtGreaterThanOrderByCreatedAtDesc(project.getId(), nowMinus2MonthsDate); + if(news == null) { + Map model = new HashMap<>(); + model.put("_user_email_", project.getLeader().getEmail()); + model.put("_organization_id_", project.getOrganization().getId()); + model.put("user_fullname", project.getLeader().getFullname()); + model.put("project_title", project.getTitle()); + model.put("project_url", webUrl + "/projects/" + project.getId()); + notificationService.create(NotificationName.PROJECT_CALL_FOR_NEWS, model, project.getOrganization().getId()); + } + }); + } +} diff --git a/src/main/java/fr/lesprojetscagnottes/core/project/service/ProjectService.java b/src/main/java/fr/lesprojetscagnottes/core/project/service/ProjectService.java index 4071e1e4..c0a9570e 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/project/service/ProjectService.java +++ b/src/main/java/fr/lesprojetscagnottes/core/project/service/ProjectService.java @@ -20,10 +20,7 @@ import org.springframework.stereotype.Service; import java.security.Principal; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; @Slf4j @Service @@ -288,13 +285,13 @@ public void updateStatus(Principal principal, Long id, ProjectStatus status) { // Update status project.setStatus(status); + project.setLastStatusUpdate(new Date()); projectRepository.save(project); // Prepare & send notifications if(previousStatus.equals(ProjectStatus.DRAFT) && status.equals(ProjectStatus.IN_PROGRESS)) { Map model = new HashMap<>(); - model.put("user_username", userLoggedIn.getUsername()); model.put("user_fullname", userLoggedIn.getFullname()); model.put("project_title", project.getTitle()); model.put("project_url", webUrl + "/projects/" + project.getId()); diff --git a/src/main/java/fr/lesprojetscagnottes/core/providers/slack/SlackNotificationScheduler.java b/src/main/java/fr/lesprojetscagnottes/core/providers/slack/SlackNotificationScheduler.java index 6ff8a496..4256daf7 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/providers/slack/SlackNotificationScheduler.java +++ b/src/main/java/fr/lesprojetscagnottes/core/providers/slack/SlackNotificationScheduler.java @@ -1,11 +1,18 @@ package fr.lesprojetscagnottes.core.providers.slack; +import com.google.gson.Gson; import fr.lesprojetscagnottes.core.notification.entity.NotificationEntity; +import fr.lesprojetscagnottes.core.notification.model.NotificationVariables; import fr.lesprojetscagnottes.core.notification.service.NotificationService; import fr.lesprojetscagnottes.core.providers.slack.entity.SlackNotificationEntity; +import fr.lesprojetscagnottes.core.providers.slack.entity.SlackTeamEntity; +import fr.lesprojetscagnottes.core.providers.slack.entity.SlackUserEntity; import fr.lesprojetscagnottes.core.providers.slack.service.SlackClientService; import fr.lesprojetscagnottes.core.providers.slack.service.SlackNotificationService; import fr.lesprojetscagnottes.core.providers.slack.service.SlackTeamService; +import fr.lesprojetscagnottes.core.providers.slack.service.SlackUserService; +import fr.lesprojetscagnottes.core.user.entity.UserEntity; +import fr.lesprojetscagnottes.core.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -31,6 +38,15 @@ public class SlackNotificationScheduler { @Autowired private SlackTeamService slackTeamService; + @Autowired + private SlackUserService slackUserService; + + @Autowired + private UserService userService; + + @Autowired + private Gson gson; + @Autowired private NotificationService notificationService; @@ -50,8 +66,31 @@ public void processNotifications() { slackNotification.setTeam(slackTeamService.findByOrganizationId(notification.getOrganization().getId())); slackNotification = slackNotificationService.save(slackNotification); } - if(!slackNotification.getSent()) { + if(slackNotification.getTeam() != null && !slackNotification.getSent()) { log.debug("Sending Slack notification {}", slackNotification.getId()); + + NotificationVariables notificationVariables = gson.fromJson(notification.getVariables(), NotificationVariables.class); + if(notificationVariables.get("_user_email_") != null && notificationVariables.get("_organization_id_") != null) { + UserEntity user = userService.findByEmail(notificationVariables.get("_user_email_").toString()); + log.debug("User : {}", user); + if(user != null) { + Long organizationId = Double.valueOf(notificationVariables.get("_organization_id_").toString()).longValue(); + SlackTeamEntity slackTeam = slackTeamService.findByOrganizationId(organizationId); + log.debug("SlackTeam matching organization {} : {}", organizationId, slackTeam); + if(slackTeam != null) { + SlackUserEntity slackUser = slackUserService.findByUserIdAndSlackTeamId(user.getId(), slackTeam.getId()); + log.debug("SlackUser matching user {} & slackteam {} : {}", user.getId(), slackTeam.getId(), slackUser); + if(slackUser != null) { + notificationVariables.put("user_tagname", "<@" + slackUser.getSlackId() + ">"); + } + } + } + } + if(notificationVariables.get("user_tagname") == null) { + notificationVariables.put("user_tagname", "*" + notificationVariables.get("user_fullname") + "*"); + } + notification.setVariables(gson.toJson(notificationVariables)); + slackClientService.sendNotification(notification, slackNotification); slackNotification.setSent(true); slackNotificationService.save(slackNotification); diff --git a/src/main/java/fr/lesprojetscagnottes/core/providers/slack/repository/SlackUserRepository.java b/src/main/java/fr/lesprojetscagnottes/core/providers/slack/repository/SlackUserRepository.java index a4471df0..5848b5ad 100644 --- a/src/main/java/fr/lesprojetscagnottes/core/providers/slack/repository/SlackUserRepository.java +++ b/src/main/java/fr/lesprojetscagnottes/core/providers/slack/repository/SlackUserRepository.java @@ -14,4 +14,5 @@ public interface SlackUserRepository extends JpaRepository + \ No newline at end of file diff --git a/src/main/resources/demo/dataset.sql b/src/main/resources/demo/dataset.sql index fe4322b5..fe43c525 100644 --- a/src/main/resources/demo/dataset.sql +++ b/src/main/resources/demo/dataset.sql @@ -5,9 +5,9 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; delete from public.ms_notifications; delete from public.ms_user; delete from public.ms_team; +delete from public.slack_notifications; delete from public.slack_user; delete from public.slack_team; -delete from public.slack_notifications; delete from public.notifications; delete from public.ideas; delete from public.news; diff --git a/src/main/resources/templates/slack/fr/PROJECT_CALL_FOR_NEWS.txt b/src/main/resources/templates/slack/fr/PROJECT_CALL_FOR_NEWS.txt new file mode 100644 index 00000000..dd6fa218 --- /dev/null +++ b/src/main/resources/templates/slack/fr/PROJECT_CALL_FOR_NEWS.txt @@ -0,0 +1,5 @@ +:face_with_monocle: Cela fait maintenant 2 mois que nous n'entendons plus parler du projet *[(${project_title})]*. +[(${user_tagname})], quelles sont les nouvelles ? + +Rends-toi sur la page de visualisation du projet et ajoute une actualité dans la section du bas : +[(${project_url})] \ No newline at end of file