Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merge develop into main #68

Merged
merged 3 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.10.0] - 2024-007-30
### Added
- Add admin GET survey responses API [#66](https://github.com/rokwire/surveys-building-block/issues/66)
### Added
- Sort Order when showing all (public) surveys [#64](https://github.com/rokwire/surveys-building-block/issues/64)

## [1.9.0] - 2024-007-26
### Added
- Add "complete" field to show if the survey is completed [#61](https://github.com/rokwire/surveys-building-block/issues/61)
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Patches for **Surveys Building Block** in this repository will only be applied t

| Version | Supported |
| ------- | ------------------ |
| 1.9.0 | :white_check_mark: |
| < 1.9.0 | :x: |
| 1.10.0 | :white_check_mark: |
| < 1.10.0 | :x: |

## Reporting a Bug or Vulnerability

Expand Down
30 changes: 30 additions & 0 deletions core/app_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,36 @@ func (a appAdmin) GetAllSurveyResponses(orgID string, appID string, surveyID str
return allResponses, nil
}

// GetAllSurveysResponses returns survey responses matching the provided query
func (a appAdmin) GetAllSurveysResponses(orgID string, appID string, surveyID string, userID string, externalIDs map[string]string, startDate *time.Time, endDate *time.Time, limit *int, offset *int) ([]model.SurveyResponse, error) {
var allResponses []model.SurveyResponse
var err error

survey, err := a.app.shared.getSurvey(surveyID, orgID, appID)
if err != nil {
return nil, err
}

// Check if survey is sensitive
if survey.Sensitive {
return nil, errors.Newf("Survey is sensitive and responses are not available")
}

allResponses, err = a.app.storage.GetSurveyResponses(&orgID, &appID, nil, []string{surveyID}, nil, startDate, endDate, limit, offset)
if err != nil {
return nil, err
}

// If survey is anonymous strip userIDs
if survey.Anonymous {
for i := range allResponses {
allResponses[i].UserID = ""
}
}

return allResponses, nil
}

// CreateSurvey creates a new survey
func (a appAdmin) CreateSurvey(survey model.Survey, externalIDs map[string]string) (*model.Survey, error) {
return a.app.shared.createSurvey(survey, externalIDs)
Expand Down
1 change: 1 addition & 0 deletions core/interfaces/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Admin interface {

// Survey Responses
GetAllSurveyResponses(orgID string, appID string, surveyID string, userID string, externalIDs map[string]string, startDate *time.Time, endDate *time.Time, limit *int, offset *int) ([]model.SurveyResponse, error)
GetAllSurveysResponses(orgID string, appID string, surveyID string, userID string, externalIDs map[string]string, startDate *time.Time, endDate *time.Time, limit *int, offset *int) ([]model.SurveyResponse, error)

// Alert Contacts
GetAlertContacts(orgID string, appID string) ([]model.AlertContact, error)
Expand Down
1 change: 1 addition & 0 deletions driver/web/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (a Adapter) Start() {
adminRouter.HandleFunc("/surveys/{id}", a.wrapFunc(a.adminAPIsHandler.updateSurvey, a.auth.admin.Permissions)).Methods("PUT")
adminRouter.HandleFunc("/surveys/{id}", a.wrapFunc(a.adminAPIsHandler.deleteSurvey, a.auth.admin.Permissions)).Methods("DELETE")
adminRouter.HandleFunc("/surveys/{id}/responses", a.wrapFunc(a.adminAPIsHandler.getAllSurveyResponses, a.auth.admin.User)).Methods("GET")
adminRouter.HandleFunc("/surveys/{id}/response", a.wrapFunc(a.adminAPIsHandler.getAllSurveysResponses, a.auth.admin.Permissions)).Methods("GET")

adminRouter.HandleFunc("/alert-contacts", a.wrapFunc(a.adminAPIsHandler.getAlertContacts, a.auth.admin.Permissions)).Methods("GET")
adminRouter.HandleFunc("/alert-contacts/{id}", a.wrapFunc(a.adminAPIsHandler.getAlertContact, a.auth.admin.Permissions)).Methods("GET")
Expand Down
2 changes: 2 additions & 0 deletions driver/web/admin_permission_policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ p, update_configs_surveys, /surveys/api/admin/configs/*, (GET)|(PUT), Update sur
p, update_configs_surveys, /surveys/api/admin/configs, (GET)|(POST),
p, delete_configs_surveys, /surveys/api/admin/configs/*, (GET)|(DELETE), Delete surveys configs
p, delete_configs_surveys, /surveys/api/admin/configs, (GET),

p, get_survey_responses, /surveys/api/admin/surveys/*/response, (GET),
57 changes: 57 additions & 0 deletions driver/web/apis_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,64 @@ func (h AdminAPIsHandler) getAllSurveyResponses(l *logs.Log, r *http.Request, cl
}
return l.HTTPResponseSuccessJSON(data)
}
func (h AdminAPIsHandler) getAllSurveysResponses(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse {
vars := mux.Vars(r)
id := vars["id"]
if len(id) <= 0 {
return l.HTTPResponseErrorData(logutils.StatusMissing, logutils.TypePathParam, logutils.StringArgs("id"), nil, http.StatusBadRequest, false)
}

startDateRaw := r.URL.Query().Get("start_date")
var startDate *time.Time
if len(startDateRaw) > 0 {
dateParsed, err := time.Parse(time.RFC3339, startDateRaw)
if err != nil {
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("start_date"), nil, http.StatusBadRequest, false)
}
startDate = &dateParsed
}

endDateRaw := r.URL.Query().Get("end_date")
var endDate *time.Time
if len(endDateRaw) > 0 {
dateParsed, err := time.Parse(time.RFC3339, endDateRaw)
if err != nil {
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("end_date"), nil, http.StatusBadRequest, false)
}
endDate = &dateParsed
}

limitRaw := r.URL.Query().Get("limit")
limit := 20
if len(limitRaw) > 0 {
intParsed, err := strconv.Atoi(limitRaw)
if err != nil {
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("limit"), nil, http.StatusBadRequest, false)
}
limit = intParsed
}

offsetRaw := r.URL.Query().Get("offset")
offset := 0
if len(offsetRaw) > 0 {
intParsed, err := strconv.Atoi(offsetRaw)
if err != nil {
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("offset"), nil, http.StatusBadRequest, false)
}
offset = intParsed
}

resData, err := h.app.Admin.GetAllSurveysResponses(claims.OrgID, claims.AppID, id, claims.Subject, claims.ExternalIDs, startDate, endDate, &limit, &offset)
if err != nil {
return l.HTTPResponseErrorAction(logutils.ActionGet, model.TypeSurvey, nil, err, http.StatusInternalServerError, true)
}

data, err := json.Marshal(resData)
if err != nil {
return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.TypeResponseBody, nil, err, http.StatusInternalServerError, false)
}
return l.HTTPResponseSuccessJSON(data)
}
func (h AdminAPIsHandler) getAlertContacts(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse {
resData, err := h.app.Admin.GetAlertContacts(claims.OrgID, claims.AppID)
if err != nil {
Expand Down
9 changes: 3 additions & 6 deletions driver/web/apis_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"io/ioutil"
"log"
"net/http"
"sort"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -151,12 +150,10 @@ func (h ClientAPIsHandler) getSurveys(l *logs.Log, r *http.Request, claims *toke
return l.HTTPResponseErrorAction(logutils.ActionGet, model.TypeSurvey, nil, err, http.StatusInternalServerError, true)
}

resData := getSurveysResData(surveys, surverysRsponse, completed)
sort.Slice(resData, func(i, j int) bool {
return resData[i].DateCreated.After(resData[j].DateCreated)
})
list := getSurveysResData(surveys, surverysRsponse, completed)
respData := sortIfpublicIsTrue(list, public)

rdata, err := json.Marshal(resData)
rdata, err := json.Marshal(respData)
if err != nil {
return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.TypeResponseBody, nil, err, http.StatusInternalServerError, false)
}
Expand Down
59 changes: 59 additions & 0 deletions driver/web/convertions_surveys.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package web

import (
"application/core/model"
"sort"
"time"
)

Expand Down Expand Up @@ -139,9 +140,67 @@ func getSurveysResData(items []model.Survey, surveyResponses []model.SurveyRespo
Archived: item.Archived,
EstimatedCompletionTime: item.EstimatedCompletionTime,
Completed: &isCompleted,
DateCreated: item.DateCreated,
})
}
}
sort.Slice(list, func(i, j int) bool {
return list[i].DateCreated.After(list[j].DateCreated)
})

