Skip to content

Commit 60a409f

Browse files
authored
merge develop into main (#68)
2 parents c06614e + edd9cd0 commit 60a409f

12 files changed

+298
-10
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
## [1.10.0] - 2024-007-30
9+
### Added
10+
- Add admin GET survey responses API [#66](https://github.com/rokwire/surveys-building-block/issues/66)
11+
### Added
12+
- Sort Order when showing all (public) surveys [#64](https://github.com/rokwire/surveys-building-block/issues/64)
13+
814
## [1.9.0] - 2024-007-26
915
### Added
1016
- Add "complete" field to show if the survey is completed [#61](https://github.com/rokwire/surveys-building-block/issues/61)

SECURITY.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Patches for **Surveys Building Block** in this repository will only be applied t
66

77
| Version | Supported |
88
| ------- | ------------------ |
9-
| 1.9.0 | :white_check_mark: |
10-
| < 1.9.0 | :x: |
9+
| 1.10.0 | :white_check_mark: |
10+
| < 1.10.0 | :x: |
1111

1212
## Reporting a Bug or Vulnerability
1313

core/app_admin.go

+30
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,36 @@ func (a appAdmin) GetAllSurveyResponses(orgID string, appID string, surveyID str
8484
return allResponses, nil
8585
}
8686

87+
// GetAllSurveysResponses returns survey responses matching the provided query
88+
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) {
89+
var allResponses []model.SurveyResponse
90+
var err error
91+
92+
survey, err := a.app.shared.getSurvey(surveyID, orgID, appID)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
// Check if survey is sensitive
98+
if survey.Sensitive {
99+
return nil, errors.Newf("Survey is sensitive and responses are not available")
100+
}
101+
102+
allResponses, err = a.app.storage.GetSurveyResponses(&orgID, &appID, nil, []string{surveyID}, nil, startDate, endDate, limit, offset)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
// If survey is anonymous strip userIDs
108+
if survey.Anonymous {
109+
for i := range allResponses {
110+
allResponses[i].UserID = ""
111+
}
112+
}
113+
114+
return allResponses, nil
115+
}
116+
87117
// CreateSurvey creates a new survey
88118
func (a appAdmin) CreateSurvey(survey model.Survey, externalIDs map[string]string) (*model.Survey, error) {
89119
return a.app.shared.createSurvey(survey, externalIDs)

core/interfaces/core.go

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Admin interface {
6666

6767
// Survey Responses
6868
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)
69+
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)
6970

7071
// Alert Contacts
7172
GetAlertContacts(orgID string, appID string) ([]model.AlertContact, error)

driver/web/adapter.go

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func (a Adapter) Start() {
102102
adminRouter.HandleFunc("/surveys/{id}", a.wrapFunc(a.adminAPIsHandler.updateSurvey, a.auth.admin.Permissions)).Methods("PUT")
103103
adminRouter.HandleFunc("/surveys/{id}", a.wrapFunc(a.adminAPIsHandler.deleteSurvey, a.auth.admin.Permissions)).Methods("DELETE")
104104
adminRouter.HandleFunc("/surveys/{id}/responses", a.wrapFunc(a.adminAPIsHandler.getAllSurveyResponses, a.auth.admin.User)).Methods("GET")
105+
adminRouter.HandleFunc("/surveys/{id}/response", a.wrapFunc(a.adminAPIsHandler.getAllSurveysResponses, a.auth.admin.Permissions)).Methods("GET")
105106

106107
adminRouter.HandleFunc("/alert-contacts", a.wrapFunc(a.adminAPIsHandler.getAlertContacts, a.auth.admin.Permissions)).Methods("GET")
107108
adminRouter.HandleFunc("/alert-contacts/{id}", a.wrapFunc(a.adminAPIsHandler.getAlertContact, a.auth.admin.Permissions)).Methods("GET")

driver/web/admin_permission_policy.csv

+2
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ p, update_configs_surveys, /surveys/api/admin/configs/*, (GET)|(PUT), Update sur
2626
p, update_configs_surveys, /surveys/api/admin/configs, (GET)|(POST),
2727
p, delete_configs_surveys, /surveys/api/admin/configs/*, (GET)|(DELETE), Delete surveys configs
2828
p, delete_configs_surveys, /surveys/api/admin/configs, (GET),
29+
30+
p, get_survey_responses, /surveys/api/admin/surveys/*/response, (GET),

driver/web/apis_admin.go

+57
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,64 @@ func (h AdminAPIsHandler) getAllSurveyResponses(l *logs.Log, r *http.Request, cl
417417
}
418418
return l.HTTPResponseSuccessJSON(data)
419419
}
420+
func (h AdminAPIsHandler) getAllSurveysResponses(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse {
421+
vars := mux.Vars(r)
422+
id := vars["id"]
423+
if len(id) <= 0 {
424+
return l.HTTPResponseErrorData(logutils.StatusMissing, logutils.TypePathParam, logutils.StringArgs("id"), nil, http.StatusBadRequest, false)
425+
}
426+
427+
startDateRaw := r.URL.Query().Get("start_date")
428+
var startDate *time.Time
429+
if len(startDateRaw) > 0 {
430+
dateParsed, err := time.Parse(time.RFC3339, startDateRaw)
431+
if err != nil {
432+
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("start_date"), nil, http.StatusBadRequest, false)
433+
}
434+
startDate = &dateParsed
435+
}
436+
437+
endDateRaw := r.URL.Query().Get("end_date")
438+
var endDate *time.Time
439+
if len(endDateRaw) > 0 {
440+
dateParsed, err := time.Parse(time.RFC3339, endDateRaw)
441+
if err != nil {
442+
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("end_date"), nil, http.StatusBadRequest, false)
443+
}
444+
endDate = &dateParsed
445+
}
420446

447+
limitRaw := r.URL.Query().Get("limit")
448+
limit := 20
449+
if len(limitRaw) > 0 {
450+
intParsed, err := strconv.Atoi(limitRaw)
451+
if err != nil {
452+
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("limit"), nil, http.StatusBadRequest, false)
453+
}
454+
limit = intParsed
455+
}
456+
457+
offsetRaw := r.URL.Query().Get("offset")
458+
offset := 0
459+
if len(offsetRaw) > 0 {
460+
intParsed, err := strconv.Atoi(offsetRaw)
461+
if err != nil {
462+
return l.HTTPResponseErrorData(logutils.StatusInvalid, logutils.TypeQueryParam, logutils.StringArgs("offset"), nil, http.StatusBadRequest, false)
463+
}
464+
offset = intParsed
465+
}
466+
467+
resData, err := h.app.Admin.GetAllSurveysResponses(claims.OrgID, claims.AppID, id, claims.Subject, claims.ExternalIDs, startDate, endDate, &limit, &offset)
468+
if err != nil {
469+
return l.HTTPResponseErrorAction(logutils.ActionGet, model.TypeSurvey, nil, err, http.StatusInternalServerError, true)
470+
}
471+
472+
data, err := json.Marshal(resData)
473+
if err != nil {
474+
return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.TypeResponseBody, nil, err, http.StatusInternalServerError, false)
475+
}
476+
return l.HTTPResponseSuccessJSON(data)
477+
}
421478
func (h AdminAPIsHandler) getAlertContacts(l *logs.Log, r *http.Request, claims *tokenauth.Claims) logs.HTTPResponse {
422479
resData, err := h.app.Admin.GetAlertContacts(claims.OrgID, claims.AppID)
423480
if err != nil {

driver/web/apis_client.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"io/ioutil"
2222
"log"
2323
"net/http"
24-
"sort"
2524
"strconv"
2625
"strings"
2726
"time"
@@ -151,12 +150,10 @@ func (h ClientAPIsHandler) getSurveys(l *logs.Log, r *http.Request, claims *toke
151150
return l.HTTPResponseErrorAction(logutils.ActionGet, model.TypeSurvey, nil, err, http.StatusInternalServerError, true)
152151
}
153152

154-
resData := getSurveysResData(surveys, surverysRsponse, completed)
155-
sort.Slice(resData, func(i, j int) bool {
156-
return resData[i].DateCreated.After(resData[j].DateCreated)
157-
})
153+
list := getSurveysResData(surveys, surverysRsponse, completed)
154+
respData := sortIfpublicIsTrue(list, public)
158155

159-
rdata, err := json.Marshal(resData)
156+
rdata, err := json.Marshal(respData)
160157
if err != nil {
161158
return l.HTTPResponseErrorAction(logutils.ActionMarshal, logutils.TypeResponseBody, nil, err, http.StatusInternalServerError, false)
162159
}

driver/web/convertions_surveys.go

+59
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package web
22

33
import (
44
"application/core/model"
5+
"sort"
56
"time"
67
)
78

@@ -139,9 +140,67 @@ func getSurveysResData(items []model.Survey, surveyResponses []model.SurveyRespo
139140
Archived: item.Archived,
140141
EstimatedCompletionTime: item.EstimatedCompletionTime,
141142
Completed: &isCompleted,
143+
DateCreated: item.DateCreated,
142144
})
143145
}
144146
}
147+
sort.Slice(list, func(i, j int) bool {
148+
return list[i].DateCreated.After(list[j].DateCreated)
149+
})
145150

