-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SurveyResponse: add download for survey results CSV
- Loading branch information
1 parent
5ef5dd4
commit 7a57f01
Showing
6 changed files
with
320 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { createObjectCsvStringifier } from "csv-writer" | ||
import { NextApiRequest, NextApiResponse } from "next" | ||
import { api } from "src/blitz-server" | ||
import { getSession } from "@blitzjs/auth" | ||
import { AuthorizationError } from "blitz" | ||
import { ZodError } from "zod" | ||
import dbGetSurvey from "src/surveys/queries/getSurvey" | ||
import { Survey } from "db" | ||
|
||
const DEBUG = false | ||
|
||
export const getSurvey = async ( | ||
req: NextApiRequest, | ||
res: NextApiResponse, | ||
): Promise<undefined | Survey> => { | ||
await api(() => null) | ||
|
||
const err = (status: number, message: string) => { | ||
res.status(status).json({ error: true, status: status, message }) | ||
res.end() | ||
} | ||
|
||
const session = await getSession(req, res) | ||
|
||
let survey | ||
try { | ||
// @ts-ignore | ||
survey = await dbGetSurvey({ id: Number(req.query.surveyId) }, { session }) | ||
} catch (e) { | ||
if (e instanceof AuthorizationError) { | ||
err(403, "Forbidden") | ||
} | ||
// @ts-ignore | ||
if (e.code === "P2025" || e instanceof ZodError) { | ||
err(404, "Not Found") | ||
} | ||
|
||
console.error(e) | ||
err(500, "Internal Server Error") | ||
|
||
return | ||
} | ||
|
||
return survey | ||
} | ||
|
||
export const sendCsv = ( | ||
res: NextApiResponse, | ||
headers: { id: string; title: string }[], | ||
data: Record<string, any>[], | ||
filename: string, | ||
) => { | ||
const csvStringifier = createObjectCsvStringifier({ | ||
header: headers, | ||
fieldDelimiter: ";", | ||
alwaysQuote: true, | ||
}) | ||
const csvString = csvStringifier.getHeaderString() + csvStringifier.stringifyRecords(data) | ||
if (DEBUG) { | ||
res.setHeader("Content-Type", "text/plain") | ||
} else { | ||
res.setHeader("Content-Type", "text/csv") | ||
res.setHeader("Content-Disposition", `attachment; filename=${filename}`) | ||
} | ||
res.send(csvString) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { NextApiRequest, NextApiResponse } from "next" | ||
import { TSurvey } from "src/survey-public/components/types" | ||
import { getSurvey, sendCsv } from "./_shared" | ||
import { getSurveyDefinitionBySurveySlug } from "src/survey-public/utils/getConfigBySurveySlug" | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
const survey = await getSurvey(req, res) | ||
if (!survey) return | ||
|
||
const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug) | ||
|
||
const headers = [ | ||
{ id: "id", title: "frage_id" }, | ||
{ id: "type", title: "typ" }, | ||
{ id: "question", title: "frage" }, | ||
] | ||
|
||
const types = { | ||
singleResponse: "single", | ||
multipleResponse: "multi", | ||
} | ||
|
||
type Question = { id: number | string; type: string; question: string } | ||
let data: Question[] = [] | ||
const addQuestions = (definition: TSurvey) => { | ||
definition.pages.forEach((page) => { | ||
if (!page.questions) return | ||
page.questions.forEach(({ id, component, label }) => { | ||
// @ts-ignore | ||
data.push({ id, type: types[component] || component, question: label.de }) | ||
}) | ||
}) | ||
} | ||
|
||
addQuestions(surveyDefinition) | ||
|
||
// for now we only want questions, not feedback part | ||
// in case we want to include the feedack part we cvan uncomment that line | ||
// @ts-ignore | ||
// addQuestions(feedbackDefinition) | ||
|
||
sendCsv(res, headers, data, "fragen.csv") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { NextApiRequest, NextApiResponse } from "next" | ||
import { TSurvey } from "src/survey-public/components/types" | ||
import { getSurveyDefinitionBySurveySlug } from "src/survey-public/utils/getConfigBySurveySlug" | ||
import { getSurvey, sendCsv } from "./_shared" | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
const survey = await getSurvey(req, res) | ||
if (!survey) return | ||
const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug) | ||
|
||
const headers = [ | ||
{ id: "questionId", title: "frage_id" }, | ||
{ id: "responseId", title: "antwort_id" }, | ||
{ id: "text", title: "antwort" }, | ||
] | ||
|
||
type Question = { questionId: number | string; responseId: number | string; text: string } | ||
let data: Question[] = [] | ||
const addQuestions = (definition: TSurvey) => { | ||
definition.pages.forEach((page) => { | ||
if (!page.questions) return | ||
page.questions.forEach((question) => { | ||
if (!["singleResponse", "multipleResponse"].includes(question.component)) return | ||
// @ts-ignore | ||
question.props.responses.forEach((response) => { | ||
data.push({ | ||
questionId: question.id, | ||
responseId: response.id, | ||
text: response.text.de, | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
// @ts-ignore | ||
addQuestions(surveyDefinition) | ||
|
||
// for now we only want questions, not feedback part | ||
// in case we want to include the feedack part we cvan uncomment that line | ||
// @ts-ignore | ||
// addQuestions(feedbackDefinition) | ||
|
||
sendCsv(res, headers, data, "antworten.csv") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import db from "db" | ||
import { NextApiRequest, NextApiResponse } from "next" | ||
import { | ||
getFeedbackDefinitionBySurveySlug, | ||
getSurveyDefinitionBySurveySlug, | ||
} from "src/survey-public/utils/getConfigBySurveySlug" | ||
import { getSurvey, sendCsv } from "./_shared" | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
const survey = await getSurvey(req, res) | ||
if (!survey) return | ||
|
||
const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug) | ||
const feedbackDefinition = getFeedbackDefinitionBySurveySlug(survey.slug) | ||
const surveys = Object.fromEntries([surveyDefinition, feedbackDefinition].map((o) => [o.part, o])) | ||
const questions = {} | ||
|
||
Object.values(surveys).forEach((survey) => { | ||
survey.pages.forEach((page) => { | ||
if (!page.questions) return | ||
page.questions.forEach((question) => { | ||
// @ts-ignore | ||
if (["singleResponse", "multipleResponse"].includes(question.component)) { | ||
// @ts-ignore | ||
question.responses = Object.fromEntries(question.props.responses.map((r) => [r.id, r])) | ||
} | ||
// @ts-ignore | ||
questions[question.id] = question | ||
}) | ||
}) | ||
}) | ||
|
||
const surveySessions = await db.surveySession.findMany({ | ||
where: { surveyId: survey.id }, | ||
include: { responses: { where: { surveyPart: 1 } } }, | ||
}) | ||
|
||
// for now we only want questions, not feedback part | ||
// in case we want to include the feedack part we cvan uncomment these lines | ||
|
||
const headers = [ | ||
{ id: "createdAt", title: "datum" }, | ||
{ id: "sessionId", title: "sitzung_id" }, | ||
{ id: "questionId", title: "frage_id" }, | ||
{ id: "responseId", title: "ergebnis_antwort_id" }, | ||
// { id: "responseText", title: "responseText" }, | ||
// { id: "responseData", title: "responseData" }, | ||
] | ||
|
||
type Result = { | ||
createdAt: string | ||
sessionId: string | ||
questionId: string | ||
responseId?: string | ||
// responseText?: string | ||
// responseData?: string | ||
} | ||
|
||
const csvData: Result[] = [] | ||
|
||
// as we only want to include the latest questions in the export we need to check if the question is in the array of latest questions; in the frm7 project we deleted questions after the survey was live | ||
const deletedQuestionIds = surveyDefinition.deletedQuestions?.map((q) => q.id) | ||
|
||
surveySessions.forEach((surveySession) => { | ||
const { id, createdAt, responses } = surveySession | ||
|
||
responses.forEach(({ data }) => { | ||
// @ts-expect-error | ||
data = JSON.parse(data) | ||
Object.entries(data).map(([questionId, responseData]) => { | ||
if (!deletedQuestionIds || !deletedQuestionIds?.includes(Number(questionId))) { | ||
let row: Result = { | ||
createdAt: createdAt.toLocaleDateString("de-DE"), | ||
sessionId: String(id), | ||
questionId: "n/a", | ||
} | ||
const responseId = responseData | ||
row = { ...row, questionId, responseId } | ||
csvData.push(row) | ||
} | ||
}) | ||
}) | ||
}) | ||
|
||
sendCsv(res, headers, csvData, "ergebnisse.csv") | ||
} |
Oops, something went wrong.