canvaslms 5.10__py3-none-any.whl → 6.1__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/__init__.py +13 -1
- canvaslms/cli/assignments.nw +151 -49
- canvaslms/cli/assignments.py +249 -84
- canvaslms/cli/cli.nw +16 -1
- canvaslms/cli/content.nw +290 -14
- canvaslms/cli/content.py +93 -8
- canvaslms/cli/pages.nw +130 -77
- canvaslms/cli/pages.py +86 -69
- canvaslms/cli/quizzes.nw +100 -14
- canvaslms/cli/quizzes.py +103 -13
- {canvaslms-5.10.dist-info → canvaslms-6.1.dist-info}/METADATA +1 -1
- {canvaslms-5.10.dist-info → canvaslms-6.1.dist-info}/RECORD +15 -15
- {canvaslms-5.10.dist-info → canvaslms-6.1.dist-info}/WHEEL +0 -0
- {canvaslms-5.10.dist-info → canvaslms-6.1.dist-info}/entry_points.txt +0 -0
- {canvaslms-5.10.dist-info → canvaslms-6.1.dist-info}/licenses/LICENSE +0 -0
canvaslms/cli/__init__.py
CHANGED
|
@@ -31,7 +31,14 @@ dirs = appdirs.AppDirs("canvaslms", "dbosk@kth.se")
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class EmptyListError(Exception):
|
|
34
|
-
"""Exception raised when a
|
|
34
|
+
"""Exception raised when a query returns no matching items"""
|
|
35
|
+
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MultipleMatchesError(Exception):
|
|
40
|
+
"""Exception raised when a query matches multiple items
|
|
41
|
+
but exactly one was expected"""
|
|
35
42
|
|
|
36
43
|
pass
|
|
37
44
|
|
|
@@ -221,3 +228,8 @@ def main():
|
|
|
221
228
|
err(1, str(e))
|
|
222
229
|
else:
|
|
223
230
|
sys.exit(1)
|
|
231
|
+
except MultipleMatchesError as e:
|
|
232
|
+
if args.quiet == 0:
|
|
233
|
+
err(1, str(e))
|
|
234
|
+
else:
|
|
235
|
+
sys.exit(1)
|
canvaslms/cli/assignments.nw
CHANGED
|
@@ -381,6 +381,8 @@ and pushed back without losing any formatting.
|
|
|
381
381
|
view_parser = assignments_subp.add_parser("view",
|
|
382
382
|
help="View assignment details",
|
|
383
383
|
description="Views detailed information about assignments. "
|
|
384
|
+
"When piped (not a TTY), outputs YAML front matter with a 'regex' field "
|
|
385
|
+
"for stable identification, followed by the description as Markdown. "
|
|
384
386
|
"Use --html to preserve HTML instead of converting to Markdown.")
|
|
385
387
|
view_parser.set_defaults(func=assignments_view_command)
|
|
386
388
|
add_assignment_option(view_parser)
|
|
@@ -418,16 +420,24 @@ edit_parser = assignments_subp.add_parser("edit",
|
|
|
418
420
|
description="Edit assignment content. Without -f, opens each matching "
|
|
419
421
|
"assignment in your editor for interactive editing with preview. "
|
|
420
422
|
"With -f, reads from a Markdown file with YAML front matter and updates "
|
|
421
|
-
"directly (script-friendly).
|
|
422
|
-
"
|
|
423
|
-
"
|
|
424
|
-
"
|
|
423
|
+
"directly (script-friendly). "
|
|
424
|
+
"Assignment matching workflow: "
|
|
425
|
+
"(1) If the YAML contains a 'regex' field, it matches assignments by "
|
|
426
|
+
"name or Canvas numeric ID. The regex is tested against both. "
|
|
427
|
+
"This provides stable identification: export an assignment with "
|
|
428
|
+
"'assignments view -a \"Lab 1.*\" > lab.md', edit locally, and push "
|
|
429
|
+
"back with 'assignments edit -f lab.md'---the regex stays constant "
|
|
430
|
+
"even if the name changes slightly. "
|
|
431
|
+
"(2) If no 'regex' is present but 'name' is, exact name matching is "
|
|
432
|
+
"used. "
|
|
433
|
+
"(3) Otherwise, falls back to -a, -A, and -M filters. "
|
|
434
|
+
"Use --html to read/edit HTML directly without Markdown conversion.")
|
|
425
435
|
edit_parser.set_defaults(func=assignments_edit_command)
|
|
426
436
|
add_assignment_option(edit_parser, ungraded=False, required=True)
|
|
427
437
|
canvaslms.cli.content.add_file_option(edit_parser)
|
|
428
438
|
edit_parser.add_argument("--create",
|
|
429
439
|
action="store_true",
|
|
430
|
-
help="Create a new assignment if
|
|
440
|
+
help="Create a new assignment if no match is found")
|
|
431
441
|
edit_parser.add_argument("--html",
|
|
432
442
|
action="store_true",
|
|
433
443
|
help="Read file as HTML instead of converting from Markdown. "
|
|
@@ -633,11 +643,14 @@ contain this assignment. When editing:
|
|
|
633
643
|
<<output assignment in editable format>>=
|
|
634
644
|
<<get current modules for assignment>>
|
|
635
645
|
<<get rubric for assignment>>
|
|
646
|
+
<<determine regex for assignment>>
|
|
636
647
|
output = canvaslms.cli.content.render_to_markdown(
|
|
637
648
|
assignment,
|
|
638
649
|
canvaslms.cli.content.ASSIGNMENT_SCHEMA,
|
|
639
650
|
content_attr='description',
|
|
640
|
-
extra_attributes={'modules': assignment_modules,
|
|
651
|
+
extra_attributes={'modules': assignment_modules,
|
|
652
|
+
'rubric': assignment_rubric,
|
|
653
|
+
'regex': assignment_regex})
|
|
641
654
|
print(output)
|
|
642
655
|
@
|
|
643
656
|
|
|
@@ -657,6 +670,19 @@ assignment_modules = modules.get_item_modules(
|
|
|
657
670
|
assignment.course, 'Assignment', assignment.id)
|
|
658
671
|
@
|
|
659
672
|
|
|
673
|
+
When determining the regex for export, we follow the same approach as for pages.
|
|
674
|
+
If the user specified a non-default [[-a]] pattern, we use that regex verbatim
|
|
675
|
+
(capturing their intent). Otherwise, we generate an exact-match regex from the
|
|
676
|
+
assignment name by escaping special characters and adding anchors.
|
|
677
|
+
<<determine regex for assignment>>=
|
|
678
|
+
if hasattr(args, 'assignment') and args.assignment not in (None, '.*'):
|
|
679
|
+
# User specified a -a filter, use it verbatim
|
|
680
|
+
assignment_regex = args.assignment
|
|
681
|
+
else:
|
|
682
|
+
# Generate exact-match regex from name
|
|
683
|
+
assignment_regex = '^' + re.escape(assignment.name) + '$'
|
|
684
|
+
@
|
|
685
|
+
|
|
660
686
|
\subsection{HTML output format}
|
|
661
687
|
|
|
662
688
|
When [[--html]] is specified, we preserve the original HTML content instead of
|
|
@@ -691,11 +717,14 @@ and pushed back using [[assignments edit --html -f]].
|
|
|
691
717
|
<<output assignment in html format>>=
|
|
692
718
|
<<get current modules for assignment>>
|
|
693
719
|
<<get rubric for assignment>>
|
|
720
|
+
<<determine regex for assignment>>
|
|
694
721
|
output = canvaslms.cli.content.render_to_html(
|
|
695
722
|
assignment,
|
|
696
723
|
canvaslms.cli.content.ASSIGNMENT_SCHEMA,
|
|
697
724
|
content_attr='description',
|
|
698
|
-
extra_attributes={'modules': assignment_modules,
|
|
725
|
+
extra_attributes={'modules': assignment_modules,
|
|
726
|
+
'rubric': assignment_rubric,
|
|
727
|
+
'regex': assignment_regex})
|
|
699
728
|
print(output)
|
|
700
729
|
@
|
|
701
730
|
|
|
@@ -1032,69 +1061,91 @@ try:
|
|
|
1032
1061
|
# body_content is Markdown or HTML depending on args.html
|
|
1033
1062
|
attributes, body_content = canvaslms.cli.content.read_content_from_file(args.file)
|
|
1034
1063
|
except FileNotFoundError:
|
|
1035
|
-
|
|
1036
|
-
print(f"Error: File not found: {args.file}", file=sys.stderr)
|
|
1037
|
-
return
|
|
1064
|
+
canvaslms.cli.err(1, f"File not found: {args.file}")
|
|
1038
1065
|
except Exception as e:
|
|
1039
|
-
|
|
1040
|
-
print(f"Error reading file: {e}", file=sys.stderr)
|
|
1041
|
-
return
|
|
1066
|
+
canvaslms.cli.err(1, f"Error reading file: {e}")
|
|
1042
1067
|
|
|
1043
1068
|
errors = canvaslms.cli.content.validate_attributes(
|
|
1044
1069
|
attributes, canvaslms.cli.content.ASSIGNMENT_SCHEMA)
|
|
1045
1070
|
if errors:
|
|
1046
1071
|
for error in errors:
|
|
1047
|
-
|
|
1048
|
-
|
|
1072
|
+
logger.error(f"Validation error: {error}")
|
|
1073
|
+
canvaslms.cli.err(1, "File validation failed")
|
|
1049
1074
|
@
|
|
1050
1075
|
|
|
1051
1076
|
\subsection{Updating assignment with new content}
|
|
1052
1077
|
|
|
1053
1078
|
We convert the content to HTML for Canvas (unless [[--html]] is specified, in
|
|
1054
|
-
which case the content is already HTML).
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1079
|
+
which case the content is already HTML).
|
|
1080
|
+
|
|
1081
|
+
The matching strategy prioritizes reliability and safety:
|
|
1082
|
+
\begin{enumerate}
|
|
1083
|
+
\item If the YAML contains a [[regex]] field, we compile it and match against
|
|
1084
|
+
assignment names and Canvas numeric IDs. This provides stable identification
|
|
1085
|
+
even when names change, as long as the new name still matches the pattern.
|
|
1086
|
+
\item If no [[regex]] is in the YAML but a [[name]] is present, we use exact
|
|
1087
|
+
name matching. This is safe because it won't accidentally match multiple
|
|
1088
|
+
assignments.
|
|
1089
|
+
\item As a last resort (for backward compatibility), we fall back to
|
|
1090
|
+
filter-based matching using [[-a]], [[-A]], and [[-M]] options only if no
|
|
1091
|
+
name is specified.
|
|
1092
|
+
\end{enumerate}
|
|
1062
1093
|
<<update assignment with new content>>=
|
|
1063
1094
|
if args.html:
|
|
1064
1095
|
html_content = body_content # Already HTML, no conversion needed
|
|
1065
1096
|
else:
|
|
1066
1097
|
html_content = pypandoc.convert_text(body_content, 'html', format='md')
|
|
1067
1098
|
|
|
1068
|
-
if '
|
|
1069
|
-
<<update or create assignment by
|
|
1099
|
+
if 'regex' in attributes and attributes['regex']:
|
|
1100
|
+
<<update or create assignment by regex>>
|
|
1070
1101
|
else:
|
|
1071
1102
|
<<update assignments by filter matching>>
|
|
1072
1103
|
@
|
|
1073
1104
|
|
|
1074
|
-
\subsubsection{
|
|
1105
|
+
\subsubsection{Regex-based assignment identification}
|
|
1075
1106
|
|
|
1076
|
-
When the YAML contains
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1107
|
+
When the YAML contains a [[regex]] field, we use [[match_item_by_regex]] from
|
|
1108
|
+
the content module to find the matching assignment. The function handles the
|
|
1109
|
+
three-way dispatch (one match, zero matches, multiple matches) and raises
|
|
1110
|
+
[[EmptyListError]] or [[MultipleMatchesError]] as appropriate.
|
|
1111
|
+
|
|
1112
|
+
We match the regex against both assignment names and Canvas numeric IDs,
|
|
1113
|
+
providing flexibility: an assignment can be identified by a pattern in its name
|
|
1114
|
+
(\enquote{Lab 1.*}) or by its numeric ID (\verb|^12345$|).
|
|
1115
|
+
|
|
1116
|
+
If exactly one assignment matches, we update it. If no assignments match and
|
|
1117
|
+
[[--create]] is specified, we create a new one. If multiple assignments match,
|
|
1118
|
+
the exception message lists the matching names so the user can make the regex
|
|
1119
|
+
more specific.
|
|
1120
|
+
<<update or create assignment by regex>>=
|
|
1121
|
+
regex_pattern = attributes['regex']
|
|
1081
1122
|
course_list = courses.process_course_option(canvas, args)
|
|
1082
1123
|
if not course_list:
|
|
1083
|
-
|
|
1084
|
-
return
|
|
1124
|
+
canvaslms.cli.err(1, "No courses found matching criteria")
|
|
1085
1125
|
|
|
1086
1126
|
course = course_list[0]
|
|
1127
|
+
|
|
1128
|
+
all_assignments = list(course.get_assignments())
|
|
1129
|
+
for a in all_assignments:
|
|
1130
|
+
a.course = course
|
|
1131
|
+
|
|
1087
1132
|
try:
|
|
1088
|
-
assignment =
|
|
1089
|
-
|
|
1133
|
+
assignment = canvaslms.cli.content.match_item_by_regex(
|
|
1134
|
+
all_assignments, regex_pattern,
|
|
1135
|
+
[lambda a: a.name, lambda a: str(a.id)],
|
|
1136
|
+
item_type="assignment",
|
|
1137
|
+
name_func=lambda a: a.name)
|
|
1090
1138
|
<<update existing assignment>>
|
|
1091
|
-
except
|
|
1139
|
+
except re.error as e:
|
|
1140
|
+
canvaslms.cli.err(
|
|
1141
|
+
1, f"Invalid regex pattern '{regex_pattern}': {e}")
|
|
1142
|
+
except canvaslms.cli.EmptyListError:
|
|
1092
1143
|
if args.create:
|
|
1093
1144
|
<<create new assignment>>
|
|
1094
1145
|
else:
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1146
|
+
canvaslms.cli.err(1,
|
|
1147
|
+
f"No assignments match regex '{regex_pattern}'. "
|
|
1148
|
+
f"Use --create to create a new assignment.")
|
|
1098
1149
|
@
|
|
1099
1150
|
|
|
1100
1151
|
When updating an existing assignment, we build the update dictionary and call
|
|
@@ -1115,8 +1166,8 @@ try:
|
|
|
1115
1166
|
logger.info(f"Updated assignment: {assignment.name}")
|
|
1116
1167
|
<<update assignment module membership>>
|
|
1117
1168
|
except Exception as e:
|
|
1118
|
-
|
|
1119
|
-
|
|
1169
|
+
canvaslms.cli.err(1,
|
|
1170
|
+
f"Error updating assignment '{assignment.name}': {e}")
|
|
1120
1171
|
@
|
|
1121
1172
|
|
|
1122
1173
|
\subsubsection{Creating new assignments}
|
|
@@ -1142,11 +1193,11 @@ if 'published' in attributes:
|
|
|
1142
1193
|
try:
|
|
1143
1194
|
new_assignment = course.create_assignment(assignment=create_params)
|
|
1144
1195
|
new_assignment.course = course
|
|
1145
|
-
|
|
1196
|
+
logger.info(f"Created assignment: {new_assignment.name} "
|
|
1197
|
+
f"(id: {new_assignment.id})")
|
|
1146
1198
|
<<update new assignment module membership>>
|
|
1147
1199
|
except Exception as e:
|
|
1148
|
-
|
|
1149
|
-
print(f"Error creating assignment: {e}", file=sys.stderr)
|
|
1200
|
+
canvaslms.cli.err(1, f"Error creating assignment: {e}")
|
|
1150
1201
|
@
|
|
1151
1202
|
|
|
1152
1203
|
For newly created assignments, we also process module membership if specified.
|
|
@@ -1161,14 +1212,65 @@ if 'modules' in attributes:
|
|
|
1161
1212
|
|
|
1162
1213
|
\subsubsection{Filter-based assignment matching}
|
|
1163
1214
|
|
|
1164
|
-
When no
|
|
1165
|
-
|
|
1166
|
-
|
|
1215
|
+
When no [[regex]] is specified in the YAML, we check whether a [[name]] is
|
|
1216
|
+
present. If so, we use exact name matching to find the assignment---this is
|
|
1217
|
+
critical for safety. Without it, the command would fall back to filter-based
|
|
1218
|
+
matching with the default [[-a '.*']] pattern, which matches \emph{all}
|
|
1219
|
+
assignments in the course. The result would be catastrophic: every assignment
|
|
1220
|
+
would be overwritten with the content from the file.
|
|
1221
|
+
|
|
1222
|
+
If no [[name]] is present either, we fall back to filter-based matching using
|
|
1223
|
+
the [[-a]], [[-A]], and [[-M]] options (the original behavior). To prevent
|
|
1224
|
+
accidental mass-updates, we refuse to proceed if the filter matches more than
|
|
1225
|
+
one assignment.
|
|
1167
1226
|
<<update assignments by filter matching>>=
|
|
1168
|
-
|
|
1227
|
+
if 'name' in attributes and attributes['name']:
|
|
1228
|
+
<<update or create assignment by name>>
|
|
1229
|
+
else:
|
|
1230
|
+
assignment_list = process_assignment_option(canvas, args)
|
|
1231
|
+
if len(assignment_list) > 1:
|
|
1232
|
+
canvaslms.cli.err(1,
|
|
1233
|
+
f"{len(assignment_list)} assignments match filter. "
|
|
1234
|
+
f"Add 'name' or 'regex' to YAML, or use -a to narrow selection.")
|
|
1235
|
+
for assignment in assignment_list:
|
|
1236
|
+
<<update existing assignment>>
|
|
1237
|
+
@
|
|
1169
1238
|
|
|
1170
|
-
|
|
1239
|
+
\subsubsection{Name-based assignment identification}
|
|
1240
|
+
|
|
1241
|
+
When no [[regex]] is specified in the YAML but a [[name]] is present, we use
|
|
1242
|
+
[[match_item_by_name]] from the content module for exact name matching. This
|
|
1243
|
+
avoids the catastrophic scenario where the default [[-a '.*']] pattern matches
|
|
1244
|
+
all assignments and overwrites them all with the file content.
|
|
1245
|
+
|
|
1246
|
+
If exactly one assignment matches, we update it. If none match and [[--create]]
|
|
1247
|
+
is specified, we create a new assignment. If multiple assignments share the same
|
|
1248
|
+
name, the exception tells the user to add a [[regex]] field to disambiguate.
|
|
1249
|
+
<<update or create assignment by name>>=
|
|
1250
|
+
name = attributes['name']
|
|
1251
|
+
course_list = courses.process_course_option(canvas, args)
|
|
1252
|
+
if not course_list:
|
|
1253
|
+
canvaslms.cli.err(1, "No courses found matching criteria")
|
|
1254
|
+
|
|
1255
|
+
course = course_list[0]
|
|
1256
|
+
|
|
1257
|
+
all_assignments = list(course.get_assignments())
|
|
1258
|
+
for a in all_assignments:
|
|
1259
|
+
a.course = course
|
|
1260
|
+
|
|
1261
|
+
try:
|
|
1262
|
+
assignment = canvaslms.cli.content.match_item_by_name(
|
|
1263
|
+
all_assignments, name,
|
|
1264
|
+
lambda a: a.name,
|
|
1265
|
+
item_type="assignment")
|
|
1171
1266
|
<<update existing assignment>>
|
|
1267
|
+
except canvaslms.cli.EmptyListError:
|
|
1268
|
+
if args.create:
|
|
1269
|
+
<<create new assignment>>
|
|
1270
|
+
else:
|
|
1271
|
+
canvaslms.cli.err(1,
|
|
1272
|
+
f"No assignments match name '{name}'. "
|
|
1273
|
+
f"Use --create to create a new assignment.")
|
|
1172
1274
|
@
|
|
1173
1275
|
|
|
1174
1276
|
We add optional attributes only if they are present in the YAML front matter.
|