diff --git a/.github/workflows/compile-test.yml b/.github/workflows/compile-test.yml index 3f6539f0..06df24f1 100644 --- a/.github/workflows/compile-test.yml +++ b/.github/workflows/compile-test.yml @@ -1,6 +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: @@ -37,7 +41,7 @@ jobs: Build-and-Deploy: name: "Build and Push Docker Image" runs-on: ubuntu-latest - if: github.ref_name == 'main' + if: github.ref_name == 'main' || github.event.inputs.dockerTag != '' needs: - Compile-and-Test steps: @@ -48,7 +52,9 @@ jobs: distribution: 'temurin' java-version: 17 - name: Build JIB container and publish to GitHub Packages - run: ./mvnw -B -U + run: + TAG=${{github.event.inputs.dockerTag}} && + ./mvnw -B -U --no-transfer-progress clean deploy -Drevision=${{github.run_number}} @@ -56,7 +62,7 @@ jobs: -Dsha1=.${GITHUB_SHA:0:7} -Dquick -Ddocker.namespace=${DOCKER_NAMESPACE,,} - -Djib.to.tags=latest + -Djib.to.tags=${TAG:=latest} -Djib.to.auth.username=${{ github.actor }} -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} env: 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 3b0d4414..5aa7bc21 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 @@ -84,6 +84,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/model/Intervention.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Intervention.java index 08af2ec5..9517ac7e 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 @@ -8,6 +8,9 @@ */ 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 { @@ -16,7 +19,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; @@ -65,11 +68,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 547146ad..fdcbc68a 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 @@ -9,6 +9,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; @@ -21,7 +22,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; @@ -99,11 +100,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/Participant.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/Participant.java index a835a514..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,6 +17,7 @@ public class Participant { private Status status; private Instant created; private Instant modified; + private Instant start; private String registrationToken; @@ -51,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; 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 f3d112f3..870f9a5b 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 @@ -8,6 +8,8 @@ */ 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; @@ -19,6 +21,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; @@ -100,6 +103,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; } @@ -192,6 +204,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/StudyGroup.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyGroup.java index a1613297..63a66d17 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 @@ -8,6 +8,8 @@ */ package io.redlink.more.studymanager.model; +import io.redlink.more.studymanager.model.scheduler.Duration; + import java.time.Instant; public class StudyGroup { @@ -15,6 +17,7 @@ public class StudyGroup { private Integer studyGroupId; private String title; private String purpose; + private Duration duration; private Instant created; private Instant modified; @@ -54,6 +57,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 new file mode 100644 index 00000000..894044d3 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java @@ -0,0 +1,161 @@ +package io.redlink.more.studymanager.model.scheduler; + +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 { + + 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 + "'"); + } + } + + 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; + + 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 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() + .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 61% 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 28c46058..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,20 +1,19 @@ -/* - * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more - * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute - * for Digital Health and Prevention -- A research institute of the - * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur - * Förderung der wissenschaftlichen Forschung). - * Licensed under the Elastic License 2.0. - */ -package io.redlink.more.studymanager.model; +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; } @@ -41,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 80% 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 13e5a00b..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,12 +1,4 @@ -/* - * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more - * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute - * for Digital Health and Prevention -- A research institute of the - * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur - * Förderung der wissenschaftlichen Forschung). - * Licensed under the Elastic License 2.0. - */ -package io.redlink.more.studymanager.model; +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 3e931166..e653a0b3 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 @@ -8,33 +8,63 @@ */ 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; } @@ -65,4 +95,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 babc7f97..ef1e726f 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 @@ -23,7 +23,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) { @@ -33,7 +33,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 e257deed..077ac44f 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 @@ -28,7 +28,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()); } @@ -43,7 +43,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/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/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java index 4b2ac51b..7ad8a436 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 @@ -10,6 +10,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 { @@ -21,7 +22,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) { @@ -30,6 +32,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 bbb55edd..5253deb2 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 @@ -8,12 +8,12 @@ */ 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; + +import java.util.Optional; public class StudyTransformer { @@ -30,6 +30,7 @@ public static Study fromStudyDTO_V1(StudyDTO studyDTO) { .setPurpose(studyDTO.getPurpose()) .setFinishText(studyDTO.getFinishText()) .setParticipantInfo(studyDTO.getParticipantInfo()) + .setDuration(Duration.fromStudyDurationDTO(studyDTO.getDuration())) .setConsentInfo(studyDTO.getConsentInfo()) .setPlannedStartDate(studyDTO.getPlannedStart()) .setPlannedEndDate(studyDTO.getPlannedEnd()) @@ -45,6 +46,7 @@ public static StudyDTO toStudyDTO_V1(Study study) { .purpose(study.getPurpose()) .finishText(study.getFinishText()) .participantInfo(study.getParticipantInfo()) + .duration(Duration.toStudyDurationDTO(study.getDuration())) .consentInfo(study.getConsentInfo()) .status(StudyStatusDTO.fromValue(study.getStudyState().getValue())) .start(study.getStartDate()) 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/java/io/redlink/more/studymanager/repository/InterventionRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java index 23f5f5a0..118dcbcd 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 @@ -12,7 +12,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 dd882f23..eb5e1743 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 @@ -11,8 +11,9 @@ 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.model.scheduler.ScheduleEvent; import io.redlink.more.studymanager.utils.MapperUtils; import java.util.List; import java.util.Optional; @@ -145,7 +146,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/ParticipantRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java index 5855b231..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=?"; @@ -49,6 +49,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; @@ -81,6 +93,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); @@ -147,6 +163,7 @@ private static RowMapper getParticipantRowMapper() { .setCreated(RepositoryUtils.readInstant(rs, "created")) .setModified(RepositoryUtils.readInstant(rs, "modified")) .setStatus(RepositoryUtils.readParticipantStatus(rs, "status")) + .setStart(RepositoryUtils.readInstant(rs, "start")) .setRegistrationToken(rs.getString("token")); } } 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 6158246d..0441bae0 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 @@ -10,6 +10,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; @@ -28,7 +30,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"; @@ -78,7 +80,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() { @@ -87,6 +90,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")); } 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 fe349115..42c8ff41 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 @@ -15,6 +15,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; @@ -25,8 +28,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 *, " + @@ -48,7 +51,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"; @@ -135,6 +138,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()) @@ -154,6 +158,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/java/io/redlink/more/studymanager/service/ParticipantService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ParticipantService.java index c1e8b7ee..9fecaaf0 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 @@ -45,6 +45,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 15d20ad3..2bb003d0 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 @@ -18,6 +18,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.*; @@ -131,6 +132,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); 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/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/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; 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..16d8a6dd 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: @@ -1277,6 +1294,8 @@ components: type: string consentInfo: type: string + duration: + $ref: '#/components/schemas/StudyDuration' finishText: type: string status: @@ -1354,6 +1373,8 @@ components: type: string purpose: type: string + duration: + $ref: '#/components/schemas/StudyDuration' numberOfParticipants: type: integer readOnly: true @@ -1365,7 +1386,18 @@ components: type: string format: date-time readOnly: true - + StudyDuration: + type: object + properties: + value: + type: integer + unit: + type: string + description: unit of time + enum: + - MINUTE + - HOUR + - DAY Participant: type: object properties: @@ -1382,6 +1414,10 @@ components: readOnly: true status: $ref: '#/components/schemas/ParticipantStatus' + start: + type: string + format: date-time + readOnly: true created: type: string format: date-time @@ -1422,7 +1458,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 +1559,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 6dcf265a..55fc8145 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 @@ -13,6 +13,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 f1f6ebee..0be3b95c 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 @@ -16,7 +16,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 02daa6de..2ddfcbc3 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 @@ -11,7 +11,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; @@ -144,7 +146,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") @@ -154,7 +156,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 bcd1b93c..652a7738 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 @@ -10,6 +10,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 6207f9fe..2e18ae2c 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 @@ -11,6 +11,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; @@ -72,7 +74,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 2bace6f4..7452d1c7 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 @@ -10,6 +10,7 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.scheduler.*; import io.redlink.more.studymanager.utils.MapperUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -102,6 +103,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 6e9303f0..ade58668 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 @@ -10,6 +10,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; @@ -64,6 +65,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); @@ -71,13 +73,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(); @@ -89,11 +91,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 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 cf61bd1f..ba9187b2 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 @@ -12,6 +12,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); + + } +}