QuizGenerator 0.8.0__tar.gz → 0.8.1__tar.gz

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.
Files changed (58) hide show
  1. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/PKG-INFO +1 -1
  2. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/contentast.py +42 -9
  3. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/process.py +1 -7
  4. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -1
  5. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/question.py +37 -0
  6. quizgenerator-0.8.1/pyproject.toml +76 -0
  7. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/uv.lock +1 -1
  8. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/.envrc +0 -0
  9. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/.gitignore +0 -0
  10. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/CODEOWNERS +0 -0
  11. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/LICENSE +0 -0
  12. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/__init__.py +0 -0
  13. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/__main__.py +0 -0
  14. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/canvas/__init__.py +0 -0
  15. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/canvas/canvas_interface.py +0 -0
  16. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/canvas/classes.py +0 -0
  17. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/constants.py +0 -0
  18. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/generate.py +0 -0
  19. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/misc.py +0 -0
  20. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/mixins.py +0 -0
  21. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/performance.py +0 -0
  22. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/__init__.py +0 -0
  23. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/basic.py +0 -0
  24. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/__init__.py +0 -0
  25. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/languages.py +0 -0
  26. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/math_questions.py +0 -0
  27. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/memory_questions.py +0 -0
  28. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +0 -0
  29. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst334/persistence_questions.py +0 -0
  30. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  31. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +0 -0
  32. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +0 -0
  33. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +0 -0
  34. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +0 -0
  35. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +0 -0
  36. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +0 -0
  37. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -0
  38. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  39. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/attention.py +0 -0
  40. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/cnns.py +0 -0
  41. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/matrices.py +0 -0
  42. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/rnns.py +0 -0
  43. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/text.py +0 -0
  44. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/models/weight_counting.py +0 -0
  45. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +0 -0
  46. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +0 -0
  47. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +0 -0
  48. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +0 -0
  49. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/qrcode_generator.py +0 -0
  50. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/quiz.py +0 -0
  51. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/regenerate.py +0 -0
  52. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/QuizGenerator/typst_utils.py +0 -0
  53. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/README.md +0 -0
  54. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/examples/web_ui_integration_example.py +0 -0
  55. /quizgenerator-0.8.0/pyproject.toml → /quizgenerator-0.8.1/pyproject_prev.toml +0 -0
  56. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/scripts/generate_practice_yaml.sh +0 -0
  57. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/scripts/print.sh +0 -0
  58. {quizgenerator-0.8.0 → quizgenerator-0.8.1}/scripts/vendor_lms_interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.8.0
3
+ Version: 0.8.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
@@ -2263,6 +2263,10 @@ class Answer(Leaf):
2263
2263
  unit_part = f" {self.unit}" if self.unit else ""
2264
2264
 
2265
2265
  return f"{label_part} {blank}{unit_part}".strip()
2266
+
2267
+ @classmethod
2268
+ def get_entry_warning(cls) -> List[str] | None:
2269
+ return None
2266
2270
 
2267
2271
  # Factory methods for common answer types
2268
2272
  @classmethod
@@ -2330,17 +2334,19 @@ class Answer(Leaf):
2330
2334
  0.123444... → ["0.1234"]
