QuizGenerator 0.6.1__py3-none-any.whl → 0.6.3__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.
@@ -154,7 +154,8 @@ class CanvasCourse(LMSWrapper):
154
154
  # Get the question in a format that is ready for canvas (e.g. json)
155
155
  # Use large gaps between base seeds to avoid overlap with backoff attempts
156
156
  # Each variation gets seeds: base_seed, base_seed+1, base_seed+2, ... for backoffs
157
- base_seed = attempt_number * 1000
157
+ # Include question_i in the seed so different questions get different seed spaces
158
+ base_seed = (question_i * 100_000) + (attempt_number * 1000)
158
159
  question_for_canvas = question.get__canvas(self.course, canvas_quiz, rng_seed=base_seed)
159
160
 
160
161
  question_fingerprint = question_for_canvas["question_text"]
@@ -2315,61 +2315,53 @@ class ContentAST:
2315
2315
  return fractions.Fraction(decimal.Decimal(str(x)))
2316
2316
 
2317
2317
  @staticmethod
2318
- def accepted_strings(
2319
- value,
2320
- *,
2321
- allow_integer=True,
2322
- allow_simple_fraction=True,
2323
- max_denominator=720,
2324
- allow_mixed=False,
2325
- include_spaces=False,
2326
- include_fixed_even_if_integer=False
2327
- ):
2328
- """Return a sorted list of strings you can paste into Canvas as alternate correct answers."""
2329
- decimal.getcontext().prec = max(34, (ContentAST.Answer.DEFAULT_ROUNDING_DIGITS or 0) + 10)
2330
- f = ContentAST.Answer._to_fraction(value)
2318
+ def accepted_strings(value, max_denominator=720):
2319
+ """
2320
+ Return a sorted list of acceptable answer strings for Canvas.
2321
+
2322
+ Generates:
2323
+ - Integer form (if value is a whole number)
2324
+ - Fixed decimal with DEFAULT_ROUNDING_DIGITS (e.g., "1.0000")
2325
+ - Trimmed decimal without trailing zeros (e.g., "1.25")
2326
+ - Simple fraction if exactly representable (e.g., "5/4")
2327
+
2328
+ Examples:
2329
+ 1 ["1", "1.0000"]
2330
+ 1.25 ["1.25", "1.2500", "5/4"]
2331
+ 0.123444... → ["0.1234"]
2332
+ """
2333
+ rounding_digits = ContentAST.Answer.DEFAULT_ROUNDING_DIGITS
2334
+ decimal.getcontext().prec = max(34, rounding_digits + 10)
2335
+
2331
2336
  outs = set()
2332
2337
 
2333
- # Integer form
2334
- if f.denominator == 1 and allow_integer:
2335
- outs.add(str(f.numerator))
2336
- if include_fixed_even_if_integer:
2337
- q = decimal.Decimal(1).scaleb(-ContentAST.Answer.DEFAULT_ROUNDING_DIGITS)
2338
- d = decimal.Decimal(f.numerator).quantize(q, rounding=decimal.ROUND_HALF_UP)
2339
- outs.add(format(d, 'f'))
2340
-
2341
- # Simple fraction
2342
- if allow_simple_fraction:
2343
- fr = f.limit_denominator(max_denominator)
2344
- if fr == f:
2345
- a, b = fr.numerator, fr.denominator
2346
- if fr.denominator > 1:
2347
- outs.add(f"{a}/{b}")
2348
- if include_spaces:
2349
- outs.add(f"{a} / {b}")
2350
- if allow_mixed and b != 1 and abs(a) > b:
2351
- sign = '-' if a < 0 else ''
2352
- A = abs(a)
2353
- whole, rem = divmod(A, b)
2354
- outs.add(f"{sign}{whole} {rem}/{b}")
2355
- else:
2356
- return sorted(outs, key=lambda s: (len(s), s))
2357
-
2358
- # Fixed-decimal form
2359
- q = decimal.Decimal(1).scaleb(-ContentAST.Answer.DEFAULT_ROUNDING_DIGITS)
2360
- d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
2361
- outs.add(format(d, 'f'))
2362
-
2363
- # Trimmed decimal
2364
- if ContentAST.Answer.DEFAULT_ROUNDING_DIGITS:
2365
- q = decimal.Decimal(1).scaleb(-ContentAST.Answer.DEFAULT_ROUNDING_DIGITS)
2366
- d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
2367
- s = format(d, 'f').rstrip('0').rstrip('.')
2368
- if s.startswith('.'):
2369
- s = '0' + s
2370
- if s == '-0':
2371
- s = '0'
2372
- outs.add(s)
2338
+ # Round to our standard precision first
2339
+ q = decimal.Decimal(1).scaleb(-rounding_digits)
2340
+ rounded_decimal = decimal.Decimal(str(value)).quantize(q, rounding=decimal.ROUND_HALF_UP)
2341
+
2342
+ # Normalize negative zero to positive zero
2343
+ if rounded_decimal == 0:
2344
+ rounded_decimal = abs(rounded_decimal)
2345
+
2346
+ # Fixed decimal form (e.g., "1.2500")
2347
+ fixed_str = format(rounded_decimal, 'f')
2348
+ outs.add(fixed_str)
2349
+
2350
+ # Trimmed decimal form (e.g., "1.25")
2351
+ trimmed_str = fixed_str.rstrip('0').rstrip('.')
2352
+ if trimmed_str.startswith('.'):
2353
+ trimmed_str = '0' + trimmed_str
2354
+ outs.add(trimmed_str)
2355
+
2356
+ # Integer form (if it's a whole number after rounding)
2357
+ if rounded_decimal == rounded_decimal.to_integral_value():
2358
+ outs.add(str(int(rounded_decimal)))
2359
+
2360
+ # Fraction form (only if exactly representable with reasonable denominator)
2361
+ f = ContentAST.Answer._to_fraction(rounded_decimal)
2362
+ fr = f.limit_denominator(max_denominator)
2363
+ if fr == f and fr.denominator > 1:
2364
+ outs.add(f"{fr.numerator}/{fr.denominator}")
2373
2365
 
