QuizGenerator 0.1.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/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +622 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1809 -0
- QuizGenerator/generate.py +362 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +480 -0
- QuizGenerator/mixins.py +539 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +395 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1398 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +396 -0
- QuizGenerator/premade_questions/cst334/process.py +649 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1264 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +657 -0
- QuizGenerator/quiz.py +468 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.1.0.dist-info/METADATA +263 -0
- quizgenerator-0.1.0.dist-info/RECORD +44 -0
- quizgenerator-0.1.0.dist-info/WHEEL +4 -0
- quizgenerator-0.1.0.dist-info/entry_points.txt +2 -0
- quizgenerator-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
from QuizGenerator.canvas.canvas_interface import CanvasInterface, CanvasCourse
|
|
11
|
+
|
|
12
|
+
from QuizGenerator.quiz import Quiz
|
|
13
|
+
|
|
14
|
+
# Load environment variables from ~/.env
|
|
15
|
+
load_dotenv(Path.home() / '.env')
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
from QuizGenerator.performance import PerformanceTracker
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_args():
|
|
24
|
+
parser = argparse.ArgumentParser()
|
|
25
|
+
|
|
26
|
+
parser.add_argument("--prod", action="store_true")
|
|
27
|
+
parser.add_argument("--course_id", type=int)
|
|
28
|
+
|
|
29
|
+
parser.add_argument("--quiz_yaml", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_files/exam_generation.yaml"))
|
|
30
|
+
parser.add_argument("--num_canvas", default=0, type=int)
|
|
31
|
+
parser.add_argument("--num_pdfs", default=0, type=int)
|
|
32
|
+
parser.add_argument("--seed", type=int, default=None,
|
|
33
|
+
help="Random seed for quiz generation (default: None for random)")
|
|
34
|
+
parser.add_argument("--typst", action="store_true",
|
|
35
|
+
help="Use Typst instead of LaTeX for PDF generation")
|
|
36
|
+
parser.add_argument("--typst-measurement", action="store_true",
|
|
37
|
+
help="Use Typst to measure question heights for optimal bin-packing (requires Typst)")
|
|
38
|
+
parser.add_argument("--delete-assignment-group", action="store_true",
|
|
39
|
+
help="Delete existing assignment group before uploading new quizzes")
|
|
40
|
+
|
|
41
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
42
|
+
test_parser = subparsers.add_parser("TEST")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
|
|
47
|
+
if args.num_canvas > 0 and args.course_id is None:
|
|
48
|
+
log.error("Must provide course_id when pushing to canvas")
|
|
49
|
+
exit(8)
|
|
50
|
+
|
|
51
|
+
return args
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test():
|
|
55
|
+
log.info("Running test...")
|
|
56
|
+
|
|
57
|
+
# Load the CST463 quiz configuration to test vector questions
|
|
58
|
+
quiz_yaml = os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_files/cst463.yaml")
|
|
59
|
+
|
|
60
|
+
# Generate a quiz
|
|
61
|
+
quizzes = Quiz.from_yaml(quiz_yaml)
|
|
62
|
+
|
|
63
|
+
# Find a vector question to test
|
|
64
|
+
from QuizGenerator.question import QuestionRegistry
|
|
65
|
+
|
|
66
|
+
print("="*60)
|
|
67
|
+
print("CANVAS ANSWER DUPLICATION TEST")
|
|
68
|
+
print("="*60)
|
|
69
|
+
|
|
70
|
+
# Test multiple question types to find which ones use VECTOR variable kind
|
|
71
|
+
question_types = ["VectorAddition", "VectorDotProduct", "VectorMagnitude", "DerivativeBasic", "DerivativeChain"]
|
|
72
|
+
|
|
73
|
+
for question_type in question_types:
|
|
74
|
+
print(f"\n" + "="*20 + f" TESTING {question_type} " + "="*20)
|
|
75
|
+
question = QuestionRegistry.create(question_type, name=f"Test {question_type}", points_value=1)
|
|
76
|
+
|
|
77
|
+
print(f"Question answers: {len(question.answers)} answer objects")
|
|
78
|
+
|
|
79
|
+
# Show all individual answers and their Canvas representations
|
|
80
|
+
for key, answer in question.answers.items():
|
|
81
|
+
canvas_answers = answer.get_for_canvas()
|
|
82
|
+
print(f"\nAnswer key '{key}':")
|
|
83
|
+
print(f" Variable kind: {answer.variable_kind}")
|
|
84
|
+
print(f" Value: {answer.value}")
|
|
85
|
+
print(f" Canvas entries: {len(canvas_answers)}")
|
|
86
|
+
for i, canvas_answer in enumerate(canvas_answers):
|
|
87
|
+
print(f" {i+1}: '{canvas_answer['answer_text']}'")
|
|
88
|
+
|
|
89
|
+
# Show duplicates if they exist
|
|
90
|
+
texts = [ca['answer_text'] for ca in canvas_answers]
|
|
91
|
+
unique_texts = set(texts)
|
|
92
|
+
duplicates = len(texts) - len(unique_texts)
|
|
93
|
+
if duplicates > 0:
|
|
94
|
+
print(f" *** {duplicates} DUPLICATE ENTRIES FOUND ***")
|
|
95
|
+
for text in sorted(unique_texts):
|
|
96
|
+
count = texts.count(text)
|
|
97
|
+
if count > 1:
|
|
98
|
+
print(f" '{text}' appears {count} times")
|
|
99
|
+
|
|
100
|
+
# Check for phantom blank_ids that aren't displayed
|
|
101
|
+
print(f"\n" + "="*20 + " PHANTOM BLANK_ID CHECK " + "="*20)
|
|
102
|
+
|
|
103
|
+
for question_type in ["DerivativeBasic", "DerivativeChain"]:
|
|
104
|
+
print(f"\n--- {question_type} ---")
|
|
105
|
+
question = QuestionRegistry.create(question_type, name=f"Test {question_type}", points_value=1)
|
|
106
|
+
|
|
107
|
+
# Get the question body to see what blank_ids are actually displayed
|
|
108
|
+
body = question.get_body()
|
|
109
|
+
body_html = body.render("html")
|
|
110
|
+
|
|
111
|
+
print(f" HTML body preview:")
|
|
112
|
+
print(f" {body_html[:200]}...")
|
|
113
|
+
|
|
114
|
+
# Extract blank_ids from the HTML using regex
|
|
115
|
+
import re
|
|
116
|
+
displayed_blank_ids = set(re.findall(r'name="([^"]*)"', body_html))
|
|
117
|
+
|
|
118
|
+
# Get all blank_ids from the answers
|
|
119
|
+
question_type_enum, canvas_answers = question.get_answers()
|
|
120
|
+
all_blank_ids = set(answer['blank_id'] for answer in canvas_answers)
|
|
121
|
+
|
|
122
|
+
print(f" Total answers in self.answers: {len(question.answers)}")
|
|
123
|
+
print(f" Total Canvas answer entries: {len(canvas_answers)}")
|
|
124
|
+
print(f" Unique blank_ids in Canvas answers: {len(all_blank_ids)}")
|
|
125
|
+
print(f" Blank_ids displayed in HTML: {len(displayed_blank_ids)}")
|
|
126
|
+
|
|
127
|
+
# Find phantom blank_ids
|
|
128
|
+
phantom_blank_ids = all_blank_ids - displayed_blank_ids
|
|
129
|
+
if phantom_blank_ids:
|
|
130
|
+
print(f" *** PHANTOM BLANK_IDS FOUND: {phantom_blank_ids} ***")
|
|
131
|
+
for phantom_id in phantom_blank_ids:
|
|
132
|
+
phantom_answers = [a for a in canvas_answers if a['blank_id'] == phantom_id]
|
|
133
|
+
print(f" '{phantom_id}': {len(phantom_answers)} entries not displayed")
|
|
134
|
+
else:
|
|
135
|
+
print(f" No phantom blank_ids found")
|
|
136
|
+
|
|
137
|
+
# Show what blank_ids are actually displayed
|
|
138
|
+
print(f" Displayed blank_ids: {sorted(displayed_blank_ids)}")
|
|
139
|
+
print(f" All answer blank_ids: {sorted(all_blank_ids)}")
|
|
140
|
+
|
|
141
|
+
# Now create a synthetic test to demonstrate the VECTOR bug
|
|
142
|
+
print(f"\n" + "="*20 + " SYNTHETIC VECTOR TEST " + "="*20)
|
|
143
|
+
from QuizGenerator.misc import Answer
|
|
144
|
+
|
|
145
|
+
# Create a synthetic answer with VECTOR variable kind to demonstrate the bug
|
|
146
|
+
vector_answer = Answer(
|
|
147
|
+
key="test_vector",
|
|
148
|
+
value=[1, 2, 3], # 3D vector
|
|
149
|
+
variable_kind=Answer.VariableKind.VECTOR
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
canvas_answers = vector_answer.get_for_canvas()
|
|
153
|
+
print(f"Synthetic VECTOR answer:")
|
|
154
|
+
print(f" Value: {vector_answer.value}")
|
|
155
|
+
print(f" Canvas entries: {len(canvas_answers)}")
|
|
156
|
+
# Only show first few to save space
|
|
157
|
+
for i, canvas_answer in enumerate(canvas_answers[:5]):
|
|
158
|
+
print(f" {i+1}: '{canvas_answer['answer_text']}'")
|
|
159
|
+
if len(canvas_answers) > 5:
|
|
160
|
+
print(f" ... and {len(canvas_answers) - 5} more entries")
|
|
161
|
+
|
|
162
|
+
print("\n" + "="*60)
|
|
163
|
+
print("TEST COMPLETE")
|
|
164
|
+
print("="*60)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def generate_latex(latex_text, remove_previous=False):
|
|
168
|
+
|
|
169
|
+
if remove_previous:
|
|
170
|
+
if os.path.exists('out'): shutil.rmtree('out')
|
|
171
|
+
|
|
172
|
+
tmp_tex = tempfile.NamedTemporaryFile('w')
|
|
173
|
+
|
|
174
|
+
tmp_tex.write(latex_text)
|
|
175
|
+
|
|
176
|
+
tmp_tex.flush()
|
|
177
|
+
shutil.copy(f"{tmp_tex.name}", "debug.tex")
|
|
178
|
+
p = subprocess.Popen(
|
|
179
|
+
f"latexmk -pdf -output-directory={os.path.join(os.getcwd(), 'out')} {tmp_tex.name}",
|
|
180
|
+
shell=True,
|
|
181
|
+
stdout=subprocess.PIPE,
|
|
182
|
+
stderr=subprocess.PIPE)
|
|
183
|
+
try:
|
|
184
|
+
p.wait(30)
|
|
185
|
+
except subprocess.TimeoutExpired:
|
|
186
|
+
logging.error("Latex Compile timed out")
|
|
187
|
+
p.kill()
|
|
188
|
+
tmp_tex.close()
|
|
189
|
+
return
|
|
190
|
+
proc = subprocess.Popen(
|
|
191
|
+
f"latexmk -c {tmp_tex.name} -output-directory={os.path.join(os.getcwd(), 'out')}",
|
|
192
|
+
shell=True,
|
|
193
|
+
stdout=subprocess.PIPE,
|
|
194
|
+
stderr=subprocess.PIPE
|
|
195
|
+
)
|
|
196
|
+
proc.wait(timeout=30)
|
|
197
|
+
tmp_tex.close()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def generate_typst(typst_text, remove_previous=False):
|
|
201
|
+
"""
|
|
202
|
+
Generate PDF from Typst source code.
|
|
203
|
+
|
|
204
|
+
Similar to generate_latex, but uses typst compiler instead of latexmk.
|
|
205
|
+
"""
|
|
206
|
+
if remove_previous:
|
|
207
|
+
if os.path.exists('out'):
|
|
208
|
+
shutil.rmtree('out')
|
|
209
|
+
|
|
210
|
+
# Ensure output directory exists
|
|
211
|
+
os.makedirs('out', exist_ok=True)
|
|
212
|
+
|
|
213
|
+
# Create temporary Typst file
|
|
214
|
+
tmp_typ = tempfile.NamedTemporaryFile('w', suffix='.typ', delete=False)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
tmp_typ.write(typst_text)
|
|
218
|
+
tmp_typ.flush()
|
|
219
|
+
tmp_typ.close()
|
|
220
|
+
|
|
221
|
+
# Save debug copy
|
|
222
|
+
shutil.copy(tmp_typ.name, "debug.typ")
|
|
223
|
+
|
|
224
|
+
# Compile with typst
|
|
225
|
+
output_pdf = os.path.join(os.getcwd(), 'out', os.path.basename(tmp_typ.name).replace('.typ', '.pdf'))
|
|
226
|
+
|
|
227
|
+
# Use --root to set the filesystem root so absolute paths work correctly
|
|
228
|
+
p = subprocess.Popen(
|
|
229
|
+
['typst', 'compile', '--root', '/', tmp_typ.name, output_pdf],
|
|
230
|
+
stdout=subprocess.PIPE,
|
|
231
|
+
stderr=subprocess.PIPE
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
p.wait(30)
|
|
236
|
+
if p.returncode != 0:
|
|
237
|
+
stderr = p.stderr.read().decode('utf-8')
|
|
238
|
+
log.error(f"Typst compilation failed: {stderr}")
|
|
239
|
+
except subprocess.TimeoutExpired:
|
|
240
|
+
log.error("Typst compile timed out")
|
|
241
|
+
p.kill()
|
|
242
|
+
|
|
243
|
+
finally:
|
|
244
|
+
# Clean up temp file
|
|
245
|
+
if os.path.exists(tmp_typ.name):
|
|
246
|
+
os.unlink(tmp_typ.name)
|
|
247
|
+
|
|
248
|
+
def generate_quiz(
|
|
249
|
+
path_to_quiz_yaml,
|
|
250
|
+
num_pdfs=0,
|
|
251
|
+
num_canvas=0,
|
|
252
|
+
use_prod=False,
|
|
253
|
+
course_id=None,
|
|
254
|
+
delete_assignment_group=False,
|
|
255
|
+
use_typst=False,
|
|
256
|
+
use_typst_measurement=False,
|
|
257
|
+
base_seed=None
|
|
258
|
+
):
|
|
259
|
+
|
|
260
|
+
quizzes = Quiz.from_yaml(path_to_quiz_yaml)
|
|
261
|
+
|
|
262
|
+
# Handle Canvas uploads with shared assignment group
|
|
263
|
+
if num_canvas > 0:
|
|
264
|
+
canvas_interface = CanvasInterface(prod=use_prod)
|
|
265
|
+
canvas_course = canvas_interface.get_course(course_id=course_id)
|
|
266
|
+
|
|
267
|
+
# Create assignment group once, with delete flag if specified
|
|
268
|
+
assignment_group = canvas_course.create_assignment_group(
|
|
269
|
+
name="dev",
|
|
270
|
+
delete_existing=delete_assignment_group
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
log.info(f"Using assignment group '{assignment_group.name}' for all quizzes")
|
|
274
|
+
|
|
275
|
+
for quiz in quizzes:
|
|
276
|
+
|
|
277
|
+
for i in range(num_pdfs):
|
|
278
|
+
log.debug(f"Generating PDF {i+1}/{num_pdfs}")
|
|
279
|
+
# If base_seed is provided, use it with an offset for each PDF
|
|
280
|
+
# Otherwise generate a random seed for this PDF
|
|
281
|
+
if base_seed is not None:
|
|
282
|
+
pdf_seed = base_seed + (i * 1000) # Large gap to avoid overlap with rng_seed_offset
|
|
283
|
+
else:
|
|
284
|
+
pdf_seed = random.randint(0, 1_000_000)
|
|
285
|
+
|
|
286
|
+
log.info(f"Generating PDF {i+1} with seed: {pdf_seed}")
|
|
287
|
+
|
|
288
|
+
if use_typst:
|
|
289
|
+
# Generate using Typst
|
|
290
|
+
typst_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render("typst")
|
|
291
|
+
generate_typst(typst_text, remove_previous=(i==0))
|
|
292
|
+
else:
|
|
293
|
+
# Generate using LaTeX (default)
|
|
294
|
+
latex_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render_latex()
|
|
295
|
+
generate_latex(latex_text, remove_previous=(i==0))
|
|
296
|
+
|
|
297
|
+
if num_canvas > 0:
|
|
298
|
+
canvas_course.push_quiz_to_canvas(
|
|
299
|
+
quiz,
|
|
300
|
+
num_canvas,
|
|
301
|
+
title=quiz.name,
|
|
302
|
+
is_practice=quiz.practice,
|
|
303
|
+
assignment_group=assignment_group
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
quiz.describe()
|
|
307
|
+
|
|
308
|
+
# Generate performance report if Canvas questions were generated
|
|
309
|
+
if num_canvas > 0:
|
|
310
|
+
print("\n" + "="*80)
|
|
311
|
+
print("PERFORMANCE ANALYSIS REPORT")
|
|
312
|
+
print("="*80)
|
|
313
|
+
PerformanceTracker.report_summary(min_duration=0.01) # Show operations taking >10ms
|
|
314
|
+
|
|
315
|
+
# Show detailed breakdown for slowest operations
|
|
316
|
+
print("\n" + "="*60)
|
|
317
|
+
print("DETAILED TIMING BREAKDOWN")
|
|
318
|
+
print("="*60)
|
|
319
|
+
|
|
320
|
+
slow_operations = ["canvas_prepare_question", "canvas_api_upload", "ast_render_body", "question_body"]
|
|
321
|
+
for op in slow_operations:
|
|
322
|
+
metrics = PerformanceTracker.get_metrics_by_operation(op)
|
|
323
|
+
if metrics:
|
|
324
|
+
print(f"\n{op.upper()}:")
|
|
325
|
+
# Show stats by question type
|
|
326
|
+
by_type = {}
|
|
327
|
+
for m in metrics:
|
|
328
|
+
qtype = m.question_type or "unknown"
|
|
329
|
+
if qtype not in by_type:
|
|
330
|
+
by_type[qtype] = []
|
|
331
|
+
by_type[qtype].append(m.duration)
|
|
332
|
+
|
|
333
|
+
for qtype, durations in by_type.items():
|
|
334
|
+
avg = sum(durations) / len(durations)
|
|
335
|
+
print(f" {qtype}: {len(durations)} calls, avg {avg:.3f}s (range: {min(durations):.3f}s - {max(durations):.3f}s)")
|
|
336
|
+
|
|
337
|
+
def main():
|
|
338
|
+
|
|
339
|
+
args = parse_args()
|
|
340
|
+
|
|
341
|
+
if args.command == "TEST":
|
|
342
|
+
test()
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# Clear any previous metrics
|
|
346
|
+
PerformanceTracker.clear_metrics()
|
|
347
|
+
|
|
348
|
+
generate_quiz(
|
|
349
|
+
args.quiz_yaml,
|
|
350
|
+
num_pdfs=args.num_pdfs,
|
|
351
|
+
num_canvas=args.num_canvas,
|
|
352
|
+
use_prod=args.prod,
|
|
353
|
+
course_id=args.course_id,
|
|
354
|
+
delete_assignment_group=getattr(args, 'delete_assignment_group', False),
|
|
355
|
+
use_typst=getattr(args, 'typst', False),
|
|
356
|
+
use_typst_measurement=getattr(args, 'typst_measurement', False),
|
|
357
|
+
base_seed=getattr(args, 'seed', None)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
disable_existing_loggers: false
|
|
3
|
+
|
|
4
|
+
formatters:
|
|
5
|
+
standard:
|
|
6
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
7
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
8
|
+
|
|
9
|
+
detailed:
|
|
10
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s:%(lineno)d - %(message)s'
|
|
11
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
12
|
+
|
|
13
|
+
simple:
|
|
14
|
+
format: '%(levelname)s - %(message)s'
|
|
15
|
+
|
|
16
|
+
handlers:
|
|
17
|
+
console:
|
|
18
|
+
class: logging.StreamHandler
|
|
19
|
+
level: INFO
|
|
20
|
+
formatter: standard
|
|
21
|
+
stream: ext://sys.stdout
|
|
22
|
+
|
|
23
|
+
file:
|
|
24
|
+
class: logging.FileHandler
|
|
25
|
+
level: INFO
|
|
26
|
+
formatter: detailed
|
|
27
|
+
filename: ${LOG_FILE:-teachingtools.log}
|
|
28
|
+
mode: a
|
|
29
|
+
|
|
30
|
+
error_file:
|
|
31
|
+
class: logging.FileHandler
|
|
32
|
+
level: ERROR
|
|
33
|
+
formatter: detailed
|
|
34
|
+
filename: ${ERROR_LOG_FILE:-teachingtools_errors.log}
|
|
35
|
+
mode: a
|
|
36
|
+
|
|
37
|
+
loggers:
|
|
38
|
+
QuizGenerator:
|
|
39
|
+
level: INFO
|
|
40
|
+
handlers: [console, file]
|
|
41
|
+
propagate: false
|
|
42
|
+
|
|
43
|
+
lms_interface:
|
|
44
|
+
level: INFO
|
|
45
|
+
handlers: [console, file]
|
|
46
|
+
propagate: false
|
|
47
|
+
|
|
48
|
+
canvasapi:
|
|
49
|
+
level: WARNING
|
|
50
|
+
handlers: [console]
|
|
51
|
+
propagate: false
|
|
52
|
+
|
|
53
|
+
root:
|
|
54
|
+
level: INFO
|
|
55
|
+
handlers: [console, file, error_file]
|