146151
return list
147152
}
153+
154+
func sortIfpublicIsTrue(list []model.SurveysResponseData, public *bool) []model.SurveysResponseData {
155+
156+
if public == nil || !*public {
157+
// If public is nil or false, just return the list as-is
158+
return list
159+
}
160+
161+
var incompleteSurveys, noEndDateSurveys, completedSurveys []model.SurveysResponseData
162+
163+
// Split surveys into categories based on completion status and end date
164+
for _, survey := range list {
165+
if survey.Completed != nil && *survey.Completed {
166+
completedSurveys = append(completedSurveys, survey)
167+
} else if survey.EndDate != nil {
168+
incompleteSurveys = append(incompleteSurveys, survey)
169+
} else {
170+
noEndDateSurveys = append(noEndDateSurveys, survey)
171+
}
172+
}
173+
174+
// Sort incomplete surveys by end date (ascending)
175+
sort.Slice(incompleteSurveys, func(i, j int) bool {
176+
return incompleteSurveys[i].EndDate.Before(*incompleteSurveys[j].EndDate)
177+
})
178+
179+
// Sort no-end-date surveys first by start date (descending) or by creation date if start date is missing
180+
sort.Slice(noEndDateSurveys, func(i, j int) bool {
181+
if noEndDateSurveys[i].StartDate != nil && noEndDateSurveys[j].StartDate != nil {
182+
return noEndDateSurveys[i].StartDate.After(*noEndDateSurveys[j].StartDate)
183+
}
184+
if noEndDateSurveys[i].StartDate != nil {
185+
return true
186+
}
187+
if noEndDateSurveys[j].StartDate != nil {
188+
return false
189+
}
190+
return noEndDateSurveys[i].DateCreated.After(noEndDateSurveys[j].DateCreated)
191+
})
192+
193+
// Sort completed surveys by estimated completion time (descending)
194+
sort.Slice(completedSurveys, func(i, j int) bool {
195+
if completedSurveys[i].EstimatedCompletionTime != nil && completedSurveys[j].EstimatedCompletionTime != nil {
196+
return *completedSurveys[i].EstimatedCompletionTime > *completedSurveys[j].EstimatedCompletionTime
197+
}
198+
return completedSurveys[i].DateCreated.After(completedSurveys[j].DateCreated)
199+
})
200+
201+
// Combine all sorted slices
202+
result := append(incompleteSurveys, noEndDateSurveys...)
203+
result = append(result, completedSurveys...)
204+
205+
return result
206+
}

