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: