canvaslms 6.0__tar.gz → 6.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 (113) hide show
  1. {canvaslms-6.0 → canvaslms-6.1}/PKG-INFO +1 -1
  2. {canvaslms-6.0 → canvaslms-6.1}/pyproject.toml +1 -1
  3. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/quizzes.nw +100 -14
  4. {canvaslms-6.0 → canvaslms-6.1}/LICENSE +0 -0
  5. {canvaslms-6.0 → canvaslms-6.1}/README.md +0 -0
  6. {canvaslms-6.0 → canvaslms-6.1}/makefiles/.circleci/config.yml +0 -0
  7. {canvaslms-6.0 → canvaslms-6.1}/makefiles/.git +0 -0
  8. {canvaslms-6.0 → canvaslms-6.1}/makefiles/.gitignore +0 -0
  9. {canvaslms-6.0 → canvaslms-6.1}/makefiles/CONTRIBUTING.md +0 -0
  10. {canvaslms-6.0 → canvaslms-6.1}/makefiles/Dockerfile +0 -0
  11. {canvaslms-6.0 → canvaslms-6.1}/makefiles/Dockerfile.nw +0 -0
  12. {canvaslms-6.0 → canvaslms-6.1}/makefiles/Dockerfile.tex +0 -0
  13. {canvaslms-6.0 → canvaslms-6.1}/makefiles/LICENSE +0 -0
  14. {canvaslms-6.0 → canvaslms-6.1}/makefiles/Makefile +0 -0
  15. {canvaslms-6.0 → canvaslms-6.1}/makefiles/Makefile.nw +0 -0
  16. {canvaslms-6.0 → canvaslms-6.1}/makefiles/Makefile.tex +0 -0
  17. {canvaslms-6.0 → canvaslms-6.1}/makefiles/README.md +0 -0
  18. {canvaslms-6.0 → canvaslms-6.1}/makefiles/doc.mk +0 -0
  19. {canvaslms-6.0 → canvaslms-6.1}/makefiles/doc.mk.nw +0 -0
  20. {canvaslms-6.0 → canvaslms-6.1}/makefiles/doc.tex +0 -0
  21. {canvaslms-6.0 → canvaslms-6.1}/makefiles/exam.bib +0 -0
  22. {canvaslms-6.0 → canvaslms-6.1}/makefiles/exam.mk +0 -0
  23. {canvaslms-6.0 → canvaslms-6.1}/makefiles/exam.mk.nw +0 -0
  24. {canvaslms-6.0 → canvaslms-6.1}/makefiles/exam.tex +0 -0
  25. {canvaslms-6.0 → canvaslms-6.1}/makefiles/gitattributes +0 -0
  26. {canvaslms-6.0 → canvaslms-6.1}/makefiles/haskell.mk +0 -0
  27. {canvaslms-6.0 → canvaslms-6.1}/makefiles/haskell.mk.nw +0 -0
  28. {canvaslms-6.0 → canvaslms-6.1}/makefiles/haskell.tex +0 -0
  29. {canvaslms-6.0 → canvaslms-6.1}/makefiles/intro.tex +0 -0
  30. {canvaslms-6.0 → canvaslms-6.1}/makefiles/latexmkrc +0 -0
  31. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/_minted/AAA0D43723DCC5BE9DB71A96B52C1142.highlight.minted +0 -0
  32. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/_minted/E5724293DA12A769F97F0E91498CAEEF.highlight.minted +0 -0
  33. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/_minted/FFC039B25D180E99FC9FEBDB2D42EBAF.highlight.minted +0 -0
  34. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/_minted/_7945F0071C510291A619CE1658F673CB.index.minted +0 -0
  35. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/_minted/default.style.minted +0 -0
  36. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.aux +0 -0
  37. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.bbl +0 -0
  38. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.bcf +0 -0
  39. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.blg +0 -0
  40. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.fdb_latexmk +0 -0
  41. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.fls +0 -0
  42. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.log +0 -0
  43. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.out +0 -0
  44. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.pdf +0 -0
  45. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.run.xml +0 -0
  46. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.toc +0 -0
  47. {canvaslms-6.0 → canvaslms-6.1}/makefiles/ltxobj/makefiles.xdv +0 -0
  48. {canvaslms-6.0 → canvaslms-6.1}/makefiles/makefiles.bib +0 -0
  49. {canvaslms-6.0 → canvaslms-6.1}/makefiles/makefiles.tex +0 -0
  50. {canvaslms-6.0 → canvaslms-6.1}/makefiles/miun.course.mk +0 -0
  51. {canvaslms-6.0 → canvaslms-6.1}/makefiles/miun.depend.mk +0 -0
  52. {canvaslms-6.0 → canvaslms-6.1}/makefiles/miun.docs.mk +0 -0
  53. {canvaslms-6.0 → canvaslms-6.1}/makefiles/miun.port.mk +0 -0
  54. {canvaslms-6.0 → canvaslms-6.1}/makefiles/miun.pub.mk +0 -0
  55. {canvaslms-6.0 → canvaslms-6.1}/makefiles/noweb.mk +0 -0
  56. {canvaslms-6.0 → canvaslms-6.1}/makefiles/noweb.mk.nw +0 -0
  57. {canvaslms-6.0 → canvaslms-6.1}/makefiles/noweb.tex +0 -0
  58. {canvaslms-6.0 → canvaslms-6.1}/makefiles/pkg.mk +0 -0
  59. {canvaslms-6.0 → canvaslms-6.1}/makefiles/pkg.mk.nw +0 -0
  60. {canvaslms-6.0 → canvaslms-6.1}/makefiles/pkg.tex +0 -0
  61. {canvaslms-6.0 → canvaslms-6.1}/makefiles/portability.mk +0 -0
  62. {canvaslms-6.0 → canvaslms-6.1}/makefiles/portability.mk.nw +0 -0
  63. {canvaslms-6.0 → canvaslms-6.1}/makefiles/portability.tex +0 -0
  64. {canvaslms-6.0 → canvaslms-6.1}/makefiles/preamble.tex +0 -0
  65. {canvaslms-6.0 → canvaslms-6.1}/makefiles/pub.mk +0 -0
  66. {canvaslms-6.0 → canvaslms-6.1}/makefiles/pub.mk.nw +0 -0
  67. {canvaslms-6.0 → canvaslms-6.1}/makefiles/pub.tex +0 -0
  68. {canvaslms-6.0 → canvaslms-6.1}/makefiles/results.mk +0 -0
  69. {canvaslms-6.0 → canvaslms-6.1}/makefiles/results.mk.nw +0 -0
  70. {canvaslms-6.0 → canvaslms-6.1}/makefiles/results.tex +0 -0
  71. {canvaslms-6.0 → canvaslms-6.1}/makefiles/subdir.mk +0 -0
  72. {canvaslms-6.0 → canvaslms-6.1}/makefiles/subdir.mk.nw +0 -0
  73. {canvaslms-6.0 → canvaslms-6.1}/makefiles/subdir.tex +0 -0
  74. {canvaslms-6.0 → canvaslms-6.1}/makefiles/tex.bib +0 -0
  75. {canvaslms-6.0 → canvaslms-6.1}/makefiles/tex.mk +0 -0
  76. {canvaslms-6.0 → canvaslms-6.1}/makefiles/tex.mk.nw +0 -0
  77. {canvaslms-6.0 → canvaslms-6.1}/makefiles/tex.tex +0 -0
  78. {canvaslms-6.0 → canvaslms-6.1}/makefiles/transform.bib +0 -0
  79. {canvaslms-6.0 → canvaslms-6.1}/makefiles/transform.mk +0 -0
  80. {canvaslms-6.0 → canvaslms-6.1}/makefiles/transform.mk.nw +0 -0
  81. {canvaslms-6.0 → canvaslms-6.1}/makefiles/transform.tex +0 -0
  82. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/Makefile +0 -0
  83. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/__init__.py +0 -0
  84. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/Makefile +0 -0
  85. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/assignments.nw +0 -0
  86. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/cache.nw +0 -0
  87. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/calendar.nw +0 -0
  88. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/cli.nw +0 -0
  89. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/content.nw +0 -0
  90. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/courses.nw +0 -0
  91. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/discussions.nw +0 -0
  92. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/grade.nw +0 -0
  93. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/login.nw +0 -0
  94. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/modules.nw +0 -0
  95. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/pages.nw +0 -0
  96. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/results.nw +0 -0
  97. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/results.py.broken +0 -0
  98. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/submissions.nw +0 -0
  99. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/users.nw +0 -0
  100. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/cli/utils.nw +0 -0
  101. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/Makefile +0 -0
  102. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/conjunctavg.nw +0 -0
  103. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/conjunctavgsurvey.nw +0 -0
  104. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/disjunctmax.nw +0 -0
  105. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/grades.nw +0 -0
  106. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/grades.py +0 -0
  107. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/maxgradesurvey.nw +0 -0
  108. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/participation.nw +0 -0
  109. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/grades/tilkryLAB1.nw +0 -0
  110. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/hacks/Makefile +0 -0
  111. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/hacks/__init__.py +0 -0
  112. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/hacks/attachment_cache.nw +0 -0
  113. {canvaslms-6.0 → canvaslms-6.1}/src/canvaslms/hacks/canvasapi.nw +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvaslms
