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/question.py
CHANGED
|
@@ -21,14 +21,21 @@ import yaml
|
|
|
21
21
|
from typing import List, Dict, Any, Tuple, Optional
|
|
22
22
|
import canvasapi.course, canvasapi.quiz
|
|
23
23
|
|
|
24
|
-
from QuizGenerator.
|
|
25
|
-
from QuizGenerator.contentast import ContentAST
|
|
24
|
+
from QuizGenerator.contentast import ContentAST, AnswerTypes
|
|
26
25
|
from QuizGenerator.performance import timer, PerformanceTracker
|
|
27
26
|
|
|
28
27
|
import logging
|
|
29
28
|
log = logging.getLogger(__name__)
|
|
30
29
|
|
|
31
30
|
|
|
31
|
+
@dataclasses.dataclass
|
|
32
|
+
class QuestionComponents:
|
|
33
|
+
"""Bundle of question parts generated during construction."""
|
|
34
|
+
body: ContentAST.Element
|
|
35
|
+
answers: List[ContentAST.Answer]
|
|
36
|
+
explanation: ContentAST.Element
|
|
37
|
+
|
|
38
|
+
|
|
32
39
|
# Spacing presets for questions
|
|
33
40
|
SPACING_PRESETS = {
|
|
34
41
|
"NONE": 0,
|
|
@@ -344,25 +351,31 @@ class Question(abc.ABC):
|
|
|
344
351
|
ensures consistent rendering across PDF/LaTeX and Canvas/HTML formats.
|
|
345
352
|
|
|
346
353
|
Required Methods:
|
|
347
|
-
-
|
|
348
|
-
-
|
|
354
|
+
- _get_body(): Return Tuple[ContentAST.Section, List[ContentAST.Answer]] with body and answers
|
|
355
|
+
- _get_explanation(): Return Tuple[ContentAST.Section, List[ContentAST.Answer]] with explanation
|
|
356
|
+
|
|
357
|
+
Note: get_body() and get_explanation() are provided for backward compatibility
|
|
358
|
+
and call the _get_* methods, returning just the first element of the tuple.
|
|
349
359
|
|
|
350
360
|
Required Class Attributes:
|
|
351
361
|
- VERSION (str): Question version number (e.g., "1.0")
|
|
352
362
|
Increment when RNG logic changes to ensure reproducibility
|
|
353
363
|
|
|
354
364
|
ContentAST Usage Examples:
|
|
355
|
-
def
|
|
365
|
+
def _get_body(self):
|
|
356
366
|
body = ContentAST.Section()
|
|
367
|
+
answers = []
|
|
357
368
|
body.add_element(ContentAST.Paragraph(["Calculate the matrix:"]))
|
|
358
369
|
|
|
359
370
|
# Use ContentAST.Matrix for math, NOT manual LaTeX
|
|
360
371
|
matrix_data = [[1, 2], [3, 4]]
|
|
361
372
|
body.add_element(ContentAST.Matrix(data=matrix_data, bracket_type="b"))
|
|
362
373
|
|
|
363
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
374
|
+
# Answer extends ContentAST.Leaf - add directly to body
|
|
375
|
+
ans = ContentAST.Answer.integer("result", 42, label="Result")
|
|
376
|
+
answers.append(ans)
|
|
377
|
+
body.add_element(ans)
|
|
378
|
+
return body, answers
|
|
366
379
|
|
|
367
380
|
Common ContentAST Elements:
|
|
368
381
|
- ContentAST.Paragraph: Text blocks
|
|
@@ -460,7 +473,7 @@ class Question(abc.ABC):
|
|
|
460
473
|
self.points_value = points_value
|
|
461
474
|
self.topic = topic
|
|
462
475
|
self.spacing = parse_spacing(kwargs.get("spacing", 0))
|
|
463
|
-
self.answer_kind = Answer.
|
|
476
|
+
self.answer_kind = ContentAST.Answer.CanvasAnswerKind.BLANK
|
|
464
477
|
|
|
465
478
|
# Support for multi-part questions (defaults to 1 for normal questions)
|
|
466
479
|
self.num_subquestions = kwargs.get("num_subquestions", 1)
|
|
@@ -472,6 +485,9 @@ class Question(abc.ABC):
|
|
|
472
485
|
|
|
473
486
|
self.rng_seed_offset = kwargs.get("rng_seed_offset", 0)
|
|
474
487
|
|
|
488
|
+
# Component caching for unified Answer architecture
|
|
489
|
+
self._components: QuestionComponents = None
|
|
490
|
+
|
|
475
491
|
# To be used throughout when generating random things
|
|
476
492
|
self.rng = random.Random()
|
|
477
493
|
|
|
@@ -569,28 +585,105 @@ class Question(abc.ABC):
|
|
|
569
585
|
|
|
570
586
|
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
571
587
|
"""
|
|
572
|
-
Gets the body of the question during generation
|
|
588
|
+
Gets the body of the question during generation (backward compatible wrapper).
|
|
589
|
+
Calls _get_explanation() and returns just the explanation.
|
|
573
590
|
:param kwargs:
|
|
574
591
|
:return: (ContentAST.Section) Containing question explanation or None
|
|
575
592
|
"""
|
|
593
|
+
# Try new pattern first
|
|
594
|
+
if hasattr(self, '_get_explanation') and callable(getattr(self, '_get_explanation')):
|
|
595
|
+
explanation, _ = self._get_explanation()
|
|
596
|
+
return explanation
|
|
597
|
+
# Fallback: default explanation
|
|
576
598
|
return ContentAST.Section(
|
|
577
599
|
[ContentAST.Text("[Please reach out to your professor for clarification]")]
|
|
578
600
|
)
|
|
579
|
-
|
|
580
|
-
def
|
|
581
|
-
|
|
601
|
+
|
|
602
|
+
def _get_body(self) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
|
|
603
|
+
"""
|
|
604
|
+
Build question body and collect answers (new pattern).
|
|
605
|
+
Questions should override this to return (body, answers) tuple.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Tuple of (body_ast, answers_list)
|
|
609
|
+
"""
|
|
610
|
+
# Fallback: call old get_body() and return empty answers
|
|
611
|
+
body = self.get_body()
|
|
612
|
+
return body, []
|
|
613
|
+
|
|
614
|
+
def _get_explanation(self) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
|
|
615
|
+
"""
|
|
616
|
+
Build question explanation and collect answers (new pattern).
|
|
617
|
+
Questions can override this to include answers in explanations.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Tuple of (explanation_ast, answers_list)
|
|
621
|
+
"""
|
|
622
|
+
return ContentAST.Section(
|
|
623
|
+
[ContentAST.Text("[Please reach out to your professor for clarification]")]
|
|
624
|
+
), []
|
|
625
|
+
|
|
626
|
+
def build_question_components(self, **kwargs) -> QuestionComponents:
|
|
627
|
+
"""
|
|
628
|
+
Build question components (body, answers, explanation) in single pass.
|
|
629
|
+
|
|
630
|
+
Calls _get_body() and _get_explanation() which return tuples of
|
|
631
|
+
(content, answers).
|
|
632
|
+
"""
|
|
633
|
+
# Build body with its answers
|
|
634
|
+
body, body_answers = self._get_body()
|
|
635
|
+
|
|
636
|
+
# Build explanation with its answers
|
|
637
|
+
explanation, explanation_answers = self._get_explanation()
|
|
638
|
+
|
|
639
|
+
# Combine all answers
|
|
640
|
+
all_answers = body_answers + explanation_answers
|
|
641
|
+
|
|
642
|
+
return QuestionComponents(
|
|
643
|
+
body=body,
|
|
644
|
+
answers=all_answers,
|
|
645
|
+
explanation=explanation
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
def get_answers(self, *args, **kwargs) -> Tuple[ContentAST.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
|
|
649
|
+
"""
|
|
650
|
+
Return answers from cached components (new pattern) or self.answers dict (old pattern).
|
|
651
|
+
"""
|
|
652
|
+
# Try component-based approach first (new pattern)
|
|
653
|
+
if self._components is None:
|
|
654
|
+
try:
|
|
655
|
+
self._components = self.build_question_components()
|
|
656
|
+
except Exception as e:
|
|
657
|
+
# If component building fails, fall back to dict
|
|
658
|
+
log.debug(f"Failed to build question components: {e}, falling back to dict")
|
|
659
|
+
pass
|
|
660
|
+
|
|
661
|
+
# Use components if available and non-empty
|
|
662
|
+
if self._components is not None and len(self._components.answers) > 0:
|
|
663
|
+
answers = self._components.answers
|
|
664
|
+
if self.can_be_numerical():
|
|
665
|
+
return (
|
|
666
|
+
ContentAST.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
|
|
667
|
+
list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in answers]))
|
|
668
|
+
)
|
|
582
669
|
return (
|
|
583
|
-
|
|
584
|
-
list(itertools.chain(*[a.get_for_canvas(
|
|
670
|
+
self.answer_kind,
|
|
671
|
+
list(itertools.chain(*[a.get_for_canvas() for a in answers]))
|
|
585
672
|
)
|
|
586
|
-
|
|
673
|
+
|
|
674
|
+
# Fall back to dict pattern (old pattern)
|
|
675
|
+
if len(self.answers.values()) > 0:
|
|
676
|
+
if self.can_be_numerical():
|
|
677
|
+
return (
|
|
678
|
+
ContentAST.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
|
|
679
|
+
list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
|
|
680
|
+
)
|
|
587
681
|
return (
|
|
588
682
|
self.answer_kind,
|
|
589
683
|
list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
|
|
590
684
|
)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
)
|
|
685
|
+
|
|
686
|
+
return (ContentAST.Answer.CanvasAnswerKind.ESSAY, [])
|
|
594
687
|
|
|
595
688
|
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
596
689
|
"""If it is necessary to regenerate aspects between usages, this is the time to do it.
|
|
@@ -601,6 +694,7 @@ class Question(abc.ABC):
|
|
|
601
694
|
:return: bool - True if the generated question is interesting, False otherwise
|
|
602
695
|
"""
|
|
603
696
|
self.answers = {}
|
|
697
|
+
self._components = None # Clear component cache
|
|
604
698
|
# Seed the RNG directly with the provided seed (no offset)
|
|
605
699
|
self.rng.seed(rng_seed)
|
|
606
700
|
# Note: We don't call is_interesting() here because child classes need to
|
|
@@ -656,7 +750,7 @@ class Question(abc.ABC):
|
|
|
656
750
|
|
|
657
751
|
def can_be_numerical(self):
|
|
658
752
|
if (len(self.answers.values()) == 1
|
|
659
|
-
and list(self.answers.values())[0]
|
|
753
|
+
and isinstance(list(self.answers.values())[0], AnswerTypes.Float)
|
|
660
754
|
):
|
|
661
755
|
return True
|
|
662
756
|
return False
|
QuizGenerator/quiz.py
CHANGED
|
@@ -11,6 +11,7 @@ import subprocess
|
|
|
11
11
|
import tempfile
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from typing import List, Dict, Optional
|
|
14
|
+
import re
|
|
14
15
|
|
|
15
16
|
import yaml
|
|
16
17
|
|
|
@@ -87,6 +88,11 @@ class Quiz:
|
|
|
87
88
|
|
|
88
89
|
# Get general quiz information from the dictionary
|
|
89
90
|
name = exam_dict.get("name", f"Unnamed Exam ({datetime.now().strftime('%a %b %d %I:%M %p')})")
|
|
91
|
+
if isinstance(name, str):
|
|
92
|
+
def replace_time(match: re.Match) -> str:
|
|
93
|
+
fmt = match.group(1) or "%b %d %I:%M%p"
|
|
94
|
+
return datetime.now().strftime(fmt)
|
|
95
|
+
name = re.sub(r"\$TIME(?:\{([^}]+)\})?", replace_time, name)
|
|
90
96
|
practice = exam_dict.get("practice", False)
|
|
91
97
|
description = exam_dict.get("description", None)
|
|
92
98
|
sort_order = list(map(lambda t: Question.Topic.from_string(t), exam_dict.get("sort order", [])))
|
|
@@ -275,10 +281,29 @@ class Quiz:
|
|
|
275
281
|
|
|
276
282
|
# For each point group, estimate heights and apply bin-packing optimization
|
|
277
283
|
optimized_questions = []
|
|
278
|
-
|
|
284
|
+
is_first_bin_overall = True # Track if we're packing the very first bin of the entire exam
|
|
279
285
|
|
|
280
286
|
log.debug("Optimizing question order for PDF layout...")
|
|
281
287
|
|
|
288
|
+
def get_spacing_priority(question):
|
|
289
|
+
"""
|
|
290
|
+
Get placement priority based on spacing. Lower values = higher priority.
|
|
291
|
+
Order: LONG (9), MEDIUM (6), SHORT (4), NONE (0), then PAGE (99), EXTRA_PAGE (199)
|
|
292
|
+
"""
|
|
293
|
+
spacing = question.spacing
|
|
294
|
+
if spacing >= 199: # EXTRA_PAGE
|
|
295
|
+
return 5
|
|
296
|
+
elif spacing >= 99: # PAGE
|
|
297
|
+
return 4
|
|
298
|
+
elif spacing >= 9: # LONG
|
|
299
|
+
return 0
|
|
300
|
+
elif spacing >= 6: # MEDIUM
|
|
301
|
+
return 1
|
|
302
|
+
elif spacing >= 4: # SHORT
|
|
303
|
+
return 2
|
|
304
|
+
else: # NONE or custom small values
|
|
305
|
+
return 3
|
|
306
|
+
|
|
282
307
|
for points in sorted(point_groups.keys(), reverse=True):
|
|
283
308
|
group = point_groups[points]
|
|
284
309
|
|
|
@@ -288,27 +313,30 @@ class Quiz:
|
|
|
288
313
|
group.sort(key=lambda q: self.question_sort_order.index(q.topic))
|
|
289
314
|
optimized_questions.extend(group)
|
|
290
315
|
log.debug(f" {points}pt questions: {len(group)} questions (order preserved by config)")
|
|
291
|
-
# After adding preserved-order questions, we're
|
|
292
|
-
|
|
316
|
+
# After adding preserved-order questions, we're no longer on the first bin
|
|
317
|
+
is_first_bin_overall = False
|
|
293
318
|
continue
|
|
294
319
|
|
|
295
320
|
# If only 1-2 questions, no optimization needed
|
|
296
321
|
if len(group) <= 2:
|
|
297
|
-
#
|
|
298
|
-
group.sort(key=lambda q: self.question_sort_order.index(q.topic))
|
|
322
|
+
# Sort by spacing priority first, then topic
|
|
323
|
+
group.sort(key=lambda q: (get_spacing_priority(q), self.question_sort_order.index(q.topic)))
|
|
299
324
|
optimized_questions.extend(group)
|
|
300
325
|
log.debug(f" {points}pt questions: {len(group)} questions (no optimization needed)")
|
|
301
|
-
|
|
326
|
+
is_first_bin_overall = False
|
|
302
327
|
continue
|
|
303
328
|
|
|
304
|
-
# Estimate height for each question
|
|
305
|
-
question_heights = [(q, self._estimate_question_height(q, **kwargs)) for q in group]
|
|
329
|
+
# Estimate height for each question, preserving original index for stable sorting
|
|
330
|
+
question_heights = [(i, q, self._estimate_question_height(q, **kwargs)) for i, q in enumerate(group)]
|
|
306
331
|
|
|
307
|
-
# Sort by
|
|
308
|
-
|
|
332
|
+
# Sort by:
|
|
333
|
+
# 1. Spacing priority (LONG, MEDIUM, SHORT, NONE, then PAGE, EXTRA_PAGE)
|
|
334
|
+
# 2. Height descending (within same spacing category)
|
|
335
|
+
# 3. Original index (for deterministic ordering)
|
|
336
|
+
question_heights.sort(key=lambda x: (get_spacing_priority(x[1]), -x[2], x[0]))
|
|
309
337
|
|
|
310
338
|
log.debug(f" Question heights for {points}pt questions:")
|
|
311
|
-
for q, h in question_heights:
|
|
339
|
+
for idx, q, h in question_heights:
|
|
312
340
|
log.debug(f" {q.name}: {h:.1f}cm (spacing={q.spacing}cm)")
|
|
313
341
|
|
|
314
342
|
# Calculate page capacity in centimeters
|
|
@@ -317,7 +345,7 @@ class Quiz:
|
|
|
317
345
|
base_page_capacity = 22.0 # cm
|
|
318
346
|
|
|
319
347
|
# First page has header (title + name line) which takes ~3cm
|
|
320
|
-
first_page_capacity = base_page_capacity - 3.0
|
|
348
|
+
first_page_capacity = base_page_capacity - 3.0 # cm
|
|
321
349
|
|
|
322
350
|
# Better bin-packing strategy: interleave large and small questions
|
|
323
351
|
# Strategy: Start each page with the largest unplaced question, then fill with smaller ones
|
|
@@ -326,21 +354,48 @@ class Quiz:
|
|
|
326
354
|
|
|
327
355
|
while not all(placed):
|
|
328
356
|
# Determine capacity for this page
|
|
329
|
-
|
|
357
|
+
# Use first_page_capacity only for the very first bin of the entire exam
|
|
358
|
+
page_capacity = first_page_capacity if (len(bins) == 0 and is_first_bin_overall) else base_page_capacity
|
|
330
359
|
|
|
331
360
|
# Find the largest unplaced question to start a new page
|
|
332
361
|
new_page = []
|
|
333
362
|
page_height = 0
|
|
334
363
|
|
|
335
|
-
for
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
364
|
+
# Special handling for first bin of entire exam: avoid questions with PAGE spacing (99+cm)
|
|
365
|
+
# to prevent them from pushing content to page 2
|
|
366
|
+
if len(bins) == 0 and is_first_bin_overall:
|
|
367
|
+
# Try to find a question without PAGE/EXTRA_PAGE spacing for the first page
|
|
368
|
+
# PAGE=99cm, EXTRA_PAGE=199cm - these need full pages
|
|
369
|
+
found_non_page_question = False
|
|
370
|
+
for i, (idx, question, height) in enumerate(question_heights):
|
|
371
|
+
if not placed[i] and question.spacing < 99:
|
|
372
|
+
new_page.append(question)
|
|
373
|
+
page_height = height
|
|
374
|
+
placed[i] = True
|
|
375
|
+
found_non_page_question = True
|
|
376
|
+
log.debug(f" First bin (page 1): Selected {question.name} with spacing={question.spacing}cm")
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
# If all questions have PAGE spacing, fall back to normal behavior (use largest question)
|
|
380
|
+
if not found_non_page_question:
|
|
381
|
+
for i, (idx, question, height) in enumerate(question_heights):
|
|
382
|
+
if not placed[i]:
|
|
383
|
+
new_page.append(question)
|
|
384
|
+
page_height = height
|
|
385
|
+
placed[i] = True
|
|
386
|
+
log.debug(f" First bin (page 1): All questions have PAGE spacing, using {question.name} (spacing={question.spacing}cm)")
|
|
387
|
+
break
|
|
388
|
+
else:
|
|
389
|
+
# Normal behavior for non-first pages
|
|
390
|
+
for i, (idx, question, height) in enumerate(question_heights):
|
|
391
|
+
if not placed[i]:
|
|
392
|
+
new_page.append(question)
|
|
393
|
+
page_height = height
|
|
394
|
+
placed[i] = True
|
|
395
|
+
break
|
|
341
396
|
|
|
342
397
|
# Now try to fill the remaining space with smaller questions
|
|
343
|
-
for i, (question, height) in enumerate(question_heights):
|
|
398
|
+
for i, (idx, question, height) in enumerate(question_heights):
|
|
344
399
|
if not placed[i] and page_height + height <= page_capacity:
|
|
345
400
|
new_page.append(question)
|
|
346
401
|
page_height += height
|
|
@@ -348,6 +403,11 @@ class Quiz:
|
|
|
348
403
|
|
|
349
404
|
bins.append((new_page, page_height))
|
|
350
405
|
|
|
406
|
+
# After creating the first bin, we're no longer on the first page
|
|
407
|
+
if len(bins) == 1 and is_first_bin_overall:
|
|
408
|
+
is_first_bin_overall = False
|
|
409
|
+
log.debug(f" First bin created, subsequent bins will use normal page capacity")
|
|
410
|
+
|
|
351
411
|
log.debug(f" {points}pt questions: {len(group)} questions packed into {len(bins)} pages")
|
|
352
412
|
for i, (page_questions, height) in enumerate(bins):
|
|
353
413
|
log.debug(f" Page {i+1}: {height:.1f}cm with {len(page_questions)} questions: {[q.name for q in page_questions]}")
|
|
@@ -356,9 +416,6 @@ class Quiz:
|
|
|
356
416
|
for bin_contents, _ in bins:
|
|
357
417
|
optimized_questions.extend(bin_contents)
|
|
358
418
|
|
|
359
|
-
# After packing questions, we're no longer on the first page
|
|
360
|
-
is_first_page = False
|
|
361
|
-
|
|
362
419
|
return optimized_questions
|
|
363
420
|
|
|
364
421
|
def get_quiz(self, **kwargs) -> ContentAST.Document:
|
|
@@ -464,4 +521,4 @@ def main():
|
|
|
464
521
|
|
|
465
522
|
if __name__ == "__main__":
|
|
466
523
|
main()
|
|
467
|
-
|
|
524
|
+
|
QuizGenerator/regenerate.py
CHANGED
|
@@ -13,6 +13,12 @@ CLI Usage:
|
|
|
13
13
|
# Scan QR codes from a scanned exam page
|
|
14
14
|
python grade_from_qr.py --image exam_page.jpg --all
|
|
15
15
|
|
|
16
|
+
# Decode an encrypted string directly
|
|
17
|
+
python -m QuizGenerator.regenerate --encrypted_str "EzE6JF86CDlf..."
|
|
18
|
+
|
|
19
|
+
# Decode with custom point value
|
|
20
|
+
python -m QuizGenerator.regenerate --encrypted_str "EzE6JF86CDlf..." --points 5.0
|
|
21
|
+
|
|
16
22
|
API Usage (recommended for web UIs):
|
|
17
23
|
from grade_from_qr import regenerate_from_encrypted
|
|
18
24
|
|
|
@@ -36,9 +42,22 @@ import argparse
|
|
|
36
42
|
import json
|
|
37
43
|
import sys
|
|
38
44
|
import logging
|
|
45
|
+
import os
|
|
39
46
|
from pathlib import Path
|
|
40
47
|
from typing import Dict, Any, Optional, List
|
|
41
48
|
|
|
49
|
+
# Load environment variables from .env file
|
|
50
|
+
try:
|
|
51
|
+
from dotenv import load_dotenv
|
|
52
|
+
# Try loading from current directory first, then home directory
|
|
53
|
+
if os.path.exists('.env'):
|
|
54
|
+
load_dotenv('.env')
|
|
55
|
+
else:
|
|
56
|
+
load_dotenv(os.path.join(os.path.expanduser("~"), ".env"))
|
|
57
|
+
except ImportError:
|
|
58
|
+
# dotenv not available, will use system environment variables only
|
|
59
|
+
pass
|
|
60
|
+
|
|
42
61
|
# Quiz generator imports (always available)
|
|
43
62
|
from QuizGenerator.qrcode_generator import QuestionQRCode
|
|
44
63
|
from QuizGenerator.question import QuestionRegistry
|
|
@@ -400,6 +419,17 @@ def main():
|
|
|
400
419
|
type=str,
|
|
401
420
|
help='Path to image file containing QR code(s)'
|
|
402
421
|
)
|
|
422
|
+
parser.add_argument(
|
|
423
|
+
'--encrypted_str',
|
|
424
|
+
type=str,
|
|
425
|
+
help='Encrypted string from QR code to decode directly'
|
|
426
|
+
)
|
|
427
|
+
parser.add_argument(
|
|
428
|
+
'--points',
|
|
429
|
+
type=float,
|
|
430
|
+
default=1.0,
|
|
431
|
+
help='Point value for the question (default: 1.0, only used with --encrypted_str)'
|
|
432
|
+
)
|
|
403
433
|
parser.add_argument(
|
|
404
434
|
'--all',
|
|
405
435
|
action='store_true',
|
|
@@ -415,44 +445,83 @@ def main():
|
|
|
415
445
|
action='store_true',
|
|
416
446
|
help='Enable verbose debug logging'
|
|
417
447
|
)
|
|
418
|
-
|
|
448
|
+
|
|
419
449
|
args = parser.parse_args()
|
|
420
|
-
|
|
450
|
+
|
|
421
451
|
if args.verbose:
|
|
422
452
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
423
|
-
|
|
424
|
-
|
|
453
|
+
|
|
454
|
+
# Check for required arguments
|
|
455
|
+
if not args.image and not args.encrypted_str:
|
|
425
456
|
parser.print_help()
|
|
426
|
-
print("\nERROR: --image is required")
|
|
457
|
+
print("\nERROR: Either --image or --encrypted_str is required")
|
|
427
458
|
sys.exit(1)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if not qr_codes:
|
|
433
|
-
print("No QR codes found in image")
|
|
459
|
+
|
|
460
|
+
if args.image and args.encrypted_str:
|
|
461
|
+
print("ERROR: Cannot use both --image and --encrypted_str at the same time")
|
|
434
462
|
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
|
-
|
|
463
|
+
|
|
441
464
|
results = []
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
465
|
+
|
|
466
|
+
# Handle direct encrypted string input
|
|
467
|
+
if args.encrypted_str:
|
|
468
|
+
try:
|
|
469
|
+
log.info(f"Decoding encrypted string (points={args.points})")
|
|
470
|
+
result = regenerate_from_encrypted(args.encrypted_str, args.points)
|
|
471
|
+
|
|
472
|
+
# Format result similar to regenerate_question_answer output
|
|
473
|
+
question_data = {
|
|
474
|
+
"question_number": "N/A",
|
|
475
|
+
"points": args.points,
|
|
476
|
+
"question_type": result["question_type"],
|
|
477
|
+
"seed": result["seed"],
|
|
478
|
+
"version": result["version"],
|
|
479
|
+
"answers": result["answers"],
|
|
480
|
+
"answer_objects": result["answer_objects"],
|
|
481
|
+
"answer_key_html": result["answer_key_html"],
|
|
482
|
+
"explanation_markdown": result.get("explanation_markdown")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if "kwargs" in result:
|
|
486
|
+
question_data["config"] = result["kwargs"]
|
|
487
|
+
|
|
454
488
|
results.append(question_data)
|
|
455
489
|
display_answer_summary(question_data)
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
log.error(f"Failed to decode encrypted string: {e}")
|
|
493
|
+
import traceback
|
|
494
|
+
if args.verbose:
|
|
495
|
+
log.debug(traceback.format_exc())
|
|
496
|
+
sys.exit(1)
|
|
497
|
+
|
|
498
|
+
# Handle image scanning
|
|
499
|
+
else:
|
|
500
|
+
# Scan QR codes from image
|
|
501
|
+
qr_codes = scan_qr_from_image(args.image)
|
|
502
|
+
|
|
503
|
+
if not qr_codes:
|
|
504
|
+
print("No QR codes found in image")
|
|
505
|
+
sys.exit(1)
|
|
506
|
+
|
|
507
|
+
# Process QR codes
|
|
508
|
+
if not args.all:
|
|
509
|
+
qr_codes = qr_codes[:1]
|
|
510
|
+
log.info("Processing only the first QR code (use --all to process all)")
|
|
511
|
+
|
|
512
|
+
for qr_string in qr_codes:
|
|
513
|
+
# Parse QR data
|
|
514
|
+
qr_data = parse_qr_data(qr_string)
|
|
515
|
+
|
|
516
|
+
if not qr_data:
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
# Regenerate question and answer
|
|
520
|
+
question_data = regenerate_question_answer(qr_data)
|
|
521
|
+
|
|
522
|
+
if question_data:
|
|
523
|
+
results.append(question_data)
|
|
524
|
+
display_answer_summary(question_data)
|
|
456
525
|
|
|
457
526
|
# Save to file if requested
|
|
458
527
|
if args.output:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QuizGenerator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
@@ -1,52 +1,50 @@
|
|
|
1
|
-
QuizGenerator/README.md,sha256=4n16gKyhIAKRBX4VKlpfcK0pyUYJ6Ht08MUsnwgxrZo,145
|
|
2
1
|
QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
|
|
3
2
|
QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
|
|
4
3
|
QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
|
|
5
|
-
QuizGenerator/contentast.py,sha256=
|
|
6
|
-
QuizGenerator/generate.py,sha256=
|
|
7
|
-
QuizGenerator/
|
|
8
|
-
QuizGenerator/
|
|
9
|
-
QuizGenerator/mixins.py,sha256=zUKTkswq7aoDZ_nGPUdRuvnza8iH8ZCi6IH2Uw-kCvs,18492
|
|
4
|
+
QuizGenerator/contentast.py,sha256=lK92FlRfKnNU-9AhG8RJbCkpQixo5n7bmwvggyPazpM,93664
|
|
5
|
+
QuizGenerator/generate.py,sha256=bOkR6JhGQE0r4zbRWhC_3Dsj5hOEk6-jzFLcjk-qwKg,14811
|
|
6
|
+
QuizGenerator/misc.py,sha256=wtlrEpmEpoE6vNRmgjNUmuWnRdQKSCYfrqeoTagNaxg,464
|
|
7
|
+
QuizGenerator/mixins.py,sha256=HEhdGdeghqGWoajADTAIdUjkzwDSYl1b65LAkUdV50U,19211
|
|
10
8
|
QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
|
|
11
9
|
QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
|
|
12
|
-
QuizGenerator/question.py,sha256=
|
|
13
|
-
QuizGenerator/quiz.py,sha256=
|
|
14
|
-
QuizGenerator/regenerate.py,sha256=
|
|
10
|
+
QuizGenerator/question.py,sha256=FjiAJZn3LUBVr7nDazx5G0bASP0uXQTUP0rqphbvyLw,31714
|
|
11
|
+
QuizGenerator/quiz.py,sha256=f2HLrawUlu3ULkNDzcihBWAt-e-49AIPz_l1edMAEQ0,21503
|
|
12
|
+
QuizGenerator/regenerate.py,sha256=Uh4B9aKQvL3zD7PT-uH-GvrcSuUygV1BimvPVuErc-g,16525
|
|
15
13
|
QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
|
|
16
14
|
QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
|
|
17
15
|
QuizGenerator/canvas/canvas_interface.py,sha256=wsEWh2lonUMgmbtXF-Zj59CAM_0NInoaERqsujlYMfc,24501
|
|
18
16
|
QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnDNw,7594
|
|
19
17
|
QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
QuizGenerator/premade_questions/basic.py,sha256=
|
|
18
|
+
QuizGenerator/premade_questions/basic.py,sha256=vMyCIYU0IJBjQVE-XVzHr9axq_kZL2ka4K1MaqeQwXM,3428
|
|
21
19
|
QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
|
|
22
|
-
QuizGenerator/premade_questions/cst334/languages.py,sha256=
|
|
23
|
-
QuizGenerator/premade_questions/cst334/math_questions.py,sha256=
|
|
24
|
-
QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=
|
|
20
|
+
QuizGenerator/premade_questions/cst334/languages.py,sha256=ENsgpup1uoGIVWEE_puDgLzWicguwdERsm-iiS-sh1E,15129
|
|
21
|
+
QuizGenerator/premade_questions/cst334/math_questions.py,sha256=zwkm3OLOkDZ_fbPF1E12UhdF373aQv9QzjBfV1bfbyU,10242
|
|
22
|
+
QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=PUyXIoyrGImXhucx6KBgoAEYmQCzTSCz0DWUu_xo6Kc,54371
|
|
25
23
|
QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
|
|
26
|
-
QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=
|
|
27
|
-
QuizGenerator/premade_questions/cst334/process.py,sha256=
|
|
24
|
+
QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=pb63H47WlSsHi_nHRVhbwUHeybF2zbWL8vXbwOguAL0,17474
|
|
25
|
+
QuizGenerator/premade_questions/cst334/process.py,sha256=ERAdardtXSwzh3SDrlkVQJr4oO0F9cZtCZZ8dkJRsfw,39865
|
|
28
26
|
QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
27
|
QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
|
|
30
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=
|
|
31
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=
|
|
32
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=
|
|
33
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=
|
|
28
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=PdWAJjgsiwYQsxeLlQiVDd3m97RUjUY1KJTJxyrrdRI,13984
|
|
29
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=yc1wwqgsFh13peGDRJM74TaWmzQrFg-N7cyqIdf7G54,10874
|
|
30
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=ftaNkwIPuh-wjAYeAj0YvklS3-xjOcQS7HvUkcNRRYY,22257
|
|
31
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=sn70FLK_3LnhYhX7TVKB9oDwZMJybQIMtYLzxn5WZxg,2675
|
|
34
32
|
QuizGenerator/premade_questions/cst463/math_and_data/__init__.py,sha256=EbIaUrx7_aK9j3Gd8Mk08h9GocTq_0OoNu2trfNwaU8,202
|
|
35
|
-
QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=
|
|
36
|
-
QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=
|
|
33
|
+
QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=oU8Z36-P92TQ9OhJ2XK6ARlyM8I_qvp1guZLYVvU_l8,29116
|
|
34
|
+
QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=3NCgxtU0OxGrnixBOyrGKObf2rOWwXuBv4TWfiC9jxQ,13226
|
|
37
35
|
QuizGenerator/premade_questions/cst463/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
QuizGenerator/premade_questions/cst463/models/attention.py,sha256=
|
|
39
|
-
QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=
|
|
36
|
+
QuizGenerator/premade_questions/cst463/models/attention.py,sha256=e_iPxGJUAXQ89zk8BXG9r1sGFKOJhBr4UaWaDQV6I7g,6100
|
|
37
|
+
QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=8Z3vP6A-CxRP80C7pb9gWJ2SctDh4X5fy6V5Gzx31Pc,6337
|
|
40
38
|
QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
|
|
41
|
-
QuizGenerator/premade_questions/cst463/models/rnns.py,sha256
|
|
42
|
-
QuizGenerator/premade_questions/cst463/models/text.py,sha256=
|
|
43
|
-
QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=
|
|
39
|
+
QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=hHe-7npGmoUpYYVy1gIlyTTBk8ra3Muw0yHxa3FrbS8,7291
|
|
40
|
+
QuizGenerator/premade_questions/cst463/models/text.py,sha256=bwEebpYnHn52yCrzJGCNz8ebWIU03fdedI9JQmQ9I4I,6978
|
|
41
|
+
QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=oXyDKQvfLdWE_-cd2CY9m6t3SI_s-SQGXP6zDSoGINI,7273
|
|
44
42
|
QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
|
|
45
|
-
QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=
|
|
43
|
+
QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=luWlTfj1UM1yQDQzs_tNzTV67qXhRUBwNt8QrV74XHs,46115
|
|
46
44
|
QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
|
|
47
|
-
QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=
|
|
48
|
-
quizgenerator-0.
|
|
49
|
-
quizgenerator-0.
|
|
50
|
-
quizgenerator-0.
|
|
51
|
-
quizgenerator-0.
|
|
52
|
-
quizgenerator-0.
|
|
45
|
+
QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=8Wo38kTd_n0Oau2ERpvcudB9uJiOVDYYQNeWu9v4Tyo,33516
|
|
46
|
+
quizgenerator-0.6.0.dist-info/METADATA,sha256=F6pWRrq1r55-tR5-lLyLpegiV26GxCeP4X7Y1w_NK8k,7212
|
|
47
|
+
quizgenerator-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
quizgenerator-0.6.0.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
|
|
49
|
+
quizgenerator-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
50
|
+
quizgenerator-0.6.0.dist-info/RECORD,,
|