canvaslms 5.1__py3-none-any.whl → 5.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.
canvaslms/cli/modules.nw CHANGED
@@ -501,16 +501,27 @@ def get_item_modules(course, item_type, item_id):
501
501
 
502
502
  The matching logic differs by item type.
503
503
  For assignments, Canvas stores the assignment ID in [[content_id]].
504
- For pages, Canvas stores the URL slug in [[page_url]].
504
+ For pages, Canvas stores the URL slug in [[page_url]]. However, Canvas may
505
+ redirect old URLs to new ones, so we need to resolve the URL before comparing
506
+ (see [[<<check if page item matches by resolving url>>]]).
505
507
  <<check if item matches and add module to list>>=
506
508
  if item_type == 'Assignment':
507
509
  if hasattr(item, 'content_id') and item.content_id == item_id:
508
510
  modules.append(module.name)
509
511
  break
510
512
  elif item_type == 'Page':
511
- if hasattr(item, 'page_url') and item.page_url == item_id:
512
- modules.append(module.name)
513
- break
513
+ if hasattr(item, 'page_url'):
514
+ if item.page_url == item_id:
515
+ modules.append(module.name)
516
+ break
517
+ # Check if URLs resolve to same page (handles redirects)
518
+ try:
519
+ resolved_page = course.get_page(item.page_url)
520
+ if resolved_page.url == item_id:
521
+ modules.append(module.name)
522
+ break
523
+ except Exception:
524
+ pass
514
525
  @
515
526
 
516
527
  \subsection{Updating module membership based on patterns}
@@ -575,6 +586,21 @@ for module in all_modules:
575
586
  @
576
587
 
577
588
  We search the module's items to find if our item is currently present.
589
+
590
+ For pages, we need to handle URL redirects. Canvas may store an old URL in the
591
+ module item that redirects to the canonical URL we're checking against. For
592
+ example, a page might have been added to a module with URL
593
+ \enquote{page-on-campus+zoom}, but Canvas later changed the canonical URL to
594
+ \enquote{page-on-zoom+campus}. The module item still stores the old URL, but
595
+ when we fetch the page using [[course.get_page()]], Canvas returns the page
596
+ with its new canonical URL.
597
+
598
+ Without handling this, the direct comparison [[item.page_url == item_id]] would
599
+ fail, causing the page to be added to the module again (creating duplicates).
600
+
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.
578
604
  <<find current module item if present>>=
579
605
  current_item = None
580
606
  try:
@@ -586,13 +612,29 @@ try:
586
612
  current_item = item
587
613
  break
588
614
  elif item_type == 'Page':
589
- if hasattr(item, 'page_url') and item.page_url == item_id:
590
- current_item = item
591
- break
615
+ if hasattr(item, 'page_url'):
616
+ <<check if page item matches by resolving url>>
592
617
  except Exception:
593
618
  pass
594
619
  @
595
620
 
621
+ When comparing page URLs, we first try a direct match. If that fails, we fetch
622
+ the page to get its canonical URL and compare that. This handles the case where
623
+ Canvas has created a redirect from an old URL to a new one.
624
+ <<check if page item matches by resolving url>>=
625
+ if item.page_url == item_id:
626
+ current_item = item
627
+ break
628
+ # URLs don't match directly; check if they resolve to same page
629
+ try:
630
+ resolved_page = course.get_page(item.page_url)
631
+ if resolved_page.url == item_id:
632
+ current_item = item
633
+ break
634
+ except Exception:
635
+ pass # Page doesn't exist or can't be fetched
636
+ @
637
+
596
638
  Finally, we add or remove based on comparing current vs desired state.
597
639
  When adding, we use [[create_module_item]] with the appropriate type and ID.
598
640
  When removing, we call [[delete()]] on the module item object.
canvaslms/cli/modules.py CHANGED
@@ -209,9 +209,18 @@ def get_item_modules(course, item_type, item_id):
209
209
  modules.append(module.name)
210
210
  break
211
211
  elif item_type == "Page":
212
- if hasattr(item, "page_url") and item.page_url == item_id:
213
- modules.append(module.name)
214
- break
212
+ if hasattr(item, "page_url"):
213
+ if item.page_url == item_id:
214
+ modules.append(module.name)
215
+ break
216
+ # Check if URLs resolve to same page (handles redirects)
217
+ try:
218
+ resolved_page = course.get_page(item.page_url)
219
+ if resolved_page.url == item_id:
220
+ modules.append(module.name)
221
+ break
222
+ except Exception:
223
+ pass
215
224
  except Exception:
216
225
  # Skip modules we can't access
217
226
  pass
@@ -254,9 +263,18 @@ def update_item_modules(course, item_type, item_id, module_regexes):
254
263
  current_item = item
255
264
  break
256
265
  elif item_type == "Page":
257
- if hasattr(item, "page_url") and item.page_url == item_id:
258
- current_item = item
259
- break
266
+ if hasattr(item, "page_url"):
267
+ if item.page_url == item_id:
268
+ current_item = item
269
+ break
270
+ # URLs don't match directly; check if they resolve to same page
271
+ try:
272
+ resolved_page = course.get_page(item.page_url)
273
+ if resolved_page.url == item_id:
274
+ current_item = item
275
+ break
276
+ except Exception:
277
+ pass # Page doesn't exist or can't be fetched
260
278
  except Exception:
261
279
  pass
262
280
  should_be_in = module.id in matching_module_ids
canvaslms/cli/pages.nw CHANGED
@@ -403,8 +403,9 @@ specific page to update. This enables a Git-based workflow where the page can
403
403
  be reliably identified even if the title changes. If the URL is not found and
404
404
  [[--create]] is specified, we create a new page instead.
405
405
 
406
- If no [[url]] is in the YAML, we fall back to the filter-based matching using
407
- [[-p]] and [[-M]] options (the existing behavior).
406
+ If no [[url]] is in the YAML, we try title-based matching first (using the
407
+ [[title]] field from the front matter), then fall back to filter-based matching
408
+ using [[-p]] and [[-M]] options only if no title is specified.
408
409
  <<update pages with new content>>=
409
410
  if args.html:
410
411
  html_content = body_content # Already HTML, no conversion needed
@@ -515,18 +516,70 @@ if 'modules' in attributes:
515
516
  print(f" Added to modules: {', '.join(added)}", file=sys.stderr)
516
517
  @
517
518
 
518
- \subsubsection{Filter-based page matching}
519
+ \subsubsection{Title-based page identification}
519
520
 
520
- When no URL is specified in the YAML, we use the filter options ([[-p]], [[-M]])
521
- to find matching pages. This maintains backwards compatibility with the original
522
- behavior.
523
- <<update pages by filter matching>>=
524
- pages = process_page_option(canvas, args)
521
+ When no [[url]] is specified in the YAML but a [[title]] is present, we use the
522
+ title to find the page. This is the most common case when creating or updating
523
+ pages from Markdown files: the user specifies a title in the front matter and
524
+ expects the command to find or create a page with that title.
525
525
 
526
- for page in pages:
527
- full_page = page.course.get_page(page.url)
528
- full_page.course = page.course
526
+ This behavior is critical for safety. Without it, the command would fall back to
527
+ filter-based matching with the default [[-p '.*']] pattern, which matches
528
+ \emph{all} pages in the course. The result would be catastrophic: every page
529
+ would be overwritten with the content from the file.
530
+
531
+ We use exact title matching (not regex) to avoid accidentally updating multiple
532
+ pages. If exactly one page matches, we update it. If no pages match and
533
+ [[--create]] is specified, we create a new page. If multiple pages match, we
534
+ report an error---the user should add a [[url]] field to disambiguate.
535
+ <<update or create page by title>>=
536
+ title = attributes['title']
537
+ course_list = canvaslms.cli.courses.process_course_option(canvas, args)
538
+ if not course_list:
539
+ print("Error: No courses found matching criteria", file=sys.stderr)
540
+ return
541
+
542
+ course = course_list[0]
543
+
544
+ matching_pages = [p for p in course.get_pages() if p.title == title]
545
+
546
+ if len(matching_pages) == 1:
547
+ full_page = course.get_page(matching_pages[0].url)
548
+ full_page.course = course
529
549
  <<update existing page>>