3
- Version: 6.0
3
+ Version: 6.1
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 = "6.0"
3
+ version = "6.1"
4
4
  description = "Command-line interface to Canvas LMS"
5
5
  authors = [
6
6
  {name = "Daniel Bosk", email = "daniel@bosk.se"}
@@ -3204,6 +3204,11 @@ def _parse_quiz_full_format(data, format_type):
3204
3204
 
3205
3205
  if 'settings' in data:
3206
3206
  settings = data['settings'].copy()
3207
+ # Allow metadata (like id/regex) at the top level.
3208
+ # This makes the format more forgiving for hand-edited files.
3209
+ for k in ('id', 'regex', 'title'):
3210
+ if k in data and k not in settings:
3211
+ settings[k] = data[k]
3207
3212
  else:
3208
3213
  # Simple format: entire dict is settings (excluding metadata keys)
3209
3214
  settings = {k: v for k, v in data.items()
@@ -3252,6 +3257,13 @@ FILE MODE (-f):
3252
3257
  The JSON/YAML format is the same as 'quizzes export' output, enabling
3253
3258
  a round-trip workflow: export, modify, edit.
3254
3259
 
3260
+ When using -f, the -a/--assignment selector is optional if the file provides
3261
+ enough information to identify the quiz. We use the first available of:
3262
+ - settings.id (exact ID match)
3263
+ - settings.regex (regex matching title or ID)
3264
+ - settings.title (exact title match; errors if duplicate titles exist
3265
+ within a course)
3266
+
3255
3267
  ITEM HANDLING:
3256
3268
  By default, items/questions in the file are ignored to protect student
3257
3269
  submissions. Use --replace-items to replace all questions (with confirmation
@@ -3266,7 +3278,13 @@ try:
3266
3278
  except argparse.ArgumentError:
3267
3279
  pass
3268
3280
 
3269
- add_quiz_option(edit_parser, required=True)
3281
+ # NOTE: We intentionally do not call add_quiz_option(required=False) here.
3282
+ # That helper defaults to '.*', which would select all quizzes if -a is
3283
+ # omitted. For quizzes edit, -a is required only in interactive mode.
3284
+ edit_parser.add_argument("-a", "--assignment",
3285
+ required=False,
3286
+ default=None,
3287
+ help="Regex matching quiz title or Canvas ID")
3270
3288
 
3271
3289
  edit_parser.add_argument("-f", "--file",
3272
3290
  help="Read content from file (format auto-detected: .json, .yaml, .yml, .md)",
@@ -3299,12 +3317,14 @@ In file mode, we read the content from the specified file.
3299
3317
  <<functions>>=
3300
3318
  def edit_command(config, canvas, args):
3301
3319
  """Edits quiz settings and instructions"""
3302
- quiz_list = process_quiz_option(canvas, args)
3303
3320
  requester = canvas._Canvas__requester
3304
3321
 
3305
3322
  if args.file:
3306
3323
  <<edit quiz from file>>
3307
3324
  else:
3325
+ if not getattr(args, 'assignment', None):
3326
+ canvaslms.cli.err(1, "-a/--assignment is required unless -f/--file is used")
3327
+ quiz_list = process_quiz_option(canvas, args)
3308
3328
  <<edit quiz interactively>>
3309
3329
  @
3310
3330
 
@@ -3329,21 +3349,80 @@ except ValueError as e:
3329
3349
  <<apply settings from file>>
3330
3350
  @
3331
3351
 
3332
- We extract [[id]] from either the top level or the settings:
3352
+ We can identify the target quiz in two ways:
3353
+ \begin{description}
3354
+ \item[By command-line selector] If [[-a/--assignment]] is provided, we select
3355
+ quizzes using that regex (matching quiz title or ID), just like interactive
3356
+ mode.
3357
+ \item[By file metadata] If [[-a]] is omitted, the file must provide one of
3358
+ [[settings.id]], [[settings.regex]], or [[settings.title]]. This is the same
3359
+ idea as the [[regex]] field used by [[pages]] and [[assignments]]: we preserve
3360
+ the user's selection expression so scripted edits stay stable.
3361
+ \end{description}
3333
3362
 
3334
3363
  <<identify target quiz from file data>>=
3335
- settings = quiz_data['settings']
3336
- quiz_id = settings.get('id')
3364
+ settings = quiz_data.get('settings') or {}
3365
+ course_list = courses.process_course_option(canvas, args)
3366
+
3367
+ if getattr(args, 'assignment', None):
3368
+ quiz_list = list(filter_quizzes(course_list, args.assignment))
3369
+ if not quiz_list:
3370
+ canvaslms.cli.err(1, f"No quiz found matching: {args.assignment}")
3371
+ else:
3372
+ quiz_id = settings.get('id')
3373
+ quiz_regex = settings.get('regex')
3374
+ quiz_title = settings.get('title')
3375
+
3376
+ if quiz_id is not None and str(quiz_id).strip() != "":
3377
+ quiz_list = []
3378
+ for course in course_list:
3379
+ for quiz in fetch_all_quizzes(course):
3380
+ if str(quiz.id) == str(quiz_id):
3381
+ quiz.course = course
3382
+ quiz_list.append(quiz)
3383
+ if not quiz_list:
3384
+ canvaslms.cli.err(1, f"Quiz with ID {quiz_id} not found")
3385
+ if len(quiz_list) > 1:
3386
+ canvaslms.cli.err(1, f"Multiple quizzes match ID {quiz_id}")
3387
+ elif quiz_regex is not None and str(quiz_regex).strip() != "":
3388
+ quiz_list = list(filter_quizzes(course_list, str(quiz_regex)))
3389
+ if not quiz_list:
3390
+ canvaslms.cli.err(1, f"No quiz found matching file regex: {quiz_regex}")
3391
+ elif quiz_title is not None and str(quiz_title).strip() != "":
3392
+ quiz_list = []
3393
+ title = str(quiz_title)
3394
+ for course in course_list:
3395
+ matches = [q for q in fetch_all_quizzes(course)
3396
+ if getattr(q, 'title', None) == title]
3397
+ for q in matches:
3398
+ q.course = course
3399
+ if len(matches) > 1:
3400
+ canvaslms.cli.err(
3401
+ 1,
3402
+ f"Multiple quizzes titled '{title}' in course {course.course_code}. "
3403
+ "Use settings.regex (or -a) to disambiguate."
3404
+ )
3405
+ if matches:
3406
+ quiz_list.extend(matches)
3407
+
3408
+ if not quiz_list:
3409
+ canvaslms.cli.err(1, f"No quiz found with title: {title}")
3410
+ else:
3411
+ canvaslms.cli.err(
3412
+ 1,
3413
+ "File mode without -a requires one of settings.id, settings.regex, "
3414
+ "or settings.title"
3415
+ )
3337
3416
 
3338
- if quiz_id:
3339
- target_quiz = None
3417
+ if len(quiz_list) > 1:
3418
+ print(f"Found {len(quiz_list)} quizzes to update:")
3340
3419
  for quiz in quiz_list:
3341
- if str(quiz.id) == str(quiz_id):
3342
- target_quiz = quiz
3343
- break
3344
- if not target_quiz:
3345
- canvaslms.cli.err(1, f"Quiz with ID {quiz_id} not found")
3346
- quiz_list = [target_quiz]
3420
+ course_code = quiz.course.course_code if hasattr(quiz, 'course') else "?"
3421
+ print(f" - {course_code}: {quiz.title} (ID: {quiz.id})")
3422
+ response = input("\nEdit all? [y/N] ").strip().lower()
3423
+ if response != 'y':
3424
+ print("Aborted.")
3425
+ return
3347
3426
  @
3348
3427
 
3349
3428
  If [[--replace-items]] is specified and the file contains items, we replace
@@ -4056,7 +4135,7 @@ def quiz_attributes_to_api_params(attributes, is_new, html_body):
4056
4135
  params = {}
4057
4136
 
4058
4137
  for key, value in attributes.items():
4059
- if key == 'id':
4138
+ if key in ('id', 'regex', 'modules'):
4060
4139
  # Don't send ID as an update parameter
4061
4140
  continue
4062
4141
 
@@ -4374,6 +4453,10 @@ def export_command(config, canvas, args):
4374
4453
  if not quiz_list:
4375
4454
  canvaslms.cli.err(1, f"No quiz found matching: {args.assignment}")
4376
4455
 
4456
+ if len(quiz_list) > 1:
4457
+ canvaslms.cli.err(1, f"Multiple quizzes match '{args.assignment}'. "
4458
+ "Please use a more specific pattern.")
4459
+
4377
4460
  quiz = quiz_list[0]
4378
4461
  requester = canvas._Canvas__requester
4379
4462
  include_banks = args.include_banks and not args.no_banks
@@ -4385,6 +4468,9 @@ def export_command(config, canvas, args):
4385
4468
  else:
4386
4469
  export = export_full_classic_quiz(quiz, importable)
4387
4470
 
4471
+ # Preserve the user's selection expression for round-trip syncing.
4472
+ export.setdefault('settings', {})['regex'] = args.assignment
4473
+
4388
4474
  # Output as JSON
4389
4475
  print(json.dumps(export, indent=2, ensure_ascii=False))
4390
4476
  @
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