quiz grouping
This commit is contained in:
@@ -25,6 +25,8 @@ type Quiz struct {
|
|||||||
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"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user