Skip to content

Commit

Permalink
feat: export questionnaire answers to xslx format spreadsheet (#134)
Browse files Browse the repository at this point in the history
closes #125
  • Loading branch information
talentedmrjones authored Oct 15, 2024
1 parent 75aab92 commit fac5630
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 8 deletions.
48 changes: 48 additions & 0 deletions backend/cmd/api/internal/controller/datacalls.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
package controller

import (
"fmt"
"net/http"
"strings"

"github.com/CMS-Enterprise/ztmf/backend/cmd/api/internal/auth"
"github.com/CMS-Enterprise/ztmf/backend/cmd/api/internal/model"
"github.com/CMS-Enterprise/ztmf/backend/cmd/api/internal/spreadsheet"
"github.com/gorilla/mux"
)

func ListDataCalls(w http.ResponseWriter, r *http.Request) {
datacalls, err := model.FindDataCalls(r.Context())
respond(w, r, datacalls, err)
}

func GetDatacallExport(w http.ResponseWriter, r *http.Request) {
user := auth.UserFromContext(r.Context())
input := model.FindAnswersInput{}

if !user.IsAdmin() {
input.UserID = &user.UserID
}

vars := mux.Vars(r)
if v, ok := vars["datacallid"]; ok {
fmt.Sscan(v, &input.DataCallID)
}

qVars := r.URL.Query()
if qVars.Has("fsids") {
for _, v := range qVars["fsids"] {
var fismaSystemID int32
fmt.Sscan(v, &fismaSystemID)
if !user.IsAdmin() && !user.IsAssignedFismaSystem(fismaSystemID) {
respond(w, r, nil, ErrForbidden)
return
}
input.FismaSystemIDs = append(input.FismaSystemIDs, &fismaSystemID)
}
}

answers, err := model.FindAnswers(r.Context(), input)
if err != nil {
respond(w, r, nil, err)
return
}

file, err := spreadsheet.Excel(answers)
if err != nil {
respond(w, r, nil, err)
return
}

w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", strings.ReplaceAll(answers[0].DataCall, " ", "")))
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
file.Write(w)
}
67 changes: 67 additions & 0 deletions backend/cmd/api/internal/model/answers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package model

import (
"context"
"log"

"github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5"
)

type Answer struct {
DataCall string
FismaSystemID int32
FismaAcronym string
DataCenterEnvironment string
Pillar string
Question string
Function string
Description string
OptionName string
Score int
Notes string
}

type FindAnswersInput struct {
FismaSystemIDs []*int32
DataCallID int32
UserID *string
}

// FindAnswers queries the DB and returns a fully comprehensive set of fields and values
// leveraging all the necessary joins that would otherwise require multiple DB calls
// if using lower-level methods such as FindFismaSystems, FindScores, FindQuestions, etc
// this is primarily meant for use in exporting to spreadsheets
func FindAnswers(ctx context.Context, input FindAnswersInput) ([]*Answer, error) {
sqlb := sqlBuilder.Select("datacalls.datacall, fismasystems.fismasystemid, fismasystems.fismaacronym, fismasystems.datacenterenvironment, pillars.pillar, questions.question, functions.function, functions.description, functionoptions.optionname, functionoptions.score, scores.notes").
From("scores").
InnerJoin("datacalls ON datacalls.datacallid=scores.datacallid AND datacalls.datacallid=?", input.DataCallID).
InnerJoin("fismasystems ON fismasystems.fismasystemid=scores.fismasystemid").
InnerJoin("functionoptions ON functionoptions.functionoptionid=scores.functionoptionid").
InnerJoin("functions ON functions.functionid=functionoptions.functionid").
InnerJoin("questions ON questions.questionid=functions.questionid").
InnerJoin("pillars ON pillars.pillarid=functions.pillarid").
OrderBy("fismasystems.fismasystemid, pillars.ordr, questions.ordr ASC")

if input.UserID != nil {
sqlb = sqlb.InnerJoin("users_fismasystems ON users_fismasystems.userid=? AND users_fismasystems.fismasystemid=fismasystems.fismasystemid", input.UserID)
}

if len(input.FismaSystemIDs) > 0 {
sqlb = sqlb.Where(squirrel.Eq{"fismasystems.fismasystemid": input.FismaSystemIDs})
}

sql, boundArgs, _ := sqlb.ToSql()
rows, err := query(ctx, sql, boundArgs...)

if err != nil {
log.Println(err, sql)
return nil, trapError(err)
}

return pgx.CollectRows(rows, func(row pgx.CollectableRow) (*Answer, error) {
answer := Answer{}
err := row.Scan(&answer.DataCall, &answer.FismaSystemID, &answer.FismaAcronym, &answer.DataCenterEnvironment, &answer.Pillar, &answer.Question, &answer.Function, &answer.Description, &answer.OptionName, &answer.Score, &answer.Notes)
return &answer, trapError(err)
})
}
8 changes: 4 additions & 4 deletions backend/cmd/api/internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var sqlBuilder = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
func query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
conn, err := db.Conn(ctx)
if err != nil {
return nil, trapError(err)
return nil, err
}

return conn.Query(ctx, sql, args...)
Expand All @@ -29,7 +29,7 @@ func query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
func queryRow(ctx context.Context, sql string, args ...any) (pgx.Row, error) {
conn, err := db.Conn(ctx)
if err != nil {
return nil, trapError(err)
return nil, err
}

row := conn.QueryRow(ctx, sql, args...)
Expand All @@ -40,8 +40,8 @@ func queryRow(ctx context.Context, sql string, args ...any) (pgx.Row, error) {
func exec(ctx context.Context, sql string, args ...any) error {
conn, err := db.Conn(ctx)
if err != nil {
return trapError(err)
return err
}
_, err = conn.Exec(ctx, sql, args...)
return trapError(err)
return err
}
9 changes: 5 additions & 4 deletions backend/cmd/api/internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ func Handler() http.Handler {
router.Use(auth.Middleware)

router.HandleFunc("/api/v1/datacalls", controller.ListDataCalls).Methods("GET")
router.HandleFunc("/api/v1/datacalls/{datacallid:[0-9]+}/export", controller.GetDatacallExport).Methods("GET")

router.HandleFunc("/api/v1/fismasystems", controller.ListFismaSystems).Methods("GET")
router.HandleFunc("/api/v1/fismasystems/{fismasystemid}", controller.GetFismaSystem).Methods("GET")
router.HandleFunc("/api/v1/fismasystems/{fismasystemid}/questions", controller.ListQuestions).Methods("GET")
router.HandleFunc("/api/v1/fismasystems/{fismasystemid:[0-9]+}", controller.GetFismaSystem).Methods("GET")
router.HandleFunc("/api/v1/fismasystems/{fismasystemid:[0-9]+}/questions", controller.ListQuestions).Methods("GET")

router.HandleFunc("/api/v1/functions/{functionid}/options", controller.ListFunctionOptions).Methods("GET")
router.HandleFunc("/api/v1/functions/{functionid:[0-9]+}/options", controller.ListFunctionOptions).Methods("GET")

router.HandleFunc("/api/v1/users", controller.ListUsers).Methods("GET")
router.HandleFunc("/api/v1/users", controller.SaveUser).Methods("POST")
Expand All @@ -34,7 +35,7 @@ func Handler() http.Handler {
router.HandleFunc("/api/v1/scores", controller.ListScores).Methods("GET")
router.HandleFunc("/api/v1/scores/aggregate", controller.GetScoresAggregate).Methods("GET") // yes "aggregate" is a noun
router.HandleFunc("/api/v1/scores", controller.SaveScore).Methods("POST")
router.HandleFunc("/api/v1/scores/{scoreid}", controller.SaveScore).Methods("PUT")
router.HandleFunc("/api/v1/scores/{scoreid:[0-9]+}", controller.SaveScore).Methods("PUT")

return router
}
40 changes: 40 additions & 0 deletions backend/cmd/api/internal/spreadsheet/spreadsheet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package spreadsheet

import (
"fmt"

"github.com/CMS-Enterprise/ztmf/backend/cmd/api/internal/model"
"github.com/xuri/excelize/v2"
)

func Excel(answers []*model.Answer) (*excelize.File, error) {

sheet := "Sheet1"

f := excelize.NewFile()

f.SetCellValue(sheet, "A1", "Fisma Acronym")
f.SetCellValue(sheet, "B1", "Data Center Environment")
f.SetCellValue(sheet, "C1", "Pillar")
f.SetCellValue(sheet, "D1", "Function")
f.SetCellValue(sheet, "E1", "Function Description")
f.SetCellValue(sheet, "F1", "Question")
f.SetCellValue(sheet, "G1", "Answer")
f.SetCellValue(sheet, "H1", "Score")
f.SetCellValue(sheet, "I1", "ADO Answer Details")

for i, a := range answers {
row := i + 2 // i starts at 0 and headers are in row 1
f.SetCellValue(sheet, fmt.Sprintf("A%d", row), a.FismaAcronym)
f.SetCellValue(sheet, fmt.Sprintf("B%d", row), a.DataCenterEnvironment)
f.SetCellValue(sheet, fmt.Sprintf("C%d", row), a.Pillar)
f.SetCellValue(sheet, fmt.Sprintf("D%d", row), a.Function)
f.SetCellValue(sheet, fmt.Sprintf("E%d", row), a.Description)
f.SetCellValue(sheet, fmt.Sprintf("F%d", row), a.Question)
f.SetCellValue(sheet, fmt.Sprintf("G%d", row), a.OptionName)
f.SetCellValue(sheet, fmt.Sprintf("H%d", row), a.Score)
f.SetCellValue(sheet, fmt.Sprintf("I%d", row), a.Notes)
}

return f, nil
}
File renamed without changes.
File renamed without changes.
7 changes: 7 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.5.5
github.com/jackc/tern/v2 v2.2.1
github.com/xuri/excelize/v2 v2.8.1
)

require (
Expand Down Expand Up @@ -38,8 +39,14 @@ require (
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
17 changes: 17 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,15 @@ github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HK
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
Expand All @@ -98,17 +105,27 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0=
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
Expand Down

0 comments on commit fac5630

Please sign in to comment.