646 lines
21 KiB
Python
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"
|
|
) |