157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
"""Главный 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-проверка приложения и подключений к БД (без авторизации)."""
|
|
db_status = app_db.ping()
|
|
overall = 'ok' if db_status.get('main') == 'ok' else 'degraded'
|
|
return jsonify(
|
|
status=overall,
|
|
service='testing-flask-app',
|
|
db=db_status,
|
|
)
|
|
|
|
|
|
@main_bp.route('/')
|
|
@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
|
|
])
|