From 12949767bb587f46dbd93fa700c381b8adcd7e10 Mon Sep 17 00:00:00 2001 From: juarez Date: Mon, 29 Jan 2024 19:11:38 +0100 Subject: [PATCH 1/4] Added: Notification User Action --- CHANGELOG.md | 1 + .../db/model/NotificationUserAction.java | 30 +++++++++++++++++++ .../NotificationUserActionRepository.java | 10 +++++++ .../V001__initialize_schema_and_tables.sql | 14 +++++++++ 4 files changed, 55 insertions(+) create mode 100644 src/main/java/de/samply/db/model/NotificationUserAction.java create mode 100644 src/main/java/de/samply/db/repository/NotificationUserActionRepository.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cc57b3d..cc2e0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,3 +78,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Frontend DTO converters - Fetch all user visible notifications - Accepted state requirement for setting user as developer or pilot +- Notification User Action diff --git a/src/main/java/de/samply/db/model/NotificationUserAction.java b/src/main/java/de/samply/db/model/NotificationUserAction.java new file mode 100644 index 0000000..169d325 --- /dev/null +++ b/src/main/java/de/samply/db/model/NotificationUserAction.java @@ -0,0 +1,30 @@ +package de.samply.db.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "notification_user_action", schema = "samply") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NotificationUserAction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "email", nullable = false) + private String email; + + @Column(name = "read", nullable = false) + private boolean read = false; + + @ManyToOne + @JoinColumn(name = "notification_id") + private Notification notification; + +} diff --git a/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java b/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java new file mode 100644 index 0000000..66e0da3 --- /dev/null +++ b/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java @@ -0,0 +1,10 @@ +package de.samply.db.repository; + +import de.samply.db.model.NotificationUserAction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationUserActionRepository extends JpaRepository { + +} diff --git a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql index f58c01e..f9d6a46 100644 --- a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql +++ b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql @@ -104,6 +104,15 @@ CREATE TABLE samply.notification error TEXT ); +CREATE TABLE samply.notification_user_action +( + id SERIAL PRIMARY KEY, + email TEXT NOT NULL, + read BOOLEAN NOT NULL DEFAULT false, + notification_id BIGINT +); + + ALTER TABLE samply.project ADD CONSTRAINT fk_project_query FOREIGN KEY (query_id) @@ -133,9 +142,14 @@ ALTER TABLE samply.notification ADD CONSTRAINT fk_project_id FOREIGN KEY (project_id) REFERENCES samply.project (id); +ALTER TABLE samply.notification_user_action + ADD CONSTRAINT fk_notification_id + FOREIGN KEY (notification_id) REFERENCES samply.notification (id); + CREATE INDEX idx_project_bridgehead_project_id ON samply.project_bridgehead (project_id); CREATE INDEX idx_project_bridgehead_user_project_bridgehead_id ON samply.project_bridgehead_user (project_bridgehead_id); CREATE INDEX idx_project_document_project_id ON samply.project_document (project_id); CREATE INDEX idx_project_query_id ON samply.project (query_id); CREATE INDEX idx_bridgehead_operation_project_id ON samply.bridgehead_operation (project_id); CREATE INDEX idx_notification_project_id ON samply.notification (project_id); +CREATE INDEX idx_notification_id ON samply.notification_user_action (notification_id); From 27661682ba50a09988bbb235935b64114474fee5 Mon Sep 17 00:00:00 2001 From: juarez Date: Mon, 29 Jan 2024 19:33:48 +0100 Subject: [PATCH 2/4] Bugfix: fetch user visible notifications --- .../notification/NotificationService.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/samply/notification/NotificationService.java b/src/main/java/de/samply/notification/NotificationService.java index 6fd813f..2fcc385 100644 --- a/src/main/java/de/samply/notification/NotificationService.java +++ b/src/main/java/de/samply/notification/NotificationService.java @@ -7,6 +7,7 @@ import de.samply.frontend.dto.DtoFactory; import de.samply.project.ProjectService; import de.samply.security.SessionUser; +import de.samply.user.roles.OrganisationRole; import jakarta.validation.constraints.NotNull; import org.springframework.stereotype.Service; @@ -57,12 +58,33 @@ private Project fetchProject(String projectCode) throws NotificationServiceExcep public List fetchUserVisibleNotifications(Optional projectCodeOptional, Optional bridgheadOptional) throws NotificationServiceException { List result = new ArrayList<>(); - List projects = (projectCodeOptional.isEmpty()) ? projectService.fetchAllUserVisibleProjects() : List.of(fetchProject(projectCodeOptional.get())); - List bridgeheads = (bridgheadOptional.isEmpty()) ? sessionUser.getBridgeheads().stream().toList() : List.of(bridgheadOptional.get()); - projects.forEach(project -> bridgeheads.forEach(bridgehead -> result.addAll( - notificationRepository.findAllByProjectAndBridgeheadOrBridgeheadIsNullOrderByTimestampDesc(project, bridgehead)))); + List projects = (projectCodeOptional.isEmpty()) ? + projectService.fetchAllUserVisibleProjects() : List.of(fetchProject(projectCodeOptional.get())); + List bridgeheads = fetchUserVisibleBridgeheads(bridgheadOptional); + projects.forEach(project -> { + if (bridgeheads.isEmpty() && sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN)) { + notificationRepository.findAllByProjectOrderByTimestampDesc(project); + } else { + bridgeheads.forEach(bridgehead -> result.addAll( + notificationRepository.findAllByProjectAndBridgeheadOrBridgeheadIsNullOrderByTimestampDesc(project, bridgehead))); + } + }); return result.stream().map(DtoFactory::convert).toList(); } + List fetchUserVisibleBridgeheads(Optional requestedBridgehead) { + if (sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN)) { + return (requestedBridgehead.isEmpty()) ? new ArrayList<>() : List.of(requestedBridgehead.get()); + } else { + if (requestedBridgehead.isEmpty()) { + return sessionUser.getBridgeheads().stream().toList(); + } else { + return (sessionUser.getBridgeheads().contains(requestedBridgehead.get())) ? + List.of(requestedBridgehead.get()) : new ArrayList<>(); + } + + } + } + } From bd1d36a054c052445750dc6ea6f50ba5adda2873 Mon Sep 17 00:00:00 2001 From: juarez Date: Tue, 30 Jan 2024 10:12:19 +0100 Subject: [PATCH 3/4] Changed: Replace Bridgehead Operation through Notification --- CHANGELOG.md | 1 + .../samply/db/model/BridgeheadOperation.java | 48 ------------------- .../java/de/samply/db/model/Notification.java | 6 +++ .../BridgeheadOperationRepository.java | 10 ---- .../de/samply/exporter/ExporterService.java | 37 +++++--------- .../notification/NotificationService.java | 4 +- .../de/samply/notification/OperationType.java | 5 +- .../V001__initialize_schema_and_tables.sql | 21 +------- 8 files changed, 29 insertions(+), 103 deletions(-) delete mode 100644 src/main/java/de/samply/db/model/BridgeheadOperation.java delete mode 100644 src/main/java/de/samply/db/repository/BridgeheadOperationRepository.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2e0f3..4d87c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,3 +79,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fetch all user visible notifications - Accepted state requirement for setting user as developer or pilot - Notification User Action +- Replace Bridgehead Operation through Notification diff --git a/src/main/java/de/samply/db/model/BridgeheadOperation.java b/src/main/java/de/samply/db/model/BridgeheadOperation.java deleted file mode 100644 index cadcf4a..0000000 --- a/src/main/java/de/samply/db/model/BridgeheadOperation.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.samply.db.model; - -import de.samply.bridgehead.BridgeheadOperationType; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.http.HttpStatus; - -import java.time.Instant; - -@Entity -@Table(name = "bridgehead_operation", schema = "samply") -@Data -@NoArgsConstructor -@AllArgsConstructor -public class BridgeheadOperation { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - private Long id; - - @Column(name = "bridgehead", nullable = false) - private String bridgehead; - - @Column(name = "user_email", nullable = false) - private String userEmail; - - @Column(name = "timestamp", nullable = false) - private Instant timestamp = Instant.now(); - - @Column(name = "http_status", nullable = false) - @Enumerated(EnumType.STRING) - private HttpStatus httpStatus; - - @Column(name = "error") - private String error; - - @Column(name = "type", nullable = false) - @Enumerated(EnumType.STRING) - private BridgeheadOperationType type; - - @ManyToOne - @JoinColumn(name = "project_id", nullable = false) - private Project project; - -} diff --git a/src/main/java/de/samply/db/model/Notification.java b/src/main/java/de/samply/db/model/Notification.java index cda74fd..4e445c0 100644 --- a/src/main/java/de/samply/db/model/Notification.java +++ b/src/main/java/de/samply/db/model/Notification.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; import java.time.Instant; @@ -43,4 +44,9 @@ public class Notification { @Column(name = "error") private String error; + @Column(name = "http_status") + @Enumerated(EnumType.STRING) + private HttpStatus httpStatus; + + } diff --git a/src/main/java/de/samply/db/repository/BridgeheadOperationRepository.java b/src/main/java/de/samply/db/repository/BridgeheadOperationRepository.java deleted file mode 100644 index 9e27623..0000000 --- a/src/main/java/de/samply/db/repository/BridgeheadOperationRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.samply.db.repository; - -import de.samply.db.model.BridgeheadOperation; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface BridgeheadOperationRepository extends JpaRepository { - -} diff --git a/src/main/java/de/samply/exporter/ExporterService.java b/src/main/java/de/samply/exporter/ExporterService.java index 0b7b675..9e5fb9b 100644 --- a/src/main/java/de/samply/exporter/ExporterService.java +++ b/src/main/java/de/samply/exporter/ExporterService.java @@ -5,15 +5,14 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.samply.app.ProjectManagerConst; -import de.samply.bridgehead.BridgeheadOperationType; -import de.samply.db.model.BridgeheadOperation; import de.samply.db.model.Project; import de.samply.db.model.Query; -import de.samply.db.repository.BridgeheadOperationRepository; import de.samply.db.repository.ProjectRepository; import de.samply.exporter.focus.FocusQuery; import de.samply.exporter.focus.FocusService; import de.samply.exporter.focus.FocusServiceException; +import de.samply.notification.NotificationService; +import de.samply.notification.OperationType; import de.samply.project.ProjectType; import de.samply.security.SessionUser; import de.samply.utils.Base64Utils; @@ -35,10 +34,8 @@ import reactor.netty.http.client.HttpClient; import java.time.Duration; -import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -59,7 +56,7 @@ public class ExporterService { private final int webClientBufferSizeInBytes; private final SessionUser sessionUser; private final ProjectRepository projectRepository; - private final BridgeheadOperationRepository bridgeheadOperationRepository; + private final NotificationService notificationService; private final Set exportTemplates; private final Set datashieldTemplates; private final String focusProjectManagerId; @@ -84,7 +81,7 @@ public ExporterService( FocusService focusService, ProjectRepository projectRepository, SessionUser sessionUser, - BridgeheadOperationRepository bridgeheadOperationRepository) { + NotificationService notificationService) { this.focusService = focusService; this.webClientMaxNumberOfRetries = webClientMaxNumberOfRetries; this.webClientTimeInSecondsAfterRetryWithFailure = webClientTimeInSecondsAfterRetryWithFailure; @@ -96,7 +93,7 @@ public ExporterService( this.webClientBufferSizeInBytes = webClientBufferSizeInBytes; this.sessionUser = sessionUser; this.projectRepository = projectRepository; - this.bridgeheadOperationRepository = bridgeheadOperationRepository; + this.notificationService = notificationService; this.exportTemplates = exportTemplates; this.datashieldTemplates = datashieldTemplates; this.focusProjectManagerId = focusProjectManagerId; @@ -154,14 +151,14 @@ private void postRequest(String bridgehead, String projectCode, FocusQuery focus } // We don't use the normal retry functionality of webclient, because focus requires to change the focus query ID after every retry if (numberOfRetries >= webClientMaxNumberOfRetries) { - createBridgeheadOperation((HttpStatus) ex.getStatusCode(), error, bridgehead, projectCode, email, toBeExecuted); + createBridgeheadNotification((HttpStatus) ex.getStatusCode(), error, bridgehead, projectCode, email, toBeExecuted); } else { waitUntilNextRetry(); focusQuery.setId(focusService.generateId()); // Generate new Focus Query ID postRequest(bridgehead, projectCode, focusQuery, toBeExecuted, numberOfRetries + 1); } }) - .subscribe(result -> createBridgeheadOperation(HttpStatus.OK, null, bridgehead, projectCode, email, toBeExecuted)); + .subscribe(result -> createBridgeheadNotification(HttpStatus.OK, null, bridgehead, projectCode, email, toBeExecuted)); } private void waitUntilNextRetry() { @@ -176,22 +173,15 @@ private String fetchAuthorization() { return ProjectManagerConst.API_KEY + ' ' + focusProjectManagerId + ' ' + focusApiKey; } - private void createBridgeheadOperation( + private void createBridgeheadNotification( HttpStatus status, String error, String bridgehead, String projectCode, String email, boolean toBeExecuted) { - BridgeheadOperation bridgeheadOperation = new BridgeheadOperation(); - bridgeheadOperation.setBridgehead(bridgehead); - bridgeheadOperation.setProject(projectRepository.findByCode(projectCode).get()); - bridgeheadOperation.setTimestamp(Instant.now()); - bridgeheadOperation.setType(fetchBridgeheadOperationType(toBeExecuted)); - bridgeheadOperation.setHttpStatus(status); - bridgeheadOperation.setError(error); - bridgeheadOperation.setUserEmail(email); - bridgeheadOperationRepository.save(bridgeheadOperation); + notificationService.createNotification( + projectCode, bridgehead, email, + fetchBridgeheadOperationType(toBeExecuted), null, error, status); } - private BridgeheadOperationType fetchBridgeheadOperationType(boolean toBeExecuted) { - return (toBeExecuted) ? BridgeheadOperationType.SEND_QUERY_TO_BRIDGEHEAD_AND_EXECUTE : - BridgeheadOperationType.SEND_QUERY_TO_BRIDGEHEAD; + private OperationType fetchBridgeheadOperationType(boolean toBeExecuted) { + return (toBeExecuted) ? OperationType.SEND_QUERY_TO_BRIDGEHEAD_AND_EXECUTE : OperationType.SEND_QUERY_TO_BRIDGEHEAD; } private String convertToBase64String(Object jsonObject) { @@ -276,7 +266,6 @@ public Set getExporterTemplates(@NotNull ProjectType projectType) { return switch (projectType) { case EXPORT -> exportTemplates; case DATASHIELD -> datashieldTemplates; - default -> new HashSet<>(); }; } diff --git a/src/main/java/de/samply/notification/NotificationService.java b/src/main/java/de/samply/notification/NotificationService.java index 2fcc385..9f695aa 100644 --- a/src/main/java/de/samply/notification/NotificationService.java +++ b/src/main/java/de/samply/notification/NotificationService.java @@ -9,6 +9,7 @@ import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import jakarta.validation.constraints.NotNull; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -35,7 +36,7 @@ public NotificationService(NotificationRepository notificationRepository, public void createNotification(@NotNull String projectCode, String bridgehead, @NotNull String email, @NotNull OperationType operationType, - @NotNull String details, String error + @NotNull String details, String error, HttpStatus httpStatus ) throws NotificationServiceException { Project project = fetchProject(projectCode); Notification notification = new Notification(); @@ -45,6 +46,7 @@ public void createNotification(@NotNull String projectCode, String bridgehead, @ notification.setOperationType(operationType); notification.setDetails(details); notification.setError(error); + notification.setHttpStatus(httpStatus); notificationRepository.save(notification); } diff --git a/src/main/java/de/samply/notification/OperationType.java b/src/main/java/de/samply/notification/OperationType.java index 55ac213..1cd9d3a 100644 --- a/src/main/java/de/samply/notification/OperationType.java +++ b/src/main/java/de/samply/notification/OperationType.java @@ -1,5 +1,8 @@ package de.samply.notification; public enum OperationType { - GENERATE_TOKEN + SEND_QUERY_TO_BRIDGEHEAD_AND_EXECUTE, + SEND_QUERY_TO_BRIDGEHEAD, + CREATE_DATASHIELD_TOKEN + } diff --git a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql index f9d6a46..43e5f7b 100644 --- a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql +++ b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql @@ -80,18 +80,6 @@ CREATE TABLE samply.project_document label TEXT ); -CREATE TABLE samply.bridgehead_operation -( - id SERIAL PRIMARY KEY, - bridgehead TEXT NOT NULL, - user_email TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL, - http_status TEXT NOT NULL, - error TEXT, - type TEXT NOT NULL, - project_id BIGINT NOT NULL -); - CREATE TABLE samply.notification ( id SERIAL NOT NULL PRIMARY KEY, @@ -101,7 +89,8 @@ CREATE TABLE samply.notification bridgehead TEXT, operation_type TEXT NOT NULL, details TEXT NOT NULL, - error TEXT + error TEXT, + http_status TEXT ); CREATE TABLE samply.notification_user_action @@ -133,11 +122,6 @@ ALTER TABLE samply.project_document FOREIGN KEY (project_id) REFERENCES samply.project (id); -ALTER TABLE samply.bridgehead_operation - ADD CONSTRAINT fk_project_id - FOREIGN KEY (project_id) - REFERENCES samply.project (id); - ALTER TABLE samply.notification ADD CONSTRAINT fk_project_id FOREIGN KEY (project_id) REFERENCES samply.project (id); @@ -150,6 +134,5 @@ CREATE INDEX idx_project_bridgehead_project_id ON samply.project_bridgehead (pro CREATE INDEX idx_project_bridgehead_user_project_bridgehead_id ON samply.project_bridgehead_user (project_bridgehead_id); CREATE INDEX idx_project_document_project_id ON samply.project_document (project_id); CREATE INDEX idx_project_query_id ON samply.project (query_id); -CREATE INDEX idx_bridgehead_operation_project_id ON samply.bridgehead_operation (project_id); CREATE INDEX idx_notification_project_id ON samply.notification (project_id); CREATE INDEX idx_notification_id ON samply.notification_user_action (notification_id); From 68a7c775d3f85e5e088e2af7bdda366d8830f266 Mon Sep 17 00:00:00 2001 From: juarez Date: Tue, 30 Jan 2024 14:54:24 +0100 Subject: [PATCH 4/4] Changed: Replace Bridgehead Operation through Notification --- CHANGELOG.md | 1 + .../de/samply/app/ProjectManagerConst.java | 3 ++ .../samply/app/ProjectManagerController.java | 13 +++++- .../java/de/samply/db/model/Notification.java | 4 +- .../db/model/NotificationUserAction.java | 7 +++- .../NotificationUserActionRepository.java | 5 +++ .../de/samply/frontend/dto/DtoFactory.java | 10 +++-- .../de/samply/frontend/dto/Notification.java | 5 ++- .../notification/NotificationService.java | 40 ++++++++++++++++++- .../V001__initialize_schema_and_tables.sql | 11 ++--- 10 files changed, 83 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d87c2c..c96aa5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,3 +80,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Accepted state requirement for setting user as developer or pilot - Notification User Action - Replace Bridgehead Operation through Notification +- Set notification as read diff --git a/src/main/java/de/samply/app/ProjectManagerConst.java b/src/main/java/de/samply/app/ProjectManagerConst.java index 6661e62..121b7df 100644 --- a/src/main/java/de/samply/app/ProjectManagerConst.java +++ b/src/main/java/de/samply/app/ProjectManagerConst.java @@ -67,6 +67,7 @@ public class ProjectManagerConst { public final static String REJECT_PROJECT_RESULTS_ACTION = "REJECT_PROJECT_RESULTS"; public final static String REQUEST_CHANGES_IN_PROJECT_ACTION = "REQUEST_CHANGES_IN_PROJECT"; public final static String FETCH_NOTIFICATIONS_ACTION = "FETCH_NOTIFICATIONS"; + public final static String SET_NOTIFICATION_AS_READ_ACTION = "SET_NOTIFICATION_AS_READ"; // REST Services @@ -122,9 +123,11 @@ public class ProjectManagerConst { public final static String FETCH_PUBLICATIONS = "/publications"; public final static String FETCH_OTHER_DOCUMENTS = "/other-documents"; public final static String FETCH_NOTIFICATIONS = "/notifications"; + public final static String SET_NOTIFICATION_AS_READ = "/read-notification"; // REST Parameters public final static String PROJECT_CODE = "project-code"; + public final static String NOTIFICATION_ID = "notification-id"; public final static String BRIDGEHEAD = "bridgehead"; public final static String BRIDGEHEADS = "bridgeheads"; public final static String SITE = "site"; diff --git a/src/main/java/de/samply/app/ProjectManagerController.java b/src/main/java/de/samply/app/ProjectManagerController.java index df23eb8..f0bc569 100644 --- a/src/main/java/de/samply/app/ProjectManagerController.java +++ b/src/main/java/de/samply/app/ProjectManagerController.java @@ -228,7 +228,7 @@ public ResponseEntity createQueryAndDesignProject( String projectCode = this.projectEventService.draft(bridgeheads, queryCode, projectType); return convertToResponseEntity(() -> this.frontendService.fetchUrl( ProjectManagerConst.PROJECT_VIEW_SITE, - Map.of(ProjectManagerConst.QUERY_CODE, projectCode) + Map.of(ProjectManagerConst.PROJECT_CODE, projectCode) )); } @@ -813,6 +813,17 @@ public ResponseEntity fetchNotifications( return convertToResponseEntity(() -> this.notificationService.fetchUserVisibleNotifications(Optional.ofNullable(projectCode), Optional.ofNullable(bridgehead))); } + @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.NOTIFICATIONS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_DASHBOARD_SITE, module = ProjectManagerConst.NOTIFICATIONS_MODULE) + @FrontendAction(action = ProjectManagerConst.SET_NOTIFICATION_AS_READ_ACTION) + @PostMapping(value = ProjectManagerConst.SET_NOTIFICATION_AS_READ) + public ResponseEntity setNotificationAsRead( + @RequestParam(name = ProjectManagerConst.NOTIFICATION_ID) Long notificationId + ) { + return convertToResponseEntity(() -> this.notificationService.setNotificationAsRead(notificationId)); + } + private ResponseEntity convertToResponseEntity(RunnableWithException runnable) { try { diff --git a/src/main/java/de/samply/db/model/Notification.java b/src/main/java/de/samply/db/model/Notification.java index 4e445c0..604ea09 100644 --- a/src/main/java/de/samply/db/model/Notification.java +++ b/src/main/java/de/samply/db/model/Notification.java @@ -34,11 +34,11 @@ public class Notification { @Column(name = "bridgehead") private String bridgehead; - @Column(name = "operation_type", nullable = false) + @Column(name = "operation_type") @Enumerated(EnumType.STRING) private OperationType operationType; - @Column(name = "details", nullable = false) + @Column(name = "details") private String details; @Column(name = "error") diff --git a/src/main/java/de/samply/db/model/NotificationUserAction.java b/src/main/java/de/samply/db/model/NotificationUserAction.java index 169d325..e0fde62 100644 --- a/src/main/java/de/samply/db/model/NotificationUserAction.java +++ b/src/main/java/de/samply/db/model/NotificationUserAction.java @@ -5,6 +5,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.time.Instant; + @Entity @Table(name = "notification_user_action", schema = "samply") @Data @@ -24,7 +26,10 @@ public class NotificationUserAction { private boolean read = false; @ManyToOne - @JoinColumn(name = "notification_id") + @JoinColumn(name = "notification_id", nullable = false) private Notification notification; + @Column(name = "modified_at", nullable = false) + private Instant modifiedAt = Instant.now(); + } diff --git a/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java b/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java index 66e0da3..6cd736f 100644 --- a/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java +++ b/src/main/java/de/samply/db/repository/NotificationUserActionRepository.java @@ -1,10 +1,15 @@ package de.samply.db.repository; +import de.samply.db.model.Notification; import de.samply.db.model.NotificationUserAction; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface NotificationUserActionRepository extends JpaRepository { + Optional findByNotification(Notification notification); + } diff --git a/src/main/java/de/samply/frontend/dto/DtoFactory.java b/src/main/java/de/samply/frontend/dto/DtoFactory.java index 44b7aba..fa570c4 100644 --- a/src/main/java/de/samply/frontend/dto/DtoFactory.java +++ b/src/main/java/de/samply/frontend/dto/DtoFactory.java @@ -1,9 +1,9 @@ package de.samply.frontend.dto; -import de.samply.project.state.ProjectBridgeheadState; +import de.samply.db.model.NotificationUserAction; import jakarta.validation.constraints.NotNull; -import java.time.Instant; +import java.util.function.Supplier; public class DtoFactory { @@ -29,7 +29,7 @@ public static Project convert(@NotNull de.samply.db.model.Project project) { ); } - public static Notification convert(@NotNull de.samply.db.model.Notification notification) { + public static Notification convert(@NotNull de.samply.db.model.Notification notification, Supplier userActionSupplier) { return new Notification( notification.getEmail(), notification.getTimestamp(), @@ -37,7 +37,9 @@ public static Notification convert(@NotNull de.samply.db.model.Notification noti notification.getBridgehead(), notification.getOperationType(), notification.getDetails(), - notification.getError() + notification.getError(), + notification.getHttpStatus(), + userActionSupplier.get().isRead() ); } diff --git a/src/main/java/de/samply/frontend/dto/Notification.java b/src/main/java/de/samply/frontend/dto/Notification.java index d56ed4b..a279954 100644 --- a/src/main/java/de/samply/frontend/dto/Notification.java +++ b/src/main/java/de/samply/frontend/dto/Notification.java @@ -1,6 +1,7 @@ package de.samply.frontend.dto; import de.samply.notification.OperationType; +import org.springframework.http.HttpStatus; import java.time.Instant; @@ -11,6 +12,8 @@ public record Notification( String bridgehead, OperationType operationType, String details, - String error + String error, + HttpStatus httpStatus, + Boolean read ) { } diff --git a/src/main/java/de/samply/notification/NotificationService.java b/src/main/java/de/samply/notification/NotificationService.java index 9f695aa..11a4442 100644 --- a/src/main/java/de/samply/notification/NotificationService.java +++ b/src/main/java/de/samply/notification/NotificationService.java @@ -1,8 +1,10 @@ package de.samply.notification; import de.samply.db.model.Notification; +import de.samply.db.model.NotificationUserAction; import de.samply.db.model.Project; import de.samply.db.repository.NotificationRepository; +import de.samply.db.repository.NotificationUserActionRepository; import de.samply.db.repository.ProjectRepository; import de.samply.frontend.dto.DtoFactory; import de.samply.project.ProjectService; @@ -12,6 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -20,15 +23,18 @@ public class NotificationService { private final NotificationRepository notificationRepository; + private final NotificationUserActionRepository notificationUserActionRepository; private final ProjectRepository projectRepository; private final ProjectService projectService; private final SessionUser sessionUser; public NotificationService(NotificationRepository notificationRepository, + NotificationUserActionRepository notificationUserActionRepository, ProjectRepository projectRepository, ProjectService projectService, SessionUser sessionUser) { this.notificationRepository = notificationRepository; + this.notificationUserActionRepository = notificationUserActionRepository; this.projectRepository = projectRepository; this.projectService = projectService; this.sessionUser = sessionUser; @@ -71,10 +77,11 @@ public List fetchUserVisibleNotifications(O notificationRepository.findAllByProjectAndBridgeheadOrBridgeheadIsNullOrderByTimestampDesc(project, bridgehead))); } }); - return result.stream().map(DtoFactory::convert).toList(); + return result.stream().map(notification -> + DtoFactory.convert(notification, () -> fetchNotificationUserAction(notification))).toList(); } - List fetchUserVisibleBridgeheads(Optional requestedBridgehead) { + private List fetchUserVisibleBridgeheads(Optional requestedBridgehead) { if (sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN)) { return (requestedBridgehead.isEmpty()) ? new ArrayList<>() : List.of(requestedBridgehead.get()); } else { @@ -88,5 +95,34 @@ List fetchUserVisibleBridgeheads(Optional requestedBridgehead) { } } + public void setNotificationAsRead(@NotNull Long notificationId) { + NotificationUserAction notificationUserAction = fetchNotificationUserAction(notificationId); + notificationUserAction.setRead(true); + notificationUserAction.setModifiedAt(Instant.now()); + notificationUserActionRepository.save(notificationUserAction); + } + + public NotificationUserAction fetchNotificationUserAction(@NotNull Long notificationId) { + Optional notificationOptional = notificationRepository.findById(notificationId); + if (notificationOptional.isEmpty()) { + throw new NotificationServiceException("Notification " + notificationId + " not found"); + } + return fetchNotificationUserAction(notificationOptional.get()); + } + + public NotificationUserAction fetchNotificationUserAction(@NotNull Notification notification) { + Optional notificationUserActionOptional = notificationUserActionRepository.findByNotification(notification); + NotificationUserAction notificationUserAction; + if (notificationUserActionOptional.isEmpty()) { + notificationUserAction = new NotificationUserAction(); + notificationUserAction.setNotification(notification); + notificationUserAction.setEmail(sessionUser.getEmail()); + notificationUserActionRepository.save(notificationUserAction); + } else { + notificationUserAction = notificationUserActionOptional.get(); + } + return notificationUserAction; + } + } diff --git a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql index 43e5f7b..50ad749 100644 --- a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql +++ b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql @@ -87,8 +87,8 @@ CREATE TABLE samply.notification timestamp TIMESTAMP WITH TIME ZONE NOT NULL, project_id BIGINT NOT NULL, bridgehead TEXT, - operation_type TEXT NOT NULL, - details TEXT NOT NULL, + operation_type TEXT, + details TEXT, error TEXT, http_status TEXT ); @@ -96,9 +96,10 @@ CREATE TABLE samply.notification CREATE TABLE samply.notification_user_action ( id SERIAL PRIMARY KEY, - email TEXT NOT NULL, - read BOOLEAN NOT NULL DEFAULT false, - notification_id BIGINT + email TEXT NOT NULL, + read BOOLEAN NOT NULL DEFAULT false, + notification_id BIGINT NOT NULL, + modified_at TIMESTAMP NOT NULL );