canvaslms 5.5__tar.gz → 5.6__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 (112) hide show
  1. {canvaslms-5.5 → canvaslms-5.6}/PKG-INFO +1 -1
  2. {canvaslms-5.5 → canvaslms-5.6}/pyproject.toml +1 -1
  3. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/modules.nw +22 -7
  4. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/quizzes.nw +68 -0
  5. {canvaslms-5.5 → canvaslms-5.6}/LICENSE +0 -0
  6. {canvaslms-5.5 → canvaslms-5.6}/README.md +0 -0
  7. {canvaslms-5.5 → canvaslms-5.6}/makefiles/.circleci/config.yml +0 -0
  8. {canvaslms-5.5 → canvaslms-5.6}/makefiles/.git +0 -0
  9. {canvaslms-5.5 → canvaslms-5.6}/makefiles/.gitignore +0 -0
  10. {canvaslms-5.5 → canvaslms-5.6}/makefiles/CONTRIBUTING.md +0 -0
  11. {canvaslms-5.5 → canvaslms-5.6}/makefiles/Dockerfile +0 -0
  12. {canvaslms-5.5 → canvaslms-5.6}/makefiles/Dockerfile.nw +0 -0
  13. {canvaslms-5.5 → canvaslms-5.6}/makefiles/Dockerfile.tex +0 -0
  14. {canvaslms-5.5 → canvaslms-5.6}/makefiles/LICENSE +0 -0
  15. {canvaslms-5.5 → canvaslms-5.6}/makefiles/Makefile +0 -0
  16. {canvaslms-5.5 → canvaslms-5.6}/makefiles/Makefile.nw +0 -0
  17. {canvaslms-5.5 → canvaslms-5.6}/makefiles/Makefile.tex +0 -0
  18. {canvaslms-5.5 → canvaslms-5.6}/makefiles/README.md +0 -0
  19. {canvaslms-5.5 → canvaslms-5.6}/makefiles/doc.mk +0 -0
  20. {canvaslms-5.5 → canvaslms-5.6}/makefiles/doc.mk.nw +0 -0
  21. {canvaslms-5.5 → canvaslms-5.6}/makefiles/doc.tex +0 -0
  22. {canvaslms-5.5 → canvaslms-5.6}/makefiles/exam.bib +0 -0
  23. {canvaslms-5.5 → canvaslms-5.6}/makefiles/exam.mk +0 -0
  24. {canvaslms-5.5 → canvaslms-5.6}/makefiles/exam.mk.nw +0 -0
  25. {canvaslms-5.5 → canvaslms-5.6}/makefiles/exam.tex +0 -0
  26. {canvaslms-5.5 → canvaslms-5.6}/makefiles/gitattributes +0 -0
  27. {canvaslms-5.5 → canvaslms-5.6}/makefiles/haskell.mk +0 -0
  28. {canvaslms-5.5 → canvaslms-5.6}/makefiles/haskell.mk.nw +0 -0
  29. {canvaslms-5.5 → canvaslms-5.6}/makefiles/haskell.tex +0 -0
  30. {canvaslms-5.5 → canvaslms-5.6}/makefiles/intro.tex +0 -0
  31. {canvaslms-5.5 → canvaslms-5.6}/makefiles/latexmkrc +0 -0
  32. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/_minted/AAA0D43723DCC5BE9DB71A96B52C1142.highlight.minted +0 -0
  33. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/_minted/E5724293DA12A769F97F0E91498CAEEF.highlight.minted +0 -0
  34. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/_minted/FFC039B25D180E99FC9FEBDB2D42EBAF.highlight.minted +0 -0
  35. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/_minted/_7945F0071C510291A619CE1658F673CB.index.minted +0 -0
  36. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/_minted/default.style.minted +0 -0
  37. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.aux +0 -0
  38. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.bbl +0 -0
  39. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.bcf +0 -0
  40. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.blg +0 -0
  41. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.fdb_latexmk +0 -0
  42. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.fls +0 -0
  43. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.log +0 -0
  44. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.out +0 -0
  45. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.pdf +0 -0
  46. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.run.xml +0 -0
  47. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.toc +0 -0
  48. {canvaslms-5.5 → canvaslms-5.6}/makefiles/ltxobj/makefiles.xdv +0 -0
  49. {canvaslms-5.5 → canvaslms-5.6}/makefiles/makefiles.bib +0 -0
  50. {canvaslms-5.5 → canvaslms-5.6}/makefiles/makefiles.tex +0 -0
  51. {canvaslms-5.5 → canvaslms-5.6}/makefiles/miun.course.mk +0 -0
  52. {canvaslms-5.5 → canvaslms-5.6}/makefiles/miun.depend.mk +0 -0
  53. {canvaslms-5.5 → canvaslms-5.6}/makefiles/miun.docs.mk +0 -0
  54. {canvaslms-5.5 → canvaslms-5.6}/makefiles/miun.port.mk +0 -0
  55. {canvaslms-5.5 → canvaslms-5.6}/makefiles/miun.pub.mk +0 -0
  56. {canvaslms-5.5 → canvaslms-5.6}/makefiles/noweb.mk +0 -0
  57. {canvaslms-5.5 → canvaslms-5.6}/makefiles/noweb.mk.nw +0 -0
  58. {canvaslms-5.5 → canvaslms-5.6}/makefiles/noweb.tex +0 -0
  59. {canvaslms-5.5 → canvaslms-5.6}/makefiles/pkg.mk +0 -0
  60. {canvaslms-5.5 → canvaslms-5.6}/makefiles/pkg.mk.nw +0 -0
  61. {canvaslms-5.5 → canvaslms-5.6}/makefiles/pkg.tex +0 -0
  62. {canvaslms-5.5 → canvaslms-5.6}/makefiles/portability.mk +0 -0
  63. {canvaslms-5.5 → canvaslms-5.6}/makefiles/portability.mk.nw +0 -0
  64. {canvaslms-5.5 → canvaslms-5.6}/makefiles/portability.tex +0 -0
  65. {canvaslms-5.5 → canvaslms-5.6}/makefiles/preamble.tex +0 -0
  66. {canvaslms-5.5 → canvaslms-5.6}/makefiles/pub.mk +0 -0
  67. {canvaslms-5.5 → canvaslms-5.6}/makefiles/pub.mk.nw +0 -0
  68. {canvaslms-5.5 → canvaslms-5.6}/makefiles/pub.tex +0 -0
  69. {canvaslms-5.5 → canvaslms-5.6}/makefiles/results.mk +0 -0
  70. {canvaslms-5.5 → canvaslms-5.6}/makefiles/results.mk.nw +0 -0
  71. {canvaslms-5.5 → canvaslms-5.6}/makefiles/results.tex +0 -0
  72. {canvaslms-5.5 → canvaslms-5.6}/makefiles/subdir.mk +0 -0
  73. {canvaslms-5.5 → canvaslms-5.6}/makefiles/subdir.mk.nw +0 -0
  74. {canvaslms-5.5 → canvaslms-5.6}/makefiles/subdir.tex +0 -0
  75. {canvaslms-5.5 → canvaslms-5.6}/makefiles/tex.bib +0 -0
  76. {canvaslms-5.5 → canvaslms-5.6}/makefiles/tex.mk +0 -0
  77. {canvaslms-5.5 → canvaslms-5.6}/makefiles/tex.mk.nw +0 -0
  78. {canvaslms-5.5 → canvaslms-5.6}/makefiles/tex.tex +0 -0
  79. {canvaslms-5.5 → canvaslms-5.6}/makefiles/transform.bib +0 -0
  80. {canvaslms-5.5 → canvaslms-5.6}/makefiles/transform.mk +0 -0
  81. {canvaslms-5.5 → canvaslms-5.6}/makefiles/transform.mk.nw +0 -0
  82. {canvaslms-5.5 → canvaslms-5.6}/makefiles/transform.tex +0 -0
  83. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/Makefile +0 -0
  84. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/__init__.py +0 -0
  85. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/Makefile +0 -0
  86. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/assignments.nw +0 -0
  87. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/cache.nw +0 -0
  88. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/calendar.nw +0 -0
  89. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/cli.nw +0 -0
  90. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/content.nw +0 -0
  91. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/courses.nw +0 -0
  92. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/discussions.nw +0 -0
  93. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/grade.nw +0 -0
  94. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/login.nw +0 -0
  95. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/pages.nw +0 -0
  96. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/results.nw +0 -0
  97. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/results.py.broken +0 -0
  98. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/submissions.nw +0 -0
  99. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/users.nw +0 -0
  100. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/cli/utils.nw +0 -0
  101. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/Makefile +0 -0
  102. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/conjunctavg.nw +0 -0
  103. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/conjunctavgsurvey.nw +0 -0
  104. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/disjunctmax.nw +0 -0
  105. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/grades.nw +0 -0
  106. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/grades.py +0 -0
  107. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/maxgradesurvey.nw +0 -0
  108. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/grades/tilkryLAB1.nw +0 -0
  109. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/hacks/Makefile +0 -0
  110. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/hacks/__init__.py +0 -0
  111. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/hacks/attachment_cache.nw +0 -0
  112. {canvaslms-5.5 → canvaslms-5.6}/src/canvaslms/hacks/canvasapi.nw +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvaslms
