canvaslms 5.5__py3-none-any.whl → 5.7__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.
canvaslms/cli/login.py CHANGED
@@ -16,8 +16,7 @@ def login_command(config, canvas, args):
16
16
  )
17
17
  hostname = input("Canvas hostname: ")
18
18
 
19
- print(
20
- f"""
19
+ print(f"""
21
20
  Open
22
21
 
23
22
  https://{hostname}/profile/settings
@@ -25,8 +24,7 @@ Open
25
24
  in your browser. Scroll down to approved integrations and click the
26
25
  '+ New access token' button. Fill in the required data and click the
27
26
  'Generate token' button. Enter the token here.
28
- """
29
- )
27
+ """)
30
28
 
31
29
  token = input("Canvas token: ")
32
30
 
canvaslms/cli/modules.nw CHANGED
@@ -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:
canvaslms/cli/modules.py CHANGED
@@ -198,6 +198,14 @@ def get_item_modules(course, item_type, item_id):
198
198
  Returns:
199
199
  List of module names containing this item
200
200
  """
201
+ target_url = None
202
+ if item_type == "Page":
203
+ try:
204
+ target_url = course.get_page(item_id).url
205
+ except Exception:
206
+ # If we can't resolve the target page, fall back to raw comparisons.
207
+ target_url = item_id
208
+
201
209
  modules = []
202
210
  for module in course.get_modules():
203
211
  try:
@@ -213,10 +221,10 @@ def get_item_modules(course, item_type, item_id):
213
221
  if item.page_url == item_id:
214
222
  modules.append(module.name)
215
223
  break
216
- # Check if URLs resolve to same page (handles redirects)
224
+ # Compare canonical URLs to avoid duplicates when Canvas redirects old slugs.
217
225
  try:
218
226
  resolved_page = course.get_page(item.page_url)
219
- if resolved_page.url == item_id:
227
+ if resolved_page.url == target_url:
220
228
  modules.append(module.name)
221
229
  break
222
230
  except Exception:
@@ -254,6 +262,13 @@ def update_item_modules(course, item_type, item_id, module_regexes):
254
262
 
255
263
  for module in all_modules:
256
264
  current_item = None
265
+ canonical_page_url = None
266
+ if item_type == "Page":
267
+ try:
268
+ canonical_page_url = course.get_page(item_id).url
269
+ except Exception:
270
+ canonical_page_url = item_id
271
+
257
272
  try:
258
273
  for item in module.get_module_items():
259
274
  if not hasattr(item, "type") or item.type != item_type:
@@ -267,10 +282,10 @@ def update_item_modules(course, item_type, item_id, module_regexes):
267
282
  if item.page_url == item_id:
268
283
  current_item = item
269
284
  break
270
- # URLs don't match directly; check if they resolve to same page
285
+ # URLs don't match directly; compare canonical URLs to avoid duplicates.
271
286
  try:
272
287
  resolved_page = course.get_page(item.page_url)
273
- if resolved_page.url == item_id:
288
+ if resolved_page.url == canonical_page_url:
274
289
  current_item = item
275
290
  break
276
291
  except Exception:
canvaslms/cli/quizzes.nw CHANGED
@@ -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'),
canvaslms/cli/quizzes.py CHANGED
@@ -17,6 +17,7 @@ import canvaslms.cli
17
17
  import canvaslms.cli.courses as courses
18
18
  import canvaslms.cli.assignments as assignments
19
19
  import canvaslms.cli.content as content
20
+ import canvaslms.cli.modules as modules
20
21
  import canvaslms.cli.utils
21
22
  from rich.console import Console
22
23
  from rich.markdown import Markdown
@@ -2227,6 +2228,18 @@ def create_classic_quiz(course, quiz_params):
2227
2228
  return None
2228
2229
 
2229
2230
 
2231
+ def update_quiz_module_membership(quiz, module_regexes):
2232
+ """Update module membership for a quiz based on module regex list"""
2233
+ item_id = int(quiz.id) if is_new_quiz(quiz) else quiz.id
2234
+ added, removed = modules.update_item_modules(
2235
+ quiz.course, "Assignment", item_id, module_regexes
2236
+ )
2237
+ if added:
2238
+ print(f" Added to modules: {', '.join(added)}", file=sys.stderr)
2239
+ if removed:
2240
+ print(f" Removed from modules: {', '.join(removed)}", file=sys.stderr)
2241
+
2242
+
2230
2243
  def detect_quiz_file_format(filepath):
2231
2244
  """Detect quiz file format from extension and content
