Files
WiseGrade/backend/wisegrade/exports.py
T

142 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from html import escape
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill
from sqlalchemy import select
from weasyprint import HTML
from .config import get_settings
from .knowledge_pages import evidence_links
from .models import (CriterionScore, ExportTask, Project, ScoreEvidence, ScoringBatch, Student, SubmissionFile)
from .rules import totals
def project_result(db, batch: ScoringBatch, project: Project):
scores = db.scalars(select(CriterionScore).where(CriterionScore.batch_id == batch.id,
CriterionScore.project_id == project.id)).all()
by_id = {score.criterion_id: score for score in scores}
criteria = batch.rubric_snapshot["criteria"]
totals_ = totals({key: score.final_score for key, score in by_id.items()}, criteria, batch.rubric_snapshot["weights"])
rows = []
def evidence_dict(item):
links = evidence_links(db, item)
return {"page": item.page, "quote": item.quote, "file_id": item.submission_file_id,
"document_id": item.document_id,
"document": db.get(SubmissionFile, item.submission_file_id).relative_path if item.submission_file_id else "课程资料",
**links}
for criterion in criteria:
score = by_id.get(criterion["criterion_id"])
evidence = db.scalars(select(ScoreEvidence).where(ScoreEvidence.score_id == score.id)).all() if score else []
rows.append({"criterion": criterion, "score_id": score.id if score else None,
"model_score": score.model_score if score else None, "teacher_score": score.teacher_score if score else None,
"final_score": score.final_score if score else None,
"confidence": score.confidence if score else None,
"model_comment": score.model_comment if score else "", "teacher_comment": score.teacher_comment if score else None,
"missing_evidence": score.missing_evidence if score else [], "suggestion": score.suggestion if score else "",
"status": score.status if score else "pending", "error": score.error if score else "",
"evidence": [evidence_dict(item) for item in evidence]})
student = db.get(Student, project.student_id)
return {"project_id": project.id, "student_no": student.student_no, "name": student.name, "topic": project.topic,
"totals": totals_, "rows": rows, "facts": project.facts}
def batch_projects(db, batch: ScoringBatch):
if batch.project_ids is None:
return db.scalars(select(Project).where(Project.course_version_id == batch.course_version_id).order_by(Project.id)).all()
if not batch.project_ids:
return []
by_id = {project.id: project for project in db.scalars(select(Project).where(Project.id.in_(batch.project_ids))).all()}
return [by_id[project_id] for project_id in batch.project_ids if project_id in by_id]
def export_excel(db, batch_id: int):
batch = db.get(ScoringBatch, batch_id)
if not batch:
raise ValueError("评分批次不存在")
root = get_settings().exports_path
root.mkdir(parents=True, exist_ok=True)
task = ExportTask(batch_id=batch_id, kind="excel", status="running")
db.add(task)
db.flush()
path = root / f"batch-{batch_id}-scores-{task.id}.xlsx"
try:
book = Workbook()
sheet = book.active
sheet.title = "成绩"
final_criteria = [c for c in batch.rubric_snapshot["criteria"] if c["category"] == "final"]
sheet.append(["学号", "姓名", "选题", "阶段报告1", "阶段报告2", "阶段报告3", *[c["name"] for c in final_criteria], "期末成绩", "总评", "状态", "批次", "规则版本"])
for project in batch_projects(db, batch):
result = project_result(db, batch, project)
scores = {row["criterion"]["criterion_id"]: row["final_score"] for row in result["rows"]}
totals_ = result["totals"]
sheet.append([result["student_no"], result["name"], result["topic"], *[totals_[key] for key in ("stage1", "stage2", "stage3")],
*[scores.get(c["criterion_id"]) for c in final_criteria], totals_["final"], totals_["overall"],
"完成" if totals_["overall"] is not None else "未完成/待人工处理", batch.id, batch.rubric_version_id])
sheet.freeze_panes = "D2"
sheet.auto_filter.ref = sheet.dimensions
for cell in sheet[1]:
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = PatternFill("solid", fgColor="153C65")
for column in sheet.columns:
letter = column[0].column_letter
sheet.column_dimensions[letter].width = min(50, max(12, max(len(str(cell.value or "")) for cell in column) + 2))
book.save(path)
task.path, task.status = str(path), "completed"
except Exception as exc:
task.status, task.error = "failed", str(exc)[:1000]
db.commit()
raise
db.commit()
return task
def _html_report(result, batch):
esc = lambda value: escape(str(value or ""))
rows = []
for row in result["rows"]:
criterion = row["criterion"]
evidences = "".join(
f"<li>{esc(e['document'])} · 第{e['page']}{'张幻灯片' if str(e['document']).lower().endswith('.pptx') else '页'}:{esc(e['quote'])}</li>"
for e in row["evidence"]
)
missing = "、".join(row["missing_evidence"] or [])
rows.append(f"<section><h3>{esc(criterion['name'])}({esc(row['final_score'])} / {criterion['max_score']})</h3>"
f"<p>模型原始分:{esc(row['model_score'])};教师修改分:{esc(row['teacher_score'])};状态:{esc(row['status'])}</p>"
f"<p>模型评语:{esc(row['model_comment'])}</p><p>教师评语:{esc(row['teacher_comment'])}</p>"
f"<p>缺失证据:{esc(missing)}</p><p>建议:{esc(row['suggestion'])}</p><p>异常:{esc(row['error'])}</p>"
f"<ul>{evidences}</ul></section>")
totals_ = result["totals"]
return f"""<!doctype html><html lang="zh"><meta charset="utf-8"><style>
body {{ font-family: 'Noto Sans CJK SC', sans-serif; font-size: 11px; color: #172b4d; }}
h1 {{ color: #153c65; }} section {{ border-bottom: 1px solid #ccd7e0; break-inside: avoid; }}
h3 {{ margin-bottom: 3px; }} p {{ margin: 3px 0; }} li {{ margin: 3px 0; }}
</style><body><h1>WiseGrade 智评 · 学生反馈</h1>
<p>学号:{esc(result['student_no'])} 姓名:{esc(result['name'])} 选题:{esc(result['topic'])}</p>
<p>评分批次:{batch.id} 课程版本:{batch.course_version_id} 规则版本:{batch.rubric_version_id}</p>
<p>模型:{esc(batch.model_config_snapshot.get('model'))} 嵌入模型:{esc(batch.embedding_model_name)} ({esc(batch.embedding_model_version)})</p>
<h2>成绩</h2><p>阶段1:{esc(totals_['stage1'])} 阶段2:{esc(totals_['stage2'])} 阶段3:{esc(totals_['stage3'])} 期末:{esc(totals_['final'])} 总评:{esc(totals_['overall'])}</p>
{''.join(rows)}</body></html>"""
def export_project_pdf(db, batch_id: int, project_id: int):
batch = db.get(ScoringBatch, batch_id)
project = db.get(Project, project_id)
if not batch or not project or project.course_version_id != batch.course_version_id or (batch.project_ids is not None and project_id not in batch.project_ids):
raise ValueError("批次或项目不存在")
root = get_settings().exports_path
root.mkdir(parents=True, exist_ok=True)
task = ExportTask(batch_id=batch_id, project_id=project_id, kind="pdf", status="running")
db.add(task)
db.flush()
path = root / f"batch-{batch_id}-project-{project_id}-{task.id}.pdf"
try:
HTML(string=_html_report(project_result(db, batch, project), batch)).write_pdf(path)
task.path, task.status = str(path), "completed"
except Exception as exc:
task.status, task.error = "failed", str(exc)[:1000]
db.commit()
raise
db.commit()
return task