2331
2335
  """
2332
2336
  rounding_digits = Answer.DEFAULT_ROUNDING_DIGITS
2333
- decimal.getcontext().prec = max(34, rounding_digits + 10)
2334
-
2335
2337
  outs = set()
2336
2338
 
2337
2339
  # Round to our standard precision first
2340
+ decimal.getcontext().prec = max(34, rounding_digits + 10)
2338
2341
  q = decimal.Decimal(1).scaleb(-rounding_digits)
2339
- rounded_decimal = decimal.Decimal(str(value)).quantize(q, rounding=decimal.ROUND_HALF_UP)
2340
-
2341
- # Normalize negative zero to positive zero
2342
- if rounded_decimal == 0:
2343
- rounded_decimal = abs(rounded_decimal)
2342
+ if isinstance(value, str) and '/' in value:
2343
+ f = Answer._to_fraction(value)
2344
+ decimal_value = decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)
2345
+ elif isinstance(value, fractions.Fraction):
2346
+ decimal_value = decimal.Decimal(value.numerator) / decimal.Decimal(value.denominator)
2347
+ else:
2348
+ decimal_value = decimal.Decimal(str(value))
2349
+ rounded_decimal = decimal_value.quantize(q, rounding=decimal.ROUND_HALF_UP)
2344
2350
 
2345
2351
  # Fixed decimal form (e.g., "1.2500")
2346
2352
  fixed_str = format(rounded_decimal, 'f')
@@ -2431,6 +2437,15 @@ class AnswerTypes:
2431
2437
 
2432
2438
  # Concrete type answers
2433
2439
  class Float(Answer):
2440
+ @classmethod
2441
+ def get_entry_warning(cls) -> List[str] | None:
2442
+ digits = Answer.DEFAULT_ROUNDING_DIGITS
2443
+ return [
2444
+ f"Round floats to {digits} decimal places (fewer if exact, e.g., `1.25`). "
2445
+ "No mixed numbers (e.g. use `5/4`, not `1 1/4`). "
2446
+ "Integers as integers (e.g., `2`, not `2/1`)."
2447
+ ]
2448
+
2434
2449
  def get_for_canvas(self, single_answer=False) -> List[dict]:
2435
2450
  if single_answer:
2436
2451
  canvas_answers = [
@@ -2460,8 +2475,8 @@ class AnswerTypes:
2460
2475
  return canvas_answers
2461
2476
 
2462
2477
  def get_display_string(self) -> str:
2463
- rounded = round(self.value, Answer.DEFAULT_ROUNDING_DIGITS)
2464
- return f"{self.fix_negative_zero(rounded)}"
2478
+ answer_strings = Answer.accepted_strings(self.value)
2479
+ return answer_strings[0] if len(answer_strings) > 0 else f"{self.value}"
2465
2480
 
2466
2481
  class Int(Answer):
2467
2482
 
@@ -2486,6 +2501,10 @@ class AnswerTypes:
2486
2501
  pass
2487
2502
 
2488
2503
  class List(Answer):
2504
+ @classmethod
2505
+ def get_entry_warning(cls) -> List[str] | None:
2506
+ return ["Enter lists as comma-separated values with a space after the comma (e.g., `1, 2, 3`)."]
2507
+
2489
2508
  def __init__(self, order_matters=True, *args, **kwargs):
2490
2509
  super().__init__(*args, **kwargs)
2491
2510
  self.order_matters = order_matters
@@ -2523,6 +2542,13 @@ class AnswerTypes:
2523
2542
  """
2524
2543
  These are self-contained vectors that will go in a single answer block
2525
2544
  """
2545
+
2546
+ @classmethod
2547
+ def get_entry_warning(cls) -> List[str] | None:
2548
+ return [
2549
+ "Enter vectors as comma-separated values with a space after the comma, "
2550
+ "with optional parentheses (e.g., `1, 2` or `(1, 2)`)."
2551
+ ]
2526
2552
 
2527
2553
  # Canvas export methods (from misc.Answer)
2528
2554
  def get_for_canvas(self, single_answer=False) -> List[dict]:
@@ -2565,6 +2591,13 @@ class AnswerTypes:
2565
2591
  """
2566
2592
  Matrix answers generate multiple blank_ids (e.g., M_0_0, M_0_1, M_1_0, M_1_1).
2567
2593
  """
2594
+
2595
+ @classmethod
2596
+ def get_entry_warning(cls) -> List[str] | None:
2597
+ return [
2598
+ "For result matrices, enter `-` in any cell that does not exist.",
2599
+ *AnswerTypes.Float.get_entry_warning()
2600
+ ]
2568
2601
 
2569
2602
  def __init__(self, value, *args, **kwargs):
2570
2603
  super().__init__(value=value, *args, **kwargs)
@@ -433,13 +433,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
433
433
  f"Break any ties using the job number."
434
434
  )
435
435
 
436
- instructions = ca.OnlyHtml([ca.Paragraph([
437
- f"Please format answer as fractions, mixed numbers, or numbers rounded to a maximum of {ca.Answer.DEFAULT_ROUNDING_DIGITS} digits after the decimal. "
438
- "Examples of appropriately formatted answers would be `0`, `3/2`, `1 1/3`, `1.6667`, and `1.25`. "
439
- "Note that answers that can be rounded to whole numbers should be, rather than being left in fractional form."
440
- ])])
441
-
442
- body = cls.create_fill_in_table_body(intro_text, instructions, scheduling_table)
436
+ body = cls.create_fill_in_table_body(intro_text, None, scheduling_table)
443
437
  body.add_element(average_block)
444
438
  return body
445
439
 
@@ -352,7 +352,6 @@ class MatrixMultiplication(MatrixMathQuestion):
352
352
  answers.extend(table_answers)
353
353
  body.add_element(
354
354
  ca.OnlyHtml([
355
- ca.Paragraph(["Result matrix (use '-' if cell doesn't exist):"]),
356
355
  table
357
356
  ])
358
357
  )
@@ -382,6 +382,7 @@ class RegenerableChoiceMixin:
382
382
  del choice_info['_temp_fixed_value']
383
383
 
384
384
  class Question(abc.ABC):
385
+ AUTO_ENTRY_WARNINGS = True
385
386
  """
386
387
  Base class for all quiz questions with cross-format rendering support.
387
388
 
@@ -584,6 +585,10 @@ class Question(abc.ABC):
584
585
 
585
586
  can_be_numerical = self._can_be_numerical_from_answers(answers)
586
587
 