return list
}

func sortIfpublicIsTrue(list []model.SurveysResponseData, public *bool) []model.SurveysResponseData {

if public == nil || !*public {
// If public is nil or false, just return the list as-is
return list
}

var incompleteSurveys, noEndDateSurveys, completedSurveys []model.SurveysResponseData

// Split surveys into categories based on completion status and end date
for _, survey := range list {
if survey.Completed != nil && *survey.Completed {
completedSurveys = append(completedSurveys, survey)
} else if survey.EndDate != nil {
incompleteSurveys = append(incompleteSurveys, survey)
} else {
noEndDateSurveys = append(noEndDateSurveys, survey)
}
}

// Sort incomplete surveys by end date (ascending)
sort.Slice(incompleteSurveys, func(i, j int) bool {
return incompleteSurveys[i].EndDate.Before(*incompleteSurveys[j].EndDate)
})

// Sort no-end-date surveys first by start date (descending) or by creation date if start date is missing
sort.Slice(noEndDateSurveys, func(i, j int) bool {
if noEndDateSurveys[i].StartDate != nil && noEndDateSurveys[j].StartDate != nil {
return noEndDateSurveys[i].StartDate.After(*noEndDateSurveys[j].StartDate)
}
if noEndDateSurveys[i].StartDate != nil {
return true
}
if noEndDateSurveys[j].StartDate != nil {
return false
}
return noEndDateSurveys[i].DateCreated.After(noEndDateSurveys[j].DateCreated)
})

// Sort completed surveys by estimated completion time (descending)
sort.Slice(completedSurveys, func(i, j int) bool {
if completedSurveys[i].EstimatedCompletionTime != nil && completedSurveys[j].EstimatedCompletionTime != nil {
return *completedSurveys[i].EstimatedCompletionTime > *completedSurveys[j].EstimatedCompletionTime
}
return completedSurveys[i].DateCreated.After(completedSurveys[j].DateCreated)
})

// Combine all sorted slices
result := append(incompleteSurveys, noEndDateSurveys...)
result = append(result, completedSurveys...)

return result
}
69 changes: 68 additions & 1 deletion driver/web/docs/gen/def.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Rokwire Surveys Building Block API
description: Surveys Building Block API Documentation
version: 1.9.0
version: 1.10.0
servers:
- url: 'https://api.rokwire.illinois.edu/surveys'
description: Production server
Expand Down Expand Up @@ -1354,6 +1354,73 @@ paths:
description: Forbidden
'500':
description: Internal error
'/api/admin/surveys/{id}/response':
get:
tags:
- Admin
summary: Retrieves all survey responses for specified survey
description: |
Retrieves all survey responses for specified survey

