QuizGenerator 0.4.2__py3-none-any.whl → 0.6.0__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/contentast.py +809 -117
- QuizGenerator/generate.py +219 -11
- QuizGenerator/misc.py +0 -556
- QuizGenerator/mixins.py +50 -29
- QuizGenerator/premade_questions/basic.py +3 -3
- QuizGenerator/premade_questions/cst334/languages.py +183 -175
- QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
- QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
- QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
- QuizGenerator/premade_questions/cst334/process.py +558 -79
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
- QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
- QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
- QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
- QuizGenerator/premade_questions/cst463/models/text.py +29 -15
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
- QuizGenerator/question.py +114 -20
- QuizGenerator/quiz.py +81 -24
- QuizGenerator/regenerate.py +98 -29
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
- QuizGenerator/README.md +0 -5
- QuizGenerator/logging.yaml +0 -55
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/generate.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!env python
|
|
2
2
|
import argparse
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
import os
|
|
4
5
|
import random
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import tempfile
|
|
9
|
+
import traceback
|
|
10
|
+
import re
|
|
8
11
|
from pathlib import Path
|
|
9
12
|
from dotenv import load_dotenv
|
|
10
13
|
from QuizGenerator.canvas.canvas_interface import CanvasInterface
|
|
11
14
|
|
|
12
15
|
from QuizGenerator.quiz import Quiz
|
|
16
|
+
from QuizGenerator.question import QuestionRegistry
|
|
13
17
|
|
|
14
18
|
import logging
|
|
15
19
|
log = logging.getLogger(__name__)
|
|
@@ -41,8 +45,13 @@ def parse_args():
|
|
|
41
45
|
|
|
42
46
|
# PDF Flags
|
|
43
47
|
parser.add_argument("--num_pdfs", default=0, type=int, help="How many PDF quizzes to create")
|
|
44
|
-
parser.add_argument("--
|
|
45
|
-
|
|
48
|
+
parser.add_argument("--latex", action="store_false", dest="typst", help="Use Typst instead of LaTeX for PDF generation")
|
|
49
|
+
|
|
50
|
+
# Testing flags
|
|
51
|
+
parser.add_argument("--test_all", type=int, default=0, metavar="N",
|
|
52
|
+
help="Generate N variations of ALL registered questions to test they work correctly")
|
|
53
|
+
parser.add_argument("--strict", action="store_true",
|
|
54
|
+
help="With --test_all, skip PDF/Canvas generation if any questions fail")
|
|
46
55
|
|
|
47
56
|
subparsers = parser.add_subparsers(dest='command')
|
|
48
57
|
test_parser = subparsers.add_parser("TEST")
|
|
@@ -63,14 +72,169 @@ def test():
|
|
|
63
72
|
print("\n" + "="*60)
|
|
64
73
|
print("TEST COMPLETE")
|
|
65
74
|
print("="*60)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def generate_latex(latex_text, remove_previous=False):
|
|
69
75
|
|
|
76
|
+
|
|
77
|
+
def test_all_questions(
|
|
78
|
+
num_variations: int,
|
|
79
|
+
generate_pdf: bool = False,
|
|
80
|
+
use_typst: bool = True,
|
|
81
|
+
canvas_course=None,
|
|
82
|
+
strict: bool = False
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Test all registered questions by generating N variations of each.
|
|
86
|
+
|
|
87
|
+
This helps verify that all question types work correctly and can generate
|
|
88
|
+
valid output without errors.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
num_variations: Number of variations to generate for each question type
|
|
92
|
+
generate_pdf: If True, generate a PDF with all successful questions
|
|
93
|
+
use_typst: If True, use Typst for PDF generation; otherwise use LaTeX
|
|
94
|
+
canvas_course: If provided, push a test quiz to this Canvas course
|
|
95
|
+
strict: If True, skip PDF/Canvas generation if any questions fail
|
|
96
|
+
"""
|
|
97
|
+
# Ensure all premade questions are loaded
|
|
98
|
+
QuestionRegistry.load_premade_questions()
|
|
99
|
+
|
|
100
|
+
registered_questions = QuestionRegistry._registry
|
|
101
|
+
total_questions = len(registered_questions)
|
|
102
|
+
|
|
103
|
+
# Test defaults for questions that require external input
|
|
104
|
+
# These are "template" questions that can't work without content
|
|
105
|
+
TEST_DEFAULTS = {
|
|
106
|
+
'fromtext': {'text': 'Test question placeholder text.'},
|
|
107
|
+
'fromgenerator': {'generator': 'return "Generated test content"'},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
print(f"\nTesting {total_questions} registered question types with {num_variations} variations each...")
|
|
111
|
+
print("=" * 70)
|
|
112
|
+
|
|
113
|
+
failed_questions = []
|
|
114
|
+
successful_questions = []
|
|
115
|
+
# Collect question instances for PDF/Canvas generation
|
|
116
|
+
test_question_instances = []
|
|
117
|
+
|
|
118
|
+
for i, (question_name, question_class) in enumerate(sorted(registered_questions.items()), 1):
|
|
119
|
+
print(f"\n[{i}/{total_questions}] Testing: {question_name}")
|
|
120
|
+
print(f" Class: {question_class.__name__}")
|
|
121
|
+
|
|
122
|
+
question_failures = []
|
|
123
|
+
|
|
124
|
+
for variation in range(num_variations):
|
|
125
|
+
seed = variation * 1000 # Use different seeds for each variation
|
|
126
|
+
try:
|
|
127
|
+
# Get any test defaults for this question type
|
|
128
|
+
extra_kwargs = TEST_DEFAULTS.get(question_name, {})
|
|
129
|
+
|
|
130
|
+
# Create question instance with minimal required params
|
|
131
|
+
question = question_class(
|
|
132
|
+
name=f"{question_name} (v{variation+1})",
|
|
133
|
+
points_value=1.0,
|
|
134
|
+
**extra_kwargs
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Generate the question (this calls refresh and builds the AST)
|
|
138
|
+
question_ast = question.get_question(rng_seed=seed)
|
|
139
|
+
|
|
140
|
+
# Try rendering to both formats to catch format-specific issues
|
|
141
|
+
try:
|
|
142
|
+
question_ast.render("html")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
tb = traceback.format_exc()
|
|
145
|
+
question_failures.append(f" Variation {variation+1}: HTML render failed - {e}\n{tb}")
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
question_ast.render("typst")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
tb = traceback.format_exc()
|
|
152
|
+
question_failures.append(f" Variation {variation+1}: Typst render failed - {e}\n{tb}")
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# If we got here, the question works - save the instance
|
|
156
|
+
test_question_instances.append(question)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
tb = traceback.format_exc()
|
|
160
|
+
question_failures.append(f" Variation {variation+1}: Generation failed - {e}\n{tb}")
|
|
161
|
+
|
|
162
|
+
if question_failures:
|
|
163
|
+
print(f" FAILED ({len(question_failures)}/{num_variations} variations)")
|
|
164
|
+
for failure in question_failures:
|
|
165
|
+
print(failure)
|
|
166
|
+
failed_questions.append((question_name, question_failures))
|
|
167
|
+
else:
|
|
168
|
+
print(f" OK ({num_variations}/{num_variations} variations)")
|
|
169
|
+
successful_questions.append(question_name)
|
|
170
|
+
|
|
171
|
+
# Summary
|
|
172
|
+
print("\n" + "=" * 70)
|
|
173
|
+
print("TEST SUMMARY")
|
|
174
|
+
print("=" * 70)
|
|
175
|
+
print(f"Total question types: {total_questions}")
|
|
176
|
+
print(f"Successful: {len(successful_questions)}")
|
|
177
|
+
print(f"Failed: {len(failed_questions)}")
|
|
178
|
+
|
|
179
|
+
if failed_questions:
|
|
180
|
+
print("\nFailed questions:")
|
|
181
|
+
for name, failures in failed_questions:
|
|
182
|
+
print(f" - {name}: {len(failures)} failures")
|
|
183
|
+
|
|
184
|
+
print("=" * 70)
|
|
185
|
+
|
|
186
|
+
# Generate PDF and/or push to Canvas if requested
|
|
187
|
+
if strict and failed_questions:
|
|
188
|
+
print("\n[STRICT MODE] Skipping PDF/Canvas generation due to failures")
|
|
189
|
+
elif (generate_pdf or canvas_course) and test_question_instances:
|
|
190
|
+
print(f"\nCreating test quiz with {len(test_question_instances)} questions...")
|
|
191
|
+
|
|
192
|
+
# Create a Quiz object with all successful questions
|
|
193
|
+
test_quiz = Quiz(
|
|
194
|
+
name="Test All Questions",
|
|
195
|
+
questions=test_question_instances,
|
|
196
|
+
practice=True
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if generate_pdf:
|
|
200
|
+
print("Generating PDF...")
|
|
201
|
+
pdf_seed = 12345 # Fixed seed for reproducibility
|
|
202
|
+
if use_typst:
|
|
203
|
+
typst_text = test_quiz.get_quiz(rng_seed=pdf_seed).render("typst")
|
|
204
|
+
generate_typst(typst_text, remove_previous=True, name_prefix="test_all_questions")
|
|
205
|
+
else:
|
|
206
|
+
latex_text = test_quiz.get_quiz(rng_seed=pdf_seed).render_latex()
|
|
207
|
+
generate_latex(latex_text, remove_previous=True, name_prefix="test_all_questions")
|
|
208
|
+
print("PDF generated in out/ directory")
|
|
209
|
+
|
|
210
|
+
if canvas_course:
|
|
211
|
+
print("Pushing to Canvas...")
|
|
212
|
+
quiz_title = f"Test All Questions ({int(datetime.now().timestamp())} : {datetime.now().strftime('%b %d %I:%M%p')})"
|
|
213
|
+
canvas_course.push_quiz_to_canvas(
|
|
214
|
+
test_quiz,
|
|
215
|
+
num_variations=1,
|
|
216
|
+
title=quiz_title,
|
|
217
|
+
is_practice=True
|
|
218
|
+
)
|
|
219
|
+
print(f"Quiz '{quiz_title}' pushed to Canvas")
|
|
220
|
+
|
|
221
|
+
return len(failed_questions) == 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_latex(latex_text, remove_previous=False, name_prefix=None):
|
|
225
|
+
"""
|
|
226
|
+
Generate PDF from LaTeX source code.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
latex_text: The LaTeX source code to compile
|
|
230
|
+
remove_previous: Whether to remove the 'out' directory before generating
|
|
231
|
+
name_prefix: Optional prefix for the temporary filename (e.g., quiz name)
|
|
232
|
+
"""
|
|
70
233
|
if remove_previous:
|
|
71
234
|
if os.path.exists('out'): shutil.rmtree('out')
|
|
72
235
|
|
|
73
|
-
|
|
236
|
+
prefix = f"{sanitize_filename(name_prefix)}-" if name_prefix else "tmp"
|
|
237
|
+
tmp_tex = tempfile.NamedTemporaryFile('w', prefix=prefix)
|
|
74
238
|
|
|
75
239
|
tmp_tex.write(latex_text)
|
|
76
240
|
|
|
@@ -98,11 +262,38 @@ def generate_latex(latex_text, remove_previous=False):
|
|
|
98
262
|
tmp_tex.close()
|
|
99
263
|
|
|
100
264
|
|
|
101
|
-
def
|
|
265
|
+
def sanitize_filename(name):
|
|
266
|
+
"""
|
|
267
|
+
Sanitize a quiz name for use as a filename prefix.
|
|
268
|
+
|
|
269
|
+
Converts spaces to underscores, removes special characters,
|
|
270
|
+
and limits length to avoid overly long filenames.
|
|
271
|
+
|
|
272
|
+
Example: "CST 334 Exam 4 (Fall 25)" -> "CST_334_Exam_4_Fall_25"
|
|
273
|
+
"""
|
|
274
|
+
# Replace spaces with underscores
|
|
275
|
+
sanitized = name.replace(' ', '_')
|
|
276
|
+
|
|
277
|
+
# Remove characters that aren't alphanumeric, underscore, or hyphen
|
|
278
|
+
sanitized = re.sub(r'[^\w\-]', '', sanitized)
|
|
279
|
+
|
|
280
|
+
# Limit length to avoid overly long filenames (keep first 50 chars)
|
|
281
|
+
if len(sanitized) > 50:
|
|
282
|
+
sanitized = sanitized[:50]
|
|
283
|
+
|
|
284
|
+
return sanitized
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def generate_typst(typst_text, remove_previous=False, name_prefix=None):
|
|
102
288
|
"""
|
|
103
289
|
Generate PDF from Typst source code.
|
|
104
290
|
|
|
105
291
|
Similar to generate_latex, but uses typst compiler instead of latexmk.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
typst_text: The Typst source code to compile
|
|
295
|
+
remove_previous: Whether to remove the 'out' directory before generating
|
|
296
|
+
name_prefix: Optional prefix for the temporary filename (e.g., quiz name)
|
|
106
297
|
"""
|
|
107
298
|
if remove_previous:
|
|
108
299
|
if os.path.exists('out'):
|
|
@@ -111,8 +302,9 @@ def generate_typst(typst_text, remove_previous=False):
|
|
|
111
302
|
# Ensure output directory exists
|
|
112
303
|
os.makedirs('out', exist_ok=True)
|
|
113
304
|
|
|
114
|
-
# Create temporary Typst file
|
|
115
|
-
|
|
305
|
+
# Create temporary Typst file with optional name prefix
|
|
306
|
+
prefix = f"{sanitize_filename(name_prefix)}-" if name_prefix else "tmp"
|
|
307
|
+
tmp_typ = tempfile.NamedTemporaryFile('w', suffix='.typ', delete=False, prefix=prefix)
|
|
116
308
|
|
|
117
309
|
try:
|
|
118
310
|
tmp_typ.write(typst_text)
|
|
@@ -190,11 +382,11 @@ def generate_quiz(
|
|
|
190
382
|
if use_typst:
|
|
191
383
|
# Generate using Typst
|
|
192
384
|
typst_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render("typst")
|
|
193
|
-
generate_typst(typst_text, remove_previous=(i==0))
|
|
385
|
+
generate_typst(typst_text, remove_previous=(i==0), name_prefix=quiz.name)
|
|
194
386
|
else:
|
|
195
387
|
# Generate using LaTeX (default)
|
|
196
388
|
latex_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render_latex()
|
|
197
|
-
generate_latex(latex_text, remove_previous=(i==0))
|
|
389
|
+
generate_latex(latex_text, remove_previous=(i==0), name_prefix=quiz.name)
|
|
198
390
|
|
|
199
391
|
if num_canvas > 0:
|
|
200
392
|
canvas_course.push_quiz_to_canvas(
|
|
@@ -233,6 +425,22 @@ def main():
|
|
|
233
425
|
test()
|
|
234
426
|
return
|
|
235
427
|
|
|
428
|
+
if args.test_all > 0:
|
|
429
|
+
# Set up Canvas course if course_id provided
|
|
430
|
+
canvas_course = None
|
|
431
|
+
if args.course_id:
|
|
432
|
+
canvas_interface = CanvasInterface(prod=args.prod)
|
|
433
|
+
canvas_course = canvas_interface.get_course(course_id=args.course_id)
|
|
434
|
+
|
|
435
|
+
success = test_all_questions(
|
|
436
|
+
args.test_all,
|
|
437
|
+
generate_pdf=True,
|
|
438
|
+
use_typst=getattr(args, 'typst', True),
|
|
439
|
+
canvas_course=canvas_course,
|
|
440
|
+
strict=args.strict
|
|
441
|
+
)
|
|
442
|
+
exit(0 if success else 1)
|
|
443
|
+
|
|
236
444
|
# Clear any previous metrics
|
|
237
445
|
PerformanceTracker.clear_metrics()
|
|
238
446
|
|