driver/web/docs/gen/def.yaml

+68-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.0.3
22
info:
33
title: Rokwire Surveys Building Block API
44
description: Surveys Building Block API Documentation
5-
version: 1.9.0
5+
version: 1.10.0
66
servers:
77
- url: 'https://api.rokwire.illinois.edu/surveys'
88
description: Production server
@@ -1354,6 +1354,73 @@ paths:
13541354
description: Forbidden
13551355
'500':
13561356
description: Internal error
1357+
'/api/admin/surveys/{id}/response':
1358+
get:
1359+
tags:
1360+
- Admin
1361+
summary: Retrieves all survey responses for specified survey
1362+
description: |
1363+
Retrieves all survey responses for specified survey
1364+
1365+
**Auth:** Requires admin token with `get_survey_responses` permission
1366+
security:
1367+
- bearerAuth: []
1368+
parameters:
1369+
- name: id
1370+
in: path
1371+
description: id
1372+
required: true
1373+
style: simple
1374+
explode: false
1375+
schema:
1376+
type: string
1377+
- name: start_date
1378+
in: query
1379+
description: The start of the date range to search for
1380+
required: false
1381+
style: simple
1382+
explode: false
1383+
schema:
1384+
type: string
1385+
- name: end_date
1386+
in: query
1387+
description: The end of the date range to search for
1388+
required: false
1389+
style: simple
1390+
explode: false
1391+
schema:
1392+
type: string
1393+
- name: limit
1394+
in: query
1395+
description: The number of results to be loaded in one page
1396+
required: false
1397+
style: simple
1398+
explode: false
1399+
schema:
1400+
type: number
1401+
- name: offset
1402+
in: query
1403+
description: The number of results previously loaded
1404+
required: false
1405+
style: simple
1406+
explode: false
1407+
schema:
1408+
type: number
1409+
responses:
1410+
'200':
1411+
description: Success
1412+
content:
1413+
application/json:
1414+
schema:
1415+
type: array
1416+
items:
1417+
$ref: '#/components/schemas/SurveyResponse'
1418+
'400':
1419+
description: Bad request
1420+
'401':
1421+
description: Unauthorized
1422+
'500':
1423+
description: Internal error
13571424
/api/analytics/survey-responses:
13581425
get:
13591426
tags:

driver/web/docs/index.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.0.3
22
info:
33
title: Rokwire Surveys Building Block API
44
description: Surveys Building Block API Documentation
5-
version: 1.9.0
5+
version: 1.10.0
66
servers:
77
- url: 'https://api.rokwire.illinois.edu/surveys'
88
description: Production server
@@ -62,6 +62,8 @@ paths:
6262
$ref: "./resources/admin/alert-contact.yaml"
6363
/api/admin/alert-contacts/{id}:
6464
$ref: "./resources/admin/alert-contactids.yaml"
65+
/api/admin/surveys/{id}/response:
66+
$ref: "./resources/admin/surveys_responses.yaml"
6567

6668
# Analytics
6769
/api/analytics/survey-responses:

0 commit comments

Comments
 (0)