2311061111-lyt/autograde/generate_pdf_report.py
liyitian 6fd6854648
All checks were successful
autograde-final-vibevault / check-trigger (push) Successful in 2s
autograde-final-vibevault / grade (push) Has been skipped
修复PDF报告生成参数不匹配问题
2025-12-14 19:47:17 +08:00

536 lines
19 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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
generate_pdf_report.py - PDF报告生成脚本
将评分结果和报告内容整合生成PDF文件
"""
import json
import argparse
import sys
import os
from datetime import datetime
# 尝试导入reportlab如果不存在则显示错误
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,
Table, TableStyle, Image, PageBreak)
from reportlab.lib import colors
from reportlab.lib.units import cm
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='PDF Report Generation Script')
parser.add_argument('--score', '--grade', required=True, help='最终成绩文件')
parser.add_argument('--report', required=True, help='REPORT.md文件路径')
parser.add_argument('--frontend', required=True, help='FRONTEND.md文件路径')
parser.add_argument('--out', required=True, help='输出PDF文件路径')
parser.add_argument('--student-id', required=True, help='学生ID')
parser.add_argument('--student-name', required=True, help='学生姓名')
parser.add_argument('--class-id', '--class-name', help='班级ID/班级名称')
parser.add_argument('--assignment-id', default='2311061111', help='作业ID')
parser.add_argument('--images', help='图片目录路径')
parser.add_argument('--commit-sha', help='提交SHA')
return parser.parse_args()
def load_file_content(file_path: str) -> str:
"""加载文件内容"""
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def load_json_file(file_path: str) -> dict:
"""加载JSON文件内容"""
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def markdown_to_paragraphs(markdown_text: str) -> list:
"""将Markdown文本转换为ReportLab可处理的段落列表"""
# 简单的Markdown处理主要处理标题和段落
paragraphs = []
lines = markdown_text.split('\n')
current_paragraph = ""
for line in lines:
line = line.strip()
if not line:
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
continue
# 处理标题
if line.startswith('# '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h1', line[2:]))
elif line.startswith('## '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h2', line[3:]))
elif line.startswith('### '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h3', line[4:]))
elif line.startswith('#### '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h4', line[5:]))
# 处理列表
elif line.startswith('- '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('list_item', line[2:]))
elif line.startswith('* '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('list_item', line[2:]))
elif line.startswith('1. '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('list_item', line[3:]))
# 处理代码块
elif line.startswith('```'):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('code_block_start', ''))
elif paragraphs and paragraphs[-1][0] == 'code_block_start':
if line.startswith('```'):
paragraphs.append(('code_block_end', ''))
else:
paragraphs.append(('code_block', line))
# 处理引用
elif line.startswith('> '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('quote', line[2:]))
# 处理粗体和斜体
else:
current_paragraph += ' ' + line if current_paragraph else line
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
return paragraphs
def generate_pdf_report(args, score_data, report_content, frontend_content):
"""生成PDF报告"""
if not REPORTLAB_AVAILABLE:
print("❌ ERROR: reportlab库未安装无法生成PDF报告")
print("请运行: pip install reportlab")
return False
# 创建PDF文档
doc = SimpleDocTemplate(args.out, pagesize=A4)
story = []
# 获取样式
styles = getSampleStyleSheet()
# 自定义样式
styles.add(ParagraphStyle(
name='CustomHeading1',
parent=styles['Heading1'],
fontSize=24,
alignment=TA_CENTER,
textColor=colors.HexColor('#2c3e50'),
spaceAfter=30
))
styles.add(ParagraphStyle(
name='CustomHeading2',
parent=styles['Heading2'],
fontSize=18,
textColor=colors.HexColor('#34495e'),
spaceAfter=15,
spaceBefore=15
))
styles.add(ParagraphStyle(
name='CustomHeading3',
parent=styles['Heading3'],
fontSize=14,
textColor=colors.HexColor('#7f8c8d'),
spaceAfter=10
))
styles.add(ParagraphStyle(
name='CustomParagraph',
parent=styles['Normal'],
fontSize=12,
leading=18,
alignment=TA_JUSTIFY,
textColor=colors.HexColor('#000000')
))
styles.add(ParagraphStyle(
name='CustomListItem',
parent=styles['CustomParagraph'],
leftIndent=20,
bulletIndent=10
))
styles.add(ParagraphStyle(
name='CustomCentered',
parent=styles['CustomParagraph'],
alignment=TA_CENTER
))
styles.add(ParagraphStyle(
name='ScoreStyle',
parent=styles['CustomCentered'],
fontSize=36,
textColor=colors.HexColor('#e74c3c'),
fontWeight='bold',
spaceAfter=20
))
# 封面
story.append(Spacer(1, 5*cm))
story.append(Paragraph("作业成绩报告", styles['CustomHeading1']))
story.append(Spacer(1, 2*cm))
# 学生信息
student_info = [
['学生姓名:', args.student_name],
['学生ID:', args.student_id],
['班级ID:', args.class_id],
['作业ID:', args.assignment_id],
['提交日期:', datetime.now().strftime('%Y-%m-%d')]
]
table_style = TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
('RIGHTPADDING', (0, 0), (0, -1), 20),
])
info_table = Table(student_info, colWidths=[100, 200])
info_table.setStyle(table_style)
story.append(Spacer(1, 1*cm))
story.append(info_table)
# 最终成绩
story.append(Spacer(1, 3*cm))
story.append(Paragraph("最终成绩", styles['CustomHeading2']))
story.append(Paragraph(f"{score_data['final_score']:.2f}/100", styles['ScoreStyle']))
# 成绩等级
if score_data['final_score'] >= 90:
grade = "优秀"
grade_color = colors.HexColor('#27ae60')
elif score_data['final_score'] >= 80:
grade = "良好"
grade_color = colors.HexColor('#f39c12')
elif score_data['final_score'] >= 70:
grade = "中等"
grade_color = colors.HexColor('#e67e22')
elif score_data['final_score'] >= 60:
grade = "及格"
grade_color = colors.HexColor('#d35400')
else:
grade = "不及格"
grade_color = colors.HexColor('#c0392b')
story.append(Paragraph(f"等级: {grade}",
ParagraphStyle(name='GradeStyle', parent=styles['CustomCentered'], fontSize=20, textColor=grade_color)))
story.append(PageBreak())
# 成绩分布
story.append(Paragraph("成绩分布", styles['CustomHeading2']))
distribution_data = [
['部分', '得分', '权重', '占总分百分比']
]
for part, data in score_data['breakdown'].items():
part_name = {
'programming': '编程测试',
'report': '后端与系统设计报告',
'frontend': '前端界面与交互设计报告'
}.get(part, part)
distribution_data.append([
part_name,
f"{data['score']:.2f}",
f"{data['weight']*100:.0f}%",
f"{data['percentage']:.2f}"
])
distribution_table = Table(distribution_data, colWidths=[150, 60, 60, 100])
distribution_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
('TOPPADDING', (0, 0), (-1, -1), 10),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
]))
story.append(Spacer(1, 1*cm))
story.append(distribution_table)
story.append(Spacer(1, 2*cm))
# 编程测试详情
story.append(Paragraph("编程测试详情", styles['CustomHeading2']))
programming = score_data['details']['programming']
programming_groups = programming.get('groups', [])
if programming_groups:
prog_data = [['测试组', '通过测试', '总测试数', '得分', '通过率']]
for group in programming_groups:
prog_data.append([
group['name'],
group['passed'],
group['total'],
f"{group['score']:.2f}",
f"{group['passed']/group['total']*100:.1f}%"
])
prog_table = Table(prog_data, colWidths=[100, 70, 70, 60, 70])
prog_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('TOPPADDING', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
]))
story.append(Spacer(1, 1*cm))
story.append(prog_table)
else:
story.append(Paragraph("暂无编程测试详情", styles['CustomParagraph']))
story.append(Spacer(1, 2*cm))
story.append(PageBreak())
# 后端与系统设计报告
story.append(Paragraph("后端与系统设计报告", styles['CustomHeading2']))
story.append(Spacer(1, 1*cm))
report_paragraphs = markdown_to_paragraphs(report_content)
in_code_block = False
for para_type, content in report_paragraphs:
if para_type == 'h1':
story.append(Paragraph(content, styles['CustomHeading1']))
elif para_type == 'h2':
story.append(Paragraph(content, styles['CustomHeading2']))
elif para_type == 'h3':
story.append(Paragraph(content, styles['CustomHeading3']))
elif para_type == 'paragraph':
story.append(Paragraph(content, styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'list_item':
story.append(Paragraph(f'{content}', styles['CustomListItem']))
story.append(Spacer(1, 0.2*cm))
elif para_type == 'quote':
story.append(Paragraph(content,
ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'],
leftIndent=20, rightIndent=20,
borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7'))))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block_start':
in_code_block = True
story.append(Paragraph('```', styles['CustomParagraph']))
elif para_type == 'code_block_end':
in_code_block = False
story.append(Paragraph('```', styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block' and in_code_block:
story.append(Paragraph(content,
ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'],
fontName='Courier', fontSize=10,
leftIndent=20, rightIndent=20)))
story.append(PageBreak())
# 前端界面与交互设计报告
story.append(Paragraph("前端界面与交互设计报告", styles['CustomHeading2']))
story.append(Spacer(1, 1*cm))
frontend_paragraphs = markdown_to_paragraphs(frontend_content)
in_code_block = False
for para_type, content in frontend_paragraphs:
if para_type == 'h1':
story.append(Paragraph(content, styles['CustomHeading1']))
elif para_type == 'h2':
story.append(Paragraph(content, styles['CustomHeading2']))
elif para_type == 'h3':
story.append(Paragraph(content, styles['CustomHeading3']))
elif para_type == 'paragraph':
story.append(Paragraph(content, styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'list_item':
story.append(Paragraph(f'{content}', styles['CustomListItem']))
story.append(Spacer(1, 0.2*cm))
elif para_type == 'quote':
story.append(Paragraph(content,
ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'],
leftIndent=20, rightIndent=20,
borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7'))))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block_start':
in_code_block = True
story.append(Paragraph('```', styles['CustomParagraph']))
elif para_type == 'code_block_end':
in_code_block = False
story.append(Paragraph('```', styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block' and in_code_block:
story.append(Paragraph(content,
ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'],
fontName='Courier', fontSize=10,
leftIndent=20, rightIndent=20)))
# 构建并保存PDF
try:
doc.build(story)
return True
except Exception as e:
print(f"❌ PDF生成失败: {str(e)}")
return False
def generate_text_report(args, score_data, report_content, frontend_content):
"""生成纯文本报告作为备选方案"""
text_report = []
text_report.append("=" * 80)
text_report.append(" 作业成绩报告 ")
text_report.append("=" * 80)
text_report.append("")
# 学生信息
text_report.append(f"学生姓名: {args.student_name}")
text_report.append(f"学生ID: {args.student_id}")
text_report.append(f"班级ID: {args.class_id}")
text_report.append(f"作业ID: {args.assignment_id}")
text_report.append(f"提交日期: {datetime.now().strftime('%Y-%m-%d')}")
text_report.append("")
# 最终成绩
text_report.append("-" * 80)
text_report.append(f"最终成绩: {score_data['final_score']:.2f}/100")
# 成绩等级
if score_data['final_score'] >= 90:
grade = "优秀"
elif score_data['final_score'] >= 80:
grade = "良好"
elif score_data['final_score'] >= 70:
grade = "中等"
elif score_data['final_score'] >= 60:
grade = "及格"
else:
grade = "不及格"
text_report.append(f"等级: {grade}")
text_report.append("")
# 成绩分布
text_report.append("成绩分布:")
text_report.append("| 部分 | 得分 | 权重 | 占总分百分比 |")
text_report.append("|------|------|------|------------|")
for part, data in score_data['breakdown'].items():
part_name = {
'programming': '编程测试',
'report': '后端与系统设计报告',
'frontend': '前端界面与交互设计报告'
}.get(part, part)
text_report.append(f"| {part_name} | {data['score']:.2f} | {data['weight']*100:.0f}% | {data['percentage']:.2f} |")
text_report.append("")
text_report.append("=" * 80)
text_report.append("")
# 保存文本报告
text_report_path = args.out.replace('.pdf', '.txt')
with open(text_report_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(text_report))
print(f"✅ 已生成纯文本报告: {text_report_path}")
return True
def main():
"""主函数"""
args = parse_args()
print("📁 加载评分数据...")
score_data = load_json_file(args.score)
print("📁 加载后端报告内容...")
report_content = load_file_content(args.report)
print("📁 加载前端报告内容...")
frontend_content = load_file_content(args.frontend)
print("📄 生成PDF报告...")
pdf_success = generate_pdf_report(args, score_data, report_content, frontend_content)
# 如果PDF生成失败生成纯文本报告
if not pdf_success:
print("🔄 尝试生成纯文本报告...")
generate_text_report(args, score_data, report_content, frontend_content)
else:
print(f"✅ PDF报告生成成功: {args.out}")
# 生成一个简单的总结文件
summary = {
"student_info": {
"name": args.student_name,
"id": args.student_id,
"class_id": args.class_id,
"assignment_id": args.assignment_id
},
"final_score": score_data["final_score"],
"report_path": args.out,
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"status": "completed"
}
summary_path = args.out.replace('.pdf', '_summary.json')
with open(summary_path, 'w', encoding='utf-8') as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"✅ 总结文件生成成功: {summary_path}")
if __name__ == '__main__':
main()