QuizGenerator 0.3.1__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/cst463/models/cnns.py +1 -1
- QuizGenerator/premade_questions/cst463/models/text.py +4 -2
- QuizGenerator/regenerate.py +472 -0
- {quizgenerator-0.3.1.dist-info → quizgenerator-0.4.1.dist-info}/METADATA +1 -1
- {quizgenerator-0.3.1.dist-info → quizgenerator-0.4.1.dist-info}/RECORD +10 -9
- {quizgenerator-0.3.1.dist-info → quizgenerator-0.4.1.dist-info}/entry_points.txt +1 -0
- {quizgenerator-0.3.1.dist-info → quizgenerator-0.4.1.dist-info}/WHEEL +0 -0
- {quizgenerator-0.3.1.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
|
|
|
@@ -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
|
|
|
@@ -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,7 +19,7 @@ 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
|
|
@@ -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/text.py,sha256=bUiDIzOBEzilUKQjm2yO9ufcvJGY6Gt3qfeNP9UZOrc,6400
|
|
42
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
|