550
+ elif len(matching_pages) == 0:
551
+ if args.create:
552
+ <<create new page>>
553
+ else:
554
+ print(f"Error: Page '{title}' not found. "
555
+ f"Use --create to create a new page.", file=sys.stderr)
556
+ return
557
+ else:
558
+ print(f"Error: Multiple pages with title '{title}' found. "
559
+ f"Add 'url' field to YAML to identify specific page.", file=sys.stderr)
560
+ return
561
+ @
562
+
563
+ \subsubsection{Filter-based page matching}
564
+
565
+ When neither [[url]] nor [[title]] is specified in the YAML, we fall back to the
566
+ filter options ([[-p]], [[-M]]) to find matching pages. This is a rare case but
567
+ maintains backwards compatibility. We add a safety check: if multiple pages
568
+ match, we report an error rather than updating all of them.
569
+ <<update pages by filter matching>>=
570
+ if 'title' in attributes and attributes['title']:
571
+ <<update or create page by title>>
572
+ else:
573
+ pages = process_page_option(canvas, args)
574
+ if len(pages) > 1:
575
+ print(f"Error: {len(pages)} pages match filter. "
576
+ f"Add 'title' or 'url' to YAML, or use -p to narrow selection.",
577
+ file=sys.stderr)
578
+ return
579
+ for page in pages:
580
+ full_page = page.course.get_page(page.url)
581
+ full_page.course = page.course
582
+ <<update existing page>>
530
583
  @
531
584
 
532
585
  We add optional attributes only if they are present in the YAML front matter.
canvaslms/cli/pages.py CHANGED
@@ -296,53 +296,177 @@ def pages_edit_command(config, canvas, args):
296
296
  )
297
297
  return
298
298
  else:
299
- pages = process_page_option(canvas, args)
299
+ if "title" in attributes and attributes["title"]:
300
+ title = attributes["title"]
301
+ course_list = canvaslms.cli.courses.process_course_option(canvas, args)
302
+ if not course_list:
303
+ print("Error: No courses found matching criteria", file=sys.stderr)
304
+ return
300
305
 
