feat: Add comprehensive API usage examples for v3, ERD, data dictionary, and course cloning documentation.
This commit is contained in:
parent
6c4d10963e
commit
a1f7ee057d
9 changed files with 1661 additions and 10 deletions
360
docs/api-docs/api_course_cloning.md
Normal file
360
docs/api-docs/api_course_cloning.md
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# Course Cloning API
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
API endpoint for cloning an entire course including all content, lessons, quizzes, and attachments.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Clone Course
|
||||
|
||||
### Endpoint
|
||||
```http
|
||||
POST /api/instructor/courses/:courseId/clone
|
||||
Authorization: Bearer <instructor-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"new_title": {
|
||||
"th": "Python สำหรับผู้เริ่มต้น (ปรับปรุง 2026)",
|
||||
"en": "Python for Beginners (Updated 2026)"
|
||||
},
|
||||
"new_slug": "python-beginners-2026",
|
||||
"copy_settings": {
|
||||
"copy_chapters": true,
|
||||
"copy_lessons": true,
|
||||
"copy_quizzes": true,
|
||||
"copy_attachments": true,
|
||||
"copy_announcements": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response 201
|
||||
```json
|
||||
{
|
||||
"new_course_id": 25,
|
||||
"cloned_from_course_id": 1,
|
||||
"status": "DRAFT",
|
||||
"message": "Course cloned successfully",
|
||||
"cloned_content": {
|
||||
"chapters": 10,
|
||||
"lessons": 45,
|
||||
"quizzes": 10,
|
||||
"questions": 150,
|
||||
"attachments": 30,
|
||||
"total_duration_minutes": 1200
|
||||
},
|
||||
"next_steps": [
|
||||
"Edit content as needed",
|
||||
"Update videos if necessary",
|
||||
"Submit for approval when ready"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 What Gets Cloned
|
||||
|
||||
### ✅ Copied:
|
||||
- Course info (title, description, price, thumbnail)
|
||||
- All chapters
|
||||
- All lessons (including video references)
|
||||
- All quizzes and questions
|
||||
- All attachments (files copied to new location)
|
||||
- Course settings (is_free, have_certificate)
|
||||
- Lesson prerequisites
|
||||
- Sort orders
|
||||
|
||||
### ❌ NOT Copied:
|
||||
- Enrollments
|
||||
- Student progress
|
||||
- Reviews/ratings
|
||||
- Announcements (optional)
|
||||
- Approval status (new course = DRAFT)
|
||||
- Statistics
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### 1. Update Videos
|
||||
```
|
||||
1. Clone course
|
||||
2. Replace videos in new course
|
||||
3. Submit for approval
|
||||
4. Publish
|
||||
```
|
||||
|
||||
### 2. Add Lessons
|
||||
```
|
||||
1. Clone course
|
||||
2. Add new lessons/chapters
|
||||
3. Submit for approval
|
||||
4. Publish
|
||||
```
|
||||
|
||||
### 3. Major Restructure
|
||||
```
|
||||
1. Clone course
|
||||
2. Reorganize chapters/lessons
|
||||
3. Update content
|
||||
4. Submit for approval
|
||||
5. Publish
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Permissions
|
||||
|
||||
**Who can clone**:
|
||||
- ✅ Course owner (created_by)
|
||||
- ✅ Primary instructor (is_primary = true)
|
||||
- ✅ Admin
|
||||
|
||||
**Restrictions**:
|
||||
- ❌ Cannot clone archived courses
|
||||
- ❌ Cannot clone rejected courses
|
||||
- ✅ Can clone approved/published courses
|
||||
|
||||
---
|
||||
|
||||
## 📊 Clone Process
|
||||
|
||||
### Backend Process:
|
||||
```javascript
|
||||
async function cloneCourse(originalCourseId, newData) {
|
||||
// 1. Create new course
|
||||
const newCourse = await createCourse({
|
||||
...originalCourse,
|
||||
title: newData.new_title,
|
||||
slug: newData.new_slug,
|
||||
status: 'DRAFT',
|
||||
created_by: currentUser.id
|
||||
});
|
||||
|
||||
// 2. Clone chapters
|
||||
for (const chapter of originalChapters) {
|
||||
const newChapter = await createChapter({
|
||||
...chapter,
|
||||
course_id: newCourse.id
|
||||
});
|
||||
|
||||
// 3. Clone lessons
|
||||
for (const lesson of chapter.lessons) {
|
||||
const newLesson = await createLesson({
|
||||
...lesson,
|
||||
chapter_id: newChapter.id
|
||||
});
|
||||
|
||||
// 4. Clone attachments (copy files)
|
||||
for (const attachment of lesson.attachments) {
|
||||
await copyFileToS3(attachment.file_path, newPath);
|
||||
await createAttachment({
|
||||
...attachment,
|
||||
lesson_id: newLesson.id,
|
||||
file_path: newPath
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Clone quizzes
|
||||
if (lesson.quiz) {
|
||||
const newQuiz = await createQuiz({
|
||||
...lesson.quiz,
|
||||
lesson_id: newLesson.id
|
||||
});
|
||||
|
||||
// 6. Clone questions
|
||||
for (const question of lesson.quiz.questions) {
|
||||
const newQuestion = await createQuestion({
|
||||
...question,
|
||||
quiz_id: newQuiz.id
|
||||
});
|
||||
|
||||
// 7. Clone choices
|
||||
for (const choice of question.choices) {
|
||||
await createChoice({
|
||||
...choice,
|
||||
question_id: newQuestion.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newCourse;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Error Handling
|
||||
|
||||
### Error: Course Not Found
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "COURSE_NOT_FOUND",
|
||||
"message": "Course not found or you don't have permission"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error: Slug Already Exists
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "SLUG_EXISTS",
|
||||
"message": "Course slug already exists. Please use a different slug.",
|
||||
"suggestion": "python-beginners-2026-v2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error: Clone Failed
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "CLONE_FAILED",
|
||||
"message": "Failed to clone course. Please try again.",
|
||||
"details": "Error copying attachment: file.pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Frontend Integration
|
||||
|
||||
### Clone Button
|
||||
```javascript
|
||||
async function cloneCourse(courseId) {
|
||||
const response = await fetch(`/api/instructor/courses/${courseId}/clone`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
new_title: {
|
||||
th: `${originalTitle.th} (คัดลอก)`,
|
||||
en: `${originalTitle.en} (Copy)`
|
||||
},
|
||||
new_slug: `${originalSlug}-copy-${Date.now()}`
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Redirect to edit new course
|
||||
window.location.href = `/instructor/courses/${data.new_course_id}/edit`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 Performance Considerations
|
||||
|
||||
### Large Courses:
|
||||
- Use background job for cloning
|
||||
- Show progress indicator
|
||||
- Send notification when complete
|
||||
|
||||
```http
|
||||
POST /api/instructor/courses/:courseId/clone
|
||||
{
|
||||
"async": true
|
||||
}
|
||||
|
||||
Response 202:
|
||||
{
|
||||
"job_id": "clone-job-123",
|
||||
"status": "PROCESSING",
|
||||
"message": "Clone in progress. You will be notified when complete.",
|
||||
"estimated_time_minutes": 5
|
||||
}
|
||||
```
|
||||
|
||||
### Check Job Status:
|
||||
```http
|
||||
GET /api/instructor/clone-jobs/:jobId
|
||||
|
||||
Response:
|
||||
{
|
||||
"job_id": "clone-job-123",
|
||||
"status": "COMPLETED",
|
||||
"new_course_id": 25,
|
||||
"progress": {
|
||||
"chapters": "10/10",
|
||||
"lessons": "45/45",
|
||||
"quizzes": "10/10"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Endpoints
|
||||
|
||||
### Get Clone History
|
||||
```http
|
||||
GET /api/instructor/courses/:courseId/clones
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"original_course_id": 1,
|
||||
"clones": [
|
||||
{
|
||||
"id": 25,
|
||||
"title": "Python for Beginners (Updated 2026)",
|
||||
"status": "PUBLISHED",
|
||||
"cloned_at": "2026-01-06T15:00:00Z",
|
||||
"cloned_by": "Prof. John Smith"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"title": "Python for Beginners (Advanced)",
|
||||
"status": "DRAFT",
|
||||
"cloned_at": "2026-01-05T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
**Endpoint**: `POST /instructor/courses/:courseId/clone`
|
||||
|
||||
**Copies**:
|
||||
- ✅ All chapters, lessons, quizzes
|
||||
- ✅ All attachments (files copied)
|
||||
- ✅ All settings and configurations
|
||||
|
||||
**Does NOT copy**:
|
||||
- ❌ Enrollments or student data
|
||||
- ❌ Reviews or ratings
|
||||
- ❌ Approval status
|
||||
|
||||
**Result**: New DRAFT course ready for editing
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
After cloning:
|
||||
1. Edit new course as needed
|
||||
2. Update videos if necessary
|
||||
3. Add/remove lessons
|
||||
4. Submit for approval
|
||||
5. Publish when approved
|
||||
Loading…
Add table
Add a link
Reference in a new issue