588
+ if self.AUTO_ENTRY_WARNINGS:
589
+ warnings = self._entry_warnings_from_answers(answers)
590
+ components.body = self._append_entry_warnings(components.body, warnings)
591
+
587
592
  config_params = dict(self.config_params)
588
593
  if isinstance(ctx, dict) and ctx.get("_config_params"):
589
594
  config_params.update(ctx.get("_config_params"))
@@ -723,6 +728,38 @@ class Question(abc.ABC):
723
728
  merged.append(ans)
724
729
  return merged
725
730
 
731
+ @classmethod
732
+ def _entry_warnings_from_answers(cls, answers: List[ca.Answer]) -> List[str]:
733
+ warnings: List[str] = []
734
+ seen: set[str] = set()
735
+ for answer in answers:
736
+ warning = None
737
+ if hasattr(answer.__class__, "get_entry_warning"):
738
+ warning = answer.__class__.get_entry_warning()
739
+ if not warning:
740
+ continue
741
+ if isinstance(warning, str):
742
+ warning_list = [warning]
743
+ else:
744
+ warning_list = list(warning)
745
+ for item in warning_list:
746
+ if item and item not in seen:
747
+ warnings.append(item)
748
+ seen.add(item)
749
+ return warnings
750
+
751
+ @classmethod
752
+ def _append_entry_warnings(cls, body: ca.Element, warnings: List[str]) -> ca.Element:
753
+ if not warnings:
754
+ return body
755
+ notes_lines = ["**Notes for answer entry**", ""]
756
+ notes_lines.extend(f"- {warning}" for warning in warnings)
757
+ warning_elements = ca.OnlyHtml([ca.Text("\n".join(notes_lines))])
758
+ if isinstance(body, ca.Container):
759
+ body.add_element(warning_elements)
760
+ return body
761
+ return ca.Section([body, warning_elements])
762
+
726
763
  @classmethod
727
764
  def _can_be_numerical_from_answers(cls, answers: List[ca.Answer]) -> bool:
728
765
  return (
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "QuizGenerator"
7
+ version = "0.8.1"
8
+ description = "Generate randomized quiz questions for Canvas LMS and PDF exams"
9
+ readme = "README.md"
10
+ license = {text = "GPL-3.0-or-later"}
11
+ authors = [
12
+ {name = "Sam Ogden", email = "samuel.s.ogden@gmail.com"},
13
+ ]
14
+ requires-python = ">=3.12"
15
+
16
+ keywords = ["education", "quiz", "canvas", "lms", "assessment", "teaching", "exam", "testing"]
17
+
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Education",
21
+ "Topic :: Education :: Testing",
22
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ ]
26
+
27
+ dependencies = [
28
+ "Jinja2==3.1.3",
29
+ "python-dotenv==1.0.1",
30
+ "PyYAML==6.0.1",
31
+ "requests==2.32.2",
32
+ "pypandoc~=1.6.3",
33
+ "pytablewriter~=1.2.0",
34
+ "pylatex>=1.4.2",
35
+ "matplotlib",
36
+ "sympy>=1.14.0",
37
+ "markdown>=3.9",
38
+ "segno>=1.6.0",
39
+ "cryptography>=41.0.0",
40
+ "graphviz>=0.21",
41
+ "canvasapi==3.2.0",
42
+ "keras>=3.12.0",
43
+ "tensorflow>=2.20.0",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/OtterDen-Lab/QuizGenerator"
48
+ Documentation = "https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation"
49
+ Repository = "https://github.com/OtterDen-Lab/QuizGenerator"
50
+ "Bug Tracker" = "https://github.com/OtterDen-Lab/QuizGenerator/issues"
51
+
52
+ [project.scripts]
53
+ quizgen = "QuizGenerator.generate:main"
54
+ quizregen = "QuizGenerator.regenerate:main"
55
+
56
+ [project.optional-dependencies]
57
+ grading = [
58
+ "pyzbar>=0.1.9",
59
+ "pillow>=10.0.0",
60
+ ]
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["QuizGenerator"]
64
+
65
+ [tool.hatch.metadata]
66
+ allow-direct-references = true
67
+
68
+ # Entry point group for custom question types (for plugin developers)
69
+ # Users can register custom question types in their own packages using:
70
+ # [project.entry-points."quizgenerator.questions"]
71
+ # my_custom_question = "my_package.module:MyQuestionClass"
72
+ #
73
+ # Example:
74
+ # [project.entry-points."quizgenerator.questions"]
75
+ # advanced_scheduling = "university_questions.os:AdvancedSchedulingQuestion"
76
+ # memory_hierarchy = "university_questions.os:MemoryHierarchyQuestion"
@@ -1214,7 +1214,7 @@ wheels = [
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "quizgenerator"
1217
- version = "0.8.0"
1217
+ version = "0.8.1"
1218
1218
  source = { editable = "." }
1219
1219
  dependencies = [
1220
1220
  { name = "canvasapi" },
File without changes
File without changes
File without changes
File without changes
File without changes