unique quiz constraint

This commit is contained in:
Krispire Admin
2026-10-16 15:23:06 +05:30
parent b1f78b1655
commit eb02d08f14
6 changed files with 128 additions and 6 deletions
+31 -5
View File
@@ -12,11 +12,14 @@ import (
)
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")
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")
// ErrQuizTargetTaken: that chapter, lecture or conversation already has a
// quiz (enforced by the quizzes_one_per_target index).
ErrQuizTargetTaken = errors.New("only one quiz is allowed per link")
ErrQuizAlreadyAnswer = errors.New("that question is already answered")
ErrQuizNotFinished = errors.New("the quiz isn't finished yet")
)
@@ -148,6 +151,9 @@ func (r *QuizRepo) Create(ctx context.Context, in QuizInput, createdBy string) (
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle, createdBy,
).Scan(&id); err != nil {
if isUniqueViolation(err, "quizzes_one_per_target") {
return "", ErrQuizTargetTaken
}
return "", fmt.Errorf("create quiz: %w", err)
}
if err := insertQuestions(ctx, tx, id, in.Questions); err != nil {
@@ -171,6 +177,9 @@ func (r *QuizRepo) Update(ctx context.Context, id string, in QuizInput, replaceQ
WHERE id = $1 AND deleted_at IS NULL`,
id, in.Title, in.Description, in.TargetKind, in.TargetPath, in.TargetTitle)
if err != nil {
if isUniqueViolation(err, "quizzes_one_per_target") {
return ErrQuizTargetTaken
}
return fmt.Errorf("update quiz: %w", err)
}
if tag.RowsAffected() == 0 {
@@ -207,6 +216,23 @@ func insertQuestions(ctx context.Context, tx pgx.Tx, quizID string, qs []Questio
return nil
}
// QuizOnTarget returns the live quiz (published or draft) already attached to
// a content path, other than exceptID — or "" when the path is free.
func (r *QuizRepo) QuizOnTarget(ctx context.Context, path, exceptID string) (id, title string, err error) {
err = r.pg.Pool.QueryRow(ctx, `
SELECT id, title FROM quizzes
WHERE target_path = $1 AND deleted_at IS NULL
AND ($2 = '' OR id::text <> $2)
LIMIT 1`, path, exceptID).Scan(&id, &title)
if errors.Is(err, pgx.ErrNoRows) {
return "", "", nil
}
if err != nil {
return "", "", fmt.Errorf("quiz on target: %w", err)
}
return id, title, 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)