Старт
This commit is contained in:
10
backend/app/routes/__init__.py
Normal file
10
backend/app/routes/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from .auth import router as auth_router
|
||||
from .courses import router as courses_router
|
||||
from .stats import router as stats_router
|
||||
from .favorites import router as favorites_router
|
||||
from .progress import router as progress_router
|
||||
from .course_builder import router as course_builder_router
|
||||
from .admin import router as admin_router
|
||||
from .learn import router as learn_router
|
||||
|
||||
__all__ = ["auth_router", "courses_router", "stats_router", "favorites_router", "progress_router", "course_builder_router", "admin_router", "learn_router"]
|
||||
104
backend/app/routes/admin.py
Normal file
104
backend/app/routes/admin.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict
|
||||
from .. import crud, schemas
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
import os
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
# Проверка админ-секрета через переменную окружения
|
||||
def verify_admin_secret(admin_secret: str) -> bool:
|
||||
"""
|
||||
Проверка admin-секрета
|
||||
Разрешенные источники:
|
||||
1. DEVELOPER_SECRET из переменной окружения
|
||||
2. Можно добавить дополнительные проверки (IP, токен и т.д.)
|
||||
"""
|
||||
valid_secret = os.getenv("DEVELOPER_SECRET")
|
||||
if not valid_secret:
|
||||
raise HTTPException(status_code=500, detail="DEVELOPER_SECRET not configured")
|
||||
|
||||
return admin_secret == valid_secret
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/make-developer")
|
||||
def make_user_developer(
|
||||
user_id: int,
|
||||
admin_secret: str = Body(..., embed=True),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Назначить статус разработчика для пользователя
|
||||
Требует admin_secret в запросе
|
||||
"""
|
||||
if not verify_admin_secret(admin_secret):
|
||||
raise HTTPException(status_code=403, detail="Invalid admin secret")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user.is_developer = True
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return {
|
||||
"message": f"Developer status enabled for user {user.email}",
|
||||
"user_id": user.id,
|
||||
"email": user.email,
|
||||
"is_developer": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/remove-developer")
|
||||
def remove_user_developer(
|
||||
user_id: int,
|
||||
admin_secret: str = Body(..., embed=True),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Убрать статус разработчика у пользователя
|
||||
Требует admin_secret в запросе
|
||||
"""
|
||||
if not verify_admin_secret(admin_secret):
|
||||
raise HTTPException(status_code=403, detail="Invalid admin secret")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user.is_developer = False
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return {
|
||||
"message": f"Developer status removed for user {user.email}",
|
||||
"user_id": user.id,
|
||||
"email": user.email,
|
||||
"is_developer": False
|
||||
}
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
def list_users(
|
||||
admin_secret: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
if not verify_admin_secret(admin_secret):
|
||||
raise HTTPException(status_code=403, detail="Invalid admin secret")
|
||||
|
||||
users = db.query(User).all()
|
||||
return {
|
||||
"users": [
|
||||
{
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"is_developer": user.is_developer,
|
||||
"is_active": user.is_active,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None
|
||||
}
|
||||
for user in users
|
||||
]
|
||||
}
|
||||
105
backend/app/routes/auth.py
Normal file
105
backend/app/routes/auth.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict
|
||||
from .. import crud, schemas, auth
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
@router.post("/register", response_model=schemas.SuccessResponse)
|
||||
def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
||||
# Check if user already exists
|
||||
db_user = crud.get_user_by_email(db, email=user.email)
|
||||
if db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already registered"
|
||||
)
|
||||
|
||||
# Create user
|
||||
crud.create_user(db=db, user=user)
|
||||
|
||||
return schemas.SuccessResponse(
|
||||
message="User registered successfully",
|
||||
data={"email": user.email}
|
||||
)
|
||||
|
||||
@router.post("/login", response_model=schemas.SuccessResponse)
|
||||
def login(credentials: schemas.UserLogin, db: Session = Depends(get_db)):
|
||||
# Authenticate user
|
||||
user = crud.authenticate_user(db, credentials.email, credentials.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password"
|
||||
)
|
||||
|
||||
# Create tokens
|
||||
access_token, refresh_token = auth.create_tokens(user.email)
|
||||
|
||||
return schemas.SuccessResponse(
|
||||
message="Login successful",
|
||||
data={
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"email": user.email,
|
||||
"id": user.id,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@router.post("/logout", response_model=schemas.SuccessResponse)
|
||||
def logout():
|
||||
# In a real app, you might want to blacklist the token
|
||||
return schemas.SuccessResponse(message="Logout successful")
|
||||
|
||||
@router.get("/me", response_model=schemas.SuccessResponse)
|
||||
def get_current_user(
|
||||
current_user: User = Depends(auth.get_current_user)
|
||||
):
|
||||
return schemas.SuccessResponse(
|
||||
data={
|
||||
"email": current_user.email,
|
||||
"id": current_user.id,
|
||||
"created_at": current_user.created_at.isoformat() if current_user.created_at else None,
|
||||
"is_active": current_user.is_active,
|
||||
"is_developer": current_user.is_developer
|
||||
}
|
||||
)
|
||||
|
||||
@router.put("/me/developer-status", response_model=schemas.SuccessResponse)
|
||||
def update_developer_status(
|
||||
is_developer: bool = Body(..., embed=True),
|
||||
current_user: User = Depends(auth.get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Update developer status for current user
|
||||
"""
|
||||
current_user.is_developer = is_developer
|
||||
db.commit()
|
||||
|
||||
return schemas.SuccessResponse(
|
||||
message=f"Developer status updated to: {is_developer}",
|
||||
data={"is_developer": is_developer}
|
||||
)
|
||||
|
||||
@router.post("/refresh", response_model=schemas.Token)
|
||||
def refresh_token(
|
||||
current_user: User = Depends(auth.get_current_user)
|
||||
):
|
||||
"""
|
||||
Refresh access token using refresh token
|
||||
"""
|
||||
# Create new access token for the current user
|
||||
access_token, refresh_token = auth.create_tokens(current_user.email)
|
||||
|
||||
return schemas.Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer"
|
||||
)
|
||||
646
backend/app/routes/course_builder.py
Normal file
646
backend/app/routes/course_builder.py
Normal file
@@ -0,0 +1,646 @@
|
||||
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"
|
||||
)
|
||||
141
backend/app/routes/courses.py
Normal file
141
backend/app/routes/courses.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from .. import crud, schemas, auth
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
|
||||
router = APIRouter(prefix="/courses", tags=["courses"])
|
||||
|
||||
@router.get("/", response_model=schemas.SuccessResponse)
|
||||
def get_courses(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=100),
|
||||
level: Optional[str] = Query(None, pattern="^(beginner|intermediate|advanced)$"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# Get published courses for all users (including guests)
|
||||
courses = crud.get_courses(db, skip=skip, limit=limit, level=level, published_only=True)
|
||||
|
||||
# Convert SQLAlchemy models to Pydantic models
|
||||
course_responses = [schemas.CourseResponse.from_orm(course) for course in courses]
|
||||
|
||||
return schemas.SuccessResponse(
|
||||
data={
|
||||
"courses": course_responses,
|
||||
"total": len(courses),
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"level": level
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/{course_id}", response_model=schemas.SuccessResponse)
|
||||
def get_course(
|
||||
course_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(auth.get_optional_current_user)
|
||||
):
|
||||
course = crud.get_course_by_id(db, course_id=course_id)
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found"
|
||||
)
|
||||
|
||||
# Check access based on status
|
||||
course_status = getattr(course, 'status', 'draft')
|
||||
author_id = getattr(course, 'author_id', None)
|
||||
|
||||
if course_status == 'published':
|
||||
pass # Anyone can access published course
|
||||
elif course_status == 'draft':
|
||||
# Only author can access drafts
|
||||
if current_user is None or author_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not authorized to access this draft"
|
||||
)
|
||||
else:
|
||||
# Archived - author-only access
|
||||
if current_user is None or author_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not authorized to access this course"
|
||||
)
|
||||
|
||||
# Convert SQLAlchemy model to Pydantic model
|
||||
course_response = schemas.CourseResponse.from_orm(course)
|
||||
|
||||
return schemas.SuccessResponse(data={"course": course_response})
|
||||
|
||||
@router.put("/{course_id}", response_model=schemas.SuccessResponse)
|
||||
def update_course(
|
||||
course_id: int,
|
||||
course_update: schemas.CourseUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(auth.get_current_user)
|
||||
):
|
||||
course = crud.get_course_by_id(db, course_id)
|
||||
if not course:
|
||||
raise HTTPException(status_code=404, detail="Course not found")
|
||||
|
||||
if course.author_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized to update this course")
|
||||
|
||||
updated_course = crud.update_course(db, course_id, course_update)
|
||||
course_response = schemas.CourseResponse.from_orm(updated_course)
|
||||
|
||||
return schemas.SuccessResponse(data={"course": course_response})
|
||||
|
||||
@router.get("/{course_id}/structure", response_model=schemas.SuccessResponse)
|
||||
def get_course_structure(
|
||||
course_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(auth.get_optional_current_user)
|
||||
):
|
||||
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")
|
||||
|
||||
if course.status != 'published':
|
||||
if current_user is None or course.author_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
course_data = {
|
||||
"id": course.id,
|
||||
"title": course.title,
|
||||
"description": course.description,
|
||||
"level": course.level,
|
||||
"duration": course.duration,
|
||||
"status": course.status,
|
||||
"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,
|
||||
"order_index": step.order_index
|
||||
} for step in sorted(lesson.steps, key=lambda s: s.order_index)
|
||||
] if lesson.steps else []
|
||||
}
|
||||
for lesson in sorted(module.lessons, key=lambda l: l.order_index)
|
||||
] if module.lessons else []
|
||||
}
|
||||
for module in sorted(course.modules, key=lambda m: m.order_index)
|
||||
] if course.modules else []
|
||||
}
|
||||
|
||||
return schemas.SuccessResponse(data={"course": course_data})
|
||||
58
backend/app/routes/favorites.py
Normal file
58
backend/app/routes/favorites.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from .. import crud, schemas, auth
|
||||
from ..database import get_db
|
||||
from ..auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/favorites", tags=["favorites"])
|
||||
|
||||
@router.get("/", response_model=List[schemas.FavoriteResponse])
|
||||
def get_my_favorites(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
favorites = crud.get_favorite_courses(db, user_id=current_user.id)
|
||||
# Convert SQLAlchemy models to Pydantic models
|
||||
return [schemas.FavoriteResponse.model_validate(favorite) for favorite in favorites]
|
||||
|
||||
@router.post("/", response_model=schemas.FavoriteResponse)
|
||||
def add_to_favorites(
|
||||
favorite: schemas.FavoriteCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
# Check if course exists
|
||||
course = crud.get_course_by_id(db, favorite.course_id)
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Course not found"
|
||||
)
|
||||
|
||||
# Add to favorites
|
||||
db_favorite = crud.add_favorite_course(db, user_id=current_user.id, course_id=favorite.course_id)
|
||||
return schemas.FavoriteResponse.model_validate(db_favorite)
|
||||
|
||||
@router.delete("/{course_id}", response_model=schemas.SuccessResponse)
|
||||
def remove_from_favorites(
|
||||
course_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
# Check if course exists
|
||||
course = crud.get_course_by_id(db, course_id)
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Course not found"
|
||||
)
|
||||
|
||||
success = crud.remove_favorite_course(db, user_id=current_user.id, course_id=course_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Course not in favorites"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Course removed from favorites"}
|
||||
179
backend/app/routes/learn.py
Normal file
179
backend/app/routes/learn.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from .. import crud, schemas, auth
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
|
||||
router = APIRouter(prefix="/learn", tags=["learning"])
|
||||
|
||||
@router.get("/steps/{step_id}", response_model=schemas.SuccessResponse)
|
||||
def get_step_content(
|
||||
step_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(auth.get_optional_current_user)
|
||||
):
|
||||
step = crud.get_step_by_id(db, step_id=step_id)
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail="Step not found")
|
||||
|
||||
lesson = crud.get_lesson_by_id(db, step.lesson_id)
|
||||
if not lesson:
|
||||
raise HTTPException(status_code=404, detail="Lesson not found")
|
||||
|
||||
module = crud.get_module_by_id(db, lesson.module_id)
|
||||
if not module:
|
||||
raise HTTPException(status_code=404, detail="Module not found")
|
||||
|
||||
from ..models import Course
|
||||
course = db.query(Course).filter(Course.id == module.course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(status_code=404, detail="Course not found")
|
||||
|
||||
if course.status != 'published':
|
||||
if current_user is None or course.author_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
return schemas.SuccessResponse(data={
|
||||
"step": {
|
||||
"id": step.id,
|
||||
"step_type": step.step_type,
|
||||
"title": step.title,
|
||||
"content": step.content,
|
||||
"content_html": step.content_html,
|
||||
"file_url": step.file_url,
|
||||
"test_settings": step.test_settings,
|
||||
"order_index": step.order_index
|
||||
}
|
||||
})
|
||||
|
||||
@router.get("/steps/{step_id}/test", response_model=schemas.SuccessResponse)
|
||||
def get_test_for_viewing(
|
||||
step_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(auth.get_current_user)
|
||||
):
|
||||
"""
|
||||
Get test questions for viewing (without correct answers)
|
||||
"""
|
||||
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")
|
||||
|
||||
questions = crud.get_test_questions(db, step_id)
|
||||
|
||||
# Prepare questions without correct answers
|
||||
questions_for_viewer = []
|
||||
for q in questions:
|
||||
question_data = {
|
||||
"id": q.id,
|
||||
"question_text": q.question_text,
|
||||
"question_type": q.question_type,
|
||||
"points": q.points,
|
||||
"order_index": q.order_index
|
||||
}
|
||||
|
||||
if q.question_type in ['single_choice', 'multiple_choice']:
|
||||
# Return answers without is_correct flag
|
||||
question_data["answers"] = [
|
||||
{"id": a.id, "answer_text": a.answer_text, "order_index": a.order_index}
|
||||
for a in q.answers
|
||||
]
|
||||
elif q.question_type == 'match':
|
||||
# Return match items shuffled
|
||||
question_data["match_items"] = [
|
||||
{"id": m.id, "left_text": m.left_text, "right_text": m.right_text, "order_index": m.order_index}
|
||||
for m in q.match_items
|
||||
]
|
||||
|
||||
questions_for_viewer.append(question_data)
|
||||
|
||||
# Get user's previous attempts
|
||||
attempts = crud.get_test_attempts(db, current_user.id, step_id)
|
||||
attempts_data = [
|
||||
{
|
||||
"id": a.id,
|
||||
"score": float(a.score),
|
||||
"max_score": float(a.max_score),
|
||||
"passed": a.passed,
|
||||
"completed_at": a.completed_at.isoformat() if a.completed_at else None
|
||||
}
|
||||
for a in attempts
|
||||
]
|
||||
|
||||
return schemas.SuccessResponse(data={
|
||||
"questions": questions_for_viewer,
|
||||
"settings": step.test_settings,
|
||||
"attempts": attempts_data
|
||||
})
|
||||
|
||||
@router.post("/steps/{step_id}/test/submit", response_model=schemas.SuccessResponse)
|
||||
def submit_test(
|
||||
step_id: int,
|
||||
attempt: schemas.TestAttemptCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(auth.get_current_user)
|
||||
):
|
||||
"""
|
||||
Submit test answers and get results
|
||||
"""
|
||||
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")
|
||||
|
||||
# Check max attempts limit
|
||||
settings = step.test_settings or {}
|
||||
max_attempts = settings.get('max_attempts', 0)
|
||||
|
||||
if max_attempts > 0:
|
||||
attempts = crud.get_test_attempts(db, current_user.id, step_id)
|
||||
if len(attempts) >= max_attempts:
|
||||
raise HTTPException(status_code=400, detail="Maximum attempts reached")
|
||||
|
||||
# Create attempt and calculate score
|
||||
test_attempt = crud.create_test_attempt(db, current_user.id, step_id, attempt.answers)
|
||||
if not test_attempt:
|
||||
raise HTTPException(status_code=500, detail="Failed to create test attempt")
|
||||
|
||||
return schemas.SuccessResponse(data={
|
||||
"attempt": {
|
||||
"id": test_attempt.id,
|
||||
"score": float(test_attempt.score),
|
||||
"max_score": float(test_attempt.max_score),
|
||||
"passed": test_attempt.passed,
|
||||
"completed_at": test_attempt.completed_at.isoformat() if test_attempt.completed_at else None
|
||||
}
|
||||
})
|
||||
|
||||
@router.get("/steps/{step_id}/test/attempts", response_model=schemas.SuccessResponse)
|
||||
def get_test_attempts(
|
||||
step_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(auth.get_current_user)
|
||||
):
|
||||
"""
|
||||
Get user's test attempts history
|
||||
"""
|
||||
step = crud.get_step_by_id(db, step_id)
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail="Step not found")
|
||||
|
||||
attempts = crud.get_test_attempts(db, current_user.id, step_id)
|
||||
attempts_data = [
|
||||
{
|
||||
"id": a.id,
|
||||
"score": float(a.score),
|
||||
"max_score": float(a.max_score),
|
||||
"passed": a.passed,
|
||||
"completed_at": a.completed_at.isoformat() if a.completed_at else None
|
||||
}
|
||||
for a in attempts
|
||||
]
|
||||
|
||||
return schemas.SuccessResponse(data={"attempts": attempts_data})
|
||||
106
backend/app/routes/progress.py
Normal file
106
backend/app/routes/progress.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from .. import crud, schemas, auth
|
||||
from ..database import get_db
|
||||
from ..auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/progress", tags=["progress"])
|
||||
|
||||
@router.get("/", response_model=List[schemas.ProgressResponse])
|
||||
def get_my_progress(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
progress_list = crud.get_user_progress(db, user_id=current_user.id)
|
||||
# Calculate percentage for each progress
|
||||
result = []
|
||||
for progress in progress_list:
|
||||
percentage = (progress.completed_lessons / progress.total_lessons * 100) if progress.total_lessons > 0 else 0
|
||||
result.append({
|
||||
"user_id": progress.user_id,
|
||||
"course_id": progress.course_id,
|
||||
"completed_lessons": progress.completed_lessons,
|
||||
"total_lessons": progress.total_lessons,
|
||||
"percentage": percentage,
|
||||
"updated_at": progress.updated_at
|
||||
})
|
||||
return result
|
||||
|
||||
@router.get("/{course_id}", response_model=schemas.ProgressResponse)
|
||||
def get_course_progress(
|
||||
course_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
progress = crud.get_progress(db, user_id=current_user.id, course_id=course_id)
|
||||
if not progress:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Progress not found for this course"
|
||||
)
|
||||
|
||||
percentage = (progress.completed_lessons / progress.total_lessons * 100) if progress.total_lessons > 0 else 0
|
||||
return {
|
||||
"user_id": progress.user_id,
|
||||
"course_id": progress.course_id,
|
||||
"completed_lessons": progress.completed_lessons,
|
||||
"total_lessons": progress.total_lessons,
|
||||
"percentage": percentage,
|
||||
"updated_at": progress.updated_at
|
||||
}
|
||||
|
||||
@router.post("/{course_id}", response_model=schemas.ProgressResponse)
|
||||
def update_progress(
|
||||
course_id: int,
|
||||
progress_update: schemas.ProgressUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
# Check if course exists
|
||||
course = crud.get_course_by_id(db, course_id)
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Course not found"
|
||||
)
|
||||
|
||||
# Validate completed_lessons <= total_lessons
|
||||
if progress_update.completed_lessons > progress_update.total_lessons:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Completed lessons cannot exceed total lessons"
|
||||
)
|
||||
|
||||
progress = crud.create_or_update_progress(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
course_id=course_id,
|
||||
completed_lessons=progress_update.completed_lessons,
|
||||
total_lessons=progress_update.total_lessons
|
||||
)
|
||||
|
||||
percentage = (progress.completed_lessons / progress.total_lessons * 100) if progress.total_lessons > 0 else 0
|
||||
return {
|
||||
"user_id": progress.user_id,
|
||||
"course_id": progress.course_id,
|
||||
"completed_lessons": progress.completed_lessons,
|
||||
"total_lessons": progress.total_lessons,
|
||||
"percentage": percentage,
|
||||
"updated_at": progress.updated_at
|
||||
}
|
||||
|
||||
@router.delete("/{course_id}", response_model=schemas.SuccessResponse)
|
||||
def delete_progress(
|
||||
course_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: schemas.UserResponse = Depends(get_current_user)
|
||||
):
|
||||
success = crud.delete_progress(db, user_id=current_user.id, course_id=course_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Progress not found"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "Progress deleted"}
|
||||
12
backend/app/routes/stats.py
Normal file
12
backend/app/routes/stats.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import crud, schemas
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter(prefix="/stats", tags=["stats"])
|
||||
|
||||
@router.get("/", response_model=schemas.SuccessResponse)
|
||||
def get_stats(db: Session = Depends(get_db)):
|
||||
stats = crud.get_stats(db)
|
||||
|
||||
return schemas.SuccessResponse(data=stats)
|
||||
Reference in New Issue
Block a user