361 lines
6.8 KiB
Markdown
361 lines
6.8 KiB
Markdown
|
|
# 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
|