fix: история решений по отметке, пересчёт кодов и баллов карточек, вес отметки преподавателя из весов занятия
This commit is contained in:
parent
3f5f57877a
commit
db8787c6b3
4 changed files with 100 additions and 16 deletions
|
|
@ -14,12 +14,10 @@ from app.domain.events import Metric
|
|||
from app.domain.taxonomy import Finding, FindingDecision, FindingReview, FindingSource
|
||||
from app.scoring.competency import radar
|
||||
from app.scoring.gost import GostResult
|
||||
from app.scoring.taxonomy import METRIC_MAP
|
||||
from app.scoring.taxonomy import INSTRUCTOR_METRIC, METRIC_MAP
|
||||
|
||||
#: Метрика для ручной отметки, которой нечего провалить: у кода нет метрики
|
||||
#: в METRIC_MAP (D4) или в карточке она не считалась. Вес — как у метрики по
|
||||
#: умолчанию, чтобы отметка преподавателя не весила больше автоматической.
|
||||
INSTRUCTOR_METRIC = "instructor_finding"
|
||||
__all__ = ["INSTRUCTOR_METRIC", "add_finding", "has_finding", "linked", "review_finding",
|
||||
"reviewed_metrics"]
|
||||
|
||||
|
||||
def _in_scope(finding: Finding, metric: Metric) -> bool:
|
||||
|
|
@ -46,7 +44,8 @@ def _credit(metric: Metric) -> float:
|
|||
return metric.credit if metric.credit is not None else float(metric.passed)
|
||||
|
||||
|
||||
def reviewed_metrics(metrics: list[Metric], findings: list[Finding]) -> list[Metric]:
|
||||
def reviewed_metrics(metrics: list[Metric], findings: list[Finding],
|
||||
instructor_weight: float = 1.0) -> list[Metric]:
|
||||
"""Метрики с учётом решений преподавателя.
|
||||
|
||||
Проваленная метрика засчитывается, только если все её автоматические
|
||||
|
|
@ -93,15 +92,47 @@ def reviewed_metrics(metrics: list[Metric], findings: list[Finding]) -> list[Met
|
|||
norm=finding.norm or "по решению преподавателя",
|
||||
ref=finding.ref,
|
||||
passed=False,
|
||||
weight=instructor_weight,
|
||||
card=finding.card,
|
||||
service=finding.service,
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
def _instructor_weight(report: dict) -> float:
|
||||
"""Вес метрики преподавателя из весов занятия: веса настраивает преподаватель."""
|
||||
criteria = (report.get("full_report") or {}).get("criteria") or {}
|
||||
return float((criteria.get("score_weights") or {}).get(INSTRUCTOR_METRIC, 1.0))
|
||||
|
||||
|
||||
def _codes(findings: list[Finding]) -> dict[str, int]:
|
||||
"""Коды действующих отметок: по ним строятся частые ошибки группы и профиль."""
|
||||
codes: dict[str, int] = {}
|
||||
for finding in findings:
|
||||
if not _dismissed(finding):
|
||||
codes[finding.code.value] = codes.get(finding.code.value, 0) + 1
|
||||
return codes
|
||||
|
||||
|
||||
def _cards(cards: list[dict], metrics: list[Metric], findings: list[Finding]) -> list[dict]:
|
||||
"""Карточки очереди ДДС с тем же разбором: их метрики — часть метрик занятия."""
|
||||
updated = []
|
||||
for number, card in enumerate(cards, 1):
|
||||
own = [metric for metric in metrics if metric.card == number]
|
||||
updated.append({
|
||||
**card,
|
||||
"metrics": [metric.model_dump(mode="json") for metric in own],
|
||||
"findings": [finding.model_dump(mode="json")
|
||||
for finding in findings if finding.card == number],
|
||||
"score_reviewed": GostResult(metrics=own).score,
|
||||
})
|
||||
return updated
|
||||
|
||||
|
||||
def _rescore(report: dict, findings: list[Finding]) -> dict:
|
||||
metrics = reviewed_metrics(
|
||||
[Metric.model_validate(item) for item in report.get("metrics", [])], findings,
|
||||
_instructor_weight(report),
|
||||
)
|
||||
score = GostResult(metrics=metrics).score
|
||||
fields = {
|
||||
|
|
@ -114,12 +145,17 @@ def _rescore(report: dict, findings: list[Finding]) -> dict:
|
|||
if not report.get("overridden_by"):
|
||||
fields["score_final"] = score
|
||||
updated = {**report, **fields}
|
||||
if "summary" in report:
|
||||
updated["summary"] = {**(report.get("summary") or {}), "codes": _codes(findings)}
|
||||
if report.get("card_results"):
|
||||
updated["card_results"] = _cards(report["card_results"], metrics, findings)
|
||||
archived = report.get("full_report")
|
||||
if isinstance(archived, dict):
|
||||
failed = sum(not metric.passed and metric.weight > 0 for metric in metrics)
|
||||
allowed = (archived.get("criteria") or {}).get("allowed_errors", 0)
|
||||
updated["full_report"] = {
|
||||
**archived, **fields,
|
||||
"card_results": _cards(archived.get("card_results") or [], metrics, findings),
|
||||
"failed_metrics": failed,
|
||||
"passed": failed <= allowed,
|
||||
"score_final": updated.get("score_final", archived.get("score_final")),
|
||||
|
|
@ -132,16 +168,26 @@ def _findings(report: dict) -> list[Finding]:
|
|||
|
||||
|
||||
def review_finding(report: dict, index: int, review: FindingReview) -> dict:
|
||||
"""Подтвердить или снять отметку. Повторное решение заменяет прежнее."""
|
||||
"""Подтвердить или снять отметку. Решение дописывается, действует последнее."""
|
||||
findings = _findings(report)
|
||||
if not 0 <= index < len(findings):
|
||||
raise LookupError(f"нет отметки {index}")
|
||||
findings[index] = findings[index].model_copy(update={"review": review})
|
||||
findings[index] = findings[index].model_copy(
|
||||
update={"reviews": [*findings[index].reviews, review]})
|
||||
return _rescore(report, findings)
|
||||
|
||||
|
||||
def has_finding(report: dict, client_id) -> bool:
|
||||
"""Ручная отметка с этим ключом уже в разборе — повтор запроса."""
|
||||
return client_id is not None and any(
|
||||
finding.client_id == client_id for finding in _findings(report))
|
||||
|
||||
|
||||
def add_finding(report: dict, finding: Finding) -> dict:
|
||||
"""Добавить отметку преподавателя; она штрафует связанную метрику."""
|
||||
if finding.source is not FindingSource.INSTRUCTOR:
|
||||
raise ValueError("вручную добавляется только отметка преподавателя")
|
||||
# Повтор (ретрай сети, вторая вкладка) не штрафует второй раз.
|
||||
if has_finding(report, finding.client_id):
|
||||
return report
|
||||
return _rescore(report, [*_findings(report), finding])
|
||||
|
|
|
|||
Loading…
Reference in a new issue