From 71bc09a63865249db3523165418ffe3bf556f4bc Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 20 May 2026 04:11:09 -0700 Subject: [PATCH] Make practice title optional on create. POST /practices and exam-prep practice create accept missing or null title; persist as empty string. Refresh OpenAPI and document the behavior. Co-authored-by: Cursor --- docs/PRACTICE_CREATION_API_GUIDE.md | 2 + docs/docs.go | 71 ++++++++++++++++--- docs/swagger.json | 71 ++++++++++++++++--- docs/swagger.yaml | 54 ++++++++++++-- internal/domain/exam_prep_practice.go | 2 +- internal/domain/practice.go | 2 +- .../repository/exam_prep_lesson_practices.go | 2 +- internal/repository/lms_practices.go | 9 ++- 8 files changed, 187 insertions(+), 26 deletions(-) diff --git a/docs/PRACTICE_CREATION_API_GUIDE.md b/docs/PRACTICE_CREATION_API_GUIDE.md index 3494b52..5c95520 100644 --- a/docs/PRACTICE_CREATION_API_GUIDE.md +++ b/docs/PRACTICE_CREATION_API_GUIDE.md @@ -415,6 +415,8 @@ This creates the practice record scoped to lesson. ### Request +`title` is optional; omit it or use an empty string to create a practice without a display title (stored as empty). + Include `publish_status`: `DRAFT` to hide the practice from subscribed learners until you set it to `PUBLISHED` (via create or `PUT /practices/:id`). Omit the field or send `PUBLISHED` to go live immediately (backward compatible). ```json diff --git a/docs/docs.go b/docs/docs.go index b303066..1408e45 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -10434,13 +10434,21 @@ const docTemplate = `{ "domain.CreateExamPrepPracticeInput": { "type": "object", "required": [ - "question_set_id", - "title" + "question_set_id" ], "properties": { "persona_id": { "type": "integer" }, + "publish_status": { + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, @@ -10485,11 +10493,19 @@ const docTemplate = `{ "type": "string" }, "publish_status": { - "description": "Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.", - "type": "string" + "description": "Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.", + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] }, "sort_order": { - "type": "integer" + "description": "SortOrder within the module when set; omit to append after current max within module_id.", + "type": "integer", + "minimum": 0 }, "thumbnail": { "type": "string" @@ -10516,6 +10532,11 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "sort_order": { + "description": "SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course).", + "type": "integer", + "minimum": 0 } } }, @@ -10524,8 +10545,7 @@ const docTemplate = `{ "required": [ "parent_id", "parent_kind", - "question_set_id", - "title" + "question_set_id" ], "properties": { "parent_id": { @@ -10546,6 +10566,16 @@ const docTemplate = `{ "persona_id": { "type": "integer" }, + "publish_status": { + "description": "Omit or empty for backward compatibility defaults to PUBLISHED; set DRAFT to save hidden from learners until published.", + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, @@ -11327,6 +11357,15 @@ const docTemplate = `{ "persona_id": { "type": "integer" }, + "publish_status": { + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, @@ -11380,8 +11419,13 @@ const docTemplate = `{ "type": "string" }, "publish_status": { - "description": "DRAFT or PUBLISHED", - "type": "string" + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] }, "sort_order": { "type": "integer" @@ -11420,6 +11464,15 @@ const docTemplate = `{ "persona_id": { "type": "integer" }, + "publish_status": { + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, diff --git a/docs/swagger.json b/docs/swagger.json index 6e65d4f..99f7481 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -10426,13 +10426,21 @@ "domain.CreateExamPrepPracticeInput": { "type": "object", "required": [ - "question_set_id", - "title" + "question_set_id" ], "properties": { "persona_id": { "type": "integer" }, + "publish_status": { + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, @@ -10477,11 +10485,19 @@ "type": "string" }, "publish_status": { - "description": "Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.", - "type": "string" + "description": "Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.", + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] }, "sort_order": { - "type": "integer" + "description": "SortOrder within the module when set; omit to append after current max within module_id.", + "type": "integer", + "minimum": 0 }, "thumbnail": { "type": "string" @@ -10508,6 +10524,11 @@ }, "name": { "type": "string" + }, + "sort_order": { + "description": "SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course).", + "type": "integer", + "minimum": 0 } } }, @@ -10516,8 +10537,7 @@ "required": [ "parent_id", "parent_kind", - "question_set_id", - "title" + "question_set_id" ], "properties": { "parent_id": { @@ -10538,6 +10558,16 @@ "persona_id": { "type": "integer" }, + "publish_status": { + "description": "Omit or empty for backward compatibility defaults to PUBLISHED; set DRAFT to save hidden from learners until published.", + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, @@ -11319,6 +11349,15 @@ "persona_id": { "type": "integer" }, + "publish_status": { + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, @@ -11372,8 +11411,13 @@ "type": "string" }, "publish_status": { - "description": "DRAFT or PUBLISHED", - "type": "string" + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] }, "sort_order": { "type": "integer" @@ -11412,6 +11456,15 @@ "persona_id": { "type": "integer" }, + "publish_status": { + "type": "string", + "enum": [ + "DRAFT", + "draft", + "PUBLISHED", + "published" + ] + }, "question_set_id": { "type": "integer" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5ff816a..e56b056 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -389,6 +389,13 @@ definitions: properties: persona_id: type: integer + publish_status: + enum: + - DRAFT + - draft + - PUBLISHED + - published + type: string question_set_id: type: integer quick_tips: @@ -401,7 +408,6 @@ definitions: type: string required: - question_set_id - - title type: object domain.CreateExamPrepUnitInput: properties: @@ -419,9 +425,18 @@ definitions: description: type: string publish_status: - description: Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons. + description: Omit or empty defaults to DRAFT; set PUBLISHED to make visible + to learners immediately. + enum: + - DRAFT + - draft + - PUBLISHED + - published type: string sort_order: + description: SortOrder within the module when set; omit to append after current + max within module_id. + minimum: 0 type: integer thumbnail: type: string @@ -440,6 +455,11 @@ definitions: type: string name: type: string + sort_order: + description: SortOrder within the course when set; omit to append after current + max within course_id (uniqueness is per-course). + minimum: 0 + type: integer required: - name type: object @@ -456,6 +476,15 @@ definitions: - LESSON persona_id: type: integer + publish_status: + description: Omit or empty for backward compatibility defaults to PUBLISHED; + set DRAFT to save hidden from learners until published. + enum: + - DRAFT + - draft + - PUBLISHED + - published + type: string question_set_id: type: integer quick_tips: @@ -470,7 +499,6 @@ definitions: - parent_id - parent_kind - question_set_id - - title type: object domain.CreateProgramInput: properties: @@ -996,6 +1024,13 @@ definitions: properties: persona_id: type: integer + publish_status: + enum: + - DRAFT + - draft + - PUBLISHED + - published + type: string question_set_id: type: integer quick_tips: @@ -1031,7 +1066,11 @@ definitions: description: type: string publish_status: - description: DRAFT or PUBLISHED + enum: + - DRAFT + - draft + - PUBLISHED + - published type: string sort_order: type: integer @@ -1057,6 +1096,13 @@ definitions: properties: persona_id: type: integer + publish_status: + enum: + - DRAFT + - draft + - PUBLISHED + - published + type: string question_set_id: type: integer quick_tips: diff --git a/internal/domain/exam_prep_practice.go b/internal/domain/exam_prep_practice.go index ef780fc..91e335d 100644 --- a/internal/domain/exam_prep_practice.go +++ b/internal/domain/exam_prep_practice.go @@ -24,7 +24,7 @@ func (p ExamPrepPractice) VisibleToLearners() bool { // CreateExamPrepPracticeInput is the body for POST .../exam-prep/lessons/{lessonId}/practices (lesson from path). type CreateExamPrepPracticeInput struct { - Title string `json:"title" validate:"required"` + Title *string `json:"title,omitempty"` StoryDescription *string `json:"story_description,omitempty"` StoryImage *string `json:"story_image,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"` diff --git a/internal/domain/practice.go b/internal/domain/practice.go index dca0eba..765b50e 100644 --- a/internal/domain/practice.go +++ b/internal/domain/practice.go @@ -63,7 +63,7 @@ func (p Practice) VisibleToLearners() bool { type CreatePracticeInput struct { ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"` ParentID int64 `json:"parent_id" validate:"required,gt=0"` - Title string `json:"title" validate:"required"` + Title *string `json:"title,omitempty"` StoryDescription *string `json:"story_description,omitempty"` StoryImage *string `json:"story_image,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"` diff --git a/internal/repository/exam_prep_lesson_practices.go b/internal/repository/exam_prep_lesson_practices.go index a26294a..b928c57 100644 --- a/internal/repository/exam_prep_lesson_practices.go +++ b/internal/repository/exam_prep_lesson_practices.go @@ -51,7 +51,7 @@ func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64 ps := domain.ParsePracticePublishStatusInput(in.PublishStatus) p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{ UnitModuleLessonID: lessonID, - Title: in.Title, + Title: derefString(in.Title), StoryDescription: toPgText(in.StoryDescription), StoryImage: toPgText(in.StoryImage), PersonaID: int64PtrToPg8(in.PersonaID), diff --git a/internal/repository/lms_practices.go b/internal/repository/lms_practices.go index 4dbd309..a84eb16 100644 --- a/internal/repository/lms_practices.go +++ b/internal/repository/lms_practices.go @@ -33,6 +33,13 @@ func optionalInt8UpdateID(val *int64) pgtype.Int8 { return pgtype.Int8{Int64: *val, Valid: true} } +func derefString(p *string) string { + if p == nil { + return "" + } + return *p +} + func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice { out := domain.Practice{ ID: p.ID, @@ -98,7 +105,7 @@ func (s *Store) CreateLmsPractice( CourseID: int64PtrToPg8(courseID), ModuleID: int64PtrToPg8(moduleID), LessonID: int64PtrToPg8(lessonID), - Title: in.Title, + Title: derefString(in.Title), StoryDescription: toPgText(in.StoryDescription), StoryImage: toPgText(in.StoryImage), PersonaID: int64PtrToPg8(in.PersonaID),