301
- for page in pages:
302
- full_page = page.course.get_page(page.url)
303
- full_page.course = page.course
304
- update_data = {
305
- "wiki_page": {
306
- "title": attributes.get("title", full_page.title),
307
- "body": html_content,
306
+ course = course_list[0]
307
+
308
+ matching_pages = [p for p in course.get_pages() if p.title == title]
309
+
310
+ if len(matching_pages) == 1:
311
+ full_page = course.get_page(matching_pages[0].url)
312
+ full_page.course = course
313
+ update_data = {
314
+ "wiki_page": {
315
+ "title": attributes.get("title", full_page.title),
316
+ "body": html_content,
317
+ }
308
318
  }
309
- }
310
319
 
311
- if "published" in attributes:
312
- update_data["wiki_page"]["published"] = attributes["published"]
313
- if "front_page" in attributes:
314
- update_data["wiki_page"]["front_page"] = attributes["front_page"]
315
- if "editing_roles" in attributes and attributes["editing_roles"]:
316
- update_data["wiki_page"]["editing_roles"] = attributes[
317
- "editing_roles"
318
- ]
320
+ if "published" in attributes:
321
+ update_data["wiki_page"]["published"] = attributes["published"]
322
+ if "front_page" in attributes:
323
+ update_data["wiki_page"]["front_page"] = attributes[
324
+ "front_page"
325
+ ]
326
+ if "editing_roles" in attributes and attributes["editing_roles"]:
327
+ update_data["wiki_page"]["editing_roles"] = attributes[
328
+ "editing_roles"
329
+ ]
319
330
 
320
- try:
321
- full_page.edit(**update_data)
322
- full_page._fetched_at = datetime.now()
323
- if hasattr(full_page.course, "page_cache"):
324
- full_page.course.page_cache[full_page.url] = (full_page, {})
325
- logger.info(f"Updated page: {full_page.title}")
326
- if "modules" in attributes:
327
- module_regexes = attributes["modules"]
328
- added, removed = canvaslms.cli.modules.update_item_modules(
329
- full_page.course, "Page", full_page.url, module_regexes
330
- )
331
- if added:
332
- print(
333
- f" Added to modules: {', '.join(added)}",
334
- file=sys.stderr,
331
+ try:
332
+ full_page.edit(**update_data)
333
+ full_page._fetched_at = datetime.now()
334
+ if hasattr(full_page.course, "page_cache"):
335
+ full_page.course.page_cache[full_page.url] = (full_page, {})
336
+ logger.info(f"Updated page: {full_page.title}")
337
+ if "modules" in attributes:
338
+ module_regexes = attributes["modules"]
339
+ added, removed = canvaslms.cli.modules.update_item_modules(
340
+ full_page.course, "Page", full_page.url, module_regexes
335
341
  )
336
- if removed:
342
+ if added:
343
+ print(
344
+ f" Added to modules: {', '.join(added)}",
345
+ file=sys.stderr,
346
+ )
347
+ if removed:
348
+ print(
349
+ f" Removed from modules: {', '.join(removed)}",
350
+ file=sys.stderr,
351
+ )
352
+ except Exception as e:
353
+ logger.error(f"Error updating page '{full_page.title}': {e}")
354
+ print(
355
+ f"Error updating page '{full_page.title}': {e}",
356
+ file=sys.stderr,
357
+ )
358
+ elif len(matching_pages) == 0:
359
+ if args.create:
360
+ create_params = {
361
+ "title": attributes.get("title", "Untitled Page"),
362
+ "body": html_content,
363
+ }
364
+ if "published" in attributes:
365
+ create_params["published"] = attributes["published"]
366
+ if "front_page" in attributes:
367
+ create_params["front_page"] = attributes["front_page"]
368
+ if (
369
+ "editing_roles" in attributes
370
+ and attributes["editing_roles"]
371
+ ):
372
+ create_params["editing_roles"] = attributes["editing_roles"]
373
+
374
+ try:
375
+ new_page = course.create_page(wiki_page=create_params)
376
+ new_page.course = course
337
377
  print(
338
- f" Removed from modules: {', '.join(removed)}",
378
+ f"Created page: {new_page.title} (url: {new_page.url})",
339
379
  file=sys.stderr,
340
380
  )
341
- except Exception as e:
342
- logger.error(f"Error updating page '{full_page.title}': {e}")
381
+ if "modules" in attributes:
382
+ module_regexes = attributes["modules"]
383
+ added, removed = (
384
+ canvaslms.cli.modules.update_item_modules(
385
+ new_page.course,
386
+ "Page",
387
+ new_page.url,
388
+ module_regexes,
389
+ )
390
+ )
391
+ if added:
392
+ print(
393
+ f" Added to modules: {', '.join(added)}",
394
+ file=sys.stderr,
395
+ )
396
+ except Exception as e:
397
+ logger.error(f"Error creating page: {e}")
398
+ print(f"Error creating page: {e}", file=sys.stderr)
399
+ else:
400
+ print(
401
+ f"Error: Page '{title}' not found. "
402
+ f"Use --create to create a new page.",
403
+ file=sys.stderr,
404
+ )
405
+ return
406
+ else:
343
407
  print(
344
- f"Error updating page '{full_page.title}': {e}", file=sys.stderr
408
+ f"Error: Multiple pages with title '{title}' found. "
409
+ f"Add 'url' field to YAML to identify specific page.",
410
+ file=sys.stderr,
345
411
  )
412
+ return
413
+ else:
414
+ pages = process_page_option(canvas, args)
415
+ if len(pages) > 1:
416
+ print(
417
+ f"Error: {len(pages)} pages match filter. "
418
+ f"Add 'title' or 'url' to YAML, or use -p to narrow selection.",
419
+ file=sys.stderr,
420
+ )
421
+ return
422
+ for page in pages:
423
+ full_page = page.course.get_page(page.url)
424
+ full_page.course = page.course
425
+ update_data = {
426
+ "wiki_page": {
427
+ "title": attributes.get("title", full_page.title),
428
+ "body": html_content,
429
+ }
430
+ }
431
+
432
+ if "published" in attributes:
433
+ update_data["wiki_page"]["published"] = attributes["published"]
434
+ if "front_page" in attributes:
435
+ update_data["wiki_page"]["front_page"] = attributes[
436
+ "front_page"
437
+ ]
438
+ if "editing_roles" in attributes and attributes["editing_roles"]:
439
+ update_data["wiki_page"]["editing_roles"] = attributes[
440
+ "editing_roles"
441
+ ]
442
+
443
+ try:
444
+ full_page.edit(**update_data)
445
+ full_page._fetched_at = datetime.now()
446
+ if hasattr(full_page.course, "page_cache"):
447
+ full_page.course.page_cache[full_page.url] = (full_page, {})
448
+ logger.info(f"Updated page: {full_page.title}")
449
+ if "modules" in attributes:
450
+ module_regexes = attributes["modules"]
451
+ added, removed = canvaslms.cli.modules.update_item_modules(
452
+ full_page.course, "Page", full_page.url, module_regexes
453
+ )
454
+ if added:
455
+ print(
456
+ f" Added to modules: {', '.join(added)}",
457
+ file=sys.stderr,
458
+ )
459
+ if removed:
460
+ print(
461
+ f" Removed from modules: {', '.join(removed)}",
462
+ file=sys.stderr,
463
+ )
464
+ except Exception as e:
465
+ logger.error(f"Error updating page '{full_page.title}': {e}")
466
+ print(
467
+ f"Error updating page '{full_page.title}': {e}",
468
+ file=sys.stderr,
469
+ )
346
470
  else:
347
471
  try:
348
472
  page_list = process_page_option(canvas, args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvaslms
3
- Version: 5.1
3
+ Version: 5.3
4
4
  Summary: Command-line interface to Canvas LMS
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -20,10 +20,10 @@ 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
22
  canvaslms/cli/login.py,sha256=GSO_wIT4TvCOwxaNW0VfUqjrkzsHvnImuSH4SdCu92M,2751
23
- canvaslms/cli/modules.nw,sha256=Wuh1aFwZJDEUAEzzgALnc-YVXNdRgRI819zdV7oIB_E,21864
24
- canvaslms/cli/modules.py,sha256=n1MfeZfjPTKyFwgluHwBqW8PKHwCGktWzYA6nuFLeL0,11483
25
- canvaslms/cli/pages.nw,sha256=hRYLeEmwaDhnVcUq6Sx5JkKq-uJ4BJ7dOI4dOJ6bN9E,26662
26
- canvaslms/cli/pages.py,sha256=NxH6vgQr-LhIb9jgESD3J6sYCOv6S_4K7EhAm4uyRlI,21396
23
+ canvaslms/cli/modules.nw,sha256=VZkb9u9VGmXoCyJpPyByFrOanqbKSLnIRPxODSfldxo,23689
24
+ canvaslms/cli/modules.py,sha256=7pnKU6ex6zH4OC2SxLMn4uzc0cDl3pAVFcR9WJi317c,12426
25
+ canvaslms/cli/pages.nw,sha256=njm6oQT22ryI8Z7O62d-qivQjbMX_je5OwhgHEvtVUg,28934
26
+ canvaslms/cli/pages.py,sha256=lW8DpMeyQQoVcu7ztSj_PdW-oUJC0HSh5mDeo8BuaRc,27713
27
27
  canvaslms/cli/quizzes.nw,sha256=nO0lAJLpE03fwPTV8wCC0Zr65p7J5x2H7J0OU8KW5CA,203138
28
28
  canvaslms/cli/quizzes.py,sha256=gx-SPm0rI2XNVUl5Qm7s4UHvsvitwFyrNh35J3Lay9Y,152014
29
29
  canvaslms/cli/results.nw,sha256=T-ry1k_cHCH_nJfvPl4d9UBbIl_SxvXjBMxyYfXgyaw,22503
@@ -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.1.dist-info/METADATA,sha256=pg6Y0rHkQMWRUu_y1ATILFEL164PJhTvN3GYqHVv9ms,5978
61
- canvaslms-5.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
62
- canvaslms-5.1.dist-info/entry_points.txt,sha256=lyblfkLbodN5yb7q1c6-rwIoJPV-ygXrB9PYb5boHXM,48
63
- canvaslms-5.1.dist-info/licenses/LICENSE,sha256=N_TKsbzzD5Ax5fWJqEQk9bkwtf394MJkNeFld4HV6-E,1074
64
- canvaslms-5.1.dist-info/RECORD,,
60
+ canvaslms-5.3.dist-info/METADATA,sha256=rvvX57QhCQT6AtrYaPIaBZ5JJ1KDay6O-zOSd61f2SA,5978
61
+ canvaslms-5.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
62
+ canvaslms-5.3.dist-info/entry_points.txt,sha256=lyblfkLbodN5yb7q1c6-rwIoJPV-ygXrB9PYb5boHXM,48
63
+ canvaslms-5.3.dist-info/licenses/LICENSE,sha256=N_TKsbzzD5Ax5fWJqEQk9bkwtf394MJkNeFld4HV6-E,1074
64
+ canvaslms-5.3.dist-info/RECORD,,