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