блоки 2 и 3 доработки интерфейса системы тестирования

This commit is contained in:
Константин Лебединский
2026-04-29 21:06:17 +05:00
parent eff3fda5b0
commit bba96f8f9f
37 changed files with 4440 additions and 1292 deletions
+130 -5
View File
@@ -1,18 +1,117 @@
"""Главный blueprint — посадочная страница и health-чек.
В E1.0 здесь нет бизнес-логики; страницы и эндпоинты добавляются в следующих
спринтах (E1.1 — auth, E1.2 — тесты, и т.д.).
"""
"""Главный blueprint — посадочная страница, статистика и health-чек."""
from __future__ import annotations
from flask import Blueprint, jsonify, render_template
from .. import db as app_db
from ..auth.decorators import login_required
from ..auth.decorators import current_user as get_current_user
main_bp = Blueprint('main', __name__)
def _get_stats() -> dict:
"""Собирает статистику прохождений для дашборда."""
from sqlalchemy import func, case
from ..db import get_session
from ..models import Department, User, Test, TestAttempt
session = get_session()
try:
total_tests = session.query(func.count(Test.id)).scalar() or 0
active_tests = session.query(func.count(Test.id)).filter(Test.is_active.is_(True)).scalar() or 0
total_completed = (
session.query(func.count(TestAttempt.id))
.filter(TestAttempt.status == 'completed')
.scalar() or 0
)
total_passed = (
session.query(func.count(TestAttempt.id))
.filter(TestAttempt.status == 'completed', TestAttempt.passed.is_(True))
.scalar() or 0
)
total_users = session.query(func.count(User.id)).filter(User.is_active.is_(True)).scalar() or 0
# Статистика по отделам
dept_rows = (
session.query(
Department.name,
func.count(TestAttempt.id).label('total'),
func.sum(
case((TestAttempt.passed.is_(True), 1), else_=0)
).label('passed_count'),
)
.join(User, User.department_id == Department.id)
.join(TestAttempt, TestAttempt.user_id == User.id)
.filter(TestAttempt.status == 'completed')
.group_by(Department.id, Department.name)
.order_by(Department.name)
.all()
)
from ..models import TestVersion
recent_rows = (
session.query(
TestAttempt.started_at,
TestAttempt.completed_at,
TestAttempt.passed,
TestAttempt.correct_count,
TestAttempt.total_questions,
User.full_name,
User.login,
Test.title.label('test_title'),
)
.join(User, User.id == TestAttempt.user_id)
.join(TestVersion, TestVersion.id == TestAttempt.test_version_id)
.join(Test, Test.id == TestVersion.test_id)
.filter(TestAttempt.status == 'completed')
.order_by(TestAttempt.completed_at.desc())
.limit(10)
.all()
)
dept_stats = [
{
'name': r.name,
'total': r.total,
'passed': int(r.passed_count or 0),
'rate': round(100 * int(r.passed_count or 0) / r.total) if r.total else 0,
}
for r in dept_rows
]
recent_list = [
{
'user': r.full_name or r.login,
'test': r.test_title,
'passed': r.passed,
'score': f'{r.correct_count}/{r.total_questions}' if r.correct_count is not None else '',
'at': r.completed_at.strftime('%d.%m %H:%M') if r.completed_at else '',
}
for r in recent_rows
]
pass_rate = round(100 * total_passed / total_completed) if total_completed else 0
return {
'total_tests': total_tests,
'active_tests': active_tests,
'total_completed': total_completed,
'total_passed': total_passed,
'total_users': total_users,
'pass_rate': pass_rate,
'dept_stats': dept_stats,
'recent': recent_list,
}
except Exception:
return {
'total_tests': 0, 'active_tests': 0, 'total_completed': 0,
'total_passed': 0, 'total_users': 0, 'pass_rate': 0,
'dept_stats': [], 'recent': [],
}
@main_bp.route('/health')
def health():
"""Smoke-проверка приложения и подключений к БД (без авторизации)."""
@@ -29,3 +128,29 @@ def health():
@login_required
def index():
return render_template('index.html')
@main_bp.route('/stats')
@login_required
def stats_page():
return render_template('stats.html', stats=_get_stats())
@main_bp.route('/assignments')
@login_required
def assignments_page():
from ..db import get_session
from ..models import Test, TestVersion
from sqlalchemy.orm import selectinload
session = get_session()
tests = (
session.query(Test)
.join(TestVersion, (TestVersion.test_id == Test.id) & TestVersion.is_active.is_(True))
.filter(Test.is_active.is_(True))
.order_by(Test.title)
.all()
)
return render_template('assignments.html', tests=[
{'id': str(t.id), 'title': t.title or '(без названия)'}
for t in tests
])
+30 -7
View File
@@ -1,15 +1,11 @@
"""Страница настроек: статус LLM-ключа и проверка подключения (E1.8).
Ключ — общий, читается из ENV (`DEEPSEEK_API_KEY` / `OPENAI_API_KEY`). Здесь —
только просмотр статуса и smoke-проверка. Изменение ключа — через `.env` и
рестарт процесса.
"""
"""Настройки: LLM-статус, пинг и редактор промптов."""
from __future__ import annotations
from flask import Blueprint, jsonify, render_template
from flask import Blueprint, jsonify, render_template, request
from ..auth.decorators import login_required
from ..services.llm_client import get_llm_config, ping_llm
from ..services import prompt_store
settings_bp = Blueprint('settings', __name__)
@@ -27,7 +23,34 @@ def settings_page():
)
@settings_bp.route('/settings/prompts', methods=['GET'])
@login_required
def prompts_page():
return render_template('settings_prompts.html', prompts=prompt_store.get_all())
@settings_bp.route('/api/llm/ping', methods=['POST', 'GET'])
@login_required
def api_llm_ping():
return jsonify(ping_llm())
@settings_bp.route('/api/ai/prompts', methods=['GET'])
@login_required
def api_get_prompts():
return jsonify(prompts=prompt_store.get_all())
@settings_bp.route('/api/ai/prompts/<prompt_id>', methods=['PUT'])
@login_required
def api_save_prompt(prompt_id: str):
body = request.get_json(silent=True) or {}
system = str(body.get('system') or '').strip()
user = str(body.get('user') or '').strip()
try:
prompt_store.save_prompt(prompt_id, system, user)
except KeyError as e:
return jsonify(ok=False, error=str(e)), 404
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
return jsonify(ok=True)