From a093e74a77e8dbeb30d9b4c417b2cb93bbdc57fc Mon Sep 17 00:00:00 2001 From: Pim van Nierop Date: Fri, 3 May 2024 15:26:30 +0200 Subject: [PATCH] Add project and source to Observation objects --- README.md | 2 +- .../datadashboard/api/api/ObservationDto.kt | 6 +++ .../api/domain/ObservationRepository.kt | 7 ++-- .../api/domain/mapper/Extensions.kt | 2 + .../api/domain/model/Observation.kt | 7 ++++ .../api/resource/ObservationResource.kt | 5 ++- .../api/service/ObservationService.kt | 4 +- .../changes/20220309-create-database.xml | 7 ++++ .../changes/20220321-observations.csv | 18 ++++----- .../api/DashboardIntegrationTest.kt | 4 +- .../api/resource/ObservationResourceTest.kt | 39 ++++++++++++++----- .../api/service/ObservationServiceTest.kt | 33 +++++++++++----- 12 files changed, 96 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f6ac611..eb746b2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ the data from the RADAR-base kafka service.[]\ Data dashboard applications can use the APIs as follows. -`GET */subject/{subjectId}/topic/{topicId}/observations` +`GET */project/{projectId}/subject/{subjectId}/topic/{topicId}/observations` ## Installation diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt index 73ddc03..eaf7d0b 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt @@ -23,9 +23,15 @@ data class ObservationDto( /** Unique observation ID. */ val id: Long?, + /** Unique identifier of project. */ + val project: String?, + /** Unique identifier of study subject. */ val subject: String?, + /** Unique identifier of the data source. */ + val source: String?, + /** Unique identifier of the kafka topic. */ val topic: String?, diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt index 5db3759..5f82150 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt @@ -29,14 +29,15 @@ class ObservationRepository( @Context em: Provider, ) : HibernateRepository(em) { - fun getObservations(topicId: String, subjectId: String): List { - logger.debug("Get observations in topic {} of subject {}", topicId, subjectId) + fun getObservations(projectId: String, subjectId: String, topicId: String): List { + logger.debug("Get observations in topic {} of subject {} in project {}", topicId, subjectId, projectId) return transact { createQuery( - "SELECT o FROM Observation o WHERE o.subject = :subjectId AND o.topic = :topicId ORDER BY o.date DESC", + "SELECT o FROM Observation o WHERE o.project = :projectId AND o.subject = :subjectId AND o.topic = :topicId ORDER BY o.date DESC", Observation::class.java, ).apply { + setParameter("projectId", projectId) setParameter("subjectId", subjectId) setParameter("topicId", topicId) }.resultList diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt index dc31d71..9d99177 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt @@ -24,7 +24,9 @@ import java.time.Duration fun Observation.toDto(): ObservationDto = ObservationDto( id = id, + project = project, subject = subject, + source = source, topic = topic, category = category, date = date?.toString(), diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt index f464445..cba018d 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt @@ -30,10 +30,17 @@ data class Observation( @Id val id: Long, + @Column(nullable = false) + @Id + val project: String, + @Column(nullable = false) @Id val subject: String, + @Id + val source: String, + @Column(nullable = false) @Id val topic: String, diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt index 9770b9c..3e3d4b9 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt @@ -30,7 +30,7 @@ import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.auth.filter.RadarSecurityContext import org.slf4j.LoggerFactory -@Path("subject/{subjectId}/topic/{topicId}") +@Path("project/{projectId}/subject/{subjectId}/topic/{topicId}") @Resource @Produces("application/json") @Consumes("application/json") @@ -43,6 +43,7 @@ class ObservationResource( @Path("observations") @NeedsPermission(Permission.MEASUREMENT_READ) fun getObservations( + @PathParam("projectId") projectId: String, @PathParam("subjectId") subjectId: String, @PathParam("topicId") topicId: String ): ObservationListDto { @@ -50,7 +51,7 @@ class ObservationResource( val userName = (request.securityContext as RadarSecurityContext).userPrincipal log.info("User $userName is accessing observations for $subjectId") if (!subjectId.equals(userName)) throw NotFoundException("Subjects can only request their own observations.") - return observationService.getObservations(topicId, subjectId) + return observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId) } return ObservationListDto(emptyList()) } diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt index 5086be2..9f5874c 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt @@ -26,8 +26,8 @@ import org.radarbase.datadashboard.api.domain.mapper.toDto class ObservationService( @Context private val observationRepository: ObservationRepository ) { - fun getObservations(topicId: String, subjectId: String): ObservationListDto { - val result = this.observationRepository.getObservations(topicId, subjectId) + fun getObservations(projectId: String, subjectId: String, topicId: String): ObservationListDto { + val result = this.observationRepository.getObservations(projectId = projectId, topicId = topicId, subjectId = subjectId) return ObservationListDto( result.map { it.toDto() }, ) diff --git a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml index 41c6b51..32c9ad9 100644 --- a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml +++ b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml @@ -28,9 +28,15 @@ + + + + + + @@ -47,6 +53,7 @@ + diff --git a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv index c7f744e..2d8302c 100644 --- a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv +++ b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv @@ -1,9 +1,9 @@ -subject;topic;category;variable;value_numeric;value_textual;date;end_date -sub-1;questionnaire_answer;baseline_questions;Perceived_Pain_Score;5;NULL;2021-02-20 00:00:00;NULL -sub-1;questionnaire_answer;followup_questions;Name_Of_Physician;NULL;Dr.J.Adams;2021-02-20 00:00:00;NULL -sub-2;questionnaire_answer;baseline_questions;Perceived_Pain_Score;2;NULL;2021-02-20 00:00:00;NULL -sub-2;questionnaire_answer;followup_questions;Name_Of_Physician;NULL;Dr.G.Washington;2022-05-20 00:00:00;NULL -sub-1;phone_battery_level;NULL;batteryLevel;5;NULL;2021-02-20 00:00:00;NULL -sub-1;phone_battery_level;NULL;status;NULL;CHARGING;2021-02-20 00:00:00;NULL -sub-2;phone_battery_level;NULL;batteryLevel;10;NULL;2021-02-20 00:00:00;NULL -sub-2;phone_battery_level;NULL;status;NULL;FULL;2021-02-20 00:00:00;NULL +project;subject;source;topic;category;variable;value_numeric;value_textual;date;end_date +project-1;sub-1;source-1;questionnaire_answer;baseline_questions;Perceived_Pain_Score;5;NULL;2021-02-20 00:00:00;NULL +project-1;sub-1;source-1;questionnaire_answer;followup_questions;Name_Of_Physician;NULL;Dr.J.Adams;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;questionnaire_answer;baseline_questions;Perceived_Pain_Score;2;NULL;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;questionnaire_answer;followup_questions;Name_Of_Physician;NULL;Dr.G.Washington;2022-05-20 00:00:00;NULL +project-1;sub-1;source-1;phone_battery_level;NULL;batteryLevel;5;NULL;2021-02-20 00:00:00;NULL +project-1;sub-1;source-1;phone_battery_level;NULL;status;NULL;CHARGING;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;phone_battery_level;NULL;batteryLevel;10;NULL;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;phone_battery_level;NULL;status;NULL;FULL;2021-02-20 00:00:00;NULL diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt index 07ea8a5..b73e411 100644 --- a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt @@ -71,13 +71,13 @@ class DashboardIntegrationTest: JerseyTest() { @Test fun testGetObservationsNoToken() { - val response = target("subject/sub-1/topic/phone_battery_level/observations").request().get() + val response = target("project/project-1/sub-1/topic/phone_battery_level/observations").request().get() Assertions.assertEquals(401, response.status) } @Test fun testGetObservationsWithToken() { - val response = target("subject/sub-1/topic/phone_battery_level/observations") + val response = target("project/project-1/subject/sub-1/topic/phone_battery_level/observations") .request() .header(HttpHeaders.AUTHORIZATION, "Bearer " + "... encoded token ...") .get() diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt index fdcd68c..20122d9 100644 --- a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt @@ -36,11 +36,12 @@ import java.time.ZonedDateTime class ObservationResourceTest: JerseyTest() { - @Mock lateinit var observationService: ObservationService private lateinit var observationListDto: ObservationListDto + private var observationId: Long = 1 + private val projectId = "project-1" private val subjectId = "sub-1" private val topicId = "topic-1" @@ -61,11 +62,7 @@ class ObservationResourceTest: JerseyTest() { @BeforeEach fun init() { // Create some fake observations that are returned by the service. - val obs1 = Observation(id = 1L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val obs2 = Observation(id = 2L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val obs3 = Observation(id = 3L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val obs4 = Observation(id = 4L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val observations: List = listOf(obs1, obs2, obs3, obs4) + val observations: List = listOf(createObservation(), createObservation(), createObservation(), createObservation()) // Create Dto that should be returned by the ObservationService. observationListDto = ObservationListDto( observations.map { it.toDto() }, @@ -75,9 +72,9 @@ class ObservationResourceTest: JerseyTest() { @Test fun testGetObservations() { // Instruct the mock to return the fake observations when called. - `when`(observationService.getObservations(subjectId = subjectId, topicId = topicId)).thenReturn(observationListDto) + `when`(observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId)).thenReturn(observationListDto) // Make the call to the REST endpoint. - val response = target("subject/sub-1/topic/topic-1/observations").request().get() + val response = target("project/project-1/subject/sub-1/topic/topic-1/observations").request().get() // Expect the http response to be OK and the same as the expected DTO. assertEquals(200, response.status) assertEquals(observationListDto, response.readEntity(ObservationListDto::class.java)) @@ -85,14 +82,36 @@ class ObservationResourceTest: JerseyTest() { @Test fun testGetObservations_failNoSubjectId() { - val response = target("subject//topic/topic-1/observations").request().get() + val response = target("project/project-1/subject//topic/topic-1/observations").request().get() assertEquals(404, response.status) } @Test fun testGetObservations_failNoTopicId() { - val response = target("subject/sub-1/topic//observations").request().get() + val response = target("project/project-1/subject/sub-1/topic//observations").request().get() + assertEquals(404, response.status) + } + + @Test + fun testGetObservations_failNoProjectId() { + val response = target("project//subject/sub-1/topic/topic-1/observations").request().get() assertEquals(404, response.status) } + private fun createObservation(): Observation { + return Observation( + id = observationId, + project = "project-1", + subject = subjectId, + source = "source-1", + topic = "topic-1", + category = "category-1", + variable = "variable-1", + date = ZonedDateTime.now(), + valueTextual = "value1", + valueNumeric = null, + endDate = null + ) + } + } \ No newline at end of file diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt index 5fb1e1d..3d1b2f0 100644 --- a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt @@ -31,12 +31,15 @@ import java.time.ZonedDateTime class ObservationServiceTest { - val subjectId = "sub-1" - // Create a Mockito mock of the ObservationRepository. This is instantiated in the init block. @Mock private lateinit var observationRepository: ObservationRepository + private var observationId: Long = 1 + private val projectId = "project-1" + private val subjectId = "sub-1" + private val topicId = "topic-1" + private val observationService: ObservationService init { @@ -53,17 +56,13 @@ class ObservationServiceTest { // Create some fake observations that are returned by the repository. // Each observation is linked to a Variable. - val obs1 = Observation(id = 1L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val obs2 = Observation(id = 2L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val obs3 = Observation(id = 3L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val obs4 = Observation(id = 4L, subject = subjectId, topic = "topic-1", category = "category-1", variable = "variable-1", date = ZonedDateTime.now(), valueTextual = "value1", valueNumeric = null, endDate = null) - val observations = listOf(obs1, obs2, obs3, obs4) + val observations: List = listOf(createObservation(), createObservation(), createObservation(), createObservation()) // Mock the repository to return the fake observations. - `when`(observationRepository.getObservations(subjectId = subjectId, topicId = "topic-1")).thenReturn(observations) + `when`(observationRepository.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId)).thenReturn(observations) // Call the ObservationService (class under test) to get the observations. - val result = observationService.getObservations(subjectId = subjectId, topicId = "topic-1") + val result = observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId) // Check if the result is as expected (observations transformed to ObservationListDto). val expectedDto = ObservationListDto( @@ -72,4 +71,20 @@ class ObservationServiceTest { assertEquals(expectedDto, result) } + private fun createObservation(): Observation { + return Observation( + id = observationId, + project = "project-1", + subject = subjectId, + source = "source-1", + topic = "topic-1", + category = "category-1", + variable = "variable-1", + date = ZonedDateTime.now(), + valueTextual = "value1", + valueNumeric = null, + endDate = null + ) + } + } \ No newline at end of file