LovnishVerma commited on
Commit
08f56bc
·
verified ·
1 Parent(s): 86afc36

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +413 -0
app.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CCC Plus Quiz App — Gradio
3
+ Features:
4
+ - Load quiz from CSV (upload or default `quiz.csv`)
5
+ - Interactive Q/A with immediate feedback
6
+ - Progress bar, score tracking
7
+ - Final report card with per-question breakdown
8
+ - Generate & download an "official-looking" PDF report (reportlab)
9
+ - Display NIELIT logo (if provided) and NIELIT Chandigarh copyright footer
10
+ """
11
+
12
+ import gradio as gr
13
+ import pandas as pd
14
+ import os
15
+ from datetime import datetime
16
+ from reportlab.lib.pagesizes import A4
17
+ from reportlab.lib import colors
18
+ from reportlab.lib.units import mm
19
+ from reportlab.pdfgen import canvas
20
+ from reportlab.platypus import Table, TableStyle, Paragraph, Image, Spacer
21
+ from reportlab.lib.styles import getSampleStyleSheet
22
+
23
+ # ---------------------------
24
+ # Helper: Load quiz from CSV
25
+ # ---------------------------
26
+ def load_quiz_from_path(path):
27
+ df = pd.read_csv(path)
28
+ # Basic validation and normalization
29
+ required = {"question", "option1", "option2", "option3", "option4", "answer"}
30
+ if not required.issubset(set(df.columns)):
31
+ raise ValueError(f"CSV must contain columns: {required}")
32
+ quiz = []
33
+ for _, r in df.iterrows():
34
+ quiz.append({
35
+ "question": str(r["question"]),
36
+ "options": [str(r["option1"]), str(r["option2"]), str(r["option3"]), str(r["option4"])],
37
+ "answer": str(r["answer"])
38
+ })
39
+ return quiz
40
+
41
+ # ---------------------------
42
+ # PDF Generation
43
+ # ---------------------------
44
+ def generate_report_pdf(user_name, quiz_title, history, score, logo_path=None):
45
+ """
46
+ history: list of dicts: [{"question":..., "selected":..., "answer":..., "correct": True/False}, ...]
47
+ """
48
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
49
+ filename = f"/tmp/CCCPlus_Report_{ts}.pdf"
50
+ c = canvas.Canvas(filename, pagesize=A4)
51
+ width, height = A4
52
+ margin = 25 * mm
53
+ styles = getSampleStyleSheet()
54
+ normal = styles["Normal"]
55
+ title_style = styles["Title"]
56
+ small = styles["BodyText"]
57
+ small.fontSize = 9
58
+
59
+ y = height - margin
60
+
61
+ # If logo exists, draw it at top-left
62
+ if logo_path and os.path.exists(logo_path):
63
+ try:
64
+ img_w = 35*mm
65
+ img = Image(logo_path, img_w, img_w)
66
+ img.drawOn(c, margin, y - img_w)
67
+ # Title at top center-right
68
+ x_title = margin + img_w + 10
69
+ except Exception:
70
+ x_title = margin
71
+ else:
72
+ x_title = margin
73
+
74
+ # Header text
75
+ c.setFont("Helvetica-Bold", 18)
76
+ c.drawString(x_title, y - 15, quiz_title)
77
+ c.setFont("Helvetica", 11)
78
+ c.drawString(x_title, y - 35, f"Candidate: {user_name}")
79
+ c.drawString(x_title, y - 50, f"Date: {datetime.now().strftime('%d %b %Y %H:%M:%S')}")
80
+ y -= 70
81
+
82
+ # Score big
83
+ c.setFont("Helvetica-Bold", 14)
84
+ c.drawString(margin, y, f"Final Score: {score} / {len(history)}")
85
+ y -= 25
86
+
87
+ # Build table data
88
+ table_data = [["#", "Question", "Your Answer", "Correct Answer", "Result"]]
89
+ for i, h in enumerate(history, start=1):
90
+ result = "Correct" if h["correct"] else "Incorrect"
91
+ table_data.append([str(i), h["question"], h["selected"], h["answer"], result])
92
+
93
+ # Make a table with ReportLab
94
+ table = Table(table_data, colWidths=[15*mm, 85*mm, 40*mm, 40*mm, 25*mm])
95
+ tblstyle = TableStyle([
96
+ ("GRID", (0,0), (-1,-1), 0.25, colors.grey),
97
+ ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#003366")),
98
+ ("TEXTCOLOR", (0,0), (-1,0), colors.white),
99
+ ("ALIGN", (0,0), (-1,0), "CENTER"),
100
+ ("VALIGN", (0,0), (-1,-1), "TOP"),
101
+ ("FONTSIZE", (0,0), (-1,-1), 9),
102
+ ])
103
+ table.setStyle(tblstyle)
104
+
105
+ # Calculate table height — place and wrap to next page if needed
106
+ w, h_table = table.wrapOn(c, width - 2*margin, y - margin)
107
+ if y - h_table < margin + 60:
108
+ # not enough space, start new page for table
109
+ c.showPage()
110
+ y = height - margin
111
+ if logo_path and os.path.exists(logo_path):
112
+ try:
113
+ img_w = 25*mm
114
+ img = Image(logo_path, img_w, img_w)
115
+ img.drawOn(c, margin, y - img_w)
116
+ except Exception:
117
+ pass
118
+ c.setFont("Helvetica-Bold", 14)
119
+ c.drawString(margin, y, quiz_title)
120
+ y -= 40
121
+
122
+ table.drawOn(c, margin, y - h_table)
123
+ y = y - h_table - 40
124
+
125
+ # Footer copyright
126
+ footer_text = f"NIELIT Chandigarh — © {datetime.now().year}"
127
+ c.setFont("Helvetica", 9)
128
+ c.drawCentredString(width / 2, 15 * mm, footer_text)
129
+
130
+ c.save()
131
+ return filename
132
+
133
+ # ---------------------------
134
+ # App logic functions
135
+ # ---------------------------
136
+ def prepare_quiz(csv_file, csv_upload):
137
+ """
138
+ Called when user uploads CSV or hits 'Load CSV'.
139
+ Returns initial UI state values.
140
+ """
141
+ # prefer uploaded file if present
142
+ path = None
143
+ if csv_upload is not None:
144
+ path = csv_upload.name
145
+ else:
146
+ path = csv_file if os.path.exists(csv_file) else None
147
+
148
+ if path is None:
149
+ return gr.update(value=""), gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), "No questions loaded. Upload a CSV or place quiz.csv in the app folder."
150
+
151
+ try:
152
+ quiz = load_quiz_from_path(path)
153
+ except Exception as e:
154
+ return gr.update(value=""), gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), f"Error loading CSV: {e}"
155
+
156
+ # initialize state store for quiz
157
+ store = {
158
+ "quiz": quiz,
159
+ "index": 0,
160
+ "score": 0,
161
+ "history": [], # will contain dicts {"question","selected","answer","correct"}
162
+ "started": True
163
+ }
164
+
165
+ first = quiz[0]
166
+ msg = f"Loaded {len(quiz)} questions. Ready to start!"
167
+ # Return question text, options choices, show controls, and message; store must be assigned to gr.State externally
168
+ return first["question"], gr.update(choices=first["options"], value=None), gr.update(visible=True), gr.update(visible=True), msg, store
169
+
170
+ def start_quiz(state):
171
+ # state passed in from gr.State (may be None if not loaded)
172
+ if not state or "quiz" not in state:
173
+ return "No quiz loaded. Upload a CSV.", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
174
+ state["index"] = 0
175
+ state["score"] = 0
176
+ state["history"] = []
177
+ first = state["quiz"][0]
178
+ return first["question"], gr.update(choices=first["options"], value=None), gr.update(visible=True), gr.update(visible=True), state
179
+
180
+ def submit_answer(state, selected):
181
+ """
182
+ When user submits an answer, show feedback but do NOT move to next question until user clicks Next.
183
+ We'll return feedback text and set a temporary "last" in state to hold selected answer and correctness.
184
+ """
185
+ if not state or "quiz" not in state:
186
+ return "No quiz loaded.", gr.update(visible=False), gr.update(visible=False), state
187
+
188
+ idx = state["index"]
189
+ q = state["quiz"][idx]
190
+ selected_val = selected if selected is not None else ""
191
+ correct = (selected_val == q["answer"])
192
+ # store last attempt but do not increment index yet
193
+ state["last"] = {"question": q["question"], "selected": selected_val, "answer": q["answer"], "correct": correct}
194
+
195
+ feedback = "✅ Correct!" if correct else f"❌ Incorrect. Correct answer: {q['answer']}"
196
+ # Show feedback area and enable Next button
197
+ return feedback, gr.update(visible=True), gr.update(visible=True), state
198
+
199
+ def next_q(state):
200
+ """
201
+ Advance to next question using the 'last' stored in state from submit_answer.
202
+ """
203
+ if not state or "quiz" not in state:
204
+ return "No quiz loaded.", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
205
+
206
+ # If user didn't submit before pressing next, just ignore
207
+ last = state.get("last")
208
+ if last:
209
+ state["history"].append(last)
210
+ if last["correct"]:
211
+ state["score"] += 1
212
+
213
+ # move index
214
+ state["index"] += 1
215
+ state.pop("last", None)
216
+
217
+ if state["index"] >= len(state["quiz"]):
218
+ # Quiz complete
219
+ finished_msg = f"Quiz Completed! Final Score: {state['score']} / {len(state['quiz'])}"
220
+ # hide question/option controls
221
+ return finished_msg, gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
222
+
223
+ # next question
224
+ q = state["quiz"][state["index"]]
225
+ progress = f"Question {state['index'] + 1} of {len(state['quiz'])} — Score: {state['score']}"
226
+ return q["question"], gr.update(choices=q["options"], value=None), gr.update(visible=True), gr.update(visible=True), state
227
+
228
+ def get_progress_text(state):
229
+ if not state or "quiz" not in state:
230
+ return "No quiz loaded."
231
+ return f"Question {state['index'] + 1} of {len(state['quiz'])} — Score: {state['score']}"
232
+
233
+ def show_report(state, user_name, logo_file):
234
+ """
235
+ Prepare a report summary and generate a PDF for download.
236
+ Returns: report_markdown, pdf_file_component_value
237
+ """
238
+ if not state or "history" not in state:
239
+ return "No results to show.", None
240
+
241
+ history = state["history"].copy()
242
+ # If last submitted but user hasn't pressed Next, include it too
243
+ if "last" in state:
244
+ history.append(state["last"])
245
+
246
+ # Prepare a markdown summary (display in UI)
247
+ lines = []
248
+ lines.append(f"## Report Card — {user_name if user_name else 'Candidate'}")
249
+ lines.append(f"**Date:** {datetime.now().strftime('%d %b %Y %H:%M:%S')}")
250
+ lines.append(f"**Score:** **{state['score']} / {len(state['quiz'])}**")
251
+ lines.append("")
252
+ lines.append("| # | Question | Your Answer | Correct Answer | Result |")
253
+ lines.append("|---:|---|---|---|---|")
254
+ for i, h in enumerate(history, start=1):
255
+ res = "✅" if h["correct"] else "❌"
256
+ qtext = h["question"].replace("\n", " ")
257
+ lines.append(f"| {i} | {qtext[:80]} | {h['selected']} | {h['answer']} | {res} |")
258
+
259
+ md = "\n".join(lines)
260
+
261
+ # Generate PDF
262
+ # If user uploaded a logo, use that; else try default path nielit_logo.png if present
263
+ logo_path = None
264
+ if logo_file is not None:
265
+ logo_path = logo_file.name
266
+ elif os.path.exists("nielit_logo.png"):
267
+ logo_path = "nielit_logo.png"
268
+
269
+ pdf_path = generate_report_pdf(user_name or "Candidate", "CCC Plus - Computer Concepts Plus", history, state["score"], logo_path=logo_path)
270
+ return md, pdf_path
271
+
272
+ # ---------------------------
273
+ # Build Gradio UI
274
+ # ---------------------------
275
+ with gr.Blocks(title="CCC Plus Quiz App — NIELIT Style", css="""
276
+ .header {display:flex; align-items:center; gap:12px;}
277
+ .logo-small {height:56px;}
278
+ .card {border-radius:12px; padding:14px; box-shadow: 0 6px 18px rgba(0,0,0,0.06);}
279
+ .muted {color: #666; font-size: 13px;}
280
+ """) as demo:
281
+
282
+ # top row: logo (if available) + title
283
+ with gr.Row():
284
+ with gr.Column(scale=1):
285
+ logo_img = gr.Image(value="nielit_logo.png" if os.path.exists("nielit_logo.png") else None, visible=True, elem_id="logo-img")
286
+ with gr.Column(scale=6):
287
+ gr.Markdown("<div class='header'><h2 style='margin:0'>🖥️ CCC Plus — Computer Concepts Plus</h2></div>")
288
+ gr.Markdown("<div class='muted'>Interactive quiz for practice & assessment. Upload CSV or use default quiz.csv.</div>")
289
+
290
+ gr.Markdown("---")
291
+
292
+ with gr.Row():
293
+ # Left column: controls & quiz
294
+ with gr.Column(scale=3):
295
+ gr.Markdown("### Load Questions")
296
+ csv_file_text = gr.Textbox(value="quiz.csv", label="Local CSV path (default)", interactive=True)
297
+ csv_upload = gr.File(label="Or upload CSV file", file_count="single", file_types=[".csv"])
298
+ load_btn = gr.Button("Load CSV")
299
+ load_msg = gr.Textbox(label="Status", interactive=False, lines=2)
300
+
301
+ gr.Markdown("### Candidate Info")
302
+ user_name = gr.Textbox(label="Candidate Name (to appear on report)", placeholder="Your name here")
303
+
304
+ # Start & control buttons
305
+ start_btn = gr.Button("Start / Restart Quiz", elem_id="start-btn")
306
+ restart_btn = gr.Button("Reset All", elem_id="reset-btn", visible=True)
307
+
308
+ gr.Markdown("### Controls")
309
+ submit_btn = gr.Button("Submit Answer", variant="primary", visible=True)
310
+ next_btn = gr.Button("Next Question ➡️", visible=False)
311
+ feedback = gr.Textbox(label="Feedback", interactive=False, visible=False)
312
+
313
+ # Hidden area for downloading pdf
314
+ report_download = gr.File(label="Download PDF Report", visible=False)
315
+
316
+ # Right column: question card and progress
317
+ with gr.Column(scale=4):
318
+ with gr.Card():
319
+ question_txt = gr.Markdown("**Question will appear here after you start the quiz**")
320
+ options = gr.Radio(choices=[], label="Choose one option", interactive=True)
321
+ progress = gr.Textbox(label="Progress", interactive=False)
322
+ score_display = gr.Textbox(label="Score", interactive=False)
323
+
324
+ # Report preview area
325
+ gr.Markdown("### Report Preview")
326
+ report_preview = gr.Markdown("No report yet.")
327
+
328
+ # State store
329
+ state_store = gr.State({})
330
+
331
+ # --- Event wiring ---
332
+
333
+ # Load CSV button -> loads quiz and returns initial UI values and store
334
+ def on_load(csv_path_text, uploaded_file):
335
+ # uploaded_file is a temporary file with .name attr pointing to uploaded file path
336
+ q_text, q_choices, show_submit, show_next, msg, store = prepare_quiz(csv_path_text, uploaded_file)
337
+ # We must return many outputs; order must match click outputs
338
+ return q_text, q_choices, show_submit, show_next, msg, store
339
+
340
+ load_btn.click(fn=on_load,
341
+ inputs=[csv_file_text, csv_upload],
342
+ outputs=[question_txt, options, submit_btn, next_btn, load_msg, state_store])
343
+
344
+ # Start quiz (when user clicks Start/Restart) -> reset state and show first question
345
+ def on_start(state):
346
+ if not state or "quiz" not in state:
347
+ return "No quiz loaded. Load CSV first.", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
348
+ q_text, q_choices, show_submit, show_next, new_state = start_quiz(state)
349
+ # update progress & score
350
+ return q_text, q_choices, show_submit, show_next, new_state
351
+
352
+ start_btn.click(fn=on_start, inputs=[state_store], outputs=[question_txt, options, submit_btn, next_btn, state_store])
353
+
354
+ # Submit answer -> produce feedback, show Next button
355
+ def on_submit(state, selected):
356
+ feedback_text, show_feedback, show_next_btn, new_state = submit_answer(state, selected)
357
+ return feedback_text, show_feedback, show_next_btn, new_state
358
+
359
+ submit_btn.click(fn=on_submit, inputs=[state_store, options], outputs=[feedback, feedback, next_btn, state_store])
360
+
361
+ # Next question -> advance
362
+ def on_next(state):
363
+ q_text, q_choices, show_submit, show_next, new_state = next_q(state)
364
+ # update progress and score displays
365
+ progress_text = get_progress_text(new_state)
366
+ score_text = f"{new_state['score']} / {len(new_state['quiz'])}"
367
+ # if quiz finished (no controls visible), also prepare report preview
368
+ report_md = ""
369
+ pdf_path = None
370
+ if new_state["index"] >= len(new_state["quiz"]):
371
+ # prepare a small report preview
372
+ report_md, pdf_path = show_report(new_state, "", None)
373
+ return q_text, q_choices, show_submit, show_next, new_state, progress_text, score_text, report_md, pdf_path
374
+
375
+ # Wire Next: multiple outputs
376
+ next_btn.click(fn=on_next,
377
+ inputs=[state_store],
378
+ outputs=[question_txt, options, submit_btn, next_btn, state_store, progress, score_display, report_preview, report_download])
379
+
380
+ # Show report & enable PDF generation and download button explicitly (user can press after completion)
381
+ def on_show_report(state, name, logo_file):
382
+ md, pdf_path = show_report(state, name, logo_file)
383
+ rfile = gr.File.update(value=pdf_path) if pdf_path else gr.File.update(visible=False)
384
+ # show file component and markdown preview
385
+ return md, rfile
386
+
387
+ # Button to explicitly create & show report (user may want to add name/logo)
388
+ show_report_btn = gr.Button("Generate / Refresh Report")
389
+ show_report_btn.click(fn=on_show_report, inputs=[state_store, user_name, logo_img], outputs=[report_preview, report_download])
390
+
391
+ # Reset: clear state & UI
392
+ def on_reset():
393
+ state = {}
394
+ return "", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), "", state, "", "", "", None
395
+
396
+ restart_btn.click(fn=on_reset, inputs=[], outputs=[question_txt, options, submit_btn, next_btn, load_msg, state_store, progress, score_display, report_preview, report_download])
397
+
398
+ # Update progress/score when state changes (optional)
399
+ def update_progress(state):
400
+ if not state or "quiz" not in state:
401
+ return "No quiz loaded.", "0 / 0"
402
+ return get_progress_text(state), f"{state['score']} / {len(state['quiz'])}"
403
+
404
+ # Connect state change to progress box (so user sees live progress)
405
+ state_store.change(fn=update_progress, inputs=[state_store], outputs=[progress, score_display])
406
+
407
+ # Footer
408
+ gr.Markdown("---")
409
+ gr.Markdown("NIELIT Chandigarh — © " + str(datetime.now().year))
410
+
411
+ # Run the app
412
+ if __name__ == "__main__":
413
+ demo.launch()