QuizGenerator 0.6.3__py3-none-any.whl → 0.7.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/contentast.py +2191 -2193
- QuizGenerator/misc.py +1 -1
- QuizGenerator/mixins.py +64 -64
- QuizGenerator/premade_questions/basic.py +16 -16
- QuizGenerator/premade_questions/cst334/languages.py +26 -26
- QuizGenerator/premade_questions/cst334/math_questions.py +42 -42
- QuizGenerator/premade_questions/cst334/memory_questions.py +124 -124
- QuizGenerator/premade_questions/cst334/persistence_questions.py +48 -48
- QuizGenerator/premade_questions/cst334/process.py +38 -38
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +45 -45
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +34 -34
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +53 -53
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +65 -65
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +39 -39
- QuizGenerator/premade_questions/cst463/models/attention.py +36 -36
- QuizGenerator/premade_questions/cst463/models/cnns.py +26 -26
- QuizGenerator/premade_questions/cst463/models/rnns.py +36 -36
- QuizGenerator/premade_questions/cst463/models/text.py +32 -32
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +15 -15
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +124 -124
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +161 -161
- QuizGenerator/question.py +41 -41
- QuizGenerator/quiz.py +7 -7
- QuizGenerator/regenerate.py +114 -13
- QuizGenerator/typst_utils.py +2 -2
- {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/METADATA +1 -1
- {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/RECORD +31 -31
- {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/WHEEL +0 -0
- {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/question.py
CHANGED
|
@@ -21,7 +21,7 @@ import yaml
|
|
|
21
21
|
from typing import List, Dict, Any, Tuple, Optional
|
|
22
22
|
import canvasapi.course, canvasapi.quiz
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
import QuizGenerator.contentast as ca
|
|
25
25
|
from QuizGenerator.performance import timer, PerformanceTracker
|
|
26
26
|
|
|
27
27
|
import logging
|
|
@@ -31,9 +31,9 @@ log = logging.getLogger(__name__)
|
|
|
31
31
|
@dataclasses.dataclass
|
|
32
32
|
class QuestionComponents:
|
|
33
33
|
"""Bundle of question parts generated during construction."""
|
|
34
|
-
body:
|
|
35
|
-
answers: List[
|
|
36
|
-
explanation:
|
|
34
|
+
body: ca.Element
|
|
35
|
+
answers: List[ca.Answer]
|
|
36
|
+
explanation: ca.Element
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
# Spacing presets for questions
|
|
@@ -344,15 +344,15 @@ class Question(abc.ABC):
|
|
|
344
344
|
"""
|
|
345
345
|
Base class for all quiz questions with cross-format rendering support.
|
|
346
346
|
|
|
347
|
-
CRITICAL: When implementing Question subclasses, ALWAYS use
|
|
347
|
+
CRITICAL: When implementing Question subclasses, ALWAYS use content AST elements
|
|
348
348
|
for all content in get_body() and get_explanation() methods.
|
|
349
349
|
|
|
350
|
-
NEVER create manual LaTeX, HTML, or Markdown strings. The
|
|
350
|
+
NEVER create manual LaTeX, HTML, or Markdown strings. The content AST system
|
|
351
351
|
ensures consistent rendering across PDF/LaTeX and Canvas/HTML formats.
|
|
352
352
|
|
|
353
353
|
Required Methods:
|
|
354
|
-
- _get_body(): Return Tuple[
|
|
355
|
-
- _get_explanation(): Return Tuple[
|
|
354
|
+
- _get_body(): Return Tuple[ca.Section, List[ca.Answer]] with body and answers
|
|
355
|
+
- _get_explanation(): Return Tuple[ca.Section, List[ca.Answer]] with explanation
|
|
356
356
|
|
|
357
357
|
Note: get_body() and get_explanation() are provided for backward compatibility
|
|
358
358
|
and call the _get_* methods, returning just the first element of the tuple.
|
|
@@ -361,28 +361,28 @@ class Question(abc.ABC):
|
|
|
361
361
|
- VERSION (str): Question version number (e.g., "1.0")
|
|
362
362
|
Increment when RNG logic changes to ensure reproducibility
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
Content AST Usage Examples:
|
|
365
365
|
def _get_body(self):
|
|
366
|
-
body =
|
|
366
|
+
body = ca.Section()
|
|
367
367
|
answers = []
|
|
368
|
-
body.add_element(
|
|
368
|
+
body.add_element(ca.Paragraph(["Calculate the matrix:"]))
|
|
369
369
|
|
|
370
|
-
# Use
|
|
370
|
+
# Use ca.Matrix for math, NOT manual LaTeX
|
|
371
371
|
matrix_data = [[1, 2], [3, 4]]
|
|
372
|
-
body.add_element(
|
|
372
|
+
body.add_element(ca.Matrix(data=matrix_data, bracket_type="b"))
|
|
373
373
|
|
|
374
|
-
# Answer extends
|
|
375
|
-
ans =
|
|
374
|
+
# Answer extends ca.Leaf - add directly to body
|
|
375
|
+
ans = ca.Answer.integer("result", 42, label="Result")
|
|
376
376
|
answers.append(ans)
|
|
377
377
|
body.add_element(ans)
|
|
378
378
|
return body, answers
|
|
379
379
|
|
|
380
|
-
Common
|
|
381
|
-
-
|
|
382
|
-
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
385
|
-
-
|
|
380
|
+
Common Content AST Elements:
|
|
381
|
+
- ca.Paragraph: Text blocks
|
|
382
|
+
- ca.Equation: Mathematical expressions
|
|
383
|
+
- ca.Matrix: Matrices and vectors (use instead of manual LaTeX!)
|
|
384
|
+
- ca.Table: Data tables
|
|
385
|
+
- ca.OnlyHtml/OnlyLatex: Platform-specific content
|
|
386
386
|
|
|
387
387
|
Versioning Guidelines:
|
|
388
388
|
- Increment VERSION when changing:
|
|
@@ -473,7 +473,7 @@ class Question(abc.ABC):
|
|
|
473
473
|
self.points_value = points_value
|
|
474
474
|
self.topic = topic
|
|
475
475
|
self.spacing = parse_spacing(kwargs.get("spacing", 0))
|
|
476
|
-
self.answer_kind =
|
|
476
|
+
self.answer_kind = ca.Answer.CanvasAnswerKind.BLANK
|
|
477
477
|
|
|
478
478
|
# Support for multi-part questions (defaults to 1 for normal questions)
|
|
479
479
|
self.num_subquestions = kwargs.get("num_subquestions", 1)
|
|
@@ -504,11 +504,11 @@ class Question(abc.ABC):
|
|
|
504
504
|
with open(path_to_yaml) as fid:
|
|
505
505
|
question_dicts = yaml.safe_load_all(fid)
|
|
506
506
|
|
|
507
|
-
def get_question(self, **kwargs) ->
|
|
507
|
+
def get_question(self, **kwargs) -> ca.Question:
|
|
508
508
|
"""
|
|
509
509
|
Gets the question in AST format
|
|
510
510
|
:param kwargs:
|
|
511
|
-
:return: (
|
|
511
|
+
:return: (ca.Question) Containing question.
|
|
512
512
|
"""
|
|
513
513
|
# Generate the question, retrying with incremented seeds until we get an interesting one
|
|
514
514
|
with timer("question_refresh", question_name=self.name, question_type=self.__class__.__name__):
|
|
@@ -553,7 +553,7 @@ class Question(abc.ABC):
|
|
|
553
553
|
|
|
554
554
|
# Store the actual seed used and question metadata for QR code generation
|
|
555
555
|
actual_seed = None if base_seed is None else base_seed + backoff_counter - 1
|
|
556
|
-
question_ast =
|
|
556
|
+
question_ast = ca.Question(
|
|
557
557
|
body=body,
|
|
558
558
|
explanation=explanation,
|
|
559
559
|
value=self.points_value,
|
|
@@ -575,31 +575,31 @@ class Question(abc.ABC):
|
|
|
575
575
|
return question_ast
|
|
576
576
|
|
|
577
577
|
@abc.abstractmethod
|
|
578
|
-
def get_body(self, **kwargs) ->
|
|
578
|
+
def get_body(self, **kwargs) -> ca.Section:
|
|
579
579
|
"""
|
|
580
580
|
Gets the body of the question during generation
|
|
581
581
|
:param kwargs:
|
|
582
|
-
:return: (
|
|
582
|
+
:return: (ca.Section) Containing question body
|
|
583
583
|
"""
|
|
584
584
|
pass
|
|
585
585
|
|
|
586
|
-
def get_explanation(self, **kwargs) ->
|
|
586
|
+
def get_explanation(self, **kwargs) -> ca.Section:
|
|
587
587
|
"""
|
|
588
588
|
Gets the body of the question during generation (backward compatible wrapper).
|
|
589
589
|
Calls _get_explanation() and returns just the explanation.
|
|
590
590
|
:param kwargs:
|
|
591
|
-
:return: (
|
|
591
|
+
:return: (ca.Section) Containing question explanation or None
|
|
592
592
|
"""
|
|
593
593
|
# Try new pattern first
|
|
594
594
|
if hasattr(self, '_get_explanation') and callable(getattr(self, '_get_explanation')):
|
|
595
595
|
explanation, _ = self._get_explanation()
|
|
596
596
|
return explanation
|
|
597
597
|
# Fallback: default explanation
|
|
598
|
-
return
|
|
599
|
-
[
|
|
598
|
+
return ca.Section(
|
|
599
|
+
[ca.Text("[Please reach out to your professor for clarification]")]
|
|
600
600
|
)
|
|
601
601
|
|
|
602
|
-
def _get_body(self) -> Tuple[
|
|
602
|
+
def _get_body(self) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
603
603
|
"""
|
|
604
604
|
Build question body and collect answers (new pattern).
|
|
605
605
|
Questions should override this to return (body, answers) tuple.
|
|
@@ -611,7 +611,7 @@ class Question(abc.ABC):
|
|
|
611
611
|
body = self.get_body()
|
|
612
612
|
return body, []
|
|
613
613
|
|
|
614
|
-
def _get_explanation(self) -> Tuple[
|
|
614
|
+
def _get_explanation(self) -> Tuple[ca.Element, List[ca.Answer]]:
|
|
615
615
|
"""
|
|
616
616
|
Build question explanation and collect answers (new pattern).
|
|
617
617
|
Questions can override this to include answers in explanations.
|
|
@@ -619,8 +619,8 @@ class Question(abc.ABC):
|
|
|
619
619
|
Returns:
|
|
620
620
|
Tuple of (explanation_ast, answers_list)
|
|
621
621
|
"""
|
|
622
|
-
return
|
|
623
|
-
[
|
|
622
|
+
return ca.Section(
|
|
623
|
+
[ca.Text("[Please reach out to your professor for clarification]")]
|
|
624
624
|
), []
|
|
625
625
|
|
|
626
626
|
def build_question_components(self, **kwargs) -> QuestionComponents:
|
|
@@ -645,7 +645,7 @@ class Question(abc.ABC):
|
|
|
645
645
|
explanation=explanation
|
|
646
646
|
)
|
|
647
647
|
|
|
648
|
-
def get_answers(self, *args, **kwargs) -> Tuple[
|
|
648
|
+
def get_answers(self, *args, **kwargs) -> Tuple[ca.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
|
|
649
649
|
"""
|
|
650
650
|
Return answers from cached components (new pattern) or self.answers dict (old pattern).
|
|
651
651
|
"""
|
|
@@ -663,7 +663,7 @@ class Question(abc.ABC):
|
|
|
663
663
|
answers = self._components.answers
|
|
664
664
|
if self.can_be_numerical():
|
|
665
665
|
return (
|
|
666
|
-
|
|
666
|
+
ca.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
|
|
667
667
|
list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in answers]))
|
|
668
668
|
)
|
|
669
669
|
return (
|
|
@@ -675,7 +675,7 @@ class Question(abc.ABC):
|
|
|
675
675
|
if len(self.answers.values()) > 0:
|
|
676
676
|
if self.can_be_numerical():
|
|
677
677
|
return (
|
|
678
|
-
|
|
678
|
+
ca.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
|
|
679
679
|
list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
|
|
680
680
|
)
|
|
681
681
|
return (
|
|
@@ -683,7 +683,7 @@ class Question(abc.ABC):
|
|
|
683
683
|
list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
|
|
684
684
|
)
|
|
685
685
|
|
|
686
|
-
return (
|
|
686
|
+
return (ca.Answer.CanvasAnswerKind.ESSAY, [])
|
|
687
687
|
|
|
688
688
|
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
689
689
|
"""If it is necessary to regenerate aspects between usages, this is the time to do it.
|
|
@@ -750,7 +750,7 @@ class Question(abc.ABC):
|
|
|
750
750
|
|
|
751
751
|
def can_be_numerical(self):
|
|
752
752
|
if (len(self.answers.values()) == 1
|
|
753
|
-
and isinstance(list(self.answers.values())[0], AnswerTypes.Float)
|
|
753
|
+
and isinstance(list(self.answers.values())[0], ca.AnswerTypes.Float)
|
|
754
754
|
):
|
|
755
755
|
return True
|
|
756
756
|
return False
|
|
@@ -806,4 +806,4 @@ class QuestionGroup():
|
|
|
806
806
|
return attr(*args, **kwargs)
|
|
807
807
|
return wrapped_method
|
|
808
808
|
|
|
809
|
-
return attr
|
|
809
|
+
return attr
|
QuizGenerator/quiz.py
CHANGED
|
@@ -15,7 +15,7 @@ import re
|
|
|
15
15
|
|
|
16
16
|
import yaml
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import QuizGenerator.contentast as ca
|
|
19
19
|
from QuizGenerator.question import Question, QuestionRegistry, QuestionGroup
|
|
20
20
|
|
|
21
21
|
log = logging.getLogger(__name__)
|
|
@@ -34,12 +34,12 @@ class Quiz:
|
|
|
34
34
|
self.questions = questions
|
|
35
35
|
self.instructions = kwargs.get("instructions", "")
|
|
36
36
|
|
|
37
|
-
# Parse description with
|
|
37
|
+
# Parse description with content AST if provided
|
|
38
38
|
raw_description = kwargs.get("description", None)
|
|
39
39
|
if raw_description:
|
|
40
|
-
# Create a
|
|
41
|
-
desc_doc =
|
|
42
|
-
desc_doc.add_element(
|
|
40
|
+
# Create a content AST document from the description text
|
|
41
|
+
desc_doc = ca.Document()
|
|
42
|
+
desc_doc.add_element(ca.Paragraph([raw_description]))
|
|
43
43
|
self.description = desc_doc.render("html")
|
|
44
44
|
else:
|
|
45
45
|
self.description = None
|
|
@@ -418,8 +418,8 @@ class Quiz:
|
|
|
418
418
|
|
|
419
419
|
return optimized_questions
|
|
420
420
|
|
|
421
|
-
def get_quiz(self, **kwargs) ->
|
|
422
|
-
quiz =
|
|
421
|
+
def get_quiz(self, **kwargs) -> ca.Document:
|
|
422
|
+
quiz = ca.Document(title=self.name)
|
|
423
423
|
|
|
424
424
|
# Extract master RNG seed (if provided) and remove from kwargs
|
|
425
425
|
master_seed = kwargs.pop('rng_seed', None)
|
QuizGenerator/regenerate.py
CHANGED
|
@@ -39,12 +39,13 @@ the exact question and answer without needing the original exam file.
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
import argparse
|
|
42
|
+
import base64
|
|
42
43
|
import json
|
|
43
44
|
import sys
|
|
44
45
|
import logging
|
|
45
46
|
import os
|
|
46
47
|
from pathlib import Path
|
|
47
|
-
from typing import Dict, Any, Optional, List
|
|
48
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
48
49
|
|
|
49
50
|
# Load environment variables from .env file
|
|
50
51
|
try:
|
|
@@ -140,7 +141,39 @@ def parse_qr_data(qr_string: str) -> Dict[str, Any]:
|
|
|
140
141
|
return {}
|
|
141
142
|
|
|
142
143
|
|
|
143
|
-
def
|
|
144
|
+
def _inline_image_upload(img_data) -> str:
|
|
145
|
+
img_data.seek(0)
|
|
146
|
+
b64 = base64.b64encode(img_data.read()).decode("ascii")
|
|
147
|
+
return f"data:image/png;base64,{b64}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _resolve_upload_func(
|
|
151
|
+
image_mode: str,
|
|
152
|
+
upload_func: Optional[Callable]
|
|
153
|
+
) -> Optional[Callable]:
|
|
154
|
+
if image_mode == "inline":
|
|
155
|
+
return _inline_image_upload
|
|
156
|
+
if image_mode == "upload":
|
|
157
|
+
if upload_func is None:
|
|
158
|
+
raise ValueError("image_mode='upload' requires upload_func")
|
|
159
|
+
return upload_func
|
|
160
|
+
if image_mode == "none":
|
|
161
|
+
return None
|
|
162
|
+
raise ValueError(f"Unknown image_mode: {image_mode}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_html(element, upload_func=None, **kwargs) -> str:
|
|
166
|
+
if upload_func is None:
|
|
167
|
+
return element.render("html", **kwargs)
|
|
168
|
+
return element.render("html", upload_func=upload_func, **kwargs)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def regenerate_question_answer(
|
|
172
|
+
qr_data: Dict[str, Any],
|
|
173
|
+
*,
|
|
174
|
+
image_mode: str = "inline",
|
|
175
|
+
upload_func: Optional[Callable] = None
|
|
176
|
+
) -> Optional[Dict[str, Any]]:
|
|
144
177
|
"""
|
|
145
178
|
Regenerate question and extract answer using QR code metadata.
|
|
146
179
|
|
|
@@ -156,8 +189,9 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
156
189
|
"seed": int,
|
|
157
190
|
"version": str,
|
|
158
191
|
"answers": dict,
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
"explanation_markdown": str | None # Markdown explanation (None if not available)
|
|
193
|
+
"explanation_html": str | None # HTML explanation (None if not available)
|
|
194
|
+
}
|
|
161
195
|
"""
|
|
162
196
|
question_num = qr_data.get('q')
|
|
163
197
|
points = qr_data.get('pts')
|
|
@@ -220,8 +254,14 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
220
254
|
# Also store the raw answer objects for easier access
|
|
221
255
|
result['answer_objects'] = question.answers
|
|
222
256
|
|
|
257
|
+
resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
|
|
258
|
+
|
|
223
259
|
# Generate HTML answer key for grading
|
|
224
|
-
question_html =
|
|
260
|
+
question_html = _render_html(
|
|
261
|
+
question_ast.body,
|
|
262
|
+
show_answers=True,
|
|
263
|
+
upload_func=resolved_upload_func
|
|
264
|
+
)
|
|
225
265
|
result['answer_key_html'] = question_html
|
|
226
266
|
|
|
227
267
|
# Generate markdown explanation for students
|
|
@@ -232,6 +272,16 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
232
272
|
else:
|
|
233
273
|
result['explanation_markdown'] = explanation_markdown
|
|
234
274
|
|
|
275
|
+
# Generate HTML explanation (optional for web UIs)
|
|
276
|
+
explanation_html = _render_html(
|
|
277
|
+
question_ast.explanation,
|
|
278
|
+
upload_func=resolved_upload_func
|
|
279
|
+
)
|
|
280
|
+
if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
|
|
281
|
+
result["explanation_html"] = None
|
|
282
|
+
else:
|
|
283
|
+
result["explanation_html"] = explanation_html
|
|
284
|
+
|
|
235
285
|
log.info(f" Successfully regenerated question with {len(canvas_answers)} answer(s)")
|
|
236
286
|
|
|
237
287
|
return result
|
|
@@ -243,7 +293,13 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
|
|
|
243
293
|
return result
|
|
244
294
|
|
|
245
295
|
|
|
246
|
-
def regenerate_from_encrypted(
|
|
296
|
+
def regenerate_from_encrypted(
|
|
297
|
+
encrypted_data: str,
|
|
298
|
+
points: float = 1.0,
|
|
299
|
+
*,
|
|
300
|
+
image_mode: str = "inline",
|
|
301
|
+
upload_func: Optional[Callable] = None
|
|
302
|
+
) -> Dict[str, Any]:
|
|
247
303
|
"""
|
|
248
304
|
Regenerate question answers from encrypted QR code data (RECOMMENDED API).
|
|
249
305
|
|
|
@@ -253,6 +309,8 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
|
|
|
253
309
|
Args:
|
|
254
310
|
encrypted_data: The encrypted 's' field from the QR code JSON
|
|
255
311
|
points: Point value for the question (default: 1.0)
|
|
312
|
+
image_mode: "inline", "upload", or "none" for HTML image handling
|
|
313
|
+
upload_func: Optional upload function used when image_mode="upload"
|
|
256
314
|
|
|
257
315
|
Returns:
|
|
258
316
|
Dictionary with regenerated answers:
|
|
@@ -266,6 +324,7 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
|
|
|
266
324
|
"answer_objects": dict, # Raw Answer objects with values/tolerances
|
|
267
325
|
"answer_key_html": str, # HTML rendering of question with answers shown
|
|
268
326
|
"explanation_markdown": str | None # Markdown explanation (None if not available)
|
|
327
|
+
"explanation_html": str | None # HTML explanation (None if not available)
|
|
269
328
|
}
|
|
270
329
|
|
|
271
330
|
Raises:
|
|
@@ -287,12 +346,21 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
|
|
|
287
346
|
kwargs = decrypted.get('config', {})
|
|
288
347
|
|
|
289
348
|
# Use the existing regeneration logic
|
|
290
|
-
return regenerate_from_metadata(
|
|
349
|
+
return regenerate_from_metadata(
|
|
350
|
+
question_type,
|
|
351
|
+
seed,
|
|
352
|
+
version,
|
|
353
|
+
points,
|
|
354
|
+
kwargs,
|
|
355
|
+
image_mode=image_mode,
|
|
356
|
+
upload_func=upload_func
|
|
357
|
+
)
|
|
291
358
|
|
|
292
359
|
|
|
293
360
|
def regenerate_from_metadata(
|
|
294
361
|
question_type: str, seed: int, version: str,
|
|
295
|
-
points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None
|
|
362
|
+
points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None,
|
|
363
|
+
*, image_mode: str = "inline", upload_func: Optional[Callable] = None
|
|
296
364
|
) -> Dict[str, Any]:
|
|
297
365
|
"""
|
|
298
366
|
Regenerate question answers from explicit metadata fields.
|
|
@@ -306,6 +374,8 @@ def regenerate_from_metadata(
|
|
|
306
374
|
points: Point value for the question (default: 1.0)
|
|
307
375
|
kwargs: Optional dictionary of question-specific configuration parameters
|
|
308
376
|
(e.g., {"num_bits_va": 32, "max_value": 100})
|
|
377
|
+
image_mode: "inline", "upload", or "none" for HTML image handling
|
|
378
|
+
upload_func: Optional upload function used when image_mode="upload"
|
|
309
379
|
|
|
310
380
|
Returns:
|
|
311
381
|
Dictionary with regenerated answers (same format as regenerate_from_encrypted)
|
|
@@ -335,8 +405,14 @@ def regenerate_from_metadata(
|
|
|
335
405
|
# Extract answers
|
|
336
406
|
answer_kind, canvas_answers = question.get_answers()
|
|
337
407
|
|
|
408
|
+
resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
|
|
409
|
+
|
|
338
410
|
# Generate HTML answer key for grading
|
|
339
|
-
question_html =
|
|
411
|
+
question_html = _render_html(
|
|
412
|
+
question_ast.body,
|
|
413
|
+
show_answers=True,
|
|
414
|
+
upload_func=resolved_upload_func
|
|
415
|
+
)
|
|
340
416
|
|
|
341
417
|
# Generate markdown explanation for students
|
|
342
418
|
explanation_markdown = question_ast.explanation.render("markdown")
|
|
@@ -344,6 +420,13 @@ def regenerate_from_metadata(
|
|
|
344
420
|
if not explanation_markdown or "[Please reach out to your professor for clarification]" in explanation_markdown:
|
|
345
421
|
explanation_markdown = None
|
|
346
422
|
|
|
423
|
+
explanation_html = _render_html(
|
|
424
|
+
question_ast.explanation,
|
|
425
|
+
upload_func=resolved_upload_func
|
|
426
|
+
)
|
|
427
|
+
if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
|
|
428
|
+
explanation_html = None
|
|
429
|
+
|
|
347
430
|
result = {
|
|
348
431
|
"question_type": question_type,
|
|
349
432
|
"seed": seed,
|
|
@@ -355,7 +438,8 @@ def regenerate_from_metadata(
|
|
|
355
438
|
},
|
|
356
439
|
"answer_objects": question.answers,
|
|
357
440
|
"answer_key_html": question_html,
|
|
358
|
-
"explanation_markdown": explanation_markdown
|
|
441
|
+
"explanation_markdown": explanation_markdown,
|
|
442
|
+
"explanation_html": explanation_html
|
|
359
443
|
}
|
|
360
444
|
|
|
361
445
|
# Include kwargs in result if provided
|
|
@@ -407,6 +491,9 @@ def display_answer_summary(question_data: Dict[str, Any]) -> None:
|
|
|
407
491
|
if 'explanation_markdown' in question_data and question_data['explanation_markdown'] is not None:
|
|
408
492
|
print("Markdown explanation available in result['explanation_markdown']")
|
|
409
493
|
|
|
494
|
+
if 'explanation_html' in question_data and question_data['explanation_html'] is not None:
|
|
495
|
+
print("HTML explanation available in result['explanation_html']")
|
|
496
|
+
|
|
410
497
|
print("=" * 60)
|
|
411
498
|
|
|
412
499
|
|
|
@@ -445,6 +532,12 @@ def main():
|
|
|
445
532
|
action='store_true',
|
|
446
533
|
help='Enable verbose debug logging'
|
|
447
534
|
)
|
|
535
|
+
parser.add_argument(
|
|
536
|
+
'--image-mode',
|
|
537
|
+
choices=['inline', 'none'],
|
|
538
|
+
default='inline',
|
|
539
|
+
help='HTML image handling (default: inline)'
|
|
540
|
+
)
|
|
448
541
|
|
|
449
542
|
args = parser.parse_args()
|
|
450
543
|
|
|
@@ -467,7 +560,11 @@ def main():
|
|
|
467
560
|
if args.encrypted_str:
|
|
468
561
|
try:
|
|
469
562
|
log.info(f"Decoding encrypted string (points={args.points})")
|
|
470
|
-
result = regenerate_from_encrypted(
|
|
563
|
+
result = regenerate_from_encrypted(
|
|
564
|
+
args.encrypted_str,
|
|
565
|
+
args.points,
|
|
566
|
+
image_mode=args.image_mode
|
|
567
|
+
)
|
|
471
568
|
|
|
472
569
|
# Format result similar to regenerate_question_answer output
|
|
473
570
|
question_data = {
|
|
@@ -479,7 +576,8 @@ def main():
|
|
|
479
576
|
"answers": result["answers"],
|
|
480
577
|
"answer_objects": result["answer_objects"],
|
|
481
578
|
"answer_key_html": result["answer_key_html"],
|
|
482
|
-
"explanation_markdown": result.get("explanation_markdown")
|
|
579
|
+
"explanation_markdown": result.get("explanation_markdown"),
|
|
580
|
+
"explanation_html": result.get("explanation_html")
|
|
483
581
|
}
|
|
484
582
|
|
|
485
583
|
if "kwargs" in result:
|
|
@@ -517,7 +615,10 @@ def main():
|
|
|
517
615
|
continue
|
|
518
616
|
|
|
519
617
|
# Regenerate question and answer
|
|
520
|
-
question_data = regenerate_question_answer(
|
|
618
|
+
question_data = regenerate_question_answer(
|
|
619
|
+
qr_data,
|
|
620
|
+
image_mode=args.image_mode
|
|
621
|
+
)
|
|
521
622
|
|
|
522
623
|
if question_data:
|
|
523
624
|
results.append(question_data)
|
QuizGenerator/typst_utils.py
CHANGED
|
@@ -12,7 +12,7 @@ import tempfile
|
|
|
12
12
|
import textwrap
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Optional
|
|
15
|
-
|
|
15
|
+
import QuizGenerator.contentast as ca
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
log = logging.getLogger(__name__)
|
|
@@ -31,7 +31,7 @@ def measure_typst_content(typst_content: str, page_width_cm: float = 18.0) -> Op
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
# Get the Typst header which includes fillline and other helper functions
|
|
34
|
-
typst_header =
|
|
34
|
+
typst_header = ca.Document.TYPST_HEADER
|
|
35
35
|
|
|
36
36
|
# Create temporary Typst file with measurement wrapper
|
|
37
37
|
typst_code = textwrap.dedent(f"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QuizGenerator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
|
|
2
2
|
QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
|
|
3
3
|
QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
|
|
4
|
-
QuizGenerator/contentast.py,sha256=
|
|
4
|
+
QuizGenerator/contentast.py,sha256=8i7iF3cxcYbrVkpK89WdwhNtH4Bo25_rqDCxrSc0z8s,86281
|
|
5
5
|
QuizGenerator/generate.py,sha256=AWzNL0QTYDTcJFKaYfHIoRHvhx9MYRAbsD6z7E5_c9k,15733
|
|
6
|
-
QuizGenerator/misc.py,sha256=
|
|
7
|
-
QuizGenerator/mixins.py,sha256=
|
|
6
|
+
QuizGenerator/misc.py,sha256=MXrguUhhdrWSV4Hqdl4G21ktowODu1AcKy6-5mvy3aI,454
|
|
7
|
+
QuizGenerator/mixins.py,sha256=aFn2573APCh-XKn6U7Xy95Iyj1a-BsRXnorjpN1FT6E,18711
|
|
8
8
|
QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
|
|
9
9
|
QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
|
|
10
|
-
QuizGenerator/question.py,sha256=
|
|
11
|
-
QuizGenerator/quiz.py,sha256=
|
|
12
|
-
QuizGenerator/regenerate.py,sha256=
|
|
13
|
-
QuizGenerator/typst_utils.py,sha256=
|
|
10
|
+
QuizGenerator/question.py,sha256=CqKq_-HDzETrgEr-E8fQu1sRiMZv6ZoBAjig6gy34vw,31395
|
|
11
|
+
QuizGenerator/quiz.py,sha256=4W_3xZMLx-pMzB5mn8GOhbmE-7bYiIqYLtsVYxJSdVc,21463
|
|
12
|
+
QuizGenerator/regenerate.py,sha256=q8fGecZXx_sMUEpaMMmS8Zp3QW_m63WE9_5g6IKtKUk,19520
|
|
13
|
+
QuizGenerator/typst_utils.py,sha256=JGQn_u5bEHd8HAtjAHuZoVJwLkx-Rd4ZCBWffwFZa3o,3136
|
|
14
14
|
QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
|
|
15
15
|
QuizGenerator/canvas/canvas_interface.py,sha256=StMcdXgLvTA1EayQ44m_le2GXGQpDQnduYXVeUYsqW0,24618
|
|
16
16
|
QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnDNw,7594
|
|
17
17
|
QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
QuizGenerator/premade_questions/basic.py,sha256=
|
|
18
|
+
QuizGenerator/premade_questions/basic.py,sha256=hZCZHZxyTAQSDr-Hwo3xIMlU3L0-tyzfEZqvz7B-P64,3317
|
|
19
19
|
QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
|
|
20
|
-
QuizGenerator/premade_questions/cst334/languages.py,sha256=
|
|
21
|
-
QuizGenerator/premade_questions/cst334/math_questions.py,sha256=
|
|
22
|
-
QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=
|
|
20
|
+
QuizGenerator/premade_questions/cst334/languages.py,sha256=t4LEfVeJei0xqOHjUb9vV4yG1Oyp-LE-wmF4AY_oMbk,14909
|
|
21
|
+
QuizGenerator/premade_questions/cst334/math_questions.py,sha256=TW4VR6kmcYC8qMOn5vNwe1_7OCUKjPeOqJbwoKOWZ-s,9930
|
|
22
|
+
QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=S_Fndsq-RqWoFeIuKmMmCrdO7R1dFeQ0HrGWu93RRiE,53749
|
|
23
23
|
QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
|
|
24
|
-
QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=
|
|
25
|
-
QuizGenerator/premade_questions/cst334/process.py,sha256=
|
|
24
|
+
QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=eyCIwfy3pmSm-XEwIV-7i9uU0-kZcuq1D0Apm0m2SfA,17163
|
|
25
|
+
QuizGenerator/premade_questions/cst334/process.py,sha256=YrtrLZA8Uk63adpD2WOuXyIaJsFKoO5xZ0yYDzMFWqs,39593
|
|
26
26
|
QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
|
|
28
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=
|
|
29
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=
|
|
30
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=
|
|
31
|
-
QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=
|
|
28
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=6TzPi-NMI5CILcd3gbzRpEs3T_280SzA1kauTZMXC_g,13607
|
|
29
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=fEpYB4jFOJfyDdli0oh-zpsarC_w8fIGZBOXCkMOIGQ,10604
|
|
30
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=Tq_-EVaVmuVz6-V9Wbcp-oIlc2BJ5ehN4MdCx8G_Dk4,21824
|
|
31
|
+
QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=0R-nFeD3zsqJyde5CXWrF6Npjmpx6_HbzfCbThLi3os,2657
|
|
32
32
|
QuizGenerator/premade_questions/cst463/math_and_data/__init__.py,sha256=EbIaUrx7_aK9j3Gd8Mk08h9GocTq_0OoNu2trfNwaU8,202
|
|
33
|
-
QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=
|
|
34
|
-
QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=
|
|
33
|
+
QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=_oODuW4WMEVLuEJFmVjMWYTZn3RiE2MqlJSEIwRXNMo,28740
|
|
34
|
+
QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=nNu4Pqvy-zXO2Ly9UtqXIbx-gw2fUC40yii-PhuK-_c,12927
|
|
35
35
|
QuizGenerator/premade_questions/cst463/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
QuizGenerator/premade_questions/cst463/models/attention.py,sha256=
|
|
37
|
-
QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=
|
|
36
|
+
QuizGenerator/premade_questions/cst463/models/attention.py,sha256=i49u_bTbc_x0uLkmO-w7zQtLeoBpejVd78opVUnP9sg,5803
|
|
37
|
+
QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=yFqZJ-sKInGaFButUEuYiRRZkALqx8Tpv8I-fTfvQcU,6109
|
|
38
38
|
QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
|
|
39
|
-
QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=
|
|
40
|
-
QuizGenerator/premade_questions/cst463/models/text.py,sha256=
|
|
41
|
-
QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=
|
|
39
|
+
QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=CXoXHMNIhJRD2MEZ0WNemTKQLQRV7ahTnGVRjOe77vU,6903
|
|
40
|
+
QuizGenerator/premade_questions/cst463/models/text.py,sha256=vf9Lp9dvYs4Eok_ljt5MEw94O7YdhpP0JaDfDaPL0Vs,6705
|
|
41
|
+
QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=wz63jKluW3sFYOWUePgtcNQVpFJykyG3BxJzt0djrvY,7133
|
|
42
42
|
QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
|
|
43
|
-
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=LTsUZqdP5zdG8VB10u5FYkQMm2Q1wvlaFAOOVQQesmg,45176
|
|
44
44
|
QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
|
|
45
|
-
QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=
|
|
46
|
-
quizgenerator-0.
|
|
47
|
-
quizgenerator-0.
|
|
48
|
-
quizgenerator-0.
|
|
49
|
-
quizgenerator-0.
|
|
50
|
-
quizgenerator-0.
|
|
45
|
+
QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=KcYSDQQfbZcwNJQrBbcAmqIp69pYro8bJAWa1djDGsE,32263
|
|
46
|
+
quizgenerator-0.7.1.dist-info/METADATA,sha256=OvjwBmgGzYvr9VhlQ2zgi-UzKdCY7qRn-h6VjmZZ4r4,7212
|
|
47
|
+
quizgenerator-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
quizgenerator-0.7.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
|
|
49
|
+
quizgenerator-0.7.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
50
|
+
quizgenerator-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|