2374
2366
  return sorted(outs, key=lambda s: (len(s), s))
2375
2367
 
@@ -2378,6 +2370,10 @@ class ContentAST:
2378
2370
  """Fix -0.0 display issue."""
2379
2371
  return 0.0 if x == 0 else x
2380
2372
 
2373
+ @staticmethod
2374
+ def get_spacing_variations_of_list(l, remove_space=False):
2375
+ return [', '.join(l)] + ([', '.join(l)] if remove_space else [])
2376
+
2381
2377
 
2382
2378
  class AnswerTypes:
2383
2379
  # Multibase answers that can accept either hex, binary or decimal
@@ -2451,12 +2447,7 @@ class AnswerTypes:
2451
2447
  # Use the accepted_strings helper
2452
2448
  answer_strings = ContentAST.Answer.accepted_strings(
2453
2449
  self.value,
2454
- allow_integer=True,
2455
- allow_simple_fraction=True,
2456
- max_denominator=60,
2457
- allow_mixed=True,
2458
- include_spaces=False,
2459
- include_fixed_even_if_integer=True
2450
+ max_denominator=60
2460
2451
  )
2461
2452
 
2462
2453
  canvas_answers = [
@@ -2505,37 +2496,23 @@ class AnswerTypes:
2505
2496
  canvas_answers = [
2506
2497
  {
2507
2498
  "blank_id": self.key,
2508
- "answer_text": ', '.join(map(str, self.value)),
2509
- "answer_weight": 100 if self.correct else 0,
2510
- },
2511
- {
2512
- "blank_id": self.key,
2513
- "answer_text": ','.join(map(str, self.value)),
2499
+ "answer_text": spacing_variation,
2514
2500
  "answer_weight": 100 if self.correct else 0,
2515
2501
  }
2502
+ for spacing_variation in self.get_spacing_variations_of_list(map(str, self.value))
2516
2503
  ]
2517
2504
  else:
2518
2505
  canvas_answers = []
2519
-
2520
- # With spaces
2521
- canvas_answers.extend([
2506
+ canvas_answers = [
2522
2507
  {
2523
2508
  "blank_id": self.key,
2524
- "answer_text": ', '.join(map(str, possible_state)),
2509
+ "answer_text": spacing_variation,
2525
2510
  "answer_weight": 100 if self.correct else 0,
2526
2511
  }
2527
2512
  for possible_state in itertools.permutations(self.value)
2528
- ])
2513
+ for spacing_variation in self.get_spacing_variations_of_list(possible_state)
2514
+ ]
2529
2515
 
2530
- # Without spaces
2531
- canvas_answers.extend([
2532
- {
2533
- "blank_id": self.key,
2534
- "answer_text": ','.join(map(str, possible_state)),
2535
- "answer_weight": 100 if self.correct else 0,
2536
- }
2537
- for possible_state in itertools.permutations(self.value)
2538
- ])
2539
2516
  return canvas_answers
2540
2517
 
2541
2518
  def get_display_string(self) -> str:
@@ -2558,41 +2535,19 @@ class AnswerTypes:
2558
2535
 
2559
2536
  canvas_answers = []
2560
2537
  for combination in itertools.product(*answer_variations):
2561
- # Add without anything surrounding
2562
- canvas_answers.extend([
2563
- {
2564
- "blank_id": self.key,
2565
- "answer_weight": 100 if self.correct else 0,
2566
- "answer_text": f"{', '.join(combination)}",
2567
- },
2568
- {
2569
- "blank_id": self.key,
2570
- "answer_weight": 100 if self.correct else 0,
2571
- "answer_text": f"{','.join(combination)}",
2572
- },
2573
- # Add parentheses format
2574
- {
2575
- "blank_id": self.key,
2576
- "answer_weight": 100 if self.correct else 0,
2577
- "answer_text": f"({', '.join(list(combination))})",
2578
- },
2579
- {
2580
- "blank_id": self.key,
2581
- "answer_weight": 100 if self.correct else 0,
2582
- "answer_text": f"({','.join(list(combination))})",
2583
- },
2584
- # Add square brackets
2585
- {
2586
- "blank_id": self.key,
2587
- "answer_weight": 100 if self.correct else 0,
2588
- "answer_text": f"[{', '.join(list(combination))}]",
2589
- },
2590
- {
2591
- "blank_id": self.key,
2592
- "answer_weight": 100 if self.correct else 0,
2593
- "answer_text": f"[{','.join(list(combination))}]",
2594
- }
2595
- ])
2538
+ for spacing_variation in self.get_spacing_variations_of_list(list(combination)):
2539
+ canvas_answers.extend([
2540
+ {
2541
+ "blank_id": self.key,
2542
+ "answer_weight": 100 if self.correct else 0,
2543
+ "answer_text": f"{spacing_variation}",
2544
+ }, # without parenthesis
2545
+ {
2546
+ "blank_id": self.key,
2547
+ "answer_weight": 100 if self.correct else 0,
2548
+ "answer_text": f"({spacing_variation})",
2549
+ } # with parenthesis
2550
+ ])
2596
2551
  return canvas_answers
2597
2552
 
2598
2553
  def get_display_string(self) -> str:
QuizGenerator/generate.py CHANGED
@@ -50,6 +50,8 @@ def parse_args():
50
50
  # Testing flags
51
51
  parser.add_argument("--test_all", type=int, default=0, metavar="N",
52
52
  help="Generate N variations of ALL registered questions to test they work correctly")
53
+ parser.add_argument("--test_questions", nargs='+', metavar="NAME",
54
+ help="Only test specific question types by name (use with --test_all)")
53
55
  parser.add_argument("--strict", action="store_true",
54
56
  help="With --test_all, skip PDF/Canvas generation if any questions fail")
55
57
 
@@ -79,7 +81,8 @@ def test_all_questions(
79
81
  generate_pdf: bool = False,
80
82
  use_typst: bool = True,
81
83
  canvas_course=None,
82
- strict: bool = False
84
+ strict: bool = False,
85
+ question_filter: list = None
83
86
  ):
84
87
  """
85
88
  Test all registered questions by generating N variations of each.
@@ -93,11 +96,26 @@ def test_all_questions(
93
96
  use_typst: If True, use Typst for PDF generation; otherwise use LaTeX
94
97
  canvas_course: If provided, push a test quiz to this Canvas course
95
98
  strict: If True, skip PDF/Canvas generation if any questions fail
99
+ question_filter: If provided, only test questions whose names contain one of these strings (case-insensitive)
96
100
  """
97
101
  # Ensure all premade questions are loaded
98
102
  QuestionRegistry.load_premade_questions()
99
103
 
100
104
  registered_questions = QuestionRegistry._registry
105
+
106
+ # Filter questions if a filter list is provided
107
+ if question_filter:
108
+ filter_lower = [f.lower() for f in question_filter]
109
+ registered_questions = {
110
+ name: cls for name, cls in registered_questions.items()
111
+ if any(f in name.lower() for f in filter_lower)
112
+ }
113
+ if not registered_questions:
114
+ print(f"No questions matched filter: {question_filter}")
115
+ print(f"Available questions: {sorted(QuestionRegistry._registry.keys())}")
116
+ return False
117
+ print(f"Filtered to {len(registered_questions)} questions matching: {question_filter}")
118
+
101
119
  total_questions = len(registered_questions)
102
120
 
103
121
  # Test defaults for questions that require external input
@@ -437,7 +455,8 @@ def main():
437
455
  generate_pdf=True,
438
456
  use_typst=getattr(args, 'typst', True),
439
457
  canvas_course=canvas_course,
440
- strict=args.strict
458
+ strict=args.strict,
459
+ question_filter=args.test_questions
441
460
  )
