quiz grouping

This commit is contained in:
Krispire Admin
2026-10-18 13:02:13 +05:30
parent eb02d08f14
commit fa0255e9cd
5 changed files with 29 additions and 19 deletions
+8 -6
View File
@@ -19,12 +19,14 @@ const (
// Quiz is a set of multiple-choice questions, optionally tied to a page. // Quiz is a set of multiple-choice questions, optionally tied to a page.
type Quiz struct { type Quiz struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
TargetKind string `json:"target_kind"` TargetKind string `json:"target_kind"`
TargetPath *string `json:"target_path,omitempty"` TargetPath *string `json:"target_path,omitempty"`
TargetTitle string `json:"target_title"` 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"` PublishedAt *time.Time `json:"published_at,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
+8 -7
View File
@@ -35,6 +35,7 @@ type QuizInput struct {
TargetKind string TargetKind string
TargetPath *string TargetPath *string
TargetTitle string TargetTitle string
TargetYear *int
Questions []QuestionInput Questions []QuestionInput
} }
@@ -51,12 +52,12 @@ type OptionInput struct {
} }
const quizColumns = `q.id, q.title, q.description, q.target_kind, q.target_path, 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)` (SELECT count(*) FROM quiz_questions qq WHERE qq.quiz_id = q.id)`
func scanQuiz(row pgx.Row, q *model.Quiz, extra ...any) error { func scanQuiz(row pgx.Row, q *model.Quiz, extra ...any) error {
dest := []any{&q.ID, &q.Title, &q.Description, &q.TargetKind, &q.TargetPath, 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...)...) 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 defer tx.Rollback(ctx) //nolint:errcheck
var id string var id string
if err := tx.QueryRow(ctx, ` if err := tx.QueryRow(ctx, `
INSERT INTO quizzes (title, description, target_kind, target_path, target_title, created_by) INSERT INTO quizzes (title, description, target_kind, target_path, target_title, target_year, created_by)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle, createdBy, in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle, in.TargetYear, createdBy,
).Scan(&id); err != nil { ).Scan(&id); err != nil {
if isUniqueViolation(err, "quizzes_one_per_target") { if isUniqueViolation(err, "quizzes_one_per_target") {
return "", ErrQuizTargetTaken return "", ErrQuizTargetTaken
@@ -173,9 +174,9 @@ func (r *QuizRepo) Update(ctx context.Context, id string, in QuizInput, replaceQ
defer tx.Rollback(ctx) //nolint:errcheck defer tx.Rollback(ctx) //nolint:errcheck
tag, err := tx.Exec(ctx, ` tag, err := tx.Exec(ctx, `
UPDATE quizzes SET title = $2, description = $3, target_kind = $4, 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`, 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 err != nil {
if isUniqueViolation(err, "quizzes_one_per_target") { if isUniqueViolation(err, "quizzes_one_per_target") {
return ErrQuizTargetTaken return ErrQuizTargetTaken
+7 -6
View File
@@ -73,10 +73,10 @@ func NormalizeContentPath(raw string) string {
} }
// resolveTarget works out what a link points at. // 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) p := NormalizeContentPath(raw)
if p == "" { if p == "" {
return model.QuizStandalone, nil, "", nil return model.QuizStandalone, nil, "", nil, nil
} }
parts := strings.Split(strings.Trim(p, "/"), "/") 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": case len(parts) == 2 && parts[0] == "transcriptions":
t, err := s.content.TranscriptBySlug(ctx, parts[1]) t, err := s.content.TranscriptBySlug(ctx, parts[1])
if err != nil { if err != nil {
return "", nil, "", ErrQuizTarget return "", nil, "", nil, ErrQuizTarget
} }
kind = model.QuizLecture kind = model.QuizLecture
if t.Type == "conversation" { if t.Type == "conversation" {
kind = model.QuizConversation kind = model.QuizConversation
} }
title = t.Name title = t.Name
year = t.Year
case len(parts) >= 3 && parts[0] == "books": case len(parts) >= 3 && parts[0] == "books":
kind = model.QuizChapter kind = model.QuizChapter
@@ -110,13 +111,13 @@ func (s *QuizService) resolveTarget(ctx context.Context, raw, override string) (
} }
default: default:
return "", nil, "", ErrQuizTarget return "", nil, "", nil, ErrQuizTarget
} }
if o := strings.TrimSpace(override); o != "" { if o := strings.TrimSpace(override); o != "" {
title = o title = o
} }
return kind, &p, title, nil return kind, &p, title, year, nil
} }
func validateQuestions(qs []repository.QuestionInput) ([]repository.QuestionInput, error) { 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 return in, ErrQuizTitle
} }
var err error 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 return in, err
} }
if withQuestions { if withQuestions {
@@ -0,0 +1 @@
ALTER TABLE quizzes DROP COLUMN IF EXISTS target_year;
+5
View File
@@ -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;