Feature: серверная часть, ФИО и результаты теста в БД; правка ответа эскалация 80%
Made-with: Cursor
This commit is contained in:
18
.env.example
18
.env.example
@@ -1,12 +1,12 @@
|
||||
# Управление ИТ (ОМС) — система дистанционного обучения
|
||||
# Скопировать в .env и при необходимости изменить значения
|
||||
# Скопировать в .env и задать пароль БД
|
||||
|
||||
# База данных (будет использоваться серверной частью)
|
||||
# POSTGRES_HOST=db
|
||||
# POSTGRES_PORT=5432
|
||||
# POSTGRES_DB=lms_it_oms
|
||||
# POSTGRES_USER=lms
|
||||
# POSTGRES_PASSWORD=задать_пароль
|
||||
# База данных
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=lms_it_oms
|
||||
POSTGRES_USER=lms
|
||||
POSTGRES_PASSWORD=задать_надёжный_пароль
|
||||
|
||||
# Приложение
|
||||
# SECRET_KEY=задать_секретный_ключ
|
||||
# Путь к контенту в контейнере (не менять при стандартном запуске)
|
||||
# CONTENT_PATH=/content
|
||||
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
venv/
|
||||
21
HISTORY.md
21
HISTORY.md
@@ -1,5 +1,26 @@
|
||||
# История изменений — Управление ИТ (ОМС)
|
||||
|
||||
## 2025-03-16 14:00 UTC – Серверная часть и связка с фронтом
|
||||
|
||||
**Проблема:** нужна фиксация ФИО при старте обучения и сохранение результатов тестирования в БД.
|
||||
|
||||
**Решение:** реализован backend на FastAPI, PostgreSQL, экран ввода ФИО и вызовы API из курса.
|
||||
|
||||
**Изменения:**
|
||||
- **Тест:** исправлен правильный ответ на вопрос про эскалацию (80% вместо 100%).
|
||||
- **Backend:** FastAPI, SQLAlchemy, модель `Participant` (id, fio, started_at, completed_at, score, total_questions, percent, passed). API: `POST /api/start`, `POST /api/complete`, `GET /api/health`. Статика курса из каталога `content/`.
|
||||
- **Фронт:** экран приветствия с формой ФИО; после ввода — запрос `/api/start`, сохранение `participant_id` в sessionStorage, отображение «Вы вошли как: ФИО»; после прохождения теста — отправка результатов через `/api/complete`.
|
||||
- **Docker:** образ backend (Python 3.12), PostgreSQL 15, volume для БД, монтирование `content`. Volume добавлен в `include-volumes.txt`.
|
||||
- Обновлены README.md, .env.example, HISTORY.md.
|
||||
|
||||
**Проверка:**
|
||||
```bash
|
||||
cd /opt/lms-it-oms && docker compose up -d
|
||||
# Открыть http://localhost:8000/ — ввод ФИО, прохождение курса и теста, результат сохраняется в БД.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2025-03-16 12:00 UTC – Создание проекта
|
||||
|
||||
**Проблема:** нужна система дистанционного обучения «Управление ИТ (ОМС)» с HTML-контентом и последующей серверной частью для учёта сотрудников и результатов тестов.
|
||||
|
||||
70
README.md
70
README.md
@@ -5,22 +5,46 @@
|
||||
## Назначение
|
||||
|
||||
- Раздача контента обучения и тестирования (HTML).
|
||||
- Фиксация ФИО сотрудника при старте обучения и при прохождении теста.
|
||||
- Сохранение результатов тестирования в базе данных.
|
||||
- Фиксация ФИО сотрудника при старте обучения (экран входа).
|
||||
- Сохранение результатов тестирования в PostgreSQL после прохождения теста.
|
||||
|
||||
## Требования и зависимости
|
||||
|
||||
- Docker, Docker Compose
|
||||
- (Планируется) PostgreSQL для хранения записей о прохождении и результатах тестов
|
||||
- PostgreSQL 15 (запускается в контейнере)
|
||||
|
||||
## Установка и настройка
|
||||
|
||||
1. Скопировать `.env.example` в `.env` и при необходимости задать переменные.
|
||||
2. Запуск: `docker compose up -d` (после добавления серверной части).
|
||||
1. Скопировать `.env.example` в `.env` и задать пароль БД:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Отредактировать .env: POSTGRES_PASSWORD=...
|
||||
```
|
||||
2. Запуск:
|
||||
```bash
|
||||
cd /opt/lms-it-oms
|
||||
docker compose up -d
|
||||
```
|
||||
3. Курс доступен по адресу: **http://localhost:8000/** (редирект на `/content/it_course_v2.html`).
|
||||
|
||||
## Конфигурация
|
||||
|
||||
См. `.env.example`. Переменные задаются в `.env` (файл не коммитится).
|
||||
Переменные в `.env` (файл не коммитится):
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| POSTGRES_HOST | Хост БД | db |
|
||||
| POSTGRES_PORT | Порт БД | 5432 |
|
||||
| POSTGRES_DB | Имя БД | lms_it_oms |
|
||||
| POSTGRES_USER | Пользователь БД | lms |
|
||||
| POSTGRES_PASSWORD | Пароль БД | **обязательно задать** |
|
||||
| CONTENT_PATH | Путь к каталогу content в контейнере | /content |
|
||||
|
||||
## API
|
||||
|
||||
- `POST /api/start` — регистрация начала обучения. Тело: `{"fio": "ФИО"}`. Ответ: `{"participant_id": "...", "fio": "..."}`.
|
||||
- `POST /api/complete` — фиксация результата теста. Тело: `{"participant_id": "...", "score": 20, "total_questions": 25, "percent": 80, "passed": true}`.
|
||||
- `GET /api/health` — проверка доступности API.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
@@ -30,27 +54,37 @@
|
||||
├── HISTORY.md
|
||||
├── .env.example
|
||||
├── docker-compose.yml
|
||||
├── content/ # HTML-файлы обучения и тестирования (положить сюда для анализа)
|
||||
└── (backend/ и static/ — будут добавлены при реализации серверной части)
|
||||
├── content/ # HTML курса
|
||||
│ └── it_course_v2.html
|
||||
└── backend/
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
└── app/
|
||||
├── main.py # FastAPI, статика, API
|
||||
├── config.py
|
||||
├── database.py
|
||||
├── models.py
|
||||
└── schemas.py
|
||||
```
|
||||
|
||||
### Куда положить HTML для анализа
|
||||
## Docker Compose
|
||||
|
||||
**Положите ваши HTML-файлы обучения и тестирования в каталог:**
|
||||
|
||||
```
|
||||
/opt/lms-it-oms/content/
|
||||
```
|
||||
|
||||
После размещения файлов можно запросить анализ и корректировку контента, а затем — добавление серверной части для учёта ФИО и результатов тестов.
|
||||
- **app** — FastAPI на порту 8000, раздаёт статику из `./content` и обрабатывает `/api/*`.
|
||||
- **db** — PostgreSQL 15, volume `lms_it_oms_data` для данных.
|
||||
|
||||
## Логирование
|
||||
|
||||
(Будет описано после внедрения серверной части.)
|
||||
- Логи приложения: `docker compose logs -f app`
|
||||
- Логи БД: `docker compose logs -f db`
|
||||
- Ротация логов app: json-file, max-size 10m, max-file 3.
|
||||
|
||||
## Резервное копирование
|
||||
|
||||
Volumes проекта будут добавлены в `/opt/gen7x/backup/include-volumes.txt`.
|
||||
Volume `lms-it-oms_lms_it_oms_data` добавлен в `/opt/gen7x/backup/include-volumes.txt`. Бэкапить данные PostgreSQL по общему регламенту платформы.
|
||||
|
||||
## Восстановление из бэкапа
|
||||
|
||||
Восстановить volume БД по инструкции платформы. После восстановления перезапустить: `docker compose up -d`.
|
||||
|
||||
## История изменений
|
||||
|
||||
|
||||
17
backend/Dockerfile
Normal file
17
backend/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app/ ./app/
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# LMS IT OMS — backend
|
||||
27
backend/app/config.py
Normal file
27
backend/app/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Конфигурация приложения."""
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Настройки из переменных окружения."""
|
||||
|
||||
postgres_host: str = "db"
|
||||
postgres_port: int = 5432
|
||||
postgres_db: str = "lms_it_oms"
|
||||
postgres_user: str = "lms"
|
||||
postgres_password: str = ""
|
||||
content_path: str = "" # Путь к каталогу content (в Docker: /content)
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
return (
|
||||
f"postgresql://{self.postgres_user}:{self.postgres_password}"
|
||||
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
|
||||
)
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_prefix = ""
|
||||
|
||||
|
||||
settings = Settings()
|
||||
33
backend/app/database.py
Normal file
33
backend/app/database.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Подключение к БД и сессия."""
|
||||
import logging
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
engine = create_engine(
|
||||
settings.database_url,
|
||||
pool_pre_ping=True,
|
||||
echo=False,
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Генератор сессии БД для FastAPI."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Создание таблиц при старте."""
|
||||
from app import models # noqa: F401
|
||||
Base.metadata.create_all(bind=engine)
|
||||
logger.info("Таблицы БД созданы или уже существуют")
|
||||
109
backend/app/main.py
Normal file
109
backend/app/main.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""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)
|
||||
30
backend/app/models.py
Normal file
30
backend/app/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Модели БД."""
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Column, String, Integer, Boolean, DateTime, Numeric
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
def gen_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def utc_now():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class Participant(Base):
|
||||
"""Участник обучения: ФИО, старт, завершение теста, результат."""
|
||||
|
||||
__tablename__ = "participants"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=gen_uuid)
|
||||
fio = Column(String(500), nullable=False, index=True)
|
||||
started_at = Column(DateTime(timezone=True), nullable=False, default=utc_now)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
score = Column(Integer, nullable=True)
|
||||
total_questions = Column(Integer, nullable=True)
|
||||
percent = Column(Numeric(5, 2), nullable=True)
|
||||
passed = Column(Boolean, nullable=True)
|
||||
32
backend/app/schemas.py
Normal file
32
backend/app/schemas.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Pydantic-схемы для API."""
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class StartRequest(BaseModel):
|
||||
"""Запрос на начало обучения."""
|
||||
|
||||
fio: str = Field(..., min_length=1, max_length=500, description="ФИО сотрудника")
|
||||
|
||||
|
||||
class StartResponse(BaseModel):
|
||||
"""Ответ после регистрации начала обучения."""
|
||||
|
||||
participant_id: str
|
||||
fio: str
|
||||
|
||||
|
||||
class CompleteRequest(BaseModel):
|
||||
"""Запрос на фиксацию результата теста."""
|
||||
|
||||
participant_id: str = Field(..., min_length=1)
|
||||
score: int = Field(..., ge=0)
|
||||
total_questions: int = Field(..., gt=0)
|
||||
percent: float = Field(..., ge=0, le=100)
|
||||
passed: bool
|
||||
|
||||
|
||||
class CompleteResponse(BaseModel):
|
||||
"""Ответ после сохранения результата."""
|
||||
|
||||
participant_id: str
|
||||
saved: bool = True
|
||||
5
backend/requirements.txt
Normal file
5
backend/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.115.5
|
||||
uvicorn[standard]==0.32.1
|
||||
sqlalchemy>=2.0.0
|
||||
psycopg2-binary>=2.9.9
|
||||
pydantic-settings>=2.0.0
|
||||
1586
content/it_course_v2.html
Normal file
1586
content/it_course_v2.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,46 @@
|
||||
# Управление ИТ (ОМС) — система дистанционного обучения
|
||||
# Серверная часть и БД будут добавлены после размещения HTML-контента
|
||||
|
||||
version: "3.9"
|
||||
# Сервисы: app (FastAPI + статика), db (PostgreSQL)
|
||||
|
||||
services:
|
||||
# Заглушка: после добавления backend и БД здесь появятся app и db
|
||||
# app:
|
||||
# build: .
|
||||
# ports:
|
||||
# - "8000:8000"
|
||||
# env_file: .env
|
||||
# depends_on:
|
||||
# - db
|
||||
# db:
|
||||
# image: postgres:15-alpine
|
||||
# environment:
|
||||
# POSTGRES_DB: ${POSTGRES_DB}
|
||||
# POSTGRES_USER: ${POSTGRES_USER}
|
||||
# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
# volumes:
|
||||
# - lms_it_oms_data:/var/lib/postgresql/data
|
||||
app:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: ${POSTGRES_DB:-lms_it_oms}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-lms}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
CONTENT_PATH: /content
|
||||
volumes:
|
||||
- ./content:/content:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# volumes:
|
||||
# lms_it_oms_data:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-lms_it_oms}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-lms}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- lms_it_oms_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-lms} -d ${POSTGRES_DB:-lms_it_oms}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
lms_it_oms_data:
|
||||
|
||||
Reference in New Issue
Block a user