442
461
  exit(0 if success else 1)
443
462
 
@@ -324,7 +324,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
324
324
  jobs_to_run=jobs,
325
325
  selector=(lambda j, curr_time: (j.last_run, j.job_id)),
326
326
  preemptable=True,
327
- time_quantum=1e-04
327
+ time_quantum=1e-05
328
328
  )
329
329
  case _:
330
330
  self.run_simulation(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.6.1
3
+ Version: 0.6.3
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,8 +1,8 @@
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=MzM5yaQ1XVLIMh9ag-KYWJ9qrD539aQZkgDp8v7At_E,93788
5
- QuizGenerator/generate.py,sha256=bOkR6JhGQE0r4zbRWhC_3Dsj5hOEk6-jzFLcjk-qwKg,14811
4
+ QuizGenerator/contentast.py,sha256=vO6t1VrRApS2zwW0Pi0PFlcKW_Li5R2bpEcCQP6a9Dc,92331
5
+ QuizGenerator/generate.py,sha256=AWzNL0QTYDTcJFKaYfHIoRHvhx9MYRAbsD6z7E5_c9k,15733
6
6
  QuizGenerator/misc.py,sha256=wtlrEpmEpoE6vNRmgjNUmuWnRdQKSCYfrqeoTagNaxg,464
7
7
  QuizGenerator/mixins.py,sha256=HEhdGdeghqGWoajADTAIdUjkzwDSYl1b65LAkUdV50U,19211
8
8
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
@@ -12,7 +12,7 @@ QuizGenerator/quiz.py,sha256=f2HLrawUlu3ULkNDzcihBWAt-e-49AIPz_l1edMAEQ0,21503
12
12
  QuizGenerator/regenerate.py,sha256=Uh4B9aKQvL3zD7PT-uH-GvrcSuUygV1BimvPVuErc-g,16525
13
13
  QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
14
14
  QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
15
- QuizGenerator/canvas/canvas_interface.py,sha256=wsEWh2lonUMgmbtXF-Zj59CAM_0NInoaERqsujlYMfc,24501
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
18
  QuizGenerator/premade_questions/basic.py,sha256=vMyCIYU0IJBjQVE-XVzHr9axq_kZL2ka4K1MaqeQwXM,3428
@@ -22,7 +22,7 @@ QuizGenerator/premade_questions/cst334/math_questions.py,sha256=zwkm3OLOkDZ_fbPF
22
22
  QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=PUyXIoyrGImXhucx6KBgoAEYmQCzTSCz0DWUu_xo6Kc,54371
23
23
  QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
24
24
  QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=pb63H47WlSsHi_nHRVhbwUHeybF2zbWL8vXbwOguAL0,17474
25
- QuizGenerator/premade_questions/cst334/process.py,sha256=ERAdardtXSwzh3SDrlkVQJr4oO0F9cZtCZZ8dkJRsfw,39865
25
+ QuizGenerator/premade_questions/cst334/process.py,sha256=aZKbsa9Cvh20HooaRV7CXv5kFAGvTkiWmYpQn6J62Nk,39865
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
28
  QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=PdWAJjgsiwYQsxeLlQiVDd3m97RUjUY1KJTJxyrrdRI,13984
@@ -43,8 +43,8 @@ QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=
43
43
  QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=luWlTfj1UM1yQDQzs_tNzTV67qXhRUBwNt8QrV74XHs,46115
44
44
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
45
45
  QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=8Wo38kTd_n0Oau2ERpvcudB9uJiOVDYYQNeWu9v4Tyo,33516
46
- quizgenerator-0.6.1.dist-info/METADATA,sha256=2Odb3SpBU-voYkEMehqtzhjU1K_Tn4OZo5vmY88HM9I,7212
47
- quizgenerator-0.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- quizgenerator-0.6.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
- quizgenerator-0.6.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
- quizgenerator-0.6.1.dist-info/RECORD,,
46
+ quizgenerator-0.6.3.dist-info/METADATA,sha256=BfoHO7-H8rocvQsKEeyHig-wwhhb7VoLx6yMEPaZj-Q,7212
47
+ quizgenerator-0.6.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ quizgenerator-0.6.3.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
+ quizgenerator-0.6.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
+ quizgenerator-0.6.3.dist-info/RECORD,,