Files
pyisu/backend/app/routes/course_builder.py
2026-03-13 14:39:43 +08:00

646 lines
21 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from sqlalchemy.orm import Session
import shutil
import os
import time
from pathlib import Path
from .. import crud, schemas, auth
from ..database import get_db
from ..models import User, CourseDraft
router = APIRouter(prefix="/course-builder", tags=["course-builder"])
# Configure upload directory - same as in main.py StaticFiles
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@router.get("/steps/{step_id}", response_model=schemas.SuccessResponse)
def get_step_content(
step_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get step content by ID
"""
step = crud.get_step_by_id(db, step_id=step_id)
if not step:
raise HTTPException(status_code=404, detail="Step not found")
# Check if user has access to the course
# TODO: Implement proper access check
# For now, allow access if user is authenticated
step_response = schemas.StepResponse.from_orm(step)
return schemas.SuccessResponse(data={"step": step_response})
@router.put("/steps/{step_id}/content", response_model=schemas.SuccessResponse)
def update_step_content(
step_id: int,
content_update: schemas.StepUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update step content (markdown)
"""
step = crud.get_step_by_id(db, step_id=step_id)
if not step:
raise HTTPException(status_code=404, detail="Step not found")
# Check if user is the author of the course
# TODO: Implement proper authorization
# For now, allow if user is authenticated
updated_step = crud.update_step(db, step_id=step_id, step_update=content_update)
if not updated_step:
raise HTTPException(status_code=500, detail="Failed to update step")
step_response = schemas.StepResponse.from_orm(updated_step)
return schemas.SuccessResponse(
data={"step": step_response},
message="Step content updated successfully"
)
@router.post("/upload", response_model=schemas.SuccessResponse)
async def upload_file(
file: UploadFile = File(...),
current_user: User = Depends(auth.get_current_user)
):
"""
Upload file (image, document, etc.) for use in course content
"""
# Validate file type
allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.pdf', '.doc', '.docx', '.txt', '.md'}
file_extension = Path(file.filename).suffix.lower()
if file_extension not in allowed_extensions:
raise HTTPException(
status_code=400,
detail=f"File type not allowed. Allowed types: {', '.join(allowed_extensions)}"
)
# Generate unique filename
unique_filename = f"{current_user.id}_{int(time.time())}_{file.filename}"
file_path = UPLOAD_DIR / unique_filename
# Save file
try:
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
# Return file URL
file_url = f"/uploads/{unique_filename}"
return schemas.SuccessResponse(
data={
"filename": file.filename,
"url": file_url,
"size": file_path.stat().st_size
},
message="File uploaded successfully"
)
@router.get("/render/markdown", response_model=schemas.SuccessResponse)
def render_markdown(
markdown: str = Form(...),
current_user: User = Depends(auth.get_current_user)
):
"""
Render markdown to HTML (server-side rendering)
This can be used for preview functionality
"""
# Simple markdown rendering (basic conversion)
# In production, use a proper markdown library like markdown2 or mistune
html_content = markdown.replace("\n", "<br>")
return schemas.SuccessResponse(
data={"html": html_content},
message="Markdown rendered (basic conversion)"
)
@router.get("/courses/{course_id}/structure", response_model=schemas.SuccessResponse)
def get_course_structure(
course_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get complete course structure for editing
"""
course = crud.get_course_by_id(db, course_id=course_id, include_structure=True)
if not course:
raise HTTPException(status_code=404, detail="Course not found")
# Check if user is the author
# Note: Add is_admin field to User model for admin access
if course.author_id != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized to edit this course")
# Convert to response schema
course_structure = schemas.CourseStructureResponse.from_orm(course)
return schemas.SuccessResponse(data={"course": course_structure})
@router.post("/steps/{step_id}/autosave", response_model=schemas.SuccessResponse)
def autosave_step_content(
step_id: int,
content: str = Form(...),
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Autosave step content (frequent saves during editing)
"""
step = crud.get_step_by_id(db, step_id=step_id)
if not step:
raise HTTPException(status_code=404, detail="Step not found")
# Update content only
step_update = schemas.StepUpdate(content=content)
updated_step = crud.update_step(db, step_id=step_id, step_update=step_update)
if not updated_step:
raise HTTPException(status_code=500, detail="Failed to autosave")
return schemas.SuccessResponse(
data={"step_id": step_id, "saved_at": updated_step.updated_at},
message="Content autosaved"
)
@router.post("/create", response_model=schemas.SuccessResponse)
async def create_course(
course_data: schemas.CourseCreateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Create a new course with content and structure
"""
try:
db_course = crud.create_course_with_content(db, course_data, current_user.id)
course_response = schemas.CourseResponse.from_orm(db_course)
return schemas.SuccessResponse(
data={"course": course_response},
message="Course created successfully"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to create course: {str(e)}"
)
# Draft endpoints - now using Course.status='draft'
@router.get("/drafts", response_model=schemas.SuccessResponse)
def get_drafts(
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get all draft courses for current user (status='draft')
"""
drafts = crud.get_user_drafts(db, current_user.id)
draft_responses = [schemas.CourseResponse.from_orm(d) for d in drafts]
return schemas.SuccessResponse(
data={"drafts": draft_responses, "total": len(draft_responses)},
message="Drafts retrieved successfully"
)
@router.get("/drafts/{draft_id}", response_model=schemas.SuccessResponse)
def get_draft(
draft_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get a specific draft course with full structure
"""
draft = crud.get_user_draft(db, current_user.id, draft_id, include_structure=True)
if not draft:
raise HTTPException(status_code=404, detail="Draft not found")
draft_data = {
"id": draft.id,
"title": draft.title,
"description": draft.description,
"level": draft.level,
"duration": draft.duration,
"content": draft.content,
"status": draft.status,
"author_id": draft.author_id,
"created_at": draft.created_at.isoformat() if draft.created_at else None,
"updated_at": draft.updated_at.isoformat() if draft.updated_at else None,
"modules": [
{
"id": module.id,
"title": module.title,
"description": module.description,
"duration": module.duration,
"order_index": module.order_index,
"lessons": [
{
"id": lesson.id,
"title": lesson.title,
"description": lesson.description,
"order_index": lesson.order_index,
"steps": [
{
"id": step.id,
"step_type": step.step_type,
"title": step.title,
"content": step.content,
"order_index": step.order_index
} for step in lesson.steps
] if lesson.steps else []
}
for lesson in module.lessons
] if module.lessons else []
}
for module in draft.modules
] if draft.modules else []
}
return schemas.SuccessResponse(data={"draft": draft_data})
@router.put("/drafts/{draft_id}", response_model=schemas.SuccessResponse)
def update_draft(
draft_id: int,
course_update: schemas.CourseUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update a draft course - replaces old /drafts/save endpoint
"""
draft = crud.get_user_draft(db, current_user.id, draft_id)
if not draft:
raise HTTPException(status_code=404, detail="Draft not found")
updated_draft = crud.update_course(db, draft_id, course_update)
draft_response = schemas.CourseResponse.from_orm(updated_draft)
return schemas.SuccessResponse(
data={"draft": draft_response, "saved_at": updated_draft.updated_at},
message="Draft updated successfully"
)
@router.delete("/drafts/{draft_id}", response_model=schemas.SuccessResponse)
def delete_draft(
draft_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Delete a draft course
"""
deleted = crud.delete_user_draft(db, draft_id, current_user.id)
if not deleted:
raise HTTPException(status_code=404, detail="Draft not found")
return schemas.SuccessResponse(message="Draft deleted successfully")
@router.post("/drafts/{draft_id}/publish", response_model=schemas.SuccessResponse)
def publish_draft(
draft_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Publish a draft by changing status to 'published'
"""
published_course = crud.publish_course(db, draft_id, current_user.id)
if not published_course:
raise HTTPException(status_code=404, detail="Draft not found")
return schemas.SuccessResponse(
data={"course": schemas.CourseResponse.from_orm(published_course)},
message="Course published successfully"
)
# Module endpoints
@router.post("/modules", response_model=schemas.SuccessResponse)
def create_module(
module: schemas.ModuleCreate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Create a new module for a course
"""
# Check if user has access to the course (optional: add course_id validation)
created_module = crud.create_module(db, module)
module_response = schemas.ModuleResponse.from_orm(created_module)
return schemas.SuccessResponse(
data={"module": module_response},
message="Module created successfully"
)
@router.get("/modules/{module_id}", response_model=schemas.SuccessResponse)
def get_module(
module_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get a module by ID
"""
module = crud.get_module_by_id(db, module_id)
if not module:
raise HTTPException(status_code=404, detail="Module not found")
# TODO: Check if user has access to this module
module_response = schemas.ModuleResponse.from_orm(module)
return schemas.SuccessResponse(data={"module": module_response})
# Lesson endpoints
@router.post("/lessons", response_model=schemas.SuccessResponse)
def create_lesson(
lesson: schemas.LessonCreate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Create a new lesson for a module
"""
# Validate that module exists
crud.get_module_by_id(db, lesson.module_id)
created_lesson = crud.create_lesson(db, lesson)
lesson_response = schemas.LessonResponse.from_orm(created_lesson)
return schemas.SuccessResponse(
data={"lesson": lesson_response},
message="Lesson created successfully"
)
@router.get("/lessons/{lesson_id}", response_model=schemas.SuccessResponse)
def get_lesson(
lesson_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get a lesson by ID with steps
"""
lesson = crud.get_lesson_by_id(db, lesson_id)
if not lesson:
raise HTTPException(status_code=404, detail="Lesson not found")
lesson_data = {
"id": lesson.id,
"module_id": lesson.module_id,
"title": lesson.title,
"description": lesson.description,
"order_index": lesson.order_index,
"created_at": lesson.created_at,
"updated_at": lesson.updated_at,
"steps": [
{
"id": step.id,
"step_type": step.step_type,
"title": step.title,
"content": step.content,
"content_html": step.content_html,
"file_url": step.file_url,
"order_index": step.order_index
}
for step in sorted(lesson.steps, key=lambda s: s.order_index)
] if lesson.steps else []
}
return schemas.SuccessResponse(data={"lesson": lesson_data})
# Update endpoints
@router.put("/modules/{module_id}", response_model=schemas.SuccessResponse)
def update_module(
module_id: int,
module_update: schemas.ModuleUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update a module
"""
updated_module = crud.update_module(db, module_id, module_update)
if not updated_module:
raise HTTPException(status_code=404, detail="Module not found")
module_response = schemas.ModuleResponse.from_orm(updated_module)
return schemas.SuccessResponse(
data={"module": module_response},
message="Module updated successfully"
)
@router.delete("/modules/{module_id}", response_model=schemas.SuccessResponse)
def delete_module(
module_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Delete a module
"""
deleted = crud.delete_module(db, module_id)
if not deleted:
raise HTTPException(status_code=404, detail="Module not found")
return schemas.SuccessResponse(message="Module deleted successfully")
@router.put("/lessons/{lesson_id}", response_model=schemas.SuccessResponse)
def update_lesson(
lesson_id: int,
lesson_update: schemas.LessonUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update a lesson
"""
updated_lesson = crud.update_lesson(db, lesson_id, lesson_update)
if not updated_lesson:
raise HTTPException(status_code=404, detail="Lesson not found")
lesson_response = schemas.LessonResponse.from_orm(updated_lesson)
return schemas.SuccessResponse(
data={"lesson": lesson_response},
message="Lesson updated successfully"
)
@router.delete("/lessons/{lesson_id}", response_model=schemas.SuccessResponse)
def delete_lesson(
lesson_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Delete a lesson
"""
deleted = crud.delete_lesson(db, lesson_id)
if not deleted:
raise HTTPException(status_code=404, detail="Lesson not found")
return schemas.SuccessResponse(message="Lesson deleted successfully")
# Step endpoints
@router.post("/steps", response_model=schemas.SuccessResponse)
def create_step(
step: schemas.StepCreate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Create a new step for a lesson
"""
lesson = crud.get_lesson_by_id(db, step.lesson_id)
if not lesson:
raise HTTPException(status_code=404, detail="Lesson not found")
created_step = crud.create_step(db, step)
step_response = schemas.StepResponse.from_orm(created_step)
return schemas.SuccessResponse(
data={"step": step_response},
message="Step created successfully"
)
@router.put("/steps/{step_id}", response_model=schemas.SuccessResponse)
def update_step(
step_id: int,
step_update: schemas.StepUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update a step (full update including type, title, content, order)
"""
updated_step = crud.update_step(db, step_id, step_update)
if not updated_step:
raise HTTPException(status_code=404, detail="Step not found")
step_response = schemas.StepResponse.from_orm(updated_step)
return schemas.SuccessResponse(
data={"step": step_response},
message="Step updated successfully"
)
@router.delete("/steps/{step_id}", response_model=schemas.SuccessResponse)
def delete_step(
step_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Delete a step
"""
deleted = crud.delete_step(db, step_id)
if not deleted:
raise HTTPException(status_code=404, detail="Step not found")
return schemas.SuccessResponse(message="Step deleted successfully")
# Test endpoints
@router.post("/steps/{step_id}/test/questions", response_model=schemas.SuccessResponse)
def create_test_question(
step_id: int,
question: schemas.TestQuestionCreate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Create a new test question
"""
step = crud.get_step_by_id(db, step_id)
if not step:
raise HTTPException(status_code=404, detail="Step not found")
if step.step_type != 'test':
raise HTTPException(status_code=400, detail="Step is not a test")
created_question = crud.create_test_question(db, step_id, question)
question_response = schemas.TestQuestionResponse.from_orm(created_question)
return schemas.SuccessResponse(
data={"question": question_response},
message="Question created successfully"
)
@router.get("/steps/{step_id}/test/questions", response_model=schemas.SuccessResponse)
def get_test_questions(
step_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Get all questions for a test
"""
step = crud.get_step_by_id(db, step_id)
if not step:
raise HTTPException(status_code=404, detail="Step not found")
questions = crud.get_test_questions(db, step_id)
questions_response = [schemas.TestQuestionResponse.from_orm(q) for q in questions]
return schemas.SuccessResponse(
data={"questions": questions_response, "settings": step.test_settings}
)
@router.put("/steps/{step_id}/test/questions/{question_id}", response_model=schemas.SuccessResponse)
def update_test_question(
step_id: int,
question_id: int,
question_update: schemas.TestQuestionUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update a test question
"""
updated_question = crud.update_test_question(db, question_id, question_update)
if not updated_question:
raise HTTPException(status_code=404, detail="Question not found")
question_response = schemas.TestQuestionResponse.from_orm(updated_question)
return schemas.SuccessResponse(
data={"question": question_response},
message="Question updated successfully"
)
@router.delete("/steps/{step_id}/test/questions/{question_id}", response_model=schemas.SuccessResponse)
def delete_test_question(
step_id: int,
question_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Delete a test question
"""
deleted = crud.delete_test_question(db, question_id)
if not deleted:
raise HTTPException(status_code=404, detail="Question not found")
return schemas.SuccessResponse(message="Question deleted successfully")
@router.put("/steps/{step_id}/test/settings", response_model=schemas.SuccessResponse)
def update_test_settings(
step_id: int,
settings: schemas.TestSettings,
db: Session = Depends(get_db),
current_user: User = Depends(auth.get_current_user)
):
"""
Update test settings (passing score, max attempts, time limit, show answers)
"""
updated_step = crud.update_test_settings(db, step_id, settings)
if not updated_step:
raise HTTPException(status_code=404, detail="Step not found")
return schemas.SuccessResponse(
data={"settings": updated_step.test_settings},
message="Test settings updated successfully"
)