Files
spsays-bff/internal/repository/quiz_repo.go
T
2026-10-15 19:43:17 +05:30

694 lines
22 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"sort"
"time"
"github.com/jackc/pgx/v5"
"github.com/prabhupadasays/spsays-bff/internal/model"
)
var (
ErrQuizNotFound = errors.New("quiz not found")
ErrQuizNotStarted = errors.New("start the quiz first")
ErrQuizNotCurrent = errors.New("that isn't the current question")
ErrQuizBadOption = errors.New("that option isn't part of this question")
ErrQuizChoiceCount = errors.New("choose one option, or at least one where several apply")
ErrQuizAlreadyAnswer = errors.New("that question is already answered")
ErrQuizNotFinished = errors.New("the quiz isn't finished yet")
)
type QuizRepo struct{ pg *Postgres }
func NewQuizRepo(pg *Postgres) *QuizRepo { return &QuizRepo{pg: pg} }
// QuizInput is a whole quiz as the admin editor submits it.
type QuizInput struct {
Title string
Description string
TargetKind string
TargetPath *string
TargetTitle string
Questions []QuestionInput
}
type QuestionInput struct {
Prompt string
Explanation string
Multi bool
Options []OptionInput
}
type OptionInput struct {
Body string
IsCorrect bool
}
const quizColumns = `q.id, q.title, q.description, q.target_kind, q.target_path,
q.target_title, q.published_at, q.created_at, q.updated_at,
(SELECT count(*) FROM quiz_questions qq WHERE qq.quiz_id = q.id)`
func scanQuiz(row pgx.Row, q *model.Quiz, extra ...any) error {
dest := []any{&q.ID, &q.Title, &q.Description, &q.TargetKind, &q.TargetPath,
&q.TargetTitle, &q.PublishedAt, &q.CreatedAt, &q.UpdatedAt, &q.QuestionCount}
return row.Scan(append(dest, extra...)...)
}
// ── Admin ─────────────────────────────────────────────────────────────────
func (r *QuizRepo) ListAdmin(ctx context.Context) ([]model.Quiz, error) {
rows, err := r.pg.Pool.Query(ctx, `SELECT `+quizColumns+`,
(SELECT count(*) FROM quiz_attempts a WHERE a.quiz_id = q.id),
(SELECT count(*) FROM quiz_attempts a WHERE a.quiz_id = q.id AND a.completed_at IS NOT NULL)
FROM quizzes q WHERE q.deleted_at IS NULL
ORDER BY q.updated_at DESC`)
if err != nil {
return nil, fmt.Errorf("list quizzes: %w", err)
}
defer rows.Close()
out := []model.Quiz{}
for rows.Next() {
var q model.Quiz
if err := scanQuiz(rows, &q, &q.AttemptCount, &q.CompletedCount); err != nil {
return nil, fmt.Errorf("scan quiz: %w", err)
}
out = append(out, q)
}
return out, rows.Err()
}
// Get loads a quiz with its questions. revealAnswers controls whether
// IsCorrect is filled in: true for the admin editor and results screens,
// false for anyone mid-attempt.
func (r *QuizRepo) Get(ctx context.Context, id string, revealAnswers bool) (*model.Quiz, error) {
var q model.Quiz
err := scanQuiz(r.pg.Pool.QueryRow(ctx, `SELECT `+quizColumns+`
FROM quizzes q WHERE q.id = $1 AND q.deleted_at IS NULL`, id), &q)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrQuizNotFound
}
if err != nil {
return nil, fmt.Errorf("load quiz: %w", err)
}
if q.Questions, err = r.questions(ctx, id, revealAnswers); err != nil {
return nil, err
}
return &q, nil
}
func (r *QuizRepo) questions(ctx context.Context, quizID string, reveal bool) ([]model.QuizQuestion, error) {
rows, err := r.pg.Pool.Query(ctx, `
SELECT qq.id, qq.ord, qq.prompt, qq.explanation, qq.multi,
o.id, o.ord, o.body, o.is_correct
FROM quiz_questions qq
JOIN quiz_options o ON o.question_id = qq.id
WHERE qq.quiz_id = $1
ORDER BY qq.ord, o.ord`, quizID)
if err != nil {
return nil, fmt.Errorf("load questions: %w", err)
}
defer rows.Close()
out := []model.QuizQuestion{}
for rows.Next() {
var (
qid, prompt, expl, oid, body string
qord, oord int
multi, correct bool
)
if err := rows.Scan(&qid, &qord, &prompt, &expl, &multi, &oid, &oord, &body, &correct); err != nil {
return nil, fmt.Errorf("scan question: %w", err)
}
if len(out) == 0 || out[len(out)-1].ID != qid {
out = append(out, model.QuizQuestion{ID: qid, Ord: qord, Prompt: prompt, Explanation: expl, Multi: multi})
}
opt := model.QuizOption{ID: oid, Ord: oord, Body: body}
if reveal {
c := correct
opt.IsCorrect = &c
}
last := &out[len(out)-1]
last.Options = append(last.Options, opt)
}
return out, rows.Err()
}
// Create stores a new quiz, unpublished, with its questions.
func (r *QuizRepo) Create(ctx context.Context, in QuizInput, createdBy string) (string, error) {
tx, err := r.pg.Pool.Begin(ctx)
if err != nil {
return "", fmt.Errorf("begin quiz tx: %w", err)
}
defer tx.Rollback(ctx) //nolint:errcheck
var id string
if err := tx.QueryRow(ctx, `
INSERT INTO quizzes (title, description, target_kind, target_path, target_title, created_by)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle, createdBy,
).Scan(&id); err != nil {
return "", fmt.Errorf("create quiz: %w", err)
}
if err := insertQuestions(ctx, tx, id, in.Questions); err != nil {
return "", err
}
return id, tx.Commit(ctx)
}
// Update rewrites a quiz. replaceQuestions is false once anyone has made an
// attempt: their answers point at specific questions and options, so those
// are frozen and only the title, description and target may change.
func (r *QuizRepo) Update(ctx context.Context, id string, in QuizInput, replaceQuestions bool) error {
tx, err := r.pg.Pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin quiz tx: %w", err)
}
defer tx.Rollback(ctx) //nolint:errcheck
tag, err := tx.Exec(ctx, `
UPDATE quizzes SET title = $2, description = $3, target_kind = $4,
target_path = $5, target_title = $6, updated_at = now()
WHERE id = $1 AND deleted_at IS NULL`,
id, in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle)
if err != nil {
return fmt.Errorf("update quiz: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrQuizNotFound
}
if replaceQuestions {
if _, err := tx.Exec(ctx, `DELETE FROM quiz_questions WHERE quiz_id = $1`, id); err != nil {
return fmt.Errorf("clear questions: %w", err)
}
if err := insertQuestions(ctx, tx, id, in.Questions); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func insertQuestions(ctx context.Context, tx pgx.Tx, quizID string, qs []QuestionInput) error {
for i, q := range qs {
var qid string
if err := tx.QueryRow(ctx, `
INSERT INTO quiz_questions (quiz_id, ord, prompt, explanation, multi)
VALUES ($1, $2, $3, $4, $5) RETURNING id`, quizID, i, q.Prompt, q.Explanation, q.Multi,
).Scan(&qid); err != nil {
return fmt.Errorf("insert question: %w", err)
}
for j, o := range q.Options {
if _, err := tx.Exec(ctx, `
INSERT INTO quiz_options (question_id, ord, body, is_correct)
VALUES ($1, $2, $3, $4)`, qid, j, o.Body, o.IsCorrect); err != nil {
return fmt.Errorf("insert option: %w", err)
}
}
}
return nil
}
func (r *QuizRepo) AttemptCount(ctx context.Context, id string) (int, error) {
var n int
err := r.pg.Pool.QueryRow(ctx, `SELECT count(*) FROM quiz_attempts WHERE quiz_id = $1`, id).Scan(&n)
return n, err
}
func (r *QuizRepo) SetPublished(ctx context.Context, id string, published bool) error {
tag, err := r.pg.Pool.Exec(ctx, `
UPDATE quizzes
SET published_at = CASE WHEN $2 THEN COALESCE(published_at, now()) ELSE NULL END,
updated_at = now()
WHERE id = $1 AND deleted_at IS NULL`, id, published)
if err != nil {
return fmt.Errorf("publish quiz: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrQuizNotFound
}
return nil
}
// Delete hides a quiz everywhere. Soft, so participants' results survive.
func (r *QuizRepo) Delete(ctx context.Context, id string) error {
tag, err := r.pg.Pool.Exec(ctx,
`UPDATE quizzes SET deleted_at = now() WHERE id = $1 AND deleted_at IS NULL`, id)
if err != nil {
return fmt.Errorf("delete quiz: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrQuizNotFound
}
return nil
}
// Participants lists everyone who started a quiz, best scores first.
func (r *QuizRepo) Participants(ctx context.Context, quizID string) ([]model.QuizParticipant, error) {
rows, err := r.pg.Pool.Query(ctx, `
SELECT u.id, u.handle, u.name, u.avatar_url, a.started_at, a.completed_at,
(SELECT count(*) FROM quiz_answers x WHERE x.attempt_id = a.id AND x.answered_at IS NOT NULL),
a.score, a.total,
COALESCE((SELECT sum(x.response_ms) FROM quiz_answers x WHERE x.attempt_id = a.id), 0)
FROM quiz_attempts a
JOIN users u ON u.id = a.user_id
WHERE a.quiz_id = $1
ORDER BY (a.completed_at IS NOT NULL) DESC, a.score DESC, a.started_at`, quizID)
if err != nil {
return nil, fmt.Errorf("quiz participants: %w", err)
}
defer rows.Close()
out := []model.QuizParticipant{}
for rows.Next() {
var p model.QuizParticipant
if err := rows.Scan(&p.UserID, &p.Handle, &p.Name, &p.AvatarURL, &p.StartedAt,
&p.CompletedAt, &p.Answered, &p.Score, &p.Total, &p.TotalMs); err != nil {
return nil, fmt.Errorf("scan participant: %w", err)
}
out = append(out, p)
}
return out, rows.Err()
}
// QuestionStats aggregates every answer per question and option.
func (r *QuizRepo) QuestionStats(ctx context.Context, quizID string) ([]model.QuizQuestionStat, error) {
qs, err := r.questions(ctx, quizID, true)
if err != nil {
return nil, err
}
rows, err := r.pg.Pool.Query(ctx, `
SELECT x.question_id, COALESCE(x.option_ids::text[], '{}'), x.is_correct, x.response_ms
FROM quiz_answers x
JOIN quiz_questions qq ON qq.id = x.question_id
WHERE qq.quiz_id = $1 AND x.answered_at IS NOT NULL`, quizID)
if err != nil {
return nil, fmt.Errorf("quiz answers: %w", err)
}
defer rows.Close()
picks := map[string]int{}
type agg struct {
answered, correct int
times []int
}
per := map[string]*agg{}
for rows.Next() {
var qid string
var oids []string
var correct *bool
var ms *int
if err := rows.Scan(&qid, &oids, &correct, &ms); err != nil {
return nil, fmt.Errorf("scan answer: %w", err)
}
a := per[qid]
if a == nil {
a = &agg{}
per[qid] = a
}
a.answered++
if correct != nil && *correct {
a.correct++
}
if ms != nil {
a.times = append(a.times, *ms)
}
for _, oid := range oids {
picks[oid]++
}
}
if err := rows.Err(); err != nil {
return nil, err
}
out := make([]model.QuizQuestionStat, 0, len(qs))
for _, q := range qs {
st := model.QuizQuestionStat{ID: q.ID, Ord: q.Ord, Prompt: q.Prompt, Multi: q.Multi}
if a := per[q.ID]; a != nil {
st.Answered, st.Correct = a.answered, a.correct
if n := len(a.times); n > 0 {
sum := 0
for _, t := range a.times {
sum += t
}
st.AvgMs = sum / n
sort.Ints(a.times)
st.MedianMs = a.times[n/2]
}
}
for _, o := range q.Options {
st.Options = append(st.Options, model.QuizOptionStat{QuizOption: o, Picks: picks[o.ID]})
}
out = append(out, st)
}
return out, nil
}
// ── Taking a quiz ─────────────────────────────────────────────────────────
// PublishedForPath returns the newest live quiz on a content page, or nil.
func (r *QuizRepo) PublishedForPath(ctx context.Context, path string) (*model.Quiz, error) {
var q model.Quiz
err := scanQuiz(r.pg.Pool.QueryRow(ctx, `SELECT `+quizColumns+`
FROM quizzes q
WHERE q.target_path = $1 AND q.deleted_at IS NULL AND q.published_at IS NOT NULL
ORDER BY q.published_at DESC LIMIT 1`, path), &q)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("quiz for path: %w", err)
}
return &q, nil
}
// ListPublished returns every live quiz, newest first.
func (r *QuizRepo) ListPublished(ctx context.Context) ([]model.Quiz, error) {
rows, err := r.pg.Pool.Query(ctx, `SELECT `+quizColumns+`
FROM quizzes q
WHERE q.deleted_at IS NULL AND q.published_at IS NOT NULL
ORDER BY q.published_at DESC`)
if err != nil {
return nil, fmt.Errorf("list published quizzes: %w", err)
}
defer rows.Close()
out := []model.Quiz{}
for rows.Next() {
var q model.Quiz
if err := scanQuiz(rows, &q); err != nil {
return nil, fmt.Errorf("scan quiz: %w", err)
}
out = append(out, q)
}
return out, rows.Err()
}
// Statuses returns the caller's progress on each of the given quizzes.
// Quizzes they haven't touched are simply absent.
func (r *QuizRepo) Statuses(ctx context.Context, userID string, quizIDs []string) (map[string]model.QuizStatus, error) {
out := map[string]model.QuizStatus{}
if userID == "" || len(quizIDs) == 0 {
return out, nil
}
rows, err := r.pg.Pool.Query(ctx, `
SELECT a.quiz_id, a.completed_at IS NOT NULL, a.score, a.total,
(SELECT count(*) FROM quiz_answers x WHERE x.attempt_id = a.id AND x.answered_at IS NOT NULL)
FROM quiz_attempts a
WHERE a.user_id = $1 AND a.quiz_id = ANY($2)`, userID, quizIDs)
if err != nil {
return nil, fmt.Errorf("quiz statuses: %w", err)
}
defer rows.Close()
for rows.Next() {
var id string
var done bool
var score, total, answered int
if err := rows.Scan(&id, &done, &score, &total, &answered); err != nil {
return nil, fmt.Errorf("scan status: %w", err)
}
st := model.QuizStatus{State: model.QuizInProgress, Answered: answered, Total: total}
if done {
st.State = model.QuizCompleted
s := score
st.Score = &s
}
out[id] = st
}
return out, rows.Err()
}
type attemptRow struct {
ID string
StartedAt time.Time
CompletedAt *time.Time
Score int
Total int
}
func (r *QuizRepo) attempt(ctx context.Context, quizID, userID string) (*attemptRow, error) {
var a attemptRow
err := r.pg.Pool.QueryRow(ctx, `
SELECT id, started_at, completed_at, score, total
FROM quiz_attempts WHERE quiz_id = $1 AND user_id = $2`, quizID, userID,
).Scan(&a.ID, &a.StartedAt, &a.CompletedAt, &a.Score, &a.Total)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrQuizNotStarted
}
if err != nil {
return nil, fmt.Errorf("load attempt: %w", err)
}
return &a, nil
}
// Start opens the caller's attempt. Idempotent: starting again resumes.
func (r *QuizRepo) Start(ctx context.Context, quizID, userID string, total int) error {
_, err := r.pg.Pool.Exec(ctx, `
INSERT INTO quiz_attempts (quiz_id, user_id, total) VALUES ($1, $2, $3)
ON CONFLICT (quiz_id, user_id) DO NOTHING`, quizID, userID, total)
if err != nil {
return fmt.Errorf("start attempt: %w", err)
}
return nil
}
// Next returns the first unanswered question and stamps the moment it was
// served. The stamp is set ONCE: fetching the same question again — a reload,
// a second tab — doesn't restart its clock.
func (r *QuizRepo) Next(ctx context.Context, quizID, userID string) (*model.QuizPlay, error) {
a, err := r.attempt(ctx, quizID, userID)
if err != nil {
return nil, err
}
play := &model.QuizPlay{QuizID: quizID, Total: a.Total}
if err := r.pg.Pool.QueryRow(ctx, `
SELECT count(*) FROM quiz_answers WHERE attempt_id = $1 AND answered_at IS NOT NULL`,
a.ID).Scan(&play.Answered); err != nil {
return nil, fmt.Errorf("count answered: %w", err)
}
if a.CompletedAt != nil {
play.Done = true
return play, nil
}
var qid string
err = r.pg.Pool.QueryRow(ctx, `
SELECT qq.id FROM quiz_questions qq
WHERE qq.quiz_id = $1
AND NOT EXISTS (SELECT 1 FROM quiz_answers x
WHERE x.attempt_id = $2 AND x.question_id = qq.id
AND x.answered_at IS NOT NULL)
ORDER BY qq.ord LIMIT 1`, quizID, a.ID).Scan(&qid)
if errors.Is(err, pgx.ErrNoRows) {
// Every question answered but not yet marked complete — finish it.
if err := r.complete(ctx, a.ID); err != nil {
return nil, err
}
play.Done = true
return play, nil
}
if err != nil {
return nil, fmt.Errorf("next question: %w", err)
}
if _, err := r.pg.Pool.Exec(ctx, `
INSERT INTO quiz_answers (attempt_id, question_id) VALUES ($1, $2)
ON CONFLICT DO NOTHING`, a.ID, qid); err != nil {
return nil, fmt.Errorf("stamp question: %w", err)
}
qs, err := r.questions(ctx, quizID, false)
if err != nil {
return nil, err
}
for i := range qs {
if qs[i].ID == qid {
play.Question = &qs[i]
// The explanation discusses the answer; it belongs on the
// results screen, not beside the question being answered.
play.Question.Explanation = ""
}
}
return play, nil
}
// Answer records the caller's choice on the CURRENT question: one option, or
// for a multi question every option they think applies.
//
// The question must have been served (Next stamped it), which enforces order
// and gives the server its own start time. Response time is shown→answered,
// both server clocks. An answer can't be changed once given. A multi answer
// is correct only when the chosen set equals the correct set exactly.
func (r *QuizRepo) Answer(ctx context.Context, quizID, userID, questionID string, optionIDs []string) error {
a, err := r.attempt(ctx, quizID, userID)
if err != nil {
return err
}
if a.CompletedAt != nil {
return ErrQuizAlreadyAnswer
}
chosen := map[string]bool{}
for _, id := range optionIDs {
chosen[id] = true
}
tx, err := r.pg.Pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin answer tx: %w", err)
}
defer tx.Rollback(ctx) //nolint:errcheck
rows, err := tx.Query(ctx, `
SELECT qq.multi, o.id, o.is_correct FROM quiz_options o
JOIN quiz_questions qq ON qq.id = o.question_id
WHERE o.question_id = $1 AND qq.quiz_id = $2`, questionID, quizID)
if err != nil {
return fmt.Errorf("check options: %w", err)
}
var multi bool
known, wrongPick, missed := 0, false, false
for rows.Next() {
var oid string
var isCorrect bool
if err := rows.Scan(&multi, &oid, &isCorrect); err != nil {
rows.Close()
return fmt.Errorf("scan option: %w", err)
}
if chosen[oid] {
known++
wrongPick = wrongPick || !isCorrect
} else if isCorrect {
missed = true
}
}
rows.Close()
if err := rows.Err(); err != nil {
return fmt.Errorf("check options: %w", err)
}
if known != len(chosen) {
// Something chosen isn't an option of this question (or the question
// isn't part of this quiz at all).
return ErrQuizBadOption
}
if len(chosen) == 0 || (!multi && len(chosen) != 1) {
return ErrQuizChoiceCount
}
correct := !wrongPick && !missed
ids := make([]string, 0, len(chosen))
for _, id := range optionIDs {
if chosen[id] {
ids = append(ids, id)
delete(chosen, id) // keep the first of any duplicate
}
}
var answeredAt *time.Time
err = tx.QueryRow(ctx, `
SELECT answered_at FROM quiz_answers
WHERE attempt_id = $1 AND question_id = $2 FOR UPDATE`, a.ID, questionID).Scan(&answeredAt)
if errors.Is(err, pgx.ErrNoRows) {
return ErrQuizNotCurrent
}
if err != nil {
return fmt.Errorf("load answer row: %w", err)
}
if answeredAt != nil {
return ErrQuizAlreadyAnswer
}
if _, err := tx.Exec(ctx, `
UPDATE quiz_answers
SET option_ids = $3::uuid[], answered_at = now(), is_correct = $4,
response_ms = GREATEST(0, (EXTRACT(EPOCH FROM (now() - shown_at)) * 1000)::int)
WHERE attempt_id = $1 AND question_id = $2`,
a.ID, questionID, ids, correct); err != nil {
return fmt.Errorf("record answer: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit answer: %w", err)
}
// Finish the attempt once nothing is left unanswered.
var remaining int
if err := r.pg.Pool.QueryRow(ctx, `
SELECT count(*) FROM quiz_questions qq
WHERE qq.quiz_id = $1 AND NOT EXISTS (
SELECT 1 FROM quiz_answers x WHERE x.attempt_id = $2
AND x.question_id = qq.id AND x.answered_at IS NOT NULL)`,
quizID, a.ID).Scan(&remaining); err != nil {
return fmt.Errorf("count remaining: %w", err)
}
if remaining == 0 {
return r.complete(ctx, a.ID)
}
return nil
}
func (r *QuizRepo) complete(ctx context.Context, attemptID string) error {
_, err := r.pg.Pool.Exec(ctx, `
UPDATE quiz_attempts SET completed_at = now(),
score = (SELECT count(*) FROM quiz_answers WHERE attempt_id = $1 AND is_correct)
WHERE id = $1 AND completed_at IS NULL`, attemptID)
if err != nil {
return fmt.Errorf("complete attempt: %w", err)
}
return nil
}
// Result returns an attempt with the answers revealed.
//
// allowIncomplete is false on the user's path, which refuses until the attempt
// is finished — otherwise a result would be a way to peek at the answers. The
// admin passes true to inspect an attempt still in progress.
func (r *QuizRepo) Result(ctx context.Context, quizID, userID string, allowIncomplete bool) (*model.QuizResult, error) {
a, err := r.attempt(ctx, quizID, userID)
if err != nil {
return nil, err
}
if a.CompletedAt == nil && !allowIncomplete {
return nil, ErrQuizNotFinished
}
quiz, err := r.Get(ctx, quizID, true)
if err != nil {
return nil, err
}
res := &model.QuizResult{
Score: a.Score, Total: a.Total, StartedAt: a.StartedAt, CompletedAt: a.CompletedAt,
}
rows, err := r.pg.Pool.Query(ctx, `
SELECT question_id, COALESCE(option_ids::text[], '{}'), COALESCE(is_correct, false), response_ms
FROM quiz_answers WHERE attempt_id = $1`, a.ID)
if err != nil {
return nil, fmt.Errorf("load result answers: %w", err)
}
defer rows.Close()
type ans struct {
options []string
correct bool
ms *int
}
got := map[string]ans{}
for rows.Next() {
var qid string
var x ans
if err := rows.Scan(&qid, &x.options, &x.correct, &x.ms); err != nil {
return nil, fmt.Errorf("scan result answer: %w", err)
}
got[qid] = x
}
if err := rows.Err(); err != nil {
return nil, err
}
for _, q := range quiz.Questions {
item := model.QuizResultItem{QuizQuestion: q, ChosenOptionIDs: []string{}}
if x, ok := got[q.ID]; ok {
item.ChosenOptionIDs, item.IsCorrect, item.ResponseMs = x.options, x.correct, x.ms
if x.ms != nil {
res.TotalMs += *x.ms
}
}
res.Items = append(res.Items, item)
}
quiz.Questions = nil
res.Quiz = *quiz
return res, nil
}