From fa0255e9cda01be6b45169054f83c0e010f5b103 Mon Sep 17 00:00:00 2001 From: Krispire Admin Date: Sun, 18 Oct 2026 13:02:13 +0530 Subject: [PATCH] quiz grouping --- internal/model/quiz.go | 14 ++++++++------ internal/repository/quiz_repo.go | 15 ++++++++------- internal/service/quiz_service.go | 13 +++++++------ migrations/0061_quiz_target_year.down.sql | 1 + migrations/0061_quiz_target_year.up.sql | 5 +++++ 5 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 migrations/0061_quiz_target_year.down.sql create mode 100644 migrations/0061_quiz_target_year.up.sql diff --git a/internal/model/quiz.go b/internal/model/quiz.go index b97835d..ae776fc 100644 --- a/internal/model/quiz.go +++ b/internal/model/quiz.go @@ -19,12 +19,14 @@ const ( // Quiz is a set of multiple-choice questions, optionally tied to a page. type Quiz struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - TargetKind string `json:"target_kind"` - TargetPath *string `json:"target_path,omitempty"` - TargetTitle string `json:"target_title"` + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + TargetKind string `json:"target_kind"` + TargetPath *string `json:"target_path,omitempty"` + TargetTitle string `json:"target_title"` + // TargetYear is the lecture or conversation's year, for grouping. + TargetYear *int `json:"target_year,omitempty"` PublishedAt *time.Time `json:"published_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/internal/repository/quiz_repo.go b/internal/repository/quiz_repo.go index 374deff..ad174b8 100644 --- a/internal/repository/quiz_repo.go +++ b/internal/repository/quiz_repo.go @@ -35,6 +35,7 @@ type QuizInput struct { TargetKind string TargetPath *string TargetTitle string + TargetYear *int Questions []QuestionInput } @@ -51,12 +52,12 @@ type OptionInput struct { } 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, + q.target_title, q.target_year, 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} + &q.TargetTitle, &q.TargetYear, &q.PublishedAt, &q.CreatedAt, &q.UpdatedAt, &q.QuestionCount} return row.Scan(append(dest, extra...)...) } @@ -147,9 +148,9 @@ func (r *QuizRepo) Create(ctx context.Context, in QuizInput, createdBy string) ( 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, + INSERT INTO quizzes (title, description, target_kind, target_path, target_title, target_year, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, + in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle, in.TargetYear, createdBy, ).Scan(&id); err != nil { if isUniqueViolation(err, "quizzes_one_per_target") { return "", ErrQuizTargetTaken @@ -173,9 +174,9 @@ func (r *QuizRepo) Update(ctx context.Context, id string, in QuizInput, replaceQ 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() + target_path = $5, target_title = $6, target_year = $7, updated_at = now() WHERE id = $1 AND deleted_at IS NULL`, - id, in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle) + id, in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle, in.TargetYear) if err != nil { if isUniqueViolation(err, "quizzes_one_per_target") { return ErrQuizTargetTaken diff --git a/internal/service/quiz_service.go b/internal/service/quiz_service.go index cdfb530..bf5b1cf 100644 --- a/internal/service/quiz_service.go +++ b/internal/service/quiz_service.go @@ -73,10 +73,10 @@ func NormalizeContentPath(raw string) string { } // resolveTarget works out what a link points at. -func (s *QuizService) resolveTarget(ctx context.Context, raw, override string) (kind string, path *string, title string, err error) { +func (s *QuizService) resolveTarget(ctx context.Context, raw, override string) (kind string, path *string, title string, year *int, err error) { p := NormalizeContentPath(raw) if p == "" { - return model.QuizStandalone, nil, "", nil + return model.QuizStandalone, nil, "", nil, nil } parts := strings.Split(strings.Trim(p, "/"), "/") @@ -84,13 +84,14 @@ func (s *QuizService) resolveTarget(ctx context.Context, raw, override string) ( case len(parts) == 2 && parts[0] == "transcriptions": t, err := s.content.TranscriptBySlug(ctx, parts[1]) if err != nil { - return "", nil, "", ErrQuizTarget + return "", nil, "", nil, ErrQuizTarget } kind = model.QuizLecture if t.Type == "conversation" { kind = model.QuizConversation } title = t.Name + year = t.Year case len(parts) >= 3 && parts[0] == "books": kind = model.QuizChapter @@ -110,13 +111,13 @@ func (s *QuizService) resolveTarget(ctx context.Context, raw, override string) ( } default: - return "", nil, "", ErrQuizTarget + return "", nil, "", nil, ErrQuizTarget } if o := strings.TrimSpace(override); o != "" { title = o } - return kind, &p, title, nil + return kind, &p, title, year, nil } func validateQuestions(qs []repository.QuestionInput) ([]repository.QuestionInput, error) { @@ -166,7 +167,7 @@ func (s *QuizService) toInput(ctx context.Context, d QuizDraft, withQuestions bo return in, ErrQuizTitle } var err error - if in.TargetKind, in.TargetPath, in.TargetTitle, err = s.resolveTarget(ctx, d.TargetURL, d.TargetTitle); err != nil { + if in.TargetKind, in.TargetPath, in.TargetTitle, in.TargetYear, err = s.resolveTarget(ctx, d.TargetURL, d.TargetTitle); err != nil { return in, err } if withQuestions { diff --git a/migrations/0061_quiz_target_year.down.sql b/migrations/0061_quiz_target_year.down.sql new file mode 100644 index 0000000..ae236c3 --- /dev/null +++ b/migrations/0061_quiz_target_year.down.sql @@ -0,0 +1 @@ +ALTER TABLE quizzes DROP COLUMN IF EXISTS target_year; diff --git a/migrations/0061_quiz_target_year.up.sql b/migrations/0061_quiz_target_year.up.sql new file mode 100644 index 0000000..90bd8c5 --- /dev/null +++ b/migrations/0061_quiz_target_year.up.sql @@ -0,0 +1,5 @@ +-- The year of a lecture or conversation a quiz is attached to, captured when +-- the quiz is saved, so the app can group quizzes by year without looking +-- each transcript up. NULL for chapters, stand-alone quizzes, and undated +-- recordings. +ALTER TABLE quizzes ADD COLUMN IF NOT EXISTS target_year SMALLINT;