6.8 KiB
6.8 KiB
Course Cloning API
📋 Overview
API endpoint for cloning an entire course including all content, lessons, quizzes, and attachments.
🔧 Clone Course
Endpoint
POST /api/instructor/courses/:courseId/clone
Authorization: Bearer <instructor-token>
Content-Type: application/json
Request Body
{
"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
{
"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:
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
{
"error": {
"code": "COURSE_NOT_FOUND",
"message": "Course not found or you don't have permission"
}
}
Error: Slug Already Exists
{
"error": {
"code": "SLUG_EXISTS",
"message": "Course slug already exists. Please use a different slug.",
"suggestion": "python-beginners-2026-v2"
}
}
Error: Clone Failed
{
"error": {
"code": "CLONE_FAILED",
"message": "Failed to clone course. Please try again.",
"details": "Error copying attachment: file.pdf"
}
}
🎨 Frontend Integration
Clone Button
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
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:
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
GET /api/instructor/courses/:courseId/clones
Response:
{
"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:
- Edit new course as needed
- Update videos if necessary
- Add/remove lessons
- Submit for approval
- Publish when approved