QuizGenerator 0.3.0__py3-none-any.whl → 0.4.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/misc.py +2 -2
- QuizGenerator/premade_questions/cst334/languages.py +9 -13
- QuizGenerator/premade_questions/cst334/persistence_questions.py +78 -23
- QuizGenerator/premade_questions/cst463/models/cnns.py +1 -1
- QuizGenerator/premade_questions/cst463/models/text.py +4 -2
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +1 -1
- QuizGenerator/regenerate.py +472 -0
- {quizgenerator-0.3.0.dist-info → quizgenerator-0.4.1.dist-info}/METADATA +1 -1
- {quizgenerator-0.3.0.dist-info → quizgenerator-0.4.1.dist-info}/RECORD +12 -11
- {quizgenerator-0.3.0.dist-info → quizgenerator-0.4.1.dist-info}/entry_points.txt +1 -0
- {quizgenerator-0.3.0.dist-info → quizgenerator-0.4.1.dist-info}/WHEEL +0 -0
- {quizgenerator-0.3.0.dist-info → quizgenerator-0.4.1.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/misc.py
CHANGED
|
@@ -564,9 +564,9 @@ class MatrixAnswer(Answer):
|
|
|
564
564
|
value=self.value[i,j]
|
|
565
565
|
)
|
|
566
566
|
)
|
|
567
|
-
for
|
|
567
|
+
for j in range(self.value.shape[1])
|
|
568
568
|
]
|
|
569
|
-
for
|
|
569
|
+
for i in range(self.value.shape[0])
|
|
570
570
|
]
|
|
571
571
|
table = ContentAST.Table(data)
|
|
572
572
|
|
|
@@ -60,11 +60,9 @@ class BNF:
|
|
|
60
60
|
|
|
61
61
|
def get_grammar_string(self):
|
|
62
62
|
lines = []
|
|
63
|
-
lines.append('```')
|
|
64
63
|
for symbol in self.symbols:
|
|
65
64
|
lines.append(f"{symbol.get_full_str()}")
|
|
66
|
-
|
|
67
|
-
lines.append('```')
|
|
65
|
+
|
|
68
66
|
return '\n'.join(lines)
|
|
69
67
|
|
|
70
68
|
class Symbol:
|
|
@@ -342,25 +340,23 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
|
|
|
342
340
|
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
343
341
|
body = ContentAST.Section()
|
|
344
342
|
|
|
345
|
-
body.
|
|
343
|
+
body.add_elements([
|
|
346
344
|
ContentAST.Paragraph([
|
|
347
|
-
ContentAST.OnlyHtml(
|
|
345
|
+
ContentAST.OnlyHtml([
|
|
348
346
|
ContentAST.Text("Given the following grammar, which of the below strings are part of the language?")
|
|
349
|
-
),
|
|
350
|
-
ContentAST.OnlyLatex(
|
|
347
|
+
]),
|
|
348
|
+
ContentAST.OnlyLatex([
|
|
351
349
|
ContentAST.Text(
|
|
352
350
|
"Given the following grammar "
|
|
353
351
|
"please circle any provided strings that are part of the language (or indicate clearly if there are none), "
|
|
354
352
|
"and on each blank line provide generate a new, unique string that is part of the language."
|
|
355
353
|
)
|
|
356
|
-
)
|
|
354
|
+
])
|
|
357
355
|
])
|
|
358
|
-
)
|
|
356
|
+
])
|
|
359
357
|
|
|
360
358
|
body.add_element(
|
|
361
|
-
ContentAST.
|
|
362
|
-
self.grammar_good.get_grammar_string()
|
|
363
|
-
])
|
|
359
|
+
ContentAST.Code(self.grammar_good.get_grammar_string())
|
|
364
360
|
)
|
|
365
361
|
|
|
366
362
|
# Add in some answers as latex-only options to be circled
|
|
@@ -374,7 +370,7 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
|
|
|
374
370
|
# For Latex-only, ask students to generate some more.
|
|
375
371
|
body.add_element(
|
|
376
372
|
ContentAST.OnlyLatex([
|
|
377
|
-
ContentAST.AnswerBlock([ContentAST.Answer() for
|
|
373
|
+
ContentAST.AnswerBlock([ContentAST.Answer(Answer.string(f"blank_line_{i}", f"blank_line_{i}"), label=f"blank_line_{i}") for i in range(self.num_answer_blanks)])
|
|
378
374
|
])
|
|
379
375
|
)
|
|
380
376
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import abc
|
|
5
|
+
import difflib
|
|
5
6
|
import logging
|
|
6
7
|
|
|
7
8
|
from QuizGenerator.question import Question, Answer, QuestionRegistry
|
|
@@ -365,31 +366,85 @@ class VSFS_states(IOQuestion):
|
|
|
365
366
|
def get_explanation(self) -> ContentAST.Section:
|
|
366
367
|
explanation = ContentAST.Section()
|
|
367
368
|
|
|
368
|
-
|
|
369
|
+
log.debug(f"self.start_state: {self.start_state}")
|
|
370
|
+
log.debug(f"self.end_state: {self.end_state}")
|
|
371
|
+
|
|
372
|
+
explanation.add_elements([
|
|
369
373
|
ContentAST.Paragraph([
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
"<a href=\"https://github.com/chyyuu/os_tutorial_lab/blob/master/ostep/ostep13-vsfs.md\">here</a>, "
|
|
373
|
-
"as well as simulator code. Please note that the code uses python 2.",
|
|
374
|
-
"",
|
|
375
|
-
"In general, I recommend looking for differences between the two outputs. Recommended steps would be:",
|
|
376
|
-
"<ol>"
|
|
377
|
-
|
|
378
|
-
"<li> Check to see if there are differences between the bitmaps "
|
|
379
|
-
"that could indicate a file/directroy were created or removed.</li>",
|
|
380
|
-
|
|
381
|
-
"<li>Check the listed inodes to see if any entries have changed. "
|
|
382
|
-
"This might be a new entry entirely or a reference count changing. "
|
|
383
|
-
"If the references increased then this was likely a link or creation, "
|
|
384
|
-
"and if it decreased then it is likely an unlink.</li>",
|
|
385
|
-
|
|
386
|
-
"<li>Look at the data blocks to see if a new entry has "
|
|
387
|
-
"been added to a directory or a new block has been mapped.</li>",
|
|
388
|
-
|
|
389
|
-
"</ol>",
|
|
390
|
-
"These steps can usually help you quickly identify "
|
|
391
|
-
"what has occured in the simulation and key you in to the right answer."
|
|
374
|
+
"The key thing to pay attention to when solving these problems is where there are differences between the start state and the end state.",
|
|
375
|
+
"In this particular problem, we can see that these lines are different:"
|
|
392
376
|
])
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
chunk_to_add = []
|
|
380
|
+
lines_that_changed = []
|
|
381
|
+
for start_line, end_line in zip(self.start_state.split('\n'), self.end_state.split('\n')):
|
|
382
|
+
if start_line == end_line:
|
|
383
|
+
continue
|
|
384
|
+
lines_that_changed.append((start_line, end_line))
|
|
385
|
+
chunk_to_add.append(
|
|
386
|
+
f" - `{start_line}` -> `{end_line}`"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
explanation.add_element(
|
|
390
|
+
ContentAST.Paragraph(chunk_to_add)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
chunk_to_add = [
|
|
394
|
+
"A great place to start is to check to see if the bitmaps have changed as this can quickly tell us a lot of information"
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
inode_bitmap_lines = list(filter(lambda s: "inode bitmap" in s[0], lines_that_changed))
|
|
398
|
+
data_bitmap_lines = list(filter(lambda s: "data bitmap" in s[0], lines_that_changed))
|
|
399
|
+
|
|
400
|
+
def get_bitmap(line: str) -> str:
|
|
401
|
+
log.debug(f"line: {line}")
|
|
402
|
+
return line.split()[-1]
|
|
403
|
+
|
|
404
|
+
def highlight_changes(a: str, b: str) -> str:
|
|
405
|
+
matcher = difflib.SequenceMatcher(None, a, b)
|
|
406
|
+
result = []
|
|
407
|
+
|
|
408
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
409
|
+
if tag == "equal":
|
|
410
|
+
result.append(b[j1:j2])
|
|
411
|
+
elif tag in ("insert", "replace"):
|
|
412
|
+
result.append(f"***{b[j1:j2]}***")
|
|
413
|
+
# for "delete", do nothing since text is removed
|
|
414
|
+
|
|
415
|
+
return "".join(result)
|
|
416
|
+
|
|
417
|
+
if len(inode_bitmap_lines) > 0:
|
|
418
|
+
inode_bitmap_lines = inode_bitmap_lines[0]
|
|
419
|
+
chunk_to_add.append(f"The inode bitmap lines have changed from {get_bitmap(inode_bitmap_lines[0])} to {get_bitmap(inode_bitmap_lines[1])}.")
|
|
420
|
+
if get_bitmap(inode_bitmap_lines[0]).count('1') < get_bitmap(inode_bitmap_lines[1]).count('1'):
|
|
421
|
+
chunk_to_add.append("We can see that we have added an inode, so we have either called `creat` or `mkdir`.")
|
|
422
|
+
else:
|
|
423
|
+
chunk_to_add.append("We can see that we have removed an inode, so we have called `unlink`.")
|
|
424
|
+
|
|
425
|
+
if len(data_bitmap_lines) > 0:
|
|
426
|
+
data_bitmap_lines = data_bitmap_lines[0]
|
|
427
|
+
chunk_to_add.append(f"The inode bitmap lines have changed from {get_bitmap(data_bitmap_lines[0])} to {get_bitmap(data_bitmap_lines[1])}.")
|
|
428
|
+
if get_bitmap(data_bitmap_lines[0]).count('1') < get_bitmap(data_bitmap_lines[1]).count('1'):
|
|
429
|
+
chunk_to_add.append("We can see that we have added a data block, so we have either called `mkdir` or `write`.")
|
|
430
|
+
else:
|
|
431
|
+
chunk_to_add.append("We can see that we have removed a data block, so we have `unlink`ed a file.")
|
|
432
|
+
|
|
433
|
+
if len(data_bitmap_lines) == 0 and len(inode_bitmap_lines) == 0:
|
|
434
|
+
chunk_to_add.append("If they have not changed, then we know we must have eithered called `link` or `unlink` and must check the references.")
|
|
435
|
+
|
|
436
|
+
explanation.add_element(
|
|
437
|
+
ContentAST.Paragraph(chunk_to_add)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
explanation.add_elements([
|
|
441
|
+
ContentAST.Paragraph(["The overall changes are highlighted with `*` symbols below"])
|
|
442
|
+
])
|
|
443
|
+
|
|
444
|
+
explanation.add_element(
|
|
445
|
+
ContentAST.Code(
|
|
446
|
+
highlight_changes(self.start_state, self.end_state)
|
|
447
|
+
)
|
|
393
448
|
)
|
|
394
449
|
|
|
395
450
|
return explanation
|
|
@@ -28,7 +28,7 @@ class ConvolutionCalculation(MatrixQuestion):
|
|
|
28
28
|
|
|
29
29
|
# Add padding
|
|
30
30
|
if padding > 0:
|
|
31
|
-
image = np.pad(image, ((padding, padding), (padding, padding)
|
|
31
|
+
image = np.pad(image, ((padding, padding), (padding, padding)), mode='constant')
|
|
32
32
|
H, W = H + 2 * padding, W + 2 * padding
|
|
33
33
|
|
|
34
34
|
# Output dimensions
|
|
@@ -60,7 +60,9 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
|
|
|
60
60
|
## Answers:
|
|
61
61
|
# center_word, center_emb, context_words, context_embs, logits, probs
|
|
62
62
|
self.answers["logits"] = Answer.vector_value(key="logits", value=self.logits)
|
|
63
|
-
|
|
63
|
+
most_likely_idx = np.argmax(self.probs)
|
|
64
|
+
most_likely_word = self.context_words[most_likely_idx]
|
|
65
|
+
self.answers["center_word"] = Answer.string(key="center_word", value=most_likely_word)
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
return True
|
|
@@ -81,7 +83,7 @@ class word2vec__skipgram(MatrixQuestion, TableQuestionMixin):
|
|
|
81
83
|
ContentAST.LineBreak(),
|
|
82
84
|
self.answers["logits"].get_ast_element("Logits"),
|
|
83
85
|
ContentAST.LineBreak(),
|
|
84
|
-
self.answers["center_word"].get_ast_element("
|
|
86
|
+
self.answers["center_word"].get_ast_element("Most likely context word")
|
|
85
87
|
])
|
|
86
88
|
|
|
87
89
|
|
|
@@ -194,7 +194,7 @@ class WeightCounting_CNN(WeightCounting):
|
|
|
194
194
|
)
|
|
195
195
|
]
|
|
196
196
|
)
|
|
197
|
-
return model, ["filters", "kernel_size", "strides", "padding", "pool_size"]
|
|
197
|
+
return model, ["units", "filters", "kernel_size", "strides", "padding", "pool_size"]
|
|
198
198
|
|
|
199
199
|
|
|
200
200
|
@QuestionRegistry.register("cst463.WeightCounting-RNN")
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
QR Code-based Grading Utility
|
|
4
|
+
|
|
5
|
+
This script scans QR codes from quiz PDFs to regenerate question answers
|
|
6
|
+
for grading. It supports both scanning from image files and interactive
|
|
7
|
+
scanning from webcam/scanner.
|
|
8
|
+
|
|
9
|
+
CLI Usage:
|
|
10
|
+
# Scan a single QR code image
|
|
11
|
+
python grade_from_qr.py --image qr_code.png
|
|
12
|
+
|
|
13
|
+
# Scan QR codes from a scanned exam page
|
|
14
|
+
python grade_from_qr.py --image exam_page.jpg --all
|
|
15
|
+
|
|
16
|
+
API Usage (recommended for web UIs):
|
|
17
|
+
from grade_from_qr import regenerate_from_encrypted
|
|
18
|
+
|
|
19
|
+
# Parse QR code JSON
|
|
20
|
+
qr_data = json.loads(qr_string)
|
|
21
|
+
|
|
22
|
+
# Regenerate answers from encrypted data (one function call!)
|
|
23
|
+
result = regenerate_from_encrypted(
|
|
24
|
+
encrypted_data=qr_data['s'],
|
|
25
|
+
points=qr_data['pts']
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Display HTML answer key to grader
|
|
29
|
+
print(result['answer_key_html'])
|
|
30
|
+
|
|
31
|
+
The QR codes contain encrypted question metadata that allows regenerating
|
|
32
|
+
the exact question and answer without needing the original exam file.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import sys
|
|
38
|
+
import logging
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Dict, Any, Optional, List
|
|
41
|
+
|
|
42
|
+
# Quiz generator imports (always available)
|
|
43
|
+
from QuizGenerator.qrcode_generator import QuestionQRCode
|
|
44
|
+
from QuizGenerator.question import QuestionRegistry
|
|
45
|
+
|
|
46
|
+
# QR code reading (optional - only needed for CLI usage with --image)
|
|
47
|
+
# Your web UI should use its own QR decoding library
|
|
48
|
+
try:
|
|
49
|
+
from pyzbar import pyzbar
|
|
50
|
+
from PIL import Image
|
|
51
|
+
|
|
52
|
+
HAS_IMAGE_SUPPORT = True
|
|
53
|
+
except ImportError:
|
|
54
|
+
HAS_IMAGE_SUPPORT = False
|
|
55
|
+
# Don't fail immediately - only fail if user tries to use --image flag
|
|
56
|
+
|
|
57
|
+
logging.basicConfig(
|
|
58
|
+
level=logging.INFO,
|
|
59
|
+
format='%(levelname)s: %(message)s'
|
|
60
|
+
)
|
|
61
|
+
log = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def scan_qr_from_image(image_path: str) -> List[str]:
|
|
65
|
+
"""
|
|
66
|
+
Scan all QR codes from an image file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
image_path: Path to image file containing QR code(s)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of decoded QR code data strings
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ImportError: If pyzbar/PIL are not installed
|
|
76
|
+
"""
|
|
77
|
+
if not HAS_IMAGE_SUPPORT:
|
|
78
|
+
raise ImportError(
|
|
79
|
+
"Image support not available. Install with: pip install pyzbar pillow"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
img = Image.open(image_path)
|
|
84
|
+
decoded_objects = pyzbar.decode(img)
|
|
85
|
+
|
|
86
|
+
if not decoded_objects:
|
|
87
|
+
log.warning(f"No QR codes found in {image_path}")
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
qr_data = [obj.data.decode('utf-8') for obj in decoded_objects]
|
|
91
|
+
log.info(f"Found {len(qr_data)} QR code(s) in {image_path}")
|
|
92
|
+
|
|
93
|
+
return qr_data
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
log.error(f"Failed to read image {image_path}: {e}")
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_qr_data(qr_string: str) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Parse QR code JSON data.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
qr_string: JSON string from QR code
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary with question metadata
|
|
109
|
+
{
|
|
110
|
+
"q": question_number,
|
|
111
|
+
"pts": points_value,
|
|
112
|
+
"s": encrypted_seed_data (optional)
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
data = json.loads(qr_string)
|
|
117
|
+
log.debug(f"Parsed QR data: {data}")
|
|
118
|
+
return data
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
log.error(f"Failed to parse QR code JSON: {e}")
|
|
121
|
+
return {}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
125
|
+
"""
|
|
126
|
+
Regenerate question and extract answer using QR code metadata.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
qr_data: Parsed QR code data dictionary
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dictionary with question info and answers, or None if regeneration fails
|
|
133
|
+
{
|
|
134
|
+
"question_number": int,
|
|
135
|
+
"points": float,
|
|
136
|
+
"question_type": str,
|
|
137
|
+
"seed": int,
|
|
138
|
+
"version": str,
|
|
139
|
+
"answers": dict,
|
|
140
|
+
"explanation_markdown": str | None # Markdown explanation (None if not available)
|
|
141
|
+
}
|
|
142
|
+
"""
|
|
143
|
+
question_num = qr_data.get('q')
|
|
144
|
+
points = qr_data.get('pts')
|
|
145
|
+
|
|
146
|
+
if question_num is None or points is None:
|
|
147
|
+
log.error("QR code missing required fields 'q' or 'pts'")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
result = {
|
|
151
|
+
"question_number": question_num,
|
|
152
|
+
"points": points
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Check if encrypted regeneration data is present
|
|
156
|
+
encrypted_data = qr_data.get('s')
|
|
157
|
+
if not encrypted_data:
|
|
158
|
+
log.warning(f"Question {question_num}: No regeneration data in QR code")
|
|
159
|
+
log.warning(" This question cannot be automatically regenerated.")
|
|
160
|
+
log.warning(" (QR codes generated before encryption feature was added)")
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Decrypt the regeneration data
|
|
165
|
+
regen_data = QuestionQRCode.decrypt_question_data(encrypted_data)
|
|
166
|
+
|
|
167
|
+
question_type = regen_data['question_type']
|
|
168
|
+
seed = regen_data['seed']
|
|
169
|
+
version = regen_data['version']
|
|
170
|
+
config = regen_data.get('config', {})
|
|
171
|
+
|
|
172
|
+
result['question_type'] = question_type
|
|
173
|
+
result['seed'] = seed
|
|
174
|
+
result['version'] = version
|
|
175
|
+
if config:
|
|
176
|
+
result['config'] = config
|
|
177
|
+
|
|
178
|
+
log.info(f"Question {question_num}: {question_type} (seed={seed}, version={version})")
|
|
179
|
+
if config:
|
|
180
|
+
log.debug(f" Config params: {config}")
|
|
181
|
+
|
|
182
|
+
# Regenerate the question using the registry, passing through config params
|
|
183
|
+
question = QuestionRegistry.create(
|
|
184
|
+
question_type,
|
|
185
|
+
name=f"Q{question_num}",
|
|
186
|
+
points_value=points,
|
|
187
|
+
**config
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Generate question with the specific seed
|
|
191
|
+
question_ast = question.get_question(rng_seed=seed)
|
|
192
|
+
|
|
193
|
+
# Extract answers
|
|
194
|
+
answer_kind, canvas_answers = question.get_answers()
|
|
195
|
+
|
|
196
|
+
result['answers'] = {
|
|
197
|
+
'kind': answer_kind.value,
|
|
198
|
+
'data': canvas_answers
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Also store the raw answer objects for easier access
|
|
202
|
+
result['answer_objects'] = question.answers
|
|
203
|
+
|
|
204
|
+
# Generate HTML answer key for grading
|
|
205
|
+
question_html = question_ast.body.render("html", show_answers=True)
|
|
206
|
+
result['answer_key_html'] = question_html
|
|
207
|
+
|
|
208
|
+
# Generate markdown explanation for students
|
|
209
|
+
explanation_markdown = question_ast.explanation.render("markdown")
|
|
210
|
+
# Return None if explanation is empty or contains the default placeholder
|
|
211
|
+
if not explanation_markdown or "[Please reach out to your professor for clarification]" in explanation_markdown:
|
|
212
|
+
result['explanation_markdown'] = None
|
|
213
|
+
else:
|
|
214
|
+
result['explanation_markdown'] = explanation_markdown
|
|
215
|
+
|
|
216
|
+
log.info(f" Successfully regenerated question with {len(canvas_answers)} answer(s)")
|
|
217
|
+
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
log.error(f"Failed to regenerate question {question_num}: {e}")
|
|
222
|
+
import traceback
|
|
223
|
+
log.debug(traceback.format_exc())
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[str, Any]:
|
|
228
|
+
"""
|
|
229
|
+
Regenerate question answers from encrypted QR code data (RECOMMENDED API).
|
|
230
|
+
|
|
231
|
+
This is the simplest function for web UI integration - just pass the encrypted
|
|
232
|
+
string from the QR code and get back the complete answer key.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
encrypted_data: The encrypted 's' field from the QR code JSON
|
|
236
|
+
points: Point value for the question (default: 1.0)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dictionary with regenerated answers:
|
|
240
|
+
{
|
|
241
|
+
"question_type": str,
|
|
242
|
+
"seed": int,
|
|
243
|
+
"version": str,
|
|
244
|
+
"points": float,
|
|
245
|
+
"kwargs": dict, # Question-specific config params (if any)
|
|
246
|
+
"answers": dict, # Canvas-formatted answers
|
|
247
|
+
"answer_objects": dict, # Raw Answer objects with values/tolerances
|
|
248
|
+
"answer_key_html": str, # HTML rendering of question with answers shown
|
|
249
|
+
"explanation_markdown": str | None # Markdown explanation (None if not available)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ValueError: If decryption fails or question regeneration fails
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
>>> # Your web UI scans QR code and gets JSON: {"q": 1, "pts": 5, "s": "gAAAAAB..."}
|
|
257
|
+
>>> encrypted_string = qr_json['s']
|
|
258
|
+
>>> result = regenerate_from_encrypted(encrypted_string, points=qr_json['pts'])
|
|
259
|
+
>>> print(result['answer_key_html']) # Display to grader!
|
|
260
|
+
"""
|
|
261
|
+
# Decrypt the data
|
|
262
|
+
decrypted = QuestionQRCode.decrypt_question_data(encrypted_data)
|
|
263
|
+
|
|
264
|
+
# Extract fields
|
|
265
|
+
question_type = decrypted['question_type']
|
|
266
|
+
seed = decrypted['seed']
|
|
267
|
+
version = decrypted['version']
|
|
268
|
+
kwargs = decrypted.get('config', {})
|
|
269
|
+
|
|
270
|
+
# Use the existing regeneration logic
|
|
271
|
+
return regenerate_from_metadata(question_type, seed, version, points, kwargs)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def regenerate_from_metadata(
|
|
275
|
+
question_type: str, seed: int, version: str,
|
|
276
|
+
points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None
|
|
277
|
+
) -> Dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Regenerate question answers from explicit metadata fields.
|
|
280
|
+
|
|
281
|
+
This is a lower-level function. Most users should use regenerate_from_encrypted() instead.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
question_type: Question class name (e.g., "VirtualAddressParts")
|
|
285
|
+
seed: Random seed used to generate the question
|
|
286
|
+
version: Question version string (e.g., "1.0")
|
|
287
|
+
points: Point value for the question (default: 1.0)
|
|
288
|
+
kwargs: Optional dictionary of question-specific configuration parameters
|
|
289
|
+
(e.g., {"num_bits_va": 32, "max_value": 100})
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Dictionary with regenerated answers (same format as regenerate_from_encrypted)
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
ValueError: If question type is not registered or regeneration fails
|
|
296
|
+
"""
|
|
297
|
+
if kwargs is None:
|
|
298
|
+
kwargs = {}
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
log.info(f"Regenerating: {question_type} (seed={seed}, version={version})")
|
|
302
|
+
if kwargs:
|
|
303
|
+
log.debug(f" Config params: {kwargs}")
|
|
304
|
+
|
|
305
|
+
# Create question instance from registry, passing through kwargs
|
|
306
|
+
question = QuestionRegistry.create(
|
|
307
|
+
question_type,
|
|
308
|
+
name=f"Q_{question_type}_{seed}",
|
|
309
|
+
points_value=points,
|
|
310
|
+
**kwargs
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Generate question with the specific seed
|
|
314
|
+
question_ast = question.get_question(rng_seed=seed)
|
|
315
|
+
|
|
316
|
+
# Extract answers
|
|
317
|
+
answer_kind, canvas_answers = question.get_answers()
|
|
318
|
+
|
|
319
|
+
# Generate HTML answer key for grading
|
|
320
|
+
question_html = question_ast.body.render("html", show_answers=True)
|
|
321
|
+
|
|
322
|
+
# Generate markdown explanation for students
|
|
323
|
+
explanation_markdown = question_ast.explanation.render("markdown")
|
|
324
|
+
# Return None if explanation is empty or contains the default placeholder
|
|
325
|
+
if not explanation_markdown or "[Please reach out to your professor for clarification]" in explanation_markdown:
|
|
326
|
+
explanation_markdown = None
|
|
327
|
+
|
|
328
|
+
result = {
|
|
329
|
+
"question_type": question_type,
|
|
330
|
+
"seed": seed,
|
|
331
|
+
"version": version,
|
|
332
|
+
"points": points,
|
|
333
|
+
"answers": {
|
|
334
|
+
"kind": answer_kind.value,
|
|
335
|
+
"data": canvas_answers
|
|
336
|
+
},
|
|
337
|
+
"answer_objects": question.answers,
|
|
338
|
+
"answer_key_html": question_html,
|
|
339
|
+
"explanation_markdown": explanation_markdown
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# Include kwargs in result if provided
|
|
343
|
+
if kwargs:
|
|
344
|
+
result["kwargs"] = kwargs
|
|
345
|
+
|
|
346
|
+
log.info(f" Successfully regenerated with {len(canvas_answers)} answer(s)")
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
log.error(f"Failed to regenerate question: {e}")
|
|
351
|
+
import traceback
|
|
352
|
+
log.debug(traceback.format_exc())
|
|
353
|
+
raise ValueError(f"Failed to regenerate question {question_type}: {e}")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def display_answer_summary(question_data: Dict[str, Any]) -> None:
|
|
357
|
+
"""
|
|
358
|
+
Display a formatted summary of the question and its answer(s).
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
question_data: Question data dictionary from regenerate_question_answer
|
|
362
|
+
"""
|
|
363
|
+
print("\n" + "=" * 60)
|
|
364
|
+
print(f"Question {question_data['question_number']}: {question_data.get('points', '?')} points")
|
|
365
|
+
|
|
366
|
+
if 'question_type' in question_data:
|
|
367
|
+
print(f"Type: {question_data['question_type']}")
|
|
368
|
+
print(f"Seed: {question_data['seed']}")
|
|
369
|
+
print(f"Version: {question_data['version']}")
|
|
370
|
+
|
|
371
|
+
if 'answer_objects' in question_data:
|
|
372
|
+
print("\nANSWERS:")
|
|
373
|
+
for key, answer_obj in question_data['answer_objects'].items():
|
|
374
|
+
print(f" {key}: {answer_obj.value}")
|
|
375
|
+
if hasattr(answer_obj, 'tolerance') and answer_obj.tolerance:
|
|
376
|
+
print(f" (tolerance: ±{answer_obj.tolerance})")
|
|
377
|
+
elif 'answers' in question_data:
|
|
378
|
+
print("\nANSWERS (raw Canvas format):")
|
|
379
|
+
print(f" Type: {question_data['answers']['kind']}")
|
|
380
|
+
for ans in question_data['answers']['data']:
|
|
381
|
+
print(f" - {ans}")
|
|
382
|
+
else:
|
|
383
|
+
print("\n(No regeneration data available)")
|
|
384
|
+
|
|
385
|
+
if 'answer_key_html' in question_data:
|
|
386
|
+
print("\nHTML answer key available in result['answer_key_html']")
|
|
387
|
+
|
|
388
|
+
if 'explanation_markdown' in question_data and question_data['explanation_markdown'] is not None:
|
|
389
|
+
print("Markdown explanation available in result['explanation_markdown']")
|
|
390
|
+
|
|
391
|
+
print("=" * 60)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def main():
|
|
395
|
+
parser = argparse.ArgumentParser(
|
|
396
|
+
description="Scan QR codes from quiz PDFs to regenerate answers for grading"
|
|
397
|
+
)
|
|
398
|
+
parser.add_argument(
|
|
399
|
+
'--image',
|
|
400
|
+
type=str,
|
|
401
|
+
help='Path to image file containing QR code(s)'
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument(
|
|
404
|
+
'--all',
|
|
405
|
+
action='store_true',
|
|
406
|
+
help='Process all QR codes found in the image (default: only first one)'
|
|
407
|
+
)
|
|
408
|
+
parser.add_argument(
|
|
409
|
+
'--output',
|
|
410
|
+
type=str,
|
|
411
|
+
help='Save results to JSON file'
|
|
412
|
+
)
|
|
413
|
+
parser.add_argument(
|
|
414
|
+
'--verbose', '-v',
|
|
415
|
+
action='store_true',
|
|
416
|
+
help='Enable verbose debug logging'
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
args = parser.parse_args()
|
|
420
|
+
|
|
421
|
+
if args.verbose:
|
|
422
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
423
|
+
|
|
424
|
+
if not args.image:
|
|
425
|
+
parser.print_help()
|
|
426
|
+
print("\nERROR: --image is required")
|
|
427
|
+
sys.exit(1)
|
|
428
|
+
|
|
429
|
+
# Scan QR codes from image
|
|
430
|
+
qr_codes = scan_qr_from_image(args.image)
|
|
431
|
+
|
|
432
|
+
if not qr_codes:
|
|
433
|
+
print("No QR codes found in image")
|
|
434
|
+
sys.exit(1)
|
|
435
|
+
|
|
436
|
+
# Process QR codes
|
|
437
|
+
if not args.all:
|
|
438
|
+
qr_codes = qr_codes[:1]
|
|
439
|
+
log.info("Processing only the first QR code (use --all to process all)")
|
|
440
|
+
|
|
441
|
+
results = []
|
|
442
|
+
|
|
443
|
+
for qr_string in qr_codes:
|
|
444
|
+
# Parse QR data
|
|
445
|
+
qr_data = parse_qr_data(qr_string)
|
|
446
|
+
|
|
447
|
+
if not qr_data:
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
# Regenerate question and answer
|
|
451
|
+
question_data = regenerate_question_answer(qr_data)
|
|
452
|
+
|
|
453
|
+
if question_data:
|
|
454
|
+
results.append(question_data)
|
|
455
|
+
display_answer_summary(question_data)
|
|
456
|
+
|
|
457
|
+
# Save to file if requested
|
|
458
|
+
if args.output:
|
|
459
|
+
output_path = Path(args.output)
|
|
460
|
+
with open(output_path, 'w') as f:
|
|
461
|
+
json.dump(results, f, indent=2, default=str)
|
|
462
|
+
log.info(f"Results saved to {output_path}")
|
|
463
|
+
|
|
464
|
+
if not results:
|
|
465
|
+
print("\nNo questions could be regenerated from the QR codes.")
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
|
|
468
|
+
print(f"\nSuccessfully regenerated {len(results)} question(s)")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
if __name__ == '__main__':
|
|
472
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QuizGenerator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -5,12 +5,13 @@ QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,43
|
|
|
5
5
|
QuizGenerator/contentast.py,sha256=Em4cnA64Y8_07VruJk_MXwiWcJwqT4-YVf-Lw7uIvYY,68327
|
|
6
6
|
QuizGenerator/generate.py,sha256=o2XezoSE0u-qjxYu1_Ofm9Lpkza7M2Tg47C-ClMcPsE,7197
|
|
7
7
|
QuizGenerator/logging.yaml,sha256=VJCdh26D8e_PNUs4McvvP1ojz9EVjQNifJzfhEk1Mbo,1114
|
|
8
|
-
QuizGenerator/misc.py,sha256=
|
|
8
|
+
QuizGenerator/misc.py,sha256=2vEztj-Kt_0Q2OnynJKC4gL_w7l1MqWsBhhIDOuVD1s,18710
|
|
9
9
|
QuizGenerator/mixins.py,sha256=zUKTkswq7aoDZ_nGPUdRuvnza8iH8ZCi6IH2Uw-kCvs,18492
|
|
10
10
|
QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
|
|
11
11
|
QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
|
|
12
12
|
QuizGenerator/question.py,sha256=uxDYyrq17JFXQ11S03Px5cyRuPYn4qKT3z7TZn9XSjg,26093
|
|
13
13
|
QuizGenerator/quiz.py,sha256=toPodXea2UYGgAf4jyor3Gz-gtXYN1YUJFJFQ5u70v4,18718
|
|
14
|
+
QuizGenerator/regenerate.py,sha256=EvtFhDUXYaWEBCGJ4RW-zN65qj3cMxWa_Y_Rn44WU6c,14282
|
|
14
15
|
QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
|
|
15
16
|
QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
|
|
16
17
|
QuizGenerator/canvas/canvas_interface.py,sha256=wsEWh2lonUMgmbtXF-Zj59CAM_0NInoaERqsujlYMfc,24501
|
|
@@ -18,11 +19,11 @@ QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnD
|
|
|
18
19
|
QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
20
|
QuizGenerator/premade_questions/basic.py,sha256=wAvVZED6a7VToIvSCdAx6SrExmR0xVRo5dL40kycdXI,3402
|
|
20
21
|
QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
|
|
21
|
-
QuizGenerator/premade_questions/cst334/languages.py,sha256=
|
|
22
|
+
QuizGenerator/premade_questions/cst334/languages.py,sha256=MTqprY8VUWgNlP0zRRpZXOAP2dd6ocx_XWVqcNlxYg8,14390
|
|
22
23
|
QuizGenerator/premade_questions/cst334/math_questions.py,sha256=za8lNqhM0RB8qefmPP-Ww0WB_SQn0iRcBKOrZgyHCQQ,9290
|
|
23
24
|
QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=B4hpnMliJY-x65hNbjwbf22m-jiTi3WEXmauKv_YA84,51598
|
|
24
25
|
QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
|
|
25
|
-
QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=
|
|
26
|
+
QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=hIOi-_K-0B_owWtV_YnXXW8Bb51uQF_lpVcXQkAlbXc,16520
|
|
26
27
|
QuizGenerator/premade_questions/cst334/process.py,sha256=EB0iuT9Q8FfOnmlQoXL7gkfsPyVJP55cRFOe2mWfamc,23647
|
|
27
28
|
QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
29
|
QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
|
|
@@ -35,17 +36,17 @@ QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=
|
|
|
35
36
|
QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=tNxfR6J1cZHsHG9GfwVyl6lxxN_TEnhKDmMq4aVLwow,20793
|
|
36
37
|
QuizGenerator/premade_questions/cst463/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
QuizGenerator/premade_questions/cst463/models/attention.py,sha256=i8h6DihzJTc_QFrdm1eaYhnuhlXKRUv_vIDg3jk_LZ8,5502
|
|
38
|
-
QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=
|
|
39
|
+
QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=M0_9wlPhQICje1UdwIbDoBA4qzjmJtmP9VVVneYM5Mc,5766
|
|
39
40
|
QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
|
|
40
41
|
QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=-tXeGgqPkctBBUy4RvEPqhv2kfPqoyO2wk-lNJLNWmY,6697
|
|
41
|
-
QuizGenerator/premade_questions/cst463/models/text.py,sha256=
|
|
42
|
-
QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=
|
|
42
|
+
QuizGenerator/premade_questions/cst463/models/text.py,sha256=bUiDIzOBEzilUKQjm2yO9ufcvJGY6Gt3qfeNP9UZOrc,6400
|
|
43
|
+
QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=acygK-MobvdmwS4UYKVVL4Ey59M1qmq8dITWOT6V-aI,6793
|
|
43
44
|
QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
|
|
44
45
|
QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=pyTSvibCaLuT0LnYAZfjUoZlzywaPWWAaSZ5VepH04E,44148
|
|
45
46
|
QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
|
|
46
47
|
QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=dPn8Sj0yk4m02np62esMKZ7CvcljhYq3Tq51nY9aJnA,29781
|
|
47
|
-
quizgenerator-0.
|
|
48
|
-
quizgenerator-0.
|
|
49
|
-
quizgenerator-0.
|
|
50
|
-
quizgenerator-0.
|
|
51
|
-
quizgenerator-0.
|
|
48
|
+
quizgenerator-0.4.1.dist-info/METADATA,sha256=Yo9okMZOhafNPqYyPT9V-wovJs8YcSvxeTXCr3U7ILs,7212
|
|
49
|
+
quizgenerator-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
50
|
+
quizgenerator-0.4.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
|
|
51
|
+
quizgenerator-0.4.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
52
|
+
quizgenerator-0.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|