**Auth:** Requires admin token with `get_survey_responses` permission
security:
- bearerAuth: []
parameters:
- name: id
in: path
description: id
required: true
style: simple
explode: false
schema:
type: string
- name: start_date
in: query
description: The start of the date range to search for
required: false
style: simple
explode: false
schema:
type: string
- name: end_date
in: query
description: The end of the date range to search for
required: false
style: simple
explode: false
schema:
type: string
- name: limit
in: query
description: The number of results to be loaded in one page
required: false
style: simple
explode: false
schema:
type: number
- name: offset
in: query
description: The number of results previously loaded
required: false
style: simple
explode: false
schema:
type: number
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SurveyResponse'
'400':
description: Bad request
'401':
description: Unauthorized
'500':
description: Internal error
/api/analytics/survey-responses:
get:
tags:
Expand Down
4 changes: 3 additions & 1 deletion driver/web/docs/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Rokwire Surveys Building Block API
description: Surveys Building Block API Documentation
version: 1.9.0
version: 1.10.0
servers:
- url: 'https://api.rokwire.illinois.edu/surveys'
description: Production server
Expand Down Expand Up @@ -62,6 +62,8 @@ paths:
$ref: "./resources/admin/alert-contact.yaml"
/api/admin/alert-contacts/{id}:
$ref: "./resources/admin/alert-contactids.yaml"
/api/admin/surveys/{id}/response:
$ref: "./resources/admin/surveys_responses.yaml"

# Analytics
/api/analytics/survey-responses:
Expand Down
Loading
Loading