3
- Version: 5.5
3
+ Version: 5.6
4
4
  Summary: Command-line interface to Canvas LMS
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "canvaslms"
3
- version = "5.5"
3
+ version = "5.6"
4
4
  description = "Command-line interface to Canvas LMS"
5
5
  authors = [
6
6
  {name = "Daniel Bosk", email = "daniel@bosk.se"}
@@ -486,6 +486,14 @@ def get_item_modules(course, item_type, item_id):
486
486
  Returns:
487
487
  List of module names containing this item
488
488
  """
489
+ target_url = None
490
+ if item_type == 'Page':
491
+ try:
492
+ target_url = course.get_page(item_id).url
493
+ except Exception:
494
+ # If we can't resolve the target page, fall back to raw comparisons.
495
+ target_url = item_id
496
+
489
497
  modules = []
490
498
  for module in course.get_modules():
491
499
  try:
@@ -514,10 +522,10 @@ elif item_type == 'Page':
514
522
  if item.page_url == item_id:
515
523
  modules.append(module.name)
516
524
  break
517
- # Check if URLs resolve to same page (handles redirects)
525
+ # Compare canonical URLs to avoid duplicates when Canvas redirects old slugs.
518
526
  try:
519
527
  resolved_page = course.get_page(item.page_url)
520
- if resolved_page.url == item_id:
528
+ if resolved_page.url == target_url:
521
529
  modules.append(module.name)
522
530
  break
523
531
  except Exception:
@@ -598,11 +606,18 @@ with its new canonical URL.
598
606
  Without handling this, the direct comparison [[item.page_url == item_id]] would
599
607
  fail, causing the page to be added to the module again (creating duplicates).
600
608
 
601
- To detect this, we fetch the page using the module item's [[page_url]] and
602
- compare its canonical [[url]] attribute with [[item_id]]. This adds an API call
603
- per page item, but only when the direct comparison fails.
609
+ To detect this, we fetch both the target page ([[item_id]]) and the module
610
+ item's page ([[item.page_url]]) and compare canonical [[url]] values. This adds
611
+ API calls, but only when the direct comparison fails.
604
612
  <<find current module item if present>>=
605
613
  current_item = None
614
+ canonical_page_url = None
615
+ if item_type == 'Page':
616
+ try:
617
+ canonical_page_url = course.get_page(item_id).url
618
+ except Exception:
619
+ canonical_page_url = item_id
620
+
606
621
  try:
607
622
  for item in module.get_module_items():
608
623
  if not hasattr(item, 'type') or item.type != item_type:
@@ -625,10 +640,10 @@ Canvas has created a redirect from an old URL to a new one.
625
640
  if item.page_url == item_id:
626
641
  current_item = item
627
642
  break
628
- # URLs don't match directly; check if they resolve to same page
643
+ # URLs don't match directly; compare canonical URLs to avoid duplicates.
629
644
  try:
630
645
  resolved_page = course.get_page(item.page_url)
631
- if resolved_page.url == item_id:
646
+ if resolved_page.url == canonical_page_url:
632
647
  current_item = item
633
648
  break
634
649
  except Exception:
@@ -343,6 +343,7 @@ import canvaslms.cli
343
343
  import canvaslms.cli.courses as courses
344
344
  import canvaslms.cli.assignments as assignments
345
345
  import canvaslms.cli.content as content
346
+ import canvaslms.cli.modules as modules
346
347
  import canvaslms.cli.utils
347
348
  from rich.console import Console
348
349
  from rich.markdown import Markdown
@@ -2982,6 +2983,24 @@ NEW_QUIZ_RESULT_VIEW_SCHEMA = {
2982
2983
  \subsection{File formats for quiz editing}
2983
2984
  \label{sec:quiz-file-formats}
2984
2985
 
2986
+ \subsection{Updating module membership}
2987
+
2988
+ When a quiz is edited from a file, the front matter may include a [[modules]]
2989
+ field. We interpret this the same way as [[assignments edit]]:
2990
+ \begin{description}
2991
+ \item[Absent] Leave module membership unchanged.
2992
+ \item[Empty list] Remove the quiz from all modules.
2993
+ \item[List of regexes] Ensure the quiz appears in every matching module and is
2994
+ removed from non-matching modules.
2995
+ \end{description}
2996
+
2997
+ New Quizzes are implemented as assignments, and the module item type Canvas uses
2998
+ is [[Assignment]]. We therefore update membership using the quiz's assignment ID.
2999
+ Classic quizzes also appear as module items and can be targeted the same way.
3000
+
3001
+ We centralize this logic in a helper function so both file-based and interactive
3002
+ edit paths behave identically.
3003
+
2985
3004
  The [[quizzes edit]] command supports three file formats, auto-detected from
2986
3005
  the file extension and verified against the content:
2987
3006
  \begin{description}
@@ -3003,6 +3022,18 @@ characters of the file:
3003
3022
  \item Front matter format starts with [[---]]
3004
3023
  \end{itemize}
3005
3024
 
3025
+ <<functions>>=
3026
+ def update_quiz_module_membership(quiz, module_regexes):
3027
+ """Update module membership for a quiz based on module regex list"""
3028
+ item_id = int(quiz.id) if is_new_quiz(quiz) else quiz.id
3029
+ added, removed = modules.update_item_modules(
3030
+ quiz.course, 'Assignment', item_id, module_regexes)
3031
+ if added:
3032
+ print(f" Added to modules: {', '.join(added)}", file=sys.stderr)
3033
+ if removed:
3034
+ print(f" Removed from modules: {', '.join(removed)}", file=sys.stderr)
3035
+
3036
+
3006
3037
  <<functions>>=
3007
3038
  def detect_quiz_file_format(filepath):
3008
3039
  """Detect quiz file format from extension and content
@@ -3270,6 +3301,15 @@ If [[--replace-items]] is specified and the file contains items, we replace
3270
3301
  the quiz questions. But first we check for student submissions and ask for
3271
3302
  confirmation if any exist.
3272
3303
 
3304
+ If the file contains a [[modules]] key (front matter, YAML, or JSON), we also
3305
+ update the quiz's module membership after a successful edit. We interpret this
3306
+ field the same way as in [[assignments edit]]:
3307
+ \begin{description}
3308
+ \item[Absent] Module membership is not modified
3309
+ \item[Empty list] Quiz is removed from all modules
3310
+ \item[List of regexes] Quiz is placed in all modules matching the patterns
3311
+ \end{description}
3312
+
3273
3313
  <<handle item replacement if requested>>=
3274
3314
  items = quiz_data.get('items')
3275
3315
  if args.replace_items and items:
@@ -3294,6 +3334,7 @@ for quiz in quiz_list:
3294
3334
  success = apply_quiz_edit(quiz, settings, body, requester, args.html)
3295
3335
  if success:
3296
3336
  print(f"Updated quiz: {quiz.title}")
3337
+ <<update quiz module membership from file>>
3297
3338
  else:
3298
3339
  canvaslms.cli.warn(f"Failed to update quiz: {quiz.title}")
3299
3340
  @
@@ -3396,6 +3437,8 @@ def edit_quiz_interactive(quiz, requester, html_mode=False):
3396
3437
 
3397
3438
  # Apply the changes
3398
3439
  success = apply_quiz_edit(quiz, final_attrs, final_body, requester, html_mode)
3440
+ if success:
3441
+ <<update quiz module membership interactively>>
3399
3442
  return 'updated' if success else 'error'
3400
3443
  @
3401
3444
 
@@ -3415,6 +3458,11 @@ The workflow is:
3415
3458
  \item Apply the changes (settings only, unless [[--replace-items]])
3416
3459
  \end{enumerate}
3417
3460
 
3461
+ <<update quiz module membership interactively>>=
3462
+ if 'modules' in final_attrs:
3463
+ update_quiz_module_membership(quiz, final_attrs['modules'])
3464
+ @
3465
+
3418
3466
  <<functions>>=
3419
3467
  def edit_quiz_interactive_json(quiz, requester, html_mode=False,
3420
3468
  replace_items=False):
@@ -3680,6 +3728,11 @@ The [[replace_quiz_items]] function handles the complete process of replacing
3680
3728
  quiz items. It first checks for submissions, then deletes existing items,
3681
3729
  and finally creates the new items.
3682
3730
 
3731
+ <<update quiz module membership from file>>=
3732
+ if 'modules' in settings:
3733
+ update_quiz_module_membership(quiz, settings['modules'])
3734
+ @
3735
+
3683
3736
  <<functions>>=
3684
3737
  def replace_quiz_items(quiz, items, requester):
3685
3738
  """Replace all items in a quiz with new ones
@@ -4256,6 +4309,11 @@ The export command finds the quiz, extracts its settings, and exports all
4256
4309
  questions. We reuse the existing [[export_new_quiz_items]] and
4257
4310
  [[export_classic_questions]] functions for question export.
4258
4311
 
4312
+ If the quiz appears in modules, we also export a [[modules]] list. The
4313
+ [[quizzes edit]] command interprets this list as regex patterns, so we export
4314
+ module names as anchored, regex-escaped patterns (e.g., [[^FBF$]]) to mean
4315
+ \enquote{match this module name exactly}.
4316
+
4259
4317
  <<functions>>=
4260
4318
  def export_command(config, canvas, args):
4261
4319
  """Exports a complete quiz (settings + questions) to JSON"""
@@ -4305,6 +4363,11 @@ def export_full_new_quiz(quiz, requester, include_banks=True, importable=False):
4305
4363
  """
4306
4364
  # Extract basic settings
4307
4365
  settings = {
4366
+ 'modules': [
4367
+ "^" + re.escape(name) + "$"
4368
+ for name in modules.get_item_modules(
4369
+ quiz.course, 'Assignment', int(quiz.id))
4370
+ ],
4308
4371
  'title': getattr(quiz, 'title', ''),
4309
4372
  'instructions': getattr(quiz, 'instructions', '') or '',
4310
4373
  'time_limit': getattr(quiz, 'time_limit', None),
@@ -4354,6 +4417,11 @@ def export_full_classic_quiz(quiz, importable=False):
4354
4417
  """
4355
4418
  # Extract settings
4356
4419
  settings = {
4420
+ 'modules': [
4421
+ "^" + re.escape(name) + "$"
4422
+ for name in modules.get_item_modules(
4423
+ quiz.course, 'Assignment', quiz.id)
4424
+ ],
4357
4425
  'title': getattr(quiz, 'title', ''),
4358
4426
  'description': getattr(quiz, 'description', '') or '',
4359
4427
  'quiz_type': getattr(quiz, 'quiz_type', 'assignment'),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes