блоки 2 и 3 доработки интерфейса системы тестирования
This commit is contained in:
@@ -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
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user