Merge remote-tracking branch 'origin/main' into feat/dds-timers
# Conflicts: # backend/app/scoring/dispatcher.py # backend/app/scoring/export.py # backend/app/session/finish.py # docs/arch/CONTRACT.md # docs/spec/GAP.md # frontend/src/features/instructor/ScoreWeightsEditor.tsx
This commit is contained in:
commit
4abdf0a901
61 changed files with 4084 additions and 167 deletions
|
|
@ -11,6 +11,7 @@ from html import escape
|
|||
from pathlib import Path
|
||||
|
||||
from app.domain.events import SessionReport
|
||||
from app.domain.taxonomy import Finding, FindingDecision, FindingSource
|
||||
|
||||
|
||||
def _cell(value: object) -> str:
|
||||
|
|
@ -18,12 +19,27 @@ def _cell(value: object) -> str:
|
|||
if value is None:
|
||||
return ""
|
||||
result = str(value)
|
||||
probe = result.lstrip(" \t\r\n\ufeff\u200b")
|
||||
probe = result.lstrip(" \t\r\n\N{ZERO WIDTH NO-BREAK SPACE}\N{ZERO WIDTH SPACE}")
|
||||
if probe.startswith(("=", "+", "-", "@")) or result.startswith(("\t", "\r", "\n")):
|
||||
return "'" + result
|
||||
return result
|
||||
|
||||
|
||||
def _finding_origin(item: Finding) -> str:
|
||||
if item.source is FindingSource.INSTRUCTOR:
|
||||
return f"; добавлена преподавателем {item.author or ''}".rstrip()
|
||||
return ""
|
||||
|
||||
|
||||
def _finding_decision(item: Finding) -> str:
|
||||
"""Решения по отметке рядом с ней, все по порядку: действует последнее."""
|
||||
return "".join(
|
||||
f"; {'снята' if review.decision is FindingDecision.DISMISSED else 'подтверждена'} "
|
||||
f"преподавателем {review.author}: {review.reason}"
|
||||
for review in item.reviews
|
||||
)
|
||||
|
||||
|
||||
def to_csv(report: SessionReport) -> bytes:
|
||||
"""UTF-8 с BOM: кириллица открывается в Excel без выбора кодировки."""
|
||||
output = io.StringIO(newline="")
|
||||
|
|
@ -38,12 +54,16 @@ def to_csv(report: SessionReport) -> bytes:
|
|||
row("Занятие", "", "Режим", report.mode.value)
|
||||
row("Занятие", "", "Попытка", report.attempt)
|
||||
row("Критерии", "", "Лимит открытия карточки ДДС, с", report.criteria.decision_time_limit_seconds)
|
||||
row("Критерии", "", "Лимит реакции на доклад бригады, с",
|
||||
report.criteria.dds_report_reaction_limit_seconds
|
||||
if report.criteria.dds_report_reaction_enabled else "не применяется")
|
||||
row("Критерии", "", "Допустимо ошибок", report.criteria.allowed_errors)
|
||||
row("Критерии", "", "Грамматика обязательна", "да" if report.criteria.require_correct_grammar else "нет")
|
||||
row("Критерии", "", "Переопределения весов", report.criteria.score_weights or "стандартные")
|
||||
row("Критерии", "", "Нарушенных метрик", report.failed_metrics)
|
||||
row("Критерии", "", "Зачёт", "да" if report.passed else "нет")
|
||||
row("Оценка", "", "Автоматическая", report.score_auto)
|
||||
row("Оценка", "", "После разбора отметок", report.score_reviewed)
|
||||
row("Оценка", "", "Итоговая", report.score_final)
|
||||
row("Оценка", "", "Изменена преподавателем", report.overridden_by)
|
||||
row("Оценка", "", "Причина изменения", report.override_comment)
|
||||
|
|
@ -54,7 +74,9 @@ def to_csv(report: SessionReport) -> bytes:
|
|||
f"Норматив: {item.norm}; результат: {'да' if item.passed else 'нет'}; "
|
||||
f"вес: {item.weight:g}{credit}; источник: {item.ref or ''}")
|
||||
for number, item in enumerate(report.findings, 1):
|
||||
row("Ошибки", number, item.code.value, item.summary, f"Факт: {item.fact}; норматив: {item.norm or ''}; источник: {item.ref or ''}")
|
||||
row("Ошибки", number, item.code.value, item.summary,
|
||||
f"Факт: {item.fact}; норматив: {item.norm or ''}; источник: {item.ref or ''}"
|
||||
f"{_finding_origin(item)}{_finding_decision(item)}")
|
||||
for number, item in enumerate(report.competencies, 1):
|
||||
row("Компетенции", number, item.competency, item.value)
|
||||
for number, item in enumerate(report.transcript, 1):
|
||||
|
|
@ -77,7 +99,7 @@ def to_csv(report: SessionReport) -> bytes:
|
|||
row("Сверка самооценки", "", label, ", ".join(values))
|
||||
for number, item in enumerate(report.notes, 1):
|
||||
row("Пометки преподавателя", number, item.transcript_ref, item.text, item.author)
|
||||
return ("\ufeff" + output.getvalue()).encode("utf-8")
|
||||
return ("\N{ZERO WIDTH NO-BREAK SPACE}" + output.getvalue()).encode("utf-8")
|
||||
|
||||
|
||||
def _font_paths() -> tuple[Path, Path | None]:
|
||||
|
|
@ -151,6 +173,9 @@ def to_pdf(report: SessionReport) -> bytes:
|
|||
("Режим", report.mode.value),
|
||||
("Попытка", report.attempt),
|
||||
("Лимит открытия карточки ДДС", f"{report.criteria.decision_time_limit_seconds} с"),
|
||||
("Лимит реакции на доклад бригады",
|
||||
f"{report.criteria.dds_report_reaction_limit_seconds} с"
|
||||
if report.criteria.dds_report_reaction_enabled else "не применяется"),
|
||||
("Допустимо ошибок", report.criteria.allowed_errors),
|
||||
("Грамматика обязательна", "да" if report.criteria.require_correct_grammar else "нет"),
|
||||
("Переопределения весов", report.criteria.score_weights or "стандартные"),
|
||||
|
|
@ -160,6 +185,9 @@ def to_pdf(report: SessionReport) -> bytes:
|
|||
("Итоговая оценка", report.score_final),
|
||||
):
|
||||
story.append(Paragraph(f"<b>{escape(label)}:</b> {escape(str(value))}", base))
|
||||
# Когда итог правили вручную, без этой строки не понять, какой балл дал разбор.
|
||||
if report.score_reviewed is not None:
|
||||
story.append(Paragraph(f"<b>После разбора отметок:</b> {report.score_reviewed}", base))
|
||||
if report.overridden_by:
|
||||
story.append(Paragraph(f"<b>Оценка изменена:</b> {escape(report.overridden_by)}", base))
|
||||
if report.override_comment:
|
||||
|
|
@ -183,7 +211,9 @@ def to_pdf(report: SessionReport) -> bytes:
|
|||
story.append(p("Ошибок не выявлено", muted))
|
||||
for item in report.findings:
|
||||
story.append(p(f"{item.code.value} - {item.summary}"))
|
||||
story.append(p(f"Факт: {item.fact}. Норматив: {item.norm or 'не указан'}. Источник: {item.ref or 'не указан'}", muted))
|
||||
story.append(p(f"Факт: {item.fact}. Норматив: {item.norm or 'не указан'}. "
|
||||
f"Источник: {item.ref or 'не указан'}"
|
||||
f"{_finding_origin(item)}{_finding_decision(item)}", muted))
|
||||
|
||||
section("Компетенции")
|
||||
for item in report.competencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue