diff --git a/controllers/cohortdata.go b/controllers/cohortdata.go index b780d328..cd5b738c 100644 --- a/controllers/cohortdata.go +++ b/controllers/cohortdata.go @@ -76,6 +76,54 @@ func (u CohortDataController) RetrieveHistogramForCohortIdAndConceptId(c *gin.Co c.JSON(http.StatusOK, gin.H{"bins": histogramData}) } +func (u CohortDataController) RetrieveStatsForCohortIdAndConceptId(c *gin.Context) { + sourceIdStr := c.Param("sourceid") + log.Printf("Querying source: %s", sourceIdStr) + cohortIdStr := c.Param("cohortid") + log.Printf("Querying cohort for cohort definition id: %s", cohortIdStr) + conceptIdStr := c.Param("conceptid") + if sourceIdStr == "" || cohortIdStr == "" || conceptIdStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "bad request"}) + c.Abort() + return + } + + filterConceptIds, cohortPairs, err := utils.ParseConceptIdsAndDichotomousDefs(c) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Error parsing request body for prefixed concept ids", "error": err.Error()}) + c.Abort() + return + } + + sourceId, _ := strconv.Atoi(sourceIdStr) + cohortId, _ := strconv.Atoi(cohortIdStr) + conceptId, _ := strconv.ParseInt(conceptIdStr, 10, 64) + + validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{cohortId}, cohortPairs) + if !validAccessRequest { + log.Printf("Error: invalid request") + c.JSON(http.StatusForbidden, gin.H{"message": "access denied"}) + c.Abort() + return + } + + cohortData, err := u.cohortDataModel.RetrieveHistogramDataBySourceIdAndCohortIdAndConceptIdsAndCohortPairs(sourceId, cohortId, conceptId, filterConceptIds, cohortPairs) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Error retrieving concept details", "error": err.Error()}) + c.Abort() + return + } + + conceptValues := []float64{} + for _, personData := range cohortData { + conceptValues = append(conceptValues, float64(*personData.ConceptValueAsNumber)) + } + + statsData := utils.GenerateStatsData(cohortId, conceptId, conceptValues) + + c.JSON(http.StatusOK, gin.H{"statsData": statsData}) +} + func (u CohortDataController) RetrieveDataBySourceIdAndCohortIdAndVariables(c *gin.Context) { // TODO - add some validation to ensure that only calls from Argo are allowed through since it outputs FULL data? diff --git a/server/router.go b/server/router.go index 75a633dd..b8cf169d 100644 --- a/server/router.go +++ b/server/router.go @@ -53,6 +53,9 @@ func NewRouter() *gin.Engine { // full data endpoints: authorized.POST("/cohort-data/by-source-id/:sourceid/by-cohort-definition-id/:cohortid", cohortData.RetrieveDataBySourceIdAndCohortIdAndVariables) + // cohort data statistics + authorized.POST("/cohort-data/stats/by-source-id/:sourceid/by-cohort-definition-id/:cohortid/by-concept-id/:conceptid", cohortData.RetrieveStatsForCohortIdAndConceptId) + // histogram endpoint authorized.POST("/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortid/by-histogram-concept-id/:histogramid", cohortData.RetrieveHistogramForCohortIdAndConceptId) diff --git a/tests/controllers_tests/controllers_test.go b/tests/controllers_tests/controllers_test.go index df8e187a..bff6b043 100644 --- a/tests/controllers_tests/controllers_test.go +++ b/tests/controllers_tests/controllers_test.go @@ -1200,3 +1200,53 @@ func TestGenerateDataDictionary(t *testing.T) { } } + +func TestRetrieveStatsForCohortIdAndConceptIdWithWrongParams(t *testing.T) { + setUp(t) + requestContext := new(gin.Context) + requestContext.Params = append(requestContext.Params, gin.Param{Key: "sourceid", Value: strconv.Itoa(tests.GetTestSourceId())}) + requestContext.Params = append(requestContext.Params, gin.Param{Key: "cohortid", Value: "4"}) + requestContext.Writer = new(tests.CustomResponseWriter) + requestContext.Request = new(http.Request) + requestBody := "{\"variables\":[{\"variable_type\": \"custom_dichotomous\", \"cohort_ids\": [1, 3]}]}" + requestContext.Request.Body = io.NopCloser(strings.NewReader(requestBody)) + //requestContext.Writer = new(tests.CustomResponseWriter) + cohortDataController.RetrieveStatsForCohortIdAndConceptId(requestContext) + // Params above are wrong, so request should abort: + if !requestContext.IsAborted() { + t.Errorf("should have aborted") + } +} + +func TestRetrieveStatsForCohortIdAndConceptIdWithCorrectParams(t *testing.T) { + setUp(t) + requestContext := new(gin.Context) + requestContext.Params = append(requestContext.Params, gin.Param{Key: "sourceid", Value: strconv.Itoa(tests.GetTestSourceId())}) + requestContext.Params = append(requestContext.Params, gin.Param{Key: "cohortid", Value: "4"}) + requestContext.Params = append(requestContext.Params, gin.Param{Key: "conceptid", Value: "2000006885"}) + requestContext.Writer = new(tests.CustomResponseWriter) + requestContext.Request = new(http.Request) + requestBody := "{\"variables\":[{\"variable_type\": \"concept\", \"concept_id\": 2000000324},{\"variable_type\": \"custom_dichotomous\", \"cohort_ids\": [1, 3]}]}" + requestContext.Request.Body = io.NopCloser(strings.NewReader(requestBody)) + cohortDataController.RetrieveStatsForCohortIdAndConceptId(requestContext) + // Params above are correct, so request should NOT abort: + if requestContext.IsAborted() { + t.Errorf("Did not expect this request to abort") + } + result := requestContext.Writer.(*tests.CustomResponseWriter) + if !strings.Contains(result.CustomResponseWriterOut, "statsData") { + t.Errorf("Expected output starting with 'statsData,...'") + } + + // the same request should fail if the teamProject authorization fails: + requestContext.Request.Body = io.NopCloser(strings.NewReader(requestBody)) + cohortDataControllerWithFailingTeamProjectAuthz.RetrieveStatsForCohortIdAndConceptId(requestContext) + result = requestContext.Writer.(*tests.CustomResponseWriter) + // expect error: + if !strings.Contains(result.CustomResponseWriterOut, "access denied") { + t.Errorf("Expected 'access denied' as result") + } + if !requestContext.IsAborted() { + t.Errorf("Expected request to be aborted") + } +} diff --git a/tests/utils_tests/utils_test.go b/tests/utils_tests/utils_test.go index 67215469..3be84f35 100644 --- a/tests/utils_tests/utils_test.go +++ b/tests/utils_tests/utils_test.go @@ -316,3 +316,19 @@ func TestSubtract(t *testing.T) { t.Errorf("Expected [] but found %v", result) } } + +func TestGenerateStatsData(t *testing.T) { + setUp(t) + + var emptyData = []float64{} + result := utils.GenerateStatsData(1, 1, emptyData) + if result != nil { + t.Errorf("Expected a nil result for an empty data set") + } + + var expectedResult = &utils.ConceptStats{CohortId: 1, ConceptId: 1, NumberOfPeople: 11, Min: 6.0, Max: 49.0, Avg: 33.18181818181818, Sd: 15.134657288477642} + result = utils.GenerateStatsData(1, 1, testData) + if !reflect.DeepEqual(expectedResult, result) { + t.Errorf("Expected %v but found %v", expectedResult, result) + } +} diff --git a/utils/stats.go b/utils/stats.go new file mode 100644 index 00000000..cafe9326 --- /dev/null +++ b/utils/stats.go @@ -0,0 +1,44 @@ +package utils + +import ( + "log" + + "github.com/montanaflynn/stats" +) + +type ConceptStats struct { + CohortId int `json:"cohortId"` + ConceptId int64 `json:"conceptId"` + NumberOfPeople int `json:"personCount"` + Min float64 `json:"min"` + Max float64 `json:"max"` + Avg float64 `json:"avg"` + Sd float64 `json:"sd"` +} + +func GenerateStatsData(cohortId int, conceptId int64, conceptValues []float64) *ConceptStats { + + if len(conceptValues) == 0 { + log.Printf("Data size is zero. Returning nil.") + return nil + } + + result := new(ConceptStats) + result.CohortId = cohortId + result.ConceptId = conceptId + result.NumberOfPeople = len(conceptValues) + + minValue, _ := stats.Min(conceptValues) + result.Min = minValue + + maxValue, _ := stats.Max(conceptValues) + result.Max = maxValue + + meanValue, _ := stats.Mean(conceptValues) + result.Avg = meanValue + + sdValue, _ := stats.StandardDeviation(conceptValues) + result.Sd = sdValue + + return result +}