Add GET question-sets question-types endpoint for practice sets.
Returns distinct question_type values with per-type counts so clients can resolve types from question_set_id without loading full questions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
256183ae64
commit
a75700ffaa
|
|
@ -102,6 +102,17 @@ JOIN questions q ON q.id = qsi.question_id
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
AND q.status != 'ARCHIVED';
|
AND q.status != 'ARCHIVED';
|
||||||
|
|
||||||
|
-- name: GetQuestionTypeCountsInSet :many
|
||||||
|
SELECT
|
||||||
|
q.question_type,
|
||||||
|
COUNT(*)::bigint AS question_count
|
||||||
|
FROM question_set_items qsi
|
||||||
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
|
WHERE qsi.set_id = $1
|
||||||
|
AND q.status != 'ARCHIVED'
|
||||||
|
GROUP BY q.question_type
|
||||||
|
ORDER BY q.question_type;
|
||||||
|
|
||||||
-- name: GetQuestionSetsContainingQuestion :many
|
-- name: GetQuestionSetsContainingQuestion :many
|
||||||
SELECT qs.*
|
SELECT qs.*
|
||||||
FROM question_sets qs
|
FROM question_sets qs
|
||||||
|
|
|
||||||
|
|
@ -362,6 +362,43 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetQuestionTypeCountsInSet = `-- name: GetQuestionTypeCountsInSet :many
|
||||||
|
SELECT
|
||||||
|
q.question_type,
|
||||||
|
COUNT(*)::bigint AS question_count
|
||||||
|
FROM question_set_items qsi
|
||||||
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
|
WHERE qsi.set_id = $1
|
||||||
|
AND q.status != 'ARCHIVED'
|
||||||
|
GROUP BY q.question_type
|
||||||
|
ORDER BY q.question_type
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetQuestionTypeCountsInSetRow struct {
|
||||||
|
QuestionType string `json:"question_type"`
|
||||||
|
QuestionCount int64 `json:"question_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetQuestionTypeCountsInSet(ctx context.Context, setID int64) ([]GetQuestionTypeCountsInSetRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetQuestionTypeCountsInSet, setID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetQuestionTypeCountsInSetRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetQuestionTypeCountsInSetRow
|
||||||
|
if err := rows.Scan(&i.QuestionType, &i.QuestionCount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const RemoveQuestionFromSet = `-- name: RemoveQuestionFromSet :exec
|
const RemoveQuestionFromSet = `-- name: RemoveQuestionFromSet :exec
|
||||||
DELETE FROM question_set_items
|
DELETE FROM question_set_items
|
||||||
WHERE set_id = $1 AND question_id = $2
|
WHERE set_id = $1 AND question_id = $2
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,19 @@ type QuestionSetItem struct {
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuestionSetQuestionTypeCount is one question_type present in a set with how many questions use it.
|
||||||
|
type QuestionSetQuestionTypeCount struct {
|
||||||
|
QuestionType string `json:"question_type"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuestionSetQuestionTypesSummary summarizes distinct question types in a question set (e.g. linked practice).
|
||||||
|
type QuestionSetQuestionTypesSummary struct {
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuestionTypes []QuestionSetQuestionTypeCount `json:"question_types"`
|
||||||
|
TotalQuestions int64 `json:"total_questions"`
|
||||||
|
}
|
||||||
|
|
||||||
type QuestionSetItemWithQuestion struct {
|
type QuestionSetItemWithQuestion struct {
|
||||||
QuestionSetItem
|
QuestionSetItem
|
||||||
QuestionText string
|
QuestionText string
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ type QuestionStore interface {
|
||||||
RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error
|
RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error
|
||||||
UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error
|
UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error
|
||||||
CountQuestionsInSet(ctx context.Context, setID int64) (int64, error)
|
CountQuestionsInSet(ctx context.Context, setID int64) (int64, error)
|
||||||
|
GetQuestionTypeCountsInSet(ctx context.Context, setID int64) ([]domain.QuestionSetQuestionTypeCount, error)
|
||||||
GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error)
|
GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error)
|
||||||
|
|
||||||
// User Personas in Question Sets
|
// User Personas in Question Sets
|
||||||
|
|
|
||||||
|
|
@ -1226,6 +1226,21 @@ func (s *Store) CountQuestionsInSet(ctx context.Context, setID int64) (int64, er
|
||||||
return s.queries.CountQuestionsInSet(ctx, setID)
|
return s.queries.CountQuestionsInSet(ctx, setID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetQuestionTypeCountsInSet(ctx context.Context, setID int64) ([]domain.QuestionSetQuestionTypeCount, error) {
|
||||||
|
rows, err := s.queries.GetQuestionTypeCountsInSet(ctx, setID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]domain.QuestionSetQuestionTypeCount, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
result[i] = domain.QuestionSetQuestionTypeCount{
|
||||||
|
QuestionType: r.QuestionType,
|
||||||
|
Count: r.QuestionCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
|
func (s *Store) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
|
||||||
sets, err := s.queries.GetQuestionSetsContainingQuestion(ctx, questionID)
|
sets, err := s.queries.GetQuestionSetsContainingQuestion(ctx, questionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,25 @@ func (s *Service) CountQuestionsInSet(ctx context.Context, setID int64) (int64,
|
||||||
return s.questionStore.CountQuestionsInSet(ctx, setID)
|
return s.questionStore.CountQuestionsInSet(ctx, setID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetQuestionTypesInSet(ctx context.Context, setID int64) (domain.QuestionSetQuestionTypesSummary, error) {
|
||||||
|
counts, err := s.questionStore.GetQuestionTypeCountsInSet(ctx, setID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.QuestionSetQuestionTypesSummary{}, err
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
for _, c := range counts {
|
||||||
|
total += c.Count
|
||||||
|
}
|
||||||
|
if counts == nil {
|
||||||
|
counts = []domain.QuestionSetQuestionTypeCount{}
|
||||||
|
}
|
||||||
|
return domain.QuestionSetQuestionTypesSummary{
|
||||||
|
QuestionSetID: setID,
|
||||||
|
QuestionTypes: counts,
|
||||||
|
TotalQuestions: total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
|
func (s *Service) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
|
||||||
return s.questionStore.GetQuestionSetsContainingQuestion(ctx, questionID)
|
return s.questionStore.GetQuestionSetsContainingQuestion(ctx, questionID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1322,6 +1322,49 @@ func (h *Handler) AddQuestionToSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetQuestionTypesInSet godoc
|
||||||
|
// @Summary List question types in a question set
|
||||||
|
// @Description Returns distinct question_type values (with counts) for non-archived questions linked to the set. Use the question_set_id from a practice to see which types were used to build it.
|
||||||
|
// @Tags question-set-items
|
||||||
|
// @Produce json
|
||||||
|
// @Param setId path int true "Question Set ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/question-sets/{setId}/question-types [get]
|
||||||
|
func (h *Handler) GetQuestionTypesInSet(c *fiber.Ctx) error {
|
||||||
|
setID, err := strconv.ParseInt(c.Params("setId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid set ID",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.questionsSvc.GetQuestionSetByID(c.Context(), setID); err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Question set not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := h.questionsSvc.GetQuestionTypesInSet(c.Context(), setID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to get question types in set",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Question types retrieved successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
Data: summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GetQuestionSetItems godoc
|
// GetQuestionSetItems godoc
|
||||||
// @Summary Get questions in set
|
// @Summary Get questions in set
|
||||||
// @Description Returns all questions in a question set with details
|
// @Description Returns all questions in a question set with details
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,7 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
// Question Set Items
|
// Question Set Items
|
||||||
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet)
|
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet)
|
||||||
|
groupV1.Get("/question-sets/:setId/question-types", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionTypesInSet)
|
||||||
groupV1.Get("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsInSet)
|
groupV1.Get("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsInSet)
|
||||||
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
|
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
|
||||||
groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)
|
groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user