From d3cd937f3d74558de90f5d6ce304054268081af8 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Fri, 27 Oct 2023 14:52:12 +0200 Subject: [PATCH 01/16] MORE2-2 extend API to new RelativeEvents * adapt OpenAPI * adapt Transformers * handle downwards compatibility (default types for existing database values) * add some tests HINT: switch to new model in UI first as 'type' property of schedule events is now required for Observation API calls --- .../more/studymanager/model/Intervention.java | 9 +- .../more/studymanager/model/Observation.java | 7 +- .../model/scheduler/Duration.java | 118 ++++++++++++++++++ .../model/{ => scheduler}/Event.java | 13 +- .../model/{ => scheduler}/RecurrenceRule.java | 2 +- .../model/scheduler/RelativeDate.java | 28 +++++ .../model/scheduler/RelativeEvent.java | 57 +++++++++ .../scheduler/RelativeRecurrenceRule.java | 29 +++++ .../model/scheduler/ScheduleEvent.java | 18 +++ .../model/transformer/EventTransformer.java | 96 +++++++++++--- .../transformer/InterventionTransformer.java | 4 +- .../transformer/ObservationTransformer.java | 4 +- .../repository/InterventionRepository.java | 2 +- .../repository/ObservationRepository.java | 2 +- .../src/main/resources/openapi/Event.yaml | 3 + .../main/resources/openapi/RelativeEvent.yaml | 63 ++++++++++ .../resources/openapi/StudyManagerAPI.yaml | 17 ++- .../ImportExportControllerTest.java | 2 + .../InterventionControllerTest.java | 2 +- .../ObservationControllerTest.java | 6 +- .../repository/IntegrationRepositoryTest.java | 2 + .../InterventionRepositoryTest.java | 4 +- .../repository/ObservationRepositoryTest.java | 2 + .../service/ImportExportServiceTest.java | 1 + .../ScheduleEventTransformerTest.java | 107 ++++++++++++++++ 25 files changed, 560 insertions(+), 38 deletions(-) create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java rename studymanager/src/main/java/io/redlink/more/studymanager/model/{ => scheduler}/Event.java (73%) rename studymanager/src/main/java/io/redlink/more/studymanager/model/{ => scheduler}/RecurrenceRule.java (97%) create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java create mode 100644 studymanager/src/main/resources/openapi/RelativeEvent.yaml create mode 100644 studymanager/src/test/java/io/redlink/more/studymanager/transformer/ScheduleEventTransformerTest.java diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/Intervention.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Intervention.java index 99f83777..44fcc59b 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/Intervention.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/Intervention.java @@ -1,5 +1,8 @@ package io.redlink.more.studymanager.model; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.ScheduleEvent; + import java.time.Instant; public class Intervention { @@ -8,7 +11,7 @@ public class Intervention { private String title; private String purpose; private Integer studyGroupId; - private Event schedule; + private ScheduleEvent schedule; private Instant created; private Instant modified; @@ -57,11 +60,11 @@ public Intervention setStudyGroupId(Integer studyGroupId) { return this; } - public Event getSchedule() { + public ScheduleEvent getSchedule() { return schedule; } - public Intervention setSchedule(Event schedule) { + public Intervention setSchedule(ScheduleEvent schedule) { this.schedule = schedule; return this; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/Observation.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Observation.java index 70018d34..e81681b3 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/Observation.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/Observation.java @@ -1,6 +1,7 @@ package io.redlink.more.studymanager.model; import io.redlink.more.studymanager.core.properties.ObservationProperties; +import io.redlink.more.studymanager.model.scheduler.ScheduleEvent; import java.time.Instant; @@ -13,7 +14,7 @@ public class Observation { private String type; private Integer studyGroupId; private ObservationProperties properties; - private Event schedule; + private ScheduleEvent schedule; private Instant created; private Instant modified; private Boolean hidden; @@ -91,11 +92,11 @@ public Observation setProperties(ObservationProperties properties) { return this; } - public Event getSchedule() { + public ScheduleEvent getSchedule() { return schedule; } - public Observation setSchedule(Event schedule) { + public Observation setSchedule(ScheduleEvent schedule) { this.schedule = schedule; return this; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java new file mode 100644 index 00000000..1477ccb7 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java @@ -0,0 +1,118 @@ +package io.redlink.more.studymanager.model.scheduler; + +import com.fasterxml.jackson.annotation.JsonCreator; +import io.redlink.more.studymanager.api.v1.model.DurationDTO; + +public class Duration { + + private Integer value; + + /** + * unit of time to offset + */ + public enum Unit { + MINUTE("MINUTE"), + + HOUR("HOUR"), + + DAY("DAY"); + + private String value; + + Unit(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static Unit fromValue(String value) { + for (Unit b : Unit.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + + public static Unit fromDurationDTOUnit(DurationDTO.UnitEnum unit) { + switch (unit) { + case MINUTE: + return MINUTE; + case HOUR: + return HOUR; + case DAY: + return DAY; + default: + throw new IllegalArgumentException("Unexpected value '" + unit + "'"); + } + } + + public static DurationDTO.UnitEnum toDurationDTOUnit(Unit unit) { + switch (unit) { + case MINUTE: + return DurationDTO.UnitEnum.MINUTE; + case HOUR: + return DurationDTO.UnitEnum.HOUR; + case DAY: + return DurationDTO.UnitEnum.DAY; + default: + throw new IllegalArgumentException("Unexpected value '" + unit + "'"); + } + } + } + + private Unit unit; + + public Duration() { + } + + public Integer getValue() { + return value; + } + + public Duration setValue(Integer value) { + this.value = value; + return this; + } + + public Unit getUnit() { + return unit; + } + + public Duration setUnit(Unit unit) { + this.unit = unit; + return this; + } + + public static DurationDTO toDurationDTO(Duration duration) { + if (duration != null) + return new DurationDTO() + .value(duration.getValue()) + .unit(Unit.toDurationDTOUnit(duration.unit)); + else return null; + } + + public static Duration fromDurationDTO(DurationDTO dto) { + if (dto != null) + return new Duration() + .setValue(dto.getValue()) + .setUnit(Unit.fromDurationDTOUnit(dto.getUnit())); + else return null; + } + + @Override + public String toString() { + return "Duration{" + + "offset=" + value + + ", unit=" + unit + + '}'; + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/Event.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java similarity index 73% rename from studymanager/src/main/java/io/redlink/more/studymanager/model/Event.java rename to studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java index 6ca3a545..dab71fb0 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/Event.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java @@ -1,12 +1,19 @@ -package io.redlink.more.studymanager.model; +package io.redlink.more.studymanager.model.scheduler; import java.time.Instant; -public class Event { +public class Event implements ScheduleEvent { + public static final String TYPE = "Event"; + private String type; private Instant dateStart; private Instant dateEnd; private RecurrenceRule recurrenceRule; + @Override + public String getType() { + return TYPE; + } + public Instant getDateStart() { return dateStart; } @@ -33,4 +40,6 @@ public Event setRRule(RecurrenceRule recurrenceRule) { this.recurrenceRule = recurrenceRule; return this; } + + } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/RecurrenceRule.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java similarity index 97% rename from studymanager/src/main/java/io/redlink/more/studymanager/model/RecurrenceRule.java rename to studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java index b88d5a00..00e68eb3 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/RecurrenceRule.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java @@ -1,4 +1,4 @@ -package io.redlink.more.studymanager.model; +package io.redlink.more.studymanager.model.scheduler; import java.time.Instant; import java.util.List; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java new file mode 100644 index 00000000..f324e44c --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java @@ -0,0 +1,28 @@ +package io.redlink.more.studymanager.model.scheduler; + +public class RelativeDate { + + private Duration offset; + private String time; + + public RelativeDate() { + } + + public Duration getOffset() { + return offset; + } + + public RelativeDate setOffset(Duration offset) { + this.offset = offset; + return this; + } + + public String getTime() { + return time; + } + + public RelativeDate setTime(String time) { + this.time = time; + return this; + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java new file mode 100644 index 00000000..5a7ef139 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java @@ -0,0 +1,57 @@ +package io.redlink.more.studymanager.model.scheduler; + +import io.redlink.more.studymanager.api.v1.model.RelativeDateDTO; +import io.redlink.more.studymanager.api.v1.model.RelativeRecurrenceRuleDTO; + +public class RelativeEvent implements ScheduleEvent { + + public static final String TYPE = "RelativeEvent"; + + private String type; + + private RelativeDate dtstart; + + private RelativeDate dtend; + + private RelativeRecurrenceRule rrrule; + + public RelativeEvent() { + } + + @Override + public String getType() { + return TYPE; + } + + public RelativeEvent setType(String type) { + this.type = type; + return this; + } + + public RelativeDate getDtstart() { + return dtstart; + } + + public RelativeEvent setDtstart(RelativeDate dtstart) { + this.dtstart = dtstart; + return this; + } + + public RelativeDate getDtend() { + return dtend; + } + + public RelativeEvent setDtend(RelativeDate dtend) { + this.dtend = dtend; + return this; + } + + public RelativeRecurrenceRule getRrrule() { + return rrrule; + } + + public RelativeEvent setRrrule(RelativeRecurrenceRule rrrule) { + this.rrrule = rrrule; + return this; + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java new file mode 100644 index 00000000..abfaf462 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java @@ -0,0 +1,29 @@ +package io.redlink.more.studymanager.model.scheduler; + +public class RelativeRecurrenceRule { + + private Duration frequency; + + private Duration endAfter; + + public RelativeRecurrenceRule() { + } + + public Duration getFrequency() { + return frequency; + } + + public RelativeRecurrenceRule setFrequency(Duration frequency) { + this.frequency = frequency; + return this; + } + + public Duration getEndAfter() { + return endAfter; + } + + public RelativeRecurrenceRule setEndAfter(Duration endAfter) { + this.endAfter = endAfter; + return this; + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java new file mode 100644 index 00000000..079867d0 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java @@ -0,0 +1,18 @@ +package io.redlink.more.studymanager.model.scheduler; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties( + value = "type", // ignore manually set type, it will be automatically generated by Jackson during serialization + allowSetters = true // allows the type to be set during deserialization +) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true, defaultImpl = Event.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = Event.class, name = Event.TYPE), + @JsonSubTypes.Type(value = RelativeEvent.class, name = RelativeEvent.TYPE) +}) +public interface ScheduleEvent { + public String getType(); +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java index 4ed3a42b..029bccc3 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java @@ -1,32 +1,62 @@ package io.redlink.more.studymanager.model.transformer; -import io.redlink.more.studymanager.api.v1.model.EventDTO; -import io.redlink.more.studymanager.api.v1.model.FrequencyDTO; -import io.redlink.more.studymanager.api.v1.model.RecurrenceRuleDTO; -import io.redlink.more.studymanager.api.v1.model.WeekdayDTO; -import io.redlink.more.studymanager.model.Event; -import io.redlink.more.studymanager.model.RecurrenceRule; +import io.redlink.more.studymanager.api.v1.model.*; +import io.redlink.more.studymanager.model.scheduler.*; public final class EventTransformer { private EventTransformer() { } - public static Event fromEventDTO_V1(EventDTO dto) { - if (dto != null) - return new Event() - .setDateStart(Transformers.toInstant(dto.getDtstart())) - .setDateEnd(Transformers.toInstant(dto.getDtend())) - .setRRule(fromRecurrenceRuleDTO(dto.getRrule())); + public static ScheduleEvent fromObservationScheduleDTO_V1(ObservationScheduleDTO genericDto) { + if (genericDto != null) { + if(genericDto.getType() == null || Event.TYPE.equals(genericDto.getType())) { + EventDTO dto = (EventDTO) genericDto; + return new Event() + .setDateStart(Transformers.toInstant(dto.getDtstart())) + .setDateEnd(Transformers.toInstant(dto.getDtend())) + .setRRule(fromRecurrenceRuleDTO(dto.getRrule())); + } else if(RelativeEvent.TYPE.equals(genericDto.getType())) { + RelativeEventDTO dto = (RelativeEventDTO) genericDto; + return new RelativeEvent() + .setDtstart(new RelativeDate() + .setOffset(fromDurationDTO(dto.getDtstart().getOffset())) + .setTime(dto.getDtstart().getTime())) + .setDtend(new RelativeDate() + .setOffset(fromDurationDTO(dto.getDtend().getOffset())) + .setTime(dto.getDtend().getTime())) + .setRrrule(fromRelativeRecurrenceRuleDTO(dto.getRrrule())); + + } else { + throw new IllegalArgumentException("Unknown Event Type: " + genericDto.getType()); + } + } else return null; } - public static EventDTO toEventDTO_V1(Event event) { + public static ObservationScheduleDTO toObservationScheduleDTO_V1(ScheduleEvent event) { if (event != null) - return new EventDTO() - .dtstart(Transformers.toOffsetDateTime(event.getDateStart())) - .dtend(Transformers.toOffsetDateTime(event.getDateEnd())) - .rrule(toRecurrenceRuleDTO(event.getRRule())); + if(event.getType() == null || Event.TYPE.equals(event.getType())) { + Event e = (Event) event; + return new EventDTO() + .type(Event.TYPE) + .dtstart(Transformers.toOffsetDateTime(e.getDateStart())) + .dtend(Transformers.toOffsetDateTime(e.getDateEnd())) + .rrule(toRecurrenceRuleDTO(e.getRRule())); + } else if(RelativeEvent.TYPE.equals(event.getType())) { + RelativeEvent e = (RelativeEvent) event; + return new RelativeEventDTO() + .type(RelativeEvent.TYPE) + .dtstart(new RelativeDateDTO() + .offset(toDurationDTO(e.getDtstart().getOffset())) + .time(e.getDtstart().getTime())) + .dtend(new RelativeDateDTO() + .offset(toDurationDTO(e.getDtend().getOffset())) + .time(e.getDtend().getTime())) + .rrrule(toRelativeRecurrenceRuleDTO(e.getRrrule())); + } else { + throw new IllegalArgumentException("Unknown Event Type: " + event.getType()); + } else return null; } @@ -57,4 +87,36 @@ private static RecurrenceRuleDTO toRecurrenceRuleDTO(RecurrenceRule recurrenceRu .bysetpos(recurrenceRule.getBySetPos()); else return null; } + + private static RelativeRecurrenceRuleDTO toRelativeRecurrenceRuleDTO(RelativeRecurrenceRule relativeRecurrenceRule) { + if (relativeRecurrenceRule != null) + return new RelativeRecurrenceRuleDTO() + .frequency(toDurationDTO(relativeRecurrenceRule.getFrequency())) + .endAfter(toDurationDTO(relativeRecurrenceRule.getEndAfter())); + else return null; + } + + private static RelativeRecurrenceRule fromRelativeRecurrenceRuleDTO(RelativeRecurrenceRuleDTO dto) { + if (dto != null) + return new RelativeRecurrenceRule() + .setFrequency(fromDurationDTO(dto.getFrequency())) + .setEndAfter(fromDurationDTO(dto.getEndAfter())); + else return null; + } + + private static Duration fromDurationDTO(DurationDTO dto) { + if (dto != null) + return new Duration() + .setValue(dto.getValue()) + .setUnit(dto.getUnit() != null ? Duration.Unit.fromDurationDTOUnit(dto.getUnit()) : null); + else return null; + } + + private static DurationDTO toDurationDTO(Duration duration) { + if (duration != null) + return new DurationDTO() + .value(duration.getValue()) + .unit(duration.getUnit() != null ? Duration.Unit.toDurationDTOUnit(duration.getUnit()) : null); + else return null; + } } 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 95943f04..2764cf30 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 @@ -15,7 +15,7 @@ public static Intervention fromInterventionDTO_V1(InterventionDTO dto) { .setTitle(dto.getTitle()) .setPurpose(dto.getPurpose()) .setStudyGroupId(dto.getStudyGroupId()) - .setSchedule(EventTransformer.fromEventDTO_V1(dto.getSchedule())); + .setSchedule(EventTransformer.fromObservationScheduleDTO_V1(dto.getSchedule())); } public static InterventionDTO toInterventionDTO_V1(Intervention intervention) { @@ -25,7 +25,7 @@ public static InterventionDTO toInterventionDTO_V1(Intervention intervention) { .title(intervention.getTitle()) .purpose(intervention.getPurpose()) .studyGroupId(intervention.getStudyGroupId()) - .schedule(EventTransformer.toEventDTO_V1(intervention.getSchedule())) + .schedule(EventTransformer.toObservationScheduleDTO_V1(intervention.getSchedule())) .created(Transformers.toOffsetDateTime(intervention.getCreated())) .modified(Transformers.toOffsetDateTime(intervention.getModified())); } 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 fc21ab5c..5e6881fa 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 @@ -20,7 +20,7 @@ public static Observation fromObservationDTO_V1(ObservationDTO dto) { .setType(dto.getType()) .setStudyGroupId(dto.getStudyGroupId()) .setProperties(MapperUtils.MAPPER.convertValue(dto.getProperties(), ObservationProperties.class)) - .setSchedule(EventTransformer.fromEventDTO_V1(dto.getSchedule())) + .setSchedule(EventTransformer.fromObservationScheduleDTO_V1(dto.getSchedule())) .setHidden(dto.getHidden()) .setNoSchedule(dto.getNoSchedule()); } @@ -35,7 +35,7 @@ public static ObservationDTO toObservationDTO_V1(Observation observation) { .type(observation.getType()) .studyGroupId(observation.getStudyGroupId()) .properties(observation.getProperties()) - .schedule(EventTransformer.toEventDTO_V1(observation.getSchedule())) + .schedule(EventTransformer.toObservationScheduleDTO_V1(observation.getSchedule())) .created(Transformers.toOffsetDateTime(observation.getCreated())) .modified(Transformers.toOffsetDateTime(observation.getModified())) .hidden(observation.getHidden()) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java index 75894520..fecc9f3b 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java @@ -4,7 +4,7 @@ import io.redlink.more.studymanager.core.properties.TriggerProperties; import io.redlink.more.studymanager.exception.BadRequestException; import io.redlink.more.studymanager.model.Action; -import io.redlink.more.studymanager.model.Event; +import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.model.Intervention; import io.redlink.more.studymanager.model.Trigger; import io.redlink.more.studymanager.utils.MapperUtils; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java index fbf59479..4326513a 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.exception.BadRequestException; -import io.redlink.more.studymanager.model.Event; +import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.model.Observation; import io.redlink.more.studymanager.utils.MapperUtils; import java.util.List; diff --git a/studymanager/src/main/resources/openapi/Event.yaml b/studymanager/src/main/resources/openapi/Event.yaml index ff98614c..a8a949a4 100644 --- a/studymanager/src/main/resources/openapi/Event.yaml +++ b/studymanager/src/main/resources/openapi/Event.yaml @@ -9,6 +9,9 @@ components: Event: type: object properties: + type: + type: string + default: Event dtstart: type: string format: date-time diff --git a/studymanager/src/main/resources/openapi/RelativeEvent.yaml b/studymanager/src/main/resources/openapi/RelativeEvent.yaml new file mode 100644 index 00000000..90898a22 --- /dev/null +++ b/studymanager/src/main/resources/openapi/RelativeEvent.yaml @@ -0,0 +1,63 @@ +openapi: "3.0.3" +info: + title: TimeRange Model for Relative Events + version: "1.0" + +components: + schemas: + Duration: + type: object + description: A duration of time + properties: + value: + type: integer + description: number of units + unit: + type: string + description: unit of time + enum: + - MINUTE + - HOUR + - DAY + RelativeDate: + type: object + description: A date relative to a specific base date (e.g study start) + properties: + offset: + $ref: '#/components/schemas/Duration' + time: + type: string + format: time + description: Follows ISO 8601 format for time + RelativeRecurrenceRule: + type: object + description: A recurrence rule relative to dtstart + properties: + frequency: + $ref: '#/components/schemas/Duration' + description: How often to repeat + endAfter: + $ref: '#/components/schemas/Duration' + description: How long to repeat + + RelativeEvent: + type: object + description: An event that occurs at a relative time + required: + - type + - dtstart + - dtend + properties: + type: + type: string + default: RelativeEvent + dtstart: + $ref: '#/components/schemas/RelativeDate' + description: When the event starts + dtend: + $ref: '#/components/schemas/RelativeDate' + description: When the event ends + rrrule: + $ref: '#/components/schemas/RelativeRecurrenceRule' + description: How to repeat the event + diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index e1f8414c..e9556076 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -1422,7 +1422,16 @@ components: type: object additionalProperties: true schedule: - $ref: './Event.yaml/#/components/schemas/Event' + oneOf: + - $ref: './Event.yaml/#/components/schemas/Event' + - $ref: './RelativeEvent.yaml/#/components/schemas/RelativeEvent' + discriminator: + propertyName: type + # mapping: + # event: + # $ref: './Event.yaml/#/components/schemas/Event' + # relativeEvent: + # $ref: './RelativeEvent.yaml/#/components/schemas/RelativeEvent' created: type: string format: date-time @@ -1514,7 +1523,11 @@ components: purpose: type: string schedule: - $ref: './Event.yaml/#/components/schemas/Event' + oneOf: + - $ref: './Event.yaml/#/components/schemas/Event' + - $ref: './RelativeEvent.yaml/#/components/schemas/RelativeEvent' + discriminator: + propertyName: type trigger: $ref: '#/components/schemas/Trigger' actions: diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ImportExportControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ImportExportControllerTest.java index f7840ae3..31bddbc7 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ImportExportControllerTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ImportExportControllerTest.java @@ -5,6 +5,8 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.properties.TriggerProperties; import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; import io.redlink.more.studymanager.repository.DownloadTokenRepository; import io.redlink.more.studymanager.service.ImportExportService; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; 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 390dbbaa..8886178a 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 @@ -8,7 +8,7 @@ import io.redlink.more.studymanager.core.properties.TriggerProperties; import io.redlink.more.studymanager.model.Action; import io.redlink.more.studymanager.model.AuthenticatedUser; -import io.redlink.more.studymanager.model.Event; +import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.model.Intervention; import io.redlink.more.studymanager.model.PlatformRole; import io.redlink.more.studymanager.model.Trigger; 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 7f776cbe..18be3b38 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 @@ -3,7 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.redlink.more.studymanager.api.v1.model.EventDTO; import io.redlink.more.studymanager.api.v1.model.ObservationDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationScheduleDTO; import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.service.IntegrationService; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; import io.redlink.more.studymanager.service.ObservationService; @@ -136,7 +138,7 @@ void testEmptySchedule() throws Exception { ObservationDTO observationRequest = new ObservationDTO() .studyId(1L) .title("a different title") - .schedule(MapperUtils.readValue(new HashMap(), EventDTO.class)) + .schedule(MapperUtils.readValue("{\"type\":\"Event\"}", ObservationScheduleDTO.class)) .observationId(1); mvc.perform(post("/api/v1/studies/1/observations") @@ -146,7 +148,7 @@ void testEmptySchedule() throws Exception { .andExpect(status().isCreated()) .andExpect(jsonPath("$.title").value("title")) .andExpect(jsonPath("$.studyId").value(observationRequest.getStudyId())) - .andExpect(jsonPath("$.schedule").value(MapperUtils.readValue(new HashMap(), EventDTO.class))) + .andExpect(jsonPath("$.schedule").value(MapperUtils.readValue("{\"type\":\"Event\"}", ObservationScheduleDTO.class))) .andExpect(jsonPath("$.modified").exists()) .andExpect(jsonPath("$.created").exists()); } diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/IntegrationRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/IntegrationRepositoryTest.java index af716414..7027d205 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/IntegrationRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/IntegrationRepositoryTest.java @@ -2,6 +2,8 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java index 77406a68..37552fcd 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/InterventionRepositoryTest.java @@ -3,6 +3,8 @@ import io.redlink.more.studymanager.core.properties.ActionProperties; import io.redlink.more.studymanager.model.*; import io.redlink.more.studymanager.core.properties.TriggerProperties; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; import io.redlink.more.studymanager.utils.MapperUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -64,7 +66,7 @@ void testInsertListUpdateDelete() { assertThat(interventionResponse.getInterventionId()).isNotNull(); assertThat(interventionResponse.getTitle()).isEqualTo(intervention.getTitle()); - assertThat(interventionResponse.getSchedule().getDateStart()).isEqualTo(startTime); + assertThat(((Event)interventionResponse.getSchedule()).getDateStart()).isEqualTo(startTime); assertThat(MapperUtils.writeValueAsString(interventionResponse.getSchedule())) .isEqualTo(MapperUtils.writeValueAsString(intervention.getSchedule())); diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java index 82a2f8af..fb6e9061 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java @@ -2,6 +2,8 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; import io.redlink.more.studymanager.utils.MapperUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; 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 5ec21784..2c8741f3 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 @@ -4,6 +4,7 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.properties.TriggerProperties; import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.scheduler.Event; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/transformer/ScheduleEventTransformerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/transformer/ScheduleEventTransformerTest.java new file mode 100644 index 00000000..c705673a --- /dev/null +++ b/studymanager/src/test/java/io/redlink/more/studymanager/transformer/ScheduleEventTransformerTest.java @@ -0,0 +1,107 @@ +package io.redlink.more.studymanager.transformer; + +import co.elastic.clients.elasticsearch.watcher.Day; +import io.redlink.more.studymanager.api.v1.model.EventDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationScheduleDTO; +import io.redlink.more.studymanager.api.v1.model.RelativeEventDTO; +import io.redlink.more.studymanager.model.scheduler.*; +import io.redlink.more.studymanager.model.transformer.EventTransformer; +import io.redlink.more.studymanager.utils.MapperUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; + +public class ScheduleEventTransformerTest { + + @Test + public void testJsonToEventTransformer() { + String jsonEvent = "{\"rrule\": null, \"dateEnd\": 1683755999.000000000, \"dateStart\": 1683669600.000000000}"; + + String jsonRelativeEvent1 = """ +{ + "type":"RelativeEvent", + "dtstart":{ + "offset":{ + "value":1, + "unit":"DAY" + }, + "time":"12:00:00" + }, + "dtend":{ + "offset":{ + "value":2, + "unit":"DAY" + }, + "time":"12:00:00" + } +} + """; + + String jsonRelativeEvent2 = """ +{ + "type":"RelativeEvent", + "dtstart":{ + "offset":{ + "value":1, + "unit":"DAY" + }, + "time":"12:00:00" + }, + "dtend":{ + "offset":{ + "value":2, + "unit":"DAY" + }, + "time":"12:00:00" + }, + "rrrule":{ + "frequency":{ + "unit":"DAY", + "value":1 + }, + "endAfter":{ + "unit":"DAY", + "value":10 + } + } +} + """; + + ScheduleEvent event = MapperUtils.readValue(jsonEvent, ScheduleEvent.class); + Assertions.assertTrue(event instanceof Event); + + ScheduleEvent eventRelative1 = MapperUtils.readValue(jsonRelativeEvent1, ScheduleEvent.class); + Assertions.assertTrue(eventRelative1 instanceof RelativeEvent); + + ScheduleEvent eventRelative2 = MapperUtils.readValue(jsonRelativeEvent2, ScheduleEvent.class); + Assertions.assertTrue(eventRelative2 instanceof RelativeEvent); + } + + @Test + public void testDTOTransformer() { + Event event = new Event() + .setDateStart(Instant.now().plus(1, ChronoUnit.DAYS)) + .setDateEnd(Instant.now().plus(2, ChronoUnit.DAYS)); + + RelativeEvent relativeEvent = new RelativeEvent() + .setDtstart(new RelativeDate() + .setTime("12:00:00") + .setOffset(new Duration().setUnit(Duration.Unit.DAY).setValue(3))) + .setDtend(new RelativeDate() + .setTime("12:00:00") + .setOffset(new Duration().setUnit(Duration.Unit.DAY).setValue(4))) + .setRrrule(new RelativeRecurrenceRule() + .setFrequency(new Duration().setUnit(Duration.Unit.DAY).setValue(1)) + .setEndAfter(new Duration().setUnit(Duration.Unit.DAY).setValue(10))); + + ObservationScheduleDTO eventDTO = EventTransformer.toObservationScheduleDTO_V1(event); + ObservationScheduleDTO relativeEventDTO = EventTransformer.toObservationScheduleDTO_V1(relativeEvent); + + Assertions.assertTrue(eventDTO instanceof EventDTO); + Assertions.assertTrue(relativeEventDTO instanceof RelativeEventDTO); + + } +} From 84d9df613b16e1813a0637e17ce0da8f072e5864 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Wed, 8 Nov 2023 10:12:16 +0100 Subject: [PATCH 02/16] MORE2-3 extend API studies and groups to duration --- .../main/resources/openapi/StudyManagerAPI.yaml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index e9556076..20ea802c 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -1277,6 +1277,8 @@ components: type: string consentInfo: type: string + duration: + $ref: '#/components/schemas/StudyDuration' finishText: type: string status: @@ -1354,6 +1356,8 @@ components: type: string purpose: type: string + duration: + $ref: '#/components/schemas/StudyDuration' numberOfParticipants: type: integer readOnly: true @@ -1365,7 +1369,18 @@ components: type: string format: date-time readOnly: true - + StudyDuration: + type: object + properties: + value: + type: number + unit: + type: string + description: unit of time + enum: + - MINUTE + - HOUR + - DAY Participant: type: object properties: From 06ed71bb6f26fbbe32efde0d1e95d84a5935d109 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 9 Nov 2023 12:38:37 +0100 Subject: [PATCH 03/16] MORE2-3 add duration to study and study group --- .../more/studymanager/model/Study.java | 13 ++++++++++++ .../model/transformer/StudyTransformer.java | 20 +++++++++++++++---- .../repository/ObservationRepository.java | 3 ++- .../repository/StudyRepository.java | 11 +++++++--- ..._add_duration_to_study_and_study_group.sql | 5 +++++ .../resources/openapi/StudyManagerAPI.yaml | 2 +- .../repository/ObservationRepositoryTest.java | 10 ++++++++-- .../repository/StudyRepositoryTest.java | 13 +++++++++++- 8 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 studymanager/src/main/resources/db/migration/V1_12_0__add_duration_to_study_and_study_group.sql diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/Study.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Study.java index f680ef53..c7c7a043 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/Study.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/Study.java @@ -1,5 +1,7 @@ package io.redlink.more.studymanager.model; +import io.redlink.more.studymanager.model.scheduler.Duration; + import java.time.Instant; import java.time.LocalDate; import java.util.Set; @@ -11,6 +13,7 @@ public class Study { private String participantInfo; private String consentInfo; private String finishText; + private Duration duration; private Status studyState; private LocalDate startDate; private LocalDate endDate; @@ -92,6 +95,15 @@ public Study setFinishText(String finishText) { return this; } + public Duration getDuration() { + return duration; + } + + public Study setDuration(Duration duration) { + this.duration = duration; + return this; + } + public Status getStudyState() { return studyState; } @@ -184,6 +196,7 @@ public String toString() { ", endDate=" + endDate + ", plannedStartDate=" + plannedStartDate + ", plannedEndDate=" + plannedEndDate + + ", duration=" + duration + ", created=" + created + ", modified=" + modified + ", institute=" + contact.getInstitute() + diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java index bb40f227..6371c8ce 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java @@ -1,11 +1,9 @@ package io.redlink.more.studymanager.model.transformer; -import io.redlink.more.studymanager.api.v1.model.ContactDTO; -import io.redlink.more.studymanager.api.v1.model.StatusChangeDTO; -import io.redlink.more.studymanager.api.v1.model.StudyDTO; -import io.redlink.more.studymanager.api.v1.model.StudyStatusDTO; +import io.redlink.more.studymanager.api.v1.model.*; import io.redlink.more.studymanager.model.Contact; import io.redlink.more.studymanager.model.Study; +import io.redlink.more.studymanager.model.scheduler.Duration; public class StudyTransformer { @@ -22,12 +20,19 @@ public static Study fromStudyDTO_V1(StudyDTO studyDTO) { .setPurpose(studyDTO.getPurpose()) .setFinishText(studyDTO.getFinishText()) .setParticipantInfo(studyDTO.getParticipantInfo()) + .setDuration(toDuration(studyDTO.getDuration())) .setConsentInfo(studyDTO.getConsentInfo()) .setPlannedStartDate(studyDTO.getPlannedStart()) .setPlannedEndDate(studyDTO.getPlannedEnd()) .setContact(ContactTransformer.fromContactDTO_V1(studyDTO.getContact())); } + private static Duration toDuration(StudyDurationDTO duration) { + return new Duration() + .setValue(duration.getValue()) + .setUnit(Duration.Unit.valueOf(duration.getUnit().getValue().toUpperCase())); + } + public static StudyDTO toStudyDTO_V1(Study study) { if(study.getContact() == null) study.setContact(new Contact()); @@ -37,6 +42,7 @@ public static StudyDTO toStudyDTO_V1(Study study) { .purpose(study.getPurpose()) .finishText(study.getFinishText()) .participantInfo(study.getParticipantInfo()) + .duration(toStudyDurationDTO(study.getDuration())) .consentInfo(study.getConsentInfo()) .status(StudyStatusDTO.fromValue(study.getStudyState().getValue())) .start(study.getStartDate()) @@ -52,4 +58,10 @@ public static StudyDTO toStudyDTO_V1(Study study) { public static Study.Status fromStatusChangeDTO_V1(StatusChangeDTO statusChangeDTO) { return Study.Status.valueOf(statusChangeDTO.getStatus().getValue().toUpperCase()); } + + public static StudyDurationDTO toStudyDurationDTO(Duration duration) { + return new StudyDurationDTO() + .value(duration.getValue()) + .unit(StudyDurationDTO.UnitEnum.fromValue(duration.getUnit().getValue())); + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java index 4326513a..0ab6c796 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java @@ -5,6 +5,7 @@ import io.redlink.more.studymanager.exception.BadRequestException; import io.redlink.more.studymanager.model.scheduler.Event; 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.List; import java.util.Optional; @@ -137,7 +138,7 @@ private static RowMapper getObservationRowMapper() { .setType(rs.getString("type")) .setStudyGroupId(getValidNullableIntegerValue(rs, "study_group_id")) .setProperties(MapperUtils.readValue(rs.getString("properties"), ObservationProperties.class)) - .setSchedule(MapperUtils.readValue(rs.getString("schedule"), Event.class)) + .setSchedule(MapperUtils.readValue(rs.getString("schedule"), ScheduleEvent.class)) .setCreated(RepositoryUtils.readInstant(rs, "created")) .setModified(RepositoryUtils.readInstant(rs, "modified")) .setHidden(rs.getBoolean("hidden")) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyRepository.java index 3d28ec08..af129768 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyRepository.java @@ -7,6 +7,9 @@ import java.util.List; import java.util.Optional; import java.util.Set; + +import io.redlink.more.studymanager.model.scheduler.Duration; +import io.redlink.more.studymanager.utils.MapperUtils; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -17,8 +20,8 @@ public class StudyRepository { private static final String INSERT_STUDY = - "INSERT INTO studies (title,purpose,participant_info,consent_info,finish_text,planned_start_date,planned_end_date,institute,contact_person,contact_email,contact_phone) " + - "VALUES (:title,:purpose,:participant_info,:consent_info,:finish_text,:planned_start_date,:planned_end_date,:institute,:contact_person,:contact_email,:contact_phone) " + + "INSERT INTO studies (title,purpose,participant_info,consent_info,finish_text,planned_start_date,planned_end_date,duration,institute,contact_person,contact_email,contact_phone) " + + "VALUES (:title,:purpose,:participant_info,:consent_info,:finish_text,:planned_start_date,:planned_end_date,:duration::jsonb,:institute,:contact_person,:contact_email,:contact_phone) " + "RETURNING *"; private static final String GET_STUDY_BY_ID = "SELECT *, " + @@ -40,7 +43,7 @@ public class StudyRepository { "ORDER BY modified DESC"; private static final String UPDATE_STUDY = "UPDATE studies SET title = :title, purpose = :purpose, participant_info = :participant_info, consent_info = :consent_info, finish_text = :finish_text, planned_start_date = :planned_start_date, " + - "planned_end_date = :planned_end_date, modified = now(), institute = :institute, contact_person = :contact_person, contact_email = :contact_email, contact_phone = :contact_phone " + + "planned_end_date = :planned_end_date, duration = :duration::jsonb, modified = now(), institute = :institute, contact_person = :contact_person, contact_email = :contact_email, contact_phone = :contact_phone " + "WHERE study_id = :study_id " + "RETURNING *, (SELECT user_roles FROM study_roles_by_user WHERE study_roles_by_user.study_id = studies.study_id AND user_id = :userId) AS user_roles"; @@ -127,6 +130,7 @@ private static MapSqlParameterSource studyToParams(Study study) { .addValue("finish_text", study.getFinishText()) .addValue("planned_start_date", study.getPlannedStartDate()) .addValue("planned_end_date", study.getPlannedEndDate()) + .addValue("duration", MapperUtils.writeValueAsString(study.getDuration())) .addValue("institute", study.getContact().getInstitute()) .addValue("contact_person", study.getContact().getPerson()) .addValue("contact_email", study.getContact().getEmail()) @@ -146,6 +150,7 @@ private static RowMapper getStudyRowMapper() { .setPlannedEndDate(RepositoryUtils.readLocalDate(rs,"planned_end_date")) .setStartDate(RepositoryUtils.readLocalDate(rs,"start_date")) .setEndDate(RepositoryUtils.readLocalDate(rs,"end_date")) + .setDuration(MapperUtils.readValue(rs.getString("duration"), Duration.class)) .setCreated(RepositoryUtils.readInstant(rs, "created")) .setModified(RepositoryUtils.readInstant(rs, "modified")) .setStudyState(Study.Status.valueOf(rs.getString("status").toUpperCase())) diff --git a/studymanager/src/main/resources/db/migration/V1_12_0__add_duration_to_study_and_study_group.sql b/studymanager/src/main/resources/db/migration/V1_12_0__add_duration_to_study_and_study_group.sql new file mode 100644 index 00000000..f82b42d3 --- /dev/null +++ b/studymanager/src/main/resources/db/migration/V1_12_0__add_duration_to_study_and_study_group.sql @@ -0,0 +1,5 @@ +ALTER TABLE studies + ADD COLUMN duration JSONB; + +ALTER TABLE study_groups + ADD COLUMN duration JSONB; diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index 20ea802c..db7e621f 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -1373,7 +1373,7 @@ components: type: object properties: value: - type: number + type: integer unit: type: string description: unit of time diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java index fb6e9061..aed54b1e 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ObservationRepositoryTest.java @@ -2,8 +2,7 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.model.*; -import io.redlink.more.studymanager.model.scheduler.Event; -import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; +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; @@ -96,6 +95,13 @@ public void testInsertListUpdateDelete() { assertThat((observationRepository.listObservations(studyId)).size()).isEqualTo(1); observationRepository.deleteObservation(studyId, observationResponse2.getObservationId()); assertThat((observationRepository.listObservations(studyId)).size()).isEqualTo(0); + + observation.setSchedule(new RelativeEvent() + .setDtstart(new RelativeDate().setOffset(new Duration().setValue(1).setUnit(Duration.Unit.DAY)).setTime("12:00:00")) + .setDtend(new RelativeDate().setOffset(new Duration().setValue(2).setUnit(Duration.Unit.DAY)).setTime("13:00:00"))); + + Observation observationResponse3 = observationRepository.insert(observation); + assertThat(observationResponse3.getSchedule()).isInstanceOf(RelativeEvent.class); } @Test diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/StudyRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/StudyRepositoryTest.java index 4a2f6f64..e8399e34 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/StudyRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/StudyRepositoryTest.java @@ -2,6 +2,7 @@ import io.redlink.more.studymanager.model.Contact; import io.redlink.more.studymanager.model.Study; +import io.redlink.more.studymanager.model.scheduler.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -56,6 +57,7 @@ void testInsert() { void testUpdate() { Study insert = new Study() .setTitle("some title") + .setDuration(new Duration().setValue(1).setUnit(Duration.Unit.DAY)) .setContact(new Contact().setPerson("test").setEmail("test")); Study inserted = studyRepository.insert(insert); @@ -63,13 +65,13 @@ void testUpdate() { Study update = new Study() .setStudyId(inserted.getStudyId()) .setTitle("some new title") + .setDuration(new Duration().setValue(2).setUnit(Duration.Unit.HOUR)) .setContact(new Contact().setPerson("test2").setEmail("test2")); Optional optUpdated = studyRepository.update(update, null); assertThat(optUpdated).isPresent(); Study updated = optUpdated.get(); - Optional optQueried = studyRepository.getById(inserted.getStudyId()); assertThat(optQueried).isPresent(); Study queried = optQueried.get(); @@ -81,11 +83,20 @@ void testUpdate() { assertThat(queried.getContact().getEmail()).isEqualTo(updated.getContact().getEmail()); assertThat(update.getTitle()).isEqualTo(updated.getTitle()); + assertThat(update.getDuration().getValue()).isEqualTo(updated.getDuration().getValue()); + assertThat(update.getDuration().getUnit()).isEqualTo(updated.getDuration().getUnit()); assertThat(inserted.getStudyId()).isEqualTo(updated.getStudyId()); assertThat(inserted.getCreated()).isEqualTo(updated.getCreated()); assertThat(inserted.getModified().toEpochMilli()).isLessThan(updated.getModified().toEpochMilli()); assertThat(inserted.getContact().getPerson()).isNotEqualTo(updated.getContact().getPerson()); assertThat(inserted.getContact().getEmail()).isNotEqualTo(updated.getContact().getEmail()); + + Study insert_no_duration = new Study() + .setTitle("some title") + .setContact(new Contact().setPerson("test").setEmail("test")); + + Study inserted_no_duration = studyRepository.insert(insert_no_duration); + assertThat(inserted_no_duration.getDuration()).isNull(); } @Test From 2662175cec431c0bd43fed166af1647c4f3f4244 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 9 Nov 2023 13:14:34 +0100 Subject: [PATCH 04/16] Enable manual deployment --- .github/workflows/compile-test-deploy.yml | 71 +++++++++++++++++++++++ .github/workflows/compile-test.yml | 29 --------- 2 files changed, 71 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/compile-test-deploy.yml diff --git a/.github/workflows/compile-test-deploy.yml b/.github/workflows/compile-test-deploy.yml new file mode 100644 index 00000000..b523c8ca --- /dev/null +++ b/.github/workflows/compile-test-deploy.yml @@ -0,0 +1,71 @@ +name: Test, Compile and Deploy +on: + workflow_dispatch: + +jobs: + Compile-and-Test: + name: Compile and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17 + - name: Compile and test project + run: ./mvnw -B -U + --no-transfer-progress + compile test + - name: Show 3rd-Party Licenses + run: | + cat ./studymanager/target/generated-sources/license/THIRD-PARTY.txt + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v3 + with: + name: Test Results + path: "**/TEST-*.xml" + - name: Upload Licenses List + if: always() + uses: actions/upload-artifact@v3 + with: + name: Licenses List + path: "./studymanager/target/generated-sources/license/THIRD-PARTY.txt" + + Build-and-Deploy: + name: "Build and Push Docker Image" + runs-on: ubuntu-latest + needs: + - Compile-and-Test + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17 + - name: Build JIB container and publish to GitHub Packages + run: ./mvnw -B -U + --no-transfer-progress + clean deploy + -Drevision=${{github.run_number}} + -Dchangelist= + -Dsha1=.${GITHUB_SHA:0:7} + -Dquick + -Ddocker.namespace=${DOCKER_NAMESPACE,,} + -Djib.to.tags=latest + -Djib.to.auth.username=${{ github.actor }} + -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} + env: + DOCKER_NAMESPACE: ghcr.io/${{ github.repository_owner }} + + event_file: + name: "Event File" + runs-on: ubuntu-latest + steps: + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: Event File + path: ${{ github.event_path }} diff --git a/.github/workflows/compile-test.yml b/.github/workflows/compile-test.yml index 3f6539f0..03de995c 100644 --- a/.github/workflows/compile-test.yml +++ b/.github/workflows/compile-test.yml @@ -1,6 +1,5 @@ name: Test and Compile on: - workflow_dispatch: push: jobs: @@ -34,34 +33,6 @@ jobs: name: Licenses List path: "./studymanager/target/generated-sources/license/THIRD-PARTY.txt" - Build-and-Deploy: - name: "Build and Push Docker Image" - runs-on: ubuntu-latest - if: github.ref_name == 'main' - needs: - - Compile-and-Test - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Build JIB container and publish to GitHub Packages - run: ./mvnw -B -U - --no-transfer-progress - clean deploy - -Drevision=${{github.run_number}} - -Dchangelist= - -Dsha1=.${GITHUB_SHA:0:7} - -Dquick - -Ddocker.namespace=${DOCKER_NAMESPACE,,} - -Djib.to.tags=latest - -Djib.to.auth.username=${{ github.actor }} - -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} - env: - DOCKER_NAMESPACE: ghcr.io/${{ github.repository_owner }} - event_file: name: "Event File" runs-on: ubuntu-latest From db13d1f1d6455d2f7a2057024620b469c6875f6e Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 9 Nov 2023 14:37:16 +0100 Subject: [PATCH 05/16] MORE2-3 fix NPE in duration transformer --- .../model/transformer/StudyTransformer.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java index 6371c8ce..e1e4c63c 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java @@ -5,6 +5,8 @@ import io.redlink.more.studymanager.model.Study; import io.redlink.more.studymanager.model.scheduler.Duration; +import java.util.Optional; + public class StudyTransformer { private StudyTransformer() {} @@ -28,9 +30,10 @@ public static Study fromStudyDTO_V1(StudyDTO studyDTO) { } private static Duration toDuration(StudyDurationDTO duration) { - return new Duration() - .setValue(duration.getValue()) - .setUnit(Duration.Unit.valueOf(duration.getUnit().getValue().toUpperCase())); + return Optional.of(duration).map(d -> new Duration() + .setValue(d.getValue()) + .setUnit(Duration.Unit.valueOf(d.getUnit().getValue().toUpperCase()))) + .orElse(null); } public static StudyDTO toStudyDTO_V1(Study study) { @@ -60,8 +63,9 @@ public static Study.Status fromStatusChangeDTO_V1(StatusChangeDTO statusChangeDT } public static StudyDurationDTO toStudyDurationDTO(Duration duration) { - return new StudyDurationDTO() - .value(duration.getValue()) - .unit(StudyDurationDTO.UnitEnum.fromValue(duration.getUnit().getValue())); + return Optional.ofNullable(duration).map(d -> new StudyDurationDTO() + .value(d.getValue()) + .unit(StudyDurationDTO.UnitEnum.fromValue(d.getUnit().getValue()))) + .orElse(null); } } From c098553e1086aaf962829d4e50c5828598029b19 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 9 Nov 2023 14:48:57 +0100 Subject: [PATCH 06/16] MORE2-3 fix 2nd NPE in duration transformer --- .../more/studymanager/model/transformer/StudyTransformer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java index e1e4c63c..b0463c74 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java @@ -30,7 +30,7 @@ public static Study fromStudyDTO_V1(StudyDTO studyDTO) { } private static Duration toDuration(StudyDurationDTO duration) { - return Optional.of(duration).map(d -> new Duration() + return Optional.ofNullable(duration).map(d -> new Duration() .setValue(d.getValue()) .setUnit(Duration.Unit.valueOf(d.getUnit().getValue().toUpperCase()))) .orElse(null); From 0eea40c04824518c424f6e93f499bb5e90b12dff Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Tue, 14 Nov 2023 16:00:03 +0100 Subject: [PATCH 07/16] MORE2 add start property to participants table --- .../migration/V1_13_0__add_start_timestamp_to_participant.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 studymanager/src/main/resources/db/migration/V1_13_0__add_start_timestamp_to_participant.sql diff --git a/studymanager/src/main/resources/db/migration/V1_13_0__add_start_timestamp_to_participant.sql b/studymanager/src/main/resources/db/migration/V1_13_0__add_start_timestamp_to_participant.sql new file mode 100644 index 00000000..e184a0b7 --- /dev/null +++ b/studymanager/src/main/resources/db/migration/V1_13_0__add_start_timestamp_to_participant.sql @@ -0,0 +1,2 @@ +ALTER TABLE participants + ADD COLUMN start TIMESTAMP; From 9b16a451d72ba1cdddd153c15bf4587ca09c49b6 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 16 Nov 2023 11:31:37 +0100 Subject: [PATCH 08/16] Enable build on push for action testing --- .github/workflows/compile-test-deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/compile-test-deploy.yml b/.github/workflows/compile-test-deploy.yml index b523c8ca..5713ea5c 100644 --- a/.github/workflows/compile-test-deploy.yml +++ b/.github/workflows/compile-test-deploy.yml @@ -1,6 +1,7 @@ name: Test, Compile and Deploy on: workflow_dispatch: + push: jobs: Compile-and-Test: From 4bae8ad71002019b74443a62aff1b92bc8c3c1c9 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 16 Nov 2023 11:32:24 +0100 Subject: [PATCH 09/16] Revert changes for testing --- .github/workflows/compile-test-deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/compile-test-deploy.yml b/.github/workflows/compile-test-deploy.yml index 5713ea5c..b523c8ca 100644 --- a/.github/workflows/compile-test-deploy.yml +++ b/.github/workflows/compile-test-deploy.yml @@ -1,7 +1,6 @@ name: Test, Compile and Deploy on: workflow_dispatch: - push: jobs: Compile-and-Test: From 6a313cb528f9bbfb252ee0e584226ef283efc5f9 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Fri, 17 Nov 2023 19:20:51 +0100 Subject: [PATCH 10/16] MORE2 store duration for study groups --- .../more/studymanager/model/StudyGroup.java | 12 ++++++ .../model/scheduler/Duration.java | 43 +++++++++++++++++++ .../transformer/StudyGroupTransformer.java | 5 ++- .../model/transformer/StudyTransformer.java | 18 +------- .../repository/StudyGroupRepository.java | 8 +++- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyGroup.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyGroup.java index 89be3a64..daf9fdd5 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyGroup.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyGroup.java @@ -1,5 +1,7 @@ package io.redlink.more.studymanager.model; +import io.redlink.more.studymanager.model.scheduler.Duration; + import java.time.Instant; public class StudyGroup { @@ -7,6 +9,7 @@ public class StudyGroup { private Integer studyGroupId; private String title; private String purpose; + private Duration duration; private Instant created; private Instant modified; @@ -46,6 +49,15 @@ public StudyGroup setPurpose(String purpose) { return this; } + public Duration getDuration() { + return duration; + } + + public StudyGroup setDuration(Duration duration) { + this.duration = duration; + return this; + } + public Instant getCreated() { return created; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java index 1477ccb7..894044d3 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import io.redlink.more.studymanager.api.v1.model.DurationDTO; +import io.redlink.more.studymanager.api.v1.model.StudyDurationDTO; public class Duration { @@ -67,6 +68,32 @@ public static DurationDTO.UnitEnum toDurationDTOUnit(Unit unit) { throw new IllegalArgumentException("Unexpected value '" + unit + "'"); } } + + public static Unit fromStudyDurationDTOUnit(StudyDurationDTO.UnitEnum unit) { + switch (unit) { + case MINUTE: + return MINUTE; + case HOUR: + return HOUR; + case DAY: + return DAY; + default: + throw new IllegalArgumentException("Unexpected value '" + unit + "'"); + } + } + + public static StudyDurationDTO.UnitEnum toStudyDurationDTOUnit(Unit unit) { + switch (unit) { + case MINUTE: + return StudyDurationDTO.UnitEnum.MINUTE; + case HOUR: + return StudyDurationDTO.UnitEnum.HOUR; + case DAY: + return StudyDurationDTO.UnitEnum.DAY; + default: + throw new IllegalArgumentException("Unexpected value '" + unit + "'"); + } + } } private Unit unit; @@ -92,6 +119,22 @@ public Duration setUnit(Unit unit) { return this; } + public static StudyDurationDTO toStudyDurationDTO(Duration duration) { + if (duration != null) + return new StudyDurationDTO() + .value(duration.getValue()) + .unit(Unit.toStudyDurationDTOUnit(duration.unit)); + else return null; + } + + public static Duration fromStudyDurationDTO(StudyDurationDTO dto) { + if (dto != null) + return new Duration() + .setValue(dto.getValue()) + .setUnit(Unit.fromStudyDurationDTOUnit(dto.getUnit())); + else return null; + } + public static DurationDTO toDurationDTO(Duration duration) { if (duration != null) return new DurationDTO() diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java index 52d5447e..3c8c8fbe 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java @@ -2,6 +2,7 @@ import io.redlink.more.studymanager.api.v1.model.StudyGroupDTO; import io.redlink.more.studymanager.model.StudyGroup; +import io.redlink.more.studymanager.model.scheduler.Duration; public final class StudyGroupTransformer { @@ -13,7 +14,8 @@ public static StudyGroup fromStudyGroupDTO_V1(StudyGroupDTO studyGroupDTO) { .setStudyId(studyGroupDTO.getStudyId()) .setStudyGroupId(studyGroupDTO.getStudyGroupId()) .setTitle(studyGroupDTO.getTitle()) - .setPurpose(studyGroupDTO.getPurpose()); + .setPurpose(studyGroupDTO.getPurpose()) + .setDuration(Duration.fromStudyDurationDTO(studyGroupDTO.getDuration())); } public static StudyGroupDTO toStudyGroupDTO_V1(StudyGroup studyGroup) { @@ -22,6 +24,7 @@ public static StudyGroupDTO toStudyGroupDTO_V1(StudyGroup studyGroup) { .studyGroupId(studyGroup.getStudyGroupId()) .title(studyGroup.getTitle()) .purpose(studyGroup.getPurpose()) + .duration(Duration.toStudyDurationDTO(studyGroup.getDuration())) .created(Transformers.toOffsetDateTime(studyGroup.getCreated())) .modified(Transformers.toOffsetDateTime(studyGroup.getModified())); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java index b0463c74..dd1d4c72 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java @@ -22,20 +22,13 @@ public static Study fromStudyDTO_V1(StudyDTO studyDTO) { .setPurpose(studyDTO.getPurpose()) .setFinishText(studyDTO.getFinishText()) .setParticipantInfo(studyDTO.getParticipantInfo()) - .setDuration(toDuration(studyDTO.getDuration())) + .setDuration(Duration.fromStudyDurationDTO(studyDTO.getDuration())) .setConsentInfo(studyDTO.getConsentInfo()) .setPlannedStartDate(studyDTO.getPlannedStart()) .setPlannedEndDate(studyDTO.getPlannedEnd()) .setContact(ContactTransformer.fromContactDTO_V1(studyDTO.getContact())); } - private static Duration toDuration(StudyDurationDTO duration) { - return Optional.ofNullable(duration).map(d -> new Duration() - .setValue(d.getValue()) - .setUnit(Duration.Unit.valueOf(d.getUnit().getValue().toUpperCase()))) - .orElse(null); - } - public static StudyDTO toStudyDTO_V1(Study study) { if(study.getContact() == null) study.setContact(new Contact()); @@ -45,7 +38,7 @@ public static StudyDTO toStudyDTO_V1(Study study) { .purpose(study.getPurpose()) .finishText(study.getFinishText()) .participantInfo(study.getParticipantInfo()) - .duration(toStudyDurationDTO(study.getDuration())) + .duration(Duration.toStudyDurationDTO(study.getDuration())) .consentInfo(study.getConsentInfo()) .status(StudyStatusDTO.fromValue(study.getStudyState().getValue())) .start(study.getStartDate()) @@ -61,11 +54,4 @@ public static StudyDTO toStudyDTO_V1(Study study) { public static Study.Status fromStatusChangeDTO_V1(StatusChangeDTO statusChangeDTO) { return Study.Status.valueOf(statusChangeDTO.getStatus().getValue().toUpperCase()); } - - public static StudyDurationDTO toStudyDurationDTO(Duration duration) { - return Optional.ofNullable(duration).map(d -> new StudyDurationDTO() - .value(d.getValue()) - .unit(StudyDurationDTO.UnitEnum.fromValue(d.getUnit().getValue()))) - .orElse(null); - } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyGroupRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyGroupRepository.java index 65f94977..aa6b93e5 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyGroupRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyGroupRepository.java @@ -2,6 +2,8 @@ import io.redlink.more.studymanager.exception.BadRequestException; import io.redlink.more.studymanager.model.StudyGroup; +import io.redlink.more.studymanager.model.scheduler.Duration; +import io.redlink.more.studymanager.utils.MapperUtils; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -20,7 +22,7 @@ public class StudyGroupRepository { private static final String GET_STUDY_GROUP_BY_IDS = "SELECT * FROM study_groups WHERE study_id = ? AND study_group_id = ?"; private static final String LIST_STUDY_GROUPS_ORDER_BY_STUDY_GROUP_ID = "SELECT * FROM study_groups WHERE study_id = ? ORDER BY study_group_id"; private static final String UPDATE_STUDY = - "UPDATE study_groups SET title = :title, purpose = :purpose, modified = now() WHERE study_id = :study_id AND study_group_id = :study_group_id"; + "UPDATE study_groups SET title = :title, purpose = :purpose, duration = :duration::jsonb, modified = now() WHERE study_id = :study_id AND study_group_id = :study_group_id"; private static final String DELETE_STUDY_GROUP_BY_ID = "DELETE FROM study_groups WHERE study_id = ? AND study_group_id = ?"; private static final String CLEAR_STUDY_GROUPS = "DELETE FROM study_groups"; @@ -70,7 +72,8 @@ private static MapSqlParameterSource toParams(StudyGroup studyGroup) { return new MapSqlParameterSource() .addValue("study_id", studyGroup.getStudyId()) .addValue("title", studyGroup.getTitle()) - .addValue("purpose", studyGroup.getPurpose()); + .addValue("purpose", studyGroup.getPurpose()) + .addValue("duration", MapperUtils.writeValueAsString(studyGroup.getDuration())); } private static RowMapper getStudyGroupRowMapper() { @@ -79,6 +82,7 @@ private static RowMapper getStudyGroupRowMapper() { .setStudyGroupId(rs.getInt("study_group_id")) .setTitle(rs.getString("title")) .setPurpose(rs.getString("purpose")) + .setDuration(MapperUtils.readValue(rs.getString("duration"), Duration.class)) .setCreated(RepositoryUtils.readInstant(rs, "created")) .setModified(RepositoryUtils.readInstant(rs, "modified")); } From 5a432348e6900d8eee5454e2a1c2272bffbc2a73 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Tue, 21 Nov 2023 11:46:42 +0100 Subject: [PATCH 11/16] TT-11 Add Calendar export option to study manager frontend --- .../WebSecurityConfiguration.java | 1 + .../studymanager/CalendarApiV1Controller.java | 29 +++++++++++++++++++ .../properties/GatewayProperties.java | 17 +++++++++++ .../src/main/resources/application.yaml | 2 ++ .../resources/openapi/StudyManagerAPI.yaml | 17 +++++++++++ 5 files changed, 66 insertions(+) create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java b/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java index 341eb986..50d51956 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java @@ -76,6 +76,7 @@ protected SecurityFilterChain filterChain(HttpSecurity http, //TODO specific handling of temporary sidecar .requestMatchers("/api/v1/components/observation/lime-survey-observation/end.html").permitAll() .requestMatchers("/api/v1/studies/*/export/studydata/*").permitAll() + .requestMatchers("/api/v1/studies/*/calendar.ics").permitAll() .requestMatchers("/api/v1/**").authenticated() .requestMatchers("/kibana/**").authenticated() .requestMatchers("/login/init").authenticated() 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 new file mode 100644 index 00000000..770171c5 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java @@ -0,0 +1,29 @@ +package io.redlink.more.studymanager.controller.studymanager; + +import io.redlink.more.studymanager.api.v1.webservices.CalendarApi; +import io.redlink.more.studymanager.properties.GatewayProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) +@EnableConfigurationProperties(GatewayProperties.class) +public class CalendarApiV1Controller implements CalendarApi { + + private final GatewayProperties properties; + + public CalendarApiV1Controller(GatewayProperties properties) { + this.properties = properties; + } + + @Override + public ResponseEntity getStudyCalendar(Long studyId) { + return ResponseEntity + .status(301) + .header("Location", properties.getBaseUrl() + "/api/v1/calendar/studies/" + studyId + "/calendar.ics") + .build(); + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java b/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java new file mode 100644 index 00000000..6dc942c2 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java @@ -0,0 +1,17 @@ +package io.redlink.more.studymanager.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "more.gateway") +public class GatewayProperties { + private String baseUrl; + + public String getBaseUrl() { + return baseUrl; + } + + public GatewayProperties setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } +} diff --git a/studymanager/src/main/resources/application.yaml b/studymanager/src/main/resources/application.yaml index 4cc7df79..c945047c 100644 --- a/studymanager/src/main/resources/application.yaml +++ b/studymanager/src/main/resources/application.yaml @@ -83,6 +83,8 @@ management: show-components: always more: + gateway: + base-url: '${GATEWAY_BASE_URL:http://localhost:8085}' components: lime-survey-observation: username: '${LIME_ADMIN_USER:more-admin}' diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index db7e621f..bcf4669e 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -337,6 +337,23 @@ paths: description: Cleared '409': description: removal failed + /studies/{studyId}/calendar.ics: + get: + tags: + - calendar + description: Get study calendar for study as iCal + operationId: getStudyCalendar + parameters: + - $ref: '#/components/parameters/StudyId' + responses: + '200': + description: Successfully returned study calendar + content: + text/calendar: + schema: + type: string + '404': + description: Not found /studies/{studyId}/studyGroups: post: From 91df528f8c1deae5d6854303068c9ab28fd96dfa Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Tue, 21 Nov 2023 14:34:52 +0100 Subject: [PATCH 12/16] TT-4 set participant status to locked when duration is done --- .../repository/ParticipantRepository.java | 16 ++++++++++++++ .../service/ParticipantService.java | 4 ++++ .../studymanager/service/StudyService.java | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java index e0c9ac4f..7352efff 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java @@ -41,6 +41,18 @@ public class ParticipantRepository { "WHERE study_id = :study_id AND participant_id = :participant_id " + " AND status = :current_status::participant_status " + "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 " + + "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 " + + "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()"; + private static final String DELETE_ALL = "DELETE FROM participants"; private final JdbcTemplate template; private final NamedParameterJdbcTemplate namedTemplate; @@ -73,6 +85,10 @@ public List listParticipants(Long studyId) { return template.query(LIST_PARTICIPANTS_BY_STUDY, getParticipantRowMapper(), studyId); } + public List listParticipantsForClosing() { + return template.query(LIST_PARTICIPANTS_FOR_CLOSING, getParticipantRowMapper()); + } + @Transactional public void deleteParticipant(Long studyId, Integer participantId) { template.update(DELETE_PARTICIPANT, studyId, participantId); diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/ParticipantService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ParticipantService.java index 35644e86..52fa2f95 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/ParticipantService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/ParticipantService.java @@ -37,6 +37,10 @@ public List listParticipants(Long studyId) { return participantRepository.listParticipants(studyId); } + public List listParticipantsForClosing() { + return participantRepository.listParticipantsForClosing(); + } + public Participant getParticipant(Long studyId, Integer participantId) { return participantRepository.getByIds(studyId, participantId); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java index 441d11b0..359fe0ac 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java @@ -10,6 +10,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.*; @@ -123,6 +124,27 @@ public void setStatus(Long studyId, Study.Status status, User user) { }); } + // every minute + @Scheduled(cron = "0 * * * * ?") + public void closeParticipationsForStudiesWithDurations() { + List participantsToClose = participantService.listParticipantsForClosing(); + log.debug("Selected {} paticipants to close", participantsToClose.size()); + participantsToClose.forEach(participant -> { + pushNotificationService.sendPushNotification( + participant.getStudyId(), + participant.getParticipantId(), + "Your Study has been closed", + "Your study was updated. For more information, please launch the app!", + Map.of("key", "STUDY_STATE_CHANGED", + "oldState", Study.Status.ACTIVE.getValue(), + "newState", Study.Status.CLOSED.getValue()) + ); + participantService.setStatus( + participant.getStudyId(), participant.getParticipantId(), Participant.Status.LOCKED + ); + }); + } + private void alignWithStudyState(Study s) { interventionService.alignInterventionsWithStudyState(s); observationService.alignObservationsWithStudyState(s); From 65690b1fa41d9b073a90e2302bfc31a22c0f140f Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Wed, 22 Nov 2023 11:19:16 +0100 Subject: [PATCH 13/16] Fork Mgmt: revert changes in github actions and add docker img parameter --- .github/workflows/compile-test-deploy.yml | 71 ----------------------- .github/workflows/compile-test.yml | 35 +++++++++++ 2 files changed, 35 insertions(+), 71 deletions(-) delete mode 100644 .github/workflows/compile-test-deploy.yml diff --git a/.github/workflows/compile-test-deploy.yml b/.github/workflows/compile-test-deploy.yml deleted file mode 100644 index b523c8ca..00000000 --- a/.github/workflows/compile-test-deploy.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Test, Compile and Deploy -on: - workflow_dispatch: - -jobs: - Compile-and-Test: - name: Compile and Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Compile and test project - run: ./mvnw -B -U - --no-transfer-progress - compile test - - name: Show 3rd-Party Licenses - run: | - cat ./studymanager/target/generated-sources/license/THIRD-PARTY.txt - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v3 - with: - name: Test Results - path: "**/TEST-*.xml" - - name: Upload Licenses List - if: always() - uses: actions/upload-artifact@v3 - with: - name: Licenses List - path: "./studymanager/target/generated-sources/license/THIRD-PARTY.txt" - - Build-and-Deploy: - name: "Build and Push Docker Image" - runs-on: ubuntu-latest - needs: - - Compile-and-Test - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Build JIB container and publish to GitHub Packages - run: ./mvnw -B -U - --no-transfer-progress - clean deploy - -Drevision=${{github.run_number}} - -Dchangelist= - -Dsha1=.${GITHUB_SHA:0:7} - -Dquick - -Ddocker.namespace=${DOCKER_NAMESPACE,,} - -Djib.to.tags=latest - -Djib.to.auth.username=${{ github.actor }} - -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} - env: - DOCKER_NAMESPACE: ghcr.io/${{ github.repository_owner }} - - event_file: - name: "Event File" - runs-on: ubuntu-latest - steps: - - name: Upload - uses: actions/upload-artifact@v3 - with: - name: Event File - path: ${{ github.event_path }} diff --git a/.github/workflows/compile-test.yml b/.github/workflows/compile-test.yml index 03de995c..06df24f1 100644 --- a/.github/workflows/compile-test.yml +++ b/.github/workflows/compile-test.yml @@ -1,5 +1,10 @@ name: Test and Compile on: + workflow_dispatch: + inputs: + dockerTag: + description: If set, docker img is built and tagged accordingly + required: false push: jobs: @@ -33,6 +38,36 @@ jobs: name: Licenses List path: "./studymanager/target/generated-sources/license/THIRD-PARTY.txt" + Build-and-Deploy: + name: "Build and Push Docker Image" + runs-on: ubuntu-latest + if: github.ref_name == 'main' || github.event.inputs.dockerTag != '' + needs: + - Compile-and-Test + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17 + - name: Build JIB container and publish to GitHub Packages + run: + TAG=${{github.event.inputs.dockerTag}} && + ./mvnw -B -U + --no-transfer-progress + clean deploy + -Drevision=${{github.run_number}} + -Dchangelist= + -Dsha1=.${GITHUB_SHA:0:7} + -Dquick + -Ddocker.namespace=${DOCKER_NAMESPACE,,} + -Djib.to.tags=${TAG:=latest} + -Djib.to.auth.username=${{ github.actor }} + -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} + env: + DOCKER_NAMESPACE: ghcr.io/${{ github.repository_owner }} + event_file: name: "Event File" runs-on: ubuntu-latest From 6db5c8c12b137b2a0f5aa04945d5cb82f34d07a8 Mon Sep 17 00:00:00 2001 From: iaigner Date: Thu, 21 Dec 2023 11:28:24 +0100 Subject: [PATCH 14/16] TT-108: Extend StudyManager API with Participant Start (readonly) --- .../io/redlink/more/studymanager/action/ActionService.java | 2 +- .../java/io/redlink/more/studymanager/model/Participant.java | 3 +++ .../more/studymanager/repository/ParticipantRepository.java | 1 + studymanager/src/main/resources/openapi/StudyManagerAPI.yaml | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java b/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java index e289500d..c2ec532a 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java @@ -70,7 +70,7 @@ private void executeAction(long studyId, Integer studyGroupId, int interventionI ctx.putParticipant(parameter.getParticipantId()); Action executable = factory.create( moreSDK.scopedActionSDK( - studyId, studyGroupId, interventionId, action.getActionId(), action.getType(), parameter.getParticipantId() + studyId, studyGroupId, interventionId, action.getParticipantStart(), action.getActionId(), action.getType(), parameter.getParticipantId() ), action.getProperties() ); diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java index a835a514..d387891d 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java @@ -17,6 +17,7 @@ public class Participant { private Status status; private Instant created; private Instant modified; + private String start; private String registrationToken; @@ -65,6 +66,8 @@ public Participant setParticipantId(Integer participantId) { return this; } + public String getParticipantStart() { return start; } + public String getAlias() { return alias; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java index 96bdf6ba..2775d10f 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java @@ -163,6 +163,7 @@ private static RowMapper getParticipantRowMapper() { .setCreated(RepositoryUtils.readInstant(rs, "created")) .setModified(RepositoryUtils.readInstant(rs, "modified")) .setStatus(RepositoryUtils.readParticipantStatus(rs, "status")) + .setStart(RepositoryUtils.redInstant(rs, "start")) .setRegistrationToken(rs.getString("token")); } } diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index bcf4669e..567faecf 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -1414,6 +1414,9 @@ components: readOnly: true status: $ref: '#/components/schemas/ParticipantStatus' + start: + type: string + readOnly: true created: type: string format: date-time From d465c4adf65629aace5cf6fb70c0281b59640fdb Mon Sep 17 00:00:00 2001 From: iaigner Date: Thu, 21 Dec 2023 13:58:51 +0100 Subject: [PATCH 15/16] TT-108: update participant with participant start, fix errors --- .../more/studymanager/action/ActionService.java | 2 +- .../more/studymanager/model/Participant.java | 13 ++++++++++--- .../repository/ParticipantRepository.java | 6 +++--- .../src/main/resources/openapi/StudyManagerAPI.yaml | 1 + .../repository/ParticipantRepositoryTest.java | 10 ++++++++++ 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java b/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java index c2ec532a..e289500d 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/action/ActionService.java @@ -70,7 +70,7 @@ private void executeAction(long studyId, Integer studyGroupId, int interventionI ctx.putParticipant(parameter.getParticipantId()); Action executable = factory.create( moreSDK.scopedActionSDK( - studyId, studyGroupId, interventionId, action.getParticipantStart(), action.getActionId(), action.getType(), parameter.getParticipantId() + studyId, studyGroupId, interventionId, action.getActionId(), action.getType(), parameter.getParticipantId() ), action.getProperties() ); diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java index d387891d..3f1b359f 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java @@ -17,7 +17,7 @@ public class Participant { private Status status; private Instant created; private Instant modified; - private String start; + private Instant start; private String registrationToken; @@ -52,6 +52,15 @@ public Participant setStatus(Status status) { return this; } + public Participant setStart( Instant start ) { + this.start = start; + return this; + } + + public Instant getStart() { + return start; + } + public Participant setStudyId(Long studyId) { this.studyId = studyId; return this; @@ -66,8 +75,6 @@ public Participant setParticipantId(Integer participantId) { return this; } - public String getParticipantStart() { return start; } - public String getAlias() { return alias; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java index 2775d10f..7abedf7d 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java @@ -31,8 +31,8 @@ public class ParticipantRepository { private static final String INSERT_PARTICIPANT_AND_TOKEN = "WITH p AS (INSERT INTO participants(study_id,participant_id,alias,study_group_id) VALUES (:study_id,(SELECT COALESCE(MAX(participant_id),0)+1 FROM participants WHERE study_id = :study_id),:alias,:study_group_id) RETURNING participant_id, study_id) INSERT INTO registration_tokens(participant_id,study_id,token) SELECT participant_id, study_id, :token FROM p"; - 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 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 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 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 DELETE_PARTICIPANT = "DELETE FROM participants " + "WHERE study_id=? AND participant_id=?"; @@ -163,7 +163,7 @@ private static RowMapper getParticipantRowMapper() { .setCreated(RepositoryUtils.readInstant(rs, "created")) .setModified(RepositoryUtils.readInstant(rs, "modified")) .setStatus(RepositoryUtils.readParticipantStatus(rs, "status")) - .setStart(RepositoryUtils.redInstant(rs, "start")) + .setStart(RepositoryUtils.readInstant(rs, "start")) .setRegistrationToken(rs.getString("token")); } } diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index 567faecf..16d8a6dd 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -1416,6 +1416,7 @@ components: $ref: '#/components/schemas/ParticipantStatus' start: type: string + format: date-time readOnly: true created: type: string diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java index ce907eb4..a61eba8a 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java @@ -21,6 +21,8 @@ import org.springframework.test.context.ActiveProfiles; import org.testcontainers.junit.jupiter.Testcontainers; +import java.time.Instant; + import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @@ -47,6 +49,8 @@ void testInsert() { Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId(); Integer studyGroupId = studyGroupRepository.insert(new StudyGroup() .setStudyId(studyId)).getStudyGroupId(); + Instant startTimestamp = Instant.parse("2023-12-24T20:30:42.12Z"); + Participant participant = new Participant() .setAlias("participant x") @@ -59,6 +63,11 @@ void testInsert() { assertThat(participantResponse.getAlias()).isEqualTo(participant.getAlias()); assertThat(participantResponse.getStatus()).isEqualTo(Participant.Status.NEW); assertThat(participantResponse.getParticipantId()).isNotNull(); + assertThat(participantResponse.getStart()).isNull(); + + participantResponse.setStart(startTimestamp); + + assertThat(participantResponse.getStart()).isNotNull(); Participant update = participantResponse.setAlias("new participant x"); @@ -66,6 +75,7 @@ void testInsert() { 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()); From 6ed3b4ce8fac4a97be276b76dac336e877472cd5 Mon Sep 17 00:00:00 2001 From: Thomas Kurz Date: Thu, 21 Dec 2023 14:59:17 +0100 Subject: [PATCH 16/16] #TT-108 set start in participant transformer --- .../model/transformer/ParticipantTransformer.java | 1 + .../repository/ParticipantRepositoryTest.java | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) 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 42b7728b..098774e0 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 @@ -34,6 +34,7 @@ public static ParticipantDTO toParticipantDTO_V1(Participant participant) { .studyGroupId(participant.getStudyGroupId()) .registrationToken(participant.getRegistrationToken()) .status(ParticipantStatusDTO.fromValue(participant.getStatus().getValue())) + .start(Transformers.toOffsetDateTime(participant.getStart())) .modified(Transformers.toOffsetDateTime(participant.getModified())) .created(Transformers.toOffsetDateTime(participant.getCreated())); } diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java index a61eba8a..ce907eb4 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/repository/ParticipantRepositoryTest.java @@ -21,8 +21,6 @@ import org.springframework.test.context.ActiveProfiles; import org.testcontainers.junit.jupiter.Testcontainers; -import java.time.Instant; - import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @@ -49,8 +47,6 @@ void testInsert() { Long studyId = studyRepository.insert(new Study().setContact(new Contact().setPerson("test").setEmail("test"))).getStudyId(); Integer studyGroupId = studyGroupRepository.insert(new StudyGroup() .setStudyId(studyId)).getStudyGroupId(); - Instant startTimestamp = Instant.parse("2023-12-24T20:30:42.12Z"); - Participant participant = new Participant() .setAlias("participant x") @@ -63,11 +59,6 @@ void testInsert() { assertThat(participantResponse.getAlias()).isEqualTo(participant.getAlias()); assertThat(participantResponse.getStatus()).isEqualTo(Participant.Status.NEW); assertThat(participantResponse.getParticipantId()).isNotNull(); - assertThat(participantResponse.getStart()).isNull(); - - participantResponse.setStart(startTimestamp); - - assertThat(participantResponse.getStart()).isNotNull(); Participant update = participantResponse.setAlias("new participant x"); @@ -75,7 +66,6 @@ void testInsert() { 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());