110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""FastAPI-приложение LMS «Управление ИТ (ОМС)»."""
|
||
import logging
|
||
from contextlib import asynccontextmanager
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
from fastapi import FastAPI, Depends, HTTPException
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.config import settings
|
||
from app.database import get_db, init_db
|
||
from app.models import Participant
|
||
from app.schemas import (
|
||
StartRequest,
|
||
StartResponse,
|
||
CompleteRequest,
|
||
CompleteResponse,
|
||
)
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
"""Инициализация при старте, создание таблиц."""
|
||
init_db()
|
||
yield
|
||
# shutdown при необходимости
|
||
|
||
|
||
app = FastAPI(
|
||
title="LMS Управление ИТ (ОМС)",
|
||
description="API для фиксации прохождения обучения и результатов тестирования",
|
||
lifespan=lifespan,
|
||
)
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Статика: курс (HTML) — из CONTENT_PATH в Docker или content/ рядом с backend
|
||
static_path = Path(settings.content_path) if settings.content_path else (Path(__file__).resolve().parent.parent.parent / "content")
|
||
if static_path.exists():
|
||
app.mount("/content", StaticFiles(directory=str(static_path), html=True), name="content")
|
||
|
||
|
||
@app.get("/")
|
||
async def root():
|
||
"""Редирект на страницу курса."""
|
||
from fastapi.responses import RedirectResponse
|
||
return RedirectResponse(url="/content/it_course_v2.html", status_code=302)
|
||
|
||
|
||
@app.get("/api/health")
|
||
async def health():
|
||
"""Проверка доступности API."""
|
||
return {"status": "ok"}
|
||
|
||
|
||
@app.post("/api/start", response_model=StartResponse)
|
||
def api_start(body: StartRequest, db: Session = Depends(get_db)):
|
||
"""
|
||
Регистрация начала обучения.
|
||
Принимает ФИО, создаёт запись в БД, возвращает participant_id для последующей отправки результата.
|
||
"""
|
||
fio = body.fio.strip()
|
||
if not fio:
|
||
raise HTTPException(status_code=400, detail="ФИО не может быть пустым")
|
||
participant = Participant(fio=fio)
|
||
db.add(participant)
|
||
db.commit()
|
||
db.refresh(participant)
|
||
logger.info("Обучение начато: participant_id=%s, fio=%s", participant.id, participant.fio)
|
||
return StartResponse(participant_id=participant.id, fio=participant.fio)
|
||
|
||
|
||
@app.post("/api/complete", response_model=CompleteResponse)
|
||
def api_complete(body: CompleteRequest, db: Session = Depends(get_db)):
|
||
"""
|
||
Фиксация результата тестирования.
|
||
Обновляет запись участника по participant_id: score, percent, passed, completed_at.
|
||
"""
|
||
participant = db.query(Participant).filter(Participant.id == body.participant_id).first()
|
||
if not participant:
|
||
raise HTTPException(status_code=404, detail="Участник не найден")
|
||
participant.completed_at = datetime.now(timezone.utc)
|
||
participant.score = body.score
|
||
participant.total_questions = body.total_questions
|
||
participant.percent = body.percent
|
||
participant.passed = body.passed
|
||
db.commit()
|
||
logger.info(
|
||
"Тест завершён: participant_id=%s, score=%s/%s, percent=%s, passed=%s",
|
||
participant.id,
|
||
body.score,
|
||
body.total_questions,
|
||
body.percent,
|
||
body.passed,
|
||
)
|
||
return CompleteResponse(participant_id=participant.id, saved=True)
|