diff --git a/pom.xml b/pom.xml
index ee9d4bc0..59bc5de5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,8 +40,7 @@
17
3.5.5
- 1.21.3
-
+ 2.0.2
more-project
io.redlink.more.studymanager.Application
@@ -404,20 +403,20 @@
- org.springframework.boot
- spring-boot-dependencies
- ${spring-boot.version}
+ org.testcontainers
+ testcontainers-bom
+ ${testcontainers.version}
pom
import
-
- org.testcontainers
- testcontainers-bom
- ${testcontainers.version}
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
pom
import
+
co.elastic.clients
elasticsearch-java
diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java
index 63953304..de45c43d 100644
--- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java
+++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java
@@ -37,12 +37,14 @@ public LimeSurveyObservation(MoreObservationSDK sdk, C properties, LimeSurveyReq
public void activate(){
String surveyId = checkAndGetSurveyId();
+ //FIXME: This creates a LIME survey user for every participant, regardless if the Observation is relevant to the participant
Set participantIds = sdk.participantIds(MorePlatformSDK.ParticipantFilter.ALL);
+ //FIXME: Check for the expected keys (token and limeUrl) and not just if any properties are present!
participantIds.removeIf(id -> sdk.getPropertiesForParticipant(id).isPresent());
limeSurveyRequestService.activateParticipants(participantIds, surveyId)
.forEach(data ->
sdk.setPropertiesForParticipant(
- Integer.parseInt(data.firstname()),
+ Integer.parseInt(data.firstname()), //NOTE: both the firstname and lastname are set to the participantId
new ObservationProperties(
Map.of("token", data.token(),
"limeUrl", limeSurveyRequestService.getBaseUrl())
diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java
index a4d158e4..5c7ca126 100644
--- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java
+++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java
@@ -46,11 +46,13 @@ public class LimeSurveyObservationFactory, P
public LimeSurveyObservationFactory() {}
- public LimeSurveyObservationFactory(
+ //FIXME: Try to get rid of constructors only used by UnitTests to Mock the LimeSurveyRequestService
+ LimeSurveyObservationFactory(
ComponentFactoryProperties properties, LimeSurveyRequestService limeSurveyRequestService) {
this.componentProperties = properties;
this.limeSurveyRequestService = limeSurveyRequestService;
}
+
@Override
public LimeSurveyObservationFactory init(ComponentFactoryProperties componentProperties){
this.componentProperties = componentProperties;
diff --git a/studymanager-services/pom.xml b/studymanager-services/pom.xml
index 8e01ee3f..080db993 100644
--- a/studymanager-services/pom.xml
+++ b/studymanager-services/pom.xml
@@ -139,17 +139,17 @@
org.testcontainers
- junit-jupiter
+ testcontainers-junit-jupiter
test
org.testcontainers
- postgresql
+ testcontainers-postgresql
test
org.testcontainers
- elasticsearch
+ testcontainers-elasticsearch
test
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/exception/NotFoundException.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/exception/NotFoundException.java
index 447805c8..7a93bc00 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/exception/NotFoundException.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/exception/NotFoundException.java
@@ -28,6 +28,9 @@ public static NotFoundException Study(long id) {
public static NotFoundException StudyGroup(long studyId, int studyGroupId) {
return new NotFoundException("StudyGroup", studyId + "/" + studyGroupId);
}
+ public static NotFoundException ObservationGroup(long studyId, int observationGroupId) {
+ return new NotFoundException("ObservationGroup", studyId + "/" + observationGroupId);
+ }
public static NotFoundException Participant(long studyId, int participantId) {
return new NotFoundException("Participant", studyId + "/" + participantId);
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Intervention.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Intervention.java
index f8f26670..fe4a70b0 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Intervention.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Intervention.java
@@ -21,6 +21,7 @@ public class Intervention {
private ScheduleEvent schedule;
private Instant created;
private Instant modified;
+ private Integer observationGroupId;
public Long getStudyId() {
return studyId;
@@ -67,6 +68,15 @@ public Intervention setStudyGroupId(Integer studyGroupId) {
return this;
}
+ public Integer getObservationGroupId() {
+ return observationGroupId;
+ }
+
+ public Intervention setObservationGroupId(Integer observationGroupId) {
+ this.observationGroupId = observationGroupId;
+ return this;
+ }
+
public ScheduleEvent getSchedule() {
return schedule;
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Observation.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Observation.java
index fdcbc68a..879a37fe 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Observation.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Observation.java
@@ -27,6 +27,7 @@ public class Observation {
private Instant modified;
private Boolean hidden;
private Boolean noSchedule = false;
+ private Integer observationGroupId;
public Long getStudyId() {
return studyId;
@@ -91,6 +92,15 @@ public Observation setStudyGroupId(Integer studyGroupId) {
return this;
}
+ public Integer getObservationGroupId() {
+ return observationGroupId;
+ }
+
+ public Observation setObservationGroupId(Integer observationGroupId) {
+ this.observationGroupId = observationGroupId;
+ return this;
+ }
+
public ObservationProperties getProperties() {
return properties;
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/ObservationGroup.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/ObservationGroup.java
new file mode 100644
index 00000000..143375db
--- /dev/null
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/ObservationGroup.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.model;
+
+import io.redlink.more.studymanager.model.scheduler.Duration;
+
+import java.time.Instant;
+
+public class ObservationGroup {
+ private Long studyId;
+ private Integer observationGroupId;
+ private String title;
+ private String purpose;
+ private Instant created;
+ private Instant modified;
+
+ public Long getStudyId() {
+ return studyId;
+ }
+
+ public ObservationGroup setStudyId(Long studyId) {
+ this.studyId = studyId;
+ return this;
+ }
+
+ public Integer getObservationGroupId() {
+ return observationGroupId;
+ }
+
+ public ObservationGroup setObservationGroupId(Integer observationGroupId) {
+ this.observationGroupId = observationGroupId;
+ return this;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public ObservationGroup setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public String getPurpose() {
+ return purpose;
+ }
+
+ public ObservationGroup setPurpose(String purpose) {
+ this.purpose = purpose;
+ return this;
+ }
+
+ public Instant getCreated() {
+ return created;
+ }
+
+ public ObservationGroup setCreated(Instant created) {
+ this.created = created;
+ return this;
+ }
+
+ public Instant getModified() {
+ return modified;
+ }
+
+ public ObservationGroup setModified(Instant modified) {
+ this.modified = modified;
+ return this;
+ }
+}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Participant.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Participant.java
index cc4f9d47..79928381 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Participant.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/Participant.java
@@ -8,6 +8,9 @@
*/
package io.redlink.more.studymanager.model;
import java.time.Instant;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
public class Participant {
private Long studyId;
@@ -19,6 +22,7 @@ public class Participant {
private Instant modified;
private Instant start;
private String registrationToken;
+ private Set observationGroupIds = new HashSet<>();
public Long getStudyId() {
return this.studyId;
@@ -92,6 +96,27 @@ public Participant setStudyGroupId(Integer studyGroupId) {
return this;
}
+ public Participant setObservationGroupIds(Set observationGroupIds) {
+ this.observationGroupIds = observationGroupIds == null ? new HashSet<>() : observationGroupIds;
+ return this;
+ }
+
+ public Set getObservationGroupIds() {
+ return observationGroupIds;
+ }
+
+ public Participant addObservationGroupId(Integer observationGroupId) {
+ if(observationGroupIds != null) {
+ this.observationGroupIds.add(observationGroupId);
+ }
+ return this;
+ }
+
+ public Participant removeObservationGroupId(Integer observationGroupId) {
+ this.observationGroupIds.remove(observationGroupId);
+ return this;
+ }
+
public Instant getCreated() {
return created;
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/StudyImportExport.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/StudyImportExport.java
index 656f0ac4..bec20901 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/model/StudyImportExport.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/model/StudyImportExport.java
@@ -10,11 +10,13 @@
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class StudyImportExport {
private Study study;
private List studyGroups;
+ private List observationGroups;
private List observations;
private List interventions;
private List participants;
@@ -40,6 +42,15 @@ public StudyImportExport setStudyGroups(List studyGroups) {
return this;
}
+ public List getObservationGroups() {
+ return observationGroups;
+ }
+
+ public StudyImportExport setObservationGroups(List observationGroups) {
+ this.observationGroups = observationGroups;
+ return this;
+ }
+
public List getObservations() {
return observations;
}
@@ -95,6 +106,7 @@ public StudyImportExport setIntegrations(List integrations) {
}
public record ParticipantInfo(
- Integer groupId
+ Integer groupId,
+ Set observationGroupIds
) {}
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java
index 03788160..ec6001f1 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java
@@ -24,6 +24,8 @@
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import static io.redlink.more.studymanager.repository.RepositoryUtils.getValidNullableIntegerValue;
@@ -31,14 +33,14 @@
@Component
public class InterventionRepository {
- private static final String INSERT_INTERVENTION = "INSERT INTO interventions(study_id,intervention_id,title,purpose,study_group_id,schedule) VALUES (:study_id,(SELECT COALESCE(MAX(intervention_id),0)+1 FROM interventions WHERE study_id = :study_id),:title,:purpose,:study_group_id,:schedule::jsonb) RETURNING *";
- private static final String IMPORT_INTERVENTION = "INSERT INTO interventions(study_id,intervention_id,title,purpose,study_group_id,schedule) VALUES (:study_id,:intervention_id,:title,:purpose,:study_group_id,:schedule::jsonb) RETURNING *";
+ private static final String INSERT_INTERVENTION = "INSERT INTO interventions(study_id,intervention_id,title,purpose,study_group_id,schedule,observation_group_id) VALUES (:study_id,(SELECT COALESCE(MAX(intervention_id),0)+1 FROM interventions WHERE study_id = :study_id),:title,:purpose,:study_group_id,:schedule::jsonb,:observation_group_id) RETURNING *";
+ private static final String IMPORT_INTERVENTION = "INSERT INTO interventions(study_id,intervention_id,title,purpose,study_group_id,schedule,observation_group_id) VALUES (:study_id,:intervention_id,:title,:purpose,:study_group_id,:schedule::jsonb,:observation_group_id) RETURNING *";
private static final String GET_INTERVENTION_BY_IDS = "SELECT * FROM interventions WHERE study_id = ? AND intervention_id = ?";
private static final String LIST_INTERVENTIONS = "SELECT * FROM interventions WHERE study_id = ?";
- private static final String LIST_INTERVENTIONS_FOR_GROUP = "SELECT * FROM interventions WHERE study_id = :study_id AND (study_group_id IS NULL OR study_group_id = :study_group_id)";
+ private static final String LIST_INTERVENTIONS_FOR_GROUP = "SELECT * FROM interventions WHERE study_id = :study_id AND (study_group_id IS NULL OR study_group_id = :study_group_id) AND (observation_group_id IS NULL OR observation_group_id = ANY(:observation_group_ids::INT[]))";
private static final String DELETE_INTERVENTION_BY_IDS = "DELETE FROM interventions WHERE study_id = ? AND intervention_id = ?";
private static final String DELETE_ALL = "DELETE FROM interventions";
- private static final String UPDATE_INTERVENTION = "UPDATE interventions SET title=:title, study_group_id=:study_group_id, purpose=:purpose, schedule=:schedule::jsonb WHERE study_id=:study_id AND intervention_id=:intervention_id";
+ private static final String UPDATE_INTERVENTION = "UPDATE interventions SET title=:title, study_group_id=:study_group_id, purpose=:purpose, schedule=:schedule::jsonb, observation_group_id=:observation_group_id WHERE study_id=:study_id AND intervention_id=:intervention_id";
private static final String CREATE_ACTION = "INSERT INTO actions(study_id,intervention_id,action_id,type,properties) VALUES (:study_id,:intervention_id,(SELECT COALESCE(MAX(action_id),0)+1 FROM actions WHERE study_id = :study_id AND intervention_id=:intervention_id),:type,:properties::jsonb) RETURNING *";
private static final String IMPORT_ACTION = "INSERT INTO actions(study_id,intervention_id,action_id,type,properties) VALUES (:study_id,:intervention_id,:action_id,:type,:properties::jsonb) RETURNING *";
private static final String GET_ACTION_BY_IDS = "SELECT * FROM actions WHERE study_id=? AND intervention_id=? AND action_id=?";
@@ -60,7 +62,7 @@ public Intervention insert(Intervention intervention) {
try {
return namedTemplate.queryForObject(INSERT_INTERVENTION, interventionToParams(intervention), getInterventionRowMapper());
} catch (DataIntegrityViolationException e) {
- throw new BadRequestException("Study group " + intervention.getStudyGroupId() + " does not exist on study " + intervention.getStudyId());
+ throw new BadRequestException("Unable to insert Invervention because it refers an not existing group (Study group " + intervention.getStudyGroupId() + " and/or Observation group " + intervention.getObservationGroupId() + " does not exist on study " + intervention.getStudyId());
}
}
@@ -76,7 +78,8 @@ public Intervention importIntervention(Long studyId, Intervention intervention)
"Error during import of intervention " +
intervention.getInterventionId() +
"for study " +
- intervention.getStudyId()
+ intervention.getStudyId() +
+ "(" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"
);
}
}
@@ -85,10 +88,14 @@ public List listInterventions(Long studyId) {
return template.query(LIST_INTERVENTIONS, getInterventionRowMapper(), studyId);
}
- public List listInterventionsForGroup(Long studyId, Integer groupId) {
+ public List listInterventionsForGroup(Long studyId, Integer groupId){
+ return listInterventionsForGroup(studyId, groupId, Collections.emptyList());
+ }
+ public List listInterventionsForGroup(Long studyId, Integer groupId, Collection observationGroupIds) {
return namedTemplate.query(LIST_INTERVENTIONS_FOR_GROUP,
new MapSqlParameterSource("study_id", studyId)
- .addValue("study_group_id", groupId),
+ .addValue("study_group_id", groupId)
+ .addValue("observation_group_ids", observationGroupIds == null ? new Integer[0] : observationGroupIds.toArray(new Integer[0])),
getInterventionRowMapper()
);
}
@@ -190,7 +197,8 @@ private static MapSqlParameterSource interventionToParams(Intervention intervent
.addValue("title", intervention.getTitle())
.addValue("purpose", intervention.getPurpose())
.addValue("study_group_id", intervention.getStudyGroupId())
- .addValue("schedule", MapperUtils.writeValueAsString(intervention.getSchedule()));
+ .addValue("schedule", MapperUtils.writeValueAsString(intervention.getSchedule()))
+ .addValue("observation_group_id", intervention.getObservationGroupId());
}
private static MapSqlParameterSource triggerToParams(Long studyId, Integer interventionId, Trigger trigger) {
@@ -235,6 +243,7 @@ private static RowMapper getInterventionRowMapper() {
.setSchedule(MapperUtils.readValue(rs.getString("schedule"), Event.class))
.setStudyGroupId(getValidNullableIntegerValue(rs, "study_group_id"))
.setCreated(RepositoryUtils.readInstant(rs,"created"))
- .setModified(RepositoryUtils.readInstant(rs,"modified"));
+ .setModified(RepositoryUtils.readInstant(rs,"modified"))
+ .setObservationGroupId(getValidNullableIntegerValue(rs, "observation_group_id"));
}
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationGroupRepository.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationGroupRepository.java
new file mode 100644
index 00000000..5e2ebfdc
--- /dev/null
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationGroupRepository.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.repository;
+
+import io.redlink.more.studymanager.exception.BadRequestException;
+import io.redlink.more.studymanager.model.ObservationGroup;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class ObservationGroupRepository {
+ private static final String INSERT_OBSERVATION_GROUP = "INSERT INTO observation_groups (study_id,observation_group_id,title,purpose) VALUES (:study_id,(SELECT COALESCE(MAX(observation_group_id),0)+1 FROM observation_groups WHERE study_id = :study_id),:title,:purpose) RETURNING *";
+ private static final String IMPORT_OBSERVATION_GROUP = "INSERT INTO observation_groups (study_id, observation_group_id, title, purpose) VALUES (:study_id,:observation_group_id,:title,:purpose) RETURNING *";
+ private static final String GET_OBSERVATION_GROUP_BY_IDS = "SELECT * FROM observation_groups WHERE study_id = ? AND observation_group_id = ?";
+ private static final String LIST_OBSERVATION_GROUPS_ORDER_BY_OBSERVATION_GROUP_ID = "SELECT * FROM observation_groups WHERE study_id = ? ORDER BY observation_group_id";
+ private static final String UPDATE_OBSERVATION_GROUP =
+ "UPDATE observation_groups SET title = :title, purpose = :purpose, modified = now() WHERE study_id = :study_id AND observation_group_id = :observation_group_id";
+
+ private static final String DELETE_OBSERVATION_GROUP_BY_ID = "DELETE FROM observation_groups WHERE study_id = ? AND observation_group_id = ?";
+ private static final String CLEAR_OBSERVATION_GROUPS = "DELETE FROM observation_groups";
+
+ private final JdbcTemplate template;
+ private final NamedParameterJdbcTemplate namedTemplate;
+
+ public ObservationGroupRepository(JdbcTemplate template) {
+ this.template = template;
+ this.namedTemplate = new NamedParameterJdbcTemplate(template);
+ }
+
+ public ObservationGroup insert(ObservationGroup observationGroup) {
+ try {
+ return namedTemplate.queryForObject(INSERT_OBSERVATION_GROUP, toParams(observationGroup), getObservationGroupRowMapper());
+ } catch (DataIntegrityViolationException e) {
+ throw new BadRequestException("Study " + observationGroup.getStudyId() + " does not exist");
+ }
+ }
+
+ public ObservationGroup doImport(Long studyId, ObservationGroup observationGroup) {
+ try {
+ return namedTemplate.queryForObject(
+ IMPORT_OBSERVATION_GROUP,
+ toParams(observationGroup)
+ .addValue("study_id", studyId)
+ .addValue("observation_group_id", observationGroup.getObservationGroupId()),
+ getObservationGroupRowMapper()
+ );
+ } catch (DataIntegrityViolationException e) {
+ throw new BadRequestException(
+ "Error during import of observationGroup " +
+ observationGroup.getObservationGroupId() +
+ "for study " +
+ observationGroup.getStudyId()
+ );
+ }
+ }
+
+ public ObservationGroup getByIds(long studyId, int observationGroupId) {
+ try {
+ return template.queryForObject(GET_OBSERVATION_GROUP_BY_IDS, getObservationGroupRowMapper(), studyId, observationGroupId);
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ public List listObservationGroupsOrderedByObservationGroupIdAsc(long studyId) {
+ return template.query(LIST_OBSERVATION_GROUPS_ORDER_BY_OBSERVATION_GROUP_ID, getObservationGroupRowMapper(), studyId);
+ }
+
+ public ObservationGroup update(ObservationGroup studyGroup) {
+ namedTemplate.update(UPDATE_OBSERVATION_GROUP,
+ toParams(studyGroup).addValue("observation_group_id", studyGroup.getObservationGroupId())
+ );
+ return getByIds(studyGroup.getStudyId(), studyGroup.getObservationGroupId());
+ }
+
+ public void deleteById(long studyId, int observationGroupId) {
+ template.update(DELETE_OBSERVATION_GROUP_BY_ID, studyId, observationGroupId);
+ }
+
+ private static MapSqlParameterSource toParams(ObservationGroup observationGroup) {
+ return new MapSqlParameterSource()
+ .addValue("study_id", observationGroup.getStudyId())
+ .addValue("title", observationGroup.getTitle())
+ .addValue("purpose", observationGroup.getPurpose());
+ }
+
+ private static RowMapper getObservationGroupRowMapper() {
+ return (rs, rowNum) -> new ObservationGroup()
+ .setStudyId(rs.getLong("study_id"))
+ .setObservationGroupId(rs.getInt("observation_group_id"))
+ .setTitle(rs.getString("title"))
+ .setPurpose(rs.getString("purpose"))
+ .setCreated(RepositoryUtils.readInstant(rs, "created"))
+ .setModified(RepositoryUtils.readInstant(rs, "modified"));
+ }
+
+ // for testing purpose only
+ protected void clear() {
+ template.execute(CLEAR_OBSERVATION_GROUPS);
+ }
+}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java
index 7b641f11..65b976b6 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java
@@ -14,9 +14,13 @@
import io.redlink.more.studymanager.model.Observation;
import io.redlink.more.studymanager.model.scheduler.ScheduleEvent;
import io.redlink.more.studymanager.utils.MapperUtils;
+
+import java.util.Collection;
import java.util.List;
import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -30,13 +34,15 @@
@Component
public class ObservationRepository {
- private static final String INSERT_NEW_OBSERVATION = "INSERT INTO observations(study_id,observation_id,title,purpose,participant_info,type,study_group_id,properties,schedule,hidden,no_schedule) VALUES (:study_id,(SELECT COALESCE(MAX(observation_id),0)+1 FROM observations WHERE study_id = :study_id),:title,:purpose,:participant_info,:type,:study_group_id,:properties::jsonb,:schedule::jsonb,:hidden,:no_schedule) RETURNING *";
- private static final String IMPORT_OBSERVATION = "INSERT INTO observations(study_id,observation_id,title,purpose,participant_info,type,study_group_id,properties,schedule,hidden,no_schedule) VALUES (:study_id,:observation_id,:title,:purpose,:participant_info,:type,:study_group_id,:properties::jsonb,:schedule::jsonb,:hidden,:no_schedule) RETURNING *";
+ private final static Logger LOG = LoggerFactory.getLogger(ObservationRepository.class);
+
+ private static final String INSERT_NEW_OBSERVATION = "INSERT INTO observations(study_id,observation_id,title,purpose,participant_info,type,study_group_id,properties,schedule,hidden,no_schedule,observation_group_id) VALUES (:study_id,(SELECT COALESCE(MAX(observation_id),0)+1 FROM observations WHERE study_id = :study_id),:title,:purpose,:participant_info,:type,:study_group_id,:properties::jsonb,:schedule::jsonb,:hidden,:no_schedule,:observation_group_id) RETURNING *";
+ private static final String IMPORT_OBSERVATION = "INSERT INTO observations(study_id,observation_id,title,purpose,participant_info,type,study_group_id,properties,schedule,hidden,no_schedule,observation_group_id) VALUES (:study_id,:observation_id,:title,:purpose,:participant_info,:type,:study_group_id,:properties::jsonb,:schedule::jsonb,:hidden,:no_schedule,:observation_group_id) RETURNING *";
private static final String GET_OBSERVATION_BY_IDS = "SELECT * FROM observations WHERE study_id = ? AND observation_id = ?";
private static final String DELETE_BY_IDS = "DELETE FROM observations WHERE study_id = ? AND observation_id = ?";
private static final String LIST_OBSERVATIONS = "SELECT * FROM observations WHERE study_id = :study_id";
- private static final String LIST_OBSERVATIONS_FOR_GROUP = "SELECT * FROM observations WHERE study_id = :study_id AND (study_group_id IS NULL OR study_group_id = :study_group_id)";
- private static final String UPDATE_OBSERVATION = "UPDATE observations SET title=:title, purpose=:purpose, participant_info=:participant_info, study_group_id=:study_group_id, properties=:properties::jsonb, schedule=:schedule::jsonb, modified=now(), hidden=:hidden, no_schedule=:no_schedule WHERE study_id=:study_id AND observation_id=:observation_id";
+ private static final String LIST_OBSERVATIONS_FOR_GROUP = "SELECT * FROM observations WHERE study_id = :study_id AND (study_group_id IS NULL OR study_group_id = :study_group_id) AND (observation_group_id IS NULL OR observation_group_id = ANY(:observation_group_ids::INT[]))";
+ private static final String UPDATE_OBSERVATION = "UPDATE observations SET title=:title, purpose=:purpose, participant_info=:participant_info, study_group_id=:study_group_id, properties=:properties::jsonb, schedule=:schedule::jsonb, observation_group_id=:observation_group_id, modified=now(), hidden=:hidden, no_schedule=:no_schedule WHERE study_id=:study_id AND observation_id=:observation_id";
private static final String DELETE_ALL = "DELETE FROM observations";
private static final String SET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "INSERT INTO participant_observation_properties(study_id,participant_id,observation_id,properties) VALUES (:study_id,:participant_id,:observation_id,:properties::jsonb) ON CONFLICT (study_id, participant_id, observation_id) DO UPDATE SET properties = EXCLUDED.properties";
private static final String GET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "SELECT properties FROM participant_observation_properties WHERE study_id = ? AND participant_id = ? AND observation_id = ?";
@@ -53,8 +59,25 @@ public ObservationRepository(JdbcTemplate template) {
public Observation insert(Observation observation) {
try {
return namedTemplate.queryForObject(INSERT_NEW_OBSERVATION, toParams(observation), getObservationRowMapper());
- } catch (DataIntegrityViolationException | JsonProcessingException e) {
- throw new BadRequestException("Study group " + observation.getStudyGroupId() + " does not exist on study " + observation.getStudyId());
+ } catch (DataIntegrityViolationException e) {
+ String message;
+ if(observation.getStudyGroupId() != null && observation.getObservationGroupId() != null) {
+ message = String.format("Study group %s and/or observation group %s do not exist on study %s",
+ observation.getStudyGroupId(), observation.getObservationGroupId(), observation.getStudyId());
+ } else if(observation.getStudyGroupId() != null) {
+ message = String.format("Study group %s does not exist on study %s",
+ observation.getStudyGroupId(), observation.getStudyId());
+ } else if (observation.getObservationGroupId() != null) {
+ message = String.format("Observation group %s does not exist on study %s",
+ observation.getObservationGroupId(), observation.getStudyId());
+ } else {
+ message = String.format("Encountered %s while inserting observation", e.getClass().getSimpleName());
+ LOG.warn("Unable to insert {}", observation, e);
+ }
+ throw new BadRequestException(message);
+ } catch (JsonProcessingException e){
+ LOG.warn("Unable to insert {}", observation, e);
+ throw new BadRequestException("Unable to insert observation (" + e.getClass().getSimpleName() + ": " + e.getMessage() +")");
}
}
@@ -97,11 +120,29 @@ public List listObservations(Long studyId) {
);
}
+ /**
+ * Lists all Observation based for the parsed study, study group and as per default no assigned observation group
+ * @param studyId the study
+ * @param studyGroupId the study group or NULL of none
+ * @return the Observations
+ */
public List listObservationsForGroup(Long studyId, Integer studyGroupId) {
+ return listObservationsForGroup(studyId, studyGroupId, List.of());
+ }
+
+ /**
+ * Lists all Observation based for the parsed study, study group and observation groups
+ * @param studyId the study
+ * @param studyGroupId the study group or NULL of none
+ * @param observationGroupIds the observation groups or an empty collection if none
+ * @return the Observations
+ */
+ public List listObservationsForGroup(Long studyId, Integer studyGroupId, Collection observationGroupIds) {
return namedTemplate.query(
LIST_OBSERVATIONS_FOR_GROUP,
new MapSqlParameterSource("study_id", studyId)
- .addValue("study_group_id", studyGroupId),
+ .addValue("study_group_id", studyGroupId)
+ .addValue("observation_group_ids", observationGroupIds == null ? new Integer[0] : observationGroupIds.toArray(new Integer[0])),
getObservationRowMapper()
);
}
@@ -158,7 +199,8 @@ private static MapSqlParameterSource toParams(Observation observation) throws Js
.addValue("properties", MapperUtils.writeValueAsString(observation.getProperties()))
.addValue("schedule", MapperUtils.writeValueAsString(observation.getSchedule()))
.addValue("hidden", observation.getHidden())
- .addValue("no_schedule", observation.getNoSchedule());
+ .addValue("no_schedule", observation.getNoSchedule())
+ .addValue("observation_group_id", observation.getObservationGroupId());
}
private static RowMapper getParticipantObservationPropertiesRowMapper() {
@@ -179,6 +221,7 @@ private static RowMapper getObservationRowMapper() {
.setCreated(RepositoryUtils.readInstant(rs, "created"))
.setModified(RepositoryUtils.readInstant(rs, "modified"))
.setHidden(rs.getBoolean("hidden"))
- .setNoSchedule(rs.getBoolean("no_schedule"));
+ .setNoSchedule(rs.getBoolean("no_schedule"))
+ .setObservationGroupId(RepositoryUtils.getValidNullableIntegerValue(rs,"observation_group_id"));
}
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java
index de03f205..96e9bfe7 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java
@@ -13,6 +13,8 @@
import io.redlink.more.studymanager.model.Participant;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
+
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -37,8 +39,24 @@ INSERT INTO registration_tokens(study_id, participant_id, token)
VALUES (:study_id, :participant_id, :token)
ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
""";
- private static final String GET_PARTICIPANT_BY_IDS = "SELECT p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, p.modified, p.start FROM participants p LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id WHERE p.study_id = ? AND p.participant_id = ?";
- private static final String LIST_PARTICIPANTS_BY_STUDY = "SELECT p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, p.modified, p.start FROM participants p LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id WHERE p.study_id = ?";
+ private static final String GET_PARTICIPANT_BY_IDS =
+ "SELECT " +
+ " p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
+ " p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
+ "FROM participants p " +
+ " LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +
+ " LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
+ "WHERE p.study_id = ? AND p.participant_id = ? " +
+ "GROUP BY p.study_id, p.participant_id, r.token";
+ private static final String LIST_PARTICIPANTS_BY_STUDY =
+ "SELECT " +
+ " p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
+ " p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
+ "FROM participants p " +
+ " LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +
+ " LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
+ "WHERE p.study_id = ? " +
+ "GROUP BY p.study_id, p.participant_id, r.token";
private static final String DELETE_PARTICIPANT =
"DELETE FROM participants " +
"WHERE study_id=? AND participant_id=?";
@@ -49,7 +67,9 @@ ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
private static final String SET_STATUS =
"UPDATE participants p SET status = :status::participant_status, modified = now() " +
"WHERE study_id = :study_id AND participant_id = :participant_id " +
- "RETURNING *, (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token";
+ "RETURNING *, " +
+ " (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token, " +
+ " (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";
private static final String SET_STATUS_IF =
"UPDATE participants p SET status= :new_status::participant_status, modified = now() " +
"WHERE study_id = :study_id AND participant_id = :participant_id " +
@@ -57,16 +77,28 @@ ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
"RETURNING *, (SELECT token FROM registration_tokens t WHERE t.study_id = p.study_id AND t.participant_id = p.participant_id ) as token";
private static final String LIST_PARTICIPANTS_FOR_CLOSING =
- "SELECT DISTINCT p.*, 't' as token " +
+ "SELECT DISTINCT p.*, 't' as token, ARRAY_AGG(pog.observation_group_id) AS observation_group_ids " +
"FROM studies s " +
" JOIN participants p ON s.study_id = p.study_id " +
" LEFT JOIN study_groups sg ON p.study_group_id = sg.study_group_id AND p.study_id = sg.study_id " +
+ " LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
"WHERE s.status = 'active' " +
" AND p.status = 'active' " +
" AND p.start IS NOT NULL " +
" AND COALESCE(sg.duration, s.duration) IS NOT NULL " +
" AND (p.start + ((COALESCE(sg.duration, s.duration)->>'value')::int || ' ' || (COALESCE(sg.duration, s.duration)->>'unit'))::interval) < NOW()";
+ /*
+ * SQL Statements for managing participant_observation_groups mapping for participants
+ */
+ private static final String DELETE_PARTICIPANT_OBSERVATION_GROUP_IDS =
+ "DELETE FROM participant_observation_groups " +
+ "WHERE study_id = :study_id AND participant_id = :participant_id;";
+
+ private static final String SET_PARTICIPANT_OBSERVATION_GROUP_IDS =
+ "INSERT INTO participant_observation_groups (study_id, participant_id, observation_group_id) " +
+ "SELECT :study_id, :participant_id, unnest(:observation_group_ids::int[]);";
+
private static final String DELETE_ALL = "DELETE FROM participants";
private final JdbcTemplate template;
private final NamedParameterJdbcTemplate namedTemplate;
@@ -84,7 +116,9 @@ public Participant insert(Participant participant) {
} catch (DataIntegrityViolationException e) {
throw new BadRequestException("Study " + participant.getStudyId() + " does not exist");
}
- return getByIds(participant.getStudyId(), keyHolder.getKey().intValue());
+ Integer participantId = keyHolder.getKey().intValue();
+ setParticipantObservationGroupIds(participant.getStudyId(), participantId, participant.getObservationGroupIds());
+ return getByIds(participant.getStudyId(), participantId);
}
public Participant getByIds(long studyId, int participantId) {
@@ -108,8 +142,15 @@ public void deleteParticipant(Long studyId, Integer participantId) {
template.update(DELETE_PARTICIPANT, studyId, participantId);
}
+ /**
+ * Updates the participant and the {@link Participant#getObservationGroupIds()}
+ * @param participant
+ * @return the updated participant as stored in the database
+ */
+ @Transactional
public Participant update(Participant participant) {
namedTemplate.update(UPDATE_PARTICIPANT, toParams(participant).addValue("participant_id", participant.getParticipantId()));
+ setParticipantObservationGroupIds(participant.getStudyId(), participant.getParticipantId(), participant.getObservationGroupIds());
return getByIds(participant.getStudyId(), participant.getParticipantId());
}
@@ -189,6 +230,17 @@ private static RowMapper getParticipantRowMapper() {
.setModified(RepositoryUtils.readInstant(rs, "modified"))
.setStatus(RepositoryUtils.readParticipantStatus(rs, "status"))
.setStart(RepositoryUtils.readInstant(rs, "start"))
- .setRegistrationToken(rs.getString("token"));
+ .setRegistrationToken(rs.getString("token"))
+ .setObservationGroupIds(RepositoryUtils.readSet(rs, "observation_group_ids", Integer.class));
}
+
+ private void setParticipantObservationGroupIds(Long studyId, Integer participantId, Set observationGroupIds) {
+ final var params = toParams(studyId, participantId);
+ namedTemplate.update(DELETE_PARTICIPANT_OBSERVATION_GROUP_IDS, params);
+ if(observationGroupIds != null && !observationGroupIds.isEmpty()) {
+ params.addValue("observation_group_ids", observationGroupIds.toArray(new Integer[0]));
+ namedTemplate.update(SET_PARTICIPANT_OBSERVATION_GROUP_IDS, params);
+ }
+ }
+
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java
index bf895055..41875673 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java
@@ -9,13 +9,18 @@
package io.redlink.more.studymanager.repository;
import io.redlink.more.studymanager.model.Participant;
+
+import java.sql.Array;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
-import java.util.Calendar;
-import java.util.TimeZone;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,7 +31,8 @@ public final class RepositoryUtils {
private static final Logger LOG = LoggerFactory.getLogger(RepositoryUtils.class);
public static final Calendar tzUTC = Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC));
- private RepositoryUtils() {}
+ private RepositoryUtils() {
+ }
public static Instant readInstant(ResultSet rs, String columnLabel) throws SQLException {
var timestamp = rs.getTimestamp(columnLabel);
@@ -47,9 +53,9 @@ public static Instant readInstantUTC(ResultSet rs, String columnLabel) throws SQ
return timestamp.toInstant();
}
- public static LocalDate readLocalDate(ResultSet rs, String columnLabel) throws SQLException{
+ public static LocalDate readLocalDate(ResultSet rs, String columnLabel) throws SQLException {
var date = rs.getDate(columnLabel);
- if(date == null) {
+ if (date == null) {
return null;
}
return date.toLocalDate();
@@ -98,4 +104,28 @@ public static RowMapper intReader(String columnLabel) {
return anInt;
};
}
+
+ public static void consumeArray(ResultSet rs, String columnLabel, Class type, Consumer collector) throws SQLException {
+ Array sqlArray = rs.getArray(columnLabel);
+ if (sqlArray != null) {
+ Stream.of((Object[]) sqlArray.getArray())
+ .filter(Objects::nonNull) //instead of an empty Array SQL adds a NULL element at idx:0 ...
+ .filter(e -> type.isAssignableFrom(e.getClass()))
+ .map(type::cast)
+ .forEach(collector::accept);
+ }
+ }
+
+ public static Set readSet(ResultSet rs, String columnLabel, Class type) throws SQLException {
+ Set set = new HashSet<>();
+ consumeArray(rs, columnLabel, type, set::add);
+ return set;
+ }
+
+ public static List readList(ResultSet rs, String columnLabel, Class type) throws SQLException {
+ List list = new ArrayList<>();
+ consumeArray(rs, columnLabel, type, list::add);
+ return list;
+ }
+
}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCron.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCron.java
index 46d64838..d00076bc 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCron.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCron.java
@@ -73,7 +73,7 @@ private void upsertOccurredObservations(Study study) {
for(Participant participant : participants) {
ctx.putParticipant(participant);
//NOTE: from, to are currently not supported
- var timeline = calendarService.getTimeline(study, participant, null, null, null, null);
+ var timeline = calendarService.getTimeline(study, participant, null, null, null, null, null);
for(ObservationTimelineEvent event : timeline.observationTimelineEvents()){
var start = event.start().truncatedTo(ChronoUnit.MINUTES);
if((lastOccurredObservation == null || start.isAfter(lastOccurredObservation)) && start.isBefore(current)){
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/CalendarService.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/CalendarService.java
index 04e97ce3..5e0a0978 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/CalendarService.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/CalendarService.java
@@ -24,9 +24,7 @@
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.Range;
import org.springframework.stereotype.Service;
@@ -48,7 +46,7 @@ public CalendarService(StudyService studyService, ObservationService observation
this.participantService = participantService;
}
- public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer studyGroupId, Instant referenceDate, LocalDate from, LocalDate to) {
+ public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer studyGroupId, Collection observationGroupIds, Instant referenceDate, LocalDate from, LocalDate to) {
final Study study = studyService.getStudy(studyId, null)
.orElseThrow(() -> NotFoundException.Study(studyId));
final Participant participant;
@@ -58,10 +56,10 @@ public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer st
} else {
participant = null;
}
- return getTimeline(study, participant, studyGroupId, referenceDate, from, to);
+ return getTimeline(study, participant, studyGroupId, observationGroupIds, referenceDate, from, to);
}
- public StudyTimeline getTimeline(Study study, Participant participant, Integer studyGroupId, Instant referenceDate, LocalDate from, LocalDate to) {
+ public StudyTimeline getTimeline(Study study, Participant participant, Integer studyGroupId, Collection observationGroupIds, Instant referenceDate, LocalDate from, LocalDate to) {
final Range studyRange = Range.of(
Objects.requireNonNullElse(study.getStartDate(), study.getPlannedStartDate()),
Objects.requireNonNullElse(study.getEndDate(), study.getPlannedEndDate()),
@@ -88,26 +86,32 @@ public StudyTimeline getTimeline(Study study, Participant participant, Integer s
}
/*
- * effectiveGroup:
+ * effectiveStudyGroups:
* (1) participant.group (if participant is provided)
* (2) studyGroupId (if provided by user and participant is NOT provided)
* (3) otherwise
*/
- final Integer effectiveGroup;
+ final Integer effectiveStudyGroups;
if (participant != null) {
- effectiveGroup = participant.getStudyGroupId();
+ effectiveStudyGroups = participant.getStudyGroupId();
} else {
- effectiveGroup = studyGroupId;
+ effectiveStudyGroups = studyGroupId;
+ }
+ final Collection effectiveObservationGroups;
+ if (participant != null) {
+ effectiveObservationGroups = participant.getObservationGroupIds();
+ } else {
+ effectiveObservationGroups = observationGroupIds == null ? Collections.emptyList() : observationGroupIds;
}
- final List observations = observationService.listObservationsForGroup(study.getStudyId(), effectiveGroup);
- final List interventions = interventionService.listInterventionsForGroup(study.getStudyId(), effectiveGroup);
+ final List observations = observationService.listObservationsForGroup(study.getStudyId(), effectiveStudyGroups, effectiveObservationGroups);
+ final List interventions = interventionService.listInterventionsForGroup(study.getStudyId(), effectiveStudyGroups, effectiveObservationGroups);
// Shift the effective study-start if the participant would miss a relative observation
final LocalDate firstDayInStudy = SchedulerUtils.alignStartDateToSignupInstant(participantStart, observations);
// now how long does the study run?
- final Duration studyDuration = Optional.ofNullable(effectiveGroup)
+ final Duration studyDuration = Optional.ofNullable(effectiveStudyGroups)
.flatMap(eg -> studyService.getStudyDuration(study.getStudyId(), eg))
.or(() -> Optional.of(study.getDuration()))
.or(() -> Optional.of(new Duration()
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/InterventionService.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/InterventionService.java
index 4080e291..aff4942e 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/InterventionService.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/InterventionService.java
@@ -25,10 +25,8 @@
import io.redlink.more.studymanager.sdk.MoreSDK;
import io.redlink.more.studymanager.utils.LoggingUtils;
import java.text.ParseException;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.*;
+
import org.quartz.CronExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -93,8 +91,8 @@ public List listInterventions(Long studyId) {
return repository.listInterventions(studyId);
}
- public List listInterventionsForGroup(Long studyId, Integer groupId) {
- return repository.listInterventionsForGroup(studyId, groupId);
+ public List listInterventionsForGroup(Long studyId, Integer studyGroupId, Collection observationGroupIds) {
+ return repository.listInterventionsForGroup(studyId, studyGroupId, observationGroupIds);
}
public Intervention getIntervention(Long studyId, Integer interventionId) {
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationGroupService.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationGroupService.java
new file mode 100644
index 00000000..0d327906
--- /dev/null
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationGroupService.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.service;
+
+import io.redlink.more.studymanager.exception.NotFoundException;
+import io.redlink.more.studymanager.model.ObservationGroup;
+import io.redlink.more.studymanager.model.Study;
+import io.redlink.more.studymanager.repository.ObservationGroupRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+public class ObservationGroupService {
+
+ StudyStateService studyStateService;
+ private final ObservationGroupRepository repository;
+
+ public ObservationGroupService(StudyStateService studyStateService, ObservationGroupRepository repository) {
+ this.studyStateService = studyStateService;
+ this.repository = repository;
+ }
+
+ public ObservationGroup createObservationGroup(ObservationGroup observationGroup) {
+ studyStateService.assertStudyNotInState(observationGroup.getStudyId(), Study.Status.CLOSED);
+ return this.repository.insert(observationGroup);
+ }
+
+ public ObservationGroup importObservationGroup(Long studyId, ObservationGroup observationGroupGroup) {
+ return this.repository.doImport(studyId, observationGroupGroup);
+ }
+
+ public List listObservationGroups(long studyId) {
+ return this.repository.listObservationGroupsOrderedByObservationGroupIdAsc(studyId);
+ }
+
+ public ObservationGroup getObservationGroup(long studyId, int observationGroupId) {
+ return Optional.ofNullable(repository.getByIds(studyId, observationGroupId))
+ .orElseThrow(() -> NotFoundException.ObservationGroup(studyId, observationGroupId));
+ }
+
+ public ObservationGroup updateObservationGroup(ObservationGroup observationGroup) {
+ studyStateService.assertStudyNotInState(observationGroup.getStudyId(), Study.Status.CLOSED);
+ return this.repository.update(observationGroup);
+ }
+
+ public void deleteObservationGroup(long studyId, int observationGroupId) {
+ studyStateService.assertStudyNotInState(studyId, Study.Status.CLOSED);
+ this.repository.deleteById(studyId, observationGroupId);
+ }
+}
diff --git a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationService.java b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationService.java
index e6cd04dd..8ad791dd 100644
--- a/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationService.java
+++ b/studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationService.java
@@ -78,8 +78,8 @@ public List listObservations(Long studyId) {
return repository.listObservations(studyId);
}
- public List listObservationsForGroup(Long studyId, Integer groupId) {
- return repository.listObservationsForGroup(studyId, groupId);
+ public List listObservationsForGroup(Long studyId, Integer studyGroupId, Collection observationGroupIds) {
+ return repository.listObservationsForGroup(studyId, studyGroupId, observationGroupIds);
}
public Observation updateObservation(Observation observation) {
diff --git a/studymanager-services/src/main/resources/db/migration/V1_19_0__add_observation_groups.sql b/studymanager-services/src/main/resources/db/migration/V1_19_0__add_observation_groups.sql
new file mode 100644
index 00000000..c84eb499
--- /dev/null
+++ b/studymanager-services/src/main/resources/db/migration/V1_19_0__add_observation_groups.sql
@@ -0,0 +1,51 @@
+-- Observation Groups
+--
+-- An observation_group allows to group observations and interventions the belog together.
+-- Study participants can be assigned to [0..n] observation groups.
+-- The intended use cases for observation_groups are:
+-- 1. Define an observation group for a specific indication. Define related observations and intervention
+-- within this observation group. Assign participants based on their indications to the relevant observation groups
+-- 2. Define an observation group to group observations and interventions that depend on a physical device.
+-- Assign all participants with this device to that observation group.
+
+CREATE TABLE observation_groups (
+ study_id BIGINT NOT NULL,
+ observation_group_id INT NOT NULL,
+ title VARCHAR,
+ purpose TEXT,
+ created TIMESTAMP NOT NULL DEFAULT now(),
+ modified TIMESTAMP NOT NULL DEFAULT now(),
+
+ PRIMARY KEY (study_id, observation_group_id),
+ FOREIGN KEY (study_id) REFERENCES studies(study_id) ON DELETE CASCADE
+);
+
+CREATE INDEX observation_groups_study_id ON observation_groups(study_id);
+
+-- Study observations can be assigned [0..1] to an observation_groups
+ALTER TABLE observations
+ ADD COLUMN observation_group_id INT,
+
+ ADD FOREIGN KEY (study_id, observation_group_id) REFERENCES observation_groups(study_id, observation_group_id) ON DELETE SET NULL (observation_group_id);
+
+CREATE INDEX observations_observation_group_id ON observations(study_id, observation_group_id);
+
+-- Study interventions can be assigned [0..1] to an observation_groups
+ALTER TABLE interventions
+ ADD COLUMN observation_group_id INT,
+
+ ADD FOREIGN KEY (study_id, observation_group_id) REFERENCES observation_groups(study_id, observation_group_id) ON DELETE SET NULL (observation_group_id);
+
+CREATE INDEX interventions_observation_group_id ON interventions(study_id, observation_group_id);
+
+-- Study participants can be in [0..n] observation_groups
+CREATE TABLE participant_observation_groups (
+ study_id BIGINT NOT NULL,
+ participant_id INT NOT NULL,
+ observation_group_id INT NOT NULL,
+
+ PRIMARY KEY (study_id, participant_id, observation_group_id),
+ FOREIGN KEY (study_id) REFERENCES studies(study_id) ON DELETE CASCADE,
+ FOREIGN KEY (study_id, observation_group_id) REFERENCES observation_groups(study_id, observation_group_id) ON DELETE CASCADE,
+ FOREIGN KEY (study_id, participant_id) REFERENCES participants(study_id, participant_id) ON DELETE CASCADE
+);
diff --git a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java
index 6e3e9b8a..4b1ff74b 100644
--- a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java
+++ b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java
@@ -31,6 +31,7 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,8 +39,8 @@
@Testcontainers
@EnableAutoConfiguration
@ContextConfiguration(classes = {
- InterventionRepository.class, StudyRepository.class, StudyGroupRepository.class,
- JPAConfiguration.class
+ InterventionRepository.class, StudyRepository.class, StudyGroupRepository.class, ObservationGroupRepository.class,
+ JPAConfiguration.class
})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@ActiveProfiles("test-containers-flyway")
@@ -54,6 +55,9 @@ class InterventionRepositoryTest {
@Autowired
private StudyGroupRepository studyGroupRepository;
+ @Autowired
+ private ObservationGroupRepository observationGroupRepository;
+
@BeforeEach
void deleteAll() {
interventionRepository.clear();
@@ -67,6 +71,9 @@ void testInsertListUpdateDelete() {
Instant startTime = Instant.now();
Instant endTime = Instant.now().plus(2, ChronoUnit.HOURS);
+ Integer observationGroupId1 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 1").setPurpose("test")).getObservationGroupId();
+ Integer observationGroupId2 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 2").setPurpose("test")).getObservationGroupId();
+
Intervention intervention = new Intervention()
.setStudyId(studyId)
.setTitle("some title")
@@ -75,14 +82,10 @@ void testInsertListUpdateDelete() {
.setSchedule(new Event()
.setDateStart(startTime)
.setDateEnd(endTime)
- .setRRule(new RecurrenceRule().setFreq("DAILY").setCount(7)));;
-
- Intervention intervention2 = new Intervention()
- .setStudyId(studyId)
- .setTitle("some other title")
- .setStudyGroupId(studyGroupId)
- .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)));
+ .setRRule(new RecurrenceRule().setFreq("DAILY").setCount(7)))
+ .setObservationGroupId(observationGroupId1);
+ //insert
Intervention interventionResponse = interventionRepository.insert(intervention);
assertThat(interventionResponse.getInterventionId()).isNotNull();
@@ -90,26 +93,95 @@ void testInsertListUpdateDelete() {
assertThat(((Event)interventionResponse.getSchedule()).getDateStart()).isEqualTo(startTime);
assertThat(MapperUtils.writeValueAsString(interventionResponse.getSchedule()))
.isEqualTo(MapperUtils.writeValueAsString(intervention.getSchedule()));
+ assertThat(interventionResponse.getObservationGroupId()).isEqualTo(observationGroupId1);
- interventionResponse = interventionRepository.updateIntervention(new Intervention()
- .setStudyId(studyId)
- .setInterventionId(interventionResponse.getInterventionId())
+ //update
+ interventionResponse = interventionRepository.updateIntervention(interventionResponse
.setTitle("some new title")
- .setStudyGroupId(studyGroupId)
- .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60))));
+ .setObservationGroupId(null));
assertThat(interventionResponse.getTitle()).isEqualTo("some new title");
+ assertThat(interventionResponse.getObservationGroupId()).isNull();
- int intervention2Id = interventionRepository.insert(intervention2).getInterventionId();
-
- assertThat(interventionRepository.listInterventions(studyId).size()).isEqualTo(2);
+ Intervention intervention2 = interventionRepository.insert(new Intervention()
+ .setStudyId(studyId)
+ .setTitle("Intervention 2")
+ .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)))
+ .setStudyGroupId(null)
+ .setObservationGroupId(null));
+ Intervention intervention3a = interventionRepository.insert(new Intervention()
+ .setStudyId(studyId)
+ .setTitle("Intervaion 3a")
+ .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)))
+ .setStudyGroupId(null)
+ .setObservationGroupId(observationGroupId1));
+ Intervention intervention3b = interventionRepository.insert(new Intervention()
+ .setStudyId(studyId)
+ .setTitle("Intervaion 3b")
+ .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)))
+ .setStudyGroupId(null)
+ .setObservationGroupId(observationGroupId2));
+ Intervention intervention4a = interventionRepository.insert(new Intervention()
+ .setStudyId(studyId)
+ .setTitle("Intervaion 4a")
+ .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)))
+ .setStudyGroupId(studyGroupId)
+ .setObservationGroupId(observationGroupId1));
+ Intervention intervention4b = interventionRepository.insert(new Intervention()
+ .setStudyId(studyId)
+ .setTitle("Intervaion 4b")
+ .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)))
+ .setStudyGroupId(studyGroupId)
+ .setObservationGroupId(observationGroupId2));
+
+ assertThat(interventionRepository.listInterventions(studyId).size()).isEqualTo(6);
+
+ //list interventions with no groups
+ assertThat(interventionRepository.listInterventionsForGroup(studyId,null, Set.of()))
+ .extracting(Intervention::getInterventionId)
+ .containsExactly(intervention2.getInterventionId()); //intervantion 2 is not part of any group
+ //same for none existing group IDs
+ assertThat(interventionRepository.listInterventionsForGroup(studyId,-1, Set.of(-1)))
+ .extracting(Intervention::getInterventionId)
+ .containsExactly(intervention2.getInterventionId()); //intervantion 2 is not part of any group
+ //list interventions in observation group 1 with no studyGroup (or no studyGroup)
+ assertThat(interventionRepository.listInterventionsForGroup(studyId,null, Set.of(observationGroupId1)))
+ .extracting(Intervention::getInterventionId)
+ .containsExactly(intervention2.getInterventionId(), intervention3a.getInterventionId());
+ //same for none existing group
+ assertThat(interventionRepository.listInterventionsForGroup(studyId,-1, Set.of(observationGroupId1)))
+ .extracting(Intervention::getInterventionId)
+ .containsExactly(intervention2.getInterventionId(), intervention3a.getInterventionId());
+ //list interventions in study group 1 and observation group 1 with no studyGroup (or no studyGroup)
+ assertThat(interventionRepository.listInterventionsForGroup(studyId, studyGroupId, Set.of(observationGroupId1)))
+ .extracting(Intervention::getInterventionId)
+ .containsExactly(
+ interventionResponse.getInterventionId(), intervention2.getInterventionId(),
+ intervention3a.getInterventionId(), intervention4a.getInterventionId());
+
+ //list interventions in study group 1 and observation group 1 or observation group 2with no studyGroup (or no studyGroup)
+ assertThat(interventionRepository.listInterventionsForGroup(studyId, studyGroupId, Set.of(observationGroupId1, observationGroupId2)))
+ .extracting(Intervention::getInterventionId)
+ .containsExactly(
+ interventionResponse.getInterventionId(), intervention2.getInterventionId(),
+ intervention3a.getInterventionId(), intervention3b.getInterventionId(),
+ intervention4a.getInterventionId(), intervention4b.getInterventionId());
interventionRepository.deleteByIds(interventionResponse.getStudyId(), interventionResponse.getInterventionId());
- interventionResponse = interventionRepository.getByIds(intervention2.getStudyId(), intervention2Id);
+ interventionResponse = interventionRepository.getByIds(intervention2.getStudyId(), intervention2.getInterventionId());
+
+ assertThat(interventionResponse.getInterventionId()).isEqualTo(intervention2.getInterventionId());
+ assertThat(interventionRepository.listInterventions(studyId).size()).isEqualTo(5);
+
+ //assert delete Observation Group sets property of Observation to null
+ observationGroupRepository.deleteById(studyId, observationGroupId1);
+ assertThat((interventionRepository.getByIds(studyId, intervention3a.getInterventionId()).getObservationGroupId()))
+ .isNull();
+ assertThat((interventionRepository.getByIds(studyId, intervention4a.getInterventionId()).getObservationGroupId()))
+ .isNull();
+
- assertThat(interventionResponse.getInterventionId()).isEqualTo(intervention2Id);
- assertThat(interventionRepository.listInterventions(studyId).size()).isEqualTo(1);
}
@Test
diff --git a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationGroupRepositoryTest.java b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationGroupRepositoryTest.java
new file mode 100644
index 00000000..8c61f23c
--- /dev/null
+++ b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationGroupRepositoryTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.repository;
+
+import io.redlink.more.studymanager.configuration.JPAConfiguration;
+import io.redlink.more.studymanager.core.properties.ObservationProperties;
+import io.redlink.more.studymanager.model.*;
+import io.redlink.more.studymanager.model.scheduler.*;
+import io.redlink.more.studymanager.utils.MapperUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+@Testcontainers
+@EnableAutoConfiguration
+@ContextConfiguration(classes = {
+ ObservationGroupRepository.class, StudyRepository.class,
+ JPAConfiguration.class
+})
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+@ActiveProfiles("test-containers-flyway")
+class ObservationGroupRepositoryTest {
+
+ @Autowired
+ private ObservationGroupRepository observationGroupRepository;
+
+ @Autowired
+ private StudyRepository studyRepository;
+
+ @BeforeEach
+ void deleteAll() {
+ observationGroupRepository.clear();
+ }
+
+ @Test
+ @DisplayName("ObservationGroups are inserted, updated, listed and deleted from database")
+ public void testInsertListUpdateDelete() throws InterruptedException {
+
+ Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
+
+ //INSERT
+ ObservationGroup observationGroup = new ObservationGroup()
+ .setStudyId(studyId)
+ .setTitle("Test Observation Group")
+ .setPurpose("Test Purpose");
+
+ ObservationGroup observationGroupResponse = observationGroupRepository.insert(observationGroup);
+
+ assertThat(observationGroupResponse.getObservationGroupId()).isNotNull();
+ assertThat(observationGroupResponse.getStudyId()).isEqualTo(observationGroup.getStudyId());
+ assertThat(observationGroupResponse.getTitle()).isEqualTo(observationGroup.getTitle());
+ assertThat(observationGroupResponse.getPurpose()).isEqualTo(observationGroup.getPurpose());
+ assertThat(observationGroupResponse.getCreated()).isNotNull();
+ assertThat(observationGroupResponse.getModified()).isNotNull();
+ assertThat(observationGroupResponse.getCreated()).isEqualTo(observationGroupResponse.getModified());
+
+ //UPDATE
+ observationGroupResponse
+ .setTitle("Updated Observation Group")
+ .setPurpose("Updated Purpose");
+
+ TimeUnit.MICROSECONDS.sleep(1); //to ensure a different modified time
+
+ ObservationGroup compareObservationGroupResponse = observationGroupRepository.update(observationGroupResponse);
+
+ assertThat(compareObservationGroupResponse.getTitle()).isEqualTo(observationGroupResponse.getTitle());
+ assertThat(compareObservationGroupResponse.getPurpose()).isEqualTo(observationGroupResponse.getPurpose());
+ assertThat(compareObservationGroupResponse.getStudyId()).isEqualTo(observationGroupResponse.getStudyId());
+ assertThat(compareObservationGroupResponse.getObservationGroupId()).isEqualTo(observationGroupResponse.getObservationGroupId());
+ assertThat(compareObservationGroupResponse.getCreated()).isEqualTo(observationGroupResponse.getCreated());
+ assertThat(compareObservationGroupResponse.getModified()).isAfter(observationGroupResponse.getModified());
+
+ ObservationGroup observationGroup2Response = observationGroupRepository.insert(new ObservationGroup()
+ .setStudyId(studyId)
+ .setTitle("Test Observation Group 2")
+ .setPurpose("Test Purpose 2"));
+
+ Long studyId2 = studyRepository.insert(new Study().setContact(new Contact().setPerson("test3").setEmail("test3"))).getStudyId();
+
+ ObservationGroup observationGroupStudy2Response = observationGroupRepository.insert(new ObservationGroup()
+ .setStudyId(studyId2)
+ .setTitle("Test Observation Group Study 2")
+ .setPurpose("Test Purpose 3"));
+
+ List study1ObserationGroups = observationGroupRepository.listObservationGroupsOrderedByObservationGroupIdAsc(studyId);
+ assertThat(study1ObserationGroups.size()).isEqualTo(2);
+ assertThat(study1ObserationGroups.get(0).getStudyId()).isEqualTo(studyId);
+ assertThat(study1ObserationGroups.get(0).getObservationGroupId()).isEqualTo(observationGroupResponse.getObservationGroupId());
+ assertThat(study1ObserationGroups.get(1).getStudyId()).isEqualTo(studyId);
+ assertThat(study1ObserationGroups.get(1).getObservationGroupId()).isEqualTo(observationGroup2Response.getObservationGroupId());
+
+ List study2ObserationGroups = observationGroupRepository.listObservationGroupsOrderedByObservationGroupIdAsc(studyId2);
+ assertThat(study2ObserationGroups.size()).isEqualTo(1);
+ assertThat(study2ObserationGroups.get(0).getStudyId()).isEqualTo(studyId2);
+ assertThat(study2ObserationGroups.get(0).getObservationGroupId()).isEqualTo(observationGroupStudy2Response.getObservationGroupId());
+
+ // Delete the group specific observations
+ observationGroupRepository.deleteById(studyId, observationGroup2Response.getObservationGroupId());
+ assertThat(observationGroupRepository.listObservationGroupsOrderedByObservationGroupIdAsc(studyId))
+ .hasSize(1);
+ }
+}
diff --git a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java
index 931c6640..0fe64126 100644
--- a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java
+++ b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java
@@ -30,6 +30,7 @@
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -37,7 +38,7 @@
@Testcontainers
@EnableAutoConfiguration
@ContextConfiguration(classes = {
- ObservationRepository.class, StudyRepository.class, ParticipantRepository.class,
+ ObservationRepository.class, StudyRepository.class, ParticipantRepository.class, ObservationGroupRepository.class,
StudyGroupRepository.class, JPAConfiguration.class
})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@@ -56,6 +57,9 @@ class ObservationRepositoryTest {
@Autowired
private StudyGroupRepository studyGroupRepository;
+ @Autowired
+ private ObservationGroupRepository observationGroupRepository;
+
@BeforeEach
void deleteAll() {
observationRepository.clear();
@@ -70,6 +74,9 @@ public void testInsertListUpdateDelete() {
Instant startTime = Instant.now();
Instant endTime = Instant.now().plus(2, ChronoUnit.HOURS);
+ Integer observationGroupId1 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 1").setPurpose("test")).getObservationGroupId();
+ Integer observationGroupId2 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 2").setPurpose("test")).getObservationGroupId();
+
Observation observation = new Observation()
.setStudyId(studyId)
.setType(type)
@@ -81,7 +88,8 @@ public void testInsertListUpdateDelete() {
.setDateEnd(endTime)
.setRRule(new RecurrenceRule().setFreq("DAILY").setCount(7)))
.setHidden(true)
- .setNoSchedule(false);
+ .setNoSchedule(false)
+ .setObservationGroupId(observationGroupId1);
Observation observationResponse = observationRepository.insert(observation);
@@ -90,12 +98,14 @@ public void testInsertListUpdateDelete() {
assertThat(observationResponse.getProperties()).isEqualTo(observation.getProperties());
assertThat(MapperUtils.writeValueAsString(observationResponse.getSchedule()))
.isEqualTo(MapperUtils.writeValueAsString(observation.getSchedule()));
+ assertThat(observationResponse.getObservationGroupId()).isEqualTo(observationGroupId1);
Integer oldId = observationResponse.getObservationId();
observationResponse.setType("new type")
.setTitle("some new title")
- .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)));
+ .setSchedule(new Event().setDateEnd(Instant.now()).setDateEnd(Instant.now().plusSeconds(60)))
+ .setObservationGroupId(null);
Observation compareObservationResponse = observationRepository.updateObservation(observationResponse);
@@ -103,6 +113,7 @@ public void testInsertListUpdateDelete() {
assertThat(compareObservationResponse.getType()).isEqualTo(type);
assertThat(compareObservationResponse.getObservationId()).isEqualTo(oldId);
assertThat(compareObservationResponse.getSchedule()).isNotEqualTo(observation.getSchedule());
+ assertThat(observationResponse.getObservationGroupId()).isNull();
Observation observationResponse2 = observationRepository.insert(new Observation()
.setStudyId(studyId)
@@ -111,39 +122,89 @@ public void testInsertListUpdateDelete() {
.setHidden(true)
.setNoSchedule(true)
);
+ Observation observationResponse3a = observationRepository.insert(new Observation()
+ .setStudyId(studyId)
+ .setType("gps")
+ .setType("new Title - obs-group 1")
+ .setHidden(true)
+ .setNoSchedule(true)
+ .setObservationGroupId(observationGroupId1)
+ );
+ Observation observationResponse3b = observationRepository.insert(new Observation()
+ .setStudyId(studyId)
+ .setType("gps")
+ .setType("new Title 2")
+ .setHidden(true)
+ .setNoSchedule(true)
+ .setObservationGroupId(observationGroupId2)
+ );
- assertThat((observationRepository.listObservations(studyId)))
+ assertThat(observationRepository.listObservations(studyId))
.as("List all Observations")
- .hasSize(2);
- assertThat((observationRepository.listObservationsForGroup(studyId, studyGroupId)))
- .as("Include group-specific observations and globals")
- .hasSize(2);
- assertThat((observationRepository.listObservationsForGroup(studyId, -1)))
+ .hasSize(4);
+
+ assertThat(observationRepository.listObservationsForGroup(studyId, studyGroupId))
+ .as("Include group-specific observations and globals") //NOTE: and in no observation group
+ .hasSize(2)
+ .extracting(Observation::getObservationId)
+ .containsOnly(observationResponse.getObservationId(), observationResponse2.getObservationId());
+
+ assertThat(observationRepository.listObservationsForGroup(studyId, -1))
.as("Non-existing Group should only retrieve 'global' observations")
- .hasSize(1);
- assertThat((observationRepository.listObservationsForGroup(studyId, null)))
+ .hasSize(1)
+ .extracting(Observation::getObservationId)
+ .containsExactly(observationResponse2.getObservationId());
+
+ assertThat(observationRepository.listObservationsForGroup(studyId, null))
.as("-Group should only retrieve 'global' observations")
.hasSize(1)
.as("Check for the global observation")
.extracting(Observation::getObservationId)
- .contains(observationResponse2.getObservationId());
+ .containsExactly(observationResponse2.getObservationId());
+
+ //list Observations for any study-group and no observation-group of observation-group 'observationGroupId1' ->
+ //this is true for obs2 (no group at all) and obs3 (in the correct observation group)
+ assertThat(observationRepository.listObservationsForGroup(studyId, null, Set.of(observationGroupId1)))
+ .as("Obseration-Group should only retrieve observation in that observation group")
+ .hasSize(2)
+ .extracting(Observation::getObservationId)
+ .containsExactly(observationResponse2.getObservationId(), observationResponse3a.getObservationId());
+
+
+ //list Observations for study-group '-1' and no observation-group of observation-group 'observationGroupId1' ->
+ // This is true for all expect 'obs1'
+ assertThat(observationRepository.listObservationsForGroup(studyId, -1, Set.of(observationGroupId1, observationGroupId2)))
+ .as("Obseration-Group should only retrieve observation in that observation group")
+ .hasSize(3)
+ .extracting(Observation::getObservationId)
+ .containsExactly(observationResponse2.getObservationId(), observationResponse3a.getObservationId(), observationResponse3b.getObservationId());
+
+ //test relative events
+ observation.setSchedule(new RelativeEvent()
+ .setDtstart(new RelativeDate().setOffset(new Duration().setValue(1).setUnit(Duration.Unit.DAY)).setTime(LocalTime.parse("12:00:00")))
+ .setDtend(new RelativeDate().setOffset(new Duration().setValue(2).setUnit(Duration.Unit.DAY)).setTime(LocalTime.parse("13:00:00"))));
+ Observation observationResponse4 = observationRepository.insert(observation);
+ assertThat(observationResponse4.getSchedule())
+ .isInstanceOf(RelativeEvent.class);
+
+ //assert delete Observation Group sets property of Observation to null
+ observationGroupRepository.deleteById(studyId, observationGroupId1);
+ assertThat((observationRepository.getById(studyId, observationResponse3a.getObservationId()).getObservationGroupId()))
+ .isNull();
+
// Delete the group specific observations
+ observationRepository.deleteObservation(studyId, observationResponse4.getObservationId());
observationRepository.deleteObservation(studyId, observationResponse.getObservationId());
assertThat((observationRepository.listObservations(studyId)))
- .hasSize(1);
+ .hasSize(3);
assertThat((observationRepository.listObservationsForGroup(studyId, studyGroupId)))
- .hasSize(1);
+ .hasSize(2) //now that we deleted observationGroupId1 -> observationResponse3a is also in no group
+ .extracting(Observation::getObservationId)
+ .containsExactly(observationResponse2.getObservationId(), observationResponse3a.getObservationId());
observationRepository.deleteObservation(studyId, observationResponse2.getObservationId());
assertThat((observationRepository.listObservations(studyId)))
- .hasSize(0);
-
- observation.setSchedule(new RelativeEvent()
- .setDtstart(new RelativeDate().setOffset(new Duration().setValue(1).setUnit(Duration.Unit.DAY)).setTime(LocalTime.parse("12:00:00")))
- .setDtend(new RelativeDate().setOffset(new Duration().setValue(2).setUnit(Duration.Unit.DAY)).setTime(LocalTime.parse("13:00:00"))));
+ .hasSize(2);
- Observation observationResponse3 = observationRepository.insert(observation);
- assertThat(observationResponse3.getSchedule())
- .isInstanceOf(RelativeEvent.class);
}
@Test
diff --git a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java
index 0c846b8a..15285859 100644
--- a/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java
+++ b/studymanager-services/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java
@@ -9,10 +9,7 @@
package io.redlink.more.studymanager.repository;
import io.redlink.more.studymanager.configuration.JPAConfiguration;
-import io.redlink.more.studymanager.model.Contact;
-import io.redlink.more.studymanager.model.Participant;
-import io.redlink.more.studymanager.model.Study;
-import io.redlink.more.studymanager.model.StudyGroup;
+import io.redlink.more.studymanager.model.*;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -27,13 +24,16 @@
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Testcontainers
@EnableAutoConfiguration
@ContextConfiguration(classes = {
- ParticipantRepository.class, StudyRepository.class, StudyGroupRepository.class,
+ ParticipantRepository.class, StudyRepository.class, StudyGroupRepository.class, ObservationGroupRepository.class,
JPAConfiguration.class
})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@@ -49,6 +49,9 @@ class ParticipantRepositoryTest {
@Autowired
private StudyGroupRepository studyGroupRepository;
+ @Autowired
+ private ObservationGroupRepository observationGroupRepository;
+
@BeforeEach
void deleteAll() {
participantRepository.clear();
@@ -56,38 +59,51 @@ void deleteAll() {
@Test
@DisplayName("Participant is inserted and returned")
- void testInsert() {
+ void testInsert() throws InterruptedException {
Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
Integer studyGroupId = studyGroupRepository.insert(new StudyGroup()
.setStudyId(studyId)).getStudyGroupId();
+ Integer observationGroupId1 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 1").setPurpose("test")).getObservationGroupId();
+ Integer observationGroupId2 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 2").setPurpose("test")).getObservationGroupId();
+ Integer observationGroupId3 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 3").setPurpose("test")).getObservationGroupId();
+
Participant participant = new Participant()
.setAlias("participant x")
.setStudyGroupId(studyGroupId)
.setStudyId(studyId)
- .setRegistrationToken("TEST123");
+ .setRegistrationToken("TEST123")
+ .setObservationGroupIds(Set.of(observationGroupId1, observationGroupId2));
Participant participantResponse = participantRepository.insert(participant);
assertThat(participantResponse.getAlias()).isEqualTo(participant.getAlias());
assertThat(participantResponse.getStatus()).isEqualTo(Participant.Status.NEW);
assertThat(participantResponse.getParticipantId()).isNotNull();
+ assertThat(participantResponse.getObservationGroupIds()).containsExactlyInAnyOrder(observationGroupId1, observationGroupId2);
+
+ Participant update = participantResponse
+ .setAlias("new participant x")
+ .setObservationGroupIds(Set.of(observationGroupId1, observationGroupId3)); //replace observationGroup1 with observationGroup3
- Participant update = participantResponse.setAlias("new participant x");
+ TimeUnit.MICROSECONDS.sleep(1); //to ensure a different modified time
Participant updated = participantRepository.update(update);
+ assertThat(update.getStudyId()).isEqualTo(participantResponse.getStudyId());
+ assertThat(update.getAlias()).isEqualTo(updated.getAlias());
+ assertThat(update.getObservationGroupIds()).containsExactlyInAnyOrder(observationGroupId1, observationGroupId3);
+ assertThat(updated.getCreated()).isEqualTo(participantResponse.getCreated());
+ assertThat(updated.getModified()).isAfter(participantResponse.getModified());
+
Participant queried = participantRepository.getByIds(participantResponse.getStudyId(), participantResponse.getParticipantId());
assertThat(queried.getAlias()).isEqualTo(updated.getAlias());
assertThat(queried.getStudyId()).isEqualTo(updated.getStudyId());
assertThat(queried.getCreated()).isEqualTo(updated.getCreated());
assertThat(queried.getStatus()).isEqualTo(updated.getStatus());
+ assertThat(queried.getObservationGroupIds()).containsExactlyInAnyOrder(observationGroupId1, observationGroupId3);
- assertThat(update.getAlias()).isEqualTo(updated.getAlias());
- assertThat(participantResponse.getStudyId()).isEqualTo(updated.getStudyId());
- assertThat(participantResponse.getCreated()).isEqualTo(updated.getCreated());
- assertThat(participantResponse.getModified().toEpochMilli()).isLessThan(updated.getModified().toEpochMilli());
}
@Test
@@ -143,16 +159,25 @@ private Participant createParticipant(Long studyId) {
void testSetState() {
Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId();
- Participant participant = participantRepository.insert(new Participant().setStudyId(studyId).setRegistrationToken("TEST123"));
+ Integer observationGroup1 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 1").setPurpose("Purpose 1")).getObservationGroupId();
+ Integer observationGroup2 = observationGroupRepository.insert(new ObservationGroup().setStudyId(studyId).setTitle("Observation Group 2").setPurpose("Purpose 2")).getObservationGroupId();
+
+ Participant participant = participantRepository.insert(new Participant().setStudyId(studyId).setRegistrationToken("TEST123").setObservationGroupIds(Set.of(observationGroup1,observationGroup2)));
assertThat(participant.getStatus()).isEqualTo(Participant.Status.NEW);
participant = participantRepository.getByIds(studyId, participant.getParticipantId());
assertThat(participant.getStatus()).isEqualTo(Participant.Status.NEW);
- participantRepository.setStatusByIds(studyId, participant.getParticipantId(), Participant.Status.ACTIVE);
+ participant = participantRepository.setStatusByIds(studyId, participant.getParticipantId(), Participant.Status.ACTIVE).get();
+ assertThat(participant.getStatus()).isEqualTo(Participant.Status.ACTIVE);
+ //Assert that the SQL query for the status update correctly retrieves the observation groups for the participant
+ assertThat(participant.getObservationGroupIds()).containsExactlyInAnyOrder(observationGroup1, observationGroup2);
+ //make an additional retrieval just to be sure
participant = participantRepository.getByIds(studyId, participant.getParticipantId());
assertThat(participant.getStatus()).isEqualTo(Participant.Status.ACTIVE);
+ //Assert that the SQL query for the status update correctly retrieves the observation groups for the participant
+ assertThat(participant.getObservationGroupIds()).containsExactlyInAnyOrder(observationGroup1, observationGroup2);
}
@Test
diff --git a/studymanager-services/src/test/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCronTest.java b/studymanager-services/src/test/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCronTest.java
index fb179901..4bb582ff 100644
--- a/studymanager-services/src/test/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCronTest.java
+++ b/studymanager-services/src/test/java/io/redlink/more/studymanager/scheduling/UpsertOccurredObservationsCronTest.java
@@ -97,7 +97,7 @@ public void testUpsert() {
when(participantService.listParticipants(study2.getStudyId()))
.thenReturn(List.of(study2Par1, study2Par2));
- when(calendarService.getTimeline(any(Study.class), any(Participant.class), any(), any(), any(), any()))
+ when(calendarService.getTimeline(any(Study.class), any(Participant.class), any(), any(), any(), any(), any()))
.thenAnswer(invocationOnMock -> {
Long studyId = invocationOnMock.getArgument(0, Study.class).getStudyId();
Integer participantId = invocationOnMock.getArgument(1, Participant.class).getParticipantId();
diff --git a/studymanager-services/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java b/studymanager-services/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java
index 4762d820..1cbfc953 100644
--- a/studymanager-services/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java
+++ b/studymanager-services/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java
@@ -21,6 +21,8 @@
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
+
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -53,7 +55,7 @@ class CalendarServiceTest {
@Test
void testStudyNotFound() {
when(studyService.getStudy(any(), any())).thenReturn(Optional.empty());
- assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1, 1, Instant.now(), LocalDate.now(), LocalDate.now()));
+ assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1, 1, Set.of(), Instant.now(), LocalDate.now(), LocalDate.now()));
}
@Test
@@ -66,7 +68,7 @@ void testParticipantNotFound() {
.setPlannedStartDate(LocalDate.now())
.setPlannedEndDate(LocalDate.now().plusDays(3))
));
- assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1,1, Instant.now(), LocalDate.now(), LocalDate.now()));
+ assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1,1, Set.of(), Instant.now(), LocalDate.now(), LocalDate.now()));
}
@Test
@@ -78,7 +80,7 @@ void testGetTimeline() {
.setPlannedEndDate(LocalDate.of(2024, 5, 14))
.setDuration(new Duration().setUnit(Duration.Unit.DAY).setValue(5));
- Participant participant = new Participant().setStudyGroupId(2);
+ Participant participant = new Participant().setStudyGroupId(2).setObservationGroupIds(Set.of(1,2));
Observation observationAbsolute = new Observation()
.setObservationId(1)
@@ -165,10 +167,10 @@ void testGetTimeline() {
when(participantService.getParticipant(any(), any())).thenReturn(participant);
when(studyService.getStudyDuration(any(), any()))
.thenReturn(Optional.of(new Duration().setValue(5).setUnit(Duration.Unit.DAY)));
- when(observationService.listObservationsForGroup(any(), eq(participant.getStudyGroupId()))).thenReturn(
+ when(observationService.listObservationsForGroup(any(), eq(participant.getStudyGroupId()),eq(participant.getObservationGroupIds()))).thenReturn(
List.of(observationAbsolute, observationAbsoluteRecurrent, observationRelative, observationRelativeRecurrent));
- when(interventionService.listInterventionsForGroup(any(), eq(participant.getStudyGroupId()))).thenReturn(
+ when(interventionService.listInterventionsForGroup(any(), eq(participant.getStudyGroupId()), eq(participant.getObservationGroupIds()))).thenReturn(
List.of(scheduledIntervention, relativeIntervention));
when(interventionService.getTriggerByIds(any(), eq(1))).thenReturn(relativeTrigger);
when(interventionService.getTriggerByIds(any(), eq(2))).thenReturn(scheduledTrigger);
@@ -176,7 +178,8 @@ void testGetTimeline() {
StudyTimeline timeline = calendarService.getTimeline(
1L,
1,
- 2,
+ -2, //ignored ... taken from participant
+ Set.of(-1,-2), //ignored ... taken from participant
LocalDateTime.of(
LocalDate.of(2024, 5, 11),
LocalTime.of(10,10,10)
diff --git a/studymanager/pom.xml b/studymanager/pom.xml
index e64b5209..f0dda56f 100644
--- a/studymanager/pom.xml
+++ b/studymanager/pom.xml
@@ -107,17 +107,17 @@
org.testcontainers
- junit-jupiter
+ testcontainers-junit-jupiter
test
org.testcontainers
- postgresql
+ testcontainers-postgresql
test
org.testcontainers
- elasticsearch
+ testcontainers-elasticsearch
test
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java
index 498a1e2a..de657dcf 100644
--- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java
@@ -24,6 +24,8 @@
import java.time.Instant;
import java.time.LocalDate;
+import java.util.Collections;
+import java.util.Set;
@RestController
@RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE)
@@ -50,10 +52,13 @@ public ResponseEntity getStudyCalendar(Long studyId) {
@Override
@RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR})
@Audited
- public ResponseEntity getStudyTimeline(Long studyId, Integer participant, Integer studyGroup, Instant referenceDate, LocalDate from, LocalDate to) {
+ public ResponseEntity getStudyTimeline(Long studyId, Integer participant, Integer studyGroup, Integer observationGroup, Instant referenceDate, LocalDate from, LocalDate to) {
return ResponseEntity.ok(
TimelineTransformer.toStudyTimelineDTO(
- service.getTimeline(studyId, participant, studyGroup, referenceDate, from, to)
+ service.getTimeline(
+ studyId, participant, studyGroup,
+ observationGroup == null ? Collections.emptySet() : Set.of(observationGroup),
+ referenceDate, from, to)
)
);
}
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ObservationGroupApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ObservationGroupApiV1Controller.java
new file mode 100644
index 00000000..8226bbd1
--- /dev/null
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ObservationGroupApiV1Controller.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.controller.studymanager;
+
+import io.redlink.more.studymanager.api.v1.model.ObservationGroupDTO;
+import io.redlink.more.studymanager.api.v1.webservices.ObservationGroupsApi;
+import io.redlink.more.studymanager.audit.Audited;
+import io.redlink.more.studymanager.controller.RequiresStudyRole;
+import io.redlink.more.studymanager.model.ObservationGroup;
+import io.redlink.more.studymanager.model.StudyRole;
+import io.redlink.more.studymanager.model.transformer.ObservationGroupTransformer;
+import io.redlink.more.studymanager.service.ObservationGroupService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE)
+public class ObservationGroupApiV1Controller implements ObservationGroupsApi {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ObservationGroupApiV1Controller.class);
+
+ private final ObservationGroupService service;
+
+
+ public ObservationGroupApiV1Controller(ObservationGroupService service) {
+ this.service = service;
+ }
+
+ @Override
+ @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR})
+ @Audited
+ public ResponseEntity createObservationGroup(Long studyId, ObservationGroupDTO observationGroupDTO) {
+ observationGroupDTO.setStudyId(studyId);
+ ObservationGroup observationGroup = service.createObservationGroup(
+ ObservationGroupTransformer.fromObservationGroupDTO_V1(observationGroupDTO)
+ );
+ LOGGER.debug("ObservationGroup created: {}", observationGroup);
+ return ResponseEntity.status(HttpStatus.CREATED).body(
+ ObservationGroupTransformer.toObservationGroupDTO_V1(observationGroup)
+ );
+ }
+
+ @Override
+ @RequiresStudyRole
+ @Audited
+ public ResponseEntity> listObservationGroups(Long studyId) {
+ return ResponseEntity.ok(
+ service.listObservationGroups(studyId).stream()
+ .map(ObservationGroupTransformer::toObservationGroupDTO_V1)
+ .toList()
+ );
+ }
+
+ @Override
+ @RequiresStudyRole
+ @Audited
+ public ResponseEntity getObservationGroup(Long studyId, Integer observationGroupId) {
+ return ResponseEntity.ok(
+ ObservationGroupTransformer.toObservationGroupDTO_V1(service.getObservationGroup(studyId, observationGroupId))
+ );
+ }
+
+ @Override
+ @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR})
+ @Audited
+ public ResponseEntity updateObservationGroup(Long studyId, Integer observationGroupId, ObservationGroupDTO observationGroupDTO) {
+ observationGroupDTO.setStudyId(studyId);
+ observationGroupDTO.setObservationGroupId(observationGroupId);
+ return ResponseEntity.ok(
+ ObservationGroupTransformer.toObservationGroupDTO_V1(
+ service.updateObservationGroup(
+ ObservationGroupTransformer.fromObservationGroupDTO_V1(observationGroupDTO)
+ )
+ )
+ );
+ }
+
+ @Override
+ @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR})
+ @Audited
+ public ResponseEntity deleteObservationGroup(Long studyId, Integer observationGroupId) {
+ service.deleteObservationGroup(studyId, observationGroupId);
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java
index a4979f67..9157b017 100644
--- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java
@@ -14,7 +14,10 @@
import io.redlink.more.studymanager.api.v1.model.StudyImportExportDTO;
import io.redlink.more.studymanager.model.IntegrationInfo;
import io.redlink.more.studymanager.model.StudyImportExport;
+
+import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -71,11 +74,14 @@ public static StudyImportExportDTO toStudyImportExportDTO_V1(StudyImportExport s
private static ParticipantInfoDTO toParticipantDTO_V1(StudyImportExport.ParticipantInfo participant) {
return new ParticipantInfoDTO()
- .studyGroup(participant.groupId());
+ .studyGroup(participant.groupId())
+ .observationGroups(participant.observationGroupIds());
}
private static StudyImportExport.ParticipantInfo fromParticipantDTO_V1(ParticipantInfoDTO participant) {
- return new StudyImportExport.ParticipantInfo(participant.getStudyGroup());
+ return new StudyImportExport.ParticipantInfo(
+ participant.getStudyGroup(),
+ participant.getObservationGroups() == null ? Collections.emptySet() : participant.getObservationGroups());
}
private static List transform(List list, Function transformer) {
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java
index 7595dfb7..a5db41dc 100644
--- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java
@@ -24,7 +24,8 @@ public static Intervention fromInterventionDTO_V1(InterventionDTO dto) {
.setTitle(dto.getTitle())
.setPurpose(dto.getPurpose())
.setStudyGroupId(dto.getStudyGroupId())
- .setSchedule(EventTransformer.fromObservationScheduleDTO_V1(dto.getSchedule()));
+ .setSchedule(EventTransformer.fromObservationScheduleDTO_V1(dto.getSchedule()))
+ .setObservationGroupId(dto.getObservationGroupId());
}
public static InterventionDTO toInterventionDTO_V1(Intervention intervention) {
@@ -37,6 +38,7 @@ public static InterventionDTO toInterventionDTO_V1(Intervention intervention) {
.purpose(intervention.getPurpose())
.studyGroupId(intervention.getStudyGroupId())
.schedule(EventTransformer.toObservationScheduleDTO_V1(intervention.getSchedule()))
+ .observationGroupId(intervention.getObservationGroupId())
.created(instant1)
.modified(instant);
}
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationGroupTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationGroupTransformer.java
new file mode 100644
index 00000000..1a79a411
--- /dev/null
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationGroupTransformer.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.model.transformer;
+
+import io.redlink.more.studymanager.api.v1.model.ObservationGroupDTO;
+import io.redlink.more.studymanager.model.ObservationGroup;
+
+import java.time.Instant;
+
+public final class ObservationGroupTransformer {
+
+ private ObservationGroupTransformer() {
+ }
+
+ public static ObservationGroup fromObservationGroupDTO_V1(ObservationGroupDTO ObservationGroupDTO) {
+ return new ObservationGroup()
+ .setStudyId(ObservationGroupDTO.getStudyId())
+ .setObservationGroupId(ObservationGroupDTO.getObservationGroupId())
+ .setTitle(ObservationGroupDTO.getTitle())
+ .setPurpose(ObservationGroupDTO.getPurpose());
+ }
+
+ public static ObservationGroupDTO toObservationGroupDTO_V1(ObservationGroup ObservationGroup) {
+ Instant instant = ObservationGroup.getModified();
+ Instant instant1 = ObservationGroup.getCreated();
+ return new ObservationGroupDTO()
+ .studyId(ObservationGroup.getStudyId())
+ .observationGroupId(ObservationGroup.getObservationGroupId())
+ .title(ObservationGroup.getTitle())
+ .purpose(ObservationGroup.getPurpose())
+ .created(instant1)
+ .modified(instant);
+ }
+
+
+
+}
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java
index 56baf62a..9f6843a3 100644
--- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java
@@ -31,7 +31,8 @@ public static Observation fromObservationDTO_V1(ObservationDTO dto) {
.setProperties(MapperUtils.MAPPER.convertValue(dto.getProperties(), ObservationProperties.class))
.setSchedule(EventTransformer.fromObservationScheduleDTO_V1(dto.getSchedule()))
.setHidden(dto.getHidden())
- .setNoSchedule(dto.getNoSchedule());
+ .setNoSchedule(dto.getNoSchedule())
+ .setObservationGroupId(dto.getObservationGroupId());
}
public static ObservationDTO toObservationDTO_V1(Observation observation) {
@@ -50,7 +51,8 @@ public static ObservationDTO toObservationDTO_V1(Observation observation) {
.created(instant1)
.modified(instant)
.hidden(observation.getHidden())
- .noSchedule(observation.getNoSchedule());
+ .noSchedule(observation.getNoSchedule())
+ .observationGroupId(observation.getObservationGroupId());
}
}
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java
index fc2c0484..afcbc37d 100644
--- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java
@@ -25,7 +25,8 @@ public static Participant fromParticipantDTO_V1(ParticipantDTO participantDTO) {
.setStudyId(participantDTO.getStudyId())
.setParticipantId(participantDTO.getParticipantId())
.setAlias(participantDTO.getAlias())
- .setStudyGroupId(participantDTO.getStudyGroupId());
+ .setStudyGroupId(participantDTO.getStudyGroupId())
+ .setObservationGroupIds(participantDTO.getObservationGroupIds());
}
public static ParticipantDTO toParticipantDTO_V1(Participant participant, GatewayProperties gatewayProps) {
@@ -42,6 +43,7 @@ public static ParticipantDTO toParticipantDTO_V1(Participant participant, Gatewa
.registrationToken(participant.getRegistrationToken())
.registrationUrl(registrationUri)
.status(ParticipantStatusDTO.fromValue(participant.getStatus().getValue()))
+ .observationGroupIds(participant.getObservationGroupIds())
.start(instant2)
.modified(instant1)
.created(instant);
diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java
index abd09686..a217b8b1 100644
--- a/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java
+++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java
@@ -39,6 +39,7 @@ public class ImportExportService {
private final ObservationService observationService;
private final InterventionService interventionService;
private final StudyGroupService studyGroupService;
+ private final ObservationGroupService observationGroupService;
private final IntegrationService integrationService;
private final ElasticService elasticService;
@@ -46,6 +47,7 @@ public class ImportExportService {
public ImportExportService(ParticipantService participantService, StudyService studyService, StudyStateService studyStateService,
ObservationService observationService, InterventionService interventionService, StudyGroupService studyGroupService,
+ ObservationGroupService observationGroupService,
IntegrationService integrationService, ElasticService elasticService, GatewayProperties gatewayProperties) {
this.participantService = participantService;
this.studyService = studyService;
@@ -53,6 +55,7 @@ public ImportExportService(ParticipantService participantService, StudyService s
this.observationService = observationService;
this.interventionService = interventionService;
this.studyGroupService = studyGroupService;
+ this.observationGroupService = observationGroupService;
this.integrationService = integrationService;
this.elasticService = elasticService;
this.gatewayProperties = gatewayProperties;
@@ -95,6 +98,7 @@ public StudyImportExport exportStudy(Long studyId, User user) {
.setStudy(studyService.getStudy(studyId, user)
.orElseThrow(() -> new NotFoundException("study", studyId)))
.setStudyGroups(studyGroupService.listStudyGroups(studyId))
+ .setObservationGroups(observationGroupService.listObservationGroups(studyId))
.setObservations(observationService.listObservations(studyId))
.setInterventions(interventionService.listInterventions(studyId))
.setActions(new HashMap<>())
@@ -105,7 +109,9 @@ public StudyImportExport exportStudy(Long studyId, User user) {
export.setParticipants(participantService.listParticipants(studyId)
.stream()
.sorted(Comparator.comparing(Participant::getParticipantId))
- .map(participant -> new StudyImportExport.ParticipantInfo(participant.getStudyGroupId()))
+ .map(participant -> new StudyImportExport.ParticipantInfo(
+ participant.getStudyGroupId(),
+ participant.getObservationGroupIds()))
.toList()
);
@@ -133,29 +139,31 @@ public Study importStudy(StudyImportExport studyImport, AuthenticatedUser user)
final Long studyId = newStudy.getStudyId();
studyImport.getStudyGroups().forEach(studyGroup ->
- studyGroupService.importStudyGroup(studyId, studyGroup)
- );
+ studyGroupService.importStudyGroup(studyId, studyGroup));
+
+ studyImport.getObservationGroups().forEach(observationGroup ->
+ observationGroupService.importObservationGroup(studyId, observationGroup));
+
studyImport.getObservations().forEach(observation ->
- observationService.importObservation(studyId, observation)
- );
+ observationService.importObservation(studyId, observation));
+
studyImport.getInterventions().forEach(intervention ->
interventionService.importIntervention(
studyId,
intervention,
studyImport.getTriggers().get(intervention.getInterventionId()),
- studyImport.getActions().getOrDefault(intervention.getInterventionId(), Collections.emptyList())
- )
- );
+ studyImport.getActions().getOrDefault(intervention.getInterventionId(), Collections.emptyList())));
+
studyImport.getParticipants().forEach(participant ->
participantService.createParticipant(
new Participant()
.setStudyId(studyId)
.setAlias("Participant")
.setStudyGroupId(participant.groupId())
- ));
+ .setObservationGroupIds(participant.observationGroupIds())));
+
studyImport.getIntegrations().forEach(integration ->
- integrationService.addToken(studyId, integration.observationId(), integration.name())
- );
+ integrationService.addToken(studyId, integration.observationId(), integration.name()));
return newStudy;
}
diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml
index 471246cd..77dbfdd2 100644
--- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml
+++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml
@@ -350,6 +350,10 @@ paths:
in: query
schema:
$ref: '#/components/schemas/Id'
+ - name: observationGroup
+ in: query
+ schema:
+ $ref: '#/components/schemas/Id'
- name: referenceDate
in: query
description: reference date used to calculate relative schedules
@@ -476,6 +480,110 @@ paths:
'404':
description: Not found
+ /studies/{studyId}/observationGroups:
+ post:
+ tags:
+ - observationGroups
+ operationId: createObservationGroup
+ description: Create a new observation group for a study
+ parameters:
+ - $ref: '#/components/parameters/StudyId'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObservationGroup'
+ responses:
+ '201':
+ description: Observation group successfully created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObservationGroup'
+ '400':
+ description: Observation group creation failed because of a bad request
+ '500':
+ description: Error while creating the observation group
+ get:
+ tags:
+ - observationGroups
+ description: List all observation groups for a study
+ operationId: listObservationGroups
+ parameters:
+ - $ref: '#/components/parameters/StudyId'
+ responses:
+ '200':
+ description: Successfully listed all study groups
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/ObservationGroup'
+ '404':
+ description: No study for the parsed studyId found
+
+ /studies/{studyId}/observationGroups/{observationGroupId}:
+ get:
+ tags:
+ - observationGroups
+ description: Get observation group information
+ operationId: getObservationGroup
+ parameters:
+ - $ref: '#/components/parameters/StudyId'
+ - $ref: '#/components/parameters/ObservationGroupId'
+ responses:
+ '200':
+ description: Successfully returned observation group information
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObservationGroup'
+ '404':
+ description: Not found
+ put:
+ tags:
+ - observationGroups
+ description: Update a observation group
+ operationId: updateObservationGroup
+ parameters:
+ - $ref: '#/components/parameters/StudyId'
+ - $ref: '#/components/parameters/ObservationGroupId'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObservationGroup'
+ responses:
+ '200':
+ description: Successfully updated observation group
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObservationGroup'
+ '400':
+ description: Could not update study group because of a bad request
+ '404':
+ description: Not found
+ '500':
+ description: Error while updating the observation group
+ delete:
+ tags:
+ - observationGroups
+ description: Delete a observation group
+ operationId: deleteObservationGroup
+ parameters:
+ - $ref: '#/components/parameters/StudyId'
+ - $ref: '#/components/parameters/ObservationGroupId'
+ responses:
+ '200':
+ description: Observation group deleted
+ '404':
+ description: Not found
+ '500':
+ description: Error while deleting the observation group
+
/studies/{studyId}/participants:
post:
tags:
@@ -1615,6 +1723,28 @@ components:
type: string
format: date-time
readOnly: true
+ ObservationGroup:
+ type: object
+ properties:
+ studyId:
+ $ref: '#/components/schemas/StudyId'
+ observationGroupId:
+ $ref: '#/components/schemas/Id'
+ title:
+ type: string
+ purpose:
+ type: string
+ numberOfParticipants:
+ type: integer
+ readOnly: true
+ created:
+ type: string
+ format: date-time
+ readOnly: true
+ modified:
+ type: string
+ format: date-time
+ readOnly: true
StudyDuration:
type: object
properties:
@@ -1651,6 +1781,12 @@ components:
type: string
format: date-time
readOnly: true
+ observationGroupIds:
+ type: array
+ description: The set of observation group Ids the participant is part of
+ items:
+ $ref: '#/components/schemas/IdReference'
+ uniqueItems: true
created:
type: string
format: date-time
@@ -1714,6 +1850,8 @@ components:
noSchedule:
type: boolean
default: false
+ observationGroupId:
+ $ref: '#/components/schemas/IdReference'
StudyTimeline:
@@ -1913,6 +2051,8 @@ components:
type: array
items:
$ref: '#/components/schemas/Action'
+ observationGroupId:
+ $ref: '#/components/schemas/IdReference'
created:
type: string
format: date-time
@@ -1982,6 +2122,10 @@ components:
type: array
items:
$ref: '#/components/schemas/StudyGroup'
+ observationGroups:
+ type: array
+ items:
+ $ref: '#/components/schemas/ObservationGroup'
observations:
type: array
items:
@@ -2004,6 +2148,11 @@ components:
properties:
studyGroup:
type: integer
+ observationGroups:
+ type: array
+ items:
+ type: integer
+ uniqueItems: true
IntegrationInfo:
type: object
@@ -2364,6 +2513,13 @@ components:
type: integer
format: int32
required: true
+ ObservationGroupId:
+ name: observationGroupId
+ in: path
+ schema:
+ type: integer
+ format: int32
+ required: true
ParticipantId:
name: participantId
in: path
diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java
index 163172f6..3ca00c43 100644
--- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java
+++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java
@@ -64,42 +64,40 @@ void testGetStudyTimeline() throws Exception {
LocalDate from = LocalDate.of(2024, 2, 1);
LocalDate to = LocalDate.of(2024, 5, 1);
- when(service.getTimeline(any(Long.class), any(Integer.class), any(), any(Instant.class), any(LocalDate.class), any(LocalDate.class)))
- .thenAnswer(invocationOnMock -> {
- return new StudyTimeline(
- referenceDate,
- Range.of(from, to, LocalDate::compareTo),
- List.of(
- ObservationTimelineEvent.fromObservation(
- new Observation()
- .setObservationId(1)
- .setStudyId(invocationOnMock.getArgument(0))
- .setStudyGroupId(studyGroup1)
- .setTitle("title 1")
- .setPurpose("purpose 1")
- .setType("type 1")
- .setHidden(Boolean.FALSE)
- .setSchedule(new Event()),
- ((LocalDate)invocationOnMock.getArgument(4)).atStartOfDay(ZoneId.systemDefault()).toInstant(),
- ((LocalDate)invocationOnMock.getArgument(5)).atStartOfDay(ZoneId.systemDefault()).toInstant()
- ),
- ObservationTimelineEvent.fromObservation(
- new Observation()
- .setObservationId(2)
- .setStudyId(invocationOnMock.getArgument(0))
- .setStudyGroupId(studyGroup2)
- .setTitle("title 2")
- .setPurpose("purpose 2")
- .setType("type 2")
- .setHidden(Boolean.TRUE)
- .setSchedule(new RelativeEvent()),
- ((LocalDate)invocationOnMock.getArgument(4)).atStartOfDay(ZoneId.systemDefault()).toInstant(),
- ((LocalDate)invocationOnMock.getArgument(5)).atStartOfDay(ZoneId.systemDefault()).toInstant()
- )
- ),
- List.of()
- );
- });
+ when(service.getTimeline(any(Long.class), any(Integer.class), any(), any(), any(Instant.class), any(LocalDate.class), any(LocalDate.class)))
+ .thenAnswer(invocationOnMock -> new StudyTimeline(
+ referenceDate,
+ Range.of(from, to, LocalDate::compareTo),
+ List.of(
+ ObservationTimelineEvent.fromObservation(
+ new Observation()
+ .setObservationId(1)
+ .setStudyId(invocationOnMock.getArgument(0))
+ .setStudyGroupId(studyGroup1)
+ .setTitle("title 1")
+ .setPurpose("purpose 1")
+ .setType("type 1")
+ .setHidden(Boolean.FALSE)
+ .setSchedule(new Event()),
+ ((LocalDate)invocationOnMock.getArgument(5)).atStartOfDay(ZoneId.systemDefault()).toInstant(),
+ ((LocalDate)invocationOnMock.getArgument(6)).atStartOfDay(ZoneId.systemDefault()).toInstant()
+ ),
+ ObservationTimelineEvent.fromObservation(
+ new Observation()
+ .setObservationId(2)
+ .setStudyId(invocationOnMock.getArgument(0))
+ .setStudyGroupId(studyGroup2)
+ .setTitle("title 2")
+ .setPurpose("purpose 2")
+ .setType("type 2")
+ .setHidden(Boolean.TRUE)
+ .setSchedule(new RelativeEvent()),
+ ((LocalDate)invocationOnMock.getArgument(5)).atStartOfDay(ZoneId.systemDefault()).toInstant(),
+ ((LocalDate)invocationOnMock.getArgument(6)).atStartOfDay(ZoneId.systemDefault()).toInstant()
+ )
+ ),
+ List.of()
+ ));
mvc.perform(get("/api/v1/studies/3/timeline")
.param("participant", String.valueOf(2))
diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java
index 3106313b..f74686c8 100644
--- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java
+++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java
@@ -92,6 +92,7 @@ void testAddIntervention() throws Exception {
.setSchedule(new Event()
.setDateStart(dateStart)
.setDateEnd(dateEnd))
+ .setObservationGroupId(((Intervention)invocationOnMock.getArgument(0)).getObservationGroupId())
.setCreated(Instant.now())
.setModified(Instant.now()));
@@ -103,7 +104,8 @@ void testAddIntervention() throws Exception {
.studyGroupId(1)
.schedule(new EventDTO()
.dtstart(dateStart)
- .dtend(dateEnd));
+ .dtend(dateEnd))
+ .observationGroupId(1);
mvc.perform(post("/api/v1/studies/1/interventions")
.content(mapper.writeValueAsString(interventionRequest))
@@ -112,7 +114,8 @@ void testAddIntervention() throws Exception {
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value(interventionRequest.getTitle()))
.andExpect(jsonPath("$.interventionId").value(interventionRequest.getInterventionId()))
- .andExpect(jsonPath("$.schedule").exists());
+ .andExpect(jsonPath("$.schedule").exists())
+ .andExpect(jsonPath("$.observationGroupId").value(interventionRequest.getObservationGroupId()));
}
@Test
diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java
index 61d50ef7..9e2dca24 100644
--- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java
+++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java
@@ -94,7 +94,8 @@ void testAddObservation() throws Exception {
.setStudyGroupId(((Observation)invocationOnMock.getArgument(0)).getStudyGroupId())
.setCreated(Instant.ofEpochMilli(System.currentTimeMillis()))
.setModified(Instant.ofEpochMilli(System.currentTimeMillis()))
- .setHidden(((Observation) invocationOnMock.getArgument(0)).getHidden()));
+ .setHidden(((Observation) invocationOnMock.getArgument(0)).getHidden())
+ .setObservationGroupId(((Observation) invocationOnMock.getArgument(0)).getObservationGroupId()));
ObservationDTO observationRequest = new ObservationDTO()
.title("observation 1")
@@ -105,7 +106,8 @@ void testAddObservation() throws Exception {
.type("accelerometer")
.properties(Map.of("name", "value"))
.studyGroupId(1)
- .hidden(null);
+ .hidden(null)
+ .observationGroupId(1);
mvc.perform(post("/api/v1/studies/1/observations")
.content(mapper.writeValueAsString(observationRequest))
@@ -116,7 +118,8 @@ void testAddObservation() throws Exception {
.andExpect(jsonPath("$.observationId").value(observationRequest.getObservationId()))
.andExpect(jsonPath("$.type").value(observationRequest.getType()))
.andExpect(jsonPath("$.properties.name").value("value"))
- .andExpect(jsonPath("$.hidden").value(observationRequest.getHidden()));
+ .andExpect(jsonPath("$.hidden").value(observationRequest.getHidden()))
+ .andExpect(jsonPath("$.observationGroupId").value(observationRequest.getObservationGroupId()));
}
@Test
diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationGroupControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationGroupControllerTest.java
new file mode 100644
index 00000000..3c409ff2
--- /dev/null
+++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationGroupControllerTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
+ * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
+ * for Digital Health and Prevention -- A research institute of the
+ * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur
+ * Förderung der wissenschaftlichen Forschung).
+ * Licensed under the Elastic License 2.0.
+ */
+package io.redlink.more.studymanager.controller.studymanager;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.redlink.more.studymanager.api.v1.model.EndpointTokenDTO;
+import io.redlink.more.studymanager.api.v1.model.ObservationGroupDTO;
+import io.redlink.more.studymanager.model.*;
+import io.redlink.more.studymanager.service.OAuth2AuthenticationService;
+import io.redlink.more.studymanager.service.ObservationGroupService;
+import org.assertj.core.api.Assertions;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.time.Instant;
+import java.util.*;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest({ObservationGroupApiV1Controller.class})
+@AutoConfigureMockMvc(addFilters = false)
+class ObservationGroupControllerTest {
+
+
+ @MockitoBean
+ ObservationGroupService observationGroupService;
+
+ @MockitoBean
+ OAuth2AuthenticationService oAuth2AuthenticationService;
+
+ @Autowired
+ ObjectMapper mapper;
+
+ @Autowired
+ private MockMvc mvc;
+
+ @BeforeEach
+ void setUp() {
+ when(oAuth2AuthenticationService.getCurrentUser()).thenReturn(
+ new AuthenticatedUser(
+ UUID.randomUUID().toString(),
+ "Test User", "test@example.com", "Test Inc.",
+ EnumSet.allOf(PlatformRole.class)
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("Create ObservationGroup")
+ void testCreateObservationGroup() throws Exception {
+ when(observationGroupService.createObservationGroup(any(ObservationGroup.class)))
+ .thenAnswer(invocationOnMock -> new ObservationGroup()
+ .setStudyId(((ObservationGroup)invocationOnMock.getArgument(0)).getStudyId())
+ .setObservationGroupId(1)
+ .setTitle(((ObservationGroup)invocationOnMock.getArgument(0)).getTitle())
+ .setPurpose(((ObservationGroup)invocationOnMock.getArgument(0)).getPurpose())
+ .setCreated(Instant.ofEpochMilli(System.currentTimeMillis()))
+ .setModified(Instant.ofEpochMilli(System.currentTimeMillis())));
+
+ ObservationGroupDTO observationGroupRequestBody = new ObservationGroupDTO()
+ .studyId(13L) //expected to be overridden by the controller to the id parts as path
+ .title("observation group 1")
+ .purpose("for testing only");
+
+ mvc.perform(post("/api/v1/studies/1/observationGroups")
+ .content(mapper.writeValueAsString(observationGroupRequestBody))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andDo(print())
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.studyId").value(1L))
+ .andExpect(jsonPath("$.observationGroupId").value(1))
+ .andExpect(jsonPath("$.title").value(observationGroupRequestBody.getTitle()))
+ .andExpect(jsonPath("$.purpose").value(observationGroupRequestBody.getPurpose()))
+ .andExpect(jsonPath("$.created").exists())
+ .andExpect(jsonPath("$.modified").exists());
+ }
+
+ @Test
+ @DisplayName("Update observation group")
+ void testUpdateObservationGroup() throws Exception {
+ when(observationGroupService.updateObservationGroup(any(ObservationGroup.class))).thenAnswer(invocationOnMock ->
+ new ObservationGroup()
+ .setStudyId(((ObservationGroup)invocationOnMock.getArgument(0)).getStudyId())
+ .setObservationGroupId(((ObservationGroup)invocationOnMock.getArgument(0)).getObservationGroupId())
+ .setTitle(((ObservationGroup)invocationOnMock.getArgument(0)).getTitle())
+ .setPurpose(((ObservationGroup)invocationOnMock.getArgument(0)).getPurpose())
+ .setCreated(Instant.now().minusSeconds(100))
+ .setModified(Instant.now().minusSeconds(50)));
+
+
+ ObservationGroupDTO observationRequest = new ObservationGroupDTO()
+ .studyId(13L) //need to be ignored as this is specified via path parameters
+ .observationGroupId(13) //need to be ignored as this is specified via path parameters
+ .title("the updated title")
+ .purpose("the purpose of life")
+ .created(Instant.now().minusSeconds(100));
+
+ mvc.perform(put("/api/v1/studies/1/observationGroups/1")
+ .content(mapper.writeValueAsString(observationRequest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.studyId").value(1)) //as in the path
+ .andExpect(jsonPath("$.observationGroupId").value(1)) //as in the path
+ .andExpect(jsonPath("$.title").value(observationRequest.getTitle()))
+ .andExpect(jsonPath("$.purpose").value(observationRequest.getPurpose()))
+ .andExpect(jsonPath("$.modified").exists())
+ .andExpect(jsonPath("$.created").exists());
+ }
+
+ @Test
+ @DisplayName("List all observation groups for a study")
+ void testListObservationGroup() throws Exception {
+ when(observationGroupService.listObservationGroups(any(Long.class))).thenAnswer(invocationOnMock -> {
+ Long studyId = ((Long) invocationOnMock.getArgument(0));
+ Assertions.assertThat(studyId).isEqualTo(1L);
+ return List.of(
+ new ObservationGroup()
+ .setStudyId(studyId)
+ .setObservationGroupId(1)
+ .setTitle("Observation group 1")
+ .setPurpose("purpose 1")
+ .setCreated(Instant.now().minusSeconds(100))
+ .setModified(Instant.now().minusSeconds(50)),
+ new ObservationGroup()
+ .setStudyId(studyId)
+ .setObservationGroupId(2)
+ .setTitle("Observation group 2")
+ .setPurpose("purpose 2")
+ .setCreated(Instant.now().minusSeconds(25))
+ .setModified(Instant.now().minusSeconds(12)),
+ new ObservationGroup()
+ .setStudyId(studyId)
+ .setObservationGroupId(3)
+ .setTitle("Observation group 3")
+ .setPurpose("purpose 3")
+ .setCreated(Instant.now().minusSeconds(6))
+ .setModified(Instant.now().minusSeconds(3))
+ );
+ });
+
+ mvc.perform(get("/api/v1/studies/1/observationGroups"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$").isArray())
+ .andExpect(jsonPath("$", Matchers.hasSize(3)))
+ .andExpect(jsonPath("$[0].studyId").value(1))
+ .andExpect(jsonPath("$[0].observationGroupId").value(1))
+ .andExpect(jsonPath("$[0].title").value("Observation group 1"))
+ .andExpect(jsonPath("$[0].purpose").value("purpose 1"))
+ .andExpect(jsonPath("$[0].created").exists())
+ .andExpect(jsonPath("$[0].modified").exists())
+ .andExpect(jsonPath("$[1].studyId").value(1))
+ .andExpect(jsonPath("$[1].observationGroupId").value(2))
+ .andExpect(jsonPath("$[2].studyId").value(1))
+ .andExpect(jsonPath("$[2].observationGroupId").value(3));
+ }
+
+ @Test
+ @DisplayName("Get Observation Group")
+ void testGetObservationGroup() throws Exception{
+ when(observationGroupService.getObservationGroup(any(Long.class),any(Integer.class))).thenAnswer(invocationOnMock -> {
+ Long studyId = ((Long) invocationOnMock.getArgument(0));
+ Integer observationGroup = ((Integer) invocationOnMock.getArgument(1));
+ return new ObservationGroup()
+ .setStudyId(studyId)
+ .setObservationGroupId(observationGroup)
+ .setTitle("Observation group " + observationGroup + " of study " + studyId)
+ .setPurpose("purpose of observation group " + observationGroup + " of study " + studyId)
+ .setCreated(Instant.now().minusSeconds(100))
+ .setModified(Instant.now().minusSeconds(50));
+ });
+
+ mvc.perform(get("/api/v1/studies/3/observationGroups/23"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.studyId").value(3L))
+ .andExpect(jsonPath("$.observationGroupId").value(23))
+ .andExpect(jsonPath("$.title").value("Observation group 23 of study 3"))
+ .andExpect(jsonPath("$.purpose").value("purpose of observation group 23 of study 3"))
+ .andExpect(jsonPath("$.created").exists())
+ .andExpect(jsonPath("$.modified").exists());
+ }
+ @Test
+ @DisplayName("Delete Observation Group")
+ void testDeleteObservationGroup() throws Exception{
+ Mockito.doNothing().when(observationGroupService).deleteObservationGroup(any(Long.class),any(Integer.class));
+
+ mvc.perform(delete("/api/v1/studies/3/observationGroups/23"))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ Mockito.verify(observationGroupService, times(1)).deleteObservationGroup(eq(3L), eq(23));
+ }
+}
+
+
+
diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ParticipantControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ParticipantControllerTest.java
index 39ac2e2d..73d5ae2c 100644
--- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ParticipantControllerTest.java
+++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ParticipantControllerTest.java
@@ -21,6 +21,7 @@
import java.time.Instant;
import java.util.EnumSet;
import java.util.Random;
+import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeEach;
@@ -100,12 +101,14 @@ void testCreateParticipant() throws Exception {
.setStatus(Participant.Status.NEW)
.setCreated(Instant.ofEpochMilli(System.currentTimeMillis()))
.setModified(Instant.ofEpochMilli(System.currentTimeMillis()))
- .setRegistrationToken("TEST123"));
+ .setRegistrationToken("TEST123")
+ .setObservationGroupIds(Set.of(1, 2)));
ParticipantDTO participantRequest = new ParticipantDTO()
.studyId(studyId)
.alias("participant x")
- .studyGroupId(1);
+ .studyGroupId(1)
+ .observationGroupIds(Set.of(1,2));
ParticipantDTO[] participantDTOS = new ParticipantDTO[]{participantRequest};
@@ -118,7 +121,11 @@ void testCreateParticipant() throws Exception {
.andExpect(jsonPath("$[0].participantId").value(1))
.andExpect(jsonPath("$[0].status").value("new"))
.andExpect(jsonPath("$[0].studyGroupId").value(participantRequest.getStudyGroupId()))
- .andExpect(jsonPath("$[0].registrationToken").exists());
+ .andExpect(jsonPath("$[0].registrationToken").exists())
+ .andExpect(jsonPath("$[0].observationGroupIds").isArray())
+ //NOTE: use Json path to do some kind of contains in any order ...
+ .andExpect(jsonPath("$[0].observationGroupIds[?(@ == 1)]").exists())
+ .andExpect(jsonPath("$[0].observationGroupIds[?(@ == 2)]").exists());
}
@Test
diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/service/ImportExportServiceTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/service/ImportExportServiceTest.java
index a6db7cee..b1dfbfd2 100644
--- a/studymanager/src/test/java/io/redlink/more/studymanager/service/ImportExportServiceTest.java
+++ b/studymanager/src/test/java/io/redlink/more/studymanager/service/ImportExportServiceTest.java
@@ -54,6 +54,9 @@ public class ImportExportServiceTest {
@Spy
private StudyGroupService studyGroupService = mock(StudyGroupService.class);
+ @Spy
+ private ObservationGroupService observationGroupService = mock(ObservationGroupService.class);
+
@InjectMocks
ImportExportService importExportService;
@@ -119,7 +122,8 @@ void testImportStudy() {
.setType("gps-mobile-observation")
.setStudyGroupId(3)
.setProperties(new ObservationProperties())
- .setSchedule(new Event()),
+ .setSchedule(new Event())
+ .setObservationGroupId(1),
new Observation()
.setObservationId(3)
.setTitle("observation Title")
@@ -128,7 +132,8 @@ void testImportStudy() {
.setType("gps-mobile-observation")
.setStudyGroupId(null)
.setProperties(new ObservationProperties())
- .setSchedule(new Event())))
+ .setSchedule(new Event())
+ .setObservationGroupId(2)))
.setStudyGroups(List.of(
new StudyGroup()
.setStudyGroupId(2)
@@ -138,19 +143,30 @@ void testImportStudy() {
.setStudyGroupId(3)
.setTitle("group title2")
.setPurpose("group purpose2")))
+ .setObservationGroups(List.of(
+ new ObservationGroup()
+ .setObservationGroupId(1)
+ .setTitle("observation group title 1")
+ .setPurpose("observation group purpose 1"),
+ new ObservationGroup()
+ .setObservationGroupId(2)
+ .setTitle("observation group title 2")
+ .setPurpose("observation group purpose 2")))
.setInterventions(List.of(
new Intervention()
.setInterventionId(2)
.setTitle("intervention title")
.setPurpose("intervention purpose")
.setStudyGroupId(2)
- .setSchedule(new Event()),
+ .setSchedule(new Event())
+ .setObservationGroupId(1),
new Intervention()
.setInterventionId(3)
.setTitle("intervention title")
.setPurpose("intervention purpose")
.setStudyGroupId(3)
- .setSchedule(new Event())))
+ .setSchedule(new Event())
+ .setObservationGroupId(2)))
.setTriggers(Map.of(3, new Trigger()
.setType("sth")
.setProperties(new TriggerProperties())))
@@ -158,14 +174,14 @@ void testImportStudy() {
.setType("sth")
.setProperties(new ActionProperties()))))
.setParticipants(List.of(
- new StudyImportExport.ParticipantInfo(0),
- new StudyImportExport.ParticipantInfo(0),
- new StudyImportExport.ParticipantInfo(0),
- new StudyImportExport.ParticipantInfo(2),
- new StudyImportExport.ParticipantInfo(2),
- new StudyImportExport.ParticipantInfo(2),
- new StudyImportExport.ParticipantInfo(4),
- new StudyImportExport.ParticipantInfo(4)
+ new StudyImportExport.ParticipantInfo(0, null),
+ new StudyImportExport.ParticipantInfo(0, Set.of(1)),
+ new StudyImportExport.ParticipantInfo(0, Set.of(1,2)),
+ new StudyImportExport.ParticipantInfo(2, Set.of()),
+ new StudyImportExport.ParticipantInfo(2, Set.of(2)),
+ new StudyImportExport.ParticipantInfo(2, Set.of(1,2)),
+ new StudyImportExport.ParticipantInfo(4, Set.of(1)),
+ new StudyImportExport.ParticipantInfo(4, Set.of(2))
))
.setIntegrations(List.of(
new IntegrationInfo("Integration 1", 1),
@@ -190,6 +206,13 @@ void testImportStudy() {
assertThat(studyGroupCaptor.getAllValues().get(0).getStudyGroupId()).isEqualTo(2);
assertThat(studyGroupCaptor.getAllValues().get(1).getStudyGroupId()).isEqualTo(3);
+ ArgumentCaptor observationGroupCaptor = ArgumentCaptor.forClass(ObservationGroup.class);
+ verify(observationGroupService, times(2)).importObservationGroup(idLongCaptor.capture(), observationGroupCaptor.capture());
+ assertThat(studyGroupCaptor.getAllValues()).hasSize(2);
+ assertThat(studyGroupCaptor.getAllValues().get(0).getStudyGroupId()).isEqualTo(2);
+ assertThat(studyGroupCaptor.getAllValues().get(1).getStudyGroupId()).isEqualTo(3);
+
+
verify(observationService, times(2)).importObservation(idLongCaptor.capture(), observationCaptor.capture());
verify(interventionService, times(2)).importIntervention(idLongCaptor.capture(), interventionCaptor.capture(), triggerCaptor.capture(), actionCaptor.capture());
verify(participantService, times(8)).createParticipant(participantsCaptor.capture());
@@ -197,15 +220,32 @@ void testImportStudy() {
assertThat(observationCaptor.getAllValues().get(0).getObservationId()).isEqualTo(1);
assertThat(observationCaptor.getAllValues().get(0).getStudyGroupId()).isEqualTo(3);
+ assertThat(observationCaptor.getAllValues().get(0).getObservationGroupId()).isEqualTo(1);
assertThat(observationCaptor.getAllValues().get(1).getObservationId()).isEqualTo(3);
assertThat(observationCaptor.getAllValues().get(1).getStudyGroupId()).isEqualTo(null);
+ assertThat(observationCaptor.getAllValues().get(1).getObservationGroupId()).isEqualTo(2);
assertThat(interventionCaptor.getAllValues().get(0).getStudyGroupId()).isEqualTo(2);
assertThat(interventionCaptor.getAllValues().get(0).getInterventionId()).isEqualTo(2);
+ assertThat(interventionCaptor.getAllValues().get(0).getObservationGroupId()).isEqualTo(1);
assertThat(interventionCaptor.getAllValues().get(1).getStudyGroupId()).isEqualTo(3);
assertThat(interventionCaptor.getAllValues().get(1).getInterventionId()).isEqualTo(3);
+ assertThat(interventionCaptor.getAllValues().get(1).getObservationGroupId()).isEqualTo(2);
assertThat(idLongCaptor.getAllValues()).allMatch(Predicate.isEqual(1L));
+
+ assertThat(participantsCaptor.getAllValues().stream().map(Participant::getStudyId)).allMatch(Predicate.isEqual(1L));
+ assertThat(participantsCaptor.getAllValues().subList(0,3).stream().map(Participant::getStudyGroupId)).allMatch(Predicate.isEqual(0));
+ assertThat(participantsCaptor.getAllValues().subList(3,6).stream().map(Participant::getStudyGroupId)).allMatch(Predicate.isEqual(2));
+ assertThat(participantsCaptor.getAllValues().subList(6,8).stream().map(Participant::getStudyGroupId)).allMatch(Predicate.isEqual(4));
+ assertThat(participantsCaptor.getAllValues().get(0).getObservationGroupIds()).isEmpty();
+ assertThat(participantsCaptor.getAllValues().get(1).getObservationGroupIds()).containsExactlyInAnyOrder(1);
+ assertThat(participantsCaptor.getAllValues().get(2).getObservationGroupIds()).containsExactlyInAnyOrder(1, 2);
+ assertThat(participantsCaptor.getAllValues().get(3).getObservationGroupIds()).isEmpty();
+ assertThat(participantsCaptor.getAllValues().get(4).getObservationGroupIds()).containsExactlyInAnyOrder(2);
+ assertThat(participantsCaptor.getAllValues().get(5).getObservationGroupIds()).containsExactlyInAnyOrder(1, 2);
+ assertThat(participantsCaptor.getAllValues().get(6).getObservationGroupIds()).containsExactlyInAnyOrder(1);
+ assertThat(participantsCaptor.getAllValues().get(7).getObservationGroupIds()).containsExactlyInAnyOrder(2);
}