Skip to content

Commit 656238b

Browse files
committed
MORE-Platform#203: Added Randomization model and support for reading randomized events for the CalendarService
1 parent 3a71533 commit 656238b

10 files changed

Lines changed: 943 additions & 93 deletions

File tree

studymanager-services/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
package io.redlink.more.studymanager.model.scheduler;
1010

1111
import com.fasterxml.jackson.annotation.JsonFormat;
12+
import com.fasterxml.jackson.annotation.JsonSetter;
13+
import com.fasterxml.jackson.annotation.Nulls;
14+
1215
import java.time.Instant;
1316

1417
public class Event implements ScheduleEvent {
@@ -20,7 +23,7 @@ public class Event implements ScheduleEvent {
2023
@JsonFormat(shape = JsonFormat.Shape.STRING)
2124
private Instant dateEnd;
2225
private RecurrenceRule recurrenceRule;
23-
private Randomization randomization;
26+
private Randomization randomization = Randomization.none;
2427

2528
@Override
2629
public String getType() {
@@ -58,6 +61,7 @@ public Randomization getRandomization() {
5861
return randomization;
5962
}
6063

64+
@JsonSetter(value = "randomization", nulls = Nulls.SKIP)
6165
public Event setRandomization(Randomization randomization) {
6266
this.randomization = randomization;
6367
return this;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package io.redlink.more.studymanager.model.scheduler;
22

33
public record Randomization(boolean state, int duration) {
4-
4+
public static Randomization none = new Randomization(false, 0);
55
}

studymanager-services/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
package io.redlink.more.studymanager.model.scheduler;
1010

1111
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
12+
import com.fasterxml.jackson.annotation.JsonSetter;
13+
import com.fasterxml.jackson.annotation.Nulls;
1214

1315
@JsonIgnoreProperties(ignoreUnknown = true)
1416
public class RelativeEvent implements ScheduleEvent {
@@ -21,7 +23,7 @@ public class RelativeEvent implements ScheduleEvent {
2123

2224
private RelativeRecurrenceRule rrrule;
2325

24-
private Randomization randomization;
26+
private Randomization randomization = Randomization.none;
2527

2628
public RelativeEvent() {
2729
}
@@ -62,6 +64,7 @@ public Randomization getRandomization() {
6264
return randomization;
6365
}
6466

67+
@JsonSetter(value = "randomization", nulls = Nulls.SKIP)
6568
public RelativeEvent setRandomization(Randomization randomization) {
6669
this.randomization = randomization;
6770
return this;

studymanager-services/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
package io.redlink.more.studymanager.model.scheduler;
1010

1111
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
12+
import com.fasterxml.jackson.annotation.JsonSetter;
1213
import com.fasterxml.jackson.annotation.JsonSubTypes;
1314
import com.fasterxml.jackson.annotation.JsonTypeInfo;
15+
import com.fasterxml.jackson.annotation.Nulls;
1416

1517
@JsonIgnoreProperties(
1618
value = "type", // ignore manually set type, it will be automatically generated by Jackson during serialization
@@ -22,5 +24,10 @@
2224
@JsonSubTypes.Type(value = RelativeEvent.class, name = RelativeEvent.TYPE)
2325
})
2426
public interface ScheduleEvent {
25-
public String getType();
27+
String getType();
28+
29+
Randomization getRandomization();
30+
31+
@JsonSetter(value = "randomization", nulls = Nulls.SKIP)
32+
ScheduleEvent setRandomization(Randomization randomization);
2633
}

studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@
1111
import com.google.common.base.Supplier;
1212
import io.redlink.more.studymanager.exception.BadRequestException;
1313
import io.redlink.more.studymanager.model.Participant;
14-
import java.util.List;
15-
import java.util.Optional;
16-
import java.util.Set;
17-
14+
import io.redlink.more.studymanager.utils.MapperUtils;
15+
import jakarta.annotation.PostConstruct;
1816
import org.springframework.dao.DataIntegrityViolationException;
1917
import org.springframework.dao.EmptyResultDataAccessException;
2018
import org.springframework.jdbc.core.JdbcTemplate;
@@ -26,6 +24,11 @@
2624
import org.springframework.stereotype.Component;
2725
import org.springframework.transaction.annotation.Transactional;
2826

27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Optional;
30+
import java.util.Set;
31+
2932
import static io.redlink.more.studymanager.repository.RepositoryUtils.getValidNullableIntegerValue;
3033
import static io.redlink.more.studymanager.repository.RepositoryUtils.intReader;
3134

@@ -41,53 +44,53 @@ ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
4144
""";
4245
private static final String GET_PARTICIPANT_BY_IDS =
4346
"SELECT " +
44-
" p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
45-
" p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
46-
"FROM participants p " +
47-
" LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +
48-
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
49-
"WHERE p.study_id = ? AND p.participant_id = ? " +
50-
"GROUP BY p.study_id, p.participant_id, r.token";
47+
" p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
48+
" p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
49+
"FROM participants p " +
50+
" LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +
51+
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
52+
"WHERE p.study_id = ? AND p.participant_id = ? " +
53+
"GROUP BY p.study_id, p.participant_id, r.token";
5154
private static final String LIST_PARTICIPANTS_BY_STUDY =
5255
"SELECT " +
53-
" p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
54-
" p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
55-
"FROM participants p " +
56-
" LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +
57-
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
58-
"WHERE p.study_id = ? " +
59-
"GROUP BY p.study_id, p.participant_id, r.token";
56+
" p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
57+
" p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
58+
"FROM participants p " +
59+
" LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +
60+
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
61+
"WHERE p.study_id = ? " +
62+
"GROUP BY p.study_id, p.participant_id, r.token";
6063
private static final String DELETE_PARTICIPANT =
6164
"DELETE FROM participants " +
62-
"WHERE study_id=? AND participant_id=?";
65+
"WHERE study_id=? AND participant_id=?";
6366
private static final String UPDATE_PARTICIPANT =
6467
"UPDATE participants " +
65-
"SET alias = :alias, study_group_id = :study_group_id, modified = now() " +
66-
"WHERE study_id = :study_id AND participant_id = :participant_id";
68+
"SET alias = :alias, study_group_id = :study_group_id, modified = now() " +
69+
"WHERE study_id = :study_id AND participant_id = :participant_id";
6770
private static final String SET_STATUS =
6871
"UPDATE participants p SET status = :status::participant_status, modified = now() " +
69-
"WHERE study_id = :study_id AND participant_id = :participant_id " +
70-
"RETURNING *, " +
71-
" (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token, " +
72-
" (SELECT ARRAY_AGG(observation_group_id) FROM participant_observation_groups pog WHERE pog.study_id = p.study_id AND pog.participant_id = p.participant_id ) as observation_group_ids";
72+
"WHERE study_id = :study_id AND participant_id = :participant_id " +
73+
"RETURNING *, " +
74+
" (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token, " +
75+
" (SELECT ARRAY_AGG(observation_group_id) FROM participant_observation_groups pog WHERE pog.study_id = p.study_id AND pog.participant_id = p.participant_id ) as observation_group_ids";
7376
private static final String SET_STATUS_IF =
7477
"UPDATE participants p SET status= :new_status::participant_status, modified = now() " +
75-
"WHERE study_id = :study_id AND participant_id = :participant_id " +
76-
" AND status = :current_status::participant_status " +
77-
"RETURNING *, (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token";
78+
"WHERE study_id = :study_id AND participant_id = :participant_id " +
79+
" AND status = :current_status::participant_status " +
80+
"RETURNING *, (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token";
7881

7982
private static final String LIST_PARTICIPANTS_FOR_CLOSING =
8083
"SELECT DISTINCT p.*, 't' as token, ARRAY_AGG(pog.observation_group_id) AS observation_group_ids " +
81-
"FROM studies s " +
82-
" JOIN participants p ON s.study_id = p.study_id " +
83-
" LEFT JOIN study_groups sg ON p.study_group_id = sg.study_group_id AND p.study_id = sg.study_id " +
84-
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
85-
"WHERE s.status = 'active' " +
86-
" AND p.status = 'active' " +
87-
" AND p.start IS NOT NULL " +
88-
" AND COALESCE(sg.duration, s.duration) IS NOT NULL " +
89-
" AND (p.start + ((COALESCE(sg.duration, s.duration)->>'value')::int || ' ' || (COALESCE(sg.duration, s.duration)->>'unit'))::interval) < NOW()" +
90-
"GROUP BY p.study_id, p.participant_id";
84+
"FROM studies s " +
85+
" JOIN participants p ON s.study_id = p.study_id " +
86+
" LEFT JOIN study_groups sg ON p.study_group_id = sg.study_group_id AND p.study_id = sg.study_id " +
87+
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
88+
"WHERE s.status = 'active' " +
89+
" AND p.status = 'active' " +
90+
" AND p.start IS NOT NULL " +
91+
" AND COALESCE(sg.duration, s.duration) IS NOT NULL " +
92+
" AND (p.start + ((COALESCE(sg.duration, s.duration)->>'value')::int || ' ' || (COALESCE(sg.duration, s.duration)->>'unit'))::interval) < NOW()" +
93+
"GROUP BY p.study_id, p.participant_id";
9194

9295
/*
9396
* SQL Statements for managing participant_observation_groups mapping for participants
@@ -100,15 +103,24 @@ ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
100103
"INSERT INTO participant_observation_groups (study_id, participant_id, observation_group_id) " +
101104
"SELECT :study_id, :participant_id, unnest(:observation_group_ids::int[]);";
102105

106+
private static final String GET_PARTICIPANT_OBSERVATION_PROPERTIES = "SELECT properties FROM participant_observation_properties WHERE participant_id = :participant_id AND study_id = :study_id AND observation_id = :observation_id";
107+
103108
private static final String DELETE_ALL = "DELETE FROM participants";
104109
private final JdbcTemplate template;
105110
private final NamedParameterJdbcTemplate namedTemplate;
106111

112+
private static ParticipantRepository instance;
113+
107114
public ParticipantRepository(JdbcTemplate template) {
108115
this.template = template;
109116
this.namedTemplate = new NamedParameterJdbcTemplate(template);
110117
}
111118

119+
@PostConstruct
120+
public void setInstance() {
121+
instance = this;
122+
}
123+
112124
@Transactional
113125
public Participant insert(Participant participant) {
114126
final KeyHolder keyHolder = new GeneratedKeyHolder();
@@ -145,6 +157,7 @@ public void deleteParticipant(Long studyId, Integer participantId) {
145157

146158
/**
147159
* Updates the participant and the {@link Participant#getObservationGroupIds()}
160+
*
148161
* @param participant
149162
* @return the updated participant as stored in the database
150163
*/
@@ -164,6 +177,23 @@ public Optional<Participant> setStatusByIds(Long studyId, Integer participantId,
164177
).stream().findFirst();
165178
}
166179

180+
@Transactional
181+
public Optional<Map<String, Object>> getParticipantWithObservationProperties(Long studyId, Integer participantId, Integer observationId) {
182+
try {
183+
return namedTemplate.query(
184+
GET_PARTICIPANT_OBSERVATION_PROPERTIES,
185+
new MapSqlParameterSource()
186+
.addValue("study_id", studyId)
187+
.addValue("participant_id", participantId)
188+
.addValue("observation_id", observationId),
189+
(rs, rowNum) ->
190+
(Map<String, Object>) MapperUtils.readValue(rs.getObject("properties"), Map.class)
191+
).stream().findFirst();
192+
} catch (EmptyResultDataAccessException e) {
193+
return Optional.empty();
194+
}
195+
}
196+
167197
@Transactional
168198
public void cleanupParticipant(Long studyId, Integer participantId) {
169199
final var params = toParams(studyId, participantId);
@@ -238,10 +268,13 @@ private static RowMapper<Participant> getParticipantRowMapper() {
238268
private void setParticipantObservationGroupIds(Long studyId, Integer participantId, Set<Integer> observationGroupIds) {
239269
final var params = toParams(studyId, participantId);
240270
namedTemplate.update(DELETE_PARTICIPANT_OBSERVATION_GROUP_IDS, params);
241-
if(observationGroupIds != null && !observationGroupIds.isEmpty()) {
271+
if (observationGroupIds != null && !observationGroupIds.isEmpty()) {
242272
params.addValue("observation_group_ids", observationGroupIds.toArray(new Integer[0]));
243273
namedTemplate.update(SET_PARTICIPANT_OBSERVATION_GROUP_IDS, params);
244274
}
245275
}
246276

277+
public static ParticipantRepository getInstance() {
278+
return instance;
279+
}
247280
}

studymanager-services/src/main/java/io/redlink/more/studymanager/service/CalendarService.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@
1919
import io.redlink.more.studymanager.model.timeline.ObservationTimelineEvent;
2020
import io.redlink.more.studymanager.model.timeline.StudyTimeline;
2121
import io.redlink.more.studymanager.utils.SchedulerUtils;
22+
import org.apache.commons.lang3.Range;
23+
import org.springframework.stereotype.Service;
24+
2225
import java.time.Instant;
2326
import java.time.LocalDate;
2427
import java.time.LocalTime;
2528
import java.time.ZoneId;
2629
import java.time.temporal.ChronoUnit;
27-
import java.util.*;
30+
import java.util.Collection;
31+
import java.util.Collections;
32+
import java.util.List;
33+
import java.util.Objects;
34+
import java.util.Optional;
2835
import java.util.stream.Collectors;
29-
import org.apache.commons.lang3.Range;
30-
import org.springframework.stereotype.Service;
3136

3237

3338
@Service
@@ -147,7 +152,7 @@ public StudyTimeline getTimeline(Study study, Participant participant, Integer s
147152
observations.stream()
148153
.flatMap(o -> SchedulerUtils
149154
.parseToObservationSchedules(
150-
o.getSchedule(), effectiveRange.getMinimum(), effectiveRange.getMaximum()
155+
o.getSchedule(), effectiveRange.getMinimum(), effectiveRange.getMaximum(), study.getStudyId(), participant.getParticipantId(), o.getObservationId()
151156
)
152157
.stream()
153158
// Disabled client-side filter for now...

0 commit comments

Comments
 (0)