diff --git a/backend/app/domain/events.py b/backend/app/domain/events.py index 3cd3790..0d5f931 100644 --- a/backend/app/domain/events.py +++ b/backend/app/domain/events.py @@ -23,7 +23,7 @@ from app.domain.roles import Role from app.domain.statuses import ServiceStatus, StationSnapshot from app.domain.taxonomy import Finding from app.domain.timers import TimerSnapshot -from app.scoring.taxonomy import METRIC_MAP +from app.scoring.taxonomy import INSTRUCTOR_METRIC, METRIC_MAP class SessionMode(StrEnum): @@ -60,7 +60,7 @@ class LessonCriteria(BaseModel): @field_validator("score_weights") @classmethod def validate_score_weights(cls, weights: dict[str, float]) -> dict[str, float]: - unknown = weights.keys() - METRIC_MAP.keys() + unknown = weights.keys() - METRIC_MAP.keys() - {INSTRUCTOR_METRIC} if unknown: raise ValueError(f"неизвестные метрики весов: {', '.join(sorted(unknown))}") if any(not 0 <= weight <= 10 for weight in weights.values()): @@ -747,6 +747,8 @@ class DdsCardReport(BaseModel): card_id: UUID scenario_id: str score_auto: float + #: Балл карточки после решений преподавателя по отметкам; None — решений не было. + score_reviewed: float | None = None reply_text: str metrics: list[Metric] findings: list[Finding] diff --git a/backend/app/scoring/export.py b/backend/app/scoring/export.py index 9ee879b..2ea4cf6 100644 --- a/backend/app/scoring/export.py +++ b/backend/app/scoring/export.py @@ -19,7 +19,7 @@ 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​") if probe.startswith(("=", "+", "-", "@")) or result.startswith(("\t", "\r", "\n")): return "'" + result return result @@ -32,12 +32,12 @@ def _finding_origin(item: Finding) -> str: def _finding_decision(item: Finding) -> str: - """Решение по отметке рядом с ней: снятая отметка видна вместе с причиной.""" - if item.review is None: - return "" - verdict = ("снята" if item.review.decision is FindingDecision.DISMISSED - else "подтверждена") - return f"; {verdict} преподавателем {item.review.author}: {item.review.reason}" + """Решения по отметке рядом с ней, все по порядку: действует последнее.""" + 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: @@ -96,7 +96,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 ("" + output.getvalue()).encode("utf-8") def _font_paths() -> tuple[Path, Path | None]: @@ -179,6 +179,9 @@ def to_pdf(report: SessionReport) -> bytes: ("Итоговая оценка", report.score_final), ): story.append(Paragraph(f"{escape(label)}: {escape(str(value))}", base)) + # Когда итог правили вручную, без этой строки не понять, какой балл дал разбор. + if report.score_reviewed is not None: + story.append(Paragraph(f"После разбора отметок: {report.score_reviewed}", base)) if report.overridden_by: story.append(Paragraph(f"Оценка изменена: {escape(report.overridden_by)}", base)) if report.override_comment: