From f91c85ce29bdb3446cd1749f63d9b31c31d082aa Mon Sep 17 00:00:00 2001 From: Mikolaj Luzak Date: Fri, 3 May 2024 15:51:04 +0200 Subject: [PATCH] first implementation for observation timeline --- .../studymanager/CalendarApiV1Controller.java | 23 +++- .../timeline/InterventionTimelineEvent.java | 14 +++ .../timeline/ObservationTimelineEvent.java | 15 +++ .../model/timeline/StudyTimeline.java | 34 ++++++ .../model/timeline/TimelineFilter.java | 6 + .../transformer/TimelineTransformer.java | 49 ++++++++ .../studymanager/service/CalendarService.java | 115 ++++++++++++++++++ .../resources/openapi/StudyManagerAPI.yaml | 102 ++++++++++++++++ 8 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java create mode 100644 studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java index 770171c5..3b316d2b 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java @@ -1,21 +1,32 @@ package io.redlink.more.studymanager.controller.studymanager; +import io.redlink.more.studymanager.api.v1.model.StudyTimelineDTO; import io.redlink.more.studymanager.api.v1.webservices.CalendarApi; +import io.redlink.more.studymanager.model.transformer.TimelineTransformer; import io.redlink.more.studymanager.properties.GatewayProperties; +import io.redlink.more.studymanager.service.CalendarService; +import io.redlink.more.studymanager.service.OAuth2AuthenticationService; 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; +import java.time.LocalDate; +import java.time.OffsetDateTime; + @RestController @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) @EnableConfigurationProperties(GatewayProperties.class) public class CalendarApiV1Controller implements CalendarApi { + private final CalendarService service; + OAuth2AuthenticationService auth2AuthenticationService; private final GatewayProperties properties; - public CalendarApiV1Controller(GatewayProperties properties) { + public CalendarApiV1Controller(CalendarService service, OAuth2AuthenticationService auth2AuthenticationService, GatewayProperties properties) { + this.service = service; + this.auth2AuthenticationService = auth2AuthenticationService; this.properties = properties; } @@ -26,4 +37,14 @@ public ResponseEntity getStudyCalendar(Long studyId) { .header("Location", properties.getBaseUrl() + "/api/v1/calendar/studies/" + studyId + "/calendar.ics") .build(); } + + @Override + public ResponseEntity getStudyTimeline(Long studyId, Integer participant, Integer studyGroup, OffsetDateTime referenceDate, LocalDate from, LocalDate to) { + final var currentUser = auth2AuthenticationService.getCurrentUser(); + return ResponseEntity.ok( + TimelineTransformer.toStudyTimelineDTO( + service.getTimeline(studyId, participant, studyGroup, referenceDate, from, to, currentUser) + ) + ); + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java new file mode 100644 index 00000000..c24b3783 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java @@ -0,0 +1,14 @@ +package io.redlink.more.studymanager.model.timeline; + +import java.time.Instant; + +public record InterventionTimelineEvent( + Integer interventionId, + Integer studyGroupId, + String title, + String purpose, + String type, + Instant start, + Instant end, + String scheduleType +) {} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java new file mode 100644 index 00000000..6e78f322 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java @@ -0,0 +1,15 @@ +package io.redlink.more.studymanager.model.timeline; + +import java.time.Instant; + +public record ObservationTimelineEvent( + Integer observationId, + Integer studyGroupId, + String title, + String purpose, + String type, + Instant start, + Instant end, + Boolean hidden, + String scheduleType +) {} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java new file mode 100644 index 00000000..07ac15a2 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java @@ -0,0 +1,34 @@ +package io.redlink.more.studymanager.model.timeline; + +import java.util.ArrayList; +import java.util.List; + +public class StudyTimeline { + List observationTimelineEvents; + List interventionTimelineEvents; + + public StudyTimeline() { + observationTimelineEvents = new ArrayList<>(); + interventionTimelineEvents = new ArrayList<>(); + } + + public void addObservationTimelineEvent(ObservationTimelineEvent event) { + observationTimelineEvents.add(event); + } + + public void addAllObservations(List events) { observationTimelineEvents.addAll(events); } + + public List getObservationTimelineEvents() { + return observationTimelineEvents; + } + + public void addInterventionTimelineEvent(InterventionTimelineEvent event) { + interventionTimelineEvents.add(event); + } + + public void addAllInterventions(List events) { interventionTimelineEvents.addAll(events); } + + public List getInterventionTimelineEvents() { + return interventionTimelineEvents; + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java new file mode 100644 index 00000000..be22d630 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java @@ -0,0 +1,6 @@ +package io.redlink.more.studymanager.model.timeline; + +public record TimelineFilter ( + Integer studyGroupId, + Integer participantId +) {} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java new file mode 100644 index 00000000..682a1601 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java @@ -0,0 +1,49 @@ +package io.redlink.more.studymanager.model.transformer; + + +import io.redlink.more.studymanager.api.v1.model.InterventionTimelineEventDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationTimelineEventDTO; +import io.redlink.more.studymanager.api.v1.model.StudyTimelineDTO; +import io.redlink.more.studymanager.model.timeline.InterventionTimelineEvent; +import io.redlink.more.studymanager.model.timeline.ObservationTimelineEvent; +import io.redlink.more.studymanager.model.timeline.StudyTimeline; + + +public class TimelineTransformer { + private TimelineTransformer() {} + + public static StudyTimelineDTO toStudyTimelineDTO(StudyTimeline studyTimeline) { + return new StudyTimelineDTO() + .observations(studyTimeline.getObservationTimelineEvents().stream().map( + TimelineTransformer::toObservationTimelineDTO + ).toList()) + .interventions(studyTimeline.getInterventionTimelineEvents().stream().map( + TimelineTransformer::toInterventionTimelineEventDTO + ).toList()); + } + + public static ObservationTimelineEventDTO toObservationTimelineDTO(ObservationTimelineEvent observationTimelineEvent) { + return new ObservationTimelineEventDTO() + .observationId(observationTimelineEvent.observationId()) + .studyGroupId(observationTimelineEvent.studyGroupId()) + .title(observationTimelineEvent.title()) + .purpose(observationTimelineEvent.purpose()) + .type(observationTimelineEvent.type()) + .start(Transformers.toOffsetDateTime(observationTimelineEvent.start())) + .end(Transformers.toOffsetDateTime(observationTimelineEvent.end())) + .hidden(observationTimelineEvent.hidden()) + .scheduleType(observationTimelineEvent.scheduleType()); + } + + public static InterventionTimelineEventDTO toInterventionTimelineEventDTO(InterventionTimelineEvent interventionTimelineEvent) { + return new InterventionTimelineEventDTO() + .interventionId(interventionTimelineEvent.interventionId()) + .studyGroupId(interventionTimelineEvent.studyGroupId()) + .title(interventionTimelineEvent.title()) + .purpose(interventionTimelineEvent.purpose()) + .type(interventionTimelineEvent.type()) + .start(Transformers.toOffsetDateTime(interventionTimelineEvent.start())) + .end(Transformers.toOffsetDateTime(interventionTimelineEvent.end())) + .scheduleType(interventionTimelineEvent.scheduleType()); + } +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java new file mode 100644 index 00000000..0e4ec710 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java @@ -0,0 +1,115 @@ +package io.redlink.more.studymanager.service; + +import io.redlink.more.studymanager.model.Participant; +import io.redlink.more.studymanager.model.Study; +import io.redlink.more.studymanager.model.User; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RelativeEvent; +import io.redlink.more.studymanager.model.scheduler.ScheduleEvent; +import io.redlink.more.studymanager.model.timeline.ObservationTimelineEvent; +import io.redlink.more.studymanager.model.timeline.StudyTimeline; +import io.redlink.more.studymanager.model.transformer.Transformers; +import org.springframework.stereotype.Service; + +import java.time.*; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + + +@Service +public class CalendarService { + + private final StudyService studyService; + private final ObservationService observationService; + private final InterventionService interventionService; + private final ParticipantService participantService; + public CalendarService(StudyService studyService, ObservationService observationService, InterventionService interventionService, + ParticipantService participantService) { + this.studyService = studyService; + this.observationService = observationService; + this.interventionService = interventionService; + this.participantService = participantService; + } + + public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer studyGroupId, OffsetDateTime referenceDate, LocalDate from, LocalDate to, User currentUser) { + Study study = studyService.getStudy(studyId, currentUser).orElse(null); + if(study == null) + return null; + + StudyTimeline studyTimeline = new StudyTimeline(); + Integer actualStudyGroupId; + Instant relativeDate = null; + + if(participantId != null) { + Participant participant = participantService.getParticipant(studyId, participantId); + if(participant == null) + return null; + actualStudyGroupId = participant.getStudyGroupId(); + if (participant.getStart() != null) { + relativeDate = participant.getStart(); + } + } else { + actualStudyGroupId = studyGroupId; + } + + if(relativeDate == null) { + if (referenceDate != null) { + relativeDate = referenceDate.toInstant(); + } else if (study.getStartDate() != null) { + relativeDate = study.getStartDate().atStartOfDay().toInstant(ZoneId.systemDefault().getRules().getOffset(Instant.now())); + } else { + relativeDate = study.getPlannedStartDate().atStartOfDay().toInstant(ZoneId.systemDefault().getRules().getOffset(Instant.now())); + } + } + Instant finalRelativeDate = relativeDate; + + studyTimeline.addAllObservations(observationService.listObservations(studyId) + .stream() + .map(observation -> { + ScheduleEvent e = observation.getSchedule(); + boolean isWithinTimeframe = true; + Instant start = null; + Instant end = null; + + if(Event.TYPE.equals(e.getType())) { + Event event = (Event) e; + start = event.getDateStart(); + end = event.getDateEnd(); + isWithinTimeframe = start.isAfter(Transformers.toInstant(OffsetDateTime.from(from.atStartOfDay()))) + && end.isBefore(Transformers.toInstant(OffsetDateTime.from(to.atStartOfDay().plusDays(1)))); + } else if(RelativeEvent.TYPE.equals(e.getType())) { + RelativeEvent event = (RelativeEvent) e; + start = finalRelativeDate.plus( + event.getDtstart().getOffset().getValue(), + ChronoUnit.valueOf(event.getDtstart().getOffset().getUnit().getValue()) + ); + end = finalRelativeDate.plus( + event.getDtend().getOffset().getValue(), + ChronoUnit.valueOf(event.getDtend().getOffset().getUnit().getValue()) + ); + + isWithinTimeframe = start.isAfter(Transformers.toInstant(OffsetDateTime.from(from.atStartOfDay()))) + && end.isBefore(Transformers.toInstant(OffsetDateTime.from(to.atStartOfDay().plusDays(1)))); + } + if (Objects.equals(observation.getStudyGroupId(), actualStudyGroupId) && isWithinTimeframe) + return new ObservationTimelineEvent( + observation.getObservationId(), + observation.getStudyGroupId(), + observation.getTitle(), + observation.getPurpose(), + observation.getType(), + start, + end, + observation.getHidden(), + observation.getSchedule().getType() + ); + return null; + }).filter(Objects::nonNull) + .toList() + ); + + return studyTimeline; + } + + +} diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index 16d8a6dd..d97e19a9 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -355,6 +355,49 @@ paths: '404': description: Not found + /studies/{studyId}/timeline: + get: + tags: + - calendar + description: Get study timeline for study + operationId: getStudyTimeline + parameters: + - $ref: '#/components/parameters/StudyId' + - name: participant + in: query + schema: + $ref: '#/components/schemas/Id' + - name: studyGroup + in: query + schema: + $ref: '#/components/schemas/Id' + - name: referenceDate + in: query + description: reference date used to calculate relative schedules + schema: + type: string + format: date-time + - name: from + in: query + schema: + type: string + format: date + - name: to + in: query + schema: + type: string + format: date + responses: + '200': + description: Successfully returned study timeline + content: + application/json: + schema: + $ref: '#/components/schemas/StudyTimeline' + '404': + description: Not found + + /studies/{studyId}/studyGroups: post: tags: @@ -1482,6 +1525,65 @@ components: type: boolean default: false + + StudyTimeline: + type: object + properties: + observations: + type: array + items: + $ref: '#/components/schemas/ObservationTimelineEvent' + interventions: + type: array + items: + $ref: '#/components/schemas/InterventionTimelineEvent' + + ObservationTimelineEvent: + type: object + properties: + observationId: + $ref: '#/components/schemas/Id' + studyGroupId: + $ref: '#/components/schemas/Id' + title: + type: string + purpose: + type: string + type: + type: string + start: + type: string + format: date-time + end: + type: string + format: date-time + hidden: + type: boolean + scheduleType: + type: string + + InterventionTimelineEvent: + type: object + properties: + interventionId: + $ref: '#/components/schemas/Id' + studyGroupId: + $ref: '#/components/schemas/Id' + title: + type: string + purpose: + type: string + type: + type: string + start: + type: string + format: date-time + end: + type: string + format: date-time + scheduleType: + type: string + EndpointToken: type: object properties: