Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -205,8 +205,8 @@ Click "Generate Report" to download your detailed results!""", True
|
|
| 205 |
else:
|
| 206 |
return "π Needs Improvement"
|
| 207 |
|
| 208 |
-
def generate_pdf_report(self,
|
| 209 |
-
"""Generate PDF report"""
|
| 210 |
if not self.user_answers:
|
| 211 |
return None
|
| 212 |
|
|
@@ -220,7 +220,14 @@ Click "Generate Report" to download your detailed results!""", True
|
|
| 220 |
temp_file.close()
|
| 221 |
|
| 222 |
# Create PDF
|
| 223 |
-
doc = SimpleDocTemplate(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
styles = getSampleStyleSheet()
|
| 225 |
story = []
|
| 226 |
|
|
@@ -243,10 +250,45 @@ Click "Generate Report" to download your detailed results!""", True
|
|
| 243 |
spaceAfter=20
|
| 244 |
)
|
| 245 |
|
| 246 |
-
# Header
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
# Candidate info
|
| 252 |
info_data = [
|
|
@@ -269,40 +311,60 @@ Click "Generate Report" to download your detailed results!""", True
|
|
| 269 |
story.append(info_table)
|
| 270 |
story.append(Spacer(1, 20))
|
| 271 |
|
| 272 |
-
# Results table
|
| 273 |
story.append(Paragraph("Detailed Results", styles['Heading2']))
|
| 274 |
story.append(Spacer(1, 12))
|
| 275 |
|
| 276 |
table_data = [['#', 'Question', 'Your Answer', 'Correct Answer', 'Result']]
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
for i, answer in enumerate(self.user_answers, 1):
|
| 279 |
result_icon = 'β' if answer['is_correct'] else 'β'
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
table_data.append([
|
| 283 |
str(i),
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
result_icon
|
| 288 |
])
|
| 289 |
|
| 290 |
-
|
|
|
|
| 291 |
results_table.setStyle(TableStyle([
|
| 292 |
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#003366')),
|
| 293 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 294 |
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
|
|
|
|
|
| 295 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 296 |
-
('FONTSIZE', (0, 0), (-1,
|
| 297 |
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')]),
|
| 298 |
-
('GRID', (0, 0), (-1, -1),
|
| 299 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 300 |
-
('BOTTOMPADDING', (0, 0), (-1, -1),
|
| 301 |
-
('TOPPADDING', (0, 0), (-1, -1),
|
|
|
|
|
|
|
| 302 |
]))
|
| 303 |
|
| 304 |
story.append(results_table)
|
| 305 |
-
story.append(Spacer(1,
|
| 306 |
|
| 307 |
# Footer
|
| 308 |
footer_style = ParagraphStyle(
|
|
@@ -434,8 +496,8 @@ def handle_next_question(selected_answer: str) -> Tuple[str, Any, Any, str, Any]
|
|
| 434 |
gr.update()
|
| 435 |
)
|
| 436 |
|
| 437 |
-
def handle_generate_report(
|
| 438 |
-
"""Handle report generation"""
|
| 439 |
if not quiz_app.user_answers:
|
| 440 |
return "β No quiz results available. Please complete a quiz first.", gr.update(visible=False)
|
| 441 |
|
|
@@ -469,8 +531,13 @@ def handle_generate_report(logo_file) -> Tuple[str, Any]:
|
|
| 469 |
|
| 470 |
report_md = "\n".join(report_lines)
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
# Generate PDF
|
| 473 |
-
pdf_path = quiz_app.generate_pdf_report()
|
| 474 |
|
| 475 |
if pdf_path:
|
| 476 |
return report_md, gr.update(value=pdf_path, visible=True)
|
|
@@ -592,12 +659,13 @@ def create_app():
|
|
| 592 |
report_btn = gr.Button("π Generate Report", visible=False)
|
| 593 |
reset_btn = gr.Button("π Reset All", variant="secondary")
|
| 594 |
|
| 595 |
-
#
|
| 596 |
gr.Markdown("---")
|
| 597 |
-
gr.Markdown("###
|
| 598 |
-
|
| 599 |
-
label="Upload
|
| 600 |
-
file_types=[".png", ".jpg", ".jpeg"]
|
|
|
|
| 601 |
)
|
| 602 |
|
| 603 |
# Right Panel - Quiz Area
|
|
|
|
| 205 |
else:
|
| 206 |
return "π Needs Improvement"
|
| 207 |
|
| 208 |
+
def generate_pdf_report(self, photo_path: Optional[str] = None) -> Optional[str]:
|
| 209 |
+
"""Generate PDF report with candidate photo"""
|
| 210 |
if not self.user_answers:
|
| 211 |
return None
|
| 212 |
|
|
|
|
| 220 |
temp_file.close()
|
| 221 |
|
| 222 |
# Create PDF
|
| 223 |
+
doc = SimpleDocTemplate(
|
| 224 |
+
temp_file.name,
|
| 225 |
+
pagesize=A4,
|
| 226 |
+
leftMargin=25*mm,
|
| 227 |
+
rightMargin=25*mm,
|
| 228 |
+
topMargin=25*mm,
|
| 229 |
+
bottomMargin=25*mm
|
| 230 |
+
)
|
| 231 |
styles = getSampleStyleSheet()
|
| 232 |
story = []
|
| 233 |
|
|
|
|
| 250 |
spaceAfter=20
|
| 251 |
)
|
| 252 |
|
| 253 |
+
# Header with candidate photo
|
| 254 |
+
from reportlab.platypus import Image
|
| 255 |
+
header_data = []
|
| 256 |
+
|
| 257 |
+
if photo_path and os.path.exists(photo_path):
|
| 258 |
+
try:
|
| 259 |
+
# Create header table with photo
|
| 260 |
+
photo_img = Image(photo_path, width=60, height=60)
|
| 261 |
+
header_info = [
|
| 262 |
+
"CCC Plus - Computer Concepts Plus",
|
| 263 |
+
"Quiz Assessment Report",
|
| 264 |
+
f"Candidate: {self.user_name}",
|
| 265 |
+
datetime.now().strftime('%d %B %Y at %H:%M:%S')
|
| 266 |
+
]
|
| 267 |
+
|
| 268 |
+
header_table = Table([
|
| 269 |
+
[photo_img, Paragraph("<br/>".join(header_info), styles['Normal'])]
|
| 270 |
+
], colWidths=[80, 400])
|
| 271 |
+
|
| 272 |
+
header_table.setStyle(TableStyle([
|
| 273 |
+
('ALIGN', (0, 0), (0, 0), 'CENTER'),
|
| 274 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 275 |
+
('FONTSIZE', (1, 0), (1, 0), 12),
|
| 276 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
|
| 277 |
+
]))
|
| 278 |
+
|
| 279 |
+
story.append(header_table)
|
| 280 |
+
|
| 281 |
+
except Exception as e:
|
| 282 |
+
logger.warning(f"Could not add photo to PDF: {e}")
|
| 283 |
+
# Fallback to text header
|
| 284 |
+
story.append(Paragraph("CCC Plus - Computer Concepts Plus", title_style))
|
| 285 |
+
story.append(Paragraph("Quiz Assessment Report", subtitle_style))
|
| 286 |
+
else:
|
| 287 |
+
# Text-only header
|
| 288 |
+
story.append(Paragraph("CCC Plus - Computer Concepts Plus", title_style))
|
| 289 |
+
story.append(Paragraph("Quiz Assessment Report", subtitle_style))
|
| 290 |
+
|
| 291 |
+
story.append(Spacer(1, 20))
|
| 292 |
|
| 293 |
# Candidate info
|
| 294 |
info_data = [
|
|
|
|
| 311 |
story.append(info_table)
|
| 312 |
story.append(Spacer(1, 20))
|
| 313 |
|
| 314 |
+
# Results table with proper text wrapping
|
| 315 |
story.append(Paragraph("Detailed Results", styles['Heading2']))
|
| 316 |
story.append(Spacer(1, 12))
|
| 317 |
|
| 318 |
table_data = [['#', 'Question', 'Your Answer', 'Correct Answer', 'Result']]
|
| 319 |
|
| 320 |
+
# Custom paragraph style for table cells
|
| 321 |
+
cell_style = ParagraphStyle(
|
| 322 |
+
'CellStyle',
|
| 323 |
+
parent=styles['Normal'],
|
| 324 |
+
fontSize=8,
|
| 325 |
+
leading=10,
|
| 326 |
+
wordWrap='LTR',
|
| 327 |
+
leftIndent=2,
|
| 328 |
+
rightIndent=2
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
for i, answer in enumerate(self.user_answers, 1):
|
| 332 |
result_icon = 'β' if answer['is_correct'] else 'β'
|
| 333 |
+
|
| 334 |
+
# Wrap text in Paragraph objects for proper text wrapping
|
| 335 |
+
question_para = Paragraph(answer['question'], cell_style)
|
| 336 |
+
selected_para = Paragraph(answer['selected'], cell_style)
|
| 337 |
+
correct_para = Paragraph(answer['correct_answer'], cell_style)
|
| 338 |
|
| 339 |
table_data.append([
|
| 340 |
str(i),
|
| 341 |
+
question_para,
|
| 342 |
+
selected_para,
|
| 343 |
+
correct_para,
|
| 344 |
result_icon
|
| 345 |
])
|
| 346 |
|
| 347 |
+
# Adjust column widths for better fit
|
| 348 |
+
results_table = Table(table_data, colWidths=[15*mm, 90*mm, 35*mm, 35*mm, 15*mm])
|
| 349 |
results_table.setStyle(TableStyle([
|
| 350 |
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#003366')),
|
| 351 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 352 |
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 353 |
+
('ALIGN', (0, 0), (0, -1), 'CENTER'), # Center align the # column
|
| 354 |
+
('ALIGN', (-1, 0), (-1, -1), 'CENTER'), # Center align the Result column
|
| 355 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 356 |
+
('FONTSIZE', (0, 0), (-1, 0), 9),
|
| 357 |
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')]),
|
| 358 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#dee2e6')),
|
| 359 |
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 360 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
| 361 |
+
('TOPPADDING', (0, 0), (-1, -1), 6),
|
| 362 |
+
('LEFTPADDING', (0, 0), (-1, -1), 4),
|
| 363 |
+
('RIGHTPADDING', (0, 0), (-1, -1), 4),
|
| 364 |
]))
|
| 365 |
|
| 366 |
story.append(results_table)
|
| 367 |
+
story.append(Spacer(1, 30))
|
| 368 |
|
| 369 |
# Footer
|
| 370 |
footer_style = ParagraphStyle(
|
|
|
|
| 496 |
gr.update()
|
| 497 |
)
|
| 498 |
|
| 499 |
+
def handle_generate_report(photo_file) -> Tuple[str, Any]:
|
| 500 |
+
"""Handle report generation with candidate photo"""
|
| 501 |
if not quiz_app.user_answers:
|
| 502 |
return "β No quiz results available. Please complete a quiz first.", gr.update(visible=False)
|
| 503 |
|
|
|
|
| 531 |
|
| 532 |
report_md = "\n".join(report_lines)
|
| 533 |
|
| 534 |
+
# Extract photo path
|
| 535 |
+
photo_path = None
|
| 536 |
+
if photo_file is not None:
|
| 537 |
+
photo_path = photo_file.name if hasattr(photo_file, 'name') else str(photo_file)
|
| 538 |
+
|
| 539 |
# Generate PDF
|
| 540 |
+
pdf_path = quiz_app.generate_pdf_report(photo_path)
|
| 541 |
|
| 542 |
if pdf_path:
|
| 543 |
return report_md, gr.update(value=pdf_path, visible=True)
|
|
|
|
| 659 |
report_btn = gr.Button("π Generate Report", visible=False)
|
| 660 |
reset_btn = gr.Button("π Reset All", variant="secondary")
|
| 661 |
|
| 662 |
+
# Candidate photo upload
|
| 663 |
gr.Markdown("---")
|
| 664 |
+
gr.Markdown("### πΈ Candidate Photo (Optional)")
|
| 665 |
+
photo_upload = gr.File(
|
| 666 |
+
label="Upload Candidate Photo",
|
| 667 |
+
file_types=[".png", ".jpg", ".jpeg"],
|
| 668 |
+
info="Photo will be included in the PDF report"
|
| 669 |
)
|
| 670 |
|
| 671 |
# Right Panel - Quiz Area
|