From c998e55b077b63f840571b409a7477427b37e4ea Mon Sep 17 00:00:00 2001 From: Rupert Westenthaler Date: Mon, 29 Sep 2025 10:53:24 +0200 Subject: [PATCH] prehab2rehab #421: This assures that occurred observation information are cleared in the draft and closed state. Darft because occurred observations are created in the preview state and it is expected that all preview data get deleted when the study is back in the draft status. --- .../OccurredObservationRepository.java | 10 ++++ .../service/OccurredObservationService.java | 17 +++++++ .../studymanager/service/StudyService.java | 18 +++++-- .../OccurredObservationRepositoryTest.java | 50 +++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/OccurredObservationRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/OccurredObservationRepository.java index d9eb53d9..db013cc0 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/OccurredObservationRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/OccurredObservationRepository.java @@ -69,6 +69,7 @@ ON CONFLICT (study_id,observation_id,participant_id,start) DO UPDATE SET "end" = """; private static final String DELETE_ALL = "DELETE FROM occurred_observation"; + private static final String CLEANUP_BY_STUDY_ID ="DELETE FROM occurred_observation WHERE study_id = :study_id" ; private final JdbcTemplate template; private final NamedParameterJdbcTemplate namedTemplate; @@ -202,4 +203,13 @@ private static RowMapper getInstantRowMapper(String field, boolean with return (rs, rowNum) -> withTimezone ? RepositoryUtils.readInstantUTC(rs, field) : RepositoryUtils.readInstant(rs, field); } + + /** + * Cleans all OccurredOccurrencies for the parsed StudyId + * @param studyId + */ + public void cleanup(Long studyId) { + final var params = toParams(studyId); + namedTemplate.update(CLEANUP_BY_STUDY_ID, params); + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/OccurredObservationService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/OccurredObservationService.java index 4f1aa21c..0b4f870d 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/OccurredObservationService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/OccurredObservationService.java @@ -9,8 +9,11 @@ package io.redlink.more.studymanager.service; import io.redlink.more.studymanager.model.OccurredObservation; +import io.redlink.more.studymanager.model.Study; +import io.redlink.more.studymanager.model.generator.RandomTokenGenerator; import io.redlink.more.studymanager.repository.OccurredObservationRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.*; @@ -89,4 +92,18 @@ public Stream streamActiveOccurredObservationsForParticipan ACTIVE_DATA_STATES); } + /** + * Aligns OccurredObservations with the study status + * @param study + */ + @Transactional + public void alignParticipantsWithStudyState(Study study) { + //NOTE: for CLOSE this should be a NO-OP, as participants and observations are also deleted and + // OccurredObservations use delete cascade on those FK + if (EnumSet.of(Study.Status.DRAFT, Study.Status.CLOSED).contains(study.getStudyState())) { + repository.cleanup(study.getStudyId()); + } + + } + } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java index d262dc5c..6c0cc16f 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java @@ -59,14 +59,24 @@ public class StudyService { private final StudyStateService studyStateService; private final IntegrationService integrationService; private final ElasticService elasticService; + private final OccurredObservationService occurredObservationService; private final PushNotificationService pushNotificationService; private final StudyGroupRepository studyGroupRepository; - public StudyService(StudyRepository studyRepository, StudyAclRepository aclRepository, UserRepository userRepo, - StudyStateService studyStateService, InterventionService interventionService, ObservationService observationService, - ParticipantService participantService, IntegrationService integrationService, ElasticService elasticService, PushNotificationService pushNotificationService, StudyGroupRepository studyGroupRepository) { + public StudyService(StudyRepository studyRepository, + StudyAclRepository aclRepository, + UserRepository userRepo, + StudyStateService studyStateService, + InterventionService interventionService, + ObservationService observationService, + ParticipantService participantService, + IntegrationService integrationService, + ElasticService elasticService, + OccurredObservationService occurredObservationService, + PushNotificationService pushNotificationService, + StudyGroupRepository studyGroupRepository) { this.studyRepository = studyRepository; this.aclRepository = aclRepository; this.userRepo = userRepo; @@ -76,6 +86,7 @@ public StudyService(StudyRepository studyRepository, StudyAclRepository aclRepos this.participantService = participantService; this.integrationService = integrationService; this.elasticService = elasticService; + this.occurredObservationService = occurredObservationService; this.pushNotificationService = pushNotificationService; this.studyGroupRepository = studyGroupRepository; } @@ -135,6 +146,7 @@ public Optional setStatus(Long studyId, Study.Status newState, User user) pushNotificationService.sendStudyStateUpdate(participant, oldState, s.getStudyState()) ); participantService.alignParticipantsWithStudyState(s); + occurredObservationService.alignParticipantsWithStudyState(s); if (s.getStudyState() == Study.Status.DRAFT) { log.info("Study {} transitioned back to {}, dropping collected observation data", study.getStudyId(), s.getStudyState()); elasticService.deleteIndex(s.getStudyId()); diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/OccurredObservationRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/OccurredObservationRepositoryTest.java index 353eefc4..e52e3c68 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/OccurredObservationRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/OccurredObservationRepositoryTest.java @@ -244,6 +244,56 @@ public void testInsertListUpdateDelete() { //validate that searching for a combination with no entry returns null var lastStartTimeNotFound = occurredObservationRepository.getLatestStartTime(studyId, -1, null, null, null); assertThat(lastStartTimeNotFound).isNull(); + + //Test cleanup by StudyId + + Long study2Id = studyRepository.insert(new Study().setContact(new Contact().setPerson("test2").setEmail("test2"))).getStudyId(); + Integer study2GroupId = studyGroupRepository.insert(new StudyGroup().setStudyId(study2Id)).getStudyGroupId(); + Instant study2StartTime = Instant.now().truncatedTo(ChronoUnit.MINUTES).minus(1, ChronoUnit.DAYS); + Instant study2EndTime = study2StartTime.plus(2, ChronoUnit.HOURS); + + Observation study2Observation = observationRepository.insert(new Observation() + .setStudyId(study2Id) + .setType(type) + .setTitle("Accelerometer Observation") + .setStudyGroupId(study2GroupId) + .setProperties(new ObservationProperties(Map.of("testProperty", "testValue"))) + .setSchedule(new Event() + .setDateStart(startTime) + .setDateEnd(endTime) + .setRRule(new RecurrenceRule().setFreq("DAILY").setCount(7))) + .setHidden(false) + .setNoSchedule(false)); + + Participant study2Participant = participantRepository.insert(new Participant() + .setAlias("participant x") + .setStudyGroupId(study2GroupId) + .setStudyId(study2Id) + .setRegistrationToken("TEST456")); + + var study2OccurredObservation = occurredObservationRepository.upsert(new OccurredObservation( + study2Id, + study2Observation.getObservationId(), + study2Participant.getParticipantId(), + study2StartTime, + study2EndTime)); + + //validate that their exists now an occurred observation for the 2nd study + assertThat(occurredObservationRepository.listOccurredObservations( + study2Id, null, null, null, null).count()).isEqualTo(1); + + //not clean all occurred observations for the first study + occurredObservationRepository.cleanup(studyId); + + //assert that all accurred observations are deleted + assertThat(occurredObservationRepository.listOccurredObservations( + studyId, null, null, null, null).count()).isEqualTo(0); + + //assert that the occurred observations for study2 are still present + assertThat(occurredObservationRepository.listOccurredObservations( + study2Id, null, null, null, null).count()).isEqualTo(1); + + } }