Return learner-visible practices with access metadata.
Expose practices to learner roles based on practice shell publish state and include per-practice access fields derived from linked question set readiness so clients can manage completion/access UX explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ffbb885d06
commit
c77a97b40d
|
|
@ -34,15 +34,10 @@ SELECT
|
||||||
p.created_at,
|
p.created_at,
|
||||||
p.updated_at
|
p.updated_at
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
INNER JOIN question_sets qs ON qs.id = p.question_set_id
|
|
||||||
WHERE p.course_id = $1
|
WHERE p.course_id = $1
|
||||||
AND (
|
AND (
|
||||||
sqlc.arg('published_only')::boolean = FALSE
|
sqlc.arg('published_only')::boolean = FALSE
|
||||||
OR (
|
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||||
p.publish_status = 'PUBLISHED'::TEXT
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
LIMIT $2 OFFSET $3;
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
@ -64,15 +59,10 @@ SELECT
|
||||||
p.created_at,
|
p.created_at,
|
||||||
p.updated_at
|
p.updated_at
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
INNER JOIN question_sets qs ON qs.id = p.question_set_id
|
|
||||||
WHERE p.module_id = $1
|
WHERE p.module_id = $1
|
||||||
AND (
|
AND (
|
||||||
sqlc.arg('published_only')::boolean = FALSE
|
sqlc.arg('published_only')::boolean = FALSE
|
||||||
OR (
|
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||||
p.publish_status = 'PUBLISHED'::TEXT
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
LIMIT $2 OFFSET $3;
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
@ -94,15 +84,10 @@ SELECT
|
||||||
p.created_at,
|
p.created_at,
|
||||||
p.updated_at
|
p.updated_at
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
INNER JOIN question_sets qs ON qs.id = p.question_set_id
|
|
||||||
WHERE p.lesson_id = $1
|
WHERE p.lesson_id = $1
|
||||||
AND (
|
AND (
|
||||||
sqlc.arg('published_only')::boolean = FALSE
|
sqlc.arg('published_only')::boolean = FALSE
|
||||||
OR (
|
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||||
p.publish_status = 'PUBLISHED'::TEXT
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
LIMIT $2 OFFSET $3;
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
|
||||||
|
|
@ -147,15 +147,10 @@ SELECT
|
||||||
p.created_at,
|
p.created_at,
|
||||||
p.updated_at
|
p.updated_at
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
INNER JOIN question_sets qs ON qs.id = p.question_set_id
|
|
||||||
WHERE p.course_id = $1
|
WHERE p.course_id = $1
|
||||||
AND (
|
AND (
|
||||||
$4::boolean = FALSE
|
$4::boolean = FALSE
|
||||||
OR (
|
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||||
p.publish_status = 'PUBLISHED'::TEXT
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
LIMIT $2 OFFSET $3
|
LIMIT $2 OFFSET $3
|
||||||
|
|
@ -242,15 +237,10 @@ SELECT
|
||||||
p.created_at,
|
p.created_at,
|
||||||
p.updated_at
|
p.updated_at
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
INNER JOIN question_sets qs ON qs.id = p.question_set_id
|
|
||||||
WHERE p.lesson_id = $1
|
WHERE p.lesson_id = $1
|
||||||
AND (
|
AND (
|
||||||
$4::boolean = FALSE
|
$4::boolean = FALSE
|
||||||
OR (
|
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||||
p.publish_status = 'PUBLISHED'::TEXT
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
LIMIT $2 OFFSET $3
|
LIMIT $2 OFFSET $3
|
||||||
|
|
@ -337,15 +327,10 @@ SELECT
|
||||||
p.created_at,
|
p.created_at,
|
||||||
p.updated_at
|
p.updated_at
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
INNER JOIN question_sets qs ON qs.id = p.question_set_id
|
|
||||||
WHERE p.module_id = $1
|
WHERE p.module_id = $1
|
||||||
AND (
|
AND (
|
||||||
$4::boolean = FALSE
|
$4::boolean = FALSE
|
||||||
OR (
|
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||||
p.publish_status = 'PUBLISHED'::TEXT
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
LIMIT $2 OFFSET $3
|
LIMIT $2 OFFSET $3
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,16 @@ type Practice struct {
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
PublishStatus PracticePublishStatus `json:"publish_status"`
|
PublishStatus PracticePublishStatus `json:"publish_status"`
|
||||||
QuickTips *string `json:"quick_tips,omitempty"`
|
QuickTips *string `json:"quick_tips,omitempty"`
|
||||||
|
Access *PracticeAccess `json:"access,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PracticeAccess struct {
|
||||||
|
IsAccessible bool `json:"is_accessible"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// VisibleToLearners is true when the practice shell should appear in subscribed learner catalogs and progression.
|
// VisibleToLearners is true when the practice shell should appear in subscribed learner catalogs and progression.
|
||||||
func (p Practice) VisibleToLearners() bool {
|
func (p Practice) VisibleToLearners() bool {
|
||||||
return p.PublishStatus == PracticePublishPublished
|
return p.PublishStatus == PracticePublishPublished
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
@ -76,7 +77,8 @@ func (h *Handler) ListPracticesByCourse(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
publishedOnly := !h.canManageLMSPractices(c)
|
role, _ := c.Locals("role").(domain.Role)
|
||||||
|
publishedOnly := role.IsCustomerLearnerRole() || !h.canManageLMSPractices(c)
|
||||||
items, total, err := h.practiceSvc.ListByCourse(c.Context(), courseID, publishedOnly, int32(limit), int32(offset))
|
items, total, err := h.practiceSvc.ListByCourse(c.Context(), courseID, publishedOnly, int32(limit), int32(offset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
|
@ -84,6 +86,9 @@ func (h *Handler) ListPracticesByCourse(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := h.applyPracticeAccess(c.Context(), c, items); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to build practices", Error: err.Error()})
|
||||||
|
}
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Practices retrieved successfully",
|
Message: "Practices retrieved successfully",
|
||||||
Data: fiber.Map{
|
Data: fiber.Map{
|
||||||
|
|
@ -108,7 +113,8 @@ func (h *Handler) ListPracticesByModule(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
publishedOnly := !h.canManageLMSPractices(c)
|
role, _ := c.Locals("role").(domain.Role)
|
||||||
|
publishedOnly := role.IsCustomerLearnerRole() || !h.canManageLMSPractices(c)
|
||||||
items, total, err := h.practiceSvc.ListByModule(c.Context(), moduleID, publishedOnly, int32(limit), int32(offset))
|
items, total, err := h.practiceSvc.ListByModule(c.Context(), moduleID, publishedOnly, int32(limit), int32(offset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||||
|
|
@ -116,6 +122,9 @@ func (h *Handler) ListPracticesByModule(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := h.applyPracticeAccess(c.Context(), c, items); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to build practices", Error: err.Error()})
|
||||||
|
}
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Practices retrieved successfully",
|
Message: "Practices retrieved successfully",
|
||||||
Data: fiber.Map{
|
Data: fiber.Map{
|
||||||
|
|
@ -140,7 +149,8 @@ func (h *Handler) ListPracticesByLesson(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
publishedOnly := !h.canManageLMSPractices(c)
|
role, _ := c.Locals("role").(domain.Role)
|
||||||
|
publishedOnly := role.IsCustomerLearnerRole() || !h.canManageLMSPractices(c)
|
||||||
items, total, err := h.practiceSvc.ListByLesson(c.Context(), lessonID, publishedOnly, int32(limit), int32(offset))
|
items, total, err := h.practiceSvc.ListByLesson(c.Context(), lessonID, publishedOnly, int32(limit), int32(offset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, lessons.ErrLessonNotFound) {
|
if errors.Is(err, lessons.ErrLessonNotFound) {
|
||||||
|
|
@ -148,6 +158,9 @@ func (h *Handler) ListPracticesByLesson(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := h.applyPracticeAccess(c.Context(), c, items); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to build practices", Error: err.Error()})
|
||||||
|
}
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Practices retrieved successfully",
|
Message: "Practices retrieved successfully",
|
||||||
Data: fiber.Map{
|
Data: fiber.Map{
|
||||||
|
|
@ -180,9 +193,49 @@ func (h *Handler) GetPractice(c *fiber.Ctx) error {
|
||||||
if !p.VisibleToLearners() && !h.canManageLMSPractices(c) {
|
if !p.VisibleToLearners() && !h.canManageLMSPractices(c) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
|
||||||
}
|
}
|
||||||
|
if err := h.applyPracticeAccess(c.Context(), c, []domain.Practice{p}); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to build practice", Error: err.Error()})
|
||||||
|
}
|
||||||
|
role, _ := c.Locals("role").(domain.Role)
|
||||||
|
if role.IsCustomerLearnerRole() {
|
||||||
|
if set, err := h.questionsSvc.GetQuestionSetByID(c.Context(), p.QuestionSetID); err == nil {
|
||||||
|
p.Access = practiceAccessForQuestionSet(set)
|
||||||
|
} else {
|
||||||
|
p.Access = &domain.PracticeAccess{IsAccessible: false, Reason: "Question set not found"}
|
||||||
|
}
|
||||||
|
}
|
||||||
return c.JSON(domain.Response{Message: "Practice retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
return c.JSON(domain.Response{Message: "Practice retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) applyPracticeAccess(ctx context.Context, c *fiber.Ctx, items []domain.Practice) error {
|
||||||
|
role, _ := c.Locals("role").(domain.Role)
|
||||||
|
if !role.IsCustomerLearnerRole() {
|
||||||
|
for i := range items {
|
||||||
|
items[i].Access = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range items {
|
||||||
|
set, err := h.questionsSvc.GetQuestionSetByID(ctx, items[i].QuestionSetID)
|
||||||
|
if err != nil {
|
||||||
|
items[i].Access = &domain.PracticeAccess{IsAccessible: false, Reason: "Question set not found"}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items[i].Access = practiceAccessForQuestionSet(set)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func practiceAccessForQuestionSet(set domain.QuestionSet) *domain.PracticeAccess {
|
||||||
|
if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) {
|
||||||
|
return &domain.PracticeAccess{IsAccessible: false, Reason: "Question set is not a practice"}
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(set.Status, "PUBLISHED") {
|
||||||
|
return &domain.PracticeAccess{IsAccessible: false, Reason: "Practice is not published yet"}
|
||||||
|
}
|
||||||
|
return &domain.PracticeAccess{IsAccessible: true}
|
||||||
|
}
|
||||||
|
|
||||||
// UpdatePractice godoc
|
// UpdatePractice godoc
|
||||||
// @Tags practices
|
// @Tags practices
|
||||||
// @Param id path int true "Practice ID"
|
// @Param id path int true "Practice ID"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user