You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
156 lines
5.2 KiB
156 lines
5.2 KiB
"""Главный 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 |
|
])
|
|
|