Skip to content

Commit 5fec92f

Browse files
authored
Merge pull request #55 from redlink-gmbh/umm/393-adherence-checks-for-goals
MORE-Platform#393: Adherence Checks for Goals (Studymanager Backend)
2 parents 2f7ca90 + 09a2adf commit 5fec92f

7 files changed

Lines changed: 180 additions & 11 deletions

File tree

studymanager-goaltemplates/src/main/java/io/redlink/more/studymanager/core/factory/GoalTemplateFactory.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ public final String getDescription() {
127127
.setDescription(GLOBAL_PROPERTY_PREFIX + "instanceState.description")
128128
.setDefaultValue(false);
129129

130+
/**
131+
* If enabled the participant can select adherence checks for the goal. If missing/disabled the adherence checks
132+
* as defined by the template are used.
133+
*/
134+
public static final Value<Boolean> CUSTOM_ADHERENCE_CHECKS_STATE = new BooleanValue("custom-adherence-checks-state")
135+
.setName(GLOBAL_PROPERTY_PREFIX + "customAdherenceChecksState.name")
136+
.setDescription(GLOBAL_PROPERTY_PREFIX + "customAdherenceChecksState.description")
137+
.setDefaultValue(false);
138+
130139
/**
131140
* Allows to configure a self report time.
132141
*/

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import io.redlink.more.studymanager.core.properties.GoalProperties;
44

55
import java.time.Instant;
6+
import java.util.HashSet;
7+
import java.util.Set;
68

79
public class Goal {
810
private Long studyId;
911
private Integer goalId;
1012
private Integer participantId;
1113
private Integer templateId;
14+
private String title;
15+
private Set<Integer> adherenceCheckIds;
1216
private GoalProperties properties;
1317
private Instant created;
1418
private Instant modified;
@@ -49,6 +53,24 @@ public Goal setTemplateId(Integer templateId) {
4953
return this;
5054
}
5155

56+
public String getTitle() {
57+
return title;
58+
}
59+
60+
public Goal setTitle(String title) {
61+
this.title = title;
62+
return this;
63+
}
64+
65+
public Goal setAdherenceCheckIds(Set<Integer> adherenceCheckIds) {
66+
this.adherenceCheckIds = adherenceCheckIds == null ? new HashSet<>() : adherenceCheckIds;
67+
return this;
68+
}
69+
70+
public Set<Integer> getAdherenceCheckIds() {
71+
return adherenceCheckIds;
72+
}
73+
5274
public GoalProperties getProperties() {
5375
return properties;
5476
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ INSERT INTO goal_adherence_checks(study_id, check_id, title, time)
6666
private static final String DELETE_ADHERENCE_CHECKS = "DELETE FROM goal_adherence_checks WHERE study_id = ?";
6767
private static final String UPDATE_ADHERENCE_CHECK = "UPDATE goal_adherence_checks SET title = :title, time = :time WHERE study_id = :study_id AND check_id = :check_id";
6868

69+
private static final String DELETE_ALL = "DELETE FROM study_goal_config";
70+
71+
6972
private final JdbcTemplate template;
7073
private final NamedParameterJdbcTemplate namedTemplate;
7174

@@ -196,6 +199,10 @@ public void deleteCheck(Long studyId, Integer checkId) {
196199
}
197200
}
198201

202+
public void clear() {
203+
template.execute(DELETE_ALL);
204+
}
205+
199206
private MapSqlParameterSource toParams(StudyGoalConfig config) {
200207
return new MapSqlParameterSource()
201208
.addValue("study_id", config.getStudyId())

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

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.transaction.annotation.Transactional;
2222

2323
import java.sql.Types;
24+
import java.util.Collection;
2425
import java.util.List;
2526
import java.util.Optional;
2627

@@ -30,25 +31,45 @@ public class GoalRepository {
3031
private final static Logger LOG = LoggerFactory.getLogger(GoalRepository.class);
3132

3233
private static final String INSERT_NEW_GOAL = """
33-
INSERT INTO goal(study_id,goal_id,participant_id,template_id,properties)
34+
INSERT INTO goal(study_id,goal_id,participant_id,template_id, title, properties)
3435
VALUES (:study_id,
3536
(SELECT COALESCE(MAX(goal_id),0)+1 FROM goal WHERE study_id = :study_id),
36-
:participant_id,:template_id,:properties::jsonb)""";
37+
:participant_id,:template_id,:title, :properties::jsonb)""";
38+
39+
private static final String GET_GOAL_BY_IDS = """
40+
SELECT g.*,
41+
ARRAY_AGG(ggac.check_id) FILTER (WHERE ggac.check_id IS NOT NULL) AS adherence_check_ids
42+
FROM goal g
43+
LEFT JOIN goal_goal_adherence_checks ggac ON g.study_id = ggac.study_id AND g.goal_id = ggac.goal_id
44+
WHERE g.study_id = ? AND g.goal_id = ?
45+
GROUP BY g.study_id, g.goal_id""";
3746

38-
private static final String GET_GOAL_BY_IDS = "SELECT * FROM goal WHERE study_id = ? AND goal_id = ?";
3947
private static final String LIST_GOALS = """
40-
SELECT * FROM goal
41-
WHERE study_id = :study_id
42-
AND (:participant_id IS NULL OR participant_id = :participant_id)
43-
AND (:template_id IS NULL OR template_id = :template_id)""";
48+
SELECT g.*,
49+
ARRAY_AGG(ggac.check_id) FILTER (WHERE ggac.check_id IS NOT NULL) AS adherence_check_ids
50+
FROM goal g
51+
LEFT JOIN goal_goal_adherence_checks ggac ON g.study_id = ggac.study_id AND g.goal_id = ggac.goal_id
52+
WHERE g.study_id = :study_id
53+
AND (:participant_id IS NULL OR g.participant_id = :participant_id)
54+
AND (:template_id IS NULL OR g.template_id = :template_id)
55+
GROUP BY g.study_id, g.goal_id""";
56+
4457
private static final String UPDATE_GOAL = """
4558
UPDATE goal
46-
SET participant_id=:participant_id, template_id=:template_id,
59+
SET participant_id=:participant_id, template_id=:template_id, title=:title,
4760
properties=:properties::jsonb, modified=now()
4861
WHERE study_id=:study_id AND goal_id=:goal_id""";
62+
4963
private static final String DELETE_BY_IDS = "DELETE FROM goal WHERE study_id = ? AND goal_id = ?";
5064
private static final String DELETE_ALL = "DELETE FROM goal";
5165

66+
private static final String DELETE_GOAL_ADHERENCE_CHECKS =
67+
"DELETE FROM goal_goal_adherence_checks WHERE study_id = :study_id AND goal_id = :goal_id";
68+
69+
private static final String SET_GOAL_ADHERENCE_CHECKS =
70+
"INSERT INTO goal_goal_adherence_checks (study_id, goal_id, check_id) " +
71+
"SELECT :study_id, :goal_id, unnest(:adherence_check_ids::int[])";
72+
5273
private final JdbcTemplate template;
5374
private final NamedParameterJdbcTemplate namedTemplate;
5475

@@ -67,6 +88,7 @@ public Goal insert(Goal goal) {
6788
throw new BadRequestException("Unable to insert goal");
6889
}
6990
Integer goalId = keyHolder.getKey().intValue();
91+
setGoalAdherenceChecks(goal.getStudyId(), goalId, goal.getAdherenceCheckIds());
7092
return getById(goal.getStudyId(), goalId);
7193
}
7294

@@ -102,6 +124,7 @@ public Goal update(Goal goal) {
102124
try {
103125
namedTemplate.update(UPDATE_GOAL,
104126
toParams(goal).addValue("goal_id", goal.getGoalId()));
127+
updateGoalAdherenceChecks(goal.getStudyId(), goal.getGoalId(), goal.getAdherenceCheckIds());
105128
return getById(goal.getStudyId(), goal.getGoalId());
106129
} catch (JsonProcessingException e) {
107130
LOG.error("Json error while updating goal", e);
@@ -113,11 +136,42 @@ public void clear() {
113136
template.execute(DELETE_ALL);
114137
}
115138

139+
/**
140+
* Sets the check ids for the parsed check ids for goal referenced by studyId and goalId
141+
* @param studyId
142+
* @param goalId
143+
* @param checkIds the checks Ids. Does nothing if NULL or empty
144+
*/
145+
private void setGoalAdherenceChecks(Long studyId, Integer goalId, Collection<Integer> checkIds) {
146+
if (checkIds != null && !checkIds.isEmpty()) {
147+
final var params = new MapSqlParameterSource()
148+
.addValue("study_id", studyId)
149+
.addValue("goal_id", goalId);
150+
params.addValue("adherence_check_ids", checkIds.toArray(new Integer[0]));
151+
namedTemplate.update(SET_GOAL_ADHERENCE_CHECKS, params);
152+
} //else nothing to do
153+
}
154+
155+
/**
156+
* Deletes existing and sets the parsed check ids for goal referenced by studyId and goalId
157+
* @param studyId
158+
* @param goalId
159+
* @param checkIds the checks Ids. NULL or empty if none
160+
*/
161+
private void updateGoalAdherenceChecks(Long studyId, Integer goalId, Collection<Integer> checkIds) {
162+
final var params = new MapSqlParameterSource()
163+
.addValue("study_id", studyId)
164+
.addValue("goal_id", goalId);
165+
namedTemplate.update(DELETE_GOAL_ADHERENCE_CHECKS, params);
166+
setGoalAdherenceChecks(studyId, goalId, checkIds);
167+
}
168+
116169
private static MapSqlParameterSource toParams(Goal goal) throws JsonProcessingException {
117170
return new MapSqlParameterSource()
118171
.addValue("study_id", goal.getStudyId(), Types.BIGINT)
119172
.addValue("participant_id", goal.getParticipantId(), Types.INTEGER)
120173
.addValue("template_id", goal.getTemplateId(), Types.INTEGER)
174+
.addValue("title", goal.getTitle(), Types.VARCHAR)
121175
.addValue("properties", MapperUtils.writeValueAsString(goal.getProperties()));
122176
}
123177

@@ -127,6 +181,8 @@ private static RowMapper<Goal> getGoalRowMapper() {
127181
.setGoalId(rs.getInt("goal_id"))
128182
.setParticipantId(rs.getInt("participant_id"))
129183
.setTemplateId(rs.getInt("template_id"))
184+
.setTitle(rs.getString("title"))
185+
.setAdherenceCheckIds(RepositoryUtils.readSet(rs, "adherence_check_ids", Integer.class))
130186
.setProperties(MapperUtils.readValue(rs.getString("properties"), GoalProperties.class))
131187
.setCreated(RepositoryUtils.readInstant(rs, "created"))
132188
.setModified(RepositoryUtils.readInstant(rs, "modified"));
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Allows to define for a goal when users are ask about goal adherence
2+
-- for a goal that is based on this template
3+
CREATE TABLE goal_goal_adherence_checks (
4+
study_id BIGINT NOT NULL,
5+
goal_id INT NOT NULL,
6+
check_id INT NOT NULL,
7+
8+
PRIMARY KEY (study_id, goal_id, check_id),
9+
FOREIGN KEY (study_id, goal_id) REFERENCES goal(study_id, goal_id) ON DELETE CASCADE,
10+
FOREIGN KEY (study_id, check_id) REFERENCES goal_adherence_checks(study_id, check_id) ON DELETE RESTRICT
11+
);
12+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE goal
2+
ADD COLUMN title VARCHAR;
3+

studymanager-services/src/test/java/io/redlink/more/studymanager/repository/goals/GoalRepositoryTest.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.redlink.more.studymanager.repository.ParticipantRepository;
88
import io.redlink.more.studymanager.repository.StudyRepository;
99
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.DisplayName;
1011
import org.junit.jupiter.api.Test;
1112
import org.springframework.beans.factory.annotation.Autowired;
1213
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -15,15 +16,17 @@
1516
import org.springframework.test.context.ContextConfiguration;
1617
import org.testcontainers.junit.jupiter.Testcontainers;
1718

19+
import java.time.LocalTime;
1820
import java.util.Map;
21+
import java.util.Set;
1922

2023
import static org.assertj.core.api.Assertions.assertThat;
2124

2225
@SpringBootTest
2326
@Testcontainers
2427
@EnableAutoConfiguration
2528
@ContextConfiguration(classes = {
26-
GoalRepository.class, GoalTemplateRepository.class, StudyRepository.class,
29+
GoalRepository.class, GoalTemplateRepository.class, GoalConfigurationRepository.class, StudyRepository.class,
2730
ParticipantRepository.class, JPAConfiguration.class
2831
})
2932
@ActiveProfiles("test-containers-flyway")
@@ -35,20 +38,32 @@ class GoalRepositoryTest {
3538
@Autowired
3639
private GoalTemplateRepository goalTemplateRepository;
3740

41+
@Autowired
42+
private GoalConfigurationRepository goalConfigurationRepository;
43+
3844
@Autowired
3945
private StudyRepository studyRepository;
4046

4147
@Autowired
4248
private ParticipantRepository participantRepository;
4349

50+
Long studyId = null;
51+
4452
@BeforeEach
4553
void deleteAll() {
4654
goalRepository.clear();
55+
participantRepository.clear();
56+
goalTemplateRepository.clear();
57+
goalConfigurationRepository.clear();
58+
if(studyId != null) {
59+
studyRepository.deleteById(studyId);
60+
studyId = null;
61+
}
4762
}
4863

4964
@Test
5065
public void testInsertListUpdateDeleteAndFlexibleQueries() {
51-
Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
66+
studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
5267
Integer participantId = participantRepository.insert(new Participant().setStudyId(studyId).setRegistrationToken("t")).getParticipantId();
5368
Integer templateId = goalTemplateRepository.insert(new GoalTemplate().setStudyId(studyId).setType("test")).getTemplateId();
5469

@@ -80,7 +95,7 @@ public void testInsertListUpdateDeleteAndFlexibleQueries() {
8095
@Test
8196
public void testFlexibleListQueriesWithMultipleGoalsPerParticipantAndTemplate() {
8297
// === Setup ===
83-
Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
98+
studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
8499

85100
Integer p1 = participantRepository.insert(new Participant().setStudyId(studyId).setRegistrationToken("p1")).getParticipantId();
86101
Integer p2 = participantRepository.insert(new Participant().setStudyId(studyId).setRegistrationToken("p2")).getParticipantId();
@@ -179,4 +194,49 @@ public void testFlexibleListQueriesWithMultipleGoalsPerParticipantAndTemplate()
179194
goalRepository.deleteGoal(studyId, gP1T1b.getGoalId());
180195
assertThat(goalRepository.list(studyId, null, null)).hasSize(4);
181196
}
197+
198+
@Test
199+
@DisplayName("Goal adherence checks are correctly saved, loaded and updated")
200+
void testGoalAdherenceChecksMapping() {
201+
studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
202+
Integer participantId = participantRepository.insert(new Participant().setStudyId(studyId).setRegistrationToken("t")).getParticipantId();
203+
Integer templateId = goalTemplateRepository.insert(new GoalTemplate().setStudyId(studyId).setType("test")).getTemplateId();
204+
205+
// Create some adherence checks
206+
GoalAdherenceCheck check1 = goalConfigurationRepository.upsertCheck(
207+
new GoalAdherenceCheck().setStudyId(studyId).setCheckId(1).setTitle("Morning").setTime(LocalTime.of(8,0)));
208+
GoalAdherenceCheck check2 = goalConfigurationRepository.upsertCheck(
209+
new GoalAdherenceCheck().setStudyId(studyId).setCheckId(3).setTitle("Evening").setTime(LocalTime.of(20,0)));
210+
211+
Goal goal = new Goal()
212+
.setStudyId(studyId)
213+
.setParticipantId(participantId)
214+
.setTitle("My custom goal title")
215+
.setTemplateId(templateId)
216+
.setProperties(new GoalProperties(Map.of("progress", 50)))
217+
.setAdherenceCheckIds(Set.of(check1.getCheckId(), check2.getCheckId()));
218+
219+
Goal inserted = goalRepository.insert(goal);
220+
221+
assertThat(inserted.getAdherenceCheckIds())
222+
.containsExactlyInAnyOrder(check1.getCheckId(), check2.getCheckId());
223+
assertThat(inserted.getTitle()).isEqualTo(goal.getTitle());
224+
225+
// Verify via getById
226+
Goal loaded = goalRepository.getById(studyId, inserted.getGoalId());
227+
assertThat(loaded.getAdherenceCheckIds())
228+
.containsExactlyInAnyOrder(check1.getCheckId(), check2.getCheckId());
229+
230+
// Update - change adherence checks
231+
loaded.setAdherenceCheckIds(Set.of(check2.getCheckId()));
232+
Goal updated = goalRepository.update(loaded);
233+
234+
assertThat(updated.getAdherenceCheckIds())
235+
.containsExactly(check2.getCheckId());
236+
237+
// Clear adherence checks
238+
updated.setAdherenceCheckIds(null);
239+
Goal cleared = goalRepository.update(updated);
240+
assertThat(cleared.getAdherenceCheckIds()).isEmpty();
241+
}
182242
}

0 commit comments

Comments
 (0)