QuizGenerator 0.7.0__py3-none-any.whl → 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QuizGenerator/regenerate.py +114 -13
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.7.1.dist-info}/METADATA +1 -1
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.7.1.dist-info}/RECORD +6 -6
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.7.1.dist-info}/WHEEL +0 -0
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.7.1.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.7.0.dist-info → quizgenerator-0.7.1.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/regenerate.py
CHANGED
|
@@ -39,12 +39,13 @@ the exact question and answer without needing the original exam file.
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
import argparse
|
|
42
|
+
import base64
|
|
42
43
|
import json
|
|
43
44
|
import sys
|
|
44
45
|
import logging
|
|
45
46
|
import os
|
|
46
47
|
from pathlib import Path
|
|
47
|
-
from typing import Dict, Any, Optional, List
|
|
48
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
48
49
|
|
|
49
50
|
# Load environment variables from .env file
|
|
50
51
|
try:
|
|
@@ -140,7 +141,39 @@ def parse_qr_data(qr_string: str) -> Dict[str, Any]:
|
|
|
140
141
|
return {}
|
|
141
142
|
|
|
142
143
|
|
|
143
|
-
def
|
|
144
|
+
def _inline_image_upload(img_data) -> str:
|
|
145
|
+
img_data.seek(0)
|
|
146
|
+
b64 = base64.b64encode(img_data.read()).decode("ascii")
|
|
147
|
+
return f"data:image/png;base64,{b64}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _resolve_upload_func(
|
|
151
|
+
image_mode: str,
|
|
152
|
+
upload_func: Optional[Callable]
|
|
153
|
+
) -> Optional[Callable]:
|
|
154
|
+
if image_mode == "inline":
|
|
155
|
+
return _inline_image_upload
|
|
156
|
+
if image_mode == "upload":
|
|
157
|
+
if upload_func is None:
|
|
158
|
+
raise ValueError("image_mode='upload' requires upload_func")
|
|
159
|
+
return upload_func
|
|
160
|
+
if image_mode == "none":
|
|
161
|
+
return None
|
|
162
|
+
raise ValueError(f"Unknown image_mode: {image_mode}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_html(element, upload_func=None, **kwargs) -> str:
|
|
166
|
+
if upload_func is None:
|
|
167
|
+
return element.render("html", **kwargs)
|
|
168
|
+
return element.render("html", upload_func=upload_func, **kwargs)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def regenerate_question_answer(
|
|
172
|
+
qr_data: Dict[str, Any],
|
|
173
|
+
*,
|
|
174
|
+
image_mode: str = "inline",
|
|
175
|
+
upload_func: Optional[Callable] = None
|
|
176
|
+
) -> Optional[Dict[str, Any]]:
|
|
144
177
|
"""
|
|
145
178
|
Regenerate question and extract answer using QR code metadata.
|
|
146
179
|
|
|
@@ -156,8 +189,9 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
156
189
|
"seed": int,
|
|
157
190
|
"version": str,
|
|
158
191
|
"answers": dict,
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
"explanation_markdown": str | None # Markdown explanation (None if not available)
|
|
193
|
+
"explanation_html": str | None # HTML explanation (None if not available)
|
|
194
|
+
}
|
|
161
195
|
"""
|
|
162
196
|
question_num = qr_data.get('q')
|
|
163
197
|
points = qr_data.get('pts')
|
|
@@ -220,8 +254,14 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
220
254
|
# Also store the raw answer objects for easier access
|
|
221
255
|
result['answer_objects'] = question.answers
|
|
222
256
|
|
|
257
|
+
resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
|
|
258
|
+
|
|
223
259
|
# Generate HTML answer key for grading
|
|
224
|
-
question_html =
|
|
260
|
+
question_html = _render_html(
|
|
261
|
+
question_ast.body,
|
|
262
|
+
show_answers=True,
|
|
263
|
+
upload_func=resolved_upload_func
|
|
264
|
+
)
|
|
225
265
|
result['answer_key_html'] = question_html
|
|
226
266
|
|
|
227
267
|
# Generate markdown explanation for students
|
|
@@ -232,6 +272,16 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
232
272
|
else:
|
|
233
273
|
result['explanation_markdown'] = explanation_markdown
|
|
234
274
|
|
|
275
|
+
# Generate HTML explanation (optional for web UIs)
|
|
276
|
+
explanation_html = _render_html(
|
|
277
|
+
question_ast.explanation,
|
|
278
|
+
upload_func=resolved_upload_func
|
|
279
|
+
)
|
|
280
|
+
if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
|
|
281
|
+
result["explanation_html"] = None
|
|
282
|
+
else:
|
|
283
|
+
result["explanation_html"] = explanation_html
|
|
284
|
+
|
|
235
285
|
log.info(f" Successfully regenerated question with {len(canvas_answers)} answer(s)")
|
|
236
286
|
|
|
237
287
|
return result
|
|
@@ -243,7 +293,13 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
243
293
|
return result
|
|
244
294
|
|
|
245
295
|
|
|
246
|
-
def regenerate_from_encrypted(
|
|
296
|
+
def regenerate_from_encrypted(
|
|
297
|
+
encrypted_data: str,
|
|
298
|
+
points: float = 1.0,
|
|
299
|
+
*,
|
|
300
|
+
image_mode: str = "inline",
|
|
301
|
+
upload_func: Optional[Callable] = None
|
|
302
|
+
) -> Dict[str, Any]:
|
|
247
303
|
"""
|
|
248
304
|
Regenerate question answers from encrypted QR code data (RECOMMENDED API).
|
|
249
305
|
|
|
@@ -253,6 +309,8 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
|
|
|
253
309
|
Args:
|
|
254
310
|
encrypted_data: The encrypted 's' field from the QR code JSON
|
|
255
311
|
points: Point value for the question (default: 1.0)
|
|
312
|
+
image_mode: "inline", "upload", or "none" for HTML image handling
|
|
313
|
+
upload_func: Optional upload function used when image_mode="upload"
|
|
256
314
|
|
|
257
315
|
Returns:
|
|
258
316
|
Dictionary with regenerated answers:
|
|
@@ -266,6 +324,7 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
|
|
|
266
324
|
"answer_objects": dict, # Raw Answer objects with values/tolerances
|
|
267
325
|
"answer_key_html": str, # HTML rendering of question with answers shown
|
|
268
326
|
"explanation_markdown": str | None # Markdown explanation (None if not available)
|
|
327
|
+
"explanation_html": str | None # HTML explanation (None if not available)
|
|
269
328
|
}
|
|
270
329
|
|
|
271
330
|
Raises:
|
|
@@ -287,12 +346,21 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
|
|
|
287
346
|
kwargs = decrypted.get('config', {})
|
|
288
347
|
|
|
289
348
|
# Use the existing regeneration logic
|
|
290
|
-
return regenerate_from_metadata(
|
|
349
|
+
return regenerate_from_metadata(
|
|
350
|
+
question_type,
|
|
351
|
+
seed,
|
|
352
|
+
version,
|
|
353
|
+
points,
|
|
354
|
+
kwargs,
|
|
355
|
+
image_mode=image_mode,
|
|
356
|
+
upload_func=upload_func
|
|
357
|
+
)
|
|
291
358
|
|
|
292
359
|
|
|
293
360
|
def regenerate_from_metadata(
|
|
294
361
|
question_type: str, seed: int, version: str,
|
|
295
|
-
points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None
|
|
362
|
+
points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None,
|
|
363
|
+
*, image_mode: str = "inline", upload_func: Optional[Callable] = None
|
|
296
364
|
) -> Dict[str, Any]:
|
|
297
365
|
"""
|
|
298
366
|
Regenerate question answers from explicit metadata fields.
|
|
@@ -306,6 +374,8 @@ def regenerate_from_metadata(
|
|
|
306
374
|
points: Point value for the question (default: 1.0)
|
|
307
375
|
kwargs: Optional dictionary of question-specific configuration parameters
|
|
308
376
|
(e.g., {"num_bits_va": 32, "max_value": 100})
|
|
377
|
+
image_mode: "inline", "upload", or "none" for HTML image handling
|
|
378
|
+
upload_func: Optional upload function used when image_mode="upload"
|
|
309
379
|
|
|
310
380
|
Returns:
|
|
311
381
|
Dictionary with regenerated answers (same format as regenerate_from_encrypted)
|
|
@@ -335,8 +405,14 @@ def regenerate_from_metadata(
|
|
|
335
405
|
# Extract answers
|
|
336
406
|
answer_kind, canvas_answers = question.get_answers()
|
|
337
407
|
|
|
408
|
+
resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
|
|
409
|
+
|
|
338
410
|
# Generate HTML answer key for grading
|
|
339
|
-
question_html =
|
|
411
|
+
question_html = _render_html(
|
|
412
|
+
question_ast.body,
|
|
413
|
+
show_answers=True,
|
|
414
|
+
upload_func=resolved_upload_func
|
|
415
|
+
)
|
|
340
416
|
|
|
341
417
|
# Generate markdown explanation for students
|
|
342
418
|
explanation_markdown = question_ast.explanation.render("markdown")
|
|
@@ -344,6 +420,13 @@ def regenerate_from_metadata(
|
|
|
344
420
|
if not explanation_markdown or "[Please reach out to your professor for clarification]" in explanation_markdown:
|
|
345
421
|
explanation_markdown = None
|
|
346
422
|
|
|
423
|
+
explanation_html = _render_html(
|
|
424
|
+
question_ast.explanation,
|
|
425
|
+
upload_func=resolved_upload_func
|
|
426
|
+
)
|
|
427
|
+
if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
|
|
428
|
+
explanation_html = None
|
|
429
|
+
|
|
347
430
|
result = {
|
|
348
431
|
"question_type": question_type,
|
|
349
432
|
"seed": seed,
|
|
@@ -355,7 +438,8 @@ def regenerate_from_metadata(
|
|
|
355
438
|
},
|
|
356
439
|
"answer_objects": question.answers,
|
|
357
440
|
"answer_key_html": question_html,
|
|
358
|
-
"explanation_markdown": explanation_markdown
|
|
441
|
+
"explanation_markdown": explanation_markdown,
|
|
442
|
+
"explanation_html": explanation_html
|
|
359
443
|
}
|
|
360
444
|
|
|
361
445
|
# Include kwargs in result if provided
|
|
@@ -407,6 +491,9 @@ def display_answer_summary(question_data: Dict[str, Any]) -> None:
|
|
|
407
491
|
if 'explanation_markdown' in question_data and question_data['explanation_markdown'] is not None:
|
|
408
492
|
print("Markdown explanation available in result['explanation_markdown']")
|
|
409
493
|
|
|
494
|
+
if 'explanation_html' in question_data and question_data['explanation_html'] is not None:
|
|
495
|
+
print("HTML explanation available in result['explanation_html']")
|
|
496
|
+
|
|
410
497
|
print("=" * 60)
|
|
411
498
|
|
|
412
499
|
|
|
@@ -445,6 +532,12 @@ def main():
|
|
|
445
532
|
action='store_true',
|
|
446
533
|
help='Enable verbose debug logging'
|
|
447
534
|
)
|
|
535
|
+
parser.add_argument(
|
|
536
|
+
'--image-mode',
|
|
537
|
+
choices=['inline', 'none'],
|
|
538
|
+
default='inline',
|
|
539
|
+
help='HTML image handling (default: inline)'
|
|
540
|
+
)
|
|
448
541
|
|
|
449
542
|
args = parser.parse_args()
|
|
450
543
|
|
|
@@ -467,7 +560,11 @@ def main():
|
|
|
467
560
|
if args.encrypted_str:
|
|
468
561
|
try:
|
|
469
562
|
log.info(f"Decoding encrypted string (points={args.points})")
|
|
470
|
-
result = regenerate_from_encrypted(
|
|
563
|
+
result = regenerate_from_encrypted(
|
|
564
|
+
args.encrypted_str,
|
|
565
|
+
args.points,
|
|
566
|
+
image_mode=args.image_mode
|
|
567
|
+
)
|
|
471
568
|
|
|
472
569
|
# Format result similar to regenerate_question_answer output
|
|
473
570
|
question_data = {
|
|
@@ -479,7 +576,8 @@ def main():
|
|
|
479
576
|
"answers": result["answers"],
|
|
480
577
|
"answer_objects": result["answer_objects"],
|
|
481
578
|
"answer_key_html": result["answer_key_html"],
|
|
482
|
-
"explanation_markdown": result.get("explanation_markdown")
|
|
579
|
+
"explanation_markdown": result.get("explanation_markdown"),
|
|
580
|
+
"explanation_html": result.get("explanation_html")
|
|
483
581
|
}
|
|
484
582
|
|
|
485
583
|
if "kwargs" in result:
|
|
@@ -517,7 +615,10 @@ def main():
|
|
|
517
615
|
continue
|
|
518
616
|
|
|
519
617
|
# Regenerate question and answer
|
|
520
|
-
question_data = regenerate_question_answer(
|
|
618
|
+
question_data = regenerate_question_answer(
|
|
619
|
+
qr_data,
|
|
620
|
+
image_mode=args.image_mode
|
|
621
|
+
)
|
|
521
622
|
|
|
522
623
|
if question_data:
|
|
523
624
|
results.append(question_data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QuizGenerator
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
|
|
5
5
|
Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
|
|
6
6
|
Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
|
|
@@ -9,7 +9,7 @@ QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,
|
|
|
9
9
|
QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
|
|
10
10
|
QuizGenerator/question.py,sha256=CqKq_-HDzETrgEr-E8fQu1sRiMZv6ZoBAjig6gy34vw,31395
|
|
11
11
|
QuizGenerator/quiz.py,sha256=4W_3xZMLx-pMzB5mn8GOhbmE-7bYiIqYLtsVYxJSdVc,21463
|
|
12
|
-
QuizGenerator/regenerate.py,sha256=
|
|
12
|
+
QuizGenerator/regenerate.py,sha256=q8fGecZXx_sMUEpaMMmS8Zp3QW_m63WE9_5g6IKtKUk,19520
|
|
13
13
|
QuizGenerator/typst_utils.py,sha256=JGQn_u5bEHd8HAtjAHuZoVJwLkx-Rd4ZCBWffwFZa3o,3136
|
|
14
14
|
QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
|
|
15
15
|
QuizGenerator/canvas/canvas_interface.py,sha256=StMcdXgLvTA1EayQ44m_le2GXGQpDQnduYXVeUYsqW0,24618
|
|
@@ -43,8 +43,8 @@ QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=
|
|
|
43
43
|
QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=LTsUZqdP5zdG8VB10u5FYkQMm2Q1wvlaFAOOVQQesmg,45176
|
|
44
44
|
QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
|
|
45
45
|
QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=KcYSDQQfbZcwNJQrBbcAmqIp69pYro8bJAWa1djDGsE,32263
|
|
46
|
-
quizgenerator-0.7.
|
|
47
|
-
quizgenerator-0.7.
|
|
48
|
-
quizgenerator-0.7.
|
|
49
|
-
quizgenerator-0.7.
|
|
50
|
-
quizgenerator-0.7.
|
|
46
|
+
quizgenerator-0.7.1.dist-info/METADATA,sha256=OvjwBmgGzYvr9VhlQ2zgi-UzKdCY7qRn-h6VjmZZ4r4,7212
|
|
47
|
+
quizgenerator-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
quizgenerator-0.7.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
|
|
49
|
+
quizgenerator-0.7.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
50
|
+
quizgenerator-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|