2232
2245
 
@@ -2414,6 +2427,8 @@ def edit_command(config, canvas, args):
2414
2427
  success = apply_quiz_edit(quiz, settings, body, requester, args.html)
2415
2428
  if success:
2416
2429
  print(f"Updated quiz: {quiz.title}")
2430
+ if "modules" in settings:
2431
+ update_quiz_module_membership(quiz, settings["modules"])
2417
2432
  else:
2418
2433
  canvaslms.cli.warn(f"Failed to update quiz: {quiz.title}")
2419
2434
  else:
@@ -2494,6 +2509,9 @@ def edit_quiz_interactive(quiz, requester, html_mode=False):
2494
2509
 
2495
2510
  # Apply the changes
2496
2511
  success = apply_quiz_edit(quiz, final_attrs, final_body, requester, html_mode)
2512
+ if success:
2513
+ if "modules" in final_attrs:
2514
+ update_quiz_module_membership(quiz, final_attrs["modules"])
2497
2515
  return "updated" if success else "error"
2498
2516
 
2499
2517
 
@@ -3176,6 +3194,12 @@ def export_full_new_quiz(quiz, requester, include_banks=True, importable=False):
3176
3194
  """
3177
3195
  # Extract basic settings
3178
3196
  settings = {
3197
+ "modules": [
3198
+ "^" + re.escape(name) + "$"
3199
+ for name in modules.get_item_modules(
3200
+ quiz.course, "Assignment", int(quiz.id)
3201
+ )
3202
+ ],
3179
3203
  "title": getattr(quiz, "title", ""),
3180
3204
  "instructions": getattr(quiz, "instructions", "") or "",
3181
3205
  "time_limit": getattr(quiz, "time_limit", None),
@@ -3214,6 +3238,10 @@ def export_full_classic_quiz(quiz, importable=False):
3214
3238
  """
3215
3239
  # Extract settings
3216
3240
  settings = {
3241
+ "modules": [
3242
+ "^" + re.escape(name) + "$"
3243
+ for name in modules.get_item_modules(quiz.course, "Assignment", quiz.id)
3244
+ ],
3217
3245
  "title": getattr(quiz, "title", ""),
3218
3246
  "description": getattr(quiz, "description", "") or "",
3219
3247
  "quiz_type": getattr(quiz, "quiz_type", "assignment"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvaslms
3
- Version: 5.5
3
+ Version: 5.7
4
4
  Summary: Command-line interface to Canvas LMS
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -28,6 +28,7 @@ Requires-Dist: llm (>=0.16) ; (python_version >= "3.9") and (extra == "llm")
28
28
  Requires-Dist: llm-anthropic (>=0.19,<0.20) ; (python_version >= "3.9") and (extra == "llm")
29
29
  Requires-Dist: llm-azure (>=1.1,<2.0) ; (python_version >= "3.9") and (extra == "llm")
30
30
  Requires-Dist: llm-gemini (>=0.26.1,<0.27.0) ; (python_version >= "3.9") and (extra == "llm")
31
+ Requires-Dist: llm-github-copilot (>=0.3.1,<1.0.0) ; (python_version >= "3.9") and (extra == "llm")
31
32
  Requires-Dist: llm-openai-plugin (>=0.7,<0.8) ; (python_version >= "3.9") and (extra == "llm")
32
33
  Requires-Dist: llm-python (>=0.1,<0.2) ; (python_version >= "3.9") and (extra == "llm")
33
34
  Requires-Dist: patiencediff (>=0.2.18,<0.3.0) ; (python_version >= "3.9") and (extra == "diff")
@@ -19,13 +19,13 @@ canvaslms/cli/discussions.py,sha256=R6GbFbHYXCa0vyDf4eegGp74Pj6WGC38MEb6kY0PkWY,
19
19
  canvaslms/cli/grade.nw,sha256=ms7sBiGRPbK0CJLKxxYx_CDY9LMWgKxUuf2M3FYepMs,2197
20
20
  canvaslms/cli/grade.py,sha256=b7YkFIz64oXzcV2FcptpYJphevuCU3cdx9CilZHcG_A,662
21
21
  canvaslms/cli/login.nw,sha256=93LyHO_LXL1WdEvMg3OLhWulgkdoO8pfjYZVLwUbX4I,4419
22
- canvaslms/cli/login.py,sha256=GSO_wIT4TvCOwxaNW0VfUqjrkzsHvnImuSH4SdCu92M,2751
23
- canvaslms/cli/modules.nw,sha256=VZkb9u9VGmXoCyJpPyByFrOanqbKSLnIRPxODSfldxo,23689
24
- canvaslms/cli/modules.py,sha256=7pnKU6ex6zH4OC2SxLMn4uzc0cDl3pAVFcR9WJi317c,12426
22
+ canvaslms/cli/login.py,sha256=wbA5Q9fTsW1J-vraRcdq2kG4h_LFtvH_MTEay6h8GcE,2737
23
+ canvaslms/cli/modules.nw,sha256=rvrZe4AU_ok955GZ_ZwwFNFIHIKzft1LR9zTVLW_gic,24122
24
+ canvaslms/cli/modules.py,sha256=8fZpW_x7eUMUgcn8JrvbGuwtrmHsbVdNvqfcRgfF4x8,12953
25
25
  canvaslms/cli/pages.nw,sha256=njm6oQT22ryI8Z7O62d-qivQjbMX_je5OwhgHEvtVUg,28934
26
26
  canvaslms/cli/pages.py,sha256=lW8DpMeyQQoVcu7ztSj_PdW-oUJC0HSh5mDeo8BuaRc,27713
27
- canvaslms/cli/quizzes.nw,sha256=DKgyhekjxFrFVX0VpMT8FhNTYNmB15NGUeI8UG7seKg,260250
28
- canvaslms/cli/quizzes.py,sha256=Tuz1ZKZWhL_rkpFuldHDcbWFkoAvRN8UIjVXv3bDwXs,197100
27
+ canvaslms/cli/quizzes.nw,sha256=xgivQGColOzayvMn3OcxJf0V7E2K2lzindzPeBqymBw,262940
28
+ canvaslms/cli/quizzes.py,sha256=1gX16Mo49rO4j98xy_j7xSuIs0E9DcOJigB90imBNR4,198225
29
29
  canvaslms/cli/results.nw,sha256=T-ry1k_cHCH_nJfvPl4d9UBbIl_SxvXjBMxyYfXgyaw,22503
30
30
  canvaslms/cli/results.py,sha256=8ODAhC4r1ndyTHnWSSwFEeV4ab_snxTi1nkrsqqwRLg,15033
31
31
  canvaslms/cli/results.py.broken,sha256=njHu8mKfPHqH4daxy-4LMpO6FdUBLPHiVKFFmyH8aJQ,13047
@@ -57,8 +57,8 @@ canvaslms/hacks/attachment_cache.py,sha256=LcOZqaa6jPrEJWUD-JYN5GTc3bxCbv2fr_vqu
57
57
  canvaslms/hacks/canvasapi.nw,sha256=ixmIHn4tgy-ZKtQ1rqWSw97hfY2m0qtGX0de2x89lwA,136470
58
58
  canvaslms/hacks/canvasapi.py,sha256=A-r48x7gO6143_QkuZ8n6EW66i-a2AXXr7X7oeehOAU,31868
59
59
  canvaslms/hacks/test_hacks.py,sha256=JSJNvZqHu1E_s51HsPD7yr1gC-R-xVe-tuMMAKU9Gj8,66709
60
- canvaslms-5.5.dist-info/METADATA,sha256=_EawniDKbGV32ywHCpRIx8N0N8WzrBxjHNeuQztO_QQ,5978
61
- canvaslms-5.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
62
- canvaslms-5.5.dist-info/entry_points.txt,sha256=lyblfkLbodN5yb7q1c6-rwIoJPV-ygXrB9PYb5boHXM,48
63
- canvaslms-5.5.dist-info/licenses/LICENSE,sha256=N_TKsbzzD5Ax5fWJqEQk9bkwtf394MJkNeFld4HV6-E,1074
64
- canvaslms-5.5.dist-info/RECORD,,
60
+ canvaslms-5.7.dist-info/METADATA,sha256=39VhZz1uFN7Bm1W7GU2Yyxt6RI_wh3u5dd1t3kweD_w,6078
61
+ canvaslms-5.7.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
62
+ canvaslms-5.7.dist-info/entry_points.txt,sha256=lyblfkLbodN5yb7q1c6-rwIoJPV-ygXrB9PYb5boHXM,48
63
+ canvaslms-5.7.dist-info/licenses/LICENSE,sha256=N_TKsbzzD5Ax5fWJqEQk9bkwtf394MJkNeFld4HV6-E,1074
64
+ canvaslms-5